4969 lines
137 KiB
JavaScript
4969 lines
137 KiB
JavaScript
|
/* Riot Compiler, @license MIT */
|
|||
|
'use strict';
|
|||
|
|
|||
|
Object.defineProperty(exports, '__esModule', { value: true });
|
|||
|
|
|||
|
var recast = require('recast');
|
|||
|
var globals_json = require('globals/globals.json');
|
|||
|
var typescript = require('recast/parsers/typescript');
|
|||
|
var util = require('recast/lib/util');
|
|||
|
var sourceMap = require('source-map');
|
|||
|
var cssEscape = require('cssesc');
|
|||
|
|
|||
|
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
|
|||
|
|
|||
|
var cssEscape__default = /*#__PURE__*/_interopDefaultLegacy(cssEscape);
|
|||
|
|
|||
|
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 = recast.types;
|
|||
|
const builders = recast.types.builders;
|
|||
|
const namedTypes = recast.types.namedTypes;
|
|||
|
|
|||
|
const browserAPIs = ['window', 'document', 'console'];
|
|||
|
const builtinAPIs = Object.keys(globals_json.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 recast.parse(source, {
|
|||
|
parser: {
|
|||
|
parse: (source, opts) => typescript.parse(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 util.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 sourceMap.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 recast.print(ast, {
|
|||
|
...options,
|
|||
|
parser: {
|
|||
|
parse: (source, opts) => typescript.parse(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__default["default"](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;
|
|||
|
|
|||
|
exports.compile = compile;
|
|||
|
exports.createInitialInput = createInitialInput;
|
|||
|
exports.generateSlotsFromString = generateSlotsFromString;
|
|||
|
exports.generateTemplateFunctionFromString = generateTemplateFunctionFromString;
|
|||
|
exports.registerPostprocessor = registerPostprocessor;
|
|||
|
exports.registerPreprocessor = registerPreprocessor;
|