@riotjs/compiler
Version:
Compiler for Riot.js .riot files
1,937 lines (1,798 loc) • 109 kB
JavaScript
/* 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 Ȣ
* @param {string} string - input string
* @returns {string} encoded string
*/
function encodeHex(string) {
const hex = string.substr(2);
return HEX_NUMBER.test(hex) ? String.fromCodePoint(parseInt(hex, 16)) : string
}
/**
* Encode unicode decimal html entities like for example Þ
* @param {string} string - input string
* @returns {string} encoded string
*/
function encodeDecimal(string) {
const nr = string.substr(1);
return DECIMAL_NUMBER.test(nr)
? String.fromCodePoint(parseInt(nr, 10))
: string
}
/**
* Encode html entities in strings like
* @param {string} string - input string
* @returns {string} encoded string
*/
function encodeHTMLEntities(string) {
return string.replace(HTMLEntityRe, (match, entity) => {
const [firstChar, secondChar] = entity;
if (firstChar === '#') {
return secondChar === 'x' ? encodeHex(entity) : encodeDecimal(entity)
} else {
return entities[entity] || entity
}
})
}
/**
* Native String.prototype.trimEnd method with fallback to String.prototype.trimRight
* Edge doesn't support the first one
* @param {string} string - input string
* @returns {string} trimmed output
*/
function trimEnd(string) {
return (string.trimEnd || string.trimRight).apply(string)
}
/**
* Native String.prototype.trimStart method with fallback to String.prototype.trimLeft
* Edge doesn't support the first one
* @param {string} string - input string
* @returns {string} trimmed output
*/
function trimStart(string) {
return (string.trimStart || string.trimLeft).apply(string)
}
/**
* Unescape the user escaped chars
* @param {string} string - input string
* @param {string} char - probably a '{' or anything the user want's to escape
* @returns {string} cleaned up string
*/
function unescapeChar(string, char) {
return string.replace(RegExp(`\\\\${char}`, 'gm'), char)
}
/**
* Generate the pure immutable string chunks from a RiotParser.Node.Text
* @param {RiotParser.Node.Text} node - riot parser text node
* @param {string} sourceCode sourceCode - source code
* @returns {Array} array containing the immutable string chunks
*/
function generateLiteralStringChunksFromNode(node, sourceCode) {
return (
node.expressions
.reduce((chunks, expression, index) => {
const start = index ? node.expressions[index - 1].end : node.start;
const string = encodeHTMLEntities(
sourceCode.substring(start, expression.start),
);
// trimStart the first string
chunks.push(index === 0 ? trimStart(string) : string);
// add the tail to the string
if (index === node.expressions.length - 1)
chunks.push(
encodeHTMLEntities(
trimEnd(sourceCode.substring(expression.end, node.end)),
),
);
return chunks
}, [])
// comments are not supported here
.filter((str) => !isCommentString(str))
.map((str) => (node.unescape ? unescapeChar(str, node.unescape) : str))
)
}
/**
* Simple bindings might contain multiple expressions like for example: "{foo} and {bar}"
* This helper aims to merge them in a template literal if it's necessary
* @param {RiotParser.Node} node - riot parser node
* @param {string} sourceFile - original tag file
* @param {string} sourceCode - original tag source code
* @returns { Object } a template literal expression object
*/
function mergeNodeExpressions(node, sourceFile, sourceCode) {
if (node.parts.length === 1)
return transformExpression(node.expressions[0], sourceFile, sourceCode)
const pureStringChunks = generateLiteralStringChunksFromNode(node, sourceCode);
const stringsArray = pureStringChunks
.reduce((acc, str, index) => {
const expr = node.expressions[index];
return [
...acc,
builders.literal(str),
expr ? transformExpression(expr, sourceFile, sourceCode) : nullNode(),
]
}, [])
// filter the empty literal expressions
.filter((expr) => !isLiteral(expr) || expr.value);
return createArrayString(stringsArray)
}
/**
* Create a text expression
* @param {RiotParser.Node.Text} sourceNode - text node to parse
* @param {string} sourceFile - source file path
* @param {string} sourceCode - original source
* @param {number} childNodeIndex - position of the child text node in its parent children nodes
* @returns {AST.Node} object containing the expression binding keys
*/
function createTextExpression(
sourceNode,
sourceFile,
sourceCode,
childNodeIndex,
) {
return builders.objectExpression([
simplePropertyNode(
BINDING_TYPE_KEY,
builders.memberExpression(
builders.identifier(EXPRESSION_TYPES),
builders.identifier(TEXT_EXPRESSION_TYPE),
false,
),
),
simplePropertyNode(
BINDING_CHILD_NODE_INDEX_KEY,
builders.literal(childNodeIndex),
),
simplePropertyNode(
BINDING_EVALUATE_KEY,
wrapASTInFunctionWithScope(
mergeNodeExpressions(sourceNode, sourceFile, sourceCode),
),
),
])
}
function createValueExpression(
sourceNode,
sourceFile,
sourceCode,
) {
return builders.objectExpression([
simplePropertyNode(
BINDING_TYPE_KEY,
builders.memberExpression(
builders.identifier(EXPRESSION_TYPES),
builders.identifier(VALUE_EXPRESSION_TYPE),
false,
),
),
simplePropertyNode(
BINDING_EVALUATE_KEY,
createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode),
),
])
}
function createExpression(
sourceNode,
sourceFile,
sourceCode,
childNodeIndex,
parentNode,
) {
switch (true) {
case isTextNode(sourceNode):
return createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex)
// progress nodes value attributes will be rendered as attributes
// see https://github.com/riot/compiler/issues/122
case isValueAttribute(sourceNode) &&
hasValueAttribute(parentNode.name) &&
!isProgressNode(parentNode):
return createValueExpression(sourceNode, sourceFile, sourceCode)
case isEventAttribute(sourceNode):
return createEventExpression(sourceNode, sourceFile, sourceCode)
default:
return createAttributeExpression(sourceNode, 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) ||