parcoursup/node_modules/@riotjs/compiler/dist/index.js

4969 lines
137 KiB
JavaScript
Raw Normal View History

2023-03-05 13:23:23 +01:00
/* 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 &#x222;
* @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 &#222;
* @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 &nbsp;
* @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;