@riotjs/parser
Advanced tools
Comparing version 0.7.0 to 0.8.0
# Changes for riot-parser | ||
### v0.8.0 | ||
- Add support for the spread attributes `<a {...foo.bar}>` | ||
- Fixed the `isCustom` boolean that will be added also to the root nodes | ||
### v0.6.9 | ||
@@ -4,0 +8,0 @@ - Remove the unecessary PUBLIC_JAVASCRIPT and PRIVATE_JAVASCRIPT nodes |
1673
index.js
@@ -31,42 +31,2 @@ 'use strict'; | ||
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}` | ||
} | ||
/** | ||
* 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} msg - Error message | ||
* @param {pos} [number] - Position of the error | ||
*/ | ||
function panic(data, msg, pos) { | ||
const message = formatError(data, msg, pos); | ||
throw new Error(message) | ||
} | ||
/** | ||
* Outputs the last parsed node. Can be used with a builder too. | ||
* | ||
* @param {ParserStore} store - Parsing store | ||
* @private | ||
*/ | ||
function flush(store) { | ||
const last = store.last; | ||
store.last = null; | ||
if (last && store.root) { | ||
store.builder.push(last); | ||
} | ||
} | ||
const rootTagNotFound = 'Root tag not found.'; | ||
@@ -101,3 +61,10 @@ const unclosedTemplateLiteral = 'Unclosed ES6 template literal.'; | ||
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. | ||
@@ -132,250 +99,507 @@ * Used by parseText fo find the end of the block. | ||
const IS_CUSTOM = 'isCustom'; | ||
const IS_SPREAD = 'isSpread'; | ||
/*--------------------------------------------------------------------- | ||
* Tree builder for the riot tag parser. | ||
/** | ||
* 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. | ||
* | ||
* 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. | ||
* @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 = '`'; | ||
/** | ||
* Escape the carriage return and the line feed from a string | ||
* @param {string} string - input string | ||
* @returns {string} output string escaped | ||
* 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 escapeReturn(string) { | ||
return string | ||
.replace(/\r/g, '\\r') | ||
.replace(/\n/g, '\\n') | ||
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) | ||
} | ||
/** | ||
* Escape double slashes in a string | ||
* @param {string} string - input string | ||
* @returns {string} output string escaped | ||
* 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 escapeSlashes(string) { | ||
return string.replace(/\\/g, '\\\\') | ||
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; | ||
/** | ||
* Replace the multiple spaces with only one | ||
* @param {string} string - input string | ||
* @returns {string} string without trailing spaces | ||
* 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 cleanSpaces(string) { | ||
return string.replace(/\s+/g, ' ') | ||
function _prev(code, pos) { | ||
while (--pos >= 0 && /\s/.test(code[pos])); | ||
return pos | ||
} | ||
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 | ||
*/ | ||
push(node) { | ||
const store = this.store; | ||
switch (node.type) { | ||
case TEXT: | ||
this.pushText(store, node); | ||
break | ||
case TAG: { | ||
const name = node.name; | ||
if (name[0] === '/') { | ||
this.closeTag(store, node, name); | ||
} else { | ||
this.openTag(store, node); | ||
} | ||
break | ||
} | ||
} | ||
}, | ||
closeTag(store, node) { | ||
const last = store.scryle || store.last; | ||
/** | ||
* 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++; | ||
last.end = node.end; | ||
// `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 (store.scryle) { | ||
store.scryle = null; | ||
} else { | ||
store.last = store.stack.pop(); | ||
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 | ||
} | ||
}, | ||
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); | ||
// 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; | ||
} | ||
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 (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 (!node[IS_SELF_CLOSING] && !node[IS_VOID]) { | ||
store.stack.push(lastTag); | ||
newNode.nodes = []; | ||
store.last = newNode; | ||
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; | ||
} | ||
} | ||
} | ||
} | ||
if (attrs) { | ||
this.attrs(attrs); | ||
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}`; | ||
} | ||
}, | ||
attrs(attributes) { | ||
for (let i = 0; i < attributes.length; i++) { | ||
const attr = attributes[i]; | ||
if (attr.value) { | ||
this.split(attr, attr.value, attr.valueStart, true); | ||
} | ||
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); | ||
} | ||
}, | ||
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; | ||
for (let i = 0; i < expressions.length; i++) { | ||
const expr = expressions[i]; | ||
const text = source.slice(pos, expr.start - start); | ||
let code = expr.text; | ||
parts.push(this.sanitise(node, text, pack), escapeReturn(escapeSlashes(code).trim())); | ||
pos = expr.end - start; | ||
} | ||
if ((pos += start) < node.end) { | ||
parts.push(this.sanitise(node, source.slice(pos), pack)); | ||
} | ||
} else { | ||
parts[0] = this.sanitise(node, source, pack); | ||
if (char === '}' && stack[stack.length - 1] === $_ES6_BQ) { | ||
char = stack.pop(); | ||
} | ||
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++; | ||
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 | ||
} | ||
} | ||
text = escapeSlashes(text); | ||
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; | ||
} | ||
return pack ? cleanSpaces(text) : escapeReturn(text) | ||
if (stack.length) { | ||
panic(code, unclosedExpression, end); | ||
} | ||
}); | ||
} | ||
function createTreeBuilder(data, options) { | ||
const root = { | ||
type: TAG, | ||
name: '', | ||
start: 0, | ||
end: 0, | ||
nodes: [] | ||
}; | ||
/** | ||
* 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); | ||
} | ||
} | ||
return Object.assign(Object.create(TREE_BUILDER_STRUCT), { | ||
compact: options.compact !== false, | ||
store: { | ||
last: root, | ||
stack: [], | ||
scryle: null, | ||
root, | ||
style: null, | ||
script: null, | ||
data | ||
/** | ||
* 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 | ||
} | ||
/** | ||
* 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 | ||
* 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 curry(fn, ...acc) { | ||
return (...args) => { | ||
args = [...acc, ...args]; | ||
function parseExpressions(state, re) { | ||
const { data, options } = state; | ||
const { brackets } = options; | ||
const expressions = []; | ||
let unescape, pos, match; | ||
return args.length < fn.length ? | ||
curry(fn, ...args) : | ||
fn(...args) | ||
// 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 | ||
} | ||
} | ||
/** | ||
* 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 | ||
* 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 execFromPos(re, pos, string) { | ||
re.lastIndex = pos; | ||
return re.exec(string) | ||
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] | ||
} | ||
@@ -768,89 +992,153 @@ | ||
/** | ||
* Pushes a new *tag* and set `last` to this, so any attributes | ||
* will be included on this and shifts the `end`. | ||
* The more complex parsing is for attributes as it can contain quoted or | ||
* unquoted values or expressions. | ||
* | ||
* @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) | ||
* @param {ParserStore} state - Parser state | ||
* @returns {number} New parser mode. | ||
* @private | ||
*/ | ||
function pushTag(state, name, start, end) { | ||
const root = state.root; | ||
const last = { type: TAG, name, start, end }; | ||
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); | ||
if (isCustom(name) && !root) { | ||
last[IS_CUSTOM] = true; | ||
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); | ||
} | ||
if (isVoid(name)) { | ||
last[IS_VOID] = true; | ||
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 re = ATTR_START; // (\S[^>/=\s]*)(?:\s*=\s*([^>/])?)? g | ||
const start = re.lastIndex = pos; // first non-whitespace | ||
const match = re.exec(data); | ||
if (match) { | ||
const end = re.lastIndex; | ||
const attr = parseAttribute(state, match, start, end); | ||
//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); | ||
} | ||
} | ||
state.pos = end; | ||
function parseNomalAttribute(state, attr, quote) { | ||
const { data } = state; | ||
let { end } = attr; | ||
if (root) { | ||
if (name === root.name) { | ||
state.count++; | ||
} else if (name === root.close) { | ||
state.count--; | ||
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 | ||
} | ||
flush(state); | ||
} else { | ||
// start with root (keep ref to output) | ||
state.root = { name: last.name, close: `/${name}` }; | ||
state.count = 1; | ||
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 | ||
}) | ||
} | ||
state.last = last; | ||
return attr | ||
} | ||
/** | ||
* 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) | ||
function parseSpreadAttribute(state, attr, quote) { | ||
let end = expr(state, attr, quote || '[>/\\s]', attr.start); | ||
return { | ||
[IS_SPREAD]: true, | ||
start: attr.start, | ||
expressions: attr.expressions.map(expr$$1 => Object.assign(expr$$1, { | ||
text: expr$$1.text.replace(SPREAD_OPERATOR, '') | ||
})), | ||
end: quote ? ++end : 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 | ||
* @private | ||
* 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 | ||
* @returns {Object} attribute object | ||
*/ | ||
function pushText(state, start, end, extra = {}) { | ||
const text = getChunk(state.data, start, end); | ||
const expressions = extra.expressions; | ||
const unescape = extra.unescape; | ||
function parseAttribute(state, match, start, end) { | ||
const attr = { | ||
name: match[1], | ||
value: '', | ||
start, | ||
end | ||
}; | ||
const quote = match[2]; // first letter of value or nothing | ||
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 (SPREAD_OPERATOR.test(attr.name)) { | ||
return parseSpreadAttribute(state, attr, quote) | ||
} | ||
if (expressions && expressions.length) { | ||
q.expressions = (q.expressions || []).concat(expressions); | ||
} | ||
return parseNomalAttribute(state, attr, quote) | ||
} | ||
if (unescape) { | ||
q.unescape = unescape; | ||
/** | ||
* 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) | ||
} | ||
return TEXT | ||
} | ||
@@ -862,5 +1150,6 @@ | ||
* | ||
* @param {ParserState} state - Parser state | ||
* @param {string} data - Buffer to parse | ||
* @param {number} start - Position of the '<!' sequence | ||
* @param {ParserState} state - Parser state | ||
* @param {string} data - Buffer to parse | ||
* @param {number} start - Position of the '<!' sequence | ||
* @returns {number} node type id | ||
* @private | ||
@@ -883,5 +1172,6 @@ */ | ||
* | ||
* @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 {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 | ||
@@ -898,2 +1188,43 @@ */ | ||
/** | ||
* 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 | ||
@@ -941,577 +1272,284 @@ * if an invalid tag name is found. | ||
// 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`. | ||
* Parses regular text and script/style blocks ...scryle for short :-) | ||
* (the content of script and style is text as well) | ||
* | ||
* @param {string} code - Buffer to search | ||
* @param {number} pos - Starting position | ||
* @returns {number} Position of the first non-blank character to the left. | ||
* @param {ParserState} state - Parser state | ||
* @returns {number} New parser mode. | ||
* @private | ||
*/ | ||
function _prev(code, pos) { | ||
while (--pos >= 0 && /\s/.test(code[pos])); | ||
return pos | ||
} | ||
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); | ||
/** | ||
* 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 | ||
if (!match) { | ||
panic(data, unclosedNamedBlock.replace('%1', name), pos - 1); | ||
} | ||
// 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; | ||
} | ||
} | ||
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 start | ||
return TEXT | ||
} | ||
/** | ||
* Escape special characters in a given string, in preparation to create a regex. | ||
* | ||
* @param {string} str - Raw string | ||
* @returns {string} Escaped string. | ||
* 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 | ||
*/ | ||
var escapeStr = (str) => str.replace(/(?=[-[\](){^*+?.$|\\])/g, '\\'); | ||
function parseSpecialTagsContent(state, name, match) { | ||
const { pos } = state; | ||
const start = match.index; | ||
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 | ||
if (name === TEXTAREA_TAG) { | ||
expr(state, null, match[0], pos); | ||
} else { | ||
pushText(state, pos, start); | ||
} | ||
throw formatError(code, unclosedTemplateLiteral, pos) | ||
} | ||
/* | ||
* 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). | ||
/*--------------------------------------------------------------------- | ||
* Tree builder for the riot tag parser. | ||
* | ||
* @const | ||
* @private | ||
*/ | ||
const S_STRING = `${S_SQ_STR}|${S_SQ_STR.replace(/'/g, '"')}`; | ||
/** | ||
* Regex cache | ||
* The output has a root property and separate arrays for `html`, `css`, | ||
* and `js` tags. | ||
* | ||
* @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. | ||
* The root tag is included as first element in the `html` array. | ||
* Script tags marked with "defer" are included in `html` instead `js`. | ||
* | ||
* @param {string} b - Closing brackets | ||
* @returns {RegExp} | ||
* - 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. | ||
*/ | ||
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 | ||
* Escape the carriage return and the line feed from a string | ||
* @param {string} string - input string | ||
* @returns {string} output string escaped | ||
*/ | ||
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 } | ||
function escapeReturn(string) { | ||
return string | ||
.replace(/\r/g, '\\r') | ||
.replace(/\n/g, '\\n') | ||
} | ||
/** | ||
* 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)) { | ||
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); | ||
} | ||
* Escape double slashes in a string | ||
* @param {string} string - input string | ||
* @returns {string} output string escaped | ||
*/ | ||
function escapeSlashes(string) { | ||
return string.replace(/\\/g, '\\\\') | ||
} | ||
/** | ||
* 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} pos - Starting position | ||
* @returns {number} Ending position | ||
* @private | ||
* Replace the multiple spaces with only one | ||
* @param {string} string - input string | ||
* @returns {string} string without trailing spaces | ||
*/ | ||
function expr(state, node, endingChars, start) { | ||
const re = b0re(state, endingChars); | ||
function cleanSpaces(string) { | ||
return string.replace(/\s+/g, ' ') | ||
} | ||
re.lastIndex = start; // reset re position | ||
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] | ||
} | ||
}, | ||
const { unescape, expressions, end } = parseExpressions(state, re); | ||
/** | ||
* 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; | ||
if (node) { | ||
if (unescape) { | ||
node.unescape = unescape; | ||
switch (node.type) { | ||
case TEXT: | ||
this.pushText(store, node); | ||
break | ||
case TAG: { | ||
const name = node.name; | ||
if (name[0] === '/') { | ||
this.closeTag(store, node, name); | ||
} else { | ||
this.openTag(store, node); | ||
} | ||
break | ||
} | ||
if (expressions.length) { | ||
node.expressions = expressions; | ||
} | ||
} else { | ||
pushText(state, start, end, {expressions, unescape}); | ||
} | ||
}, | ||
closeTag(store, node) { | ||
const last = store.scryle || store.last; | ||
return end | ||
} | ||
last.end = node.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 | ||
if (store.scryle) { | ||
store.scryle = null; | ||
} else { | ||
const tmpExpr = exprExtr(data, pos, brackets); | ||
if (tmpExpr) { | ||
expressions.push(tmpExpr); | ||
re.lastIndex = tmpExpr.end; | ||
} | ||
store.last = store.stack.pop(); | ||
} | ||
} | ||
}, | ||
// Even for text, the parser needs match a closing char | ||
if (!match) { | ||
panic(data, unexpectedEndOfFile, pos); | ||
} | ||
openTag(store, node) { | ||
const name = node.name; | ||
const attrs = node.attributes; | ||
return { | ||
unescape, | ||
expressions, | ||
end: match.index | ||
} | ||
} | ||
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; | ||
/** | ||
* 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]; | ||
lastTag.nodes.push(newNode); | ||
if (re) return re | ||
if (lastTag[IS_RAW] || RAW_TAGS.test(name)) { | ||
node[IS_RAW] = true; | ||
} | ||
const b0 = escapeStr(brackets[0]); | ||
// cache the regex extending the regexCache object | ||
Object.assign(state.regexCache, { [str]: new RegExp(`(${str})|${b0}`, 'g' ) }); | ||
if (!node[IS_SELF_CLOSING] && !node[IS_VOID]) { | ||
store.stack.push(lastTag); | ||
newNode.nodes = []; | ||
store.last = newNode; | ||
} | ||
} | ||
return state.regexCache[str] | ||
} | ||
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 = []; | ||
/** | ||
* 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 | ||
} | ||
if (expressions) { | ||
let pos = 0; | ||
/** | ||
* 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); | ||
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; | ||
}); | ||
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 | ||
if ((pos += start) < node.end) { | ||
parts.push(this.sanitise(node, source.slice(pos), pack)); | ||
} | ||
} else { | ||
parts[0] = this.sanitise(node, source, pack); | ||
} | ||
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 | ||
} | ||
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++; | ||
} | ||
} | ||
/** | ||
* 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 | ||
* @private | ||
*/ | ||
function setAttribute(state, pos, tag) { | ||
const { data } = state; | ||
const re = ATTR_START; // (\S[^>/=\s]*)(?:\s*=\s*([^>/])?)? g | ||
const start = re.lastIndex = pos; // first non-whitespace | ||
const match = re.exec(data); | ||
text = escapeSlashes(text); | ||
if (match) { | ||
let end = re.lastIndex; | ||
const attr = parseAttribute(state, match, start, end); | ||
//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); | ||
return pack ? cleanSpaces(text) : escapeReturn(text) | ||
} | ||
} | ||
}); | ||
/** | ||
* 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 | ||
* @returns {object} attribute object | ||
*/ | ||
function parseAttribute(state, match, start, end) { | ||
const { data } = state; | ||
const attr = { | ||
name: match[1], | ||
value: '', | ||
start, | ||
end | ||
function createTreeBuilder(data, options) { | ||
const root = { | ||
type: TAG, | ||
name: '', | ||
start: 0, | ||
end: 0, | ||
nodes: [] | ||
}; | ||
if (isBoolAttribute(attr.name)) { | ||
attr[IS_BOOLEAN] = true; | ||
} | ||
let quote = match[2]; // first letter of value or nothing | ||
// 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 | ||
return Object.assign(Object.create(TREE_BUILDER_STRUCT), { | ||
compact: options.compact !== false, | ||
store: { | ||
last: root, | ||
stack: [], | ||
scryle: null, | ||
root, | ||
style: null, | ||
script: null, | ||
data | ||
} | ||
end = expr(state, attr, quote || '[>/\\s]', valueStart); | ||
// adjust the bounds of the value and save its content | ||
Object.assign(attr, { | ||
value: getChunk(data, valueStart, end), | ||
valueStart, | ||
end: quote ? ++end : end | ||
}); | ||
} | ||
return attr | ||
}) | ||
} | ||
/** | ||
* 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} data - Buffer to parse | ||
* @param {string} name - one of the tags matched by the RE_SCRYLE regex | ||
* @returns {array} match - result of the regex matching the content of the parsed tag | ||
*/ | ||
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); | ||
} | ||
} | ||
/** | ||
* Factory for the Parser class, exposing only the `parse` method. | ||
@@ -1533,6 +1571,6 @@ * The export adds the Parser class as property. | ||
* Create a new state object | ||
* @param {object} userOptions - parser options | ||
* @param {Function} customBuilder - Tree builder factory | ||
* @param {Object} userOptions - parser options | ||
* @param {Function} builder - Tree builder factory | ||
* @param {string} data - data to parse | ||
* @returns {ParserState} | ||
* @returns {ParserState} it represents the current parser state | ||
*/ | ||
@@ -1587,3 +1625,4 @@ function createParserState(userOptions, builder, data) { | ||
* @param {ParserState} state - Current parser state | ||
* @param {string} type - current parsing context | ||
* @param {string} type - current parsing context | ||
* @returns {undefined} void function | ||
*/ | ||
@@ -1590,0 +1629,0 @@ function walk(state, type) { |
{ | ||
"name": "@riotjs/parser", | ||
"version": "0.7.0", | ||
"version": "0.8.0", | ||
"description": "The parser for Riot tags", | ||
@@ -56,3 +56,3 @@ "main": "./index.js", | ||
"nyc": "^13.1.0", | ||
"rollup": "^0.66.6", | ||
"rollup": "^1.0.0", | ||
"rollup-plugin-node-resolve": "^3.4.0" | ||
@@ -59,0 +59,0 @@ }, |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
51114
1497
1