'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

const JAVASCRIPT_OUTPUT_NAME = 'javascript';
const CSS_OUTPUT_NAME = 'css';
const TEMPLATE_OUTPUT_NAME = 'template';

// Tag names
const JAVASCRIPT_TAG = 'script';
const STYLE_TAG = 'style';
const TEXTAREA_TAG = 'textarea';

// Boolean attributes
const IS_RAW = 'isRaw';
const IS_SELF_CLOSING = 'isSelfClosing';
const IS_VOID = 'isVoid';
const IS_BOOLEAN = 'isBoolean';
const IS_CUSTOM = 'isCustom';
const IS_SPREAD = 'isSpread';

var c = /*#__PURE__*/Object.freeze({
  JAVASCRIPT_OUTPUT_NAME: JAVASCRIPT_OUTPUT_NAME,
  CSS_OUTPUT_NAME: CSS_OUTPUT_NAME,
  TEMPLATE_OUTPUT_NAME: TEMPLATE_OUTPUT_NAME,
  JAVASCRIPT_TAG: JAVASCRIPT_TAG,
  STYLE_TAG: STYLE_TAG,
  TEXTAREA_TAG: TEXTAREA_TAG,
  IS_RAW: IS_RAW,
  IS_SELF_CLOSING: IS_SELF_CLOSING,
  IS_VOID: IS_VOID,
  IS_BOOLEAN: IS_BOOLEAN,
  IS_CUSTOM: IS_CUSTOM,
  IS_SPREAD: IS_SPREAD
});

/**
 * Not all the types are handled in this module.
 *
 * @enum {number}
 * @readonly
 */
const TAG = 1; /* TAG */
const ATTR = 2; /* ATTR */
const TEXT = 3; /* TEXT */
const CDATA = 4; /* CDATA */
const COMMENT = 8; /* COMMENT */
const DOCUMENT = 9; /* DOCUMENT */
const DOCTYPE = 10; /* DOCTYPE */
const DOCUMENT_FRAGMENT = 11; /* DOCUMENT_FRAGMENT */

var types = /*#__PURE__*/Object.freeze({
  TAG: TAG,
  ATTR: ATTR,
  TEXT: TEXT,
  CDATA: CDATA,
  COMMENT: COMMENT,
  DOCUMENT: DOCUMENT,
  DOCTYPE: DOCTYPE,
  DOCUMENT_FRAGMENT: DOCUMENT_FRAGMENT
});

const rootTagNotFound = 'Root tag not found.';
const unclosedTemplateLiteral = 'Unclosed ES6 template literal.';
const unexpectedEndOfFile = 'Unexpected end of file.';
const unclosedComment = 'Unclosed comment.';
const unclosedNamedBlock = 'Unclosed "%1" block.';
const duplicatedNamedTag = 'Duplicate tag "<%1>".';
const unexpectedCharInExpression = 'Unexpected character %1.';
const unclosedExpression = 'Unclosed expression.';

/**
 * Matches the start of valid tags names; used with the first 2 chars after the `'<'`.
 * @const
 * @private
 */
const TAG_2C = /^(?:\/[a-zA-Z]|[a-zA-Z][^\s>/]?)/;
/**
 * Matches valid tags names AFTER the validation with `TAG_2C`.
 * $1: tag name including any `'/'`, $2: non self-closing brace (`>`) w/o attributes.
 * @const
 * @private
 */
const TAG_NAME = /(\/?[^\s>/]+)\s*(>)?/g;
/**
 * Matches an attribute name-value pair (both can be empty).
 * $1: attribute name, $2: value including any quotes.
 * @const
 * @private
 */
const ATTR_START = /(\S[^>/=\s]*)(?:\s*=\s*([^>/])?)?/g;

/**
 * Matches the spread operator
 * it will be used for the spread attributes
 * @type {RegExp}
 */
const SPREAD_OPERATOR = /\.\.\./;
/**
 * Matches the closing tag of a `script` and `style` block.
 * Used by parseText fo find the end of the block.
 * @const
 * @private
 */
const RE_SCRYLE = {
  script: /<\/script\s*>/gi,
  style: /<\/style\s*>/gi,
  textarea: /<\/textarea\s*>/gi
};

// Do not touch text content inside this tags
const RAW_TAGS = /^\/?(?:pre|textarea)$/;

/**
 * Add an item into a collection, if the collection is not an array
 * we create one and add the item to it
 * @param   {Array} collection - target collection
 * @param   {*} item - item to add to the collection
 * @returns {Array} array containing the new item added to it
 */
function addToCollection(collection = [], item) {
  collection.push(item);
  return collection
}

/**
 * Run RegExp.exec starting from a specific position
 * @param   {RegExp} re - regex
 * @param   {number} pos - last index position
 * @param   {string} string - regex target
 * @returns {Array} regex result
 */
function execFromPos(re, pos, string) {
  re.lastIndex = pos;
  return re.exec(string)
}

/**
 * Escape special characters in a given string, in preparation to create a regex.
 *
 * @param   {string} str - Raw string
 * @returns {string} Escaped string.
 */
var escapeStr = (str) => str.replace(/(?=[-[\](){^*+?.$|\\])/g, '\\');

function formatError(data, message, pos) {
  if (!pos) {
    pos = data.length;
  }
  // count unix/mac/win eols
  const line = (data.slice(0, pos).match(/\r\n?|\n/g) || '').length + 1;
  let col = 0;
  while (--pos >= 0 && !/[\r\n]/.test(data[pos])) {
    ++col;
  }
  return `[${line},${col}]: ${message}`
}

const $_ES6_BQ = '`';

/**
 * Searches the next backquote that signals the end of the ES6 Template Literal
 * or the "${" sequence that starts a JS expression, skipping any escaped
 * character.
 *
 * @param   {string}    code  - Whole code
 * @param   {number}    pos   - The start position of the template
 * @param   {string[]}  stack - To save nested ES6 TL count
 * @returns {number}    The end of the string (-1 if not found)
 */
function skipES6TL(code, pos, stack) {
  // we are in the char following the backquote (`),
  // find the next unescaped backquote or the sequence "${"
  const re = /[`$\\]/g;
  let c;
  while (re.lastIndex = pos, re.exec(code)) {
    pos = re.lastIndex;
    c = code[pos - 1];
    if (c === '`') {
      return pos
    }
    if (c === '$' && code[pos++] === '{') {
      stack.push($_ES6_BQ, '}');
      return pos
    }
    // else this is an escaped char
  }
  throw formatError(code, unclosedTemplateLiteral, pos)
}

