UNPKG

@riotjs/compiler

Version:

Compiler for Riot.js .riot files

1,937 lines (1,798 loc) 109 kB
/* Riot Compiler, @license MIT */ import riotParser, { constants, nodeTypes } from '@riotjs/parser'; import { types as types$1, parse as parse$1, print } from 'recast'; import compose from 'cumpa'; import { isNil, isNode, isObject } from '@riotjs/util/checks'; import { hasValueAttribute } from 'dom-nodes'; import { parse as parse$2 } from 'recast/parsers/typescript.js'; import { composeSourceMaps } from 'recast/lib/util.js'; import { SourceMapGenerator } from 'source-map'; import { panic } from '@riotjs/util/misc'; import cssEscape from 'cssesc'; import curry from 'curri'; import { isObject as isObject$1 } from '@riotjs/util'; const TAG_LOGIC_PROPERTY = 'exports'; const TAG_CSS_PROPERTY = 'css'; const TAG_TEMPLATE_PROPERTY = 'template'; const TAG_NAME_PROPERTY = 'name'; const RIOT_MODULE_ID = 'riot'; const RIOT_INTERFACE_WRAPPER_NAME = 'RiotComponentWrapper'; const RIOT_TAG_INTERFACE_NAME = 'RiotComponent'; const 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_IS_BOOLEAN_ATTRIBUTE = 'isBoolean'; const BINDING_CHILD_NODE_INDEX_KEY = 'childNodeIndex'; // slots keys const BINDING_BINDINGS_KEY = 'bindings'; const BINDING_ID_KEY = 'id'; const BINDING_HTML_KEY = 'html'; const BINDING_ATTRIBUTES_KEY = 'attributes'; // DOM directives const IF_DIRECTIVE = 'if'; const EACH_DIRECTIVE = 'each'; const KEY_ATTRIBUTE = 'key'; const SLOT_ATTRIBUTE = 'slot'; const NAME_ATTRIBUTE = 'name'; const IS_DIRECTIVE = 'is'; // Misc const DEFAULT_SLOT_NAME = 'default'; const TEXT_NODE_EXPRESSION_PLACEHOLDER = ' '; const BINDING_SELECTOR_PREFIX = 'expr'; const SLOT_TAG_NODE_NAME = 'slot'; const PROGRESS_TAG_NODE_NAME = 'progress'; const TEMPLATE_TAG_NODE_NAME = 'template'; // Riot Parser constants constants.IS_RAW; const IS_VOID_NODE = constants.IS_VOID; const IS_CUSTOM_NODE = constants.IS_CUSTOM; const IS_BOOLEAN_ATTRIBUTE = constants.IS_BOOLEAN; const IS_SPREAD_ATTRIBUTE = constants.IS_SPREAD; const types = types$1; const builders = types$1.builders; const namedTypes = types$1.namedTypes; var builtin = { AggregateError: false, "Array": false, "ArrayBuffer": false, Atomics: false, BigInt: false, BigInt64Array: false, BigUint64Array: false, "Boolean": false, "DataView": false, "Date": false, "decodeURI": false, "decodeURIComponent": false, "encodeURI": false, "encodeURIComponent": false, "Error": false, "escape": false, "eval": false, "EvalError": false, FinalizationRegistry: false, "Float32Array": false, "Float64Array": false, "Function": false, globalThis: false, "Infinity": false, "Int16Array": false, "Int32Array": false, "Int8Array": false, "Intl": false, "isFinite": false, "isNaN": false, Iterator: false, "JSON": false, "Map": false, "Math": false, "NaN": false, "Number": false, "Object": false, "parseFloat": false, "parseInt": false, "Promise": false, "Proxy": false, "RangeError": false, "ReferenceError": false, "Reflect": false, "RegExp": false, "Set": false, SharedArrayBuffer: false, "String": false, "Symbol": false, "SyntaxError": false, "TypeError": false, "Uint16Array": false, "Uint32Array": false, "Uint8Array": false, "Uint8ClampedArray": false, "undefined": false, "unescape": false, "URIError": false, "WeakMap": false, WeakRef: false, "WeakSet": false }; var globals = { builtin: builtin}; const browserAPIs = ['window', 'document', 'console']; const builtinAPIs = Object.keys(globals.builtin); const isIdentifier = (n) => namedTypes.Identifier.check(n); const isLiteral = (n) => namedTypes.Literal.check(n); const isExpressionStatement = (n) => namedTypes.ExpressionStatement.check(n); const isThisExpression = (n) => namedTypes.ThisExpression.check(n); const isObjectExpression = (n) => namedTypes.ObjectExpression.check(n); const isThisExpressionStatement = (n) => isExpressionStatement(n) && isMemberExpression(n.expression.left) && isThisExpression(n.expression.left.object); const isNewExpression = (n) => namedTypes.NewExpression.check(n); const isSequenceExpression = (n) => namedTypes.SequenceExpression.check(n); const isExportDefaultStatement = (n) => namedTypes.ExportDefaultDeclaration.check(n); const isMemberExpression = (n) => namedTypes.MemberExpression.check(n); const isImportDeclaration = (n) => namedTypes.ImportDeclaration.check(n); const isTypeAliasDeclaration = (n) => namedTypes.TSTypeAliasDeclaration.check(n); const isInterfaceDeclaration = (n) => namedTypes.TSInterfaceDeclaration.check(n); const isExportNamedDeclaration = (n) => namedTypes.ExportNamedDeclaration.check(n); const isBrowserAPI = ({ name }) => browserAPIs.includes(name); const isBuiltinAPI = ({ name }) => builtinAPIs.includes(name); const isRaw = (n) => n && n.raw; /** * True if the node has not expression set nor bindings directives * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true only if it's a static node that doesn't need bindings or expressions */ function isStaticNode(node) { return [ hasExpressions, findEachAttribute, findIfAttribute, isCustomNode, isSlotNode, ].every((test) => !test(node)) } /** * Check if a node should be rendered in the final component HTML * For example slot <template slot="content"> tags not using `each` or `if` directives can be removed * see also https://github.com/riot/riot/issues/2888 * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true if we can remove this tag from the component rendered HTML */ function isRemovableNode(node) { return ( isTemplateNode(node) && !isNil(findAttribute(SLOT_ATTRIBUTE, node)) && !hasEachAttribute(node) && !hasIfAttribute(node) ) } /** * Check if a node name is part of the browser or builtin javascript api or it belongs to the current scope * @param { types.NodePath } path - containing the current node visited * @returns {boolean} true if it's a global api variable */ function isGlobal({ scope, node }) { // recursively find the identifier of this AST path if (node.object) { return isGlobal({ node: node.object, scope }) } return Boolean( isRaw(node) || isBuiltinAPI(node) || isBrowserAPI(node) || isNewExpression(node) || isNodeInScope(scope, node), ) } /** * Checks if the identifier of a given node exists in a scope * @param {Scope} scope - scope where to search for the identifier * @param {types.Node} node - node to search for the identifier * @returns {boolean} true if the node identifier is defined in the given scope */ function isNodeInScope(scope, node) { const traverse = (isInScope = false) => { types.visit(node, { visitIdentifier(path) { if (scope.lookup(getName(path.node))) { isInScope = true; } this.abort(); }, }); return isInScope }; return traverse() } /** * True if the node has the isCustom attribute set * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true if either it's a riot component or a custom element */ function isCustomNode(node) { return !!(node[IS_CUSTOM_NODE] || hasIsAttribute(node)) } /** * True the node is <slot> * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true if it's a slot node */ function isSlotNode(node) { return node.name === SLOT_TAG_NODE_NAME } /** * True if the node has the isVoid attribute set * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true if the node is self closing */ function isVoidNode(node) { return !!node[IS_VOID_NODE] } /** * True if the riot parser did find a tag node * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true only for the tag nodes */ function isTagNode(node) { return node.type === nodeTypes.TAG } /** * True if the riot parser did find a text node * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true only for the text nodes */ function isTextNode(node) { return node.type === nodeTypes.TEXT } /** * True if the node parsed any of the root nodes (each, tag bindings create root nodes as well...) * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true only for the root nodes */ function isRootNode(node) { return node.isRoot } /** * True if the node parsed is the absolute root node (nested root nodes are not considered) * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true only for the root nodes */ function isAbsoluteRootNode(node) { return node.isRoot && !node.isNestedRoot } /** * True if the attribute parsed is of type spread one * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true if the attribute node is of type spread */ function isSpreadAttribute(node) { return node[IS_SPREAD_ATTRIBUTE] } /** * True if the node is an attribute and its name is "value" * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true only for value attribute nodes */ function isValueAttribute(node) { return node.name === 'value' } /** * True if the DOM node is a progress tag * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true for the progress tags */ function isProgressNode(node) { return node.name === PROGRESS_TAG_NODE_NAME } /** * True if the DOM node is a <template> tag * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true for the progress tags */ function isTemplateNode(node) { return node.name === TEMPLATE_TAG_NODE_NAME } /** * True if the node is an attribute and a DOM handler * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true only for dom listener attribute nodes */ const isEventAttribute = (() => { const EVENT_ATTR_RE = /^on/; return (node) => EVENT_ATTR_RE.test(node.name) })(); /** * Check if a string is an html comment * @param {string} string - test string * @returns {boolean} true if html comment */ function isCommentString(string) { return string.trim().indexOf('<!') === 0 } /** * True if the node has expressions or expression attributes * @param {RiotParser.Node} node - riot parser node * @returns {boolean} ditto */ function hasExpressions(node) { return !!( node.expressions || // has expression attributes getNodeAttributes(node).some((attribute) => hasExpressions(attribute)) || // has child text nodes with expressions (node.nodes && node.nodes.some((node) => isTextNode(node) && hasExpressions(node))) ) } /** * True if the node is a directive having its own template or it's a slot node * @param {RiotParser.Node} node - riot parser node * @returns {boolean} true only for the IF EACH and TAG bindings or it's a slot node */ function hasItsOwnTemplate(node) { return [findEachAttribute, findIfAttribute, isCustomNode, isSlotNode].some( (test) => test(node), ) } const hasIfAttribute = compose(Boolean, findIfAttribute); const hasEachAttribute = compose(Boolean, findEachAttribute); const hasIsAttribute = compose(Boolean, findIsAttribute); compose(Boolean, findKeyAttribute); /** * Find the attribute node * @param { string } name - name of the attribute we want to find * @param { riotParser.nodeTypes.TAG } node - a tag node * @returns { riotParser.nodeTypes.ATTR } attribute node */ function findAttribute(name, node) { return ( node.attributes && node.attributes.find((attr) => getName(attr) === name) ) } function findIfAttribute(node) { return findAttribute(IF_DIRECTIVE, node) } function findEachAttribute(node) { return findAttribute(EACH_DIRECTIVE, node) } function findKeyAttribute(node) { return findAttribute(KEY_ATTRIBUTE, node) } function findIsAttribute(node) { return findAttribute(IS_DIRECTIVE, node) } /** * Find all the node attributes that are not expressions * @param {RiotParser.Node} node - riot parser node * @returns {Array} list of all the static attributes */ function findStaticAttributes(node) { return getNodeAttributes(node).filter( (attribute) => !hasExpressions(attribute), ) } /** * Find all the node attributes that have expressions * @param {RiotParser.Node} node - riot parser node * @returns {Array} list of all the dynamic attributes */ function findDynamicAttributes(node) { return getNodeAttributes(node).filter(hasExpressions) } function nullNode() { return builders.literal(null) } function simplePropertyNode(key, value) { const property = builders.property( 'init', builders.identifier(key), value, false, ); property.sho; return property } const LINES_RE = /\r\n?|\n/g; /** * Split a string into a rows array generated from its EOL matches * @param { string } string [description] * @returns { Array } array containing all the string rows */ function splitStringByEOL(string) { return string.split(LINES_RE) } /** * Get the line and the column of a source text based on its position in the string * @param { string } string - target string * @param { number } position - target position * @returns { Object } object containing the source text line and column */ function getLineAndColumnByPosition(string, position) { const lines = splitStringByEOL(string.slice(0, position)); return { line: lines.length, column: lines[lines.length - 1].length, } } /** * Add the offset to the code that must be parsed in order to generate properly the sourcemaps * @param {string} input - input string * @param {string} source - original source code * @param {RiotParser.Node} node - node that we are going to transform * @return {string} the input string with the offset properly set */ function addLineOffset(input, source, node) { const { column, line } = getLineAndColumnByPosition(source, node.start); return `${'\n'.repeat(line - 1)}${' '.repeat(column + 1)}${input}` } /** * Create a simple attribute expression * @param {RiotParser.Node.Attr} sourceNode - the custom tag * @param {RiotParser.Node} parentNode - the html node that has received the attribute expression * @param {string} sourceFile - source file path * @param {string} sourceCode - original source * @returns {AST.Node} object containing the expression binding keys */ function createAttributeExpression( sourceNode, parentNode, sourceFile, sourceCode, ) { const isSpread = isSpreadAttribute(sourceNode); return builders.objectExpression([ simplePropertyNode( BINDING_TYPE_KEY, builders.memberExpression( builders.identifier(EXPRESSION_TYPES), builders.identifier(ATTRIBUTE_EXPRESSION_TYPE), false, ), ), simplePropertyNode( BINDING_IS_BOOLEAN_ATTRIBUTE, builders.literal( // the hidden attribute is always a boolean and can be applied to any DOM node sourceNode.name === 'hidden' || // Custom nodes can't handle boolean attrs // Riot.js will handle the bool attrs logic only on native html tags (!parentNode[IS_CUSTOM_NODE] && !isAbsoluteRootNode(parentNode) && !isSpread && !!sourceNode[IS_BOOLEAN_ATTRIBUTE]), ), ), simplePropertyNode( BINDING_NAME_KEY, isSpread ? nullNode() : builders.literal(sourceNode.name), ), simplePropertyNode( BINDING_EVALUATE_KEY, createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode), ), ]) } /** * Create a simple event expression * @param {RiotParser.Node.Attr} sourceNode - attribute containing the event handlers * @param {string} sourceFile - source file path * @param {string} sourceCode - original source * @returns {AST.Node} object containing the expression binding keys */ function createEventExpression( sourceNode, sourceFile, sourceCode, ) { return builders.objectExpression([ simplePropertyNode( BINDING_TYPE_KEY, builders.memberExpression( builders.identifier(EXPRESSION_TYPES), builders.identifier(EVENT_EXPRESSION_TYPE), false, ), ), simplePropertyNode(BINDING_NAME_KEY, builders.literal(sourceNode.name)), simplePropertyNode( BINDING_EVALUATE_KEY, createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode), ), ]) } var quot = "\""; var amp = "&"; var apos = "'"; var lt = "<"; var gt = ">"; var nbsp = " "; var iexcl = "¡"; var cent = "¢"; var pound = "£"; var curren = "¤"; var yen = "¥"; var brvbar = "¦"; var sect = "§"; var uml = "¨"; var copy = "©"; var ordf = "ª"; var laquo = "«"; var not = "¬"; var shy = "­"; var reg = "®"; var macr = "¯"; var deg = "°"; var plusmn = "±"; var sup2 = "²"; var sup3 = "³"; var acute = "´"; var micro = "µ"; var para = "¶"; var middot = "·"; var cedil = "¸"; var sup1 = "¹"; var ordm = "º"; var raquo = "»"; var frac14 = "¼"; var frac12 = "½"; var frac34 = "¾"; var iquest = "¿"; var Agrave = "À"; var Aacute = "Á"; var Acirc = "Â"; var Atilde = "Ã"; var Auml = "Ä"; var Aring = "Å"; var AElig = "Æ"; var Ccedil = "Ç"; var Egrave = "È"; var Eacute = "É"; var Ecirc = "Ê"; var Euml = "Ë"; var Igrave = "Ì"; var Iacute = "Í"; var Icirc = "Î"; var Iuml = "Ï"; var ETH = "Ð"; var Ntilde = "Ñ"; var Ograve = "Ò"; var Oacute = "Ó"; var Ocirc = "Ô"; var Otilde = "Õ"; var Ouml = "Ö"; var times = "×"; var Oslash = "Ø"; var Ugrave = "Ù"; var Uacute = "Ú"; var Ucirc = "Û"; var Uuml = "Ü"; var Yacute = "Ý"; var THORN = "Þ"; var szlig = "ß"; var agrave = "à"; var aacute = "á"; var acirc = "â"; var atilde = "ã"; var auml = "ä"; var aring = "å"; var aelig = "æ"; var ccedil = "ç"; var egrave = "è"; var eacute = "é"; var ecirc = "ê"; var euml = "ë"; var igrave = "ì"; var iacute = "í"; var icirc = "î"; var iuml = "ï"; var eth = "ð"; var ntilde = "ñ"; var ograve = "ò"; var oacute = "ó"; var ocirc = "ô"; var otilde = "õ"; var ouml = "ö"; var divide = "÷"; var oslash = "ø"; var ugrave = "ù"; var uacute = "ú"; var ucirc = "û"; var uuml = "ü"; var yacute = "ý"; var thorn = "þ"; var yuml = "ÿ"; var OElig = "Œ"; var oelig = "œ"; var Scaron = "Š"; var scaron = "š"; var Yuml = "Ÿ"; var fnof = "ƒ"; var circ = "ˆ"; var tilde = "˜"; var Alpha = "Α"; var Beta = "Β"; var Gamma = "Γ"; var Delta = "Δ"; var Epsilon = "Ε"; var Zeta = "Ζ"; var Eta = "Η"; var Theta = "Θ"; var Iota = "Ι"; var Kappa = "Κ"; var Lambda = "Λ"; var Mu = "Μ"; var Nu = "Ν"; var Xi = "Ξ"; var Omicron = "Ο"; var Pi = "Π"; var Rho = "Ρ"; var Sigma = "Σ"; var Tau = "Τ"; var Upsilon = "Υ"; var Phi = "Φ"; var Chi = "Χ"; var Psi = "Ψ"; var Omega = "Ω"; var alpha = "α"; var beta = "β"; var gamma = "γ"; var delta = "δ"; var epsilon = "ε"; var zeta = "ζ"; var eta = "η"; var theta = "θ"; var iota = "ι"; var kappa = "κ"; var lambda = "λ"; var mu = "μ"; var nu = "ν"; var xi = "ξ"; var omicron = "ο"; var pi = "π"; var rho = "ρ"; var sigmaf = "ς"; var sigma = "σ"; var tau = "τ"; var upsilon = "υ"; var phi = "φ"; var chi = "χ"; var psi = "ψ"; var omega = "ω"; var thetasym = "ϑ"; var upsih = "ϒ"; var piv = "ϖ"; var ensp = " "; var emsp = " "; var thinsp = " "; var zwnj = "‌"; var zwj = "‍"; var lrm = "‎"; var rlm = "‏"; var ndash = "–"; var mdash = "—"; var lsquo = "‘"; var rsquo = "’"; var sbquo = "‚"; var ldquo = "“"; var rdquo = "”"; var bdquo = "„"; var dagger = "†"; var Dagger = "‡"; var bull = "•"; var hellip = "…"; var permil = "‰"; var prime = "′"; var Prime = "″"; var lsaquo = "‹"; var rsaquo = "›"; var oline = "‾"; var frasl = "⁄"; var euro = "€"; var image = "ℑ"; var weierp = "℘"; var real = "ℜ"; var trade = "™"; var alefsym = "ℵ"; var larr = "←"; var uarr = "↑"; var rarr = "→"; var darr = "↓"; var harr = "↔"; var crarr = "↵"; var lArr = "⇐"; var uArr = "⇑"; var rArr = "⇒"; var dArr = "⇓"; var hArr = "⇔"; var forall = "∀"; var part = "∂"; var exist = "∃"; var empty = "∅"; var nabla = "∇"; var isin = "∈"; var notin = "∉"; var ni = "∋"; var prod = "∏"; var sum = "∑"; var minus = "−"; var lowast = "∗"; var radic = "√"; var prop = "∝"; var infin = "∞"; var ang = "∠"; var and = "∧"; var or = "∨"; var cap = "∩"; var cup = "∪"; var int = "∫"; var there4 = "∴"; var sim = "∼"; var cong = "≅"; var asymp = "≈"; var ne = "≠"; var equiv = "≡"; var le = "≤"; var ge = "≥"; var sub = "⊂"; var sup = "⊃"; var nsub = "⊄"; var sube = "⊆"; var supe = "⊇"; var oplus = "⊕"; var otimes = "⊗"; var perp = "⊥"; var sdot = "⋅"; var lceil = "⌈"; var rceil = "⌉"; var lfloor = "⌊"; var rfloor = "⌋"; var lang = "〈"; var rang = "〉"; var loz = "◊"; var spades = "♠"; var clubs = "♣"; var hearts = "♥"; var diams = "♦"; var entities = { quot: quot, amp: amp, apos: apos, lt: lt, gt: gt, nbsp: nbsp, iexcl: iexcl, cent: cent, pound: pound, curren: curren, yen: yen, brvbar: brvbar, sect: sect, uml: uml, copy: copy, ordf: ordf, laquo: laquo, not: not, shy: shy, reg: reg, macr: macr, deg: deg, plusmn: plusmn, sup2: sup2, sup3: sup3, acute: acute, micro: micro, para: para, middot: middot, cedil: cedil, sup1: sup1, ordm: ordm, raquo: raquo, frac14: frac14, frac12: frac12, frac34: frac34, iquest: iquest, Agrave: Agrave, Aacute: Aacute, Acirc: Acirc, Atilde: Atilde, Auml: Auml, Aring: Aring, AElig: AElig, Ccedil: Ccedil, Egrave: Egrave, Eacute: Eacute, Ecirc: Ecirc, Euml: Euml, Igrave: Igrave, Iacute: Iacute, Icirc: Icirc, Iuml: Iuml, ETH: ETH, Ntilde: Ntilde, Ograve: Ograve, Oacute: Oacute, Ocirc: Ocirc, Otilde: Otilde, Ouml: Ouml, times: times, Oslash: Oslash, Ugrave: Ugrave, Uacute: Uacute, Ucirc: Ucirc, Uuml: Uuml, Yacute: Yacute, THORN: THORN, szlig: szlig, agrave: agrave, aacute: aacute, acirc: acirc, atilde: atilde, auml: auml, aring: aring, aelig: aelig, ccedil: ccedil, egrave: egrave, eacute: eacute, ecirc: ecirc, euml: euml, igrave: igrave, iacute: iacute, icirc: icirc, iuml: iuml, eth: eth, ntilde: ntilde, ograve: ograve, oacute: oacute, ocirc: ocirc, otilde: otilde, ouml: ouml, divide: divide, oslash: oslash, ugrave: ugrave, uacute: uacute, ucirc: ucirc, uuml: uuml, yacute: yacute, thorn: thorn, yuml: yuml, OElig: OElig, oelig: oelig, Scaron: Scaron, scaron: scaron, Yuml: Yuml, fnof: fnof, circ: circ, tilde: tilde, Alpha: Alpha, Beta: Beta, Gamma: Gamma, Delta: Delta, Epsilon: Epsilon, Zeta: Zeta, Eta: Eta, Theta: Theta, Iota: Iota, Kappa: Kappa, Lambda: Lambda, Mu: Mu, Nu: Nu, Xi: Xi, Omicron: Omicron, Pi: Pi, Rho: Rho, Sigma: Sigma, Tau: Tau, Upsilon: Upsilon, Phi: Phi, Chi: Chi, Psi: Psi, Omega: Omega, alpha: alpha, beta: beta, gamma: gamma, delta: delta, epsilon: epsilon, zeta: zeta, eta: eta, theta: theta, iota: iota, kappa: kappa, lambda: lambda, mu: mu, nu: nu, xi: xi, omicron: omicron, pi: pi, rho: rho, sigmaf: sigmaf, sigma: sigma, tau: tau, upsilon: upsilon, phi: phi, chi: chi, psi: psi, omega: omega, thetasym: thetasym, upsih: upsih, piv: piv, ensp: ensp, emsp: emsp, thinsp: thinsp, zwnj: zwnj, zwj: zwj, lrm: lrm, rlm: rlm, ndash: ndash, mdash: mdash, lsquo: lsquo, rsquo: rsquo, sbquo: sbquo, ldquo: ldquo, rdquo: rdquo, bdquo: bdquo, dagger: dagger, Dagger: Dagger, bull: bull, hellip: hellip, permil: permil, prime: prime, Prime: Prime, lsaquo: lsaquo, rsaquo: rsaquo, oline: oline, frasl: frasl, euro: euro, image: image, weierp: weierp, real: real, trade: trade, alefsym: alefsym, larr: larr, uarr: uarr, rarr: rarr, darr: darr, harr: harr, crarr: crarr, lArr: lArr, uArr: uArr, rArr: rArr, dArr: dArr, hArr: hArr, forall: forall, part: part, exist: exist, empty: empty, nabla: nabla, isin: isin, notin: notin, ni: ni, prod: prod, sum: sum, minus: minus, lowast: lowast, radic: radic, prop: prop, infin: infin, ang: ang, and: and, or: or, cap: cap, cup: cup, int: int, there4: there4, sim: sim, cong: cong, asymp: asymp, ne: ne, equiv: equiv, le: le, ge: ge, sub: sub, sup: sup, nsub: nsub, sube: sube, supe: supe, oplus: oplus, otimes: otimes, perp: perp, sdot: sdot, lceil: lceil, rceil: rceil, lfloor: lfloor, rfloor: rfloor, lang: lang, rang: rang, loz: loz, spades: spades, clubs: clubs, hearts: hearts, diams: diams }; const HTMLEntityRe = /&(\S+);/g; const HEX_NUMBER = /^[\da-fA-F]+$/; const DECIMAL_NUMBER = /^\d+$/; /** * Encode unicode hex html entities like for example &#x222; * @param {string} string - input string * @returns {string} encoded string */ function encodeHex(string) { const hex = string.substr(2); return HEX_NUMBER.test(hex) ? String.fromCodePoint(parseInt(hex, 16)) : string } /** * Encode unicode decimal html entities like for example &#222; * @param {string} string - input string * @returns {string} encoded string */ function encodeDecimal(string) { const nr = string.substr(1); return DECIMAL_NUMBER.test(nr) ? String.fromCodePoint(parseInt(nr, 10)) : string } /** * Encode html entities in strings like &nbsp; * @param {string} string - input string * @returns {string} encoded string */ function encodeHTMLEntities(string) { return string.replace(HTMLEntityRe, (match, entity) => { const [firstChar, secondChar] = entity; if (firstChar === '#') { return secondChar === 'x' ? encodeHex(entity) : encodeDecimal(entity) } else { return entities[entity] || entity } }) } /** * Native String.prototype.trimEnd method with fallback to String.prototype.trimRight * Edge doesn't support the first one * @param {string} string - input string * @returns {string} trimmed output */ function trimEnd(string) { return (string.trimEnd || string.trimRight).apply(string) } /** * Native String.prototype.trimStart method with fallback to String.prototype.trimLeft * Edge doesn't support the first one * @param {string} string - input string * @returns {string} trimmed output */ function trimStart(string) { return (string.trimStart || string.trimLeft).apply(string) } /** * Unescape the user escaped chars * @param {string} string - input string * @param {string} char - probably a '{' or anything the user want's to escape * @returns {string} cleaned up string */ function unescapeChar(string, char) { return string.replace(RegExp(`\\\\${char}`, 'gm'), char) } /** * Generate the pure immutable string chunks from a RiotParser.Node.Text * @param {RiotParser.Node.Text} node - riot parser text node * @param {string} sourceCode sourceCode - source code * @returns {Array} array containing the immutable string chunks */ function generateLiteralStringChunksFromNode(node, sourceCode) { return ( node.expressions .reduce((chunks, expression, index) => { const start = index ? node.expressions[index - 1].end : node.start; const string = encodeHTMLEntities( sourceCode.substring(start, expression.start), ); // trimStart the first string chunks.push(index === 0 ? trimStart(string) : string); // add the tail to the string if (index === node.expressions.length - 1) chunks.push( encodeHTMLEntities( trimEnd(sourceCode.substring(expression.end, node.end)), ), ); return chunks }, []) // comments are not supported here .filter((str) => !isCommentString(str)) .map((str) => (node.unescape ? unescapeChar(str, node.unescape) : str)) ) } /** * Simple bindings might contain multiple expressions like for example: "{foo} and {bar}" * This helper aims to merge them in a template literal if it's necessary * @param {RiotParser.Node} node - riot parser node * @param {string} sourceFile - original tag file * @param {string} sourceCode - original tag source code * @returns { Object } a template literal expression object */ function mergeNodeExpressions(node, sourceFile, sourceCode) { if (node.parts.length === 1) return transformExpression(node.expressions[0], sourceFile, sourceCode) const pureStringChunks = generateLiteralStringChunksFromNode(node, sourceCode); const stringsArray = pureStringChunks .reduce((acc, str, index) => { const expr = node.expressions[index]; return [ ...acc, builders.literal(str), expr ? transformExpression(expr, sourceFile, sourceCode) : nullNode(), ] }, []) // filter the empty literal expressions .filter((expr) => !isLiteral(expr) || expr.value); return createArrayString(stringsArray) } /** * Create a text expression * @param {RiotParser.Node.Text} sourceNode - text node to parse * @param {string} sourceFile - source file path * @param {string} sourceCode - original source * @param {number} childNodeIndex - position of the child text node in its parent children nodes * @returns {AST.Node} object containing the expression binding keys */ function createTextExpression( sourceNode, sourceFile, sourceCode, childNodeIndex, ) { return builders.objectExpression([ simplePropertyNode( BINDING_TYPE_KEY, builders.memberExpression( builders.identifier(EXPRESSION_TYPES), builders.identifier(TEXT_EXPRESSION_TYPE), false, ), ), simplePropertyNode( BINDING_CHILD_NODE_INDEX_KEY, builders.literal(childNodeIndex), ), simplePropertyNode( BINDING_EVALUATE_KEY, wrapASTInFunctionWithScope( mergeNodeExpressions(sourceNode, sourceFile, sourceCode), ), ), ]) } function createValueExpression( sourceNode, sourceFile, sourceCode, ) { return builders.objectExpression([ simplePropertyNode( BINDING_TYPE_KEY, builders.memberExpression( builders.identifier(EXPRESSION_TYPES), builders.identifier(VALUE_EXPRESSION_TYPE), false, ), ), simplePropertyNode( BINDING_EVALUATE_KEY, createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode), ), ]) } function createExpression( sourceNode, sourceFile, sourceCode, childNodeIndex, parentNode, ) { switch (true) { case isTextNode(sourceNode): return createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) // progress nodes value attributes will be rendered as attributes // see https://github.com/riot/compiler/issues/122 case isValueAttribute(sourceNode) && hasValueAttribute(parentNode.name) && !isProgressNode(parentNode): return createValueExpression(sourceNode, sourceFile, sourceCode) case isEventAttribute(sourceNode): return createEventExpression(sourceNode, sourceFile, sourceCode) default: return createAttributeExpression(sourceNode, parentNode, sourceFile, sourceCode) } } /** * Create the attribute expressions * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser * @param {string} sourceFile - source file path * @param {string} sourceCode - original source * @returns {Array} array containing all the attribute expressions */ function createAttributeExpressions(sourceNode, sourceFile, sourceCode) { return findDynamicAttributes(sourceNode).map((attribute) => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode), ) } /** * Parse a js source to generate the AST * @param {string} source - javascript source * @param {Object} options - parser options * @returns {AST} AST tree */ function generateAST(source, options) { return parse$1(source, { parser: { parse: (source, opts) => parse$2(source, { ...opts, ecmaVersion: 'latest', }), }, ...options, }) } const scope = builders.identifier(SCOPE); const getName = (node) => (node && node.name ? node.name : node); /** * Replace the path scope with a member Expression * @param { types.NodePath } path - containing the current node visited * @param { types.Node } property - node we want to prefix with the scope identifier * @returns {undefined} this is a void function */ function replacePathScope(path, property) { // make sure that for the scope injection the extra parenthesis get removed removeExtraParenthesis(property); path.replace(builders.memberExpression(scope, property, false)); } /** * Change the nodes scope adding the `scope` prefix * @param { types.NodePath } path - containing the current node visited * @returns { boolean } return false if we want to stop the tree traversal * @context { types.visit } */ function updateNodeScope(path) { if (!isGlobal(path)) { replacePathScope(path, path.node); return false } this.traverse(path); } /** * Change the scope of the member expressions * @param { types.NodePath } path - containing the current node visited * @returns { boolean } return always false because we want to check only the first node object */ function visitMemberExpression(path) { const traversePathObject = () => this.traverse(path.get('object')); const currentObject = path.node.object; switch (true) { case isGlobal(path): if (currentObject.arguments && currentObject.arguments.length) { traversePathObject(); } break case !path.value.computed && isIdentifier(currentObject): replacePathScope(path, path.node); break default: this.traverse(path); } return false } /** * Objects properties should be handled a bit differently from the Identifier * @param { types.NodePath } path - containing the current node visited * @returns { boolean } return false if we want to stop the tree traversal */ function visitObjectProperty(path) { const value = path.node.value; const isShorthand = path.node.shorthand; if (isIdentifier(value) || isMemberExpression(value) || isShorthand) { // disable shorthand object properties if (isShorthand) path.node.shorthand = false; updateNodeScope.call(this, path.get('value')); } else { this.traverse(path.get('value')); } return false } /** * The this expressions should be replaced with the scope * @param { types.NodePath } path - containing the current node visited * @returns { boolean|undefined } return false if we want to stop the tree traversal */ function visitThisExpression(path) { path.replace(scope); this.traverse(path); return false } /** * Replace the identifiers with the node scope * @param { types.NodePath } path - containing the current node visited * @returns { boolean|undefined } return false if we want to stop the tree traversal */ function visitIdentifier(path) { const parentValue = path.parent.value; if ( (!isMemberExpression(parentValue) && // Esprima seem to behave differently from the default recast ast parser // fix for https://github.com/riot/riot/issues/2983 parentValue.key !== path.node) || parentValue.computed ) { updateNodeScope.call(this, path); } return false } /** * Update the scope of the global nodes * @param { Object } ast - ast program * @returns { Object } the ast program with all the global nodes updated */ function updateNodesScope(ast) { const ignorePath = () => false; types.visit(ast, { visitIdentifier, visitMemberExpression, visitObjectProperty, visitThisExpression, visitClassExpression: ignorePath, }); return ast } /** * Convert any expression to an AST tree * @param { Object } expression - expression parsed by the riot parser * @param { string } sourceFile - original tag file * @param { string } sourceCode - original tag source code * @returns { Object } the ast generated */ function createASTFromExpression(expression, sourceFile, sourceCode) { const code = sourceFile ? addLineOffset(expression.text, sourceCode, expression) : expression.text; return generateAST(`(${code})`, { sourceFileName: sourceFile, }) } /** * Create the bindings template property * @param {Array} args - arguments to pass to the template function * @returns {ASTNode} a binding template key */ function createTemplateProperty(args) { return simplePropertyNode( BINDING_TEMPLATE_KEY, args ? callTemplateFunction(...args) : nullNode(), ) } /** * Try to get the expression of an attribute node * @param { RiotParser.Node.Attribute } attribute - riot parser attribute node * @returns { RiotParser.Node.Expression } attribute expression value */ function getAttributeExpression(attribute) { return attribute.expressions ? attribute.expressions[0] : { // if no expression was found try to typecast the attribute value ...attribute, text: attribute.value, } } /** * Wrap the ast generated in a function call providing the scope argument * @param {Object} ast - function body * @returns {FunctionExpresion} function having the scope argument injected */ function wrapASTInFunctionWithScope(ast) { const fn = builders.arrowFunctionExpression([scope], ast); // object expressions need to be wrapped in parentheses // recast doesn't allow it // see also https://github.com/benjamn/recast/issues/985 if (isObjectExpression(ast)) { // doing a small hack here // trying to figure out how the recast printer works internally ast.extra = { parenthesized: true, }; } return fn } /** * Convert any parser option to a valid template one * @param { RiotParser.Node.Expression } expression - expression parsed by the riot parser * @param { string } sourceFile - original tag file * @param { string } sourceCode - original tag source code * @returns { Object } a FunctionExpression object * * @example * toScopedFunction('foo + bar') // scope.foo + scope.bar * * @example * toScopedFunction('foo.baz + bar') // scope.foo.baz + scope.bar */ function toScopedFunction(expression, sourceFile, sourceCode) { return compose(wrapASTInFunctionWithScope, transformExpression)( expression, sourceFile, sourceCode, ) } /** * Transform an expression node updating its global scope * @param {RiotParser.Node.Expr} expression - riot parser expression node * @param {string} sourceFile - source file * @param {string} sourceCode - source code * @returns {ASTExpression} ast expression generated from the riot parser expression node */ function transformExpression(expression, sourceFile, sourceCode) { return compose( removeExtraParenthesis, getExpressionAST, updateNodesScope, createASTFromExpression, )(expression, sourceFile, sourceCode) } /** * Remove the extra parents from the compiler generated expressions * @param {AST.Expression} expr - ast expression * @returns {AST.Expression} program expression output without parenthesis */ function removeExtraParenthesis(expr) { if (expr.extra) expr.extra.parenthesized = false; return expr } /** * Get the parsed AST expression of riot expression node * @param {AST.Program} sourceAST - raw node parsed * @returns {AST.Expression} program expression output */ function getExpressionAST(sourceAST) { const astBody = sourceAST.program.body; return astBody[0] ? astBody[0].expression : astBody } /** * Create the template call function * @param {Array|string|Node.Literal} template - template string * @param {Array<AST.Nodes>} bindings - template bindings provided as AST nodes * @returns {Node.CallExpression} template call expression */ function callTemplateFunction(template, bindings) { return builders.callExpression(builders.identifier(TEMPLATE_FN), [ template ? builders.literal(template) : nullNode(), bindings ? builders.arrayExpression(bindings) : nullNode(), ]) } /** * Create the template wrapper function injecting the dependencies needed to render the component html * @param {Array<AST.Nodes>|AST.BlockStatement} body - function body * @returns {AST.Node} arrow function expression */ const createTemplateDependenciesInjectionWrapper = (body) => builders.arrowFunctionExpression( [TEMPLATE_FN, EXPRESSION_TYPES, BINDING_TYPES, GET_COMPONENT_FN].map( builders.identifier, ), body, ); /** * Convert any DOM attribute into a valid DOM selector useful for the querySelector API * @param { string } attributeName - name of the attribute to query * @returns { string } the attribute transformed to a query selector */ const attributeNameToDOMQuerySelector = (attributeName) => `[${attributeName}]`; /** * Create the properties to query a DOM node * @param { string } attributeName - attribute name needed to identify a DOM node * @returns { Array<AST.Node> } array containing the selector properties needed for the binding */ function createSelectorProperties(attributeName) { return attributeName ? [ simplePropertyNode( BINDING_REDUNDANT_ATTRIBUTE_KEY, builders.literal(attributeName), ), simplePropertyNode( BINDING_SELECTOR_KEY, compose( builders.literal, attributeNameToDOMQuerySelector, )(attributeName), ), ] : [] } /** * Clone the node filtering out the selector attribute from the attributes list * @param {RiotParser.Node} node - riot parser node * @param {string} selectorAttribute - name of the selector attribute to filter out * @returns {RiotParser.Node} the node with the attribute cleaned up */ function cloneNodeWithoutSelectorAttribute(node, selectorAttribute) { return { ...node, attributes: getAttributesWithoutSelector( getNodeAttributes(node), selectorAttribute, ), } } /** * Get the node attributes without the selector one * @param {Array<RiotParser.Attr>} attributes - attributes list * @param {string} selectorAttribute - name of the selector attribute to filter out * @returns {Array<RiotParser.Attr>} filtered attributes */ function getAttributesWithoutSelector(attributes, selectorAttribute) { if (selectorAttribute) return attributes.filter( (attribute) => attribute.name !== selectorAttribute, ) return attributes } /** * Clean binding or custom attributes * @param {RiotParser.Node} node - riot parser node * @returns {Array<RiotParser.Node.Attr>} only the attributes that are not bindings or directives */ function cleanAttributes(node) { return getNodeAttributes(node).filter( (attribute) => ![ IF_DIRECTIVE, EACH_DIRECTIVE, KEY_ATTRIBUTE, SLOT_ATTRIBUTE, IS_DIRECTIVE, ].includes(attribute.name), ) } /** * Root node factory function needed for the top root nodes and the nested ones * @param {RiotParser.Node} node - riot parser node * @returns {RiotParser.Node} root node */ function rootNodeFactory(node) { return { nodes: getChildrenNodes(node), isRoot: true, } } /** * Create a root node proxing only its nodes and attributes * @param {RiotParser.Node} node - riot parser node * @returns {RiotParser.Node} root node */ function createRootNode(node) { return { ...rootNodeFactory(node), attributes: compose( // root nodes should always have attribute expressions transformStaticAttributesIntoExpressions, // root nodes shouldn't have directives cleanAttributes, )(node), } } /** * Create nested root node. Each and If directives create nested root nodes for example * @param {RiotParser.Node} node - riot parser node * @returns {RiotParser.Node} root node */ function createNestedRootNode(node) { return { ...rootNodeFactory(node), isNestedRoot: true, attributes: cleanAttributes(node), } } /** * Transform the static node attributes into expressions, useful for the root nodes * @param {Array<RiotParser.Node.Attr>} attributes - riot parser node * @returns {Array<RiotParser.Node.Attr>} all the attributes received as attribute expressions */ function transformStaticAttributesIntoExpressions(attributes) { return attributes.map((attribute) => { if (attribute.expressions) return attribute return { ...attribute, expressions: [ { start: attribute.valueStart, end: attribute.end, text: `'${ attribute.value ? attribute.value : // boolean attributes should be treated differently attribute[IS_BOOLEAN_ATTRIBUTE] ? attribute.name : '' }'`, }, ], } }) } /** * Get all the child nodes of a RiotParser.Node * @param {RiotParser.Node} node - riot parser node * @returns {Array<RiotParser.Node>} all the child nodes found */ function getChildrenNodes(node) { return node && node.nodes ? node.nodes : [] } /** * Get all the attributes of a riot parser node * @param {RiotParser.Node} node - riot parser node * @returns {Array<RiotParser.Node.Attribute>} all the attributes find */ function getNodeAttributes(node) { return node.attributes ? node.attributes : [] } /** * Create custom tag name function * @param {RiotParser.Node} node - riot parser node * @param {string} sourceFile - original tag file * @param {string} sourceCode - original tag source code * @returns {RiotParser.Node.Attr} the node name as expression attribute */ function createCustomNodeNameEvaluationFunction( node, sourceFile, sourceCode, ) { const isAttribute = findIsAttribute(node); const toRawString = (val) => `'${val}'`; if (isAttribute) { return isAttribute.expressions ? wrapASTInFunctionWithScope( mergeAttributeExpressions(isAttribute, sourceFile, sourceCode), ) : toScopedFunction( { ...isAttribute, text: toRawString(isAttribute.value), }, sourceFile, sourceCode, ) } return toScopedFunction( { ...node, text: toRawString(getName(node)) }, sourceFile, sourceCode, ) } /** * Convert all the node static attributes to strings * @param {RiotParser.Node} node - riot parser node * @returns {string} all the node static concatenated as string */ function staticAttributesToString(node) { return findStaticAttributes(node) .map((attribute) => attribute[IS_BOOLEAN_ATTRIBUTE] || !attribute.value ? attribute.name : `${attribute.name}="${unescapeNode(attribute, 'value').value}"`, ) .join(' ') } /** * Make sure that node escaped chars will be unescaped * @param {RiotParser.Node} node - riot parser node * @param {string} key - key property to unescape * @returns {RiotParser.Node} node with the text property unescaped */ function unescapeNode(node, key) { if (node.unescape) { return { ...node, [key]: unescapeChar(node[key], node.unescape), } } return node } /** * Convert a riot parser opening node into a string * @param {RiotParser.Node} node - riot parser node * @returns {string} the node as string */ function nodeToString(node) { const attributes = staticAttributesToString(node); switch (true) { case isTagNode(node): return `<${node.name}${attributes ? ` ${attributes}` : ''}${ isVoidNode(node) ? '/' : '' }>` case isTextNode(node): return hasExpressions(node) ? TEXT_NODE_EXPRESSION_PLACEHOLDER : unescapeNode(node, 'text').text default: return node.text || '' } } /** * Close an html node * @param {RiotParser.Node} node - riot parser node * @returns {string} the closing tag of the html tag node passed to this function */ function closeTag(node) { return node.name ? `</${node.name}>` : '' } /** * Create a strings array with the `join` call to transform it into a string * @param {Array} stringsArray - array containing all the strings to concatenate * @returns {AST.CallExpression} array with a `join` call */ function createArrayString(stringsArray) { return builders.callExpression( builders.memberExpression( builders.arrayExpression(stringsArray), builders.identifier('join'), false, ), [builders.literal('')], ) } /** * Simple expression bindings might contain multiple expressions like for example: "class="{foo} red {bar}"" * This helper aims to merge them in a template literal if it's necessary * @param {RiotParser.Attr} node - riot parser node * @param {string} sourceFile - original tag file * @param {string} sourceCode - original tag source code * @returns { Object } a template literal expression object */ function mergeAttributeExpressions(node, sourceFile, sourceCode) { if (!node.parts || node.parts.length === 1) { return transformExpression(node.expressions[0], sourceFile, sourceCode) } const stringsArray = [ ...node.parts.reduce((acc, str) => { const expression = node.expressions.find((e) => e.text.trim() === str); return [ ...acc, expression ? transformExpression(expression, sourceFile, sourceCode) : builders.literal(encodeHTMLEntities(str)), ] }, []), ].filter((expr) => !isLiteral(expr) ||