4956 lines
137 KiB
JavaScript
4956 lines
137 KiB
JavaScript
/* Riot Compiler, @license MIT */
|
||
import { types as types$2, parse as parse$1, print } from 'recast';
|
||
import { builtin } from 'globals/globals.json';
|
||
import { parse as parse$2 } from 'recast/parsers/typescript';
|
||
import { composeSourceMaps } from 'recast/lib/util';
|
||
import { SourceMapGenerator } from 'source-map';
|
||
import cssEscape from 'cssesc';
|
||
|
||
const TAG_LOGIC_PROPERTY = 'exports';
|
||
const TAG_CSS_PROPERTY = 'css';
|
||
const TAG_TEMPLATE_PROPERTY = 'template';
|
||
const TAG_NAME_PROPERTY = 'name';
|
||
const RIOT_MODULE_ID = 'riot';
|
||
const RIOT_INTERFACE_WRAPPER_NAME = 'RiotComponentWrapper';
|
||
const RIOT_TAG_INTERFACE_NAME = 'RiotComponent';
|
||
|
||
const JAVASCRIPT_OUTPUT_NAME = 'javascript';
|
||
const CSS_OUTPUT_NAME = 'css';
|
||
const TEMPLATE_OUTPUT_NAME = 'template';
|
||
|
||
// Tag names
|
||
const JAVASCRIPT_TAG = 'script';
|
||
const STYLE_TAG = 'style';
|
||
const TEXTAREA_TAG = 'textarea';
|
||
|
||
// Boolean attributes
|
||
const IS_RAW = 'isRaw';
|
||
const IS_SELF_CLOSING = 'isSelfClosing';
|
||
const IS_VOID = 'isVoid';
|
||
const IS_BOOLEAN = 'isBoolean';
|
||
const IS_CUSTOM = 'isCustom';
|
||
const IS_SPREAD = 'isSpread';
|
||
|
||
var c = /*#__PURE__*/Object.freeze({
|
||
__proto__: null,
|
||
JAVASCRIPT_OUTPUT_NAME: JAVASCRIPT_OUTPUT_NAME,
|
||
CSS_OUTPUT_NAME: CSS_OUTPUT_NAME,
|
||
TEMPLATE_OUTPUT_NAME: TEMPLATE_OUTPUT_NAME,
|
||
JAVASCRIPT_TAG: JAVASCRIPT_TAG,
|
||
STYLE_TAG: STYLE_TAG,
|
||
TEXTAREA_TAG: TEXTAREA_TAG,
|
||
IS_RAW: IS_RAW,
|
||
IS_SELF_CLOSING: IS_SELF_CLOSING,
|
||
IS_VOID: IS_VOID,
|
||
IS_BOOLEAN: IS_BOOLEAN,
|
||
IS_CUSTOM: IS_CUSTOM,
|
||
IS_SPREAD: IS_SPREAD
|
||
});
|
||
|
||
/**
|
||
* Not all the types are handled in this module.
|
||
*
|
||
* @enum {number}
|
||
* @readonly
|
||
*/
|
||
const TAG = 1; /* TAG */
|
||
const ATTR = 2; /* ATTR */
|
||
const TEXT = 3; /* TEXT */
|
||
const CDATA = 4; /* CDATA */
|
||
const COMMENT = 8; /* COMMENT */
|
||
const DOCUMENT = 9; /* DOCUMENT */
|
||
const DOCTYPE = 10; /* DOCTYPE */
|
||
const DOCUMENT_FRAGMENT = 11; /* DOCUMENT_FRAGMENT */
|
||
|
||
var types$1 = /*#__PURE__*/Object.freeze({
|
||
__proto__: null,
|
||
TAG: TAG,
|
||
ATTR: ATTR,
|
||
TEXT: TEXT,
|
||
CDATA: CDATA,
|
||
COMMENT: COMMENT,
|
||
DOCUMENT: DOCUMENT,
|
||
DOCTYPE: DOCTYPE,
|
||
DOCUMENT_FRAGMENT: DOCUMENT_FRAGMENT
|
||
});
|
||
|
||
const rootTagNotFound = 'Root tag not found.';
|
||
const unclosedTemplateLiteral = 'Unclosed ES6 template literal.';
|
||
const unexpectedEndOfFile = 'Unexpected end of file.';
|
||
const unclosedComment = 'Unclosed comment.';
|
||
const unclosedNamedBlock = 'Unclosed "%1" block.';
|
||
const duplicatedNamedTag = 'Multiple inline "<%1>" tags are not supported.';
|
||
const unexpectedCharInExpression = 'Unexpected character %1.';
|
||
const unclosedExpression = 'Unclosed expression.';
|
||
|
||
/**
|
||
* Matches the start of valid tags names; used with the first 2 chars after the `'<'`.
|
||
* @const
|
||
* @private
|
||
*/
|
||
const TAG_2C = /^(?:\/[a-zA-Z]|[a-zA-Z][^\s>/]?)/;
|
||
/**
|
||
* Matches valid tags names AFTER the validation with `TAG_2C`.
|
||
* $1: tag name including any `'/'`, $2: non self-closing brace (`>`) w/o attributes.
|
||
* @const
|
||
* @private
|
||
*/
|
||
const TAG_NAME = /(\/?[^\s>/]+)\s*(>)?/g;
|
||
/**
|
||
* Matches an attribute name-value pair (both can be empty).
|
||
* $1: attribute name, $2: value including any quotes.
|
||
* @const
|
||
* @private
|
||
*/
|
||
const ATTR_START = /(\S[^>/=\s]*)(?:\s*=\s*([^>/])?)?/g;
|
||
|
||
/**
|
||
* Matches the spread operator
|
||
* it will be used for the spread attributes
|
||
* @type {RegExp}
|
||
*/
|
||
const SPREAD_OPERATOR = /\.\.\./;
|
||
/**
|
||
* Matches the closing tag of a `script` and `style` block.
|
||
* Used by parseText fo find the end of the block.
|
||
* @const
|
||
* @private
|
||
*/
|
||
const RE_SCRYLE = {
|
||
script: /<\/script\s*>/gi,
|
||
style: /<\/style\s*>/gi,
|
||
textarea: /<\/textarea\s*>/gi
|
||
};
|
||
|
||
// Do not touch text content inside this tags
|
||
const RAW_TAGS = /^\/?(?:pre|textarea)$/;
|
||
|
||
/**
|
||
* Add an item into a collection, if the collection is not an array
|
||
* we create one and add the item to it
|
||
* @param {Array} collection - target collection
|
||
* @param {*} item - item to add to the collection
|
||
* @returns {Array} array containing the new item added to it
|
||
*/
|
||
function addToCollection(collection = [], item) {
|
||
collection.push(item);
|
||
return collection
|
||
}
|
||
|
||
/**
|
||
* Run RegExp.exec starting from a specific position
|
||
* @param {RegExp} re - regex
|
||
* @param {number} pos - last index position
|
||
* @param {string} string - regex target
|
||
* @returns {Array} regex result
|
||
*/
|
||
function execFromPos(re, pos, string) {
|
||
re.lastIndex = pos;
|
||
return re.exec(string)
|
||
}
|
||
|
||
/**
|
||
* Escape special characters in a given string, in preparation to create a regex.
|
||
*
|
||
* @param {string} str - Raw string
|
||
* @returns {string} Escaped string.
|
||
*/
|
||
var escapeStr = (str) => str.replace(/(?=[-[\](){^*+?.$|\\])/g, '\\');
|
||
|
||
function formatError(data, message, pos) {
|
||
if (!pos) {
|
||
pos = data.length;
|
||
}
|
||
// count unix/mac/win eols
|
||
const line = (data.slice(0, pos).match(/\r\n?|\n/g) || '').length + 1;
|
||
let col = 0;
|
||
while (--pos >= 0 && !/[\r\n]/.test(data[pos])) {
|
||
++col;
|
||
}
|
||
return `[${line},${col}]: ${message}`
|
||
}
|
||
|
||
const $_ES6_BQ = '`';
|
||
|
||
/**
|
||
* Searches the next backquote that signals the end of the ES6 Template Literal
|
||
* or the "${" sequence that starts a JS expression, skipping any escaped
|
||
* character.
|
||
*
|
||
* @param {string} code - Whole code
|
||
* @param {number} pos - The start position of the template
|
||
* @param {string[]} stack - To save nested ES6 TL count
|
||
* @returns {number} The end of the string (-1 if not found)
|
||
*/
|
||
function skipES6TL(code, pos, stack) {
|
||
// we are in the char following the backquote (`),
|
||
// find the next unescaped backquote or the sequence "${"
|
||
const re = /[`$\\]/g;
|
||
let c;
|
||
while (re.lastIndex = pos, re.exec(code)) {
|
||
pos = re.lastIndex;
|
||
c = code[pos - 1];
|
||
if (c === '`') {
|
||
return pos
|
||
}
|
||
if (c === '$' && code[pos++] === '{') {
|
||
stack.push($_ES6_BQ, '}');
|
||
return pos
|
||
}
|
||
// else this is an escaped char
|
||
}
|
||
throw formatError(code, unclosedTemplateLiteral, pos)
|
||
}
|
||
|
||
/**
|
||
* Custom error handler can be implemented replacing this method.
|
||
* The `state` object includes the buffer (`data`)
|
||
* The error position (`loc`) contains line (base 1) and col (base 0).
|
||
* @param {string} data - string containing the error
|
||
* @param {string} msg - Error message
|
||
* @param {number} pos - Position of the error
|
||
* @returns {undefined} throw an exception error
|
||
*/
|
||
function panic$1(data, msg, pos) {
|
||
const message = formatError(data, msg, pos);
|
||
throw new Error(message)
|
||
}
|
||
|
||
// forked from https://github.com/aMarCruz/skip-regex
|
||
|
||
// safe characters to precced a regex (including `=>`, `**`, and `...`)
|
||
const beforeReChars = '[{(,;:?=|&!^~>%*/';
|
||
const beforeReSign = `${beforeReChars}+-`;
|
||
|
||
// keyword that can preceed a regex (`in` is handled as special case)
|
||
const beforeReWords = [
|
||
'case',
|
||
'default',
|
||
'do',
|
||
'else',
|
||
'in',
|
||
'instanceof',
|
||
'prefix',
|
||
'return',
|
||
'typeof',
|
||
'void',
|
||
'yield'
|
||
];
|
||
|
||
// Last chars of all the beforeReWords elements to speed up the process.
|
||
const wordsEndChar = beforeReWords.reduce((s, w) => s + w.slice(-1), '');
|
||
|
||
// Matches literal regex from the start of the buffer.
|
||
// The buffer to search must not include line-endings.
|
||
const RE_LIT_REGEX = /^\/(?=[^*>/])[^[/\\]*(?:(?:\\.|\[(?:\\.|[^\]\\]*)*\])[^[\\/]*)*?\/[gimuy]*/;
|
||
|
||
// Valid characters for JavaScript variable names and literal numbers.
|
||
const RE_JS_VCHAR = /[$\w]/;
|
||
|
||
// Match dot characters that could be part of tricky regex
|
||
const RE_DOT_CHAR = /.*/g;
|
||
|
||
/**
|
||
* Searches the position of the previous non-blank character inside `code`,
|
||
* starting with `pos - 1`.
|
||
*
|
||
* @param {string} code - Buffer to search
|
||
* @param {number} pos - Starting position
|
||
* @returns {number} Position of the first non-blank character to the left.
|
||
* @private
|
||
*/
|
||
function _prev(code, pos) {
|
||
while (--pos >= 0 && /\s/.test(code[pos]));
|
||
return pos
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Check if the character in the `start` position within `code` can be a regex
|
||
* and returns the position following this regex or `start+1` if this is not
|
||
* one.
|
||
*
|
||
* NOTE: Ensure `start` points to a slash (this is not checked).
|
||
*
|
||
* @function skipRegex
|
||
* @param {string} code - Buffer to test in
|
||
* @param {number} start - Position the first slash inside `code`
|
||
* @returns {number} Position of the char following the regex.
|
||
*
|
||
*/
|
||
/* istanbul ignore next */
|
||
function skipRegex(code, start) {
|
||
let pos = RE_DOT_CHAR.lastIndex = start++;
|
||
|
||
// `exec()` will extract from the slash to the end of the line
|
||
// and the chained `match()` will match the possible regex.
|
||
const match = (RE_DOT_CHAR.exec(code) || ' ')[0].match(RE_LIT_REGEX);
|
||
|
||
if (match) {
|
||
const next = pos + match[0].length; // result comes from `re.match`
|
||
|
||
pos = _prev(code, pos);
|
||
let c = code[pos];
|
||
|
||
// start of buffer or safe prefix?
|
||
if (pos < 0 || beforeReChars.includes(c)) {
|
||
return next
|
||
}
|
||
|
||
// from here, `pos` is >= 0 and `c` is code[pos]
|
||
if (c === '.') {
|
||
// can be `...` or something silly like 5./2
|
||
if (code[pos - 1] === '.') {
|
||
start = next;
|
||
}
|
||
|
||
} else {
|
||
|
||
if (c === '+' || c === '-') {
|
||
// tricky case
|
||
if (code[--pos] !== c || // if have a single operator or
|
||
(pos = _prev(code, pos)) < 0 || // ...have `++` and no previous token
|
||
beforeReSign.includes(c = code[pos])) {
|
||
return next // ...this is a regex
|
||
}
|
||
}
|
||
|
||
if (wordsEndChar.includes(c)) { // looks like a keyword?
|
||
const end = pos + 1;
|
||
|
||
// get the complete (previous) keyword
|
||
while (--pos >= 0 && RE_JS_VCHAR.test(code[pos]));
|
||
|
||
// it is in the allowed keywords list?
|
||
if (beforeReWords.includes(code.slice(pos + 1, end))) {
|
||
start = next;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return start
|
||
}
|
||
|
||
/*
|
||
* Mini-parser for expressions.
|
||
* The main pourpose of this module is to find the end of an expression
|
||
* and return its text without the enclosing brackets.
|
||
* Does not works with comments, but supports ES6 template strings.
|
||
*/
|
||
/**
|
||
* @exports exprExtr
|
||
*/
|
||
const S_SQ_STR = /'[^'\n\r\\]*(?:\\(?:\r\n?|[\S\s])[^'\n\r\\]*)*'/.source;
|
||
/**
|
||
* Matches double quoted JS strings taking care about nested quotes
|
||
* and EOLs (escaped EOLs are Ok).
|
||
*
|
||
* @const
|
||
* @private
|
||
*/
|
||
const S_STRING = `${S_SQ_STR}|${S_SQ_STR.replace(/'/g, '"')}`;
|
||
/**
|
||
* Regex cache
|
||
*
|
||
* @type {Object.<string, RegExp>}
|
||
* @const
|
||
* @private
|
||
*/
|
||
const reBr = {};
|
||
/**
|
||
* Makes an optimal regex that matches quoted strings, brackets, backquotes
|
||
* and the closing brackets of an expression.
|
||
*
|
||
* @param {string} b - Closing brackets
|
||
* @returns {RegExp} - optimized regex
|
||
*/
|
||
function _regex(b) {
|
||
let re = reBr[b];
|
||
if (!re) {
|
||
let s = escapeStr(b);
|
||
if (b.length > 1) {
|
||
s = `${s}|[`;
|
||
} else {
|
||
s = /[{}[\]()]/.test(b) ? '[' : `[${s}`;
|
||
}
|
||
reBr[b] = re = new RegExp(`${S_STRING}|${s}\`/\\{}[\\]()]`, 'g');
|
||
}
|
||
return re
|
||
}
|
||
|
||
/**
|
||
* Update the scopes stack removing or adding closures to it
|
||
* @param {Array} stack - array stacking the expression closures
|
||
* @param {string} char - current char to add or remove from the stack
|
||
* @param {string} idx - matching index
|
||
* @param {string} code - expression code
|
||
* @returns {Object} result
|
||
* @returns {Object} result.char - either the char received or the closing braces
|
||
* @returns {Object} result.index - either a new index to skip part of the source code,
|
||
* or 0 to keep from parsing from the old position
|
||
*/
|
||
function updateStack(stack, char, idx, code) {
|
||
let index = 0;
|
||
|
||
switch (char) {
|
||
case '[':
|
||
case '(':
|
||
case '{':
|
||
stack.push(char === '[' ? ']' : char === '(' ? ')' : '}');
|
||
break
|
||
case ')':
|
||
case ']':
|
||
case '}':
|
||
if (char !== stack.pop()) {
|
||
panic$1(code, unexpectedCharInExpression.replace('%1', char), index);
|
||
}
|
||
|
||
if (char === '}' && stack[stack.length - 1] === $_ES6_BQ) {
|
||
char = stack.pop();
|
||
}
|
||
|
||
index = idx + 1;
|
||
break
|
||
case '/':
|
||
index = skipRegex(code, idx);
|
||
}
|
||
|
||
return { char, index }
|
||
}
|
||
|
||
/**
|
||
* Parses the code string searching the end of the expression.
|
||
* It skips braces, quoted strings, regexes, and ES6 template literals.
|
||
*
|
||
* @function exprExtr
|
||
* @param {string} code - Buffer to parse
|
||
* @param {number} start - Position of the opening brace
|
||
* @param {[string,string]} bp - Brackets pair
|
||
* @returns {Object} Expression's end (after the closing brace) or -1
|
||
* if it is not an expr.
|
||
*/
|
||
function exprExtr(code, start, bp) {
|
||
const [openingBraces, closingBraces] = bp;
|
||
const offset = start + openingBraces.length; // skips the opening brace
|
||
const stack = []; // expected closing braces ('`' for ES6 TL)
|
||
const re = _regex(closingBraces);
|
||
|
||
re.lastIndex = offset; // begining of the expression
|
||
|
||
let end;
|
||
let match;
|
||
|
||
while (match = re.exec(code)) { // eslint-disable-line
|
||
const idx = match.index;
|
||
const str = match[0];
|
||
end = re.lastIndex;
|
||
|
||
// end the iteration
|
||
if (str === closingBraces && !stack.length) {
|
||
return {
|
||
text: code.slice(offset, idx),
|
||
start,
|
||
end
|
||
}
|
||
}
|
||
|
||
const { char, index } = updateStack(stack, str[0], idx, code);
|
||
// update the end value depending on the new index received
|
||
end = index || end;
|
||
// update the regex last index
|
||
re.lastIndex = char === $_ES6_BQ ? skipES6TL(code, end, stack) : end;
|
||
}
|
||
|
||
if (stack.length) {
|
||
panic$1(code, unclosedExpression, end);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Outputs the last parsed node. Can be used with a builder too.
|
||
*
|
||
* @param {ParserStore} store - Parsing store
|
||
* @returns {undefined} void function
|
||
* @private
|
||
*/
|
||
function flush(store) {
|
||
const last = store.last;
|
||
store.last = null;
|
||
if (last && store.root) {
|
||
store.builder.push(last);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Get the code chunks from start and end range
|
||
* @param {string} source - source code
|
||
* @param {number} start - Start position of the chunk we want to extract
|
||
* @param {number} end - Ending position of the chunk we need
|
||
* @returns {string} chunk of code extracted from the source code received
|
||
* @private
|
||
*/
|
||
function getChunk(source, start, end) {
|
||
return source.slice(start, end)
|
||
}
|
||
|
||
/**
|
||
* states text in the last text node, or creates a new one if needed.
|
||
*
|
||
* @param {ParserState} state - Current parser state
|
||
* @param {number} start - Start position of the tag
|
||
* @param {number} end - Ending position (last char of the tag)
|
||
* @param {Object} extra - extra properties to add to the text node
|
||
* @param {RawExpr[]} extra.expressions - Found expressions
|
||
* @param {string} extra.unescape - Brackets to unescape
|
||
* @returns {undefined} - void function
|
||
* @private
|
||
*/
|
||
function pushText(state, start, end, extra = {}) {
|
||
const text = getChunk(state.data, start, end);
|
||
const expressions = extra.expressions;
|
||
const unescape = extra.unescape;
|
||
|
||
let q = state.last;
|
||
state.pos = end;
|
||
|
||
if (q && q.type === TEXT) {
|
||
q.text += text;
|
||
q.end = end;
|
||
} else {
|
||
flush(state);
|
||
state.last = q = { type: TEXT, text, start, end };
|
||
}
|
||
|
||
if (expressions && expressions.length) {
|
||
q.expressions = (q.expressions || []).concat(expressions);
|
||
}
|
||
|
||
if (unescape) {
|
||
q.unescape = unescape;
|
||
}
|
||
|
||
return TEXT
|
||
}
|
||
|
||
/**
|
||
* Find the end of the attribute value or text node
|
||
* Extract expressions.
|
||
* Detect if value have escaped brackets.
|
||
*
|
||
* @param {ParserState} state - Parser state
|
||
* @param {HasExpr} node - Node if attr, info if text
|
||
* @param {string} endingChars - Ends the value or text
|
||
* @param {number} start - Starting position
|
||
* @returns {number} Ending position
|
||
* @private
|
||
*/
|
||
function expr(state, node, endingChars, start) {
|
||
const re = b0re(state, endingChars);
|
||
|
||
re.lastIndex = start; // reset re position
|
||
|
||
const { unescape, expressions, end } = parseExpressions(state, re);
|
||
|
||
if (node) {
|
||
if (unescape) {
|
||
node.unescape = unescape;
|
||
}
|
||
if (expressions.length) {
|
||
node.expressions = expressions;
|
||
}
|
||
} else {
|
||
pushText(state, start, end, {expressions, unescape});
|
||
}
|
||
|
||
return end
|
||
}
|
||
|
||
/**
|
||
* Parse a text chunk finding all the expressions in it
|
||
* @param {ParserState} state - Parser state
|
||
* @param {RegExp} re - regex to match the expressions contents
|
||
* @returns {Object} result containing the expression found, the string to unescape and the end position
|
||
*/
|
||
function parseExpressions(state, re) {
|
||
const { data, options } = state;
|
||
const { brackets } = options;
|
||
const expressions = [];
|
||
let unescape, pos, match;
|
||
|
||
// Anything captured in $1 (closing quote or character) ends the loop...
|
||
while ((match = re.exec(data)) && !match[1]) {
|
||
// ...else, we have an opening bracket and maybe an expression.
|
||
pos = match.index;
|
||
if (data[pos - 1] === '\\') {
|
||
unescape = match[0]; // it is an escaped opening brace
|
||
} else {
|
||
const tmpExpr = exprExtr(data, pos, brackets);
|
||
if (tmpExpr) {
|
||
expressions.push(tmpExpr);
|
||
re.lastIndex = tmpExpr.end;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Even for text, the parser needs match a closing char
|
||
if (!match) {
|
||
panic$1(data, unexpectedEndOfFile, pos);
|
||
}
|
||
|
||
return {
|
||
unescape,
|
||
expressions,
|
||
end: match.index
|
||
}
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Creates a regex for the given string and the left bracket.
|
||
* The string is captured in $1.
|
||
*
|
||
* @param {ParserState} state - Parser state
|
||
* @param {string} str - String to search
|
||
* @returns {RegExp} Resulting regex.
|
||
* @private
|
||
*/
|
||
function b0re(state, str) {
|
||
const { brackets } = state.options;
|
||
const re = state.regexCache[str];
|
||
|
||
if (re) return re
|
||
|
||
const b0 = escapeStr(brackets[0]);
|
||
// cache the regex extending the regexCache object
|
||
Object.assign(state.regexCache, { [str]: new RegExp(`(${str})|${b0}`, 'g') });
|
||
|
||
return state.regexCache[str]
|
||
}
|
||
|
||
// similar to _.uniq
|
||
const uniq = l => l.filter((x, i, a) => a.indexOf(x) === i);
|
||
|
||
/**
|
||
* SVG void elements that cannot be auto-closed and shouldn't contain child nodes.
|
||
* @const {Array}
|
||
*/
|
||
const VOID_SVG_TAGS_LIST = [
|
||
'circle',
|
||
'ellipse',
|
||
'line',
|
||
'path',
|
||
'polygon',
|
||
'polyline',
|
||
'rect',
|
||
'stop',
|
||
'use'
|
||
];
|
||
|
||
/**
|
||
* List of html elements where the value attribute is allowed
|
||
* @type {Array}
|
||
*/
|
||
const HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_LIST = [
|
||
'button',
|
||
'data',
|
||
'input',
|
||
'select',
|
||
'li',
|
||
'meter',
|
||
'option',
|
||
'output',
|
||
'progress',
|
||
'textarea',
|
||
'param'
|
||
];
|
||
|
||
/**
|
||
* List of all the available svg tags
|
||
* @const {Array}
|
||
* @see {@link https://github.com/wooorm/svg-tag-names}
|
||
*/
|
||
const SVG_TAGS_LIST = uniq([
|
||
'a',
|
||
'altGlyph',
|
||
'altGlyphDef',
|
||
'altGlyphItem',
|
||
'animate',
|
||
'animateColor',
|
||
'animateMotion',
|
||
'animateTransform',
|
||
'animation',
|
||
'audio',
|
||
'canvas',
|
||
'clipPath',
|
||
'color-profile',
|
||
'cursor',
|
||
'defs',
|
||
'desc',
|
||
'discard',
|
||
'feBlend',
|
||
'feColorMatrix',
|
||
'feComponentTransfer',
|
||
'feComposite',
|
||
'feConvolveMatrix',
|
||
'feDiffuseLighting',
|
||
'feDisplacementMap',
|
||
'feDistantLight',
|
||
'feDropShadow',
|
||
'feFlood',
|
||
'feFuncA',
|
||
'feFuncB',
|
||
'feFuncG',
|
||
'feFuncR',
|
||
'feGaussianBlur',
|
||
'feImage',
|
||
'feMerge',
|
||
'feMergeNode',
|
||
'feMorphology',
|
||
'feOffset',
|
||
'fePointLight',
|
||
'feSpecularLighting',
|
||
'feSpotLight',
|
||
'feTile',
|
||
'feTurbulence',
|
||
'filter',
|
||
'font',
|
||
'font-face',
|
||
'font-face-format',
|
||
'font-face-name',
|
||
'font-face-src',
|
||
'font-face-uri',
|
||
'foreignObject',
|
||
'g',
|
||
'glyph',
|
||
'glyphRef',
|
||
'handler',
|
||
'hatch',
|
||
'hatchpath',
|
||
'hkern',
|
||
'iframe',
|
||
'image',
|
||
'linearGradient',
|
||
'listener',
|
||
'marker',
|
||
'mask',
|
||
'mesh',
|
||
'meshgradient',
|
||
'meshpatch',
|
||
'meshrow',
|
||
'metadata',
|
||
'missing-glyph',
|
||
'mpath',
|
||
'pattern',
|
||
'prefetch',
|
||
'radialGradient',
|
||
'script',
|
||
'set',
|
||
'solidColor',
|
||
'solidcolor',
|
||
'style',
|
||
'svg',
|
||
'switch',
|
||
'symbol',
|
||
'tbreak',
|
||
'text',
|
||
'textArea',
|
||
'textPath',
|
||
'title',
|
||
'tref',
|
||
'tspan',
|
||
'unknown',
|
||
'video',
|
||
'view',
|
||
'vkern'
|
||
].concat(VOID_SVG_TAGS_LIST)).sort();
|
||
|
||
/**
|
||
* HTML void elements that cannot be auto-closed and shouldn't contain child nodes.
|
||
* @type {Array}
|
||
* @see {@link http://www.w3.org/TR/html-markup/syntax.html#syntax-elements}
|
||
* @see {@link http://www.w3.org/TR/html5/syntax.html#void-elements}
|
||
*/
|
||
const VOID_HTML_TAGS_LIST = [
|
||
'area',
|
||
'base',
|
||
'br',
|
||
'col',
|
||
'embed',
|
||
'hr',
|
||
'img',
|
||
'input',
|
||
'keygen',
|
||
'link',
|
||
'menuitem',
|
||
'meta',
|
||
'param',
|
||
'source',
|
||
'track',
|
||
'wbr'
|
||
];
|
||
|
||
/**
|
||
* List of all the html tags
|
||
* @const {Array}
|
||
* @see {@link https://github.com/sindresorhus/html-tags}
|
||
*/
|
||
const HTML_TAGS_LIST = uniq([
|
||
'a',
|
||
'abbr',
|
||
'address',
|
||
'article',
|
||
'aside',
|
||
'audio',
|
||
'b',
|
||
'bdi',
|
||
'bdo',
|
||
'blockquote',
|
||
'body',
|
||
'canvas',
|
||
'caption',
|
||
'cite',
|
||
'code',
|
||
'colgroup',
|
||
'datalist',
|
||
'dd',
|
||
'del',
|
||
'details',
|
||
'dfn',
|
||
'dialog',
|
||
'div',
|
||
'dl',
|
||
'dt',
|
||
'em',
|
||
'fieldset',
|
||
'figcaption',
|
||
'figure',
|
||
'footer',
|
||
'form',
|
||
'h1',
|
||
'h2',
|
||
'h3',
|
||
'h4',
|
||
'h5',
|
||
'h6',
|
||
'head',
|
||
'header',
|
||
'hgroup',
|
||
'html',
|
||
'i',
|
||
'iframe',
|
||
'ins',
|
||
'kbd',
|
||
'label',
|
||
'legend',
|
||
'main',
|
||
'map',
|
||
'mark',
|
||
'math',
|
||
'menu',
|
||
'nav',
|
||
'noscript',
|
||
'object',
|
||
'ol',
|
||
'optgroup',
|
||
'p',
|
||
'picture',
|
||
'pre',
|
||
'q',
|
||
'rb',
|
||
'rp',
|
||
'rt',
|
||
'rtc',
|
||
'ruby',
|
||
's',
|
||
'samp',
|
||
'script',
|
||
'section',
|
||
'select',
|
||
'slot',
|
||
'small',
|
||
'span',
|
||
'strong',
|
||
'style',
|
||
'sub',
|
||
'summary',
|
||
'sup',
|
||
'svg',
|
||
'table',
|
||
'tbody',
|
||
'td',
|
||
'template',
|
||
'tfoot',
|
||
'th',
|
||
'thead',
|
||
'time',
|
||
'title',
|
||
'tr',
|
||
'u',
|
||
'ul',
|
||
'var',
|
||
'video'
|
||
]
|
||
.concat(VOID_HTML_TAGS_LIST)
|
||
.concat(HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_LIST)
|
||
).sort();
|
||
|
||
/**
|
||
* List of all boolean HTML attributes
|
||
* @const {RegExp}
|
||
* @see {@link https://www.w3.org/TR/html5/infrastructure.html#sec-boolean-attributes}
|
||
*/
|
||
const BOOLEAN_ATTRIBUTES_LIST = [
|
||
'disabled',
|
||
'visible',
|
||
'checked',
|
||
'readonly',
|
||
'required',
|
||
'allowfullscreen',
|
||
'autofocus',
|
||
'autoplay',
|
||
'compact',
|
||
'controls',
|
||
'default',
|
||
'formnovalidate',
|
||
'hidden',
|
||
'ismap',
|
||
'itemscope',
|
||
'loop',
|
||
'multiple',
|
||
'muted',
|
||
'noresize',
|
||
'noshade',
|
||
'novalidate',
|
||
'nowrap',
|
||
'open',
|
||
'reversed',
|
||
'seamless',
|
||
'selected',
|
||
'sortable',
|
||
'truespeed',
|
||
'typemustmatch'
|
||
];
|
||
|
||
/**
|
||
* Join a list of items with the pipe symbol (usefull for regex list concatenation)
|
||
* @private
|
||
* @param {Array} list - list of strings
|
||
* @returns {string} the list received joined with pipes
|
||
*/
|
||
function joinWithPipe(list) {
|
||
return list.join('|')
|
||
}
|
||
|
||
/**
|
||
* Convert list of strings to regex in order to test against it ignoring the cases
|
||
* @private
|
||
* @param {...Array} lists - array of strings
|
||
* @returns {RegExp} regex that will match all the strings in the array received ignoring the cases
|
||
*/
|
||
function listsToRegex(...lists) {
|
||
return new RegExp(`^/?(?:${joinWithPipe(lists.map(joinWithPipe))})$`, 'i')
|
||
}
|
||
|
||
/**
|
||
* Regex matching all the html tags ignoring the cases
|
||
* @const {RegExp}
|
||
*/
|
||
const HTML_TAGS_RE = listsToRegex(HTML_TAGS_LIST);
|
||
|
||
/**
|
||
* Regex matching all the svg tags ignoring the cases
|
||
* @const {RegExp}
|
||
*/
|
||
const SVG_TAGS_RE = listsToRegex(SVG_TAGS_LIST);
|
||
|
||
/**
|
||
* Regex matching all the void html tags ignoring the cases
|
||
* @const {RegExp}
|
||
*/
|
||
const VOID_HTML_TAGS_RE = listsToRegex(VOID_HTML_TAGS_LIST);
|
||
|
||
/**
|
||
* Regex matching all the void svg tags ignoring the cases
|
||
* @const {RegExp}
|
||
*/
|
||
const VOID_SVG_TAGS_RE = listsToRegex(VOID_SVG_TAGS_LIST);
|
||
|
||
/**
|
||
* Regex matching all the html tags where the value tag is allowed
|
||
* @const {RegExp}
|
||
*/
|
||
const HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_RE = listsToRegex(HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_LIST);
|
||
|
||
/**
|
||
* Regex matching all the boolean attributes
|
||
* @const {RegExp}
|
||
*/
|
||
const BOOLEAN_ATTRIBUTES_RE = listsToRegex(BOOLEAN_ATTRIBUTES_LIST);
|
||
|
||
/**
|
||
* True if it's a self closing tag
|
||
* @param {string} tag - test tag
|
||
* @returns {boolean} true if void
|
||
* @example
|
||
* isVoid('meta') // true
|
||
* isVoid('circle') // true
|
||
* isVoid('IMG') // true
|
||
* isVoid('div') // false
|
||
* isVoid('mask') // false
|
||
*/
|
||
function isVoid(tag) {
|
||
return [
|
||
VOID_HTML_TAGS_RE,
|
||
VOID_SVG_TAGS_RE
|
||
].some(r => r.test(tag))
|
||
}
|
||
|
||
/**
|
||
* True if it's not SVG nor a HTML known tag
|
||
* @param {string} tag - test tag
|
||
* @returns {boolean} true if custom element
|
||
* @example
|
||
* isCustom('my-component') // true
|
||
* isCustom('div') // false
|
||
*/
|
||
function isCustom(tag) {
|
||
return [
|
||
HTML_TAGS_RE,
|
||
SVG_TAGS_RE
|
||
].every(l => !l.test(tag))
|
||
}
|
||
|
||
/**
|
||
* True if the value attribute is allowed on this tag
|
||
* @param {string} tag - test tag
|
||
* @returns {boolean} true if the value attribute is allowed
|
||
* @example
|
||
* hasValueAttribute('input') // true
|
||
* hasValueAttribute('div') // false
|
||
*/
|
||
function hasValueAttribute(tag) {
|
||
return HTML_ELEMENTS_HAVING_VALUE_ATTRIBUTE_RE.test(tag)
|
||
}
|
||
|
||
/**
|
||
* True if it's a boolean attribute
|
||
* @param {string} attribute - test attribute
|
||
* @returns {boolean} true if the attribute is a boolean type
|
||
* @example
|
||
* isBoolAttribute('selected') // true
|
||
* isBoolAttribute('class') // false
|
||
*/
|
||
function isBoolAttribute(attribute) {
|
||
return BOOLEAN_ATTRIBUTES_RE.test(attribute)
|
||
}
|
||
|
||
/**
|
||
* Memoization function
|
||
* @param {Function} fn - function to memoize
|
||
* @returns {*} return of the function to memoize
|
||
*/
|
||
function memoize(fn) {
|
||
const cache = new WeakMap();
|
||
|
||
return (...args) => {
|
||
if (cache.has(args[0])) return cache.get(args[0])
|
||
|
||
const ret = fn(...args);
|
||
|
||
cache.set(args[0], ret);
|
||
|
||
return ret
|
||
}
|
||
}
|
||
|
||
const expressionsContentRe = memoize(brackets => RegExp(`(${brackets[0]}[^${brackets[1]}]*?${brackets[1]})`, 'g'));
|
||
const isSpreadAttribute$1 = name => SPREAD_OPERATOR.test(name);
|
||
const isAttributeExpression = (name, brackets) => name[0] === brackets[0];
|
||
const getAttributeEnd = (state, attr) => expr(state, attr, '[>/\\s]', attr.start);
|
||
|
||
/**
|
||
* The more complex parsing is for attributes as it can contain quoted or
|
||
* unquoted values or expressions.
|
||
*
|
||
* @param {ParserStore} state - Parser state
|
||
* @returns {number} New parser mode.
|
||
* @private
|
||
*/
|
||
function attr(state) {
|
||
const { data, last, pos, root } = state;
|
||
const tag = last; // the last (current) tag in the output
|
||
const _CH = /\S/g; // matches the first non-space char
|
||
const ch = execFromPos(_CH, pos, data);
|
||
|
||
switch (true) {
|
||
case !ch:
|
||
state.pos = data.length; // reaching the end of the buffer with
|
||
// NodeTypes.ATTR will generate error
|
||
break
|
||
case ch[0] === '>':
|
||
// closing char found. If this is a self-closing tag with the name of the
|
||
// Root tag, we need decrement the counter as we are changing mode.
|
||
state.pos = tag.end = _CH.lastIndex;
|
||
if (tag[IS_SELF_CLOSING]) {
|
||
state.scryle = null; // allow selfClosing script/style tags
|
||
if (root && root.name === tag.name) {
|
||
state.count--; // "pop" root tag
|
||
}
|
||
}
|
||
return TEXT
|
||
case ch[0] === '/':
|
||
state.pos = _CH.lastIndex; // maybe. delegate the validation
|
||
tag[IS_SELF_CLOSING] = true; // the next loop
|
||
break
|
||
default:
|
||
delete tag[IS_SELF_CLOSING]; // ensure unmark as selfclosing tag
|
||
setAttribute(state, ch.index, tag);
|
||
}
|
||
|
||
return ATTR
|
||
}
|
||
|
||
/**
|
||
* Parses an attribute and its expressions.
|
||
*
|
||
* @param {ParserStore} state - Parser state
|
||
* @param {number} pos - Starting position of the attribute
|
||
* @param {Object} tag - Current parent tag
|
||
* @returns {undefined} void function
|
||
* @private
|
||
*/
|
||
function setAttribute(state, pos, tag) {
|
||
const { data } = state;
|
||
const expressionContent = expressionsContentRe(state.options.brackets);
|
||
const re = ATTR_START; // (\S[^>/=\s]*)(?:\s*=\s*([^>/])?)? g
|
||
const start = re.lastIndex = expressionContent.lastIndex = pos; // first non-whitespace
|
||
const attrMatches = re.exec(data);
|
||
const isExpressionName = isAttributeExpression(attrMatches[1], state.options.brackets);
|
||
const match = isExpressionName ? [null, expressionContent.exec(data)[1], null] : attrMatches;
|
||
|
||
if (match) {
|
||
const end = re.lastIndex;
|
||
const attr = parseAttribute(state, match, start, end, isExpressionName);
|
||
|
||
//assert(q && q.type === Mode.TAG, 'no previous tag for the attr!')
|
||
// Pushes the attribute and shifts the `end` position of the tag (`last`).
|
||
state.pos = tag.end = attr.end;
|
||
tag.attributes = addToCollection(tag.attributes, attr);
|
||
}
|
||
}
|
||
|
||
function parseNomalAttribute(state, attr, quote) {
|
||
const { data } = state;
|
||
let { end } = attr;
|
||
|
||
if (isBoolAttribute(attr.name)) {
|
||
attr[IS_BOOLEAN] = true;
|
||
}
|
||
|
||
// parse the whole value (if any) and get any expressions on it
|
||
if (quote) {
|
||
// Usually, the value's first char (`quote`) is a quote and the lastIndex
|
||
// (`end`) is the start of the value.
|
||
let valueStart = end;
|
||
// If it not, this is an unquoted value and we need adjust the start.
|
||
if (quote !== '"' && quote !== '\'') {
|
||
quote = ''; // first char of value is not a quote
|
||
valueStart--; // adjust the starting position
|
||
}
|
||
|
||
end = expr(state, attr, quote || '[>/\\s]', valueStart);
|
||
|
||
// adjust the bounds of the value and save its content
|
||
return Object.assign(attr, {
|
||
value: getChunk(data, valueStart, end),
|
||
valueStart,
|
||
end: quote ? ++end : end
|
||
})
|
||
}
|
||
|
||
return attr
|
||
}
|
||
|
||
|
||
/**
|
||
* Parse expression names <a {href}>
|
||
* @param {ParserStore} state - Parser state
|
||
* @param {Object} attr - attribute object parsed
|
||
* @returns {Object} normalized attribute object
|
||
*/
|
||
function parseSpreadAttribute(state, attr) {
|
||
const end = getAttributeEnd(state, attr);
|
||
|
||
return {
|
||
[IS_SPREAD]: true,
|
||
start: attr.start,
|
||
expressions: attr.expressions.map(expr => Object.assign(expr, {
|
||
text: expr.text.replace(SPREAD_OPERATOR, '').trim()
|
||
})),
|
||
end: end
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Parse expression names <a {href}>
|
||
* @param {ParserStore} state - Parser state
|
||
* @param {Object} attr - attribute object parsed
|
||
* @returns {Object} normalized attribute object
|
||
*/
|
||
function parseExpressionNameAttribute(state, attr) {
|
||
const end = getAttributeEnd(state, attr);
|
||
|
||
return {
|
||
start: attr.start,
|
||
name: attr.expressions[0].text.trim(),
|
||
expressions: attr.expressions,
|
||
end: end
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Parse the attribute values normalising the quotes
|
||
* @param {ParserStore} state - Parser state
|
||
* @param {Array} match - results of the attributes regex
|
||
* @param {number} start - attribute start position
|
||
* @param {number} end - attribute end position
|
||
* @param {boolean} isExpressionName - true if the attribute name is an expression
|
||
* @returns {Object} attribute object
|
||
*/
|
||
function parseAttribute(state, match, start, end, isExpressionName) {
|
||
const attr = {
|
||
name: match[1],
|
||
value: '',
|
||
start,
|
||
end
|
||
};
|
||
|
||
const quote = match[2]; // first letter of value or nothing
|
||
|
||
switch (true) {
|
||
case isSpreadAttribute$1(attr.name):
|
||
return parseSpreadAttribute(state, attr)
|
||
case isExpressionName === true:
|
||
return parseExpressionNameAttribute(state, attr)
|
||
default:
|
||
return parseNomalAttribute(state, attr, quote)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Function to curry any javascript method
|
||
* @param {Function} fn - the target function we want to curry
|
||
* @param {...[args]} acc - initial arguments
|
||
* @returns {Function|*} it will return a function until the target function
|
||
* will receive all of its arguments
|
||
*/
|
||
function curry(fn, ...acc) {
|
||
return (...args) => {
|
||
args = [...acc, ...args];
|
||
|
||
return args.length < fn.length ?
|
||
curry(fn, ...args) :
|
||
fn(...args)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Parses comments in long or short form
|
||
* (any DOCTYPE & CDATA blocks are parsed as comments).
|
||
*
|
||
* @param {ParserState} state - Parser state
|
||
* @param {string} data - Buffer to parse
|
||
* @param {number} start - Position of the '<!' sequence
|
||
* @returns {number} node type id
|
||
* @private
|
||
*/
|
||
function comment(state, data, start) {
|
||
const pos = start + 2; // skip '<!'
|
||
const isLongComment = data.substr(pos, 2) === '--';
|
||
const str = isLongComment ? '-->' : '>';
|
||
const end = data.indexOf(str, pos);
|
||
|
||
if (end < 0) {
|
||
panic$1(data, unclosedComment, start);
|
||
}
|
||
|
||
pushComment(
|
||
state,
|
||
start,
|
||
end + str.length,
|
||
data.substring(start, end + str.length)
|
||
);
|
||
|
||
return TEXT
|
||
}
|
||
|
||
/**
|
||
* Parse a comment.
|
||
*
|
||
* @param {ParserState} state - Current parser state
|
||
* @param {number} start - Start position of the tag
|
||
* @param {number} end - Ending position (last char of the tag)
|
||
* @param {string} text - Comment content
|
||
* @returns {undefined} void function
|
||
* @private
|
||
*/
|
||
function pushComment(state, start, end, text) {
|
||
state.pos = end;
|
||
if (state.options.comments === true) {
|
||
flush(state);
|
||
state.last = {
|
||
type: COMMENT,
|
||
start,
|
||
end,
|
||
text
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Pushes a new *tag* and set `last` to this, so any attributes
|
||
* will be included on this and shifts the `end`.
|
||
*
|
||
* @param {ParserState} state - Current parser state
|
||
* @param {string} name - Name of the node including any slash
|
||
* @param {number} start - Start position of the tag
|
||
* @param {number} end - Ending position (last char of the tag + 1)
|
||
* @returns {undefined} - void function
|
||
* @private
|
||
*/
|
||
function pushTag(state, name, start, end) {
|
||
const root = state.root;
|
||
const last = { type: TAG, name, start, end };
|
||
|
||
if (isCustom(name)) {
|
||
last[IS_CUSTOM] = true;
|
||
}
|
||
|
||
if (isVoid(name)) {
|
||
last[IS_VOID] = true;
|
||
}
|
||
|
||
state.pos = end;
|
||
|
||
if (root) {
|
||
if (name === root.name) {
|
||
state.count++;
|
||
} else if (name === root.close) {
|
||
state.count--;
|
||
}
|
||
flush(state);
|
||
} else {
|
||
// start with root (keep ref to output)
|
||
state.root = { name: last.name, close: `/${name}` };
|
||
state.count = 1;
|
||
}
|
||
|
||
state.last = last;
|
||
}
|
||
|
||
/**
|
||
* Parse the tag following a '<' character, or delegate to other parser
|
||
* if an invalid tag name is found.
|
||
*
|
||
* @param {ParserState} state - Parser state
|
||
* @returns {number} New parser mode
|
||
* @private
|
||
*/
|
||
function tag(state) {
|
||
const { pos, data } = state; // pos of the char following '<'
|
||
const start = pos - 1; // pos of '<'
|
||
const str = data.substr(pos, 2); // first two chars following '<'
|
||
|
||
switch (true) {
|
||
case str[0] === '!':
|
||
return comment(state, data, start)
|
||
case TAG_2C.test(str):
|
||
return parseTag(state, start)
|
||
default:
|
||
return pushText(state, start, pos) // pushes the '<' as text
|
||
}
|
||
}
|
||
|
||
function parseTag(state, start) {
|
||
const { data, pos } = state;
|
||
const re = TAG_NAME; // (\/?[^\s>/]+)\s*(>)? g
|
||
const match = execFromPos(re, pos, data);
|
||
const end = re.lastIndex;
|
||
const name = match[1].toLowerCase(); // $1: tag name including any '/'
|
||
// script/style block is parsed as another tag to extract attributes
|
||
if (name in RE_SCRYLE) {
|
||
state.scryle = name; // used by parseText
|
||
}
|
||
|
||
pushTag(state, name, start, end);
|
||
// only '>' can ends the tag here, the '/' is handled in parseAttribute
|
||
if (!match[2]) {
|
||
return ATTR
|
||
}
|
||
|
||
return TEXT
|
||
}
|
||
|
||
/**
|
||
* Parses regular text and script/style blocks ...scryle for short :-)
|
||
* (the content of script and style is text as well)
|
||
*
|
||
* @param {ParserState} state - Parser state
|
||
* @returns {number} New parser mode.
|
||
* @private
|
||
*/
|
||
function text(state) {
|
||
const { pos, data, scryle } = state;
|
||
|
||
switch (true) {
|
||
case typeof scryle === 'string': {
|
||
const name = scryle;
|
||
const re = RE_SCRYLE[name];
|
||
const match = execFromPos(re, pos, data);
|
||
|
||
if (!match) {
|
||
panic$1(data, unclosedNamedBlock.replace('%1', name), pos - 1);
|
||
}
|
||
|
||
const start = match.index;
|
||
const end = re.lastIndex;
|
||
state.scryle = null; // reset the script/style flag now
|
||
// write the tag content, if any
|
||
if (start > pos) {
|
||
parseSpecialTagsContent(state, name, match);
|
||
}
|
||
// now the closing tag, either </script> or </style>
|
||
pushTag(state, `/${name}`, start, end);
|
||
break
|
||
}
|
||
case data[pos] === '<':
|
||
state.pos++;
|
||
return TAG
|
||
default:
|
||
expr(state, null, '<', pos);
|
||
}
|
||
|
||
return TEXT
|
||
}
|
||
|
||
/**
|
||
* Parse the text content depending on the name
|
||
* @param {ParserState} state - Parser state
|
||
* @param {string} name - one of the tags matched by the RE_SCRYLE regex
|
||
* @param {Array} match - result of the regex matching the content of the parsed tag
|
||
* @returns {undefined} void function
|
||
*/
|
||
function parseSpecialTagsContent(state, name, match) {
|
||
const { pos } = state;
|
||
const start = match.index;
|
||
|
||
if (name === TEXTAREA_TAG) {
|
||
expr(state, null, match[0], pos);
|
||
} else {
|
||
pushText(state, pos, start);
|
||
}
|
||
}
|
||
|
||
/*---------------------------------------------------------------------
|
||
* Tree builder for the riot tag parser.
|
||
*
|
||
* The output has a root property and separate arrays for `html`, `css`,
|
||
* and `js` tags.
|
||
*
|
||
* The root tag is included as first element in the `html` array.
|
||
* Script tags marked with "defer" are included in `html` instead `js`.
|
||
*
|
||
* - Mark SVG tags
|
||
* - Mark raw tags
|
||
* - Mark void tags
|
||
* - Split prefixes from expressions
|
||
* - Unescape escaped brackets and escape EOLs and backslashes
|
||
* - Compact whitespace (option `compact`) for non-raw tags
|
||
* - Create an array `parts` for text nodes and attributes
|
||
*
|
||
* Throws on unclosed tags or closing tags without start tag.
|
||
* Selfclosing and void tags has no nodes[] property.
|
||
*/
|
||
|
||
/**
|
||
* Escape the carriage return and the line feed from a string
|
||
* @param {string} string - input string
|
||
* @returns {string} output string escaped
|
||
*/
|
||
function escapeReturn(string) {
|
||
return string
|
||
.replace(/\r/g, '\\r')
|
||
.replace(/\n/g, '\\n')
|
||
}
|
||
|
||
// check whether a tag has the 'src' attribute set like for example `<script src="">`
|
||
const hasSrcAttribute = node => (node.attributes || []).some(attr => attr.name === 'src');
|
||
|
||
/**
|
||
* Escape double slashes in a string
|
||
* @param {string} string - input string
|
||
* @returns {string} output string escaped
|
||
*/
|
||
function escapeSlashes(string) {
|
||
return string.replace(/\\/g, '\\\\')
|
||
}
|
||
|
||
/**
|
||
* Replace the multiple spaces with only one
|
||
* @param {string} string - input string
|
||
* @returns {string} string without trailing spaces
|
||
*/
|
||
function cleanSpaces(string) {
|
||
return string.replace(/\s+/g, ' ')
|
||
}
|
||
|
||
const TREE_BUILDER_STRUCT = Object.seal({
|
||
get() {
|
||
const store = this.store;
|
||
// The real root tag is in store.root.nodes[0]
|
||
return {
|
||
[TEMPLATE_OUTPUT_NAME]: store.root.nodes[0],
|
||
[CSS_OUTPUT_NAME]: store[STYLE_TAG],
|
||
[JAVASCRIPT_OUTPUT_NAME]: store[JAVASCRIPT_TAG]
|
||
}
|
||
},
|
||
|
||
/**
|
||
* Process the current tag or text.
|
||
* @param {Object} node - Raw pseudo-node from the parser
|
||
* @returns {undefined} void function
|
||
*/
|
||
push(node) {
|
||
const store = this.store;
|
||
|
||
switch (node.type) {
|
||
case COMMENT:
|
||
this.pushComment(store, node);
|
||
break
|
||
case TEXT:
|
||
this.pushText(store, node);
|
||
break
|
||
case TAG: {
|
||
const name = node.name;
|
||
const closingTagChar = '/';
|
||
const [firstChar] = name;
|
||
|
||
if (firstChar === closingTagChar && !node.isVoid) {
|
||
this.closeTag(store, node, name);
|
||
} else if (firstChar !== closingTagChar) {
|
||
this.openTag(store, node);
|
||
}
|
||
break
|
||
}
|
||
}
|
||
},
|
||
pushComment(store, node) {
|
||
const parent = store.last;
|
||
|
||
parent.nodes.push(node);
|
||
},
|
||
closeTag(store, node) {
|
||
const last = store.scryle || store.last;
|
||
|
||
last.end = node.end;
|
||
|
||
// update always the root node end position
|
||
if (store.root.nodes[0]) store.root.nodes[0].end = node.end;
|
||
|
||
if (store.scryle) {
|
||
store.scryle = null;
|
||
} else {
|
||
store.last = store.stack.pop();
|
||
}
|
||
},
|
||
|
||
openTag(store, node) {
|
||
const name = node.name;
|
||
const attrs = node.attributes;
|
||
const isCoreTag = (JAVASCRIPT_TAG === name && !hasSrcAttribute(node) || name === STYLE_TAG);
|
||
|
||
if (isCoreTag) {
|
||
// Only accept one of each
|
||
if (store[name]) {
|
||
panic$1(this.store.data, duplicatedNamedTag.replace('%1', name), node.start);
|
||
}
|
||
|
||
store[name] = node;
|
||
store.scryle = store[name];
|
||
} else {
|
||
// store.last holds the last tag pushed in the stack and this are
|
||
// non-void, non-empty tags, so we are sure the `lastTag` here
|
||
// have a `nodes` property.
|
||
const lastTag = store.last;
|
||
const newNode = node;
|
||
|
||
lastTag.nodes.push(newNode);
|
||
|
||
if (lastTag[IS_RAW] || RAW_TAGS.test(name)) {
|
||
node[IS_RAW] = true;
|
||
}
|
||
|
||
if (!node[IS_SELF_CLOSING] && !node[IS_VOID]) {
|
||
store.stack.push(lastTag);
|
||
newNode.nodes = [];
|
||
store.last = newNode;
|
||
}
|
||
}
|
||
|
||
if (attrs) {
|
||
this.attrs(attrs);
|
||
}
|
||
},
|
||
attrs(attributes) {
|
||
attributes.forEach(attr => {
|
||
if (attr.value) {
|
||
this.split(attr, attr.value, attr.valueStart, true);
|
||
}
|
||
});
|
||
},
|
||
pushText(store, node) {
|
||
const text = node.text;
|
||
const empty = !/\S/.test(text);
|
||
const scryle = store.scryle;
|
||
if (!scryle) {
|
||
// store.last always have a nodes property
|
||
const parent = store.last;
|
||
|
||
const pack = this.compact && !parent[IS_RAW];
|
||
if (pack && empty) {
|
||
return
|
||
}
|
||
this.split(node, text, node.start, pack);
|
||
parent.nodes.push(node);
|
||
} else if (!empty) {
|
||
scryle.text = node;
|
||
}
|
||
},
|
||
split(node, source, start, pack) {
|
||
const expressions = node.expressions;
|
||
const parts = [];
|
||
|
||
if (expressions) {
|
||
let pos = 0;
|
||
|
||
expressions.forEach(expr => {
|
||
const text = source.slice(pos, expr.start - start);
|
||
const code = expr.text;
|
||
parts.push(this.sanitise(node, text, pack), escapeReturn(escapeSlashes(code).trim()));
|
||
pos = expr.end - start;
|
||
});
|
||
|
||
if (pos < node.end) {
|
||
parts.push(this.sanitise(node, source.slice(pos), pack));
|
||
}
|
||
} else {
|
||
parts[0] = this.sanitise(node, source, pack);
|
||
}
|
||
|
||
node.parts = parts.filter(p => p); // remove the empty strings
|
||
},
|
||
// unescape escaped brackets and split prefixes of expressions
|
||
sanitise(node, text, pack) {
|
||
let rep = node.unescape;
|
||
if (rep) {
|
||
let idx = 0;
|
||
rep = `\\${rep}`;
|
||
while ((idx = text.indexOf(rep, idx)) !== -1) {
|
||
text = text.substr(0, idx) + text.substr(idx + 1);
|
||
idx++;
|
||
}
|
||
}
|
||
|
||
text = escapeSlashes(text);
|
||
|
||
return pack ? cleanSpaces(text) : escapeReturn(text)
|
||
}
|
||
});
|
||
|
||
function createTreeBuilder(data, options) {
|
||
const root = {
|
||
type: TAG,
|
||
name: '',
|
||
start: 0,
|
||
end: 0,
|
||
nodes: []
|
||
};
|
||
|
||
return Object.assign(Object.create(TREE_BUILDER_STRUCT), {
|
||
compact: options.compact !== false,
|
||
store: {
|
||
last: root,
|
||
stack: [],
|
||
scryle: null,
|
||
root,
|
||
style: null,
|
||
script: null,
|
||
data
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Factory for the Parser class, exposing only the `parse` method.
|
||
* The export adds the Parser class as property.
|
||
*
|
||
* @param {Object} options - User Options
|
||
* @param {Function} customBuilder - Tree builder factory
|
||
* @returns {Function} Public Parser implementation.
|
||
*/
|
||
function parser(options, customBuilder) {
|
||
const state = curry(createParserState)(options, customBuilder || createTreeBuilder);
|
||
return {
|
||
parse: (data) => parse(state(data))
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create a new state object
|
||
* @param {Object} userOptions - parser options
|
||
* @param {Function} builder - Tree builder factory
|
||
* @param {string} data - data to parse
|
||
* @returns {ParserState} it represents the current parser state
|
||
*/
|
||
function createParserState(userOptions, builder, data) {
|
||
const options = Object.assign({
|
||
brackets: ['{', '}']
|
||
}, userOptions);
|
||
|
||
return {
|
||
options,
|
||
regexCache: {},
|
||
pos: 0,
|
||
count: -1,
|
||
root: null,
|
||
last: null,
|
||
scryle: null,
|
||
builder: builder(data, options),
|
||
data
|
||
}
|
||
}
|
||
|
||
/**
|
||
* It creates a raw output of pseudo-nodes with one of three different types,
|
||
* all of them having a start/end position:
|
||
*
|
||
* - TAG -- Opening or closing tags
|
||
* - TEXT -- Raw text
|
||
* - COMMENT -- Comments
|
||
*
|
||
* @param {ParserState} state - Current parser state
|
||
* @returns {ParserResult} Result, contains data and output properties.
|
||
*/
|
||
function parse(state) {
|
||
const { data } = state;
|
||
|
||
walk(state);
|
||
flush(state);
|
||
|
||
if (state.count) {
|
||
panic$1(data, state.count > 0 ? unexpectedEndOfFile : rootTagNotFound, state.pos);
|
||
}
|
||
|
||
return {
|
||
data,
|
||
output: state.builder.get()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Parser walking recursive function
|
||
* @param {ParserState} state - Current parser state
|
||
* @param {string} type - current parsing context
|
||
* @returns {undefined} void function
|
||
*/
|
||
function walk(state, type) {
|
||
const { data } = state;
|
||
// extend the state adding the tree builder instance and the initial data
|
||
const length = data.length;
|
||
|
||
// The "count" property is set to 1 when the first tag is found.
|
||
// This becomes the root and precedent text or comments are discarded.
|
||
// So, at the end of the parsing count must be zero.
|
||
if (state.pos < length && state.count) {
|
||
walk(state, eat(state, type));
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Function to help iterating on the current parser state
|
||
* @param {ParserState} state - Current parser state
|
||
* @param {string} type - current parsing context
|
||
* @returns {string} parsing context
|
||
*/
|
||
function eat(state, type) {
|
||
switch (type) {
|
||
case TAG:
|
||
return tag(state)
|
||
case ATTR:
|
||
return attr(state)
|
||
default:
|
||
return text(state)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Expose the internal constants
|
||
*/
|
||
const constants = c;
|
||
|
||
/**
|
||
* The nodeTypes definition
|
||
*/
|
||
const nodeTypes = types$1;
|
||
|
||
const BINDING_TYPES = 'bindingTypes';
|
||
const EACH_BINDING_TYPE = 'EACH';
|
||
const IF_BINDING_TYPE = 'IF';
|
||
const TAG_BINDING_TYPE = 'TAG';
|
||
const SLOT_BINDING_TYPE = 'SLOT';
|
||
|
||
|
||
const EXPRESSION_TYPES = 'expressionTypes';
|
||
const ATTRIBUTE_EXPRESSION_TYPE = 'ATTRIBUTE';
|
||
const VALUE_EXPRESSION_TYPE = 'VALUE';
|
||
const TEXT_EXPRESSION_TYPE = 'TEXT';
|
||
const EVENT_EXPRESSION_TYPE = 'EVENT';
|
||
|
||
const TEMPLATE_FN = 'template';
|
||
const SCOPE = '_scope';
|
||
const GET_COMPONENT_FN = 'getComponent';
|
||
|
||
// keys needed to create the DOM bindings
|
||
const BINDING_SELECTOR_KEY = 'selector';
|
||
const BINDING_GET_COMPONENT_KEY = 'getComponent';
|
||
const BINDING_TEMPLATE_KEY = 'template';
|
||
const BINDING_TYPE_KEY = 'type';
|
||
const BINDING_REDUNDANT_ATTRIBUTE_KEY = 'redundantAttribute';
|
||
const BINDING_CONDITION_KEY = 'condition';
|
||
const BINDING_ITEM_NAME_KEY = 'itemName';
|
||
const BINDING_GET_KEY_KEY = 'getKey';
|
||
const BINDING_INDEX_NAME_KEY = 'indexName';
|
||
const BINDING_EVALUATE_KEY = 'evaluate';
|
||
const BINDING_NAME_KEY = 'name';
|
||
const BINDING_SLOTS_KEY = 'slots';
|
||
const BINDING_EXPRESSIONS_KEY = 'expressions';
|
||
const BINDING_CHILD_NODE_INDEX_KEY = 'childNodeIndex';
|
||
// slots keys
|
||
const BINDING_BINDINGS_KEY = 'bindings';
|
||
const BINDING_ID_KEY = 'id';
|
||
const BINDING_HTML_KEY = 'html';
|
||
const BINDING_ATTRIBUTES_KEY = 'attributes';
|
||
|
||
// DOM directives
|
||
const IF_DIRECTIVE = 'if';
|
||
const EACH_DIRECTIVE = 'each';
|
||
const KEY_ATTRIBUTE = 'key';
|
||
const SLOT_ATTRIBUTE = 'slot';
|
||
const NAME_ATTRIBUTE = 'name';
|
||
const IS_DIRECTIVE = 'is';
|
||
|
||
// Misc
|
||
const DEFAULT_SLOT_NAME = 'default';
|
||
const TEXT_NODE_EXPRESSION_PLACEHOLDER = ' ';
|
||
const BINDING_SELECTOR_PREFIX = 'expr';
|
||
const SLOT_TAG_NODE_NAME = 'slot';
|
||
const PROGRESS_TAG_NODE_NAME = 'progress';
|
||
const TEMPLATE_TAG_NODE_NAME = 'template';
|
||
|
||
// Riot Parser constants
|
||
constants.IS_RAW;
|
||
const IS_VOID_NODE = constants.IS_VOID;
|
||
const IS_CUSTOM_NODE = constants.IS_CUSTOM;
|
||
const IS_BOOLEAN_ATTRIBUTE = constants.IS_BOOLEAN;
|
||
const IS_SPREAD_ATTRIBUTE = constants.IS_SPREAD;
|
||
|
||
const types = types$2;
|
||
const builders = types$2.builders;
|
||
const namedTypes = types$2.namedTypes;
|
||
|
||
const browserAPIs = ['window', 'document', 'console'];
|
||
const builtinAPIs = Object.keys(builtin);
|
||
|
||
const isIdentifier = n => namedTypes.Identifier.check(n);
|
||
const isLiteral = n => namedTypes.Literal.check(n);
|
||
const isExpressionStatement = n => namedTypes.ExpressionStatement.check(n);
|
||
const isThisExpression = n => namedTypes.ThisExpression.check(n);
|
||
const isObjectExpression = n => namedTypes.ObjectExpression.check(n);
|
||
const isThisExpressionStatement = n =>
|
||
isExpressionStatement(n) &&
|
||
isMemberExpression(n.expression.left) &&
|
||
isThisExpression(n.expression.left.object);
|
||
const isNewExpression = n => namedTypes.NewExpression.check(n);
|
||
const isSequenceExpression = n => namedTypes.SequenceExpression.check(n);
|
||
const isExportDefaultStatement = n => namedTypes.ExportDefaultDeclaration.check(n);
|
||
const isMemberExpression = n => namedTypes.MemberExpression.check(n);
|
||
const isImportDeclaration = n => namedTypes.ImportDeclaration.check(n);
|
||
const isTypeAliasDeclaration = n => namedTypes.TSTypeAliasDeclaration.check(n);
|
||
const isInterfaceDeclaration = n => namedTypes.TSInterfaceDeclaration.check(n);
|
||
const isExportNamedDeclaration = n => namedTypes.ExportNamedDeclaration.check(n);
|
||
|
||
const isBrowserAPI = ({name}) => browserAPIs.includes(name);
|
||
const isBuiltinAPI = ({name}) => builtinAPIs.includes(name);
|
||
const isRaw = n => n && n.raw;
|
||
|
||
/**
|
||
* Similar to compose but performs from left-to-right function composition.<br/>
|
||
* {@link https://30secondsofcode.org/function#composeright see also}
|
||
* @param {...[function]} fns) - list of unary function
|
||
* @returns {*} result of the computation
|
||
*/
|
||
|
||
/**
|
||
* Performs right-to-left function composition.<br/>
|
||
* Use Array.prototype.reduce() to perform right-to-left function composition.<br/>
|
||
* The last (rightmost) function can accept one or more arguments; the remaining functions must be unary.<br/>
|
||
* {@link https://30secondsofcode.org/function#compose original source code}
|
||
* @param {...[function]} fns) - list of unary function
|
||
* @returns {*} result of the computation
|
||
*/
|
||
function compose(...fns) {
|
||
return fns.reduce((f, g) => (...args) => f(g(...args)))
|
||
}
|
||
|
||
/**
|
||
* Quick type checking
|
||
* @param {*} element - anything
|
||
* @param {string} type - type definition
|
||
* @returns {boolean} true if the type corresponds
|
||
*/
|
||
|
||
/**
|
||
* Check if a value is an Object
|
||
* @param {*} value - anything
|
||
* @returns {boolean} true only for the value is an object
|
||
*/
|
||
function isObject(value) {
|
||
return !isNil(value) && value.constructor === Object
|
||
}
|
||
|
||
/**
|
||
* Check if a value is null or undefined
|
||
* @param {*} value - anything
|
||
* @returns {boolean} true only for the 'undefined' and 'null' types
|
||
*/
|
||
function isNil(value) {
|
||
return value === null || value === undefined
|
||
}
|
||
|
||
/**
|
||
* Detect node js environements
|
||
* @returns {boolean} true if the runtime is node
|
||
*/
|
||
function isNode() {
|
||
return typeof process !== 'undefined'
|
||
}
|
||
|
||
/**
|
||
* True if the node has not expression set nor bindings directives
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true only if it's a static node that doesn't need bindings or expressions
|
||
*/
|
||
function isStaticNode(node) {
|
||
return [
|
||
hasExpressions,
|
||
findEachAttribute,
|
||
findIfAttribute,
|
||
isCustomNode,
|
||
isSlotNode
|
||
].every(test => !test(node))
|
||
}
|
||
|
||
/**
|
||
* Check if a node should be rendered in the final component HTML
|
||
* For example slot <template slot="content"> tags not using `each` or `if` directives can be removed
|
||
* see also https://github.com/riot/riot/issues/2888
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true if we can remove this tag from the component rendered HTML
|
||
*/
|
||
function isRemovableNode(node) {
|
||
return isTemplateNode(node) && !isNil(findAttribute(SLOT_ATTRIBUTE, node)) && !hasEachAttribute(node) && !hasIfAttribute(node)
|
||
}
|
||
|
||
/**
|
||
* Check if a node name is part of the browser or builtin javascript api or it belongs to the current scope
|
||
* @param { types.NodePath } path - containing the current node visited
|
||
* @returns {boolean} true if it's a global api variable
|
||
*/
|
||
function isGlobal({ scope, node }) {
|
||
// recursively find the identifier of this AST path
|
||
if (node.object) {
|
||
return isGlobal({ node: node.object, scope })
|
||
}
|
||
|
||
return Boolean(
|
||
isRaw(node) ||
|
||
isBuiltinAPI(node) ||
|
||
isBrowserAPI(node) ||
|
||
isNewExpression(node) ||
|
||
isNodeInScope(scope, node)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Checks if the identifier of a given node exists in a scope
|
||
* @param {Scope} scope - scope where to search for the identifier
|
||
* @param {types.Node} node - node to search for the identifier
|
||
* @returns {boolean} true if the node identifier is defined in the given scope
|
||
*/
|
||
function isNodeInScope(scope, node) {
|
||
const traverse = (isInScope = false) => {
|
||
types.visit(node, {
|
||
visitIdentifier(path) {
|
||
if (scope.lookup(getName(path.node))) {
|
||
isInScope = true;
|
||
}
|
||
|
||
this.abort();
|
||
}
|
||
});
|
||
|
||
return isInScope
|
||
};
|
||
|
||
return traverse()
|
||
}
|
||
|
||
/**
|
||
* True if the node has the isCustom attribute set
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true if either it's a riot component or a custom element
|
||
*/
|
||
function isCustomNode(node) {
|
||
return !!(node[IS_CUSTOM_NODE] || hasIsAttribute(node))
|
||
}
|
||
|
||
/**
|
||
* True the node is <slot>
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true if it's a slot node
|
||
*/
|
||
function isSlotNode(node) {
|
||
return node.name === SLOT_TAG_NODE_NAME
|
||
}
|
||
|
||
/**
|
||
* True if the node has the isVoid attribute set
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true if the node is self closing
|
||
*/
|
||
function isVoidNode(node) {
|
||
return !!node[IS_VOID_NODE]
|
||
}
|
||
|
||
/**
|
||
* True if the riot parser did find a tag node
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true only for the tag nodes
|
||
*/
|
||
function isTagNode(node) {
|
||
return node.type === nodeTypes.TAG
|
||
}
|
||
|
||
/**
|
||
* True if the riot parser did find a text node
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true only for the text nodes
|
||
*/
|
||
function isTextNode(node) {
|
||
return node.type === nodeTypes.TEXT
|
||
}
|
||
|
||
/**
|
||
* True if the node parsed is the root one
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true only for the root nodes
|
||
*/
|
||
function isRootNode(node) {
|
||
return node.isRoot
|
||
}
|
||
|
||
/**
|
||
* True if the attribute parsed is of type spread one
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true if the attribute node is of type spread
|
||
*/
|
||
function isSpreadAttribute(node) {
|
||
return node[IS_SPREAD_ATTRIBUTE]
|
||
}
|
||
|
||
/**
|
||
* True if the node is an attribute and its name is "value"
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true only for value attribute nodes
|
||
*/
|
||
function isValueAttribute(node) {
|
||
return node.name === 'value'
|
||
}
|
||
|
||
/**
|
||
* True if the DOM node is a progress tag
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true for the progress tags
|
||
*/
|
||
function isProgressNode(node) {
|
||
return node.name === PROGRESS_TAG_NODE_NAME
|
||
}
|
||
|
||
/**
|
||
* True if the DOM node is a <template> tag
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true for the progress tags
|
||
*/
|
||
function isTemplateNode(node) {
|
||
return node.name === TEMPLATE_TAG_NODE_NAME
|
||
}
|
||
|
||
/**
|
||
* True if the node is an attribute and a DOM handler
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true only for dom listener attribute nodes
|
||
*/
|
||
const isEventAttribute = (() => {
|
||
const EVENT_ATTR_RE = /^on/;
|
||
return node => EVENT_ATTR_RE.test(node.name)
|
||
})();
|
||
|
||
|
||
/**
|
||
* Check if a string is an html comment
|
||
* @param {string} string - test string
|
||
* @returns {boolean} true if html comment
|
||
*/
|
||
function isCommentString(string) {
|
||
return string.trim().indexOf('<!') === 0
|
||
}
|
||
|
||
/**
|
||
* True if the node has expressions or expression attributes
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} ditto
|
||
*/
|
||
function hasExpressions(node) {
|
||
return !!(
|
||
node.expressions ||
|
||
// has expression attributes
|
||
(getNodeAttributes(node).some(attribute => hasExpressions(attribute))) ||
|
||
// has child text nodes with expressions
|
||
(node.nodes && node.nodes.some(node => isTextNode(node) && hasExpressions(node)))
|
||
)
|
||
}
|
||
|
||
/**
|
||
* True if the node is a directive having its own template
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {boolean} true only for the IF EACH and TAG bindings
|
||
*/
|
||
function hasItsOwnTemplate(node) {
|
||
return [
|
||
findEachAttribute,
|
||
findIfAttribute,
|
||
isCustomNode
|
||
].some(test => test(node))
|
||
}
|
||
|
||
const hasIfAttribute = compose(Boolean, findIfAttribute);
|
||
const hasEachAttribute = compose(Boolean, findEachAttribute);
|
||
const hasIsAttribute = compose(Boolean, findIsAttribute);
|
||
compose(Boolean, findKeyAttribute);
|
||
|
||
/**
|
||
* Find the attribute node
|
||
* @param { string } name - name of the attribute we want to find
|
||
* @param { riotParser.nodeTypes.TAG } node - a tag node
|
||
* @returns { riotParser.nodeTypes.ATTR } attribute node
|
||
*/
|
||
function findAttribute(name, node) {
|
||
return node.attributes && node.attributes.find(attr => getName(attr) === name)
|
||
}
|
||
|
||
function findIfAttribute(node) {
|
||
return findAttribute(IF_DIRECTIVE, node)
|
||
}
|
||
|
||
function findEachAttribute(node) {
|
||
return findAttribute(EACH_DIRECTIVE, node)
|
||
}
|
||
|
||
function findKeyAttribute(node) {
|
||
return findAttribute(KEY_ATTRIBUTE, node)
|
||
}
|
||
|
||
function findIsAttribute(node) {
|
||
return findAttribute(IS_DIRECTIVE, node)
|
||
}
|
||
|
||
/**
|
||
* Find all the node attributes that are not expressions
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {Array} list of all the static attributes
|
||
*/
|
||
function findStaticAttributes(node) {
|
||
return getNodeAttributes(node).filter(attribute => !hasExpressions(attribute))
|
||
}
|
||
|
||
/**
|
||
* Find all the node attributes that have expressions
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {Array} list of all the dynamic attributes
|
||
*/
|
||
function findDynamicAttributes(node) {
|
||
return getNodeAttributes(node).filter(hasExpressions)
|
||
}
|
||
|
||
function nullNode() {
|
||
return builders.literal(null)
|
||
}
|
||
|
||
function simplePropertyNode(key, value) {
|
||
const property = builders.property('init', builders.identifier(key), value, false);
|
||
|
||
property.sho;
|
||
return property
|
||
}
|
||
|
||
const LINES_RE = /\r\n?|\n/g;
|
||
|
||
/**
|
||
* Split a string into a rows array generated from its EOL matches
|
||
* @param { string } string [description]
|
||
* @returns { Array } array containing all the string rows
|
||
*/
|
||
function splitStringByEOL(string) {
|
||
return string.split(LINES_RE)
|
||
}
|
||
|
||
/**
|
||
* Get the line and the column of a source text based on its position in the string
|
||
* @param { string } string - target string
|
||
* @param { number } position - target position
|
||
* @returns { Object } object containing the source text line and column
|
||
*/
|
||
function getLineAndColumnByPosition(string, position) {
|
||
const lines = splitStringByEOL(string.slice(0, position));
|
||
|
||
return {
|
||
line: lines.length,
|
||
column: lines[lines.length - 1].length
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Add the offset to the code that must be parsed in order to generate properly the sourcemaps
|
||
* @param {string} input - input string
|
||
* @param {string} source - original source code
|
||
* @param {RiotParser.Node} node - node that we are going to transform
|
||
* @return {string} the input string with the offset properly set
|
||
*/
|
||
function addLineOffset(input, source, node) {
|
||
const {column, line} = getLineAndColumnByPosition(source, node.start);
|
||
return `${'\n'.repeat(line - 1)}${' '.repeat(column + 1)}${input}`
|
||
}
|
||
|
||
/**
|
||
* Create a simple attribute expression
|
||
* @param {RiotParser.Node.Attr} sourceNode - the custom tag
|
||
* @param {string} sourceFile - source file path
|
||
* @param {string} sourceCode - original source
|
||
* @returns {AST.Node} object containing the expression binding keys
|
||
*/
|
||
function createAttributeExpression(sourceNode, sourceFile, sourceCode) {
|
||
return builders.objectExpression([
|
||
simplePropertyNode(BINDING_TYPE_KEY,
|
||
builders.memberExpression(
|
||
builders.identifier(EXPRESSION_TYPES),
|
||
builders.identifier(ATTRIBUTE_EXPRESSION_TYPE),
|
||
false
|
||
)
|
||
),
|
||
simplePropertyNode(BINDING_NAME_KEY, isSpreadAttribute(sourceNode) ? nullNode() : builders.literal(sourceNode.name)),
|
||
simplePropertyNode(
|
||
BINDING_EVALUATE_KEY,
|
||
createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode)
|
||
)
|
||
])
|
||
}
|
||
|
||
/**
|
||
* Create a simple event expression
|
||
* @param {RiotParser.Node.Attr} sourceNode - attribute containing the event handlers
|
||
* @param {string} sourceFile - source file path
|
||
* @param {string} sourceCode - original source
|
||
* @returns {AST.Node} object containing the expression binding keys
|
||
*/
|
||
function createEventExpression(sourceNode, sourceFile, sourceCode) {
|
||
return builders.objectExpression([
|
||
simplePropertyNode(BINDING_TYPE_KEY,
|
||
builders.memberExpression(
|
||
builders.identifier(EXPRESSION_TYPES),
|
||
builders.identifier(EVENT_EXPRESSION_TYPE),
|
||
false
|
||
)
|
||
),
|
||
simplePropertyNode(BINDING_NAME_KEY, builders.literal(sourceNode.name)),
|
||
simplePropertyNode(
|
||
BINDING_EVALUATE_KEY,
|
||
createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode)
|
||
)
|
||
])
|
||
}
|
||
|
||
var quot = "\"";
|
||
var amp = "&";
|
||
var apos = "'";
|
||
var lt = "<";
|
||
var gt = ">";
|
||
var nbsp = " ";
|
||
var iexcl = "¡";
|
||
var cent = "¢";
|
||
var pound = "£";
|
||
var curren = "¤";
|
||
var yen = "¥";
|
||
var brvbar = "¦";
|
||
var sect = "§";
|
||
var uml = "¨";
|
||
var copy = "©";
|
||
var ordf = "ª";
|
||
var laquo = "«";
|
||
var not = "¬";
|
||
var shy = "";
|
||
var reg = "®";
|
||
var macr = "¯";
|
||
var deg = "°";
|
||
var plusmn = "±";
|
||
var sup2 = "²";
|
||
var sup3 = "³";
|
||
var acute = "´";
|
||
var micro = "µ";
|
||
var para = "¶";
|
||
var middot = "·";
|
||
var cedil = "¸";
|
||
var sup1 = "¹";
|
||
var ordm = "º";
|
||
var raquo = "»";
|
||
var frac14 = "¼";
|
||
var frac12 = "½";
|
||
var frac34 = "¾";
|
||
var iquest = "¿";
|
||
var Agrave = "À";
|
||
var Aacute = "Á";
|
||
var Acirc = "Â";
|
||
var Atilde = "Ã";
|
||
var Auml = "Ä";
|
||
var Aring = "Å";
|
||
var AElig = "Æ";
|
||
var Ccedil = "Ç";
|
||
var Egrave = "È";
|
||
var Eacute = "É";
|
||
var Ecirc = "Ê";
|
||
var Euml = "Ë";
|
||
var Igrave = "Ì";
|
||
var Iacute = "Í";
|
||
var Icirc = "Î";
|
||
var Iuml = "Ï";
|
||
var ETH = "Ð";
|
||
var Ntilde = "Ñ";
|
||
var Ograve = "Ò";
|
||
var Oacute = "Ó";
|
||
var Ocirc = "Ô";
|
||
var Otilde = "Õ";
|
||
var Ouml = "Ö";
|
||
var times = "×";
|
||
var Oslash = "Ø";
|
||
var Ugrave = "Ù";
|
||
var Uacute = "Ú";
|
||
var Ucirc = "Û";
|
||
var Uuml = "Ü";
|
||
var Yacute = "Ý";
|
||
var THORN = "Þ";
|
||
var szlig = "ß";
|
||
var agrave = "à";
|
||
var aacute = "á";
|
||
var acirc = "â";
|
||
var atilde = "ã";
|
||
var auml = "ä";
|
||
var aring = "å";
|
||
var aelig = "æ";
|
||
var ccedil = "ç";
|
||
var egrave = "è";
|
||
var eacute = "é";
|
||
var ecirc = "ê";
|
||
var euml = "ë";
|
||
var igrave = "ì";
|
||
var iacute = "í";
|
||
var icirc = "î";
|
||
var iuml = "ï";
|
||
var eth = "ð";
|
||
var ntilde = "ñ";
|
||
var ograve = "ò";
|
||
var oacute = "ó";
|
||
var ocirc = "ô";
|
||
var otilde = "õ";
|
||
var ouml = "ö";
|
||
var divide = "÷";
|
||
var oslash = "ø";
|
||
var ugrave = "ù";
|
||
var uacute = "ú";
|
||
var ucirc = "û";
|
||
var uuml = "ü";
|
||
var yacute = "ý";
|
||
var thorn = "þ";
|
||
var yuml = "ÿ";
|
||
var OElig = "Œ";
|
||
var oelig = "œ";
|
||
var Scaron = "Š";
|
||
var scaron = "š";
|
||
var Yuml = "Ÿ";
|
||
var fnof = "ƒ";
|
||
var circ = "ˆ";
|
||
var tilde = "˜";
|
||
var Alpha = "Α";
|
||
var Beta = "Β";
|
||
var Gamma = "Γ";
|
||
var Delta = "Δ";
|
||
var Epsilon = "Ε";
|
||
var Zeta = "Ζ";
|
||
var Eta = "Η";
|
||
var Theta = "Θ";
|
||
var Iota = "Ι";
|
||
var Kappa = "Κ";
|
||
var Lambda = "Λ";
|
||
var Mu = "Μ";
|
||
var Nu = "Ν";
|
||
var Xi = "Ξ";
|
||
var Omicron = "Ο";
|
||
var Pi = "Π";
|
||
var Rho = "Ρ";
|
||
var Sigma = "Σ";
|
||
var Tau = "Τ";
|
||
var Upsilon = "Υ";
|
||
var Phi = "Φ";
|
||
var Chi = "Χ";
|
||
var Psi = "Ψ";
|
||
var Omega = "Ω";
|
||
var alpha = "α";
|
||
var beta = "β";
|
||
var gamma = "γ";
|
||
var delta = "δ";
|
||
var epsilon = "ε";
|
||
var zeta = "ζ";
|
||
var eta = "η";
|
||
var theta = "θ";
|
||
var iota = "ι";
|
||
var kappa = "κ";
|
||
var lambda = "λ";
|
||
var mu = "μ";
|
||
var nu = "ν";
|
||
var xi = "ξ";
|
||
var omicron = "ο";
|
||
var pi = "π";
|
||
var rho = "ρ";
|
||
var sigmaf = "ς";
|
||
var sigma = "σ";
|
||
var tau = "τ";
|
||
var upsilon = "υ";
|
||
var phi = "φ";
|
||
var chi = "χ";
|
||
var psi = "ψ";
|
||
var omega = "ω";
|
||
var thetasym = "ϑ";
|
||
var upsih = "ϒ";
|
||
var piv = "ϖ";
|
||
var ensp = " ";
|
||
var emsp = " ";
|
||
var thinsp = " ";
|
||
var zwnj = "";
|
||
var zwj = "";
|
||
var lrm = "";
|
||
var rlm = "";
|
||
var ndash = "–";
|
||
var mdash = "—";
|
||
var lsquo = "‘";
|
||
var rsquo = "’";
|
||
var sbquo = "‚";
|
||
var ldquo = "“";
|
||
var rdquo = "”";
|
||
var bdquo = "„";
|
||
var dagger = "†";
|
||
var Dagger = "‡";
|
||
var bull = "•";
|
||
var hellip = "…";
|
||
var permil = "‰";
|
||
var prime = "′";
|
||
var Prime = "″";
|
||
var lsaquo = "‹";
|
||
var rsaquo = "›";
|
||
var oline = "‾";
|
||
var frasl = "⁄";
|
||
var euro = "€";
|
||
var image = "ℑ";
|
||
var weierp = "℘";
|
||
var real = "ℜ";
|
||
var trade = "™";
|
||
var alefsym = "ℵ";
|
||
var larr = "←";
|
||
var uarr = "↑";
|
||
var rarr = "→";
|
||
var darr = "↓";
|
||
var harr = "↔";
|
||
var crarr = "↵";
|
||
var lArr = "⇐";
|
||
var uArr = "⇑";
|
||
var rArr = "⇒";
|
||
var dArr = "⇓";
|
||
var hArr = "⇔";
|
||
var forall = "∀";
|
||
var part = "∂";
|
||
var exist = "∃";
|
||
var empty = "∅";
|
||
var nabla = "∇";
|
||
var isin = "∈";
|
||
var notin = "∉";
|
||
var ni = "∋";
|
||
var prod = "∏";
|
||
var sum = "∑";
|
||
var minus = "−";
|
||
var lowast = "∗";
|
||
var radic = "√";
|
||
var prop = "∝";
|
||
var infin = "∞";
|
||
var ang = "∠";
|
||
var and = "∧";
|
||
var or = "∨";
|
||
var cap = "∩";
|
||
var cup = "∪";
|
||
var int = "∫";
|
||
var there4 = "∴";
|
||
var sim = "∼";
|
||
var cong = "≅";
|
||
var asymp = "≈";
|
||
var ne = "≠";
|
||
var equiv = "≡";
|
||
var le = "≤";
|
||
var ge = "≥";
|
||
var sub = "⊂";
|
||
var sup = "⊃";
|
||
var nsub = "⊄";
|
||
var sube = "⊆";
|
||
var supe = "⊇";
|
||
var oplus = "⊕";
|
||
var otimes = "⊗";
|
||
var perp = "⊥";
|
||
var sdot = "⋅";
|
||
var lceil = "⌈";
|
||
var rceil = "⌉";
|
||
var lfloor = "⌊";
|
||
var rfloor = "⌋";
|
||
var lang = "〈";
|
||
var rang = "〉";
|
||
var loz = "◊";
|
||
var spades = "♠";
|
||
var clubs = "♣";
|
||
var hearts = "♥";
|
||
var diams = "♦";
|
||
var entities = {
|
||
quot: quot,
|
||
amp: amp,
|
||
apos: apos,
|
||
lt: lt,
|
||
gt: gt,
|
||
nbsp: nbsp,
|
||
iexcl: iexcl,
|
||
cent: cent,
|
||
pound: pound,
|
||
curren: curren,
|
||
yen: yen,
|
||
brvbar: brvbar,
|
||
sect: sect,
|
||
uml: uml,
|
||
copy: copy,
|
||
ordf: ordf,
|
||
laquo: laquo,
|
||
not: not,
|
||
shy: shy,
|
||
reg: reg,
|
||
macr: macr,
|
||
deg: deg,
|
||
plusmn: plusmn,
|
||
sup2: sup2,
|
||
sup3: sup3,
|
||
acute: acute,
|
||
micro: micro,
|
||
para: para,
|
||
middot: middot,
|
||
cedil: cedil,
|
||
sup1: sup1,
|
||
ordm: ordm,
|
||
raquo: raquo,
|
||
frac14: frac14,
|
||
frac12: frac12,
|
||
frac34: frac34,
|
||
iquest: iquest,
|
||
Agrave: Agrave,
|
||
Aacute: Aacute,
|
||
Acirc: Acirc,
|
||
Atilde: Atilde,
|
||
Auml: Auml,
|
||
Aring: Aring,
|
||
AElig: AElig,
|
||
Ccedil: Ccedil,
|
||
Egrave: Egrave,
|
||
Eacute: Eacute,
|
||
Ecirc: Ecirc,
|
||
Euml: Euml,
|
||
Igrave: Igrave,
|
||
Iacute: Iacute,
|
||
Icirc: Icirc,
|
||
Iuml: Iuml,
|
||
ETH: ETH,
|
||
Ntilde: Ntilde,
|
||
Ograve: Ograve,
|
||
Oacute: Oacute,
|
||
Ocirc: Ocirc,
|
||
Otilde: Otilde,
|
||
Ouml: Ouml,
|
||
times: times,
|
||
Oslash: Oslash,
|
||
Ugrave: Ugrave,
|
||
Uacute: Uacute,
|
||
Ucirc: Ucirc,
|
||
Uuml: Uuml,
|
||
Yacute: Yacute,
|
||
THORN: THORN,
|
||
szlig: szlig,
|
||
agrave: agrave,
|
||
aacute: aacute,
|
||
acirc: acirc,
|
||
atilde: atilde,
|
||
auml: auml,
|
||
aring: aring,
|
||
aelig: aelig,
|
||
ccedil: ccedil,
|
||
egrave: egrave,
|
||
eacute: eacute,
|
||
ecirc: ecirc,
|
||
euml: euml,
|
||
igrave: igrave,
|
||
iacute: iacute,
|
||
icirc: icirc,
|
||
iuml: iuml,
|
||
eth: eth,
|
||
ntilde: ntilde,
|
||
ograve: ograve,
|
||
oacute: oacute,
|
||
ocirc: ocirc,
|
||
otilde: otilde,
|
||
ouml: ouml,
|
||
divide: divide,
|
||
oslash: oslash,
|
||
ugrave: ugrave,
|
||
uacute: uacute,
|
||
ucirc: ucirc,
|
||
uuml: uuml,
|
||
yacute: yacute,
|
||
thorn: thorn,
|
||
yuml: yuml,
|
||
OElig: OElig,
|
||
oelig: oelig,
|
||
Scaron: Scaron,
|
||
scaron: scaron,
|
||
Yuml: Yuml,
|
||
fnof: fnof,
|
||
circ: circ,
|
||
tilde: tilde,
|
||
Alpha: Alpha,
|
||
Beta: Beta,
|
||
Gamma: Gamma,
|
||
Delta: Delta,
|
||
Epsilon: Epsilon,
|
||
Zeta: Zeta,
|
||
Eta: Eta,
|
||
Theta: Theta,
|
||
Iota: Iota,
|
||
Kappa: Kappa,
|
||
Lambda: Lambda,
|
||
Mu: Mu,
|
||
Nu: Nu,
|
||
Xi: Xi,
|
||
Omicron: Omicron,
|
||
Pi: Pi,
|
||
Rho: Rho,
|
||
Sigma: Sigma,
|
||
Tau: Tau,
|
||
Upsilon: Upsilon,
|
||
Phi: Phi,
|
||
Chi: Chi,
|
||
Psi: Psi,
|
||
Omega: Omega,
|
||
alpha: alpha,
|
||
beta: beta,
|
||
gamma: gamma,
|
||
delta: delta,
|
||
epsilon: epsilon,
|
||
zeta: zeta,
|
||
eta: eta,
|
||
theta: theta,
|
||
iota: iota,
|
||
kappa: kappa,
|
||
lambda: lambda,
|
||
mu: mu,
|
||
nu: nu,
|
||
xi: xi,
|
||
omicron: omicron,
|
||
pi: pi,
|
||
rho: rho,
|
||
sigmaf: sigmaf,
|
||
sigma: sigma,
|
||
tau: tau,
|
||
upsilon: upsilon,
|
||
phi: phi,
|
||
chi: chi,
|
||
psi: psi,
|
||
omega: omega,
|
||
thetasym: thetasym,
|
||
upsih: upsih,
|
||
piv: piv,
|
||
ensp: ensp,
|
||
emsp: emsp,
|
||
thinsp: thinsp,
|
||
zwnj: zwnj,
|
||
zwj: zwj,
|
||
lrm: lrm,
|
||
rlm: rlm,
|
||
ndash: ndash,
|
||
mdash: mdash,
|
||
lsquo: lsquo,
|
||
rsquo: rsquo,
|
||
sbquo: sbquo,
|
||
ldquo: ldquo,
|
||
rdquo: rdquo,
|
||
bdquo: bdquo,
|
||
dagger: dagger,
|
||
Dagger: Dagger,
|
||
bull: bull,
|
||
hellip: hellip,
|
||
permil: permil,
|
||
prime: prime,
|
||
Prime: Prime,
|
||
lsaquo: lsaquo,
|
||
rsaquo: rsaquo,
|
||
oline: oline,
|
||
frasl: frasl,
|
||
euro: euro,
|
||
image: image,
|
||
weierp: weierp,
|
||
real: real,
|
||
trade: trade,
|
||
alefsym: alefsym,
|
||
larr: larr,
|
||
uarr: uarr,
|
||
rarr: rarr,
|
||
darr: darr,
|
||
harr: harr,
|
||
crarr: crarr,
|
||
lArr: lArr,
|
||
uArr: uArr,
|
||
rArr: rArr,
|
||
dArr: dArr,
|
||
hArr: hArr,
|
||
forall: forall,
|
||
part: part,
|
||
exist: exist,
|
||
empty: empty,
|
||
nabla: nabla,
|
||
isin: isin,
|
||
notin: notin,
|
||
ni: ni,
|
||
prod: prod,
|
||
sum: sum,
|
||
minus: minus,
|
||
lowast: lowast,
|
||
radic: radic,
|
||
prop: prop,
|
||
infin: infin,
|
||
ang: ang,
|
||
and: and,
|
||
or: or,
|
||
cap: cap,
|
||
cup: cup,
|
||
int: int,
|
||
there4: there4,
|
||
sim: sim,
|
||
cong: cong,
|
||
asymp: asymp,
|
||
ne: ne,
|
||
equiv: equiv,
|
||
le: le,
|
||
ge: ge,
|
||
sub: sub,
|
||
sup: sup,
|
||
nsub: nsub,
|
||
sube: sube,
|
||
supe: supe,
|
||
oplus: oplus,
|
||
otimes: otimes,
|
||
perp: perp,
|
||
sdot: sdot,
|
||
lceil: lceil,
|
||
rceil: rceil,
|
||
lfloor: lfloor,
|
||
rfloor: rfloor,
|
||
lang: lang,
|
||
rang: rang,
|
||
loz: loz,
|
||
spades: spades,
|
||
clubs: clubs,
|
||
hearts: hearts,
|
||
diams: diams
|
||
};
|
||
|
||
const HTMLEntityRe = /&(\S+);/g;
|
||
const HEX_NUMBER = /^[\da-fA-F]+$/;
|
||
const DECIMAL_NUMBER = /^\d+$/;
|
||
|
||
/**
|
||
* Encode unicode hex html entities like for example Ȣ
|
||
* @param {string} string - input string
|
||
* @returns {string} encoded string
|
||
*/
|
||
function encodeHex(string) {
|
||
const hex = string.substr(2);
|
||
|
||
return HEX_NUMBER.test(hex) ?
|
||
String.fromCodePoint(parseInt(hex, 16)) :
|
||
string
|
||
}
|
||
|
||
/**
|
||
* Encode unicode decimal html entities like for example Þ
|
||
* @param {string} string - input string
|
||
* @returns {string} encoded string
|
||
*/
|
||
function encodeDecimal(string) {
|
||
const nr = string.substr(1);
|
||
|
||
return DECIMAL_NUMBER.test(nr) ?
|
||
String.fromCodePoint(parseInt(nr, 10))
|
||
: string
|
||
}
|
||
|
||
/**
|
||
* Encode html entities in strings like
|
||
* @param {string} string - input string
|
||
* @returns {string} encoded string
|
||
*/
|
||
function encodeHTMLEntities(string) {
|
||
return string.replace(HTMLEntityRe, (match, entity) => {
|
||
const [firstChar, secondChar] = entity;
|
||
|
||
if (firstChar === '#') {
|
||
return secondChar === 'x' ?
|
||
encodeHex(entity) :
|
||
encodeDecimal(entity)
|
||
} else {
|
||
return entities[entity] || entity
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Native String.prototype.trimEnd method with fallback to String.prototype.trimRight
|
||
* Edge doesn't support the first one
|
||
* @param {string} string - input string
|
||
* @returns {string} trimmed output
|
||
*/
|
||
function trimEnd(string) {
|
||
return (string.trimEnd || string.trimRight).apply(string)
|
||
}
|
||
|
||
/**
|
||
* Native String.prototype.trimStart method with fallback to String.prototype.trimLeft
|
||
* Edge doesn't support the first one
|
||
* @param {string} string - input string
|
||
* @returns {string} trimmed output
|
||
*/
|
||
function trimStart(string) {
|
||
return (string.trimStart || string.trimLeft).apply(string)
|
||
}
|
||
|
||
/**
|
||
* Unescape the user escaped chars
|
||
* @param {string} string - input string
|
||
* @param {string} char - probably a '{' or anything the user want's to escape
|
||
* @returns {string} cleaned up string
|
||
*/
|
||
function unescapeChar(string, char) {
|
||
return string.replace(RegExp(`\\\\${char}`, 'gm'), char)
|
||
}
|
||
|
||
/**
|
||
* Generate the pure immutable string chunks from a RiotParser.Node.Text
|
||
* @param {RiotParser.Node.Text} node - riot parser text node
|
||
* @param {string} sourceCode sourceCode - source code
|
||
* @returns {Array} array containing the immutable string chunks
|
||
*/
|
||
function generateLiteralStringChunksFromNode(node, sourceCode) {
|
||
return node.expressions.reduce((chunks, expression, index) => {
|
||
const start = index ? node.expressions[index - 1].end : node.start;
|
||
const string = encodeHTMLEntities(
|
||
sourceCode.substring(start, expression.start)
|
||
);
|
||
|
||
// trimStart the first string
|
||
chunks.push(index === 0 ? trimStart(string) : string);
|
||
|
||
// add the tail to the string
|
||
if (index === node.expressions.length - 1)
|
||
chunks.push(
|
||
encodeHTMLEntities(
|
||
trimEnd(sourceCode.substring(expression.end, node.end))
|
||
)
|
||
);
|
||
|
||
return chunks
|
||
}, [])
|
||
// comments are not supported here
|
||
.filter(str => !isCommentString(str))
|
||
.map(str => node.unescape ? unescapeChar(str, node.unescape) : str)
|
||
}
|
||
|
||
/**
|
||
* Simple bindings might contain multiple expressions like for example: "{foo} and {bar}"
|
||
* This helper aims to merge them in a template literal if it's necessary
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @param {string} sourceFile - original tag file
|
||
* @param {string} sourceCode - original tag source code
|
||
* @returns { Object } a template literal expression object
|
||
*/
|
||
function mergeNodeExpressions(node, sourceFile, sourceCode) {
|
||
if (node.parts.length === 1)
|
||
return transformExpression(node.expressions[0], sourceFile, sourceCode)
|
||
|
||
const pureStringChunks = generateLiteralStringChunksFromNode(node, sourceCode);
|
||
const stringsArray = pureStringChunks.reduce((acc, str, index) => {
|
||
const expr = node.expressions[index];
|
||
|
||
return [
|
||
...acc,
|
||
builders.literal(str),
|
||
expr ? transformExpression(expr, sourceFile, sourceCode) : nullNode()
|
||
]
|
||
}, [])
|
||
// filter the empty literal expressions
|
||
.filter(expr => !isLiteral(expr) || expr.value);
|
||
|
||
return createArrayString(stringsArray)
|
||
}
|
||
|
||
/**
|
||
* Create a text expression
|
||
* @param {RiotParser.Node.Text} sourceNode - text node to parse
|
||
* @param {string} sourceFile - source file path
|
||
* @param {string} sourceCode - original source
|
||
* @param {number} childNodeIndex - position of the child text node in its parent children nodes
|
||
* @returns {AST.Node} object containing the expression binding keys
|
||
*/
|
||
function createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) {
|
||
return builders.objectExpression([
|
||
simplePropertyNode(BINDING_TYPE_KEY,
|
||
builders.memberExpression(
|
||
builders.identifier(EXPRESSION_TYPES),
|
||
builders.identifier(TEXT_EXPRESSION_TYPE),
|
||
false
|
||
)
|
||
),
|
||
simplePropertyNode(
|
||
BINDING_CHILD_NODE_INDEX_KEY,
|
||
builders.literal(childNodeIndex)
|
||
),
|
||
simplePropertyNode(
|
||
BINDING_EVALUATE_KEY,
|
||
wrapASTInFunctionWithScope(
|
||
mergeNodeExpressions(sourceNode, sourceFile, sourceCode)
|
||
)
|
||
)
|
||
])
|
||
}
|
||
|
||
function createValueExpression(sourceNode, sourceFile, sourceCode) {
|
||
return builders.objectExpression([
|
||
simplePropertyNode(BINDING_TYPE_KEY,
|
||
builders.memberExpression(
|
||
builders.identifier(EXPRESSION_TYPES),
|
||
builders.identifier(VALUE_EXPRESSION_TYPE),
|
||
false
|
||
)
|
||
),
|
||
simplePropertyNode(
|
||
BINDING_EVALUATE_KEY,
|
||
createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode)
|
||
)
|
||
])
|
||
}
|
||
|
||
function createExpression(sourceNode, sourceFile, sourceCode, childNodeIndex, parentNode) {
|
||
switch (true) {
|
||
case isTextNode(sourceNode):
|
||
return createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex)
|
||
// progress nodes value attributes will be rendered as attributes
|
||
// see https://github.com/riot/compiler/issues/122
|
||
case isValueAttribute(sourceNode) && hasValueAttribute(parentNode.name) && !isProgressNode(parentNode):
|
||
return createValueExpression(sourceNode, sourceFile, sourceCode)
|
||
case isEventAttribute(sourceNode):
|
||
return createEventExpression(sourceNode, sourceFile, sourceCode)
|
||
default:
|
||
return createAttributeExpression(sourceNode, sourceFile, sourceCode)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create the attribute expressions
|
||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
|
||
* @param {string} sourceFile - source file path
|
||
* @param {string} sourceCode - original source
|
||
* @returns {Array} array containing all the attribute expressions
|
||
*/
|
||
function createAttributeExpressions(sourceNode, sourceFile, sourceCode) {
|
||
return findDynamicAttributes(sourceNode)
|
||
.map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode))
|
||
}
|
||
|
||
/**
|
||
* Parse a js source to generate the AST
|
||
* @param {string} source - javascript source
|
||
* @param {Object} options - parser options
|
||
* @returns {AST} AST tree
|
||
*/
|
||
function generateAST(source, options) {
|
||
return parse$1(source, {
|
||
parser: {
|
||
parse: (source, opts) => parse$2(source, {
|
||
...opts,
|
||
ecmaVersion: 'latest'
|
||
})
|
||
},
|
||
...options
|
||
})
|
||
}
|
||
|
||
const scope = builders.identifier(SCOPE);
|
||
const getName = node => node && node.name ? node.name : node;
|
||
|
||
/**
|
||
* Replace the path scope with a member Expression
|
||
* @param { types.NodePath } path - containing the current node visited
|
||
* @param { types.Node } property - node we want to prefix with the scope identifier
|
||
* @returns {undefined} this is a void function
|
||
*/
|
||
function replacePathScope(path, property) {
|
||
path.replace(builders.memberExpression(
|
||
scope,
|
||
property,
|
||
false
|
||
));
|
||
}
|
||
|
||
/**
|
||
* Change the nodes scope adding the `scope` prefix
|
||
* @param { types.NodePath } path - containing the current node visited
|
||
* @returns { boolean } return false if we want to stop the tree traversal
|
||
* @context { types.visit }
|
||
*/
|
||
function updateNodeScope(path) {
|
||
if (!isGlobal(path)) {
|
||
replacePathScope(path, path.node);
|
||
|
||
return false
|
||
}
|
||
|
||
this.traverse(path);
|
||
}
|
||
|
||
/**
|
||
* Change the scope of the member expressions
|
||
* @param { types.NodePath } path - containing the current node visited
|
||
* @returns { boolean } return always false because we want to check only the first node object
|
||
*/
|
||
function visitMemberExpression(path) {
|
||
const traversePathObject = () => this.traverse(path.get('object'));
|
||
const currentObject = path.node.object;
|
||
|
||
switch (true) {
|
||
case isGlobal(path):
|
||
if (currentObject.arguments && currentObject.arguments.length) {
|
||
traversePathObject();
|
||
}
|
||
break
|
||
case !path.value.computed && isIdentifier(currentObject):
|
||
replacePathScope(
|
||
path,
|
||
path.node
|
||
);
|
||
break
|
||
default:
|
||
this.traverse(path);
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* Objects properties should be handled a bit differently from the Identifier
|
||
* @param { types.NodePath } path - containing the current node visited
|
||
* @returns { boolean } return false if we want to stop the tree traversal
|
||
*/
|
||
function visitObjectProperty(path) {
|
||
const value = path.node.value;
|
||
const isShorthand = path.node.shorthand;
|
||
|
||
if (isIdentifier(value) || isMemberExpression(value) || isShorthand) {
|
||
// disable shorthand object properties
|
||
if (isShorthand) path.node.shorthand = false;
|
||
|
||
updateNodeScope.call(this, path.get('value'));
|
||
} else {
|
||
this.traverse(path.get('value'));
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* The this expressions should be replaced with the scope
|
||
* @param { types.NodePath } path - containing the current node visited
|
||
* @returns { boolean|undefined } return false if we want to stop the tree traversal
|
||
*/
|
||
function visitThisExpression(path) {
|
||
path.replace(scope);
|
||
this.traverse(path);
|
||
}
|
||
|
||
/**
|
||
* Replace the identifiers with the node scope
|
||
* @param { types.NodePath } path - containing the current node visited
|
||
* @returns { boolean|undefined } return false if we want to stop the tree traversal
|
||
*/
|
||
function visitIdentifier(path) {
|
||
const parentValue = path.parent.value;
|
||
|
||
if (!isMemberExpression(parentValue) || parentValue.computed) {
|
||
updateNodeScope.call(this, path);
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* Update the scope of the global nodes
|
||
* @param { Object } ast - ast program
|
||
* @returns { Object } the ast program with all the global nodes updated
|
||
*/
|
||
function updateNodesScope(ast) {
|
||
const ignorePath = () => false;
|
||
|
||
types.visit(ast, {
|
||
visitIdentifier,
|
||
visitMemberExpression,
|
||
visitObjectProperty,
|
||
visitThisExpression,
|
||
visitClassExpression: ignorePath
|
||
});
|
||
|
||
return ast
|
||
}
|
||
|
||
/**
|
||
* Convert any expression to an AST tree
|
||
* @param { Object } expression - expression parsed by the riot parser
|
||
* @param { string } sourceFile - original tag file
|
||
* @param { string } sourceCode - original tag source code
|
||
* @returns { Object } the ast generated
|
||
*/
|
||
function createASTFromExpression(expression, sourceFile, sourceCode) {
|
||
const code = sourceFile ?
|
||
addLineOffset(expression.text, sourceCode, expression) :
|
||
expression.text;
|
||
|
||
return generateAST(`(${code})`, {
|
||
sourceFileName: sourceFile
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Create the bindings template property
|
||
* @param {Array} args - arguments to pass to the template function
|
||
* @returns {ASTNode} a binding template key
|
||
*/
|
||
function createTemplateProperty(args) {
|
||
return simplePropertyNode(
|
||
BINDING_TEMPLATE_KEY,
|
||
args ? callTemplateFunction(...args) : nullNode()
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Try to get the expression of an attribute node
|
||
* @param { RiotParser.Node.Attribute } attribute - riot parser attribute node
|
||
* @returns { RiotParser.Node.Expression } attribute expression value
|
||
*/
|
||
function getAttributeExpression(attribute) {
|
||
return attribute.expressions ? attribute.expressions[0] : {
|
||
// if no expression was found try to typecast the attribute value
|
||
...attribute,
|
||
text: attribute.value
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Wrap the ast generated in a function call providing the scope argument
|
||
* @param {Object} ast - function body
|
||
* @returns {FunctionExpresion} function having the scope argument injected
|
||
*/
|
||
function wrapASTInFunctionWithScope(ast) {
|
||
const fn = builders.arrowFunctionExpression([scope], ast);
|
||
|
||
// object expressions need to be wrapped in parenthesis
|
||
// recast doesn't allow it
|
||
// see also https://github.com/benjamn/recast/issues/985
|
||
if (isObjectExpression(ast)) {
|
||
// doing a small hack here
|
||
// trying to figure out how the recast printer works internally
|
||
ast.extra = {
|
||
parenthesized: true
|
||
};
|
||
}
|
||
|
||
return fn
|
||
}
|
||
|
||
/**
|
||
* Convert any parser option to a valid template one
|
||
* @param { RiotParser.Node.Expression } expression - expression parsed by the riot parser
|
||
* @param { string } sourceFile - original tag file
|
||
* @param { string } sourceCode - original tag source code
|
||
* @returns { Object } a FunctionExpression object
|
||
*
|
||
* @example
|
||
* toScopedFunction('foo + bar') // scope.foo + scope.bar
|
||
*
|
||
* @example
|
||
* toScopedFunction('foo.baz + bar') // scope.foo.baz + scope.bar
|
||
*/
|
||
function toScopedFunction(expression, sourceFile, sourceCode) {
|
||
return compose(
|
||
wrapASTInFunctionWithScope,
|
||
transformExpression
|
||
)(expression, sourceFile, sourceCode)
|
||
}
|
||
|
||
/**
|
||
* Transform an expression node updating its global scope
|
||
* @param {RiotParser.Node.Expr} expression - riot parser expression node
|
||
* @param {string} sourceFile - source file
|
||
* @param {string} sourceCode - source code
|
||
* @returns {ASTExpression} ast expression generated from the riot parser expression node
|
||
*/
|
||
function transformExpression(expression, sourceFile, sourceCode) {
|
||
return compose(
|
||
getExpressionAST,
|
||
updateNodesScope,
|
||
createASTFromExpression
|
||
)(expression, sourceFile, sourceCode)
|
||
}
|
||
|
||
/**
|
||
* Get the parsed AST expression of riot expression node
|
||
* @param {AST.Program} sourceAST - raw node parsed
|
||
* @returns {AST.Expression} program expression output
|
||
*/
|
||
function getExpressionAST(sourceAST) {
|
||
const astBody = sourceAST.program.body;
|
||
|
||
return astBody[0] ? astBody[0].expression : astBody
|
||
}
|
||
|
||
/**
|
||
* Create the template call function
|
||
* @param {Array|string|Node.Literal} template - template string
|
||
* @param {Array<AST.Nodes>} bindings - template bindings provided as AST nodes
|
||
* @returns {Node.CallExpression} template call expression
|
||
*/
|
||
function callTemplateFunction(template, bindings) {
|
||
return builders.callExpression(builders.identifier(TEMPLATE_FN), [
|
||
template ? builders.literal(template) : nullNode(),
|
||
bindings ? builders.arrayExpression(bindings) : nullNode()
|
||
])
|
||
}
|
||
|
||
/**
|
||
* Create the template wrapper function injecting the dependencies needed to render the component html
|
||
* @param {Array<AST.Nodes>|AST.BlockStatement} body - function body
|
||
* @returns {AST.Node} arrow function expression
|
||
*/
|
||
const createTemplateDependenciesInjectionWrapper = (body) => builders.arrowFunctionExpression(
|
||
[
|
||
TEMPLATE_FN,
|
||
EXPRESSION_TYPES,
|
||
BINDING_TYPES,
|
||
GET_COMPONENT_FN
|
||
].map(builders.identifier),
|
||
body
|
||
);
|
||
|
||
/**
|
||
* Convert any DOM attribute into a valid DOM selector useful for the querySelector API
|
||
* @param { string } attributeName - name of the attribute to query
|
||
* @returns { string } the attribute transformed to a query selector
|
||
*/
|
||
const attributeNameToDOMQuerySelector = attributeName => `[${attributeName}]`;
|
||
|
||
/**
|
||
* Create the properties to query a DOM node
|
||
* @param { string } attributeName - attribute name needed to identify a DOM node
|
||
* @returns { Array<AST.Node> } array containing the selector properties needed for the binding
|
||
*/
|
||
function createSelectorProperties(attributeName) {
|
||
return attributeName ? [
|
||
simplePropertyNode(BINDING_REDUNDANT_ATTRIBUTE_KEY, builders.literal(attributeName)),
|
||
simplePropertyNode(BINDING_SELECTOR_KEY,
|
||
compose(builders.literal, attributeNameToDOMQuerySelector)(attributeName)
|
||
)
|
||
] : []
|
||
}
|
||
|
||
/**
|
||
* Clone the node filtering out the selector attribute from the attributes list
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @param {string} selectorAttribute - name of the selector attribute to filter out
|
||
* @returns {RiotParser.Node} the node with the attribute cleaned up
|
||
*/
|
||
function cloneNodeWithoutSelectorAttribute(node, selectorAttribute) {
|
||
return {
|
||
...node,
|
||
attributes: getAttributesWithoutSelector(getNodeAttributes(node), selectorAttribute)
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* Get the node attributes without the selector one
|
||
* @param {Array<RiotParser.Attr>} attributes - attributes list
|
||
* @param {string} selectorAttribute - name of the selector attribute to filter out
|
||
* @returns {Array<RiotParser.Attr>} filtered attributes
|
||
*/
|
||
function getAttributesWithoutSelector(attributes, selectorAttribute) {
|
||
if (selectorAttribute)
|
||
return attributes.filter(attribute => attribute.name !== selectorAttribute)
|
||
|
||
return attributes
|
||
}
|
||
|
||
/**
|
||
* Clean binding or custom attributes
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {Array<RiotParser.Node.Attr>} only the attributes that are not bindings or directives
|
||
*/
|
||
function cleanAttributes(node) {
|
||
return getNodeAttributes(node).filter(attribute => ![
|
||
IF_DIRECTIVE,
|
||
EACH_DIRECTIVE,
|
||
KEY_ATTRIBUTE,
|
||
SLOT_ATTRIBUTE,
|
||
IS_DIRECTIVE
|
||
].includes(attribute.name))
|
||
}
|
||
|
||
/**
|
||
* Root node factory function needed for the top root nodes and the nested ones
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {RiotParser.Node} root node
|
||
*/
|
||
function rootNodeFactory(node) {
|
||
return {
|
||
nodes: getChildrenNodes(node),
|
||
isRoot: true
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create a root node proxing only its nodes and attributes
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {RiotParser.Node} root node
|
||
*/
|
||
function createRootNode(node) {
|
||
return {
|
||
...rootNodeFactory(node),
|
||
attributes: compose(
|
||
// root nodes should always have attribute expressions
|
||
transformStatiAttributesIntoExpressions,
|
||
// root nodes shouldn't have directives
|
||
cleanAttributes
|
||
)(node)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create nested root node. Each and If directives create nested root nodes for example
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {RiotParser.Node} root node
|
||
*/
|
||
function createNestedRootNode(node) {
|
||
return {
|
||
...rootNodeFactory(node),
|
||
attributes: cleanAttributes(node)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Transform the static node attributes into expressions, useful for the root nodes
|
||
* @param {Array<RiotParser.Node.Attr>} attributes - riot parser node
|
||
* @returns {Array<RiotParser.Node.Attr>} all the attributes received as attribute expressions
|
||
*/
|
||
function transformStatiAttributesIntoExpressions(attributes) {
|
||
return attributes.map(attribute => {
|
||
if (attribute.expressions) return attribute
|
||
|
||
return {
|
||
...attribute,
|
||
expressions: [{
|
||
start: attribute.valueStart,
|
||
end: attribute.end,
|
||
text: `'${attribute.value || attribute.name}'`
|
||
}]
|
||
}
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Get all the child nodes of a RiotParser.Node
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {Array<RiotParser.Node>} all the child nodes found
|
||
*/
|
||
function getChildrenNodes(node) {
|
||
return node && node.nodes ? node.nodes : []
|
||
}
|
||
|
||
/**
|
||
* Get all the attributes of a riot parser node
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {Array<RiotParser.Node.Attribute>} all the attributes find
|
||
*/
|
||
function getNodeAttributes(node) {
|
||
return node.attributes ? node.attributes : []
|
||
}
|
||
|
||
/**
|
||
* Create custom tag name function
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @param {string} sourceFile - original tag file
|
||
* @param {string} sourceCode - original tag source code
|
||
* @returns {RiotParser.Node.Attr} the node name as expression attribute
|
||
*/
|
||
function createCustomNodeNameEvaluationFunction(node, sourceFile, sourceCode) {
|
||
const isAttribute = findIsAttribute(node);
|
||
const toRawString = val => `'${val}'`;
|
||
|
||
if (isAttribute) {
|
||
return isAttribute.expressions ? wrapASTInFunctionWithScope(
|
||
mergeAttributeExpressions(isAttribute, sourceFile, sourceCode)
|
||
) :
|
||
toScopedFunction({
|
||
...isAttribute,
|
||
text: toRawString(isAttribute.value)
|
||
}, sourceFile, sourceCode)
|
||
}
|
||
|
||
return toScopedFunction({ ...node, text: toRawString(getName(node)) }, sourceFile, sourceCode)
|
||
}
|
||
|
||
/**
|
||
* Convert all the node static attributes to strings
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {string} all the node static concatenated as string
|
||
*/
|
||
function staticAttributesToString(node) {
|
||
return findStaticAttributes(node)
|
||
.map(attribute => attribute[IS_BOOLEAN_ATTRIBUTE] || !attribute.value ?
|
||
attribute.name :
|
||
`${attribute.name}="${unescapeNode(attribute, 'value').value}"`
|
||
).join(' ')
|
||
}
|
||
|
||
/**
|
||
* Make sure that node escaped chars will be unescaped
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @param {string} key - key property to unescape
|
||
* @returns {RiotParser.Node} node with the text property unescaped
|
||
*/
|
||
function unescapeNode(node, key) {
|
||
if (node.unescape) {
|
||
return {
|
||
...node,
|
||
[key]: unescapeChar(node[key], node.unescape)
|
||
}
|
||
}
|
||
|
||
return node
|
||
}
|
||
|
||
/**
|
||
* Convert a riot parser opening node into a string
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {string} the node as string
|
||
*/
|
||
function nodeToString(node) {
|
||
const attributes = staticAttributesToString(node);
|
||
|
||
switch (true) {
|
||
case isTagNode(node):
|
||
return `<${node.name}${attributes ? ` ${attributes}` : ''}${isVoidNode(node) ? '/' : ''}>`
|
||
case isTextNode(node):
|
||
return hasExpressions(node) ? TEXT_NODE_EXPRESSION_PLACEHOLDER : unescapeNode(node, 'text').text
|
||
default:
|
||
return node.text || ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Close an html node
|
||
* @param {RiotParser.Node} node - riot parser node
|
||
* @returns {string} the closing tag of the html tag node passed to this function
|
||
*/
|
||
function closeTag(node) {
|
||
return node.name ? `</${node.name}>` : ''
|
||
}
|
||
|
||
/**
|
||
* Create a strings array with the `join` call to transform it into a string
|
||
* @param {Array} stringsArray - array containing all the strings to concatenate
|
||
* @returns {AST.CallExpression} array with a `join` call
|
||
*/
|
||
function createArrayString(stringsArray) {
|
||
return builders.callExpression(
|
||
builders.memberExpression(
|
||
builders.arrayExpression(stringsArray),
|
||
builders.identifier('join'),
|
||
false
|
||
),
|
||
[builders.literal('')]
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Simple expression bindings might contain multiple expressions like for example: "class="{foo} red {bar}""
|
||
* This helper aims to merge them in a template literal if it's necessary
|
||
* @param {RiotParser.Attr} node - riot parser node
|
||
* @param {string} sourceFile - original tag file
|
||
* @param {string} sourceCode - original tag source code
|
||
* @returns { Object } a template literal expression object
|
||
*/
|
||
function mergeAttributeExpressions(node, sourceFile, sourceCode) {
|
||
if (!node.parts || node.parts.length === 1) {
|
||
return transformExpression(node.expressions[0], sourceFile, sourceCode)
|
||
}
|
||
const stringsArray = [
|
||
...node.parts.reduce((acc, str) => {
|
||
const expression = node.expressions.find(e => e.text.trim() === str);
|
||
|
||
return [
|
||
...acc,
|
||
expression ?
|
||
transformExpression(expression, sourceFile, sourceCode) :
|
||
builders.literal(encodeHTMLEntities(str))
|
||
]
|
||
}, [])
|
||
].filter(expr => !isLiteral(expr) || expr.value);
|
||
|
||
|
||
return createArrayString(stringsArray)
|
||
}
|
||
|
||
/**
|
||
* Create a selector that will be used to find the node via dom-bindings
|
||
* @param {number} id - temporary variable that will be increased anytime this function will be called
|
||
* @returns {string} selector attribute needed to bind a riot expression
|
||
*/
|
||
const createBindingSelector = (function createSelector(id = 0) {
|
||
return () => `${BINDING_SELECTOR_PREFIX}${id++}`
|
||
}());
|
||
|
||
/**
|
||
* Create the AST array containing the attributes to bind to this node
|
||
* @param { RiotParser.Node.Tag } sourceNode - the custom tag
|
||
* @param { string } selectorAttribute - attribute needed to select the target node
|
||
* @param { string } sourceFile - source file path
|
||
* @param { string } sourceCode - original source
|
||
* @returns {AST.ArrayExpression} array containing the slot objects
|
||
*/
|
||
function createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) {
|
||
return builders.arrayExpression([
|
||
...compose(
|
||
attributes => attributes.map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)),
|
||
attributes => attributes.filter(hasExpressions),
|
||
attributes => getAttributesWithoutSelector(attributes, selectorAttribute),
|
||
cleanAttributes
|
||
)(sourceNode)
|
||
])
|
||
}
|
||
|
||
/**
|
||
* Create an attribute evaluation function
|
||
* @param {RiotParser.Attr} sourceNode - riot parser node
|
||
* @param {string} sourceFile - original tag file
|
||
* @param {string} sourceCode - original tag source code
|
||
* @returns { AST.Node } an AST function expression to evaluate the attribute value
|
||
*/
|
||
function createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) {
|
||
return hasExpressions(sourceNode) ?
|
||
// dynamic attribute
|
||
wrapASTInFunctionWithScope(mergeAttributeExpressions(sourceNode, sourceFile, sourceCode)) :
|
||
// static attribute
|
||
builders.arrowFunctionExpression(
|
||
[],
|
||
builders.literal(sourceNode.value || true)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Return a source map as JSON, it it has not the toJSON method it means it can
|
||
* be used right the way
|
||
* @param { SourceMapGenerator|Object } map - a sourcemap generator or simply an json object
|
||
* @returns { Object } the source map as JSON
|
||
*/
|
||
function sourcemapAsJSON(map) {
|
||
if (map && map.toJSON) return map.toJSON()
|
||
return map
|
||
}
|
||
|
||
/**
|
||
* Compose two sourcemaps
|
||
* @param { SourceMapGenerator } formerMap - original sourcemap
|
||
* @param { SourceMapGenerator } latterMap - target sourcemap
|
||
* @returns { Object } sourcemap json
|
||
*/
|
||
function composeSourcemaps(formerMap, latterMap) {
|
||
if (
|
||
isNode() &&
|
||
formerMap && latterMap && latterMap.mappings
|
||
) {
|
||
return composeSourceMaps(sourcemapAsJSON(formerMap), sourcemapAsJSON(latterMap))
|
||
} else if (isNode() && formerMap) {
|
||
return sourcemapAsJSON(formerMap)
|
||
}
|
||
|
||
return {}
|
||
}
|
||
|
||
/**
|
||
* Create a new sourcemap generator
|
||
* @param { Object } options - sourcemap options
|
||
* @returns { SourceMapGenerator } SourceMapGenerator instance
|
||
*/
|
||
function createSourcemap(options) {
|
||
return new SourceMapGenerator(options)
|
||
}
|
||
|
||
const Output = Object.freeze({
|
||
code: '',
|
||
ast: [],
|
||
meta: {},
|
||
map: null
|
||
});
|
||
|
||
/**
|
||
* Create the right output data result of a parsing
|
||
* @param { Object } data - output data
|
||
* @param { string } data.code - code generated
|
||
* @param { AST } data.ast - ast representing the code
|
||
* @param { SourceMapGenerator } data.map - source map generated along with the code
|
||
* @param { Object } meta - compilation meta infomration
|
||
* @returns { Output } output container object
|
||
*/
|
||
function createOutput(data, meta) {
|
||
const output = {
|
||
...Output,
|
||
...data,
|
||
meta
|
||
};
|
||
|
||
if (!output.map && meta && meta.options && meta.options.file)
|
||
return {
|
||
...output,
|
||
map: createSourcemap({ file: meta.options.file })
|
||
}
|
||
|
||
return output
|
||
}
|
||
|
||
/**
|
||
* Transform the source code received via a compiler function
|
||
* @param { Function } compiler - function needed to generate the output code
|
||
* @param { Object } meta - compilation meta information
|
||
* @param { string } source - source code
|
||
* @returns { Output } output - the result of the compiler
|
||
*/
|
||
function transform(compiler, meta, source) {
|
||
const result = (compiler ? compiler(source, meta) : { code: source });
|
||
return createOutput(result, meta)
|
||
}
|
||
|
||
/**
|
||
* Throw an error with a descriptive message
|
||
* @param { string } message - error message
|
||
* @returns { undefined } hoppla.. at this point the program should stop working
|
||
*/
|
||
function panic(message) {
|
||
throw new Error(message)
|
||
}
|
||
|
||
const postprocessors = new Set();
|
||
|
||
/**
|
||
* Register a postprocessor that will be used after the parsing and compilation of the riot tags
|
||
* @param { Function } postprocessor - transformer that will receive the output code ans sourcemap
|
||
* @returns { Set } the postprocessors collection
|
||
*/
|
||
function register$1(postprocessor) {
|
||
if (postprocessors.has(postprocessor)) {
|
||
panic(`This postprocessor "${postprocessor.name || postprocessor.toString()}" was already registered`);
|
||
}
|
||
|
||
postprocessors.add(postprocessor);
|
||
|
||
return postprocessors
|
||
}
|
||
|
||
/**
|
||
* Exec all the postprocessors in sequence combining the sourcemaps generated
|
||
* @param { Output } compilerOutput - output generated by the compiler
|
||
* @param { Object } meta - compiling meta information
|
||
* @returns { Output } object containing output code and source map
|
||
*/
|
||
function execute$1(compilerOutput, meta) {
|
||
return Array.from(postprocessors).reduce(function(acc, postprocessor) {
|
||
const { code, map } = acc;
|
||
const output = postprocessor(code, meta);
|
||
|
||
return {
|
||
code: output.code,
|
||
map: composeSourcemaps(map, output.map)
|
||
}
|
||
}, createOutput(compilerOutput, meta))
|
||
}
|
||
|
||
/**
|
||
* Parsers that can be registered by users to preparse components fragments
|
||
* @type { Object }
|
||
*/
|
||
const preprocessors = Object.freeze({
|
||
javascript: new Map(),
|
||
css: new Map(),
|
||
template: new Map().set('default', code => ({ code }))
|
||
});
|
||
|
||
// throw a processor type error
|
||
function preprocessorTypeError(type) {
|
||
panic(`No preprocessor of type "${type}" was found, please make sure to use one of these: 'javascript', 'css' or 'template'`);
|
||
}
|
||
|
||
// throw an error if the preprocessor was not registered
|
||
function preprocessorNameNotFoundError(name) {
|
||
panic(`No preprocessor named "${name}" was found, are you sure you have registered it?'`);
|
||
}
|
||
|
||
/**
|
||
* Register a custom preprocessor
|
||
* @param { string } type - preprocessor type either 'js', 'css' or 'template'
|
||
* @param { string } name - unique preprocessor id
|
||
* @param { Function } preprocessor - preprocessor function
|
||
* @returns { Map } - the preprocessors map
|
||
*/
|
||
function register(type, name, preprocessor) {
|
||
if (!type) panic('Please define the type of preprocessor you want to register \'javascript\', \'css\' or \'template\'');
|
||
if (!name) panic('Please define a name for your preprocessor');
|
||
if (!preprocessor) panic('Please provide a preprocessor function');
|
||
if (!preprocessors[type]) preprocessorTypeError(type);
|
||
if (preprocessors[type].has(name)) panic(`The preprocessor ${name} was already registered before`);
|
||
|
||
preprocessors[type].set(name, preprocessor);
|
||
|
||
return preprocessors
|
||
}
|
||
|
||
/**
|
||
* Exec the compilation of a preprocessor
|
||
* @param { string } type - preprocessor type either 'js', 'css' or 'template'
|
||
* @param { string } name - unique preprocessor id
|
||
* @param { Object } meta - preprocessor meta information
|
||
* @param { string } source - source code
|
||
* @returns { Output } object containing a sourcemap and a code string
|
||
*/
|
||
function execute(type, name, meta, source) {
|
||
if (!preprocessors[type]) preprocessorTypeError(type);
|
||
if (!preprocessors[type].has(name)) preprocessorNameNotFoundError(name);
|
||
|
||
return transform(preprocessors[type].get(name), meta, source)
|
||
}
|
||
|
||
/**
|
||
* Simple clone deep function, do not use it for classes or recursive objects!
|
||
* @param {*} source - possibily an object to clone
|
||
* @returns {*} the object we wanted to clone
|
||
*/
|
||
function cloneDeep(source) {
|
||
return JSON.parse(JSON.stringify(source))
|
||
}
|
||
|
||
/**
|
||
* Generate the javascript from an ast source
|
||
* @param {AST} ast - ast object
|
||
* @param {Object} options - printer options
|
||
* @returns {Object} code + map
|
||
*/
|
||
function generateJavascript(ast, options) {
|
||
return print(ast, {
|
||
...options,
|
||
parser: {
|
||
parse: (source, opts) => parse$2(source, {
|
||
...opts,
|
||
ecmaVersion: 'latest'
|
||
})
|
||
},
|
||
tabWidth: 2,
|
||
wrapColumn: 0,
|
||
quote: 'single'
|
||
})
|
||
}
|
||
|
||
const getEachItemName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[0] : expression.left;
|
||
const getEachIndexName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[1] : null;
|
||
const getEachValue = expression => expression.right;
|
||
const nameToliteral = compose(builders.literal, getName);
|
||
|
||
const generateEachItemNameKey = expression => simplePropertyNode(
|
||
BINDING_ITEM_NAME_KEY,
|
||
compose(nameToliteral, getEachItemName)(expression)
|
||
);
|
||
|
||
const generateEachIndexNameKey = expression => simplePropertyNode(
|
||
BINDING_INDEX_NAME_KEY,
|
||
compose(nameToliteral, getEachIndexName)(expression)
|
||
);
|
||
|
||
const generateEachEvaluateKey = (expression, eachExpression, sourceFile, sourceCode) => simplePropertyNode(
|
||
BINDING_EVALUATE_KEY,
|
||
compose(
|
||
e => toScopedFunction(e, sourceFile, sourceCode),
|
||
e => ({
|
||
...eachExpression,
|
||
text: generateJavascript(e).code
|
||
}),
|
||
getEachValue
|
||
)(expression)
|
||
);
|
||
|
||
/**
|
||
* Get the each expression properties to create properly the template binding
|
||
* @param { DomBinding.Expression } eachExpression - original each expression data
|
||
* @param { string } sourceFile - original tag file
|
||
* @param { string } sourceCode - original tag source code
|
||
* @returns { Array } AST nodes that are needed to build an each binding
|
||
*/
|
||
function generateEachExpressionProperties(eachExpression, sourceFile, sourceCode) {
|
||
const ast = createASTFromExpression(eachExpression, sourceFile, sourceCode);
|
||
const body = ast.program.body;
|
||
const firstNode = body[0];
|
||
|
||
if (!isExpressionStatement(firstNode)) {
|
||
panic(`The each directives supported should be of type "ExpressionStatement",you have provided a "${firstNode.type}"`);
|
||
}
|
||
|
||
const { expression } = firstNode;
|
||
|
||
return [
|
||
generateEachItemNameKey(expression),
|
||
generateEachIndexNameKey(expression),
|
||
generateEachEvaluateKey(expression, eachExpression, sourceFile, sourceCode)
|
||
]
|
||
}
|
||
|
||
/**
|
||
* Transform a RiotParser.Node.Tag into an each binding
|
||
* @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute
|
||
* @param { string } selectorAttribute - attribute needed to select the target node
|
||
* @param { string } sourceFile - source file path
|
||
* @param { string } sourceCode - original source
|
||
* @returns { AST.Node } an each binding node
|
||
*/
|
||
function createEachBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) {
|
||
const [ifAttribute, eachAttribute, keyAttribute] = [
|
||
findIfAttribute,
|
||
findEachAttribute,
|
||
findKeyAttribute
|
||
].map(f => f(sourceNode));
|
||
const attributeOrNull = attribute => attribute ? toScopedFunction(getAttributeExpression(attribute), sourceFile, sourceCode) : nullNode();
|
||
|
||
return builders.objectExpression([
|
||
simplePropertyNode(BINDING_TYPE_KEY,
|
||
builders.memberExpression(
|
||
builders.identifier(BINDING_TYPES),
|
||
builders.identifier(EACH_BINDING_TYPE),
|
||
false
|
||
)
|
||
),
|
||
simplePropertyNode(BINDING_GET_KEY_KEY, attributeOrNull(keyAttribute)),
|
||
simplePropertyNode(BINDING_CONDITION_KEY, attributeOrNull(ifAttribute)),
|
||
createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)),
|
||
...createSelectorProperties(selectorAttribute),
|
||
...compose(generateEachExpressionProperties, getAttributeExpression)(eachAttribute)
|
||
])
|
||
}
|
||
|
||
/**
|
||
* Transform a RiotParser.Node.Tag into an if binding
|
||
* @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute
|
||
* @param { string } selectorAttribute - attribute needed to select the target node
|
||
* @param { stiring } sourceFile - source file path
|
||
* @param { string } sourceCode - original source
|
||
* @returns { AST.Node } an if binding node
|
||
*/
|
||
function createIfBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) {
|
||
const ifAttribute = findIfAttribute(sourceNode);
|
||
|
||
return builders.objectExpression([
|
||
simplePropertyNode(BINDING_TYPE_KEY,
|
||
builders.memberExpression(
|
||
builders.identifier(BINDING_TYPES),
|
||
builders.identifier(IF_BINDING_TYPE),
|
||
false
|
||
)
|
||
),
|
||
simplePropertyNode(
|
||
BINDING_EVALUATE_KEY,
|
||
toScopedFunction(ifAttribute.expressions[0], sourceFile, sourceCode)
|
||
),
|
||
...createSelectorProperties(selectorAttribute),
|
||
createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute))
|
||
])
|
||
}
|
||
|
||
/**
|
||
* Create the text node expressions
|
||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
|
||
* @param {string} sourceFile - source file path
|
||
* @param {string} sourceCode - original source
|
||
* @returns {Array} array containing all the text node expressions
|
||
*/
|
||
function createTextNodeExpressions(sourceNode, sourceFile, sourceCode) {
|
||
const childrenNodes = getChildrenNodes(sourceNode);
|
||
|
||
return childrenNodes
|
||
.filter(isTextNode)
|
||
.filter(hasExpressions)
|
||
.map(node => createExpression(
|
||
node,
|
||
sourceFile,
|
||
sourceCode,
|
||
childrenNodes.indexOf(node),
|
||
sourceNode
|
||
))
|
||
}
|
||
|
||
/**
|
||
* Add a simple binding to a riot parser node
|
||
* @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute
|
||
* @param { string } selectorAttribute - attribute needed to select the target node
|
||
* @param { string } sourceFile - source file path
|
||
* @param { string } sourceCode - original source
|
||
* @returns { AST.Node } an each binding node
|
||
*/
|
||
function createSimpleBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) {
|
||
return builders.objectExpression([
|
||
// root or removable nodes do not need selectors
|
||
...(isRemovableNode(sourceNode) || isRootNode(sourceNode) ? [] : createSelectorProperties(selectorAttribute)),
|
||
simplePropertyNode(
|
||
BINDING_EXPRESSIONS_KEY,
|
||
builders.arrayExpression([
|
||
...createTextNodeExpressions(sourceNode, sourceFile, sourceCode),
|
||
...createAttributeExpressions(sourceNode, sourceFile, sourceCode)
|
||
])
|
||
)
|
||
])
|
||
}
|
||
|
||
/**
|
||
* Transform a RiotParser.Node.Tag of type slot into a slot binding
|
||
* @param { RiotParser.Node.Tag } sourceNode - slot node
|
||
* @param { string } selectorAttribute - attribute needed to select the target node
|
||
* @param { string } sourceFile - source file path
|
||
* @param { string } sourceCode - original source
|
||
* @returns { AST.Node } a slot binding node
|
||
*/
|
||
function createSlotBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) {
|
||
const slotNameAttribute = findAttribute(NAME_ATTRIBUTE, sourceNode);
|
||
const slotName = slotNameAttribute ? slotNameAttribute.value : DEFAULT_SLOT_NAME;
|
||
|
||
return builders.objectExpression([
|
||
simplePropertyNode(BINDING_TYPE_KEY,
|
||
builders.memberExpression(
|
||
builders.identifier(BINDING_TYPES),
|
||
builders.identifier(SLOT_BINDING_TYPE),
|
||
false
|
||
)
|
||
),
|
||
simplePropertyNode(
|
||
BINDING_ATTRIBUTES_KEY,
|
||
createBindingAttributes(
|
||
{
|
||
...sourceNode,
|
||
// filter the name attribute
|
||
attributes: getNodeAttributes(sourceNode)
|
||
.filter(attribute => getName(attribute) !== NAME_ATTRIBUTE)
|
||
},
|
||
selectorAttribute,
|
||
sourceFile,
|
||
sourceCode
|
||
)
|
||
),
|
||
simplePropertyNode(
|
||
BINDING_NAME_KEY,
|
||
builders.literal(slotName)
|
||
),
|
||
...createSelectorProperties(selectorAttribute)
|
||
])
|
||
}
|
||
|
||
/**
|
||
* Find the slots in the current component and group them under the same id
|
||
* @param {RiotParser.Node.Tag} sourceNode - the custom tag
|
||
* @returns {Object} object containing all the slots grouped by name
|
||
*/
|
||
function groupSlots(sourceNode) {
|
||
return getChildrenNodes(sourceNode).reduce((acc, node) => {
|
||
const slotAttribute = findSlotAttribute(node);
|
||
|
||
if (slotAttribute) {
|
||
acc[slotAttribute.value] = node;
|
||
} else {
|
||
acc.default = createNestedRootNode({
|
||
nodes: [...getChildrenNodes(acc.default), node]
|
||
});
|
||
}
|
||
|
||
return acc
|
||
}, {
|
||
default: null
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Create the slot entity to pass to the riot-dom bindings
|
||
* @param {string} id - slot id
|
||
* @param {RiotParser.Node.Tag} sourceNode - slot root node
|
||
* @param {string} sourceFile - source file path
|
||
* @param {string} sourceCode - original source
|
||
* @returns {AST.Node} ast node containing the slot object properties
|
||
*/
|
||
function buildSlot(id, sourceNode, sourceFile, sourceCode) {
|
||
const cloneNode = {
|
||
...sourceNode,
|
||
attributes: getNodeAttributes(sourceNode)
|
||
};
|
||
const [html, bindings] = build(cloneNode, sourceFile, sourceCode);
|
||
|
||
return builders.objectExpression([
|
||
simplePropertyNode(BINDING_ID_KEY, builders.literal(id)),
|
||
simplePropertyNode(BINDING_HTML_KEY, builders.literal(html)),
|
||
simplePropertyNode(BINDING_BINDINGS_KEY, builders.arrayExpression(bindings))
|
||
])
|
||
}
|
||
|
||
/**
|
||
* Create the AST array containing the slots
|
||
* @param { RiotParser.Node.Tag } sourceNode - the custom tag
|
||
* @param { string } sourceFile - source file path
|
||
* @param { string } sourceCode - original source
|
||
* @returns {AST.ArrayExpression} array containing the attributes to bind
|
||
*/
|
||
function createSlotsArray(sourceNode, sourceFile, sourceCode) {
|
||
return builders.arrayExpression([
|
||
...compose(
|
||
slots => slots.map(([key, value]) => buildSlot(key, value, sourceFile, sourceCode)),
|
||
slots => slots.filter(([, value]) => value),
|
||
Object.entries,
|
||
groupSlots
|
||
)(sourceNode)
|
||
])
|
||
}
|
||
|
||
/**
|
||
* Find the slot attribute if it exists
|
||
* @param {RiotParser.Node.Tag} sourceNode - the custom tag
|
||
* @returns {RiotParser.Node.Attr|undefined} the slot attribute found
|
||
*/
|
||
function findSlotAttribute(sourceNode) {
|
||
return getNodeAttributes(sourceNode).find(attribute => attribute.name === SLOT_ATTRIBUTE)
|
||
}
|
||
|
||
/**
|
||
* Transform a RiotParser.Node.Tag into a tag binding
|
||
* @param { RiotParser.Node.Tag } sourceNode - the custom tag
|
||
* @param { string } selectorAttribute - attribute needed to select the target node
|
||
* @param { string } sourceFile - source file path
|
||
* @param { string } sourceCode - original source
|
||
* @returns { AST.Node } tag binding node
|
||
*/
|
||
function createTagBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) {
|
||
return builders.objectExpression([
|
||
simplePropertyNode(BINDING_TYPE_KEY,
|
||
builders.memberExpression(
|
||
builders.identifier(BINDING_TYPES),
|
||
builders.identifier(TAG_BINDING_TYPE),
|
||
false
|
||
)
|
||
),
|
||
simplePropertyNode(BINDING_GET_COMPONENT_KEY, builders.identifier(GET_COMPONENT_FN)),
|
||
simplePropertyNode(
|
||
BINDING_EVALUATE_KEY,
|
||
createCustomNodeNameEvaluationFunction(sourceNode, sourceFile, sourceCode)
|
||
),
|
||
simplePropertyNode(BINDING_SLOTS_KEY, createSlotsArray(sourceNode, sourceFile, sourceCode)),
|
||
simplePropertyNode(
|
||
BINDING_ATTRIBUTES_KEY,
|
||
createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode)
|
||
),
|
||
...createSelectorProperties(selectorAttribute)
|
||
])
|
||
}
|
||
|
||
const BuildingState = Object.freeze({
|
||
html: [],
|
||
bindings: [],
|
||
parent: null
|
||
});
|
||
|
||
/**
|
||
* Nodes having bindings should be cloned and new selector properties should be added to them
|
||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
|
||
* @param {string} bindingsSelector - temporary string to identify the current node
|
||
* @returns {RiotParser.Node} the original node parsed having the new binding selector attribute
|
||
*/
|
||
function createBindingsTag(sourceNode, bindingsSelector) {
|
||
if (!bindingsSelector) return sourceNode
|
||
|
||
return {
|
||
...sourceNode,
|
||
// inject the selector bindings into the node attributes
|
||
attributes: [{
|
||
name: bindingsSelector,
|
||
value: bindingsSelector
|
||
}, ...getNodeAttributes(sourceNode)]
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create a generic dynamic node (text or tag) and generate its bindings
|
||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
|
||
* @param {string} sourceFile - source file path
|
||
* @param {string} sourceCode - original source
|
||
* @param {BuildingState} state - state representing the current building tree state during the recursion
|
||
* @returns {Array} array containing the html output and bindings for the current node
|
||
*/
|
||
function createDynamicNode(sourceNode, sourceFile, sourceCode, state) {
|
||
switch (true) {
|
||
case isTextNode(sourceNode):
|
||
// text nodes will not have any bindings
|
||
return [nodeToString(sourceNode), []]
|
||
default:
|
||
return createTagWithBindings(sourceNode, sourceFile, sourceCode)
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create only a dynamic tag node with generating a custom selector and its bindings
|
||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
|
||
* @param {string} sourceFile - source file path
|
||
* @param {string} sourceCode - original source
|
||
* @param {BuildingState} state - state representing the current building tree state during the recursion
|
||
* @returns {Array} array containing the html output and bindings for the current node
|
||
*/
|
||
function createTagWithBindings(sourceNode, sourceFile, sourceCode) {
|
||
const bindingsSelector = isRootNode(sourceNode) ? null : createBindingSelector();
|
||
const cloneNode = createBindingsTag(sourceNode, bindingsSelector);
|
||
const tagOpeningHTML = nodeToString(cloneNode);
|
||
|
||
switch (true) {
|
||
case hasEachAttribute(cloneNode):
|
||
// EACH bindings have prio 1
|
||
return [tagOpeningHTML, [createEachBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]]
|
||
case hasIfAttribute(cloneNode):
|
||
// IF bindings have prio 2
|
||
return [tagOpeningHTML, [createIfBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]]
|
||
case isCustomNode(cloneNode):
|
||
// TAG bindings have prio 3
|
||
return [tagOpeningHTML, [createTagBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]]
|
||
case isSlotNode(cloneNode):
|
||
// slot tag
|
||
return [tagOpeningHTML, [createSlotBinding(cloneNode, bindingsSelector)]]
|
||
default:
|
||
// this node has expressions bound to it
|
||
return [tagOpeningHTML, [createSimpleBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]]
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Parse a node trying to extract its template and bindings
|
||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
|
||
* @param {string} sourceFile - source file path
|
||
* @param {string} sourceCode - original source
|
||
* @param {BuildingState} state - state representing the current building tree state during the recursion
|
||
* @returns {Array} array containing the html output and bindings for the current node
|
||
*/
|
||
function parseNode(sourceNode, sourceFile, sourceCode, state) {
|
||
// static nodes have no bindings
|
||
if (isStaticNode(sourceNode)) return [nodeToString(sourceNode), []]
|
||
return createDynamicNode(sourceNode, sourceFile, sourceCode)
|
||
}
|
||
|
||
/**
|
||
* Create the tag binding
|
||
* @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute
|
||
* @param { string } sourceFile - source file path
|
||
* @param { string } sourceCode - original source
|
||
* @param { string } selector - binding selector
|
||
* @returns { Array } array with only the tag binding AST
|
||
*/
|
||
function createNestedBindings(sourceNode, sourceFile, sourceCode, selector) {
|
||
const mightBeARiotComponent = isCustomNode(sourceNode);
|
||
const node = cloneNodeWithoutSelectorAttribute(sourceNode, selector);
|
||
|
||
return mightBeARiotComponent ? [null, [
|
||
createTagBinding(
|
||
node,
|
||
null,
|
||
sourceFile,
|
||
sourceCode
|
||
)]
|
||
] : build(createNestedRootNode(node), sourceFile, sourceCode)
|
||
}
|
||
|
||
/**
|
||
* Build the template and the bindings
|
||
* @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
|
||
* @param {string} sourceFile - source file path
|
||
* @param {string} sourceCode - original source
|
||
* @param {BuildingState} state - state representing the current building tree state during the recursion
|
||
* @returns {Array} array containing the html output and the dom bindings
|
||
*/
|
||
function build(
|
||
sourceNode,
|
||
sourceFile,
|
||
sourceCode,
|
||
state
|
||
) {
|
||
if (!sourceNode) panic('Something went wrong with your tag DOM parsing, your tag template can\'t be created');
|
||
|
||
const [nodeHTML, nodeBindings] = parseNode(sourceNode, sourceFile, sourceCode);
|
||
const childrenNodes = getChildrenNodes(sourceNode);
|
||
const canRenderNodeHTML = isRemovableNode(sourceNode) === false;
|
||
const currentState = { ...cloneDeep(BuildingState), ...state };
|
||
|
||
// mutate the original arrays
|
||
canRenderNodeHTML && currentState.html.push(...nodeHTML);
|
||
currentState.bindings.push(...nodeBindings);
|
||
|
||
// do recursion if
|
||
// this tag has children and it has no special directives bound to it
|
||
if (childrenNodes.length && !hasItsOwnTemplate(sourceNode)) {
|
||
childrenNodes.forEach(node => build(node, sourceFile, sourceCode, { parent: sourceNode, ...currentState }));
|
||
}
|
||
|
||
// close the tag if it's not a void one
|
||
if (canRenderNodeHTML && isTagNode(sourceNode) && !isVoidNode(sourceNode)) {
|
||
currentState.html.push(closeTag(sourceNode));
|
||
}
|
||
|
||
return [
|
||
currentState.html.join(''),
|
||
currentState.bindings
|
||
]
|
||
}
|
||
|
||
const ATTRIBUTE_TYPE_NAME = 'type';
|
||
|
||
/**
|
||
* Get the type attribute from a node generated by the riot parser
|
||
* @param { Object} sourceNode - riot parser node
|
||
* @returns { string|null } a valid type to identify the preprocessor to use or nothing
|
||
*/
|
||
function getPreprocessorTypeByAttribute(sourceNode) {
|
||
const typeAttribute = sourceNode.attributes ?
|
||
sourceNode.attributes.find(attribute => attribute.name === ATTRIBUTE_TYPE_NAME) :
|
||
null;
|
||
|
||
return typeAttribute ? normalize(typeAttribute.value) : null
|
||
}
|
||
|
||
|
||
/**
|
||
* Remove the noise in case a user has defined the preprocessor type='text/scss'
|
||
* @param { string } value - input string
|
||
* @returns { string } normalized string
|
||
*/
|
||
function normalize(value) {
|
||
return value.replace('text/', '')
|
||
}
|
||
|
||
/**
|
||
* Preprocess a riot parser node
|
||
* @param { string } preprocessorType - either css, js
|
||
* @param { string } preprocessorName - preprocessor id
|
||
* @param { Object } meta - compilation meta information
|
||
* @param { RiotParser.nodeTypes } node - css node detected by the parser
|
||
* @returns { Output } code and sourcemap generated by the preprocessor
|
||
*/
|
||
function preprocess(preprocessorType, preprocessorName, meta, node) {
|
||
const code = node.text;
|
||
|
||
return (preprocessorName ?
|
||
execute(preprocessorType, preprocessorName, meta, code) :
|
||
{ code }
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Matches valid, multiline JavaScript comments in almost all its forms.
|
||
* @const {RegExp}
|
||
* @static
|
||
*/
|
||
const R_MLCOMMS = /\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//g;
|
||
|
||
/**
|
||
* Source for creating regexes matching valid quoted, single-line JavaScript strings.
|
||
* It recognizes escape characters, including nested quotes and line continuation.
|
||
* @const {string}
|
||
*/
|
||
const S_LINESTR = /"[^"\n\\]*(?:\\[\S\s][^"\n\\]*)*"|'[^'\n\\]*(?:\\[\S\s][^'\n\\]*)*'/.source;
|
||
|
||
/**
|
||
* Matches CSS selectors, excluding those beginning with '@' and quoted strings.
|
||
* @const {RegExp}
|
||
*/
|
||
|
||
const CSS_SELECTOR = RegExp(`([{}]|^)[; ]*((?:[^@ ;{}][^{}]*)?[^@ ;{}:] ?)(?={)|${S_LINESTR}`, 'g');
|
||
|
||
/**
|
||
* Parses styles enclosed in a "scoped" tag
|
||
* The "css" string is received without comments or surrounding spaces.
|
||
*
|
||
* @param {string} tag - Tag name of the root element
|
||
* @param {string} css - The CSS code
|
||
* @returns {string} CSS with the styles scoped to the root element
|
||
*/
|
||
function scopedCSS(tag, css) {
|
||
const host = ':host';
|
||
const selectorsBlacklist = ['from', 'to'];
|
||
|
||
return css.replace(CSS_SELECTOR, function(m, p1, p2) {
|
||
// skip quoted strings
|
||
if (!p2) return m
|
||
|
||
// we have a selector list, parse each individually
|
||
p2 = p2.replace(/[^,]+/g, function(sel) {
|
||
const s = sel.trim();
|
||
|
||
// skip selectors already using the tag name
|
||
if (s.indexOf(tag) === 0) {
|
||
return sel
|
||
}
|
||
|
||
// skips the keywords and percents of css animations
|
||
if (!s || selectorsBlacklist.indexOf(s) > -1 || s.slice(-1) === '%') {
|
||
return sel
|
||
}
|
||
|
||
// replace the `:host` pseudo-selector, where it is, with the root tag name;
|
||
// if `:host` was not included, add the tag name as prefix, and mirror all
|
||
// `[data-is]`
|
||
if (s.indexOf(host) < 0) {
|
||
return `${tag} ${s},[is="${tag}"] ${s}`
|
||
} else {
|
||
return `${s.replace(host, tag)},${
|
||
s.replace(host, `[is="${tag}"]`)}`
|
||
}
|
||
});
|
||
|
||
// add the danling bracket char and return the processed selector list
|
||
return p1 ? `${p1} ${p2}` : p2
|
||
})
|
||
}
|
||
|
||
/**
|
||
* Remove comments, compact and trim whitespace
|
||
* @param { string } code - compiled css code
|
||
* @returns { string } css code normalized
|
||
*/
|
||
function compactCss(code) {
|
||
return code.replace(R_MLCOMMS, '').replace(/\s+/g, ' ').trim()
|
||
}
|
||
|
||
const escapeBackslashes = s => s.replace(/\\/g, '\\\\');
|
||
const escapeIdentifier = identifier => escapeBackslashes(cssEscape(identifier, {
|
||
isIdentifier: true
|
||
}));
|
||
|
||
/**
|
||
* Generate the component css
|
||
* @param { Object } sourceNode - node generated by the riot compiler
|
||
* @param { string } source - original component source code
|
||
* @param { Object } meta - compilation meta information
|
||
* @param { AST } ast - current AST output
|
||
* @returns { AST } the AST generated
|
||
*/
|
||
function css(sourceNode, source, meta, ast) {
|
||
const preprocessorName = getPreprocessorTypeByAttribute(sourceNode);
|
||
const { options } = meta;
|
||
const preprocessorOutput = preprocess('css', preprocessorName, meta, sourceNode.text);
|
||
const normalizedCssCode = compactCss(preprocessorOutput.code);
|
||
const escapedCssIdentifier = escapeIdentifier(meta.tagName);
|
||
|
||
const cssCode = (options.scopedCss ?
|
||
scopedCSS(escapedCssIdentifier, escapeBackslashes(normalizedCssCode)) :
|
||
escapeBackslashes(normalizedCssCode)
|
||
).trim();
|
||
|
||
types.visit(ast, {
|
||
visitProperty(path) {
|
||
if (path.value.key.name === TAG_CSS_PROPERTY) {
|
||
path.value.value = builders.templateLiteral(
|
||
[builders.templateElement({ raw: cssCode, cooked: '' }, false)],
|
||
[]
|
||
);
|
||
|
||
return false
|
||
}
|
||
|
||
this.traverse(path);
|
||
}
|
||
});
|
||
|
||
return ast
|
||
}
|
||
|
||
/**
|
||
* Find whether there is html code outside of the root node
|
||
* @param {RiotParser.Node} root - node generated by the riot compiler
|
||
* @param {string} code - riot tag source code
|
||
* @param {Function} parse - riot parser function
|
||
* @returns {boolean} true if extra markup is detected
|
||
*/
|
||
function hasHTMLOutsideRootNode(root, code, parse) {
|
||
const additionalCode = root ? [
|
||
// head
|
||
code.substr(0, root.start),
|
||
// tail
|
||
code.substr(root.end, code.length)
|
||
].join('').trim() : '';
|
||
|
||
if (additionalCode) {
|
||
// if there are parsing errors we assume that there are no html
|
||
// tags outside of the root node
|
||
try {
|
||
const { template, javascript, css } = parse(additionalCode).output;
|
||
|
||
return [template, javascript, css].some(isObject)
|
||
} catch (error) {
|
||
return false
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
/**
|
||
* Ckeck if an Array-like object has empty length
|
||
* @param {Array} target - Array-like object
|
||
* @returns {boolean} target is empty or null
|
||
*/
|
||
function isEmptyArray(target) {
|
||
return !target || !target.length
|
||
}
|
||
|
||
/**
|
||
* True if the sourcemap has no mappings, it is empty
|
||
* @param {Object} map - sourcemap json
|
||
* @returns {boolean} true if empty
|
||
*/
|
||
function isEmptySourcemap(map) {
|
||
return !map || isEmptyArray(map.mappings)
|
||
}
|
||
|
||
/**
|
||
* Find the export default statement
|
||
* @param { Array } body - tree structure containing the program code
|
||
* @returns { Object } node containing only the code of the export default statement
|
||
*/
|
||
function findExportDefaultStatement(body) {
|
||
return body.find(isExportDefaultStatement)
|
||
}
|
||
|
||
/**
|
||
* Find all import declarations
|
||
* @param { Array } body - tree structure containing the program code
|
||
* @returns { Array } array containing all the import declarations detected
|
||
*/
|
||
function findAllImportDeclarations(body) {
|
||
return body.filter(isImportDeclaration)
|
||
}
|
||
|
||
/**
|
||
* Find all the named export declarations
|
||
* @param { Array } body - tree structure containing the program code
|
||
* @returns { Array } array containing all the named export declarations detected
|
||
*/
|
||
function findAllExportNamedDeclarations(body) {
|
||
return body.filter(isExportNamedDeclaration)
|
||
}
|
||
|
||
/**
|
||
* Filter all the import declarations
|
||
* @param { Array } body - tree structure containing the program code
|
||
* @returns { Array } array containing all the ast expressions without the import declarations
|
||
*/
|
||
function filterOutAllImportDeclarations(body) {
|
||
return body.filter(n => !isImportDeclaration(n))
|
||
}
|
||
|
||
/**
|
||
* Filter all the export declarations
|
||
* @param { Array } body - tree structure containing the program code
|
||
* @returns { Array } array containing all the ast expressions without the export declarations
|
||
*/
|
||
function filterOutAllExportDeclarations(body) {
|
||
return body.filter(n => !isExportNamedDeclaration(n) || isExportDefaultStatement(n))
|
||
}
|
||
|
||
/**
|
||
* Find the component interface exported
|
||
* @param { Array } body - tree structure containing the program code
|
||
* @returns { Object|null } the object referencing the component interface if found
|
||
*/
|
||
function findComponentInterface(body) {
|
||
const exportNamedDeclarations = body.filter(isExportNamedDeclaration).map(n => n.declaration);
|
||
const types = exportNamedDeclarations.filter(isTypeAliasDeclaration);
|
||
const interfaces = exportNamedDeclarations.filter(isInterfaceDeclaration);
|
||
const isRiotComponentTypeName = ({ typeName }) => typeName && typeName.name ? typeName.name === RIOT_TAG_INTERFACE_NAME : false;
|
||
const extendsRiotComponent = ({ expression }) => expression.name === RIOT_TAG_INTERFACE_NAME;
|
||
|
||
return types.find(
|
||
node => (node.typeAnnotation.types && node.typeAnnotation.types.some(isRiotComponentTypeName)) || isRiotComponentTypeName(node.typeAnnotation)
|
||
) || interfaces.find(
|
||
node => node.extends && node.extends.some(extendsRiotComponent)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Add the component interface to the export declaration
|
||
* @param { Object } ast - ast object generated by recast
|
||
* @param { Object } componentInterface - the component typescript interface
|
||
* @returns { Object } the component object exported combined with the riot typescript interfaces
|
||
*/
|
||
function addComponentInterfaceToExportedObject(ast, componentInterface) {
|
||
const body = getProgramBody(ast);
|
||
const RiotComponentWrapperImportSpecifier = builders.importSpecifier(
|
||
builders.identifier(RIOT_INTERFACE_WRAPPER_NAME)
|
||
);
|
||
const componentInterfaceName = componentInterface.id.name;
|
||
const riotImportDeclaration = findAllImportDeclarations(body).find(node => node.source.value === RIOT_MODULE_ID);
|
||
const exportDefaultStatement = body.find(isExportDefaultStatement);
|
||
const objectExport = exportDefaultStatement.declaration;
|
||
|
||
// add the RiotComponentWrapper to this component imports
|
||
if (riotImportDeclaration) {
|
||
riotImportDeclaration.specifiers.push(RiotComponentWrapperImportSpecifier);
|
||
} else {
|
||
// otherwise create the whole import statement from riot
|
||
body.unshift(0, builders.importDeclaration(
|
||
[RiotComponentWrapperImportSpecifier],
|
||
builders.stringLiteral(RIOT_MODULE_ID)
|
||
));
|
||
}
|
||
|
||
// override the object export adding the types detected
|
||
exportDefaultStatement.declaration = builders.tsAsExpression(
|
||
objectExport,
|
||
builders.tsTypeReference(
|
||
builders.identifier(RIOT_INTERFACE_WRAPPER_NAME),
|
||
builders.tsTypeParameterInstantiation(
|
||
[builders.tsTypeReference(builders.identifier(componentInterfaceName))]
|
||
)
|
||
)
|
||
);
|
||
|
||
return ast
|
||
}
|
||
|
||
/**
|
||
* Create the default export declaration interpreting the old riot syntax relying on "this" statements
|
||
* @param { Array } body - tree structure containing the program code
|
||
* @returns { Object } ExportDefaultDeclaration
|
||
*/
|
||
function createDefaultExportFromLegacySyntax(body) {
|
||
return builders.exportDefaultDeclaration(
|
||
builders.functionDeclaration(
|
||
builders.identifier(TAG_LOGIC_PROPERTY),
|
||
[],
|
||
builders.blockStatement([
|
||
...compose(filterOutAllImportDeclarations, filterOutAllExportDeclarations)(body),
|
||
builders.returnStatement(builders.thisExpression())
|
||
])
|
||
)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Find all the code in an ast program except for the export default statements
|
||
* @param { Array } body - tree structure containing the program code
|
||
* @returns { Array } array containing all the program code except the export default expressions
|
||
*/
|
||
function filterNonExportDefaultStatements(body) {
|
||
return body.filter(node => !isExportDefaultStatement(node) && !isThisExpressionStatement(node))
|
||
}
|
||
|
||
/**
|
||
* Get the body of the AST structure
|
||
* @param { Object } ast - ast object generated by recast
|
||
* @returns { Array } array containing the program code
|
||
*/
|
||
function getProgramBody(ast) {
|
||
return ast.body || ast.program.body
|
||
}
|
||
|
||
/**
|
||
* Extend the AST adding the new tag method containing our tag sourcecode
|
||
* @param { Object } ast - current output ast
|
||
* @param { Object } exportDefaultNode - tag export default node
|
||
* @returns { Object } the output ast having the "tag" key extended with the content of the export default
|
||
*/
|
||
function extendTagProperty(ast, exportDefaultNode) {
|
||
types.visit(ast, {
|
||
visitProperty(path) {
|
||
if (path.value.key.name === TAG_LOGIC_PROPERTY) {
|
||
path.value.value = exportDefaultNode.declaration;
|
||
return false
|
||
}
|
||
|
||
this.traverse(path);
|
||
}
|
||
});
|
||
|
||
return ast
|
||
}
|
||
|
||
/**
|
||
* Generate the component javascript logic
|
||
* @param { Object } sourceNode - node generated by the riot compiler
|
||
* @param { string } source - original component source code
|
||
* @param { Object } meta - compilation meta information
|
||
* @param { AST } ast - current AST output
|
||
* @returns { AST } the AST generated
|
||
*/
|
||
function javascript(sourceNode, source, meta, ast) {
|
||
const preprocessorName = getPreprocessorTypeByAttribute(sourceNode);
|
||
const javascriptNode = addLineOffset(sourceNode.text.text, source, sourceNode);
|
||
const { options } = meta;
|
||
const preprocessorOutput = preprocess('javascript', preprocessorName, meta, {
|
||
...sourceNode,
|
||
text: javascriptNode
|
||
});
|
||
const inputSourceMap = sourcemapAsJSON(preprocessorOutput.map);
|
||
const generatedAst = generateAST(preprocessorOutput.code, {
|
||
sourceFileName: options.file,
|
||
inputSourceMap: isEmptySourcemap(inputSourceMap) ? null : inputSourceMap
|
||
});
|
||
const generatedAstBody = getProgramBody(generatedAst);
|
||
const exportDefaultNode = findExportDefaultStatement(generatedAstBody);
|
||
const isLegacyRiotSyntax = isNil(exportDefaultNode);
|
||
const outputBody = getProgramBody(ast);
|
||
const componentInterface = findComponentInterface(generatedAstBody);
|
||
|
||
// throw in case of mixed component exports
|
||
if (exportDefaultNode && generatedAstBody.some(isThisExpressionStatement))
|
||
throw new Error('You can\t use "export default {}" and root this statements in the same component')
|
||
|
||
// add to the ast the "private" javascript content of our tag script node
|
||
outputBody.unshift(
|
||
...(
|
||
// for the legacy riot syntax we need to move all the import and (named) export statements outside of the function body
|
||
isLegacyRiotSyntax ?
|
||
[...findAllImportDeclarations(generatedAstBody), ...findAllExportNamedDeclarations(generatedAstBody)] :
|
||
// modern riot syntax will hoist all the private stuff outside of the export default statement
|
||
filterNonExportDefaultStatements(generatedAstBody)
|
||
));
|
||
|
||
// create the public component export properties from the root this statements
|
||
if (isLegacyRiotSyntax) extendTagProperty(
|
||
ast,
|
||
createDefaultExportFromLegacySyntax(generatedAstBody)
|
||
);
|
||
|
||
// convert the export default adding its content to the component property exported
|
||
if (exportDefaultNode) extendTagProperty(ast, exportDefaultNode);
|
||
|
||
return componentInterface ?
|
||
// add the component interface to the component object exported
|
||
addComponentInterfaceToExportedObject(ast, componentInterface) :
|
||
ast
|
||
}
|
||
|
||
/**
|
||
* Create the content of the template function
|
||
* @param { RiotParser.Node } sourceNode - node generated by the riot compiler
|
||
* @param { string } sourceFile - source file path
|
||
* @param { string } sourceCode - original source
|
||
* @returns {AST.BlockStatement} the content of the template function
|
||
*/
|
||
function createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) {
|
||
return callTemplateFunction(
|
||
...build(
|
||
createRootNode(sourceNode),
|
||
sourceFile,
|
||
sourceCode
|
||
)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Extend the AST adding the new template property containing our template call to render the component
|
||
* @param { Object } ast - current output ast
|
||
* @param { string } sourceFile - source file path
|
||
* @param { string } sourceCode - original source
|
||
* @param { RiotParser.Node } sourceNode - node generated by the riot compiler
|
||
* @returns { Object } the output ast having the "template" key
|
||
*/
|
||
function extendTemplateProperty(ast, sourceFile, sourceCode, sourceNode) {
|
||
types.visit(ast, {
|
||
visitProperty(path) {
|
||
if (path.value.key.name === TAG_TEMPLATE_PROPERTY) {
|
||
path.value.value = createTemplateDependenciesInjectionWrapper(
|
||
createTemplateFunctionContent(sourceNode, sourceFile, sourceCode)
|
||
);
|
||
|
||
return false
|
||
}
|
||
|
||
this.traverse(path);
|
||
}
|
||
});
|
||
|
||
return ast
|
||
}
|
||
|
||
/**
|
||
* Generate the component template logic
|
||
* @param { RiotParser.Node } sourceNode - node generated by the riot compiler
|
||
* @param { string } source - original component source code
|
||
* @param { Object } meta - compilation meta information
|
||
* @param { AST } ast - current AST output
|
||
* @returns { AST } the AST generated
|
||
*/
|
||
function template(sourceNode, source, meta, ast) {
|
||
const { options } = meta;
|
||
return extendTemplateProperty(ast, options.file, source, sourceNode)
|
||
}
|
||
|
||
const DEFAULT_OPTIONS = {
|
||
template: 'default',
|
||
file: '[unknown-source-file]',
|
||
scopedCss: true
|
||
};
|
||
|
||
/**
|
||
* Create the initial AST
|
||
* @param {string} tagName - the name of the component we have compiled
|
||
* @returns { AST } the initial AST
|
||
*
|
||
* @example
|
||
* // the output represents the following string in AST
|
||
*/
|
||
function createInitialInput({ tagName }) {
|
||
/*
|
||
generates
|
||
export default {
|
||
${TAG_CSS_PROPERTY}: null,
|
||
${TAG_LOGIC_PROPERTY}: null,
|
||
${TAG_TEMPLATE_PROPERTY}: null
|
||
}
|
||
*/
|
||
return builders.program([
|
||
builders.exportDefaultDeclaration(
|
||
builders.objectExpression([
|
||
simplePropertyNode(TAG_CSS_PROPERTY, nullNode()),
|
||
simplePropertyNode(TAG_LOGIC_PROPERTY, nullNode()),
|
||
simplePropertyNode(TAG_TEMPLATE_PROPERTY, nullNode()),
|
||
simplePropertyNode(TAG_NAME_PROPERTY, builders.literal(tagName))
|
||
])
|
||
)]
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Make sure the input sourcemap is valid otherwise we ignore it
|
||
* @param {SourceMapGenerator} map - preprocessor source map
|
||
* @returns {Object} sourcemap as json or nothing
|
||
*/
|
||
function normaliseInputSourceMap(map) {
|
||
const inputSourceMap = sourcemapAsJSON(map);
|
||
return isEmptySourcemap(inputSourceMap) ? null : inputSourceMap
|
||
}
|
||
|
||
/**
|
||
* Override the sourcemap content making sure it will always contain the tag source code
|
||
* @param {Object} map - sourcemap as json
|
||
* @param {string} source - component source code
|
||
* @returns {Object} original source map with the "sourcesContent" property overridden
|
||
*/
|
||
function overrideSourcemapContent(map, source) {
|
||
return {
|
||
...map,
|
||
sourcesContent: [source]
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create the compilation meta object
|
||
* @param { string } source - source code of the tag we will need to compile
|
||
* @param { string } options - compiling options
|
||
* @returns {Object} meta object
|
||
*/
|
||
function createMeta(source, options) {
|
||
return {
|
||
tagName: null,
|
||
fragments: null,
|
||
options: {
|
||
...DEFAULT_OPTIONS,
|
||
...options
|
||
},
|
||
source
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Parse a string to simply get its template AST
|
||
* @param { string } source - string to parse
|
||
* @param { Object } options - parser options
|
||
* @returns {Object} riot parser template output
|
||
*/
|
||
const parseSimpleString = (source, options) => {
|
||
const { parse } = parser(options);
|
||
return parse(source).output.template
|
||
};
|
||
|
||
/**
|
||
* Generate the component slots creation function from the root node
|
||
* @param { string } source - component outer html
|
||
* @param { Object } parserOptions - riot parser options
|
||
* @returns { string } content of the function that can be used to crate the slots in runtime
|
||
*/
|
||
function generateSlotsFromString(source, parserOptions) {
|
||
return compose(
|
||
({ code }) => code,
|
||
generateJavascript,
|
||
createTemplateDependenciesInjectionWrapper,
|
||
createSlotsArray
|
||
)(parseSimpleString(source, parserOptions), DEFAULT_OPTIONS.file, source)
|
||
}
|
||
|
||
/**
|
||
* Generate the Riot.js binding template function from a template string
|
||
* @param { string } source - template string
|
||
* @param { Object } parserOptions - riot parser options
|
||
* @returns { string } Riot.js bindings template function generated
|
||
*/
|
||
function generateTemplateFunctionFromString(source, parserOptions) {
|
||
return compose(
|
||
({ code }) => code,
|
||
generateJavascript,
|
||
callTemplateFunction
|
||
)(
|
||
...build(
|
||
parseSimpleString(source, parserOptions),
|
||
DEFAULT_OPTIONS.file,
|
||
source
|
||
)
|
||
)
|
||
}
|
||
|
||
/**
|
||
* Generate the output code source together with the sourcemap
|
||
* @param { string } source - source code of the tag we will need to compile
|
||
* @param { Object } opts - compiling options
|
||
* @returns { Output } object containing output code and source map
|
||
*/
|
||
function compile(source, opts = {}) {
|
||
const meta = createMeta(source, opts);
|
||
const { options } = meta;
|
||
const { code, map } = execute('template', options.template, meta, source);
|
||
const { parse } = parser(options);
|
||
const { template: template$1, css: css$1, javascript: javascript$1 } = parse(code).output;
|
||
|
||
// see also https://github.com/riot/compiler/issues/130
|
||
if (hasHTMLOutsideRootNode(template$1 || css$1 || javascript$1, code, parse)) {
|
||
throw new Error('Multiple HTML root nodes are not supported')
|
||
}
|
||
|
||
// extend the meta object with the result of the parsing
|
||
Object.assign(meta, {
|
||
tagName: template$1.name,
|
||
fragments: { template: template$1, css: css$1, javascript: javascript$1 }
|
||
});
|
||
|
||
return compose(
|
||
result => ({ ...result, meta }),
|
||
result => execute$1(result, meta),
|
||
result => ({
|
||
...result,
|
||
map: overrideSourcemapContent(result.map, source)
|
||
}),
|
||
ast => meta.ast = ast && generateJavascript(ast, {
|
||
sourceMapName: `${options.file}.map`,
|
||
inputSourceMap: normaliseInputSourceMap(map)
|
||
}),
|
||
hookGenerator(template, template$1, code, meta),
|
||
hookGenerator(javascript, javascript$1, code, meta),
|
||
hookGenerator(css, css$1, code, meta)
|
||
)(createInitialInput(meta))
|
||
}
|
||
|
||
/**
|
||
* Prepare the riot parser node transformers
|
||
* @param { Function } transformer - transformer function
|
||
* @param { Object } sourceNode - riot parser node
|
||
* @param { string } source - component source code
|
||
* @param { Object } meta - compilation meta information
|
||
* @returns { function(): Promise<Output> } Function what resolves to object containing output code and source map
|
||
*/
|
||
function hookGenerator(transformer, sourceNode, source, meta) {
|
||
const hasContent = sourceNode && (sourceNode.text || !isEmptyArray(sourceNode.nodes) || !isEmptyArray(sourceNode.attributes));
|
||
|
||
return hasContent ? curry(transformer)(sourceNode, source, meta) : result => result
|
||
}
|
||
|
||
// This function can be used to register new preprocessors
|
||
// a preprocessor can target either only the css or javascript nodes
|
||
// or the complete tag source file ('template')
|
||
const registerPreprocessor = register;
|
||
|
||
// This function can allow you to register postprocessors that will parse the output code
|
||
// here we can run prettifiers, eslint fixes...
|
||
const registerPostprocessor = register$1;
|
||
|
||
export { compile, createInitialInput, generateSlotsFromString, generateTemplateFunctionFromString, registerPostprocessor, registerPreprocessor };
|