/**
 * Custom error handler can be implemented replacing this method.
 * The `state` object includes the buffer (`data`)
 * The error position (`loc`) contains line (base 1) and col (base 0).
 * @param {string} data - string containing the error
 * @param {string} msg - Error message
 * @param {number} pos - Position of the error
 * @returns {undefined} throw an exception error
 */
function panic(data, msg, pos) {
  const message = formatError(data, msg, pos);
  throw new Error(message)
}

// forked from https://github.com/aMarCruz/skip-regex

// safe characters to precced a regex (including `=>`, `**`, and `...`)
const beforeReChars = '[{(,;:?=|&!^~>%*/';
const beforeReSign = `${beforeReChars}+-`;

// keyword that can preceed a regex (`in` is handled as special case)
const beforeReWords = [
  'case',
  'default',
  'do',
  'else',
  'in',
  'instanceof',
  'prefix',
  'return',
  'typeof',
  'void',
  'yield'
];

// Last chars of all the beforeReWords elements to speed up the process.
const wordsEndChar = beforeReWords.reduce((s, w) => s + w.slice(-1), '');

// Matches literal regex from the start of the buffer.
// The buffer to search must not include line-endings.
const RE_LIT_REGEX = /^\/(?=[^*>/])[^[/\\]*(?:(?:\\.|\[(?:\\.|[^\]\\]*)*\])[^[\\/]*)*?\/[gimuy]*/;

// Valid characters for JavaScript variable names and literal numbers.
const RE_JS_VCHAR = /[$\w]/;

// Match dot characters that could be part of tricky regex
const RE_DOT_CHAR = /.*/g;

/**
 * Searches the position of the previous non-blank character inside `code`,
 * starting with `pos - 1`.
 *
 * @param   {string} code - Buffer to search
 * @param   {number} pos  - Starting position
 * @returns {number} Position of the first non-blank character to the left.
 * @private
 */
function _prev(code, pos) {
  while (--pos >= 0 && /\s/.test(code[pos]));
  return pos
}



/**
 * Check if the character in the `start` position within `code` can be a regex
 * and returns the position following this regex or `start+1` if this is not
 * one.
 *
 * NOTE: Ensure `start` points to a slash (this is not checked).
 *
 * @function skipRegex
 * @param   {string} code  - Buffer to test in
 * @param   {number} start - Position the first slash inside `code`
 * @returns {number} Position of the char following the regex.
 *
 */
/* istanbul ignore next */
function skipRegex(code, start) {
  let pos = RE_DOT_CHAR.lastIndex = start++;

  // `exec()` will extract from the slash to the end of the line
  //   and the chained `match()` will match the possible regex.
  const match = (RE_DOT_CHAR.exec(code) || ' ')[0].match(RE_LIT_REGEX);

  if (match) {
    const next = pos + match[0].length;      // result comes from `re.match`

    pos = _prev(code, pos);
    let c = code[pos];

    // start of buffer or safe prefix?
    if (pos < 0 || beforeReChars.includes(c)) {
      return next
    }

    // from here, `pos` is >= 0 and `c` is code[pos]
    if (c === '.') {
      // can be `...` or something silly like 5./2
      if (code[pos - 1] === '.') {
        start = next;
      }

    } else {

      if (c === '+' || c === '-') {
        // tricky case
        if (code[--pos] !== c ||            // if have a single operator or
             (pos = _prev(code, pos)) < 0 ||  // ...have `++` and no previous token
             beforeReSign.includes(c = code[pos])) {
          return next                       // ...this is a regex
        }
      }

      if (wordsEndChar.includes(c)) {  // looks like a keyword?
        const end = pos + 1;

        // get the complete (previous) keyword
        while (--pos >= 0 && RE_JS_VCHAR.test(code[pos]));

        // it is in the allowed keywords list?
        if (beforeReWords.includes(code.slice(pos + 1, end))) {
          start = next;
        }
      }
    }
  }

  return start
}

/*
 * Mini-parser for expressions.
 * The main pourpose of this module is to find the end of an expression
 * and return its text without the enclosing brackets.
 * Does not works with comments, but supports ES6 template strings.
 */
/**
 * @exports exprExtr
 */
const S_SQ_STR = /'[^'\n\r\\]*(?:\\(?:\r\n?|[\S\s])[^'\n\r\\]*)*'/.source;
/**
 * Matches double quoted JS strings taking care about nested quotes
 * and EOLs (escaped EOLs are Ok).
 *
 * @const
 * @private
 */
const S_STRING = `${S_SQ_STR}|${S_SQ_STR.replace(/'/g, '"')}`;
/**
 * Regex cache
 *
 * @type {Object.<string, RegExp>}
 * @const
 * @private
 */
const reBr = {};
/**
 * Makes an optimal regex that matches quoted strings, brackets, backquotes
 * and the closing brackets of an expression.
 *
 * @param   {string} b - Closing brackets
 * @returns {RegExp} - optimized regex
 */
function _regex(b) {
  let re = reBr[b];
  if (!re) {
    let s = escapeStr(b);
    if (b.length > 1) {
      s = `${s}|[`;
    } else {
      s = /[{}[\]()]/.test(b) ? '[' : `[${s}`;
    }
    reBr[b] = re = new RegExp(`${S_STRING}|${s}\`/\\{}[\\]()]`, 'g');
  }
  return re
}

/**
 * Update the scopes stack removing or adding closures to it
 * @param   {Array} stack - array stacking the expression closures
 * @param   {string} char - current char to add or remove from the stack
 * @param   {string} idx  - matching index
 * @param   {string} code - expression code
 * @returns {Object} result
 * @returns {Object} result.char - either the char received or the closing braces
 * @returns {Object} result.index - either a new index to skip part of the source code,
 *                                  or 0 to keep from parsing from the old position
 */
function updateStack(stack, char, idx, code) {
  let index = 0;

  switch (char) {
  case '[':
  case '(':
  case '{':
    stack.push(char === '[' ? ']' : char === '(' ? ')' : '}');
    break
  case ')':
  case ']':
  case '}':
    if (char !== stack.pop()) {
      panic(code, unexpectedCharInExpression.replace('%1', char), index);
    }

    if (char === '}' && stack[stack.length - 1] === $_ES6_BQ) {
      char = stack.pop();
    }

    index = idx + 1;
    break
  case '/':
    index = skipRegex(code, idx);
  }

  return { char, index }
}

/**
   * Parses the code string searching the end of the expression.
   * It skips braces, quoted strings, regexes, and ES6 template literals.
   *
   * @function exprExtr
   * @param   {string}  code  - Buffer to parse
   * @param   {number}  start - Position of the opening brace
   * @param   {[string,string]} bp - Brackets pair
   * @returns {Object} Expression's end (after the closing brace) or -1
   *                            if it is not an expr.
   */
function exprExtr(code, start, bp) {
  const [openingBraces, closingBraces] = bp;
  const offset = start + openingBraces.length; // skips the opening brace
  const stack = []; // expected closing braces ('`' for ES6 TL)
  const re = _regex(closingBraces);

  re.lastIndex = offset; // begining of the expression

  let end;
  let match;

  while (match = re.exec(code)) { // eslint-disable-line
    const idx = match.index;
    const str = match[0];
    end = re.lastIndex;

    // end the iteration
    if (str === closingBraces && !stack.length) {
      return {
        text: code.slice(offset, idx),
        start,
        end
      }
    }

    const { char, index } = updateStack(stack, str[0], idx, code);
    // update the end value depending on the new index received
    end = index || end;
    // update the regex last index
    re.lastIndex = char === $_ES6_BQ ? skipES6TL(code, end, stack) : end;
  }

  if (stack.length) {
    panic(code, unclosedExpression, end);
  }
}

/**
 * Outputs the last parsed node. Can be used with a builder too.
 *
 * @param   {ParserStore} store - Parsing store
 * @returns {undefined} void function
 * @private
 */
function flush(store) {
  const last = store.last;
  store.last = null;
  if (last && store.root) {
    store.builder.push(last);
  }
}

/**
 * Get the code chunks from start and end range
 * @param   {string}  source  - source code
 * @param   {number}  start   - Start position of the chunk we want to extract
 * @param   {number}  end     - Ending position of the chunk we need
 * @returns {string}  chunk of code extracted from the source code received
 * @private
 */
function getChunk(source, start, end) {
  return source.slice(start, end)
}

/**
 * states text in the last text node, or creates a new one if needed.
 *
 * @param {ParserState}   state   - Current parser state
 * @param {number}  start   - Start position of the tag
 * @param {number}  end     - Ending position (last char of the tag)
 * @param {Object}  extra   - extra properties to add to the text node
 * @param {RawExpr[]} extra.expressions  - Found expressions
 * @param {string}    extra.unescape     - Brackets to unescape
 * @returns {undefined} - void function
 * @private
 */
function pushText(state, start, end, extra = {}) {
  const text = getChunk(state.data, start, end);
  const expressions = extra.expressions;
  const unescape = extra.unescape;

  let q = state.last;
  state.pos = end;

  if (q && q.type === TEXT) {
    q.text += text;
    q.end = end;
  } else {
    flush(state);
    state.last = q = { type: TEXT, text, start, end };
  }

  if (expressions && expressions.length) {
    q.expressions = (q.expressions || []).concat(expressions);
  }

  if (unescape) {
    q.unescape = unescape;
  }

  return TEXT
}

/**
 * Find the end of the attribute value or text node
 * Extract expressions.
 * Detect if value have escaped brackets.
 *
 * @param   {ParserState} state  - Parser state
 * @param   {HasExpr} node       - Node if attr, info if text
 * @param   {string} endingChars - Ends the value or text
 * @param   {number} start       - Starting position
 * @returns {number} Ending position
 * @private
 */
function expr(state, node, endingChars, start) {
  const re = b0re(state, endingChars);

  re.lastIndex = start; // reset re position

  const { unescape, expressions, end } = parseExpressions(state, re);

  if (node) {
    if (unescape) {
      node.unescape = unescape;
    }
    if (expressions.length) {
      node.expressions = expressions;
    }
  } else {
    pushText(state, start, end, {expressions, unescape});
  }

  return end
}

/**
 * Parse a text chunk finding all the expressions in it
 * @param   {ParserState} state  - Parser state
 * @param   {RegExp} re - regex to match the expressions contents
 * @returns {Object} result containing the expression found, the string to unescape and the end position
 */
function parseExpressions(state, re) {
  const { data, options } = state;
  const { brackets } = options;
  const expressions = [];
  let unescape, pos, match;

  // Anything captured in $1 (closing quote or character) ends the loop...
  while ((match = re.exec(data)) && !match[1]) {
    // ...else, we have an opening bracket and maybe an expression.
    pos = match.index;
    if (data[pos - 1] === '\\') {
      unescape = match[0]; // it is an escaped opening brace
    } else {
      const tmpExpr = exprExtr(data, pos, brackets);
      if (tmpExpr) {
        expressions.push(tmpExpr);
        re.lastIndex = tmpExpr.end;
      }
    }
  }

  // Even for text, the parser needs match a closing char
  if (!match) {
    panic(data, unexpectedEndOfFile, pos);
  }

  return {
    unescape,
    expressions,
    end: match.index
  }
}



/**
 * Creates a regex for the given string and the left bracket.
 * The string is captured in $1.
 *
 * @param   {ParserState} state  - Parser state
 * @param   {string} str - String to search
 * @returns {RegExp} Resulting regex.
 * @private
 */
function b0re(state, str) {
  const { brackets } = state.options;
  const re = state.regexCache[str];

  if (re) return re

  const b0 = escapeStr(brackets[0]);
  // cache the regex extending the regexCache object
  Object.assign(state.regexCache, { [str]: new RegExp(`(${str})|${b0}`, 'g') });

  return state.regexCache[str]
}

// similar to _.uniq
const uniq = l => l.filter((x, i, a) => a.indexOf(x) === i);

/**
 * SVG void elements that cannot be auto-closed and shouldn't contain child nodes.
 * @const {Array}
 */
const VOID_SVG_TAGS_LIST = [
  'circle',
  'ellipse',
  'line',
  'path',
  'polygon',
  'polyline',
  'rect',
  'stop',
  'use'
];

/**
 * List of html elements where the value attribute is allowed
 * @type {Array}
 */
const HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_LIST = [
  'button',
  'data',
  'input',
  'select',
  'li',
  'meter',
  'option',
  'output',
  'progress',
  'textarea',
  'param'
];

/**
 * List of all the available svg tags
 * @const {Array}
 * @see {@link https://github.com/wooorm/svg-tag-names}
 */
const SVG_TAGS_LIST = uniq([
  'a',
  'altGlyph',
  'altGlyphDef',
  'altGlyphItem',
  'animate',
  'animateColor',
  'animateMotion',
  'animateTransform',
  'animation',
  'audio',
  'canvas',
  'clipPath',
  'color-profile',
  'cursor',
  'defs',
  'desc',
  'discard',
  'feBlend',
  'feColorMatrix',
  'feComponentTransfer',
  'feComposite',
  'feConvolveMatrix',
  'feDiffuseLighting',
  'feDisplacementMap',
  'feDistantLight',
  'feDropShadow',
  'feFlood',
  'feFuncA',
  'feFuncB',
  'feFuncG',
  'feFuncR',
  'feGaussianBlur',
  'feImage',
  'feMerge',
  'feMergeNode',
  'feMorphology',
  'feOffset',
  'fePointLight',
  'feSpecularLighting',
  'feSpotLight',
  'feTile',
  'feTurbulence',
  'filter',
  'font',
  'font-face',
  'font-face-format',
  'font-face-name',
  'font-face-src',
  'font-face-uri',
  'foreignObject',
  'g',
  'glyph',
  'glyphRef',
  'handler',
  'hatch',
  'hatchpath',
  'hkern',
  'iframe',
  'image',
  'linearGradient',
  'listener',
  'marker',
  'mask',
  'mesh',
  'meshgradient',
  'meshpatch',
  'meshrow',
  'metadata',
  'missing-glyph',
  'mpath',
  'pattern',
  'prefetch',
  'radialGradient',
  'script',
  'set',
  'solidColor',
  'solidcolor',
  'style',
  'svg',
  'switch',
  'symbol',
  'tbreak',
  'text',
  'textArea',
  'textPath',
  'title',
  'tref',
  'tspan',
  'unknown',
  'video',
  'view',
  'vkern'
].concat(VOID_SVG_TAGS_LIST)).sort();

/**
 * HTML void elements that cannot be auto-closed and shouldn't contain child nodes.
 * @type {Array}
 * @see   {@link http://www.w3.org/TR/html-markup/syntax.html#syntax-elements}
 * @see   {@link http://www.w3.org/TR/html5/syntax.html#void-elements}
 */
const VOID_HTML_TAGS_LIST = [
  'area',
  'base',
  'br',
  'col',
  'embed',
  'hr',
  'img',
  'input',
  'keygen',
  'link',
  'menuitem',
  'meta',
  'param',
  'source',
  'track',
  'wbr'
];

/**
 * List of all the html tags
 * @const {Array}
 * @see {@link https://github.com/sindresorhus/html-tags}
 */
const HTML_TAGS_LIST = uniq([
  'a',
  'abbr',
  'address',
  'article',
  'aside',
  'audio',
  'b',
  'bdi',
  'bdo',
  'blockquote',
  'body',
  'canvas',
  'caption',
  'cite',
  'code',
  'colgroup',
  'datalist',
  'dd',
  'del',
  'details',
  'dfn',
  'dialog',
  'div',
  'dl',
  'dt',
  'em',
  'fieldset',
  'figcaption',
  'figure',
  'footer',
  'form',
  'h1',
  'h2',
  'h3',
  'h4',
  'h5',
  'h6',
  'head',
  'header',
  'hgroup',
  'html',
  'i',
  'iframe',
  'ins',
  'kbd',
  'label',
  'legend',
  'main',
  'map',
  'mark',
  'math',
  'menu',
  'nav',
  'noscript',
  'object',
  'ol',
  'optgroup',
  'p',
  'picture',
  'pre',
  'q',
  'rb',
  'rp',
  'rt',
  'rtc',
  'ruby',
  's',
  'samp',
  'script',
  'section',
  'select',
  'slot',
  'small',
  'span',
  'strong',
  'style',
  'sub',
  'summary',
  'sup',
  'svg',
  'table',
  'tbody',
  'td',
  'template',
  'tfoot',
  'th',
  'thead',
  'time',
  'title',
  'tr',
  'u',
  'ul',
  'var',
  'video'
]
  .concat(VOID_HTML_TAGS_LIST)
  .concat(HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_LIST)
).sort();

/**
 * List of all boolean HTML attributes
 * @const {RegExp}
 * @see {@link https://www.w3.org/TR/html5/infrastructure.html#sec-boolean-attributes}
 */
const BOOLEAN_ATTRIBUTES_LIST = [
  'disabled',
  'visible',
  'checked',
  'readonly',
  'required',
  'allowfullscreen',
  'autofocus',
  'autoplay',
  'compact',
  'controls',
  'default',
  'formnovalidate',
  'hidden',
  'ismap',
  'itemscope',
  'loop',
  'multiple',
  'muted',
  'noresize',
  'noshade',
  'novalidate',
  'nowrap',
  'open',
  'reversed',
  'seamless',
  'selected',
  'sortable',
  'truespeed',
  'typemustmatch'
];

/**
 * Join a list of items with the pipe symbol (usefull for regex list concatenation)
 * @private
 * @param   {Array} list - list of strings
 * @returns {string} the list received joined with pipes
 */
function joinWithPipe(list) {
  return list.join('|')
}

/**
 * Convert list of strings to regex in order to test against it ignoring the cases
 * @private
 * @param   {...Array} lists - array of strings
 * @returns {RegExp} regex that will match all the strings in the array received ignoring the cases
 */
function listsToRegex(...lists) {
  return new RegExp(`^/?(?:${joinWithPipe(lists.map(joinWithPipe))})$`, 'i')
}

/**
 * Regex matching all the html tags ignoring the cases
 * @const {RegExp}
 */
const HTML_TAGS_RE = listsToRegex(HTML_TAGS_LIST);

/**
 * Regex matching all the svg tags ignoring the cases
 * @const {RegExp}
 */
const SVG_TAGS_RE = listsToRegex(SVG_TAGS_LIST);

/**
 * Regex matching all the void html tags ignoring the cases
 * @const {RegExp}
 */
const VOID_HTML_TAGS_RE =  listsToRegex(VOID_HTML_TAGS_LIST);

/**
 * Regex matching all the void svg tags ignoring the cases
 * @const {RegExp}
 */
const VOID_SVG_TAGS_RE =  listsToRegex(VOID_SVG_TAGS_LIST);

/**
 * Regex matching all the html tags where the value tag is allowed
 * @const {RegExp}
 */
const HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_RE = listsToRegex(HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_LIST);

/**
 * Regex matching all the boolean attributes
 * @const {RegExp}
 */
const BOOLEAN_ATTRIBUTES_RE =  listsToRegex(BOOLEAN_ATTRIBUTES_LIST);

/**
 * True if it's a self closing tag
 * @param   {string}  tag - test tag
 * @returns {boolean} true if void
 * @example
 * isVoid('meta') // true
 * isVoid('circle') // true
 * isVoid('IMG') // true
 * isVoid('div') // false
 * isVoid('mask') // false
 */
function isVoid(tag) {
  return [
    VOID_HTML_TAGS_RE,
    VOID_SVG_TAGS_RE
  ].some(r => r.test(tag))
}

/**
 * True if it's not SVG nor a HTML known tag
 * @param   {string}  tag - test tag
 * @returns {boolean} true if custom element
 * @example
 * isCustom('my-component') // true
 * isCustom('div') // false
 */
function isCustom(tag) {
  return [
    HTML_TAGS_RE,
    SVG_TAGS_RE
  ].every(l => !l.test(tag))
}

/**
 * True if it's a boolean attribute
 * @param   {string} attribute - test attribute
 * @returns {boolean} true if the attribute is a boolean type
 * @example
 * isBoolAttribute('selected') // true
 * isBoolAttribute('class') // false
 */
function isBoolAttribute(attribute) {
  return BOOLEAN_ATTRIBUTES_RE.test(attribute)
}

/**
 * Memoization function
 * @param   {Function} fn - function to memoize
 * @returns {*} return of the function to memoize
 */
function memoize(fn) {
  const cache = new WeakMap();

  return (...args) => {
    if (cache.has(args[0])) return cache.get(args[0])

    const ret = fn(...args);

    cache.set(args[0], ret);

    return ret
  }
}

const expressionsContentRe = memoize(brackets => RegExp(`(${brackets[0]}[^${brackets[1]}]*?${brackets[1]})`, 'g'));
const isSpreadAttribute = name => SPREAD_OPERATOR.test(name);
const isAttributeExpression = (name, brackets) => name[0] === brackets[0];
const getAttributeEnd = (state, attr) => expr(state, attr, '[>/\\s]', attr.start);

/**
 * The more complex parsing is for attributes as it can contain quoted or
 * unquoted values or expressions.
 *
 * @param   {ParserStore} state  - Parser state
 * @returns {number} New parser mode.
 * @private
 */
function attr(state) {
  const { data, last, pos, root } = state;
  const tag = last; // the last (current) tag in the output
  const _CH = /\S/g; // matches the first non-space char
  const ch = execFromPos(_CH, pos, data);

  switch (true) {
  case !ch:
    state.pos = data.length; // reaching the end of the buffer with
    // NodeTypes.ATTR will generate error
    break
  case ch[0] === '>':
    // closing char found. If this is a self-closing tag with the name of the
    // Root tag, we need decrement the counter as we are changing mode.
    state.pos = tag.end = _CH.lastIndex;
    if (tag[IS_SELF_CLOSING]) {
      state.scryle = null; // allow selfClosing script/style tags
      if (root && root.name === tag.name) {
        state.count--; // "pop" root tag
      }
    }
    return TEXT
  case ch[0] === '/':
    state.pos = _CH.lastIndex; // maybe. delegate the validation
    tag[IS_SELF_CLOSING] = true; // the next loop
    break
  default:
    delete tag[IS_SELF_CLOSING]; // ensure unmark as selfclosing tag
    setAttribute(state, ch.index, tag);
  }

  return ATTR
}

/**
 * Parses an attribute and its expressions.
 *
 * @param   {ParserStore}  state  - Parser state
 * @param   {number} pos    - Starting position of the attribute
 * @param   {Object} tag    - Current parent tag
 * @returns {undefined} void function
 * @private
 */
function setAttribute(state, pos, tag) {
  const { data } = state;
  const expressionContent = expressionsContentRe(state.options.brackets);
  const re = ATTR_START; // (\S[^>/=\s]*)(?:\s*=\s*([^>/])?)? g
  const start = re.lastIndex = expressionContent.lastIndex = pos; // first non-whitespace
  const attrMatches = re.exec(data);
  const isExpressionName = isAttributeExpression(attrMatches[1], state.options.brackets);
  const match = isExpressionName ? [null, expressionContent.exec(data)[1], null] : attrMatches;

  if (match) {
    const end = re.lastIndex;
    const attr = parseAttribute(state, match, start, end, isExpressionName);

    //assert(q && q.type === Mode.TAG, 'no previous tag for the attr!')
    // Pushes the attribute and shifts the `end` position of the tag (`last`).
    state.pos = tag.end = attr.end;
    tag.attributes = addToCollection(tag.attributes, attr);
  }
}

function parseNomalAttribute(state, attr, quote) {
  const { data } = state;
  let { end } = attr;

  if (isBoolAttribute(attr.name)) {
    attr[IS_BOOLEAN] = true;
  }

  // parse the whole value (if any) and get any expressions on it
  if (quote) {
    // Usually, the value's first char (`quote`) is a quote and the lastIndex
    // (`end`) is the start of the value.
    let valueStart = end;
    // If it not, this is an unquoted value and we need adjust the start.
    if (quote !== '"' && quote !== '\'') {
      quote = ''; // first char of value is not a quote
      valueStart--; // adjust the starting position
    }

    end = expr(state, attr, quote || '[>/\\s]', valueStart);

    // adjust the bounds of the value and save its content
    return Object.assign(attr, {
      value: getChunk(data, valueStart, end),
      valueStart,
      end: quote ? ++end : end
    })
  }

  return attr
}


/**
 * Parse expression names <a {href}>
 * @param   {ParserStore}  state  - Parser state
 * @param   {Object} attr - attribute object parsed
 * @returns {Object} normalized attribute object
 */
function parseSpreadAttribute(state, attr) {
  const end = getAttributeEnd(state, attr);

  return {
    [IS_SPREAD]: true,
    start: attr.start,
    expressions: attr.expressions.map(expr => Object.assign(expr, {
      text: expr.text.replace(SPREAD_OPERATOR, '').trim()
    })),
    end: end
  }
}

/**
 * Parse expression names <a {href}>
 * @param   {ParserStore}  state  - Parser state
 * @param   {Object} attr - attribute object parsed
 * @returns {Object} normalized attribute object
 */
function parseExpressionNameAttribute(state, attr) {
  const end = getAttributeEnd(state, attr);

  return {
    start: attr.start,
    name: attr.expressions[0].text.trim(),
    expressions: attr.expressions,
    end: end
  }
}

/**
 * Parse the attribute values normalising the quotes
 * @param   {ParserStore}  state  - Parser state
 * @param   {Array} match - results of the attributes regex
 * @param   {number} start - attribute start position
 * @param   {number} end - attribute end position
 * @param   {boolean} isExpressionName - true if the attribute name is an expression
 * @returns {Object} attribute object
 */
function parseAttribute(state, match, start, end, isExpressionName) {
  const attr = {
    name: match[1],
    value: '',
    start,
    end
  };

  const quote = match[2]; // first letter of value or nothing

  switch (true) {
  case isSpreadAttribute(attr.name):
    return parseSpreadAttribute(state, attr)
  case isExpressionName === true:
    return parseExpressionNameAttribute(state, attr)
  default:
    return parseNomalAttribute(state, attr, quote)
  }
}

/**
 * Function to curry any javascript method
 * @param   {Function}  fn - the target function we want to curry
 * @param   {...[args]} acc - initial arguments
 * @returns {Function|*} it will return a function until the target function
 *                       will receive all of its arguments
 */
function curry(fn, ...acc) {
  return (...args) => {
    args = [...acc, ...args];

    return args.length < fn.length ?
      curry(fn, ...args) :
      fn(...args)
  }
}

/**
 * Parses comments in long or short form
 * (any DOCTYPE & CDATA blocks are parsed as comments).
 *
 * @param   {ParserState} state  - Parser state
 * @param   {string} data       - Buffer to parse
 * @param   {number} start      - Position of the '<!' sequence
 * @returns {number} node type id
 * @private
 */
function comment(state, data, start) {
  const pos = start + 2; // skip '<!'
  const str = data.substr(pos, 2) === '--' ? '-->' : '>';
  const end = data.indexOf(str, pos);
  if (end < 0) {
    panic(data, unclosedComment, start);
  }
  pushComment(state, start, end + str.length);

  return TEXT
}

/**
 * Parse a comment.
 *
 * @param   {ParserState}  state - Current parser state
 * @param   {number}  start - Start position of the tag
 * @param   {number}  end   - Ending position (last char of the tag)
 * @returns {undefined} void function
 * @private
 */
function pushComment(state, start, end) {
  flush(state);
  state.pos = end;
  if (state.options.comments === true) {
    state.last = { type: COMMENT, start, end };
  }
}

/**
 * Pushes a new *tag* and set `last` to this, so any attributes
 * will be included on this and shifts the `end`.
 *
 * @param   {ParserState} state  - Current parser state
 * @param   {string}  name      - Name of the node including any slash
 * @param   {number}  start     - Start position of the tag
 * @param   {number}  end       - Ending position (last char of the tag + 1)
 * @returns {undefined} - void function
 * @private
 */
function pushTag(state, name, start, end) {
  const root = state.root;
  const last = { type: TAG, name, start, end };

  if (isCustom(name)) {
    last[IS_CUSTOM] = true;
  }

  if (isVoid(name)) {
    last[IS_VOID] = true;
  }

  state.pos = end;

  if (root) {
    if (name === root.name) {
      state.count++;
    } else if (name === root.close) {
      state.count--;
    }
    flush(state);
  } else {
    // start with root (keep ref to output)
    state.root = { name: last.name, close: `/${name}` };
    state.count = 1;
  }

  state.last = last;
}

/**
 * Parse the tag following a '<' character, or delegate to other parser
 * if an invalid tag name is found.
 *
 * @param   {ParserState} state  - Parser state
 * @returns {number} New parser mode
 * @private
 */
function tag(state) {
  const { pos, data } = state; // pos of the char following '<'
  const start = pos - 1; // pos of '<'
  const str = data.substr(pos, 2); // first two chars following '<'

  switch (true) {
  case str[0] === '!':
    return comment(state, data, start)
  case TAG_2C.test(str):
    return parseTag(state, start)
  default:
    return pushText(state, start, pos) // pushes the '<' as text
  }
}

function parseTag(state, start) {
  const { data, pos } = state;
  const re = TAG_NAME; // (\/?[^\s>/]+)\s*(>)? g
  const match = execFromPos(re, pos, data);
  const end = re.lastIndex;
  const name = match[1].toLowerCase(); // $1: tag name including any '/'
  // script/style block is parsed as another tag to extract attributes
  if (name in RE_SCRYLE) {
    state.scryle = name; // used by parseText
  }

  pushTag(state, name, start, end);
  // only '>' can ends the tag here, the '/' is handled in parseAttribute
  if (!match[2]) {
    return ATTR
  }

  return TEXT
}

/**
 * Parses regular text and script/style blocks ...scryle for short :-)
 * (the content of script and style is text as well)
 *
 * @param   {ParserState} state - Parser state
 * @returns {number} New parser mode.
 * @private
 */
function text(state) {
  const { pos, data, scryle } = state;

  switch (true) {
  case typeof scryle === 'string': {
    const name = scryle;
    const re = RE_SCRYLE[name];
    const match = execFromPos(re, pos, data);

    if (!match) {
      panic(data, unclosedNamedBlock.replace('%1', name), pos - 1);
    }

    const start = match.index;
    const end = re.lastIndex;
    state.scryle = null; // reset the script/style flag now
    // write the tag content, if any
    if (start > pos) {
      parseSpecialTagsContent(state, name, match);
    }
    // now the closing tag, either </script> or </style>
    pushTag(state, `/${name}`, start, end);
    break
  }
  case data[pos] === '<':
    state.pos++;
    return TAG
  default:
    expr(state, null, '<', pos);
  }

  return TEXT
}

/**
 * Parse the text content depending on the name
 * @param   {ParserState} state - Parser state
 * @param   {string} name  - one of the tags matched by the RE_SCRYLE regex
 * @param   {Array}  match - result of the regex matching the content of the parsed tag
 * @returns {undefined} void function
 */
function parseSpecialTagsContent(state, name, match) {
  const { pos } = state;
  const start = match.index;

  if (name === TEXTAREA_TAG) {
    expr(state, null, match[0], pos);
  } else {
    pushText(state, pos, start);
  }
}

/*---------------------------------------------------------------------
 * Tree builder for the riot tag parser.
 *
 * The output has a root property and separate arrays for `html`, `css`,
 * and `js` tags.
 *
 * The root tag is included as first element in the `html` array.
 * Script tags marked with "defer" are included in `html` instead `js`.
 *
 * - Mark SVG tags
 * - Mark raw tags
 * - Mark void tags
 * - Split prefixes from expressions
 * - Unescape escaped brackets and escape EOLs and backslashes
 * - Compact whitespace (option `compact`) for non-raw tags
 * - Create an array `parts` for text nodes and attributes
 *
 * Throws on unclosed tags or closing tags without start tag.
 * Selfclosing and void tags has no nodes[] property.
 */

/**
 * Escape the carriage return and the line feed from a string
 * @param   {string} string - input string
 * @returns {string} output string escaped
 */
function escapeReturn(string) {
  return string
    .replace(/\r/g, '\\r')
    .replace(/\n/g, '\\n')
}

/**
 * Escape double slashes in a string
 * @param   {string} string - input string
 * @returns {string} output string escaped
 */
function escapeSlashes(string) {
  return string.replace(/\\/g, '\\\\')
}

/**
 * Replace the multiple spaces with only one
 * @param   {string} string - input string
 * @returns {string} string without trailing spaces
 */
function cleanSpaces(string) {
  return string.replace(/\s+/g, ' ')
}

const TREE_BUILDER_STRUCT = Object.seal({
  get() {
    const store = this.store;
    // The real root tag is in store.root.nodes[0]
    return {
      [TEMPLATE_OUTPUT_NAME]: store.root.nodes[0],
      [CSS_OUTPUT_NAME]: store[STYLE_TAG],
      [JAVASCRIPT_OUTPUT_NAME]: store[JAVASCRIPT_TAG]
    }
  },

  /**
  * Process the current tag or text.
  * @param {Object} node - Raw pseudo-node from the parser
  * @returns {undefined} void function
  */
  push(node) {
    const store = this.store;

    switch (node.type) {
    case TEXT:
      this.pushText(store, node);
      break
    case TAG: {
      const name = node.name;
      const closingTagChar = '/';
      const [firstChar] = name;

      if (firstChar === closingTagChar && !node.isVoid) {
        this.closeTag(store, node, name);
      } else if (firstChar !== closingTagChar) {
        this.openTag(store, node);
      }
      break
    }
    }
  },
  closeTag(store, node) {
    const last = store.scryle || store.last;

    last.end = node.end;

    if (store.scryle) {
      store.scryle = null;
    } else {
      store.last = store.stack.pop();
    }
  },

  openTag(store, node) {
    const name = node.name;
    const attrs = node.attributes;

    if ([JAVASCRIPT_TAG, STYLE_TAG].includes(name)) {
      // Only accept one of each
      if (store[name]) {
        panic(this.store.data, duplicatedNamedTag.replace('%1', name), node.start);
      }

      store[name] = node;
      store.scryle = store[name];

    } else {
      // store.last holds the last tag pushed in the stack and this are
      // non-void, non-empty tags, so we are sure the `lastTag` here
      // have a `nodes` property.
      const lastTag = store.last;
      const newNode = node;

      lastTag.nodes.push(newNode);

      if (lastTag[IS_RAW] || RAW_TAGS.test(name)) {
        node[IS_RAW] = true;
      }

      if (!node[IS_SELF_CLOSING] && !node[IS_VOID]) {
        store.stack.push(lastTag);
        newNode.nodes = [];
        store.last = newNode;
      }
    }

    if (attrs) {
      this.attrs(attrs);
    }
  },
  attrs(attributes) {
    attributes.forEach(attr => {
      if (attr.value) {
        this.split(attr, attr.value, attr.valueStart, true);
      }
    });
  },
  pushText(store, node) {
    const text = node.text;
    const empty = !/\S/.test(text);
    const scryle = store.scryle;
    if (!scryle) {
      // store.last always have a nodes property
      const parent = store.last;

      const pack = this.compact && !parent[IS_RAW];
      if (pack && empty) {
        return
      }
      this.split(node, text, node.start, pack);
      parent.nodes.push(node);
    } else if (!empty) {
      scryle.text = node;
    }
  },
  split(node, source, start, pack) {
    const expressions = node.expressions;
    const parts = [];

    if (expressions) {
      let pos = 0;

      expressions.forEach(expr => {
        const text = source.slice(pos, expr.start - start);
        const code = expr.text;
        parts.push(this.sanitise(node, text, pack), escapeReturn(escapeSlashes(code).trim()));
        pos = expr.end - start;
      });

      if (pos < node.end) {
        parts.push(this.sanitise(node, source.slice(pos), pack));
      }
    } else {
      parts[0] = this.sanitise(node, source, pack);
    }

    node.parts = parts.filter(p => p); // remove the empty strings
  },
  // unescape escaped brackets and split prefixes of expressions
  sanitise(node, text, pack) {
    let rep = node.unescape;
    if (rep) {
      let idx = 0;
      rep = `\\${rep}`;
      while ((idx = text.indexOf(rep, idx)) !== -1) {
        text = text.substr(0, idx) + text.substr(idx + 1);
        idx++;
      }
    }

    text = escapeSlashes(text);

    return pack ? cleanSpaces(text) : escapeReturn(text)
  }
});

function createTreeBuilder(data, options) {
  const root = {
    type: TAG,
    name: '',
    start: 0,
    end: 0,
    nodes: []
  };

  return Object.assign(Object.create(TREE_BUILDER_STRUCT), {
    compact: options.compact !== false,
    store: {
      last: root,
      stack: [],
      scryle: null,
      root,
      style: null,
      script: null,
      data
    }
  })
}

/**
 * Factory for the Parser class, exposing only the `parse` method.
 * The export adds the Parser class as property.
 *
 * @param   {Object}   options - User Options
 * @param   {Function} customBuilder - Tree builder factory
 * @returns {Function} Public Parser implementation.
 */
function parser(options, customBuilder) {
  const state = curry(createParserState)(options, customBuilder || createTreeBuilder);
  return {
    parse: (data) => parse(state(data))
  }
}

/**
 * Create a new state object
 * @param   {Object} userOptions - parser options
 * @param   {Function} builder - Tree builder factory
 * @param   {string} data - data to parse
 * @returns {ParserState} it represents the current parser state
 */
function createParserState(userOptions, builder, data) {
  const options = Object.assign({
    brackets: ['{', '}']
  }, userOptions);

  return {
    options,
    regexCache: {},
    pos: 0,
    count: -1,
    root: null,
    last: null,
    scryle: null,
    builder: builder(data, options),
    data
  }
}

/**
 * It creates a raw output of pseudo-nodes with one of three different types,
 * all of them having a start/end position:
 *
 * - TAG     -- Opening or closing tags
 * - TEXT    -- Raw text
 * - COMMENT -- Comments
 *
 * @param   {ParserState}  state - Current parser state
 * @returns {ParserResult} Result, contains data and output properties.
 */
function parse(state) {
  const { data } = state;

  walk(state);
  flush(state);

  if (state.count) {
    panic(data, state.count > 0 ? unexpectedEndOfFile : rootTagNotFound, state.pos);
  }

  return {
    data,
    output: state.builder.get()
  }
}

/**
 * Parser walking recursive function
 * @param {ParserState}  state - Current parser state
 * @param {string} type - current parsing context
 * @returns {undefined} void function
 */
function walk(state, type) {
  const { data } = state;
  // extend the state adding the tree builder instance and the initial data
  const length = data.length;

  // The "count" property is set to 1 when the first tag is found.
  // This becomes the root and precedent text or comments are discarded.
  // So, at the end of the parsing count must be zero.
  if (state.pos < length && state.count) {
    walk(state, eat(state, type));
  }
}

/**
 * Function to help iterating on the current parser state
 * @param {ParserState}  state - Current parser state
 * @param   {string} type - current parsing context
 * @returns {string} parsing context
 */
function eat(state, type) {
  switch (type) {
  case TAG:
    return tag(state)
  case ATTR:
    return attr(state)
  default:
    return text(state)
  }
}

/**
 * Expose the internal constants
 */
const constants = c;

/**
 * The nodeTypes definition
 */
const nodeTypes = types;

exports.constants = constants;
exports.default = parser;
exports.nodeTypes = nodeTypes;
