/* Riot Compiler WIP, @license MIT */ import { types as types$1, print, parse } from 'recast'; import { composeSourceMaps } from 'recast/lib/util'; import { SourceMapGenerator } from 'source-map'; import compose from 'cumpa'; import cssEscape from 'cssesc'; import curry from 'curri'; import { Parser } from 'acorn'; import globalScope from 'globals'; import riotParser, { nodeTypes } from '@riotjs/parser'; import { hasValueAttribute } from 'dom-nodes'; const TAG_LOGIC_PROPERTY = 'exports'; const TAG_CSS_PROPERTY = 'css'; const TAG_TEMPLATE_PROPERTY = 'template'; const TAG_NAME_PROPERTY = 'name'; const types = types$1; const builders = types$1.builders; const namedTypes = types$1.namedTypes; function nullNode() { return builders.literal(null) } function simplePropertyNode(key, value) { return builders.property('init', builders.literal(key), value, false) } /** * 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 } /** * Detect node js environements * @returns { boolean } true if the runtime is node */ function isNode() { return typeof process !== 'undefined' } /** * Compose two sourcemaps * @param { SourceMapGenerator } formerMap - original sourcemap * @param { SourceMapGenerator } latterMap - target sourcemap * @returns { Object } sourcemap json */ function composeSourcemaps(formerMap, latterMap) { if ( isNode() && formerMap && latterMap && latterMap.mappings ) { return composeSourceMaps(sourcemapAsJSON(formerMap), sourcemapAsJSON(latterMap)) } else if (isNode() && formerMap) { return sourcemapAsJSON(formerMap) } return {} } /** * Create a new sourcemap generator * @param { Object } options - sourcemap options * @returns { SourceMapGenerator } SourceMapGenerator instance */ function createSourcemap(options) { return new SourceMapGenerator(options) } const Output = Object.freeze({ code: '', ast: [], meta: {}, map: null }); /** * Create the right output data result of a parsing * @param { Object } data - output data * @param { string } data.code - code generated * @param { AST } data.ast - ast representing the code * @param { SourceMapGenerator } data.map - source map generated along with the code * @param { Object } meta - compilation meta infomration * @returns { Output } output container object */ function createOutput(data, meta) { const output = { ...Output, ...data, meta }; if (!output.map && meta && meta.options && meta.options.file) return { ...output, map: createSourcemap({ file: meta.options.file }) } return output } /** * Transform the source code received via a compiler function * @param { Function } compiler - function needed to generate the output code * @param { Object } meta - compilation meta information * @param { string } source - source code * @returns { Output } output - the result of the compiler */ function transform(compiler, meta, source) { const result = (compiler ? compiler(source, meta) : { code: source }); return createOutput(result, meta) } /** * Throw an error with a descriptive message * @param { string } message - error message * @returns { undefined } hoppla.. at this point the program should stop working */ function panic(message) { throw new Error(message) } const postprocessors = new Set(); /** * Register a postprocessor that will be used after the parsing and compilation of the riot tags * @param { Function } postprocessor - transformer that will receive the output code ans sourcemap * @returns { Set } the postprocessors collection */ function register(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(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$1(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$1(type, name, meta, source) { if (!preprocessors[type]) preprocessorTypeError(type); if (!preprocessors[type].has(name)) preprocessorNameNotFoundError(name); return transform(preprocessors[type].get(name), meta, source) } 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$1(preprocessorType, preprocessorName, meta, code) : { code } ) } /** * Matches valid, multiline JavaScript comments in almost all its forms. * @const {RegExp} * @static */ const R_MLCOMMS = /\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//g; /** * Source for creating regexes matching valid quoted, single-line JavaScript strings. * It recognizes escape characters, including nested quotes and line continuation. * @const {string} */ const S_LINESTR = /"[^"\n\\]*(?:\\[\S\s][^"\n\\]*)*"|'[^'\n\\]*(?:\\[\S\s][^'\n\\]*)*'/.source; /** * Matches CSS selectors, excluding those beginning with '@' and quoted strings. * @const {RegExp} */ const CSS_SELECTOR = RegExp(`([{}]|^)[; ]*((?:[^@ ;{}][^{}]*)?[^@ ;{}:] ?)(?={)|${S_LINESTR}`, 'g'); /** * Parses styles enclosed in a "scoped" tag * The "css" string is received without comments or surrounding spaces. * * @param {string} tag - Tag name of the root element * @param {string} css - The CSS code * @returns {string} CSS with the styles scoped to the root element */ function scopedCSS(tag, css) { const host = ':host'; const selectorsBlacklist = ['from', 'to']; return css.replace(CSS_SELECTOR, function(m, p1, p2) { // skip quoted strings if (!p2) return m // we have a selector list, parse each individually p2 = p2.replace(/[^,]+/g, function(sel) { const s = sel.trim(); // skip selectors already using the tag name if (s.indexOf(tag) === 0) { return sel } // skips the keywords and percents of css animations if (!s || selectorsBlacklist.indexOf(s) > -1 || s.slice(-1) === '%') { return sel } // replace the `:host` pseudo-selector, where it is, with the root tag name; // if `:host` was not included, add the tag name as prefix, and mirror all // `[data-is]` if (s.indexOf(host) < 0) { return `${tag} ${s},[is="${tag}"] ${s}` } else { return `${s.replace(host, tag)},${ s.replace(host, `[is="${tag}"]`)}` } }); // add the danling bracket char and return the processed selector list return p1 ? `${p1} ${p2}` : p2 }) } /** * Remove comments, compact and trim whitespace * @param { string } code - compiled css code * @returns { string } css code normalized */ function compactCss(code) { return code.replace(R_MLCOMMS, '').replace(/\s+/g, ' ').trim() } const escapeBackslashes = s => s.replace(/\\/g, '\\\\'); const escapeIdentifier = identifier => escapeBackslashes(cssEscape(identifier, { isIdentifier: true })); /** * Generate the component css * @param { Object } sourceNode - node generated by the riot compiler * @param { string } source - original component source code * @param { Object } meta - compilation meta information * @param { AST } ast - current AST output * @returns { AST } the AST generated */ function css(sourceNode, source, meta, ast) { const preprocessorName = getPreprocessorTypeByAttribute(sourceNode); const { options } = meta; const preprocessorOutput = preprocess('css', preprocessorName, meta, sourceNode.text); const normalizedCssCode = compactCss(preprocessorOutput.code); const escapedCssIdentifier = escapeIdentifier(meta.tagName); const cssCode = (options.scopedCss ? scopedCSS(escapedCssIdentifier, escapeBackslashes(normalizedCssCode)) : escapeBackslashes(normalizedCssCode) ).trim(); types.visit(ast, { visitProperty(path) { if (path.value.key.value === TAG_CSS_PROPERTY) { path.value.value = builders.templateLiteral( [builders.templateElement({ raw: cssCode, cooked: '' }, false)], [] ); return false } this.traverse(path); } }); return ast } /** * Generate the javascript from an ast source * @param {AST} ast - ast object * @param {Object} options - printer options * @returns {Object} code + map */ function generateJavascript(ast, options) { return print(ast, { ...options, tabWidth: 2, quote: 'single' }) } /** * 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 || !map.mappings || !map.mappings.length } 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}` } /** * Parse a js source to generate the AST * @param {string} source - javascript source * @param {Object} options - parser options * @returns {AST} AST tree */ function generateAST(source, options) { return parse(source, { parser: { parse(source, opts) { return Parser.parse(source, { ...opts, ecmaVersion: 2020 }) } }, ...options }) } const browserAPIs = Object.keys(globalScope.browser); const builtinAPIs = Object.keys(globalScope.builtin); const isIdentifier = namedTypes.Identifier.check.bind(namedTypes.Identifier); const isLiteral = namedTypes.Literal.check.bind(namedTypes.Literal); const isExpressionStatement = namedTypes.ExpressionStatement.check.bind(namedTypes.ExpressionStatement); const isObjectExpression = namedTypes.ObjectExpression.check.bind(namedTypes.ObjectExpression); const isThisExpression = namedTypes.ThisExpression.check.bind(namedTypes.ThisExpression); const isNewExpression = namedTypes.NewExpression.check.bind(namedTypes.NewExpression); const isSequenceExpression = namedTypes.SequenceExpression.check.bind(namedTypes.SequenceExpression); const isBinaryExpression = namedTypes.BinaryExpression.check.bind(namedTypes.BinaryExpression); const isExportDefaultStatement = namedTypes.ExportDefaultDeclaration.check.bind(namedTypes.ExportDefaultDeclaration); const isBrowserAPI = ({name}) => browserAPIs.includes(name); const isBuiltinAPI = ({name}) => builtinAPIs.includes(name); const isRaw = (node) => node && node.raw; // eslint-disable-line /** * 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 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)) } /** * 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.value === 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 bodyWithoutExportDefault = filterNonExportDefaultStatements(generatedAstBody); const exportDefaultNode = findExportDefaultStatement(generatedAstBody); const outputBody = getProgramBody(ast); // add to the ast the "private" javascript content of our tag script node outputBody.unshift(...bodyWithoutExportDefault); // convert the export default adding its content to the "tag" property exported if (exportDefaultNode) extendTagProperty(ast, exportDefaultNode); return ast } // import {IS_BOOLEAN,IS_CUSTOM,IS_RAW,IS_SPREAD,IS_VOID} from '@riotjs/parser/src/constants' 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 IS_VOID_NODE = 'isVoid'; const IS_CUSTOM_NODE = 'isCustom'; const IS_BOOLEAN_ATTRIBUTE = 'isBoolean'; const IS_SPREAD_ATTRIBUTE = 'isSpread'; /** * 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 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 }) { 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 * @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 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) })(); /** * 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); const hasKeyAttribute = 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) } /** * 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) } 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) { if (!isGlobal(path) && !isGlobal({ node: path.node.object, scope: path.scope })) { if (path.value.computed) { this.traverse(path); } else if (isBinaryExpression(path.node.object) || path.node.object.computed) { this.traverse(path.get('object')); } else if (!path.node.object.callee) { replacePathScope(path, isThisExpression(path.node.object) ? path.node.property : path.node); } else { this.traverse(path.get('object')); } } 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 visitProperty(path) { const value = path.node.value; if (isIdentifier(value)) { updateNodeScope(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); } /** * 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: updateNodeScope, visitMemberExpression, visitProperty, 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) { return builders.functionExpression( null, [scope], builders.blockStatement([builders.returnStatement( ast )]) ) } /** * 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} 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() ]) } /** * 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 } 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} attributes - attributes list * @param {string} selectorAttribute - name of the selector attribute to filter out * @returns {Array} 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} 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)) } /** * 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 { nodes: getChildrenNodes(node), isRoot: true, // root nodes shuold't have directives attributes: cleanAttributes(node) } } /** * Get all the child nodes of a RiotParser.Node * @param {RiotParser.Node} node - riot parser node * @returns {Array} 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} all the attributes find */ function getNodeAttributes(node) { return node.attributes ? node.attributes : [] } /** * Get the name of a custom node transforming it into an expression node * @param {RiotParser.Node} node - riot parser node * @returns {RiotParser.Node.Attr} the node name as expression attribute */ function getCustomNodeNameAsExpression(node) { const isAttribute = findIsAttribute(node); const toRawString = val => `'${val}'`; if (isAttribute) { return isAttribute.expressions ? isAttribute.expressions[0] : { ...isAttribute, text: toRawString(isAttribute.value) } } return { ...node, text: toRawString(getName(node)) } } /** * 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 '' } } /** * 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 ? `` : '' } /** * 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(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 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.functionExpression( null, [], builders.blockStatement([ builders.returnStatement(builders.literal(sourceNode.value || true)) ]), ) } /** * 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)) } 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 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) ) ]) } /** * 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; chunks.push(sourceCode.substring(start, expression.start)); // add the tail to the string if (index === node.expressions.length - 1) chunks.push(sourceCode.substring(expression.end, node.end)); return chunks }, []).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)) } /** * 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([ ...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 * @returns { AST.Node } a slot binding node */ function createSlotBinding(sourceNode, selectorAttribute) { 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_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 = createRootNode({ 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, // avoid to render the slot attribute attributes: getNodeAttributes(sourceNode).filter(attribute => attribute.name !== SLOT_ATTRIBUTE) }; 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) ]) } /** * 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 => getAttributesWithoutSelector(attributes, selectorAttribute), // eslint-disable-line cleanAttributes )(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, toScopedFunction(getCustomNodeNameAsExpression(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) { // EACH bindings have prio 1 case hasEachAttribute(cloneNode): return [tagOpeningHTML, [createEachBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] // IF bindings have prio 2 case hasIfAttribute(cloneNode): return [tagOpeningHTML, [createIfBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] // TAG bindings have prio 3 case isCustomNode(cloneNode): return [tagOpeningHTML, [createTagBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] // slot tag case isSlotNode(cloneNode): return [tagOpeningHTML, [createSlotBinding(cloneNode, bindingsSelector)]] // this node has expressions bound to it default: 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); return mightBeARiotComponent ? [null, [ createTagBinding( cloneNodeWithoutSelectorAttribute(sourceNode, selector), null, sourceFile, sourceCode )] ] : build(createRootNode(sourceNode), 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 currentState = { ...cloneDeep(BuildingState), ...state }; // mutate the original arrays 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 (isTagNode(sourceNode) && !isVoidNode(sourceNode)) { currentState.html.push(closeTag(sourceNode)); } return [ currentState.html.join(''), currentState.bindings ] } const templateFunctionArguments = [ TEMPLATE_FN, EXPRESSION_TYPES, BINDING_TYPES, GET_COMPONENT_FN ].map(builders.identifier); /** * 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 builders.blockStatement([ builders.returnStatement( 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.value === TAG_TEMPLATE_PROPERTY) { path.value.value = builders.functionExpression( null, templateFunctionArguments, 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 overriden */ 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 } } /** * Generate the output code source together with the sourcemap * @param { string } source - source code of the tag we will need to compile * @param { string } 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$1('template', options.template, meta, source); const { template: template$1, css: css$1, javascript: javascript$1 } = riotParser(options).parse(code).output; // 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(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 { Promise } object containing output code and source map */ function hookGenerator(transformer, sourceNode, source, meta) { if ( // filter missing nodes !sourceNode || // filter nodes without children (sourceNode.nodes && !sourceNode.nodes.length) || // filter empty javascript and css nodes (!sourceNode.nodes && !sourceNode.text)) { return result => result } return curry(transformer)(sourceNode, source, meta) } // 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$1; // This function can allow you to register postprocessors that will parse the output code // here we can run prettifiers, eslint fixes... const registerPostprocessor = register; export { compile, createInitialInput, registerPostprocessor, registerPreprocessor };