|
|
- /* Riot Compiler WIP, @license MIT */
- 'use strict';
-
- Object.defineProperty(exports, '__esModule', { value: true });
-
- function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
-
- var recast = require('recast');
- var util = require('recast/lib/util');
- var sourceMap = require('source-map');
- var compose = _interopDefault(require('cumpa'));
- var cssEscape = _interopDefault(require('cssesc'));
- var curry = _interopDefault(require('curri'));
- var acorn = require('acorn');
- var globalScope = _interopDefault(require('globals'));
- var riotParser = require('@riotjs/parser');
- var riotParser__default = _interopDefault(riotParser);
- var domNodes = require('dom-nodes');
-
- const TAG_LOGIC_PROPERTY = 'exports';
- const TAG_CSS_PROPERTY = 'css';
- const TAG_TEMPLATE_PROPERTY = 'template';
- const TAG_NAME_PROPERTY = 'name';
-
- const types = recast.types;
- const builders = recast.types.builders;
- const namedTypes = recast.types.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 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(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 recast.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 recast.parse(source, {
- parser: {
- parse(source, opts) {
- return acorn.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 <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 === riotParser.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 === riotParser.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<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()
- ])
- }
-
- /**
- * 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))
- }
-
- /**
- * 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<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 : []
- }
- /**
- * 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 ? `</${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) && domNodes.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__default(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<Output> } 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;
-
- exports.compile = compile;
- exports.createInitialInput = createInitialInput;
- exports.registerPostprocessor = registerPostprocessor;
- exports.registerPreprocessor = registerPreprocessor;
|