You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1715 lines
42 KiB

5 years ago
  1. 'use strict';
  2. Object.defineProperty(exports, '__esModule', { value: true });
  3. /**
  4. * Not all the types are handled in this module.
  5. *
  6. * @enum {number}
  7. * @readonly
  8. */
  9. const TAG = 1; /* TAG */
  10. const ATTR = 2; /* ATTR */
  11. const TEXT = 3; /* TEXT */
  12. const CDATA = 4; /* CDATA */
  13. const COMMENT = 8; /* COMMENT */
  14. const DOCUMENT = 9; /* DOCUMENT */
  15. const DOCTYPE = 10; /* DOCTYPE */
  16. const DOCUMENT_FRAGMENT = 11; /* DOCUMENT_FRAGMENT */
  17. var types = /*#__PURE__*/Object.freeze({
  18. TAG: TAG,
  19. ATTR: ATTR,
  20. TEXT: TEXT,
  21. CDATA: CDATA,
  22. COMMENT: COMMENT,
  23. DOCUMENT: DOCUMENT,
  24. DOCTYPE: DOCTYPE,
  25. DOCUMENT_FRAGMENT: DOCUMENT_FRAGMENT
  26. });
  27. const rootTagNotFound = 'Root tag not found.';
  28. const unclosedTemplateLiteral = 'Unclosed ES6 template literal.';
  29. const unexpectedEndOfFile = 'Unexpected end of file.';
  30. const unclosedComment = 'Unclosed comment.';
  31. const unclosedNamedBlock = 'Unclosed "%1" block.';
  32. const duplicatedNamedTag = 'Duplicate tag "<%1>".';
  33. const unexpectedCharInExpression = 'Unexpected character %1.';
  34. const unclosedExpression = 'Unclosed expression.';
  35. /**
  36. * Matches the start of valid tags names; used with the first 2 chars after the `'<'`.
  37. * @const
  38. * @private
  39. */
  40. const TAG_2C = /^(?:\/[a-zA-Z]|[a-zA-Z][^\s>/]?)/;
  41. /**
  42. * Matches valid tags names AFTER the validation with `TAG_2C`.
  43. * $1: tag name including any `'/'`, $2: non self-closing brace (`>`) w/o attributes.
  44. * @const
  45. * @private
  46. */
  47. const TAG_NAME = /(\/?[^\s>/]+)\s*(>)?/g;
  48. /**
  49. * Matches an attribute name-value pair (both can be empty).
  50. * $1: attribute name, $2: value including any quotes.
  51. * @const
  52. * @private
  53. */
  54. const ATTR_START = /(\S[^>/=\s]*)(?:\s*=\s*([^>/])?)?/g;
  55. /**
  56. * Matches the spread operator
  57. * it will be used for the spread attributes
  58. * @type {RegExp}
  59. */
  60. const SPREAD_OPERATOR = /\.\.\./;
  61. /**
  62. * Matches the closing tag of a `script` and `style` block.
  63. * Used by parseText fo find the end of the block.
  64. * @const
  65. * @private
  66. */
  67. const RE_SCRYLE = {
  68. script: /<\/script\s*>/gi,
  69. style: /<\/style\s*>/gi,
  70. textarea: /<\/textarea\s*>/gi
  71. };
  72. // Do not touch text content inside this tags
  73. const RAW_TAGS = /^\/?(?:pre|textarea)$/;
  74. const JAVASCRIPT_OUTPUT_NAME = 'javascript';
  75. const CSS_OUTPUT_NAME = 'css';
  76. const TEMPLATE_OUTPUT_NAME = 'template';
  77. // Tag names
  78. const JAVASCRIPT_TAG = 'script';
  79. const STYLE_TAG = 'style';
  80. const TEXTAREA_TAG = 'textarea';
  81. // Boolean attributes
  82. const IS_RAW = 'isRaw';
  83. const IS_SELF_CLOSING = 'isSelfClosing';
  84. const IS_VOID = 'isVoid';
  85. const IS_BOOLEAN = 'isBoolean';
  86. const IS_CUSTOM = 'isCustom';
  87. const IS_SPREAD = 'isSpread';
  88. /**
  89. * Add an item into a collection, if the collection is not an array
  90. * we create one and add the item to it
  91. * @param {Array} collection - target collection
  92. * @param {*} item - item to add to the collection
  93. * @returns {Array} array containing the new item added to it
  94. */
  95. function addToCollection(collection = [], item) {
  96. collection.push(item);
  97. return collection
  98. }
  99. /**
  100. * Run RegExp.exec starting from a specific position
  101. * @param {RegExp} re - regex
  102. * @param {number} pos - last index position
  103. * @param {string} string - regex target
  104. * @returns {Array} regex result
  105. */
  106. function execFromPos(re, pos, string) {
  107. re.lastIndex = pos;
  108. return re.exec(string)
  109. }
  110. /**
  111. * Escape special characters in a given string, in preparation to create a regex.
  112. *
  113. * @param {string} str - Raw string
  114. * @returns {string} Escaped string.
  115. */
  116. var escapeStr = (str) => str.replace(/(?=[-[\](){^*+?.$|\\])/g, '\\');
  117. function formatError(data, message, pos) {
  118. if (!pos) {
  119. pos = data.length;
  120. }
  121. // count unix/mac/win eols
  122. const line = (data.slice(0, pos).match(/\r\n?|\n/g) || '').length + 1;
  123. let col = 0;
  124. while (--pos >= 0 && !/[\r\n]/.test(data[pos])) {
  125. ++col;
  126. }
  127. return `[${line},${col}]: ${message}`
  128. }
  129. const $_ES6_BQ = '`';
  130. /**
  131. * Searches the next backquote that signals the end of the ES6 Template Literal
  132. * or the "${" sequence that starts a JS expression, skipping any escaped
  133. * character.
  134. *
  135. * @param {string} code - Whole code
  136. * @param {number} pos - The start position of the template
  137. * @param {string[]} stack - To save nested ES6 TL count
  138. * @returns {number} The end of the string (-1 if not found)
  139. */
  140. function skipES6TL(code, pos, stack) {
  141. // we are in the char following the backquote (`),
  142. // find the next unescaped backquote or the sequence "${"
  143. const re = /[`$\\]/g;
  144. let c;
  145. while (re.lastIndex = pos, re.exec(code)) {
  146. pos = re.lastIndex;
  147. c = code[pos - 1];
  148. if (c === '`') {
  149. return pos
  150. }
  151. if (c === '$' && code[pos++] === '{') {
  152. stack.push($_ES6_BQ, '}');
  153. return pos
  154. }
  155. // else this is an escaped char
  156. }
  157. throw formatError(code, unclosedTemplateLiteral, pos)
  158. }
  159. /**
  160. * Custom error handler can be implemented replacing this method.
  161. * The `state` object includes the buffer (`data`)
  162. * The error position (`loc`) contains line (base 1) and col (base 0).
  163. * @param {string} data - string containing the error
  164. * @param {string} msg - Error message
  165. * @param {number} pos - Position of the error
  166. * @returns {undefined} throw an exception error
  167. */
  168. function panic(data, msg, pos) {
  169. const message = formatError(data, msg, pos);
  170. throw new Error(message)
  171. }
  172. // forked from https://github.com/aMarCruz/skip-regex
  173. // safe characters to precced a regex (including `=>`, `**`, and `...`)
  174. const beforeReChars = '[{(,;:?=|&!^~>%*/';
  175. const beforeReSign = `${beforeReChars}+-`;
  176. // keyword that can preceed a regex (`in` is handled as special case)
  177. const beforeReWords = [
  178. 'case',
  179. 'default',
  180. 'do',
  181. 'else',
  182. 'in',
  183. 'instanceof',
  184. 'prefix',
  185. 'return',
  186. 'typeof',
  187. 'void',
  188. 'yield'
  189. ];
  190. // Last chars of all the beforeReWords elements to speed up the process.
  191. const wordsEndChar = beforeReWords.reduce((s, w) => s + w.slice(-1), '');
  192. // Matches literal regex from the start of the buffer.
  193. // The buffer to search must not include line-endings.
  194. const RE_LIT_REGEX = /^\/(?=[^*>/])[^[/\\]*(?:(?:\\.|\[(?:\\.|[^\]\\]*)*\])[^[\\/]*)*?\/[gimuy]*/;
  195. // Valid characters for JavaScript variable names and literal numbers.
  196. const RE_JS_VCHAR = /[$\w]/;
  197. // Match dot characters that could be part of tricky regex
  198. const RE_DOT_CHAR = /.*/g;
  199. /**
  200. * Searches the position of the previous non-blank character inside `code`,
  201. * starting with `pos - 1`.
  202. *
  203. * @param {string} code - Buffer to search
  204. * @param {number} pos - Starting position
  205. * @returns {number} Position of the first non-blank character to the left.
  206. * @private
  207. */
  208. function _prev(code, pos) {
  209. while (--pos >= 0 && /\s/.test(code[pos]));
  210. return pos
  211. }
  212. /**
  213. * Check if the character in the `start` position within `code` can be a regex
  214. * and returns the position following this regex or `start+1` if this is not
  215. * one.
  216. *
  217. * NOTE: Ensure `start` points to a slash (this is not checked).
  218. *
  219. * @function skipRegex
  220. * @param {string} code - Buffer to test in
  221. * @param {number} start - Position the first slash inside `code`
  222. * @returns {number} Position of the char following the regex.
  223. *
  224. */
  225. /* istanbul ignore next */
  226. function skipRegex(code, start) {
  227. let pos = RE_DOT_CHAR.lastIndex = start++;
  228. // `exec()` will extract from the slash to the end of the line
  229. // and the chained `match()` will match the possible regex.
  230. const match = (RE_DOT_CHAR.exec(code) || ' ')[0].match(RE_LIT_REGEX);
  231. if (match) {
  232. const next = pos + match[0].length; // result comes from `re.match`
  233. pos = _prev(code, pos);
  234. let c = code[pos];
  235. // start of buffer or safe prefix?
  236. if (pos < 0 || beforeReChars.includes(c)) {
  237. return next
  238. }
  239. // from here, `pos` is >= 0 and `c` is code[pos]
  240. if (c === '.') {
  241. // can be `...` or something silly like 5./2
  242. if (code[pos - 1] === '.') {
  243. start = next;
  244. }
  245. } else {
  246. if (c === '+' || c === '-') {
  247. // tricky case
  248. if (code[--pos] !== c || // if have a single operator or
  249. (pos = _prev(code, pos)) < 0 || // ...have `++` and no previous token
  250. beforeReSign.includes(c = code[pos])) {
  251. return next // ...this is a regex
  252. }
  253. }
  254. if (wordsEndChar.includes(c)) { // looks like a keyword?
  255. const end = pos + 1;
  256. // get the complete (previous) keyword
  257. while (--pos >= 0 && RE_JS_VCHAR.test(code[pos]));
  258. // it is in the allowed keywords list?
  259. if (beforeReWords.includes(code.slice(pos + 1, end))) {
  260. start = next;
  261. }
  262. }
  263. }
  264. }
  265. return start
  266. }
  267. /*
  268. * Mini-parser for expressions.
  269. * The main pourpose of this module is to find the end of an expression
  270. * and return its text without the enclosing brackets.
  271. * Does not works with comments, but supports ES6 template strings.
  272. */
  273. /**
  274. * @exports exprExtr
  275. */
  276. const S_SQ_STR = /'[^'\n\r\\]*(?:\\(?:\r\n?|[\S\s])[^'\n\r\\]*)*'/.source;
  277. /**
  278. * Matches double quoted JS strings taking care about nested quotes
  279. * and EOLs (escaped EOLs are Ok).
  280. *
  281. * @const
  282. * @private
  283. */
  284. const S_STRING = `${S_SQ_STR}|${S_SQ_STR.replace(/'/g, '"')}`;
  285. /**
  286. * Regex cache
  287. *
  288. * @type {Object.<string, RegExp>}
  289. * @const
  290. * @private
  291. */
  292. const reBr = {};
  293. /**
  294. * Makes an optimal regex that matches quoted strings, brackets, backquotes
  295. * and the closing brackets of an expression.
  296. *
  297. * @param {string} b - Closing brackets
  298. * @returns {RegExp} - optimized regex
  299. */
  300. function _regex(b) {
  301. let re = reBr[b];
  302. if (!re) {
  303. let s = escapeStr(b);
  304. if (b.length > 1) {
  305. s = `${s}|[`;
  306. } else {
  307. s = /[{}[\]()]/.test(b) ? '[' : `[${s}`;
  308. }
  309. reBr[b] = re = new RegExp(`${S_STRING}|${s}\`/\\{}[\\]()]`, 'g');
  310. }
  311. return re
  312. }
  313. /**
  314. * Update the scopes stack removing or adding closures to it
  315. * @param {Array} stack - array stacking the expression closures
  316. * @param {string} char - current char to add or remove from the stack
  317. * @param {string} idx - matching index
  318. * @param {string} code - expression code
  319. * @returns {Object} result
  320. * @returns {Object} result.char - either the char received or the closing braces
  321. * @returns {Object} result.index - either a new index to skip part of the source code,
  322. * or 0 to keep from parsing from the old position
  323. */
  324. function updateStack(stack, char, idx, code) {
  325. let index = 0;
  326. switch (char) {
  327. case '[':
  328. case '(':
  329. case '{':
  330. stack.push(char === '[' ? ']' : char === '(' ? ')' : '}');
  331. break
  332. case ')':
  333. case ']':
  334. case '}':
  335. if (char !== stack.pop()) {
  336. panic(code, unexpectedCharInExpression.replace('%1', char), index);
  337. }
  338. if (char === '}' && stack[stack.length - 1] === $_ES6_BQ) {
  339. char = stack.pop();
  340. }
  341. index = idx + 1;
  342. break
  343. case '/':
  344. index = skipRegex(code, idx);
  345. }
  346. return { char, index }
  347. }
  348. /**
  349. * Parses the code string searching the end of the expression.
  350. * It skips braces, quoted strings, regexes, and ES6 template literals.
  351. *
  352. * @function exprExtr
  353. * @param {string} code - Buffer to parse
  354. * @param {number} start - Position of the opening brace
  355. * @param {[string,string]} bp - Brackets pair
  356. * @returns {Object} Expression's end (after the closing brace) or -1
  357. * if it is not an expr.
  358. */
  359. function exprExtr(code, start, bp) {
  360. const [openingBraces, closingBraces] = bp;
  361. const offset = start + openingBraces.length; // skips the opening brace
  362. const stack = []; // expected closing braces ('`' for ES6 TL)
  363. const re = _regex(closingBraces);
  364. re.lastIndex = offset; // begining of the expression
  365. let end;
  366. let match;
  367. while (match = re.exec(code)) { // eslint-disable-line
  368. const idx = match.index;
  369. const str = match[0];
  370. end = re.lastIndex;
  371. // end the iteration
  372. if (str === closingBraces && !stack.length) {
  373. return {
  374. text: code.slice(offset, idx),
  375. start,
  376. end
  377. }
  378. }
  379. const { char, index } = updateStack(stack, str[0], idx, code);
  380. // update the end value depending on the new index received
  381. end = index || end;
  382. // update the regex last index
  383. re.lastIndex = char === $_ES6_BQ ? skipES6TL(code, end, stack) : end;
  384. }
  385. if (stack.length) {
  386. panic(code, unclosedExpression, end);
  387. }
  388. }
  389. /**
  390. * Outputs the last parsed node. Can be used with a builder too.
  391. *
  392. * @param {ParserStore} store - Parsing store
  393. * @returns {undefined} void function
  394. * @private
  395. */
  396. function flush(store) {
  397. const last = store.last;
  398. store.last = null;
  399. if (last && store.root) {
  400. store.builder.push(last);
  401. }
  402. }
  403. /**
  404. * Get the code chunks from start and end range
  405. * @param {string} source - source code
  406. * @param {number} start - Start position of the chunk we want to extract
  407. * @param {number} end - Ending position of the chunk we need
  408. * @returns {string} chunk of code extracted from the source code received
  409. * @private
  410. */
  411. function getChunk(source, start, end) {
  412. return source.slice(start, end)
  413. }
  414. /**
  415. * states text in the last text node, or creates a new one if needed.
  416. *
  417. * @param {ParserState} state - Current parser state
  418. * @param {number} start - Start position of the tag
  419. * @param {number} end - Ending position (last char of the tag)
  420. * @param {Object} extra - extra properties to add to the text node
  421. * @param {RawExpr[]} extra.expressions - Found expressions
  422. * @param {string} extra.unescape - Brackets to unescape
  423. * @returns {undefined} - void function
  424. * @private
  425. */
  426. function pushText(state, start, end, extra = {}) {
  427. const text = getChunk(state.data, start, end);
  428. const expressions = extra.expressions;
  429. const unescape = extra.unescape;
  430. let q = state.last;
  431. state.pos = end;
  432. if (q && q.type === TEXT) {
  433. q.text += text;
  434. q.end = end;
  435. } else {
  436. flush(state);
  437. state.last = q = { type: TEXT, text, start, end };
  438. }
  439. if (expressions && expressions.length) {
  440. q.expressions = (q.expressions || []).concat(expressions);
  441. }
  442. if (unescape) {
  443. q.unescape = unescape;
  444. }
  445. return TEXT
  446. }
  447. /**
  448. * Find the end of the attribute value or text node
  449. * Extract expressions.
  450. * Detect if value have escaped brackets.
  451. *
  452. * @param {ParserState} state - Parser state
  453. * @param {HasExpr} node - Node if attr, info if text
  454. * @param {string} endingChars - Ends the value or text
  455. * @param {number} start - Starting position
  456. * @returns {number} Ending position
  457. * @private
  458. */
  459. function expr(state, node, endingChars, start) {
  460. const re = b0re(state, endingChars);
  461. re.lastIndex = start; // reset re position
  462. const { unescape, expressions, end } = parseExpressions(state, re);
  463. if (node) {
  464. if (unescape) {
  465. node.unescape = unescape;
  466. }
  467. if (expressions.length) {
  468. node.expressions = expressions;
  469. }
  470. } else {
  471. pushText(state, start, end, {expressions, unescape});
  472. }
  473. return end
  474. }
  475. /**
  476. * Parse a text chunk finding all the expressions in it
  477. * @param {ParserState} state - Parser state
  478. * @param {RegExp} re - regex to match the expressions contents
  479. * @returns {Object} result containing the expression found, the string to unescape and the end position
  480. */
  481. function parseExpressions(state, re) {
  482. const { data, options } = state;
  483. const { brackets } = options;
  484. const expressions = [];
  485. let unescape, pos, match;
  486. // Anything captured in $1 (closing quote or character) ends the loop...
  487. while ((match = re.exec(data)) && !match[1]) {
  488. // ...else, we have an opening bracket and maybe an expression.
  489. pos = match.index;
  490. if (data[pos - 1] === '\\') {
  491. unescape = match[0]; // it is an escaped opening brace
  492. } else {
  493. const tmpExpr = exprExtr(data, pos, brackets);
  494. if (tmpExpr) {
  495. expressions.push(tmpExpr);
  496. re.lastIndex = tmpExpr.end;
  497. }
  498. }
  499. }
  500. // Even for text, the parser needs match a closing char
  501. if (!match) {
  502. panic(data, unexpectedEndOfFile, pos);
  503. }
  504. return {
  505. unescape,
  506. expressions,
  507. end: match.index
  508. }
  509. }
  510. /**
  511. * Creates a regex for the given string and the left bracket.
  512. * The string is captured in $1.
  513. *
  514. * @param {ParserState} state - Parser state
  515. * @param {string} str - String to search
  516. * @returns {RegExp} Resulting regex.
  517. * @private
  518. */
  519. function b0re(state, str) {
  520. const { brackets } = state.options;
  521. const re = state.regexCache[str];
  522. if (re) return re
  523. const b0 = escapeStr(brackets[0]);
  524. // cache the regex extending the regexCache object
  525. Object.assign(state.regexCache, { [str]: new RegExp(`(${str})|${b0}`, 'g') });
  526. return state.regexCache[str]
  527. }
  528. /**
  529. * SVG void elements that cannot be auto-closed and shouldn't contain child nodes.
  530. * @const {Array}
  531. */
  532. const VOID_SVG_TAGS_LIST = [
  533. 'circle',
  534. 'ellipse',
  535. 'line',
  536. 'path',
  537. 'polygon',
  538. 'polyline',
  539. 'rect',
  540. 'stop',
  541. 'use'
  542. ];
  543. /**
  544. * List of all the available svg tags
  545. * @const {Array}
  546. * @see {@link https://github.com/wooorm/svg-tag-names}
  547. */
  548. const SVG_TAGS_LIST = [
  549. 'a',
  550. 'altGlyph',
  551. 'altGlyphDef',
  552. 'altGlyphItem',
  553. 'animate',
  554. 'animateColor',
  555. 'animateMotion',
  556. 'animateTransform',
  557. 'animation',
  558. 'audio',
  559. 'canvas',
  560. 'clipPath',
  561. 'color-profile',
  562. 'cursor',
  563. 'defs',
  564. 'desc',
  565. 'discard',
  566. 'feBlend',
  567. 'feColorMatrix',
  568. 'feComponentTransfer',
  569. 'feComposite',
  570. 'feConvolveMatrix',
  571. 'feDiffuseLighting',
  572. 'feDisplacementMap',
  573. 'feDistantLight',
  574. 'feDropShadow',
  575. 'feFlood',
  576. 'feFuncA',
  577. 'feFuncB',
  578. 'feFuncG',
  579. 'feFuncR',
  580. 'feGaussianBlur',
  581. 'feImage',
  582. 'feMerge',
  583. 'feMergeNode',
  584. 'feMorphology',
  585. 'feOffset',
  586. 'fePointLight',
  587. 'feSpecularLighting',
  588. 'feSpotLight',
  589. 'feTile',
  590. 'feTurbulence',
  591. 'filter',
  592. 'font',
  593. 'font-face',
  594. 'font-face-format',
  595. 'font-face-name',
  596. 'font-face-src',
  597. 'font-face-uri',
  598. 'foreignObject',
  599. 'g',
  600. 'glyph',
  601. 'glyphRef',
  602. 'handler',
  603. 'hatch',
  604. 'hatchpath',
  605. 'hkern',
  606. 'iframe',
  607. 'image',
  608. 'linearGradient',
  609. 'listener',
  610. 'marker',
  611. 'mask',
  612. 'mesh',
  613. 'meshgradient',
  614. 'meshpatch',
  615. 'meshrow',
  616. 'metadata',
  617. 'missing-glyph',
  618. 'mpath',
  619. 'pattern',
  620. 'prefetch',
  621. 'radialGradient',
  622. 'script',
  623. 'set',
  624. 'solidColor',
  625. 'solidcolor',
  626. 'style',
  627. 'svg',
  628. 'switch',
  629. 'symbol',
  630. 'tbreak',
  631. 'text',
  632. 'textArea',
  633. 'textPath',
  634. 'title',
  635. 'tref',
  636. 'tspan',
  637. 'unknown',
  638. 'video',
  639. 'view',
  640. 'vkern'
  641. ].concat(VOID_SVG_TAGS_LIST).sort();
  642. /**
  643. * HTML void elements that cannot be auto-closed and shouldn't contain child nodes.
  644. * @type {Array}
  645. * @see {@link http://www.w3.org/TR/html-markup/syntax.html#syntax-elements}
  646. * @see {@link http://www.w3.org/TR/html5/syntax.html#void-elements}
  647. */
  648. const VOID_HTML_TAGS_LIST = [
  649. 'area',
  650. 'base',
  651. 'br',
  652. 'col',
  653. 'embed',
  654. 'hr',
  655. 'img',
  656. 'input',
  657. 'keygen',
  658. 'link',
  659. 'menuitem',
  660. 'meta',
  661. 'param',
  662. 'source',
  663. 'track',
  664. 'wbr'
  665. ];
  666. /**
  667. * List of all the html tags
  668. * @const {Array}
  669. * @see {@link https://github.com/sindresorhus/html-tags}
  670. */
  671. const HTML_TAGS_LIST = [
  672. 'a',
  673. 'abbr',
  674. 'address',
  675. 'article',
  676. 'aside',
  677. 'audio',
  678. 'b',
  679. 'bdi',
  680. 'bdo',
  681. 'blockquote',
  682. 'body',
  683. 'button',
  684. 'canvas',
  685. 'caption',
  686. 'cite',
  687. 'code',
  688. 'colgroup',
  689. 'data',
  690. 'datalist',
  691. 'dd',
  692. 'del',
  693. 'details',
  694. 'dfn',
  695. 'dialog',
  696. 'div',
  697. 'dl',
  698. 'dt',
  699. 'em',
  700. 'fieldset',
  701. 'figcaption',
  702. 'figure',
  703. 'footer',
  704. 'form',
  705. 'h1',
  706. 'h2',
  707. 'h3',
  708. 'h4',
  709. 'h5',
  710. 'h6',
  711. 'head',
  712. 'header',
  713. 'hgroup',
  714. 'html',
  715. 'i',
  716. 'iframe',
  717. 'ins',
  718. 'kbd',
  719. 'label',
  720. 'legend',
  721. 'li',
  722. 'main',
  723. 'map',
  724. 'mark',
  725. 'math',
  726. 'menu',
  727. 'meter',
  728. 'nav',
  729. 'noscript',
  730. 'object',
  731. 'ol',
  732. 'optgroup',
  733. 'option',
  734. 'output',
  735. 'p',
  736. 'picture',
  737. 'pre',
  738. 'progress',
  739. 'q',
  740. 'rb',
  741. 'rp',
  742. 'rt',
  743. 'rtc',
  744. 'ruby',
  745. 's',
  746. 'samp',
  747. 'script',
  748. 'section',
  749. 'select',
  750. 'slot',
  751. 'small',
  752. 'span',
  753. 'strong',
  754. 'style',
  755. 'sub',
  756. 'summary',
  757. 'sup',
  758. 'svg',
  759. 'table',
  760. 'tbody',
  761. 'td',
  762. 'template',
  763. 'textarea',
  764. 'tfoot',
  765. 'th',
  766. 'thead',
  767. 'time',
  768. 'title',
  769. 'tr',
  770. 'u',
  771. 'ul',
  772. 'var',
  773. 'video'
  774. ].concat(VOID_HTML_TAGS_LIST).sort();
  775. /**
  776. * Matches boolean HTML attributes in the riot tag definition.
  777. * With a long list like this, a regex is faster than `[].indexOf` in most browsers.
  778. * @const {RegExp}
  779. * @see [attributes.md](https://github.com/riot/compiler/blob/dev/doc/attributes.md)
  780. */
  781. const BOOLEAN_ATTRIBUTES_LIST = [
  782. 'disabled',
  783. 'visible',
  784. 'checked',
  785. 'readonly',
  786. 'required',
  787. 'allowfullscreen',
  788. 'autofocus',
  789. 'autoplay',
  790. 'compact',
  791. 'controls',
  792. 'default',
  793. 'formnovalidate',
  794. 'hidden',
  795. 'ismap',
  796. 'itemscope',
  797. 'loop',
  798. 'multiple',
  799. 'muted',
  800. 'noresize',
  801. 'noshade',
  802. 'novalidate',
  803. 'nowrap',
  804. 'open',
  805. 'reversed',
  806. 'seamless',
  807. 'selected',
  808. 'sortable',
  809. 'truespeed',
  810. 'typemustmatch'
  811. ];
  812. /**
  813. * Join a list of items with the pipe symbol (usefull for regex list concatenation)
  814. * @private
  815. * @param {Array} list - list of strings
  816. * @returns {String} the list received joined with pipes
  817. */
  818. function joinWithPipe(list) {
  819. return list.join('|')
  820. }
  821. /**
  822. * Convert list of strings to regex in order to test against it ignoring the cases
  823. * @private
  824. * @param {...Array} lists - array of strings
  825. * @returns {RegExp} regex that will match all the strings in the array received ignoring the cases
  826. */
  827. function listsToRegex(...lists) {
  828. return new RegExp(`^/?(?:${joinWithPipe(lists.map(joinWithPipe))})$`, 'i')
  829. }
  830. /**
  831. * Regex matching all the html tags ignoring the cases
  832. * @const {RegExp}
  833. */
  834. const HTML_TAGS_RE = listsToRegex(HTML_TAGS_LIST);
  835. /**
  836. * Regex matching all the svg tags ignoring the cases
  837. * @const {RegExp}
  838. */
  839. const SVG_TAGS_RE = listsToRegex(SVG_TAGS_LIST);
  840. /**
  841. * Regex matching all the void html tags ignoring the cases
  842. * @const {RegExp}
  843. */
  844. const VOID_HTML_TAGS_RE = listsToRegex(VOID_HTML_TAGS_LIST);
  845. /**
  846. * Regex matching all the void svg tags ignoring the cases
  847. * @const {RegExp}
  848. */
  849. const VOID_SVG_TAGS_RE = listsToRegex(VOID_SVG_TAGS_LIST);
  850. /**
  851. * Regex matching all the boolean attributes
  852. * @const {RegExp}
  853. */
  854. const BOOLEAN_ATTRIBUTES_RE = listsToRegex(BOOLEAN_ATTRIBUTES_LIST);
  855. /**
  856. * True if it's a self closing tag
  857. * @param {String} tag - test tag
  858. * @returns {Boolean}
  859. * @example
  860. * isVoid('meta') // true
  861. * isVoid('circle') // true
  862. * isVoid('IMG') // true
  863. * isVoid('div') // false
  864. * isVoid('mask') // false
  865. */
  866. function isVoid(tag) {
  867. return [
  868. VOID_HTML_TAGS_RE,
  869. VOID_SVG_TAGS_RE
  870. ].some(r => r.test(tag))
  871. }
  872. /**
  873. * True if it's not SVG nor a HTML known tag
  874. * @param {String} tag - test tag
  875. * @returns {Boolean}
  876. * @example
  877. * isCustom('my-component') // true
  878. * isCustom('div') // false
  879. */
  880. function isCustom(tag) {
  881. return [
  882. HTML_TAGS_RE,
  883. SVG_TAGS_RE
  884. ].every(l => !l.test(tag))
  885. }
  886. /**
  887. * True if it's a boolean attribute
  888. * @param {String} attribute - test attribute
  889. * @returns {Boolean}
  890. * @example
  891. * isBoolAttribute('selected') // true
  892. * isBoolAttribute('class') // false
  893. */
  894. function isBoolAttribute(attribute) {
  895. return BOOLEAN_ATTRIBUTES_RE.test(attribute)
  896. }
  897. /**
  898. * Memoization function
  899. * @param {Function} fn - function to memoize
  900. * @returns {*} return of the function to memoize
  901. */
  902. function memoize(fn) {
  903. const cache = new WeakMap();
  904. return (...args) => {
  905. if (cache.has(args[0])) return cache.get(args[0])
  906. const ret = fn(...args);
  907. cache.set(args[0], ret);
  908. return ret
  909. }
  910. }
  911. const expressionsContentRe = memoize(brackets => RegExp(`(${brackets[0]}[^${brackets[1]}]*?${brackets[1]})`, 'g'));
  912. const isSpreadAttribute = name => SPREAD_OPERATOR.test(name);
  913. const isAttributeExpression = (name, brackets) => name[0] === brackets[0];
  914. const getAttributeEnd = (state, attr) => expr(state, attr, '[>/\\s]', attr.start);
  915. /**
  916. * The more complex parsing is for attributes as it can contain quoted or
  917. * unquoted values or expressions.
  918. *
  919. * @param {ParserStore} state - Parser state
  920. * @returns {number} New parser mode.
  921. * @private
  922. */
  923. function attr(state) {
  924. const { data, last, pos, root } = state;
  925. const tag = last; // the last (current) tag in the output
  926. const _CH = /\S/g; // matches the first non-space char
  927. const ch = execFromPos(_CH, pos, data);
  928. switch (true) {
  929. case !ch:
  930. state.pos = data.length; // reaching the end of the buffer with
  931. // NodeTypes.ATTR will generate error
  932. break
  933. case ch[0] === '>':
  934. // closing char found. If this is a self-closing tag with the name of the
  935. // Root tag, we need decrement the counter as we are changing mode.
  936. state.pos = tag.end = _CH.lastIndex;
  937. if (tag[IS_SELF_CLOSING]) {
  938. state.scryle = null; // allow selfClosing script/style tags
  939. if (root && root.name === tag.name) {
  940. state.count--; // "pop" root tag
  941. }
  942. }
  943. return TEXT
  944. case ch[0] === '/':
  945. state.pos = _CH.lastIndex; // maybe. delegate the validation
  946. tag[IS_SELF_CLOSING] = true; // the next loop
  947. break
  948. default:
  949. delete tag[IS_SELF_CLOSING]; // ensure unmark as selfclosing tag
  950. setAttribute(state, ch.index, tag);
  951. }
  952. return ATTR
  953. }
  954. /**
  955. * Parses an attribute and its expressions.
  956. *
  957. * @param {ParserStore} state - Parser state
  958. * @param {number} pos - Starting position of the attribute
  959. * @param {Object} tag - Current parent tag
  960. * @returns {undefined} void function
  961. * @private
  962. */
  963. function setAttribute(state, pos, tag) {
  964. const { data } = state;
  965. const expressionContent = expressionsContentRe(state.options.brackets);
  966. const re = ATTR_START; // (\S[^>/=\s]*)(?:\s*=\s*([^>/])?)? g
  967. const start = re.lastIndex = expressionContent.lastIndex = pos; // first non-whitespace
  968. const attrMatches = re.exec(data);
  969. const isExpressionName = isAttributeExpression(attrMatches[1], state.options.brackets);
  970. const match = isExpressionName ? [null, expressionContent.exec(data)[1], null] : attrMatches;
  971. if (match) {
  972. const end = re.lastIndex;
  973. const attr = parseAttribute(state, match, start, end, isExpressionName);
  974. //assert(q && q.type === Mode.TAG, 'no previous tag for the attr!')
  975. // Pushes the attribute and shifts the `end` position of the tag (`last`).
  976. state.pos = tag.end = attr.end;
  977. tag.attributes = addToCollection(tag.attributes, attr);
  978. }
  979. }
  980. function parseNomalAttribute(state, attr, quote) {
  981. const { data } = state;
  982. let { end } = attr;
  983. if (isBoolAttribute(attr.name)) {
  984. attr[IS_BOOLEAN] = true;
  985. }
  986. // parse the whole value (if any) and get any expressions on it
  987. if (quote) {
  988. // Usually, the value's first char (`quote`) is a quote and the lastIndex
  989. // (`end`) is the start of the value.
  990. let valueStart = end;
  991. // If it not, this is an unquoted value and we need adjust the start.
  992. if (quote !== '"' && quote !== '\'') {
  993. quote = ''; // first char of value is not a quote
  994. valueStart--; // adjust the starting position
  995. }
  996. end = expr(state, attr, quote || '[>/\\s]', valueStart);
  997. // adjust the bounds of the value and save its content
  998. return Object.assign(attr, {
  999. value: getChunk(data, valueStart, end),
  1000. valueStart,
  1001. end: quote ? ++end : end
  1002. })
  1003. }
  1004. return attr
  1005. }
  1006. /**
  1007. * Parse expression names <a {href}>
  1008. * @param {ParserStore} state - Parser state
  1009. * @param {Object} attr - attribute object parsed
  1010. * @returns {Object} normalized attribute object
  1011. */
  1012. function parseSpreadAttribute(state, attr) {
  1013. const end = getAttributeEnd(state, attr);
  1014. return {
  1015. [IS_SPREAD]: true,
  1016. start: attr.start,
  1017. expressions: attr.expressions.map(expr => Object.assign(expr, {
  1018. text: expr.text.replace(SPREAD_OPERATOR, '').trim()
  1019. })),
  1020. end: end
  1021. }
  1022. }
  1023. /**
  1024. * Parse expression names <a {href}>
  1025. * @param {ParserStore} state - Parser state
  1026. * @param {Object} attr - attribute object parsed
  1027. * @returns {Object} normalized attribute object
  1028. */
  1029. function parseExpressionNameAttribute(state, attr) {
  1030. const end = getAttributeEnd(state, attr);
  1031. return {
  1032. start: attr.start,
  1033. name: attr.expressions[0].text.trim(),
  1034. expressions: attr.expressions,
  1035. end: end
  1036. }
  1037. }
  1038. /**
  1039. * Parse the attribute values normalising the quotes
  1040. * @param {ParserStore} state - Parser state
  1041. * @param {Array} match - results of the attributes regex
  1042. * @param {number} start - attribute start position
  1043. * @param {number} end - attribute end position
  1044. * @param {boolean} isExpressionName - true if the attribute name is an expression
  1045. * @returns {Object} attribute object
  1046. */
  1047. function parseAttribute(state, match, start, end, isExpressionName) {
  1048. const attr = {
  1049. name: match[1],
  1050. value: '',
  1051. start,
  1052. end
  1053. };
  1054. const quote = match[2]; // first letter of value or nothing
  1055. switch (true) {
  1056. case isSpreadAttribute(attr.name):
  1057. return parseSpreadAttribute(state, attr)
  1058. case isExpressionName === true:
  1059. return parseExpressionNameAttribute(state, attr)
  1060. default:
  1061. return parseNomalAttribute(state, attr, quote)
  1062. }
  1063. }
  1064. /**
  1065. * Function to curry any javascript method
  1066. * @param {Function} fn - the target function we want to curry
  1067. * @param {...[args]} acc - initial arguments
  1068. * @returns {Function|*} it will return a function until the target function
  1069. * will receive all of its arguments
  1070. */
  1071. function curry(fn, ...acc) {
  1072. return (...args) => {
  1073. args = [...acc, ...args];
  1074. return args.length < fn.length ?
  1075. curry(fn, ...args) :
  1076. fn(...args)
  1077. }
  1078. }
  1079. /**
  1080. * Parses comments in long or short form
  1081. * (any DOCTYPE & CDATA blocks are parsed as comments).
  1082. *
  1083. * @param {ParserState} state - Parser state
  1084. * @param {string} data - Buffer to parse
  1085. * @param {number} start - Position of the '<!' sequence
  1086. * @returns {number} node type id
  1087. * @private
  1088. */
  1089. function comment(state, data, start) {
  1090. const pos = start + 2; // skip '<!'
  1091. const str = data.substr(pos, 2) === '--' ? '-->' : '>';
  1092. const end = data.indexOf(str, pos);
  1093. if (end < 0) {
  1094. panic(data, unclosedComment, start);
  1095. }
  1096. pushComment(state, start, end + str.length);
  1097. return TEXT
  1098. }
  1099. /**
  1100. * Parse a comment.
  1101. *
  1102. * @param {ParserState} state - Current parser state
  1103. * @param {number} start - Start position of the tag
  1104. * @param {number} end - Ending position (last char of the tag)
  1105. * @returns {undefined} void function
  1106. * @private
  1107. */
  1108. function pushComment(state, start, end) {
  1109. flush(state);
  1110. state.pos = end;
  1111. if (state.options.comments === true) {
  1112. state.last = { type: COMMENT, start, end };
  1113. }
  1114. }
  1115. /**
  1116. * Pushes a new *tag* and set `last` to this, so any attributes
  1117. * will be included on this and shifts the `end`.
  1118. *
  1119. * @param {ParserState} state - Current parser state
  1120. * @param {string} name - Name of the node including any slash
  1121. * @param {number} start - Start position of the tag
  1122. * @param {number} end - Ending position (last char of the tag + 1)
  1123. * @returns {undefined} - void function
  1124. * @private
  1125. */
  1126. function pushTag(state, name, start, end) {
  1127. const root = state.root;
  1128. const last = { type: TAG, name, start, end };
  1129. if (isCustom(name)) {
  1130. last[IS_CUSTOM] = true;
  1131. }
  1132. if (isVoid(name)) {
  1133. last[IS_VOID] = true;
  1134. }
  1135. state.pos = end;
  1136. if (root) {
  1137. if (name === root.name) {
  1138. state.count++;
  1139. } else if (name === root.close) {
  1140. state.count--;
  1141. }
  1142. flush(state);
  1143. } else {
  1144. // start with root (keep ref to output)
  1145. state.root = { name: last.name, close: `/${name}` };
  1146. state.count = 1;
  1147. }
  1148. state.last = last;
  1149. }
  1150. /**
  1151. * Parse the tag following a '<' character, or delegate to other parser
  1152. * if an invalid tag name is found.
  1153. *
  1154. * @param {ParserState} state - Parser state
  1155. * @returns {number} New parser mode
  1156. * @private
  1157. */
  1158. function tag(state) {
  1159. const { pos, data } = state; // pos of the char following '<'
  1160. const start = pos - 1; // pos of '<'
  1161. const str = data.substr(pos, 2); // first two chars following '<'
  1162. switch (true) {
  1163. case str[0] === '!':
  1164. return comment(state, data, start)
  1165. case TAG_2C.test(str):
  1166. return parseTag(state, start)
  1167. default:
  1168. return pushText(state, start, pos) // pushes the '<' as text
  1169. }
  1170. }
  1171. function parseTag(state, start) {
  1172. const { data, pos } = state;
  1173. const re = TAG_NAME; // (\/?[^\s>/]+)\s*(>)? g
  1174. const match = execFromPos(re, pos, data);
  1175. const end = re.lastIndex;
  1176. const name = match[1].toLowerCase(); // $1: tag name including any '/'
  1177. // script/style block is parsed as another tag to extract attributes
  1178. if (name in RE_SCRYLE) {
  1179. state.scryle = name; // used by parseText
  1180. }
  1181. pushTag(state, name, start, end);
  1182. // only '>' can ends the tag here, the '/' is handled in parseAttribute
  1183. if (!match[2]) {
  1184. return ATTR
  1185. }
  1186. return TEXT
  1187. }
  1188. /**
  1189. * Parses regular text and script/style blocks ...scryle for short :-)
  1190. * (the content of script and style is text as well)
  1191. *
  1192. * @param {ParserState} state - Parser state
  1193. * @returns {number} New parser mode.
  1194. * @private
  1195. */
  1196. function text(state) {
  1197. const { pos, data, scryle } = state;
  1198. switch (true) {
  1199. case typeof scryle === 'string': {
  1200. const name = scryle;
  1201. const re = RE_SCRYLE[name];
  1202. const match = execFromPos(re, pos, data);
  1203. if (!match) {
  1204. panic(data, unclosedNamedBlock.replace('%1', name), pos - 1);
  1205. }
  1206. const start = match.index;
  1207. const end = re.lastIndex;
  1208. state.scryle = null; // reset the script/style flag now
  1209. // write the tag content, if any
  1210. if (start > pos) {
  1211. parseSpecialTagsContent(state, name, match);
  1212. }
  1213. // now the closing tag, either </script> or </style>
  1214. pushTag(state, `/${name}`, start, end);
  1215. break
  1216. }
  1217. case data[pos] === '<':
  1218. state.pos++;
  1219. return TAG
  1220. default:
  1221. expr(state, null, '<', pos);
  1222. }
  1223. return TEXT
  1224. }
  1225. /**
  1226. * Parse the text content depending on the name
  1227. * @param {ParserState} state - Parser state
  1228. * @param {string} name - one of the tags matched by the RE_SCRYLE regex
  1229. * @param {Array} match - result of the regex matching the content of the parsed tag
  1230. * @returns {undefined} void function
  1231. */
  1232. function parseSpecialTagsContent(state, name, match) {
  1233. const { pos } = state;
  1234. const start = match.index;
  1235. if (name === TEXTAREA_TAG) {
  1236. expr(state, null, match[0], pos);
  1237. } else {
  1238. pushText(state, pos, start);
  1239. }
  1240. }
  1241. /*---------------------------------------------------------------------
  1242. * Tree builder for the riot tag parser.
  1243. *
  1244. * The output has a root property and separate arrays for `html`, `css`,
  1245. * and `js` tags.
  1246. *
  1247. * The root tag is included as first element in the `html` array.
  1248. * Script tags marked with "defer" are included in `html` instead `js`.
  1249. *
  1250. * - Mark SVG tags
  1251. * - Mark raw tags
  1252. * - Mark void tags
  1253. * - Split prefixes from expressions
  1254. * - Unescape escaped brackets and escape EOLs and backslashes
  1255. * - Compact whitespace (option `compact`) for non-raw tags
  1256. * - Create an array `parts` for text nodes and attributes
  1257. *
  1258. * Throws on unclosed tags or closing tags without start tag.
  1259. * Selfclosing and void tags has no nodes[] property.
  1260. */
  1261. /**
  1262. * Escape the carriage return and the line feed from a string
  1263. * @param {string} string - input string
  1264. * @returns {string} output string escaped
  1265. */
  1266. function escapeReturn(string) {
  1267. return string
  1268. .replace(/\r/g, '\\r')
  1269. .replace(/\n/g, '\\n')
  1270. }
  1271. /**
  1272. * Escape double slashes in a string
  1273. * @param {string} string - input string
  1274. * @returns {string} output string escaped
  1275. */
  1276. function escapeSlashes(string) {
  1277. return string.replace(/\\/g, '\\\\')
  1278. }
  1279. /**
  1280. * Replace the multiple spaces with only one
  1281. * @param {string} string - input string
  1282. * @returns {string} string without trailing spaces
  1283. */
  1284. function cleanSpaces(string) {
  1285. return string.replace(/\s+/g, ' ')
  1286. }
  1287. const TREE_BUILDER_STRUCT = Object.seal({
  1288. get() {
  1289. const store = this.store;
  1290. // The real root tag is in store.root.nodes[0]
  1291. return {
  1292. [TEMPLATE_OUTPUT_NAME]: store.root.nodes[0],
  1293. [CSS_OUTPUT_NAME]: store[STYLE_TAG],
  1294. [JAVASCRIPT_OUTPUT_NAME]: store[JAVASCRIPT_TAG]
  1295. }
  1296. },
  1297. /**
  1298. * Process the current tag or text.
  1299. * @param {Object} node - Raw pseudo-node from the parser
  1300. * @returns {undefined} void function
  1301. */
  1302. push(node) {
  1303. const store = this.store;
  1304. switch (node.type) {
  1305. case TEXT:
  1306. this.pushText(store, node);
  1307. break
  1308. case TAG: {
  1309. const name = node.name;
  1310. const closingTagChar = '/';
  1311. const [firstChar] = name;
  1312. if (firstChar === closingTagChar && !node.isVoid) {
  1313. this.closeTag(store, node, name);
  1314. } else if (firstChar !== closingTagChar) {
  1315. this.openTag(store, node);
  1316. }
  1317. break
  1318. }
  1319. }
  1320. },
  1321. closeTag(store, node) {
  1322. const last = store.scryle || store.last;
  1323. last.end = node.end;
  1324. if (store.scryle) {
  1325. store.scryle = null;
  1326. } else {
  1327. store.last = store.stack.pop();
  1328. }
  1329. },
  1330. openTag(store, node) {
  1331. const name = node.name;
  1332. const attrs = node.attributes;
  1333. if ([JAVASCRIPT_TAG, STYLE_TAG].includes(name)) {
  1334. // Only accept one of each
  1335. if (store[name]) {
  1336. panic(this.store.data, duplicatedNamedTag.replace('%1', name), node.start);
  1337. }
  1338. store[name] = node;
  1339. store.scryle = store[name];
  1340. } else {
  1341. // store.last holds the last tag pushed in the stack and this are
  1342. // non-void, non-empty tags, so we are sure the `lastTag` here
  1343. // have a `nodes` property.
  1344. const lastTag = store.last;
  1345. const newNode = node;
  1346. lastTag.nodes.push(newNode);
  1347. if (lastTag[IS_RAW] || RAW_TAGS.test(name)) {
  1348. node[IS_RAW] = true;
  1349. }
  1350. if (!node[IS_SELF_CLOSING] && !node[IS_VOID]) {
  1351. store.stack.push(lastTag);
  1352. newNode.nodes = [];
  1353. store.last = newNode;
  1354. }
  1355. }
  1356. if (attrs) {
  1357. this.attrs(attrs);
  1358. }
  1359. },
  1360. attrs(attributes) {
  1361. attributes.forEach(attr => {
  1362. if (attr.value) {
  1363. this.split(attr, attr.value, attr.valueStart, true);
  1364. }
  1365. });
  1366. },
  1367. pushText(store, node) {
  1368. const text = node.text;
  1369. const empty = !/\S/.test(text);
  1370. const scryle = store.scryle;
  1371. if (!scryle) {
  1372. // store.last always have a nodes property
  1373. const parent = store.last;
  1374. const pack = this.compact && !parent[IS_RAW];
  1375. if (pack && empty) {
  1376. return
  1377. }
  1378. this.split(node, text, node.start, pack);
  1379. parent.nodes.push(node);
  1380. } else if (!empty) {
  1381. scryle.text = node;
  1382. }
  1383. },
  1384. split(node, source, start, pack) {
  1385. const expressions = node.expressions;
  1386. const parts = [];
  1387. if (expressions) {
  1388. let pos = 0;
  1389. expressions.forEach(expr => {
  1390. const text = source.slice(pos, expr.start - start);
  1391. const code = expr.text;
  1392. parts.push(this.sanitise(node, text, pack), escapeReturn(escapeSlashes(code).trim()));
  1393. pos = expr.end - start;
  1394. });
  1395. if (pos < node.end) {
  1396. parts.push(this.sanitise(node, source.slice(pos), pack));
  1397. }
  1398. } else {
  1399. parts[0] = this.sanitise(node, source, pack);
  1400. }
  1401. node.parts = parts.filter(p => p); // remove the empty strings
  1402. },
  1403. // unescape escaped brackets and split prefixes of expressions
  1404. sanitise(node, text, pack) {
  1405. let rep = node.unescape;
  1406. if (rep) {
  1407. let idx = 0;
  1408. rep = `\\${rep}`;
  1409. while ((idx = text.indexOf(rep, idx)) !== -1) {
  1410. text = text.substr(0, idx) + text.substr(idx + 1);
  1411. idx++;
  1412. }
  1413. }
  1414. text = escapeSlashes(text);
  1415. return pack ? cleanSpaces(text) : escapeReturn(text)
  1416. }
  1417. });
  1418. function createTreeBuilder(data, options) {
  1419. const root = {
  1420. type: TAG,
  1421. name: '',
  1422. start: 0,
  1423. end: 0,
  1424. nodes: []
  1425. };
  1426. return Object.assign(Object.create(TREE_BUILDER_STRUCT), {
  1427. compact: options.compact !== false,
  1428. store: {
  1429. last: root,
  1430. stack: [],
  1431. scryle: null,
  1432. root,
  1433. style: null,
  1434. script: null,
  1435. data
  1436. }
  1437. })
  1438. }
  1439. /**
  1440. * Factory for the Parser class, exposing only the `parse` method.
  1441. * The export adds the Parser class as property.
  1442. *
  1443. * @param {Object} options - User Options
  1444. * @param {Function} customBuilder - Tree builder factory
  1445. * @returns {Function} Public Parser implementation.
  1446. */
  1447. function parser(options, customBuilder) {
  1448. const state = curry(createParserState)(options, customBuilder || createTreeBuilder);
  1449. return {
  1450. parse: (data) => parse(state(data))
  1451. }
  1452. }
  1453. /**
  1454. * Create a new state object
  1455. * @param {Object} userOptions - parser options
  1456. * @param {Function} builder - Tree builder factory
  1457. * @param {string} data - data to parse
  1458. * @returns {ParserState} it represents the current parser state
  1459. */
  1460. function createParserState(userOptions, builder, data) {
  1461. const options = Object.assign({
  1462. brackets: ['{', '}']
  1463. }, userOptions);
  1464. return {
  1465. options,
  1466. regexCache: {},
  1467. pos: 0,
  1468. count: -1,
  1469. root: null,
  1470. last: null,
  1471. scryle: null,
  1472. builder: builder(data, options),
  1473. data
  1474. }
  1475. }
  1476. /**
  1477. * It creates a raw output of pseudo-nodes with one of three different types,
  1478. * all of them having a start/end position:
  1479. *
  1480. * - TAG -- Opening or closing tags
  1481. * - TEXT -- Raw text
  1482. * - COMMENT -- Comments
  1483. *
  1484. * @param {ParserState} state - Current parser state
  1485. * @returns {ParserResult} Result, contains data and output properties.
  1486. */
  1487. function parse(state) {
  1488. const { data } = state;
  1489. walk(state);
  1490. flush(state);
  1491. if (state.count) {
  1492. panic(data, state.count > 0 ? unexpectedEndOfFile : rootTagNotFound, state.pos);
  1493. }
  1494. return {
  1495. data,
  1496. output: state.builder.get()
  1497. }
  1498. }
  1499. /**
  1500. * Parser walking recursive function
  1501. * @param {ParserState} state - Current parser state
  1502. * @param {string} type - current parsing context
  1503. * @returns {undefined} void function
  1504. */
  1505. function walk(state, type) {
  1506. const { data } = state;
  1507. // extend the state adding the tree builder instance and the initial data
  1508. const length = data.length;
  1509. // The "count" property is set to 1 when the first tag is found.
  1510. // This becomes the root and precedent text or comments are discarded.
  1511. // So, at the end of the parsing count must be zero.
  1512. if (state.pos < length && state.count) {
  1513. walk(state, eat(state, type));
  1514. }
  1515. }
  1516. /**
  1517. * Function to help iterating on the current parser state
  1518. * @param {ParserState} state - Current parser state
  1519. * @param {string} type - current parsing context
  1520. * @returns {string} parsing context
  1521. */
  1522. function eat(state, type) {
  1523. switch (type) {
  1524. case TAG:
  1525. return tag(state)
  1526. case ATTR:
  1527. return attr(state)
  1528. default:
  1529. return text(state)
  1530. }
  1531. }
  1532. /**
  1533. * The nodeTypes definition
  1534. */
  1535. const nodeTypes = types;
  1536. exports.default = parser;
  1537. exports.nodeTypes = nodeTypes;