You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2157 lines
69 KiB

5 years ago
  1. /* Riot Compiler WIP, @license MIT */
  2. import { types as types$1, print, parse } from 'recast';
  3. import { composeSourceMaps } from 'recast/lib/util';
  4. import { SourceMapGenerator } from 'source-map';
  5. import compose from 'cumpa';
  6. import cssEscape from 'cssesc';
  7. import curry from 'curri';
  8. import { Parser } from 'acorn';
  9. import globalScope from 'globals';
  10. import riotParser, { nodeTypes } from '@riotjs/parser';
  11. import { hasValueAttribute } from 'dom-nodes';
  12. const TAG_LOGIC_PROPERTY = 'exports';
  13. const TAG_CSS_PROPERTY = 'css';
  14. const TAG_TEMPLATE_PROPERTY = 'template';
  15. const TAG_NAME_PROPERTY = 'name';
  16. const types = types$1;
  17. const builders = types$1.builders;
  18. const namedTypes = types$1.namedTypes;
  19. function nullNode() {
  20. return builders.literal(null)
  21. }
  22. function simplePropertyNode(key, value) {
  23. return builders.property('init', builders.literal(key), value, false)
  24. }
  25. /**
  26. * Return a source map as JSON, it it has not the toJSON method it means it can
  27. * be used right the way
  28. * @param { SourceMapGenerator|Object } map - a sourcemap generator or simply an json object
  29. * @returns { Object } the source map as JSON
  30. */
  31. function sourcemapAsJSON(map) {
  32. if (map && map.toJSON) return map.toJSON()
  33. return map
  34. }
  35. /**
  36. * Detect node js environements
  37. * @returns { boolean } true if the runtime is node
  38. */
  39. function isNode() {
  40. return typeof process !== 'undefined'
  41. }
  42. /**
  43. * Compose two sourcemaps
  44. * @param { SourceMapGenerator } formerMap - original sourcemap
  45. * @param { SourceMapGenerator } latterMap - target sourcemap
  46. * @returns { Object } sourcemap json
  47. */
  48. function composeSourcemaps(formerMap, latterMap) {
  49. if (
  50. isNode() &&
  51. formerMap && latterMap && latterMap.mappings
  52. ) {
  53. return composeSourceMaps(sourcemapAsJSON(formerMap), sourcemapAsJSON(latterMap))
  54. } else if (isNode() && formerMap) {
  55. return sourcemapAsJSON(formerMap)
  56. }
  57. return {}
  58. }
  59. /**
  60. * Create a new sourcemap generator
  61. * @param { Object } options - sourcemap options
  62. * @returns { SourceMapGenerator } SourceMapGenerator instance
  63. */
  64. function createSourcemap(options) {
  65. return new SourceMapGenerator(options)
  66. }
  67. const Output = Object.freeze({
  68. code: '',
  69. ast: [],
  70. meta: {},
  71. map: null
  72. });
  73. /**
  74. * Create the right output data result of a parsing
  75. * @param { Object } data - output data
  76. * @param { string } data.code - code generated
  77. * @param { AST } data.ast - ast representing the code
  78. * @param { SourceMapGenerator } data.map - source map generated along with the code
  79. * @param { Object } meta - compilation meta infomration
  80. * @returns { Output } output container object
  81. */
  82. function createOutput(data, meta) {
  83. const output = {
  84. ...Output,
  85. ...data,
  86. meta
  87. };
  88. if (!output.map && meta && meta.options && meta.options.file)
  89. return {
  90. ...output,
  91. map: createSourcemap({ file: meta.options.file })
  92. }
  93. return output
  94. }
  95. /**
  96. * Transform the source code received via a compiler function
  97. * @param { Function } compiler - function needed to generate the output code
  98. * @param { Object } meta - compilation meta information
  99. * @param { string } source - source code
  100. * @returns { Output } output - the result of the compiler
  101. */
  102. function transform(compiler, meta, source) {
  103. const result = (compiler ? compiler(source, meta) : { code: source });
  104. return createOutput(result, meta)
  105. }
  106. /**
  107. * Throw an error with a descriptive message
  108. * @param { string } message - error message
  109. * @returns { undefined } hoppla.. at this point the program should stop working
  110. */
  111. function panic(message) {
  112. throw new Error(message)
  113. }
  114. const postprocessors = new Set();
  115. /**
  116. * Register a postprocessor that will be used after the parsing and compilation of the riot tags
  117. * @param { Function } postprocessor - transformer that will receive the output code ans sourcemap
  118. * @returns { Set } the postprocessors collection
  119. */
  120. function register(postprocessor) {
  121. if (postprocessors.has(postprocessor)) {
  122. panic(`This postprocessor "${postprocessor.name || postprocessor.toString()}" was already registered`);
  123. }
  124. postprocessors.add(postprocessor);
  125. return postprocessors
  126. }
  127. /**
  128. * Exec all the postprocessors in sequence combining the sourcemaps generated
  129. * @param { Output } compilerOutput - output generated by the compiler
  130. * @param { Object } meta - compiling meta information
  131. * @returns { Output } object containing output code and source map
  132. */
  133. function execute(compilerOutput, meta) {
  134. return Array.from(postprocessors).reduce(function(acc, postprocessor) {
  135. const { code, map } = acc;
  136. const output = postprocessor(code, meta);
  137. return {
  138. code: output.code,
  139. map: composeSourcemaps(map, output.map)
  140. }
  141. }, createOutput(compilerOutput, meta))
  142. }
  143. /**
  144. * Parsers that can be registered by users to preparse components fragments
  145. * @type { Object }
  146. */
  147. const preprocessors = Object.freeze({
  148. javascript: new Map(),
  149. css: new Map(),
  150. template: new Map().set('default', code => ({ code }))
  151. });
  152. // throw a processor type error
  153. function preprocessorTypeError(type) {
  154. panic(`No preprocessor of type "${type}" was found, please make sure to use one of these: 'javascript', 'css' or 'template'`);
  155. }
  156. // throw an error if the preprocessor was not registered
  157. function preprocessorNameNotFoundError(name) {
  158. panic(`No preprocessor named "${name}" was found, are you sure you have registered it?'`);
  159. }
  160. /**
  161. * Register a custom preprocessor
  162. * @param { string } type - preprocessor type either 'js', 'css' or 'template'
  163. * @param { string } name - unique preprocessor id
  164. * @param { Function } preprocessor - preprocessor function
  165. * @returns { Map } - the preprocessors map
  166. */
  167. function register$1(type, name, preprocessor) {
  168. if (!type) panic('Please define the type of preprocessor you want to register \'javascript\', \'css\' or \'template\'');
  169. if (!name) panic('Please define a name for your preprocessor');
  170. if (!preprocessor) panic('Please provide a preprocessor function');
  171. if (!preprocessors[type]) preprocessorTypeError(type);
  172. if (preprocessors[type].has(name)) panic(`The preprocessor ${name} was already registered before`);
  173. preprocessors[type].set(name, preprocessor);
  174. return preprocessors
  175. }
  176. /**
  177. * Exec the compilation of a preprocessor
  178. * @param { string } type - preprocessor type either 'js', 'css' or 'template'
  179. * @param { string } name - unique preprocessor id
  180. * @param { Object } meta - preprocessor meta information
  181. * @param { string } source - source code
  182. * @returns { Output } object containing a sourcemap and a code string
  183. */
  184. function execute$1(type, name, meta, source) {
  185. if (!preprocessors[type]) preprocessorTypeError(type);
  186. if (!preprocessors[type].has(name)) preprocessorNameNotFoundError(name);
  187. return transform(preprocessors[type].get(name), meta, source)
  188. }
  189. const ATTRIBUTE_TYPE_NAME = 'type';
  190. /**
  191. * Get the type attribute from a node generated by the riot parser
  192. * @param { Object} sourceNode - riot parser node
  193. * @returns { string|null } a valid type to identify the preprocessor to use or nothing
  194. */
  195. function getPreprocessorTypeByAttribute(sourceNode) {
  196. const typeAttribute = sourceNode.attributes ?
  197. sourceNode.attributes.find(attribute => attribute.name === ATTRIBUTE_TYPE_NAME) :
  198. null;
  199. return typeAttribute ? normalize(typeAttribute.value) : null
  200. }
  201. /**
  202. * Remove the noise in case a user has defined the preprocessor type='text/scss'
  203. * @param { string } value - input string
  204. * @returns { string } normalized string
  205. */
  206. function normalize(value) {
  207. return value.replace('text/', '')
  208. }
  209. /**
  210. * Preprocess a riot parser node
  211. * @param { string } preprocessorType - either css, js
  212. * @param { string } preprocessorName - preprocessor id
  213. * @param { Object } meta - compilation meta information
  214. * @param { RiotParser.nodeTypes } node - css node detected by the parser
  215. * @returns { Output } code and sourcemap generated by the preprocessor
  216. */
  217. function preprocess(preprocessorType, preprocessorName, meta, node) {
  218. const code = node.text;
  219. return (preprocessorName ?
  220. execute$1(preprocessorType, preprocessorName, meta, code) :
  221. { code }
  222. )
  223. }
  224. /**
  225. * Matches valid, multiline JavaScript comments in almost all its forms.
  226. * @const {RegExp}
  227. * @static
  228. */
  229. const R_MLCOMMS = /\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//g;
  230. /**
  231. * Source for creating regexes matching valid quoted, single-line JavaScript strings.
  232. * It recognizes escape characters, including nested quotes and line continuation.
  233. * @const {string}
  234. */
  235. const S_LINESTR = /"[^"\n\\]*(?:\\[\S\s][^"\n\\]*)*"|'[^'\n\\]*(?:\\[\S\s][^'\n\\]*)*'/.source;
  236. /**
  237. * Matches CSS selectors, excluding those beginning with '@' and quoted strings.
  238. * @const {RegExp}
  239. */
  240. const CSS_SELECTOR = RegExp(`([{}]|^)[; ]*((?:[^@ ;{}][^{}]*)?[^@ ;{}:] ?)(?={)|${S_LINESTR}`, 'g');
  241. /**
  242. * Parses styles enclosed in a "scoped" tag
  243. * The "css" string is received without comments or surrounding spaces.
  244. *
  245. * @param {string} tag - Tag name of the root element
  246. * @param {string} css - The CSS code
  247. * @returns {string} CSS with the styles scoped to the root element
  248. */
  249. function scopedCSS(tag, css) {
  250. const host = ':host';
  251. const selectorsBlacklist = ['from', 'to'];
  252. return css.replace(CSS_SELECTOR, function(m, p1, p2) {
  253. // skip quoted strings
  254. if (!p2) return m
  255. // we have a selector list, parse each individually
  256. p2 = p2.replace(/[^,]+/g, function(sel) {
  257. const s = sel.trim();
  258. // skip selectors already using the tag name
  259. if (s.indexOf(tag) === 0) {
  260. return sel
  261. }
  262. // skips the keywords and percents of css animations
  263. if (!s || selectorsBlacklist.indexOf(s) > -1 || s.slice(-1) === '%') {
  264. return sel
  265. }
  266. // replace the `:host` pseudo-selector, where it is, with the root tag name;
  267. // if `:host` was not included, add the tag name as prefix, and mirror all
  268. // `[data-is]`
  269. if (s.indexOf(host) < 0) {
  270. return `${tag} ${s},[is="${tag}"] ${s}`
  271. } else {
  272. return `${s.replace(host, tag)},${
  273. s.replace(host, `[is="${tag}"]`)}`
  274. }
  275. });
  276. // add the danling bracket char and return the processed selector list
  277. return p1 ? `${p1} ${p2}` : p2
  278. })
  279. }
  280. /**
  281. * Remove comments, compact and trim whitespace
  282. * @param { string } code - compiled css code
  283. * @returns { string } css code normalized
  284. */
  285. function compactCss(code) {
  286. return code.replace(R_MLCOMMS, '').replace(/\s+/g, ' ').trim()
  287. }
  288. const escapeBackslashes = s => s.replace(/\\/g, '\\\\');
  289. const escapeIdentifier = identifier => escapeBackslashes(cssEscape(identifier, {
  290. isIdentifier: true
  291. }));
  292. /**
  293. * Generate the component css
  294. * @param { Object } sourceNode - node generated by the riot compiler
  295. * @param { string } source - original component source code
  296. * @param { Object } meta - compilation meta information
  297. * @param { AST } ast - current AST output
  298. * @returns { AST } the AST generated
  299. */
  300. function css(sourceNode, source, meta, ast) {
  301. const preprocessorName = getPreprocessorTypeByAttribute(sourceNode);
  302. const { options } = meta;
  303. const preprocessorOutput = preprocess('css', preprocessorName, meta, sourceNode.text);
  304. const normalizedCssCode = compactCss(preprocessorOutput.code);
  305. const escapedCssIdentifier = escapeIdentifier(meta.tagName);
  306. const cssCode = (options.scopedCss ?
  307. scopedCSS(escapedCssIdentifier, escapeBackslashes(normalizedCssCode)) :
  308. escapeBackslashes(normalizedCssCode)
  309. ).trim();
  310. types.visit(ast, {
  311. visitProperty(path) {
  312. if (path.value.key.value === TAG_CSS_PROPERTY) {
  313. path.value.value = builders.templateLiteral(
  314. [builders.templateElement({ raw: cssCode, cooked: '' }, false)],
  315. []
  316. );
  317. return false
  318. }
  319. this.traverse(path);
  320. }
  321. });
  322. return ast
  323. }
  324. /**
  325. * Generate the javascript from an ast source
  326. * @param {AST} ast - ast object
  327. * @param {Object} options - printer options
  328. * @returns {Object} code + map
  329. */
  330. function generateJavascript(ast, options) {
  331. return print(ast, {
  332. ...options,
  333. tabWidth: 2,
  334. quote: 'single'
  335. })
  336. }
  337. /**
  338. * True if the sourcemap has no mappings, it is empty
  339. * @param {Object} map - sourcemap json
  340. * @returns {boolean} true if empty
  341. */
  342. function isEmptySourcemap(map) {
  343. return !map || !map.mappings || !map.mappings.length
  344. }
  345. const LINES_RE = /\r\n?|\n/g;
  346. /**
  347. * Split a string into a rows array generated from its EOL matches
  348. * @param { string } string [description]
  349. * @returns { Array } array containing all the string rows
  350. */
  351. function splitStringByEOL(string) {
  352. return string.split(LINES_RE)
  353. }
  354. /**
  355. * Get the line and the column of a source text based on its position in the string
  356. * @param { string } string - target string
  357. * @param { number } position - target position
  358. * @returns { Object } object containing the source text line and column
  359. */
  360. function getLineAndColumnByPosition(string, position) {
  361. const lines = splitStringByEOL(string.slice(0, position));
  362. return {
  363. line: lines.length,
  364. column: lines[lines.length - 1].length
  365. }
  366. }
  367. /**
  368. * Add the offset to the code that must be parsed in order to generate properly the sourcemaps
  369. * @param {string} input - input string
  370. * @param {string} source - original source code
  371. * @param {RiotParser.Node} node - node that we are going to transform
  372. * @return {string} the input string with the offset properly set
  373. */
  374. function addLineOffset(input, source, node) {
  375. const {column, line} = getLineAndColumnByPosition(source, node.start);
  376. return `${'\n'.repeat(line - 1)}${' '.repeat(column + 1)}${input}`
  377. }
  378. /**
  379. * Parse a js source to generate the AST
  380. * @param {string} source - javascript source
  381. * @param {Object} options - parser options
  382. * @returns {AST} AST tree
  383. */
  384. function generateAST(source, options) {
  385. return parse(source, {
  386. parser: {
  387. parse(source, opts) {
  388. return Parser.parse(source, {
  389. ...opts,
  390. ecmaVersion: 2020
  391. })
  392. }
  393. },
  394. ...options
  395. })
  396. }
  397. const browserAPIs = Object.keys(globalScope.browser);
  398. const builtinAPIs = Object.keys(globalScope.builtin);
  399. const isIdentifier = namedTypes.Identifier.check.bind(namedTypes.Identifier);
  400. const isLiteral = namedTypes.Literal.check.bind(namedTypes.Literal);
  401. const isExpressionStatement = namedTypes.ExpressionStatement.check.bind(namedTypes.ExpressionStatement);
  402. const isObjectExpression = namedTypes.ObjectExpression.check.bind(namedTypes.ObjectExpression);
  403. const isThisExpression = namedTypes.ThisExpression.check.bind(namedTypes.ThisExpression);
  404. const isNewExpression = namedTypes.NewExpression.check.bind(namedTypes.NewExpression);
  405. const isSequenceExpression = namedTypes.SequenceExpression.check.bind(namedTypes.SequenceExpression);
  406. const isBinaryExpression = namedTypes.BinaryExpression.check.bind(namedTypes.BinaryExpression);
  407. const isExportDefaultStatement = namedTypes.ExportDefaultDeclaration.check.bind(namedTypes.ExportDefaultDeclaration);
  408. const isBrowserAPI = ({name}) => browserAPIs.includes(name);
  409. const isBuiltinAPI = ({name}) => builtinAPIs.includes(name);
  410. const isRaw = (node) => node && node.raw; // eslint-disable-line
  411. /**
  412. * Find the export default statement
  413. * @param { Array } body - tree structure containing the program code
  414. * @returns { Object } node containing only the code of the export default statement
  415. */
  416. function findExportDefaultStatement(body) {
  417. return body.find(isExportDefaultStatement)
  418. }
  419. /**
  420. * Find all the code in an ast program except for the export default statements
  421. * @param { Array } body - tree structure containing the program code
  422. * @returns { Array } array containing all the program code except the export default expressions
  423. */
  424. function filterNonExportDefaultStatements(body) {
  425. return body.filter(node => !isExportDefaultStatement(node))
  426. }
  427. /**
  428. * Get the body of the AST structure
  429. * @param { Object } ast - ast object generated by recast
  430. * @returns { Array } array containing the program code
  431. */
  432. function getProgramBody(ast) {
  433. return ast.body || ast.program.body
  434. }
  435. /**
  436. * Extend the AST adding the new tag method containing our tag sourcecode
  437. * @param { Object } ast - current output ast
  438. * @param { Object } exportDefaultNode - tag export default node
  439. * @returns { Object } the output ast having the "tag" key extended with the content of the export default
  440. */
  441. function extendTagProperty(ast, exportDefaultNode) {
  442. types.visit(ast, {
  443. visitProperty(path) {
  444. if (path.value.key.value === TAG_LOGIC_PROPERTY) {
  445. path.value.value = exportDefaultNode.declaration;
  446. return false
  447. }
  448. this.traverse(path);
  449. }
  450. });
  451. return ast
  452. }
  453. /**
  454. * Generate the component javascript logic
  455. * @param { Object } sourceNode - node generated by the riot compiler
  456. * @param { string } source - original component source code
  457. * @param { Object } meta - compilation meta information
  458. * @param { AST } ast - current AST output
  459. * @returns { AST } the AST generated
  460. */
  461. function javascript(sourceNode, source, meta, ast) {
  462. const preprocessorName = getPreprocessorTypeByAttribute(sourceNode);
  463. const javascriptNode = addLineOffset(sourceNode.text.text, source, sourceNode);
  464. const { options } = meta;
  465. const preprocessorOutput = preprocess('javascript', preprocessorName, meta, {
  466. ...sourceNode,
  467. text: javascriptNode
  468. });
  469. const inputSourceMap = sourcemapAsJSON(preprocessorOutput.map);
  470. const generatedAst = generateAST(preprocessorOutput.code, {
  471. sourceFileName: options.file,
  472. inputSourceMap: isEmptySourcemap(inputSourceMap) ? null : inputSourceMap
  473. });
  474. const generatedAstBody = getProgramBody(generatedAst);
  475. const bodyWithoutExportDefault = filterNonExportDefaultStatements(generatedAstBody);
  476. const exportDefaultNode = findExportDefaultStatement(generatedAstBody);
  477. const outputBody = getProgramBody(ast);
  478. // add to the ast the "private" javascript content of our tag script node
  479. outputBody.unshift(...bodyWithoutExportDefault);
  480. // convert the export default adding its content to the "tag" property exported
  481. if (exportDefaultNode) extendTagProperty(ast, exportDefaultNode);
  482. return ast
  483. }
  484. // import {IS_BOOLEAN,IS_CUSTOM,IS_RAW,IS_SPREAD,IS_VOID} from '@riotjs/parser/src/constants'
  485. const BINDING_TYPES = 'bindingTypes';
  486. const EACH_BINDING_TYPE = 'EACH';
  487. const IF_BINDING_TYPE = 'IF';
  488. const TAG_BINDING_TYPE = 'TAG';
  489. const SLOT_BINDING_TYPE = 'SLOT';
  490. const EXPRESSION_TYPES = 'expressionTypes';
  491. const ATTRIBUTE_EXPRESSION_TYPE = 'ATTRIBUTE';
  492. const VALUE_EXPRESSION_TYPE = 'VALUE';
  493. const TEXT_EXPRESSION_TYPE = 'TEXT';
  494. const EVENT_EXPRESSION_TYPE = 'EVENT';
  495. const TEMPLATE_FN = 'template';
  496. const SCOPE = 'scope';
  497. const GET_COMPONENT_FN = 'getComponent';
  498. // keys needed to create the DOM bindings
  499. const BINDING_SELECTOR_KEY = 'selector';
  500. const BINDING_GET_COMPONENT_KEY = 'getComponent';
  501. const BINDING_TEMPLATE_KEY = 'template';
  502. const BINDING_TYPE_KEY = 'type';
  503. const BINDING_REDUNDANT_ATTRIBUTE_KEY = 'redundantAttribute';
  504. const BINDING_CONDITION_KEY = 'condition';
  505. const BINDING_ITEM_NAME_KEY = 'itemName';
  506. const BINDING_GET_KEY_KEY = 'getKey';
  507. const BINDING_INDEX_NAME_KEY = 'indexName';
  508. const BINDING_EVALUATE_KEY = 'evaluate';
  509. const BINDING_NAME_KEY = 'name';
  510. const BINDING_SLOTS_KEY = 'slots';
  511. const BINDING_EXPRESSIONS_KEY = 'expressions';
  512. const BINDING_CHILD_NODE_INDEX_KEY = 'childNodeIndex';
  513. // slots keys
  514. const BINDING_BINDINGS_KEY = 'bindings';
  515. const BINDING_ID_KEY = 'id';
  516. const BINDING_HTML_KEY = 'html';
  517. const BINDING_ATTRIBUTES_KEY = 'attributes';
  518. // DOM directives
  519. const IF_DIRECTIVE = 'if';
  520. const EACH_DIRECTIVE = 'each';
  521. const KEY_ATTRIBUTE = 'key';
  522. const SLOT_ATTRIBUTE = 'slot';
  523. const NAME_ATTRIBUTE = 'name';
  524. const IS_DIRECTIVE = 'is';
  525. // Misc
  526. const DEFAULT_SLOT_NAME = 'default';
  527. const TEXT_NODE_EXPRESSION_PLACEHOLDER = '<!---->';
  528. const BINDING_SELECTOR_PREFIX = 'expr';
  529. const SLOT_TAG_NODE_NAME = 'slot';
  530. const PROGRESS_TAG_NODE_NAME = 'progress';
  531. const IS_VOID_NODE = 'isVoid';
  532. const IS_CUSTOM_NODE = 'isCustom';
  533. const IS_BOOLEAN_ATTRIBUTE = 'isBoolean';
  534. const IS_SPREAD_ATTRIBUTE = 'isSpread';
  535. /**
  536. * True if the node has not expression set nor bindings directives
  537. * @param {RiotParser.Node} node - riot parser node
  538. * @returns {boolean} true only if it's a static node that doesn't need bindings or expressions
  539. */
  540. function isStaticNode(node) {
  541. return [
  542. hasExpressions,
  543. findEachAttribute,
  544. findIfAttribute,
  545. isCustomNode,
  546. isSlotNode
  547. ].every(test => !test(node))
  548. }
  549. /**
  550. * Check if a node name is part of the browser or builtin javascript api or it belongs to the current scope
  551. * @param { types.NodePath } path - containing the current node visited
  552. * @returns {boolean} true if it's a global api variable
  553. */
  554. function isGlobal({ scope, node }) {
  555. return Boolean(
  556. isRaw(node) ||
  557. isBuiltinAPI(node) ||
  558. isBrowserAPI(node) ||
  559. isNewExpression(node) ||
  560. isNodeInScope(scope, node),
  561. )
  562. }
  563. /**
  564. * Checks if the identifier of a given node exists in a scope
  565. * @param {Scope} scope - scope where to search for the identifier
  566. * @param {types.Node} node - node to search for the identifier
  567. * @returns {boolean} true if the node identifier is defined in the given scope
  568. */
  569. function isNodeInScope(scope, node) {
  570. const traverse = (isInScope = false) => {
  571. types.visit(node, {
  572. visitIdentifier(path) {
  573. if (scope.lookup(getName(path.node))) {
  574. isInScope = true;
  575. }
  576. this.abort();
  577. }
  578. });
  579. return isInScope
  580. };
  581. return traverse()
  582. }
  583. /**
  584. * True if the node has the isCustom attribute set
  585. * @param {RiotParser.Node} node - riot parser node
  586. * @returns {boolean} true if either it's a riot component or a custom element
  587. */
  588. function isCustomNode(node) {
  589. return !!(node[IS_CUSTOM_NODE] || hasIsAttribute(node))
  590. }
  591. /**
  592. * True the node is <slot>
  593. * @param {RiotParser.Node} node - riot parser node
  594. * @returns {boolean} true if it's a slot node
  595. */
  596. function isSlotNode(node) {
  597. return node.name === SLOT_TAG_NODE_NAME
  598. }
  599. /**
  600. * True if the node has the isVoid attribute set
  601. * @param {RiotParser.Node} node - riot parser node
  602. * @returns {boolean} true if the node is self closing
  603. */
  604. function isVoidNode(node) {
  605. return !!node[IS_VOID_NODE]
  606. }
  607. /**
  608. * True if the riot parser did find a tag node
  609. * @param {RiotParser.Node} node - riot parser node
  610. * @returns {boolean} true only for the tag nodes
  611. */
  612. function isTagNode(node) {
  613. return node.type === nodeTypes.TAG
  614. }
  615. /**
  616. * True if the riot parser did find a text node
  617. * @param {RiotParser.Node} node - riot parser node
  618. * @returns {boolean} true only for the text nodes
  619. */
  620. function isTextNode(node) {
  621. return node.type === nodeTypes.TEXT
  622. }
  623. /**
  624. * True if the node parsed is the root one
  625. * @param {RiotParser.Node} node - riot parser node
  626. * @returns {boolean} true only for the root nodes
  627. */
  628. function isRootNode(node) {
  629. return node.isRoot
  630. }
  631. /**
  632. * True if the attribute parsed is of type spread one
  633. * @param {RiotParser.Node} node - riot parser node
  634. * @returns {boolean} true if the attribute node is of type spread
  635. */
  636. function isSpreadAttribute(node) {
  637. return node[IS_SPREAD_ATTRIBUTE]
  638. }
  639. /**
  640. * True if the node is an attribute and its name is "value"
  641. * @param {RiotParser.Node} node - riot parser node
  642. * @returns {boolean} true only for value attribute nodes
  643. */
  644. function isValueAttribute(node) {
  645. return node.name === 'value'
  646. }
  647. /**
  648. * True if the DOM node is a progress tag
  649. * @param {RiotParser.Node} node - riot parser node
  650. * @returns {boolean} true for the progress tags
  651. */
  652. function isProgressNode(node) {
  653. return node.name === PROGRESS_TAG_NODE_NAME
  654. }
  655. /**
  656. * True if the node is an attribute and a DOM handler
  657. * @param {RiotParser.Node} node - riot parser node
  658. * @returns {boolean} true only for dom listener attribute nodes
  659. */
  660. const isEventAttribute = (() => {
  661. const EVENT_ATTR_RE = /^on/;
  662. return node => EVENT_ATTR_RE.test(node.name)
  663. })();
  664. /**
  665. * True if the node has expressions or expression attributes
  666. * @param {RiotParser.Node} node - riot parser node
  667. * @returns {boolean} ditto
  668. */
  669. function hasExpressions(node) {
  670. return !!(
  671. node.expressions ||
  672. // has expression attributes
  673. (getNodeAttributes(node).some(attribute => hasExpressions(attribute))) ||
  674. // has child text nodes with expressions
  675. (node.nodes && node.nodes.some(node => isTextNode(node) && hasExpressions(node)))
  676. )
  677. }
  678. /**
  679. * True if the node is a directive having its own template
  680. * @param {RiotParser.Node} node - riot parser node
  681. * @returns {boolean} true only for the IF EACH and TAG bindings
  682. */
  683. function hasItsOwnTemplate(node) {
  684. return [
  685. findEachAttribute,
  686. findIfAttribute,
  687. isCustomNode
  688. ].some(test => test(node))
  689. }
  690. const hasIfAttribute = compose(Boolean, findIfAttribute);
  691. const hasEachAttribute = compose(Boolean, findEachAttribute);
  692. const hasIsAttribute = compose(Boolean, findIsAttribute);
  693. const hasKeyAttribute = compose(Boolean, findKeyAttribute);
  694. /**
  695. * Find the attribute node
  696. * @param { string } name - name of the attribute we want to find
  697. * @param { riotParser.nodeTypes.TAG } node - a tag node
  698. * @returns { riotParser.nodeTypes.ATTR } attribute node
  699. */
  700. function findAttribute(name, node) {
  701. return node.attributes && node.attributes.find(attr => getName(attr) === name)
  702. }
  703. function findIfAttribute(node) {
  704. return findAttribute(IF_DIRECTIVE, node)
  705. }
  706. function findEachAttribute(node) {
  707. return findAttribute(EACH_DIRECTIVE, node)
  708. }
  709. function findKeyAttribute(node) {
  710. return findAttribute(KEY_ATTRIBUTE, node)
  711. }
  712. function findIsAttribute(node) {
  713. return findAttribute(IS_DIRECTIVE, node)
  714. }
  715. /**
  716. * Find all the node attributes that are not expressions
  717. * @param {RiotParser.Node} node - riot parser node
  718. * @returns {Array} list of all the static attributes
  719. */
  720. function findStaticAttributes(node) {
  721. return getNodeAttributes(node).filter(attribute => !hasExpressions(attribute))
  722. }
  723. /**
  724. * Find all the node attributes that have expressions
  725. * @param {RiotParser.Node} node - riot parser node
  726. * @returns {Array} list of all the dynamic attributes
  727. */
  728. function findDynamicAttributes(node) {
  729. return getNodeAttributes(node).filter(hasExpressions)
  730. }
  731. /**
  732. * Unescape the user escaped chars
  733. * @param {string} string - input string
  734. * @param {string} char - probably a '{' or anything the user want's to escape
  735. * @returns {string} cleaned up string
  736. */
  737. function unescapeChar(string, char) {
  738. return string.replace(RegExp(`\\\\${char}`, 'gm'), char)
  739. }
  740. const scope = builders.identifier(SCOPE);
  741. const getName = node => node && node.name ? node.name : node;
  742. /**
  743. * Replace the path scope with a member Expression
  744. * @param { types.NodePath } path - containing the current node visited
  745. * @param { types.Node } property - node we want to prefix with the scope identifier
  746. * @returns {undefined} this is a void function
  747. */
  748. function replacePathScope(path, property) {
  749. path.replace(builders.memberExpression(
  750. scope,
  751. property,
  752. false
  753. ));
  754. }
  755. /**
  756. * Change the nodes scope adding the `scope` prefix
  757. * @param { types.NodePath } path - containing the current node visited
  758. * @returns { boolean } return false if we want to stop the tree traversal
  759. * @context { types.visit }
  760. */
  761. function updateNodeScope(path) {
  762. if (!isGlobal(path)) {
  763. replacePathScope(path, path.node);
  764. return false
  765. }
  766. this.traverse(path);
  767. }
  768. /**
  769. * Change the scope of the member expressions
  770. * @param { types.NodePath } path - containing the current node visited
  771. * @returns { boolean } return always false because we want to check only the first node object
  772. */
  773. function visitMemberExpression(path) {
  774. if (!isGlobal(path) && !isGlobal({ node: path.node.object, scope: path.scope })) {
  775. if (path.value.computed) {
  776. this.traverse(path);
  777. } else if (isBinaryExpression(path.node.object) || path.node.object.computed) {
  778. this.traverse(path.get('object'));
  779. } else if (!path.node.object.callee) {
  780. replacePathScope(path, isThisExpression(path.node.object) ? path.node.property : path.node);
  781. } else {
  782. this.traverse(path.get('object'));
  783. }
  784. }
  785. return false
  786. }
  787. /**
  788. * Objects properties should be handled a bit differently from the Identifier
  789. * @param { types.NodePath } path - containing the current node visited
  790. * @returns { boolean } return false if we want to stop the tree traversal
  791. */
  792. function visitProperty(path) {
  793. const value = path.node.value;
  794. if (isIdentifier(value)) {
  795. updateNodeScope(path.get('value'));
  796. } else {
  797. this.traverse(path.get('value'));
  798. }
  799. return false
  800. }
  801. /**
  802. * The this expressions should be replaced with the scope
  803. * @param { types.NodePath } path - containing the current node visited
  804. * @returns { boolean|undefined } return false if we want to stop the tree traversal
  805. */
  806. function visitThisExpression(path) {
  807. path.replace(scope);
  808. this.traverse(path);
  809. }
  810. /**
  811. * Update the scope of the global nodes
  812. * @param { Object } ast - ast program
  813. * @returns { Object } the ast program with all the global nodes updated
  814. */
  815. function updateNodesScope(ast) {
  816. const ignorePath = () => false;
  817. types.visit(ast, {
  818. visitIdentifier: updateNodeScope,
  819. visitMemberExpression,
  820. visitProperty,
  821. visitThisExpression,
  822. visitClassExpression: ignorePath
  823. });
  824. return ast
  825. }
  826. /**
  827. * Convert any expression to an AST tree
  828. * @param { Object } expression - expression parsed by the riot parser
  829. * @param { string } sourceFile - original tag file
  830. * @param { string } sourceCode - original tag source code
  831. * @returns { Object } the ast generated
  832. */
  833. function createASTFromExpression(expression, sourceFile, sourceCode) {
  834. const code = sourceFile ?
  835. addLineOffset(expression.text, sourceCode, expression) :
  836. expression.text;
  837. return generateAST(`(${code})`, {
  838. sourceFileName: sourceFile
  839. })
  840. }
  841. /**
  842. * Create the bindings template property
  843. * @param {Array} args - arguments to pass to the template function
  844. * @returns {ASTNode} a binding template key
  845. */
  846. function createTemplateProperty(args) {
  847. return simplePropertyNode(
  848. BINDING_TEMPLATE_KEY,
  849. args ? callTemplateFunction(...args) : nullNode()
  850. )
  851. }
  852. /**
  853. * Try to get the expression of an attribute node
  854. * @param { RiotParser.Node.Attribute } attribute - riot parser attribute node
  855. * @returns { RiotParser.Node.Expression } attribute expression value
  856. */
  857. function getAttributeExpression(attribute) {
  858. return attribute.expressions ? attribute.expressions[0] : {
  859. // if no expression was found try to typecast the attribute value
  860. ...attribute,
  861. text: attribute.value
  862. }
  863. }
  864. /**
  865. * Wrap the ast generated in a function call providing the scope argument
  866. * @param {Object} ast - function body
  867. * @returns {FunctionExpresion} function having the scope argument injected
  868. */
  869. function wrapASTInFunctionWithScope(ast) {
  870. return builders.functionExpression(
  871. null,
  872. [scope],
  873. builders.blockStatement([builders.returnStatement(
  874. ast
  875. )])
  876. )
  877. }
  878. /**
  879. * Convert any parser option to a valid template one
  880. * @param { RiotParser.Node.Expression } expression - expression parsed by the riot parser
  881. * @param { string } sourceFile - original tag file
  882. * @param { string } sourceCode - original tag source code
  883. * @returns { Object } a FunctionExpression object
  884. *
  885. * @example
  886. * toScopedFunction('foo + bar') // scope.foo + scope.bar
  887. *
  888. * @example
  889. * toScopedFunction('foo.baz + bar') // scope.foo.baz + scope.bar
  890. */
  891. function toScopedFunction(expression, sourceFile, sourceCode) {
  892. return compose(
  893. wrapASTInFunctionWithScope,
  894. transformExpression,
  895. )(expression, sourceFile, sourceCode)
  896. }
  897. /**
  898. * Transform an expression node updating its global scope
  899. * @param {RiotParser.Node.Expr} expression - riot parser expression node
  900. * @param {string} sourceFile - source file
  901. * @param {string} sourceCode - source code
  902. * @returns {ASTExpression} ast expression generated from the riot parser expression node
  903. */
  904. function transformExpression(expression, sourceFile, sourceCode) {
  905. return compose(
  906. getExpressionAST,
  907. updateNodesScope,
  908. createASTFromExpression
  909. )(expression, sourceFile, sourceCode)
  910. }
  911. /**
  912. * Get the parsed AST expression of riot expression node
  913. * @param {AST.Program} sourceAST - raw node parsed
  914. * @returns {AST.Expression} program expression output
  915. */
  916. function getExpressionAST(sourceAST) {
  917. const astBody = sourceAST.program.body;
  918. return astBody[0] ? astBody[0].expression : astBody
  919. }
  920. /**
  921. * Create the template call function
  922. * @param {Array|string|Node.Literal} template - template string
  923. * @param {Array<AST.Nodes>} bindings - template bindings provided as AST nodes
  924. * @returns {Node.CallExpression} template call expression
  925. */
  926. function callTemplateFunction(template, bindings) {
  927. return builders.callExpression(builders.identifier(TEMPLATE_FN), [
  928. template ? builders.literal(template) : nullNode(),
  929. bindings ? builders.arrayExpression(bindings) : nullNode()
  930. ])
  931. }
  932. /**
  933. * Convert any DOM attribute into a valid DOM selector useful for the querySelector API
  934. * @param { string } attributeName - name of the attribute to query
  935. * @returns { string } the attribute transformed to a query selector
  936. */
  937. const attributeNameToDOMQuerySelector = attributeName => `[${attributeName}]`;
  938. /**
  939. * Create the properties to query a DOM node
  940. * @param { string } attributeName - attribute name needed to identify a DOM node
  941. * @returns { Array<AST.Node> } array containing the selector properties needed for the binding
  942. */
  943. function createSelectorProperties(attributeName) {
  944. return attributeName ? [
  945. simplePropertyNode(BINDING_REDUNDANT_ATTRIBUTE_KEY, builders.literal(attributeName)),
  946. simplePropertyNode(BINDING_SELECTOR_KEY,
  947. compose(builders.literal, attributeNameToDOMQuerySelector)(attributeName)
  948. )
  949. ] : []
  950. }
  951. /**
  952. * Clone the node filtering out the selector attribute from the attributes list
  953. * @param {RiotParser.Node} node - riot parser node
  954. * @param {string} selectorAttribute - name of the selector attribute to filter out
  955. * @returns {RiotParser.Node} the node with the attribute cleaned up
  956. */
  957. function cloneNodeWithoutSelectorAttribute(node, selectorAttribute) {
  958. return {
  959. ...node,
  960. attributes: getAttributesWithoutSelector(getNodeAttributes(node), selectorAttribute)
  961. }
  962. }
  963. /**
  964. * Get the node attributes without the selector one
  965. * @param {Array<RiotParser.Attr>} attributes - attributes list
  966. * @param {string} selectorAttribute - name of the selector attribute to filter out
  967. * @returns {Array<RiotParser.Attr>} filtered attributes
  968. */
  969. function getAttributesWithoutSelector(attributes, selectorAttribute) {
  970. if (selectorAttribute)
  971. return attributes.filter(attribute => attribute.name !== selectorAttribute)
  972. return attributes
  973. }
  974. /**
  975. * Clean binding or custom attributes
  976. * @param {RiotParser.Node} node - riot parser node
  977. * @returns {Array<RiotParser.Node.Attr>} only the attributes that are not bindings or directives
  978. */
  979. function cleanAttributes(node) {
  980. return getNodeAttributes(node).filter(attribute => ![
  981. IF_DIRECTIVE,
  982. EACH_DIRECTIVE,
  983. KEY_ATTRIBUTE,
  984. SLOT_ATTRIBUTE,
  985. IS_DIRECTIVE
  986. ].includes(attribute.name))
  987. }
  988. /**
  989. * Create a root node proxing only its nodes and attributes
  990. * @param {RiotParser.Node} node - riot parser node
  991. * @returns {RiotParser.Node} root node
  992. */
  993. function createRootNode(node) {
  994. return {
  995. nodes: getChildrenNodes(node),
  996. isRoot: true,
  997. // root nodes shuold't have directives
  998. attributes: cleanAttributes(node)
  999. }
  1000. }
  1001. /**
  1002. * Get all the child nodes of a RiotParser.Node
  1003. * @param {RiotParser.Node} node - riot parser node
  1004. * @returns {Array<RiotParser.Node>} all the child nodes found
  1005. */
  1006. function getChildrenNodes(node) {
  1007. return node && node.nodes ? node.nodes : []
  1008. }
  1009. /**
  1010. * Get all the attributes of a riot parser node
  1011. * @param {RiotParser.Node} node - riot parser node
  1012. * @returns {Array<RiotParser.Node.Attribute>} all the attributes find
  1013. */
  1014. function getNodeAttributes(node) {
  1015. return node.attributes ? node.attributes : []
  1016. }
  1017. /**
  1018. * Get the name of a custom node transforming it into an expression node
  1019. * @param {RiotParser.Node} node - riot parser node
  1020. * @returns {RiotParser.Node.Attr} the node name as expression attribute
  1021. */
  1022. function getCustomNodeNameAsExpression(node) {
  1023. const isAttribute = findIsAttribute(node);
  1024. const toRawString = val => `'${val}'`;
  1025. if (isAttribute) {
  1026. return isAttribute.expressions ? isAttribute.expressions[0] : {
  1027. ...isAttribute,
  1028. text: toRawString(isAttribute.value)
  1029. }
  1030. }
  1031. return { ...node, text: toRawString(getName(node)) }
  1032. }
  1033. /**
  1034. * Convert all the node static attributes to strings
  1035. * @param {RiotParser.Node} node - riot parser node
  1036. * @returns {string} all the node static concatenated as string
  1037. */
  1038. function staticAttributesToString(node) {
  1039. return findStaticAttributes(node)
  1040. .map(attribute => attribute[IS_BOOLEAN_ATTRIBUTE] || !attribute.value ?
  1041. attribute.name :
  1042. `${attribute.name}="${unescapeNode(attribute, 'value').value}"`
  1043. ).join(' ')
  1044. }
  1045. /**
  1046. * Make sure that node escaped chars will be unescaped
  1047. * @param {RiotParser.Node} node - riot parser node
  1048. * @param {string} key - key property to unescape
  1049. * @returns {RiotParser.Node} node with the text property unescaped
  1050. */
  1051. function unescapeNode(node, key) {
  1052. if (node.unescape) {
  1053. return {
  1054. ...node,
  1055. [key]: unescapeChar(node[key], node.unescape)
  1056. }
  1057. }
  1058. return node
  1059. }
  1060. /**
  1061. * Convert a riot parser opening node into a string
  1062. * @param {RiotParser.Node} node - riot parser node
  1063. * @returns {string} the node as string
  1064. */
  1065. function nodeToString(node) {
  1066. const attributes = staticAttributesToString(node);
  1067. switch(true) {
  1068. case isTagNode(node):
  1069. return `<${node.name}${attributes ? ` ${attributes}` : ''}${isVoidNode(node) ? '/' : ''}>`
  1070. case isTextNode(node):
  1071. return hasExpressions(node) ? TEXT_NODE_EXPRESSION_PLACEHOLDER : unescapeNode(node, 'text').text
  1072. default:
  1073. return ''
  1074. }
  1075. }
  1076. /**
  1077. * Close an html node
  1078. * @param {RiotParser.Node} node - riot parser node
  1079. * @returns {string} the closing tag of the html tag node passed to this function
  1080. */
  1081. function closeTag(node) {
  1082. return node.name ? `</${node.name}>` : ''
  1083. }
  1084. /**
  1085. * Create a strings array with the `join` call to transform it into a string
  1086. * @param {Array} stringsArray - array containing all the strings to concatenate
  1087. * @returns {AST.CallExpression} array with a `join` call
  1088. */
  1089. function createArrayString(stringsArray) {
  1090. return builders.callExpression(
  1091. builders.memberExpression(
  1092. builders.arrayExpression(stringsArray),
  1093. builders.identifier('join'),
  1094. false
  1095. ),
  1096. [builders.literal('')],
  1097. )
  1098. }
  1099. /**
  1100. * Simple expression bindings might contain multiple expressions like for example: "class="{foo} red {bar}""
  1101. * This helper aims to merge them in a template literal if it's necessary
  1102. * @param {RiotParser.Attr} node - riot parser node
  1103. * @param {string} sourceFile - original tag file
  1104. * @param {string} sourceCode - original tag source code
  1105. * @returns { Object } a template literal expression object
  1106. */
  1107. function mergeAttributeExpressions(node, sourceFile, sourceCode) {
  1108. if (!node.parts || node.parts.length === 1) {
  1109. return transformExpression(node.expressions[0], sourceFile, sourceCode)
  1110. }
  1111. const stringsArray = [
  1112. ...node.parts.reduce((acc, str) => {
  1113. const expression = node.expressions.find(e => e.text.trim() === str);
  1114. return [
  1115. ...acc,
  1116. expression ? transformExpression(expression, sourceFile, sourceCode) : builders.literal(str)
  1117. ]
  1118. }, [])
  1119. ].filter(expr => !isLiteral(expr) || expr.value);
  1120. return createArrayString(stringsArray)
  1121. }
  1122. /**
  1123. * Create a selector that will be used to find the node via dom-bindings
  1124. * @param {number} id - temporary variable that will be increased anytime this function will be called
  1125. * @returns {string} selector attribute needed to bind a riot expression
  1126. */
  1127. const createBindingSelector = (function createSelector(id = 0) {
  1128. return () => `${BINDING_SELECTOR_PREFIX}${id++}`
  1129. }());
  1130. /**
  1131. * Create an attribute evaluation function
  1132. * @param {RiotParser.Attr} sourceNode - riot parser node
  1133. * @param {string} sourceFile - original tag file
  1134. * @param {string} sourceCode - original tag source code
  1135. * @returns { AST.Node } an AST function expression to evaluate the attribute value
  1136. */
  1137. function createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) {
  1138. return hasExpressions(sourceNode) ?
  1139. // dynamic attribute
  1140. wrapASTInFunctionWithScope(mergeAttributeExpressions(sourceNode, sourceFile, sourceCode)) :
  1141. // static attribute
  1142. builders.functionExpression(
  1143. null,
  1144. [],
  1145. builders.blockStatement([
  1146. builders.returnStatement(builders.literal(sourceNode.value || true))
  1147. ]),
  1148. )
  1149. }
  1150. /**
  1151. * Simple clone deep function, do not use it for classes or recursive objects!
  1152. * @param {*} source - possibily an object to clone
  1153. * @returns {*} the object we wanted to clone
  1154. */
  1155. function cloneDeep(source) {
  1156. return JSON.parse(JSON.stringify(source))
  1157. }
  1158. const getEachItemName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[0] : expression.left;
  1159. const getEachIndexName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[1] : null;
  1160. const getEachValue = expression => expression.right;
  1161. const nameToliteral = compose(builders.literal, getName);
  1162. const generateEachItemNameKey = expression => simplePropertyNode(
  1163. BINDING_ITEM_NAME_KEY,
  1164. compose(nameToliteral, getEachItemName)(expression)
  1165. );
  1166. const generateEachIndexNameKey = expression => simplePropertyNode(
  1167. BINDING_INDEX_NAME_KEY,
  1168. compose(nameToliteral, getEachIndexName)(expression)
  1169. );
  1170. const generateEachEvaluateKey = (expression, eachExpression, sourceFile, sourceCode) => simplePropertyNode(
  1171. BINDING_EVALUATE_KEY,
  1172. compose(
  1173. e => toScopedFunction(e, sourceFile, sourceCode),
  1174. e => ({
  1175. ...eachExpression,
  1176. text: generateJavascript(e).code
  1177. }),
  1178. getEachValue
  1179. )(expression)
  1180. );
  1181. /**
  1182. * Get the each expression properties to create properly the template binding
  1183. * @param { DomBinding.Expression } eachExpression - original each expression data
  1184. * @param { string } sourceFile - original tag file
  1185. * @param { string } sourceCode - original tag source code
  1186. * @returns { Array } AST nodes that are needed to build an each binding
  1187. */
  1188. function generateEachExpressionProperties(eachExpression, sourceFile, sourceCode) {
  1189. const ast = createASTFromExpression(eachExpression, sourceFile, sourceCode);
  1190. const body = ast.program.body;
  1191. const firstNode = body[0];
  1192. if (!isExpressionStatement(firstNode)) {
  1193. panic(`The each directives supported should be of type "ExpressionStatement",you have provided a "${firstNode.type}"`);
  1194. }
  1195. const { expression } = firstNode;
  1196. return [
  1197. generateEachItemNameKey(expression),
  1198. generateEachIndexNameKey(expression),
  1199. generateEachEvaluateKey(expression, eachExpression, sourceFile, sourceCode)
  1200. ]
  1201. }
  1202. /**
  1203. * Transform a RiotParser.Node.Tag into an each binding
  1204. * @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute
  1205. * @param { string } selectorAttribute - attribute needed to select the target node
  1206. * @param { string } sourceFile - source file path
  1207. * @param { string } sourceCode - original source
  1208. * @returns { AST.Node } an each binding node
  1209. */
  1210. function createEachBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) {
  1211. const [ifAttribute, eachAttribute, keyAttribute] = [
  1212. findIfAttribute,
  1213. findEachAttribute,
  1214. findKeyAttribute
  1215. ].map(f => f(sourceNode));
  1216. const attributeOrNull = attribute => attribute ? toScopedFunction(getAttributeExpression(attribute), sourceFile, sourceCode) : nullNode();
  1217. return builders.objectExpression([
  1218. simplePropertyNode(BINDING_TYPE_KEY,
  1219. builders.memberExpression(
  1220. builders.identifier(BINDING_TYPES),
  1221. builders.identifier(EACH_BINDING_TYPE),
  1222. false
  1223. ),
  1224. ),
  1225. simplePropertyNode(BINDING_GET_KEY_KEY, attributeOrNull(keyAttribute)),
  1226. simplePropertyNode(BINDING_CONDITION_KEY, attributeOrNull(ifAttribute)),
  1227. createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)),
  1228. ...createSelectorProperties(selectorAttribute),
  1229. ...compose(generateEachExpressionProperties, getAttributeExpression)(eachAttribute)
  1230. ])
  1231. }
  1232. /**
  1233. * Transform a RiotParser.Node.Tag into an if binding
  1234. * @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute
  1235. * @param { string } selectorAttribute - attribute needed to select the target node
  1236. * @param { stiring } sourceFile - source file path
  1237. * @param { string } sourceCode - original source
  1238. * @returns { AST.Node } an if binding node
  1239. */
  1240. function createIfBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) {
  1241. const ifAttribute = findIfAttribute(sourceNode);
  1242. return builders.objectExpression([
  1243. simplePropertyNode(BINDING_TYPE_KEY,
  1244. builders.memberExpression(
  1245. builders.identifier(BINDING_TYPES),
  1246. builders.identifier(IF_BINDING_TYPE),
  1247. false
  1248. ),
  1249. ),
  1250. simplePropertyNode(
  1251. BINDING_EVALUATE_KEY,
  1252. toScopedFunction(ifAttribute.expressions[0], sourceFile, sourceCode)
  1253. ),
  1254. ...createSelectorProperties(selectorAttribute),
  1255. createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute))
  1256. ])
  1257. }
  1258. /**
  1259. * Create a simple attribute expression
  1260. * @param {RiotParser.Node.Attr} sourceNode - the custom tag
  1261. * @param {string} sourceFile - source file path
  1262. * @param {string} sourceCode - original source
  1263. * @returns {AST.Node} object containing the expression binding keys
  1264. */
  1265. function createAttributeExpression(sourceNode, sourceFile, sourceCode) {
  1266. return builders.objectExpression([
  1267. simplePropertyNode(BINDING_TYPE_KEY,
  1268. builders.memberExpression(
  1269. builders.identifier(EXPRESSION_TYPES),
  1270. builders.identifier(ATTRIBUTE_EXPRESSION_TYPE),
  1271. false
  1272. ),
  1273. ),
  1274. simplePropertyNode(BINDING_NAME_KEY, isSpreadAttribute(sourceNode) ? nullNode() : builders.literal(sourceNode.name)),
  1275. simplePropertyNode(
  1276. BINDING_EVALUATE_KEY,
  1277. createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode)
  1278. )
  1279. ])
  1280. }
  1281. /**
  1282. * Create a simple event expression
  1283. * @param {RiotParser.Node.Attr} sourceNode - attribute containing the event handlers
  1284. * @param {string} sourceFile - source file path
  1285. * @param {string} sourceCode - original source
  1286. * @returns {AST.Node} object containing the expression binding keys
  1287. */
  1288. function createEventExpression(sourceNode, sourceFile, sourceCode) {
  1289. return builders.objectExpression([
  1290. simplePropertyNode(BINDING_TYPE_KEY,
  1291. builders.memberExpression(
  1292. builders.identifier(EXPRESSION_TYPES),
  1293. builders.identifier(EVENT_EXPRESSION_TYPE),
  1294. false
  1295. ),
  1296. ),
  1297. simplePropertyNode(BINDING_NAME_KEY, builders.literal(sourceNode.name)),
  1298. simplePropertyNode(
  1299. BINDING_EVALUATE_KEY,
  1300. createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode)
  1301. )
  1302. ])
  1303. }
  1304. /**
  1305. * Generate the pure immutable string chunks from a RiotParser.Node.Text
  1306. * @param {RiotParser.Node.Text} node - riot parser text node
  1307. * @param {string} sourceCode sourceCode - source code
  1308. * @returns {Array} array containing the immutable string chunks
  1309. */
  1310. function generateLiteralStringChunksFromNode(node, sourceCode) {
  1311. return node.expressions.reduce((chunks, expression, index) => {
  1312. const start = index ? node.expressions[index - 1].end : node.start;
  1313. chunks.push(sourceCode.substring(start, expression.start));
  1314. // add the tail to the string
  1315. if (index === node.expressions.length - 1)
  1316. chunks.push(sourceCode.substring(expression.end, node.end));
  1317. return chunks
  1318. }, []).map(str => node.unescape ? unescapeChar(str, node.unescape) : str)
  1319. }
  1320. /**
  1321. * Simple bindings might contain multiple expressions like for example: "{foo} and {bar}"
  1322. * This helper aims to merge them in a template literal if it's necessary
  1323. * @param {RiotParser.Node} node - riot parser node
  1324. * @param {string} sourceFile - original tag file
  1325. * @param {string} sourceCode - original tag source code
  1326. * @returns { Object } a template literal expression object
  1327. */
  1328. function mergeNodeExpressions(node, sourceFile, sourceCode) {
  1329. if (node.parts.length === 1)
  1330. return transformExpression(node.expressions[0], sourceFile, sourceCode)
  1331. const pureStringChunks = generateLiteralStringChunksFromNode(node, sourceCode);
  1332. const stringsArray = pureStringChunks.reduce((acc, str, index) => {
  1333. const expr = node.expressions[index];
  1334. return [
  1335. ...acc,
  1336. builders.literal(str),
  1337. expr ? transformExpression(expr, sourceFile, sourceCode) : nullNode()
  1338. ]
  1339. }, [])
  1340. // filter the empty literal expressions
  1341. .filter(expr => !isLiteral(expr) || expr.value);
  1342. return createArrayString(stringsArray)
  1343. }
  1344. /**
  1345. * Create a text expression
  1346. * @param {RiotParser.Node.Text} sourceNode - text node to parse
  1347. * @param {string} sourceFile - source file path
  1348. * @param {string} sourceCode - original source
  1349. * @param {number} childNodeIndex - position of the child text node in its parent children nodes
  1350. * @returns {AST.Node} object containing the expression binding keys
  1351. */
  1352. function createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) {
  1353. return builders.objectExpression([
  1354. simplePropertyNode(BINDING_TYPE_KEY,
  1355. builders.memberExpression(
  1356. builders.identifier(EXPRESSION_TYPES),
  1357. builders.identifier(TEXT_EXPRESSION_TYPE),
  1358. false
  1359. ),
  1360. ),
  1361. simplePropertyNode(
  1362. BINDING_CHILD_NODE_INDEX_KEY,
  1363. builders.literal(childNodeIndex)
  1364. ),
  1365. simplePropertyNode(
  1366. BINDING_EVALUATE_KEY,
  1367. wrapASTInFunctionWithScope(
  1368. mergeNodeExpressions(sourceNode, sourceFile, sourceCode)
  1369. )
  1370. )
  1371. ])
  1372. }
  1373. function createValueExpression(sourceNode, sourceFile, sourceCode) {
  1374. return builders.objectExpression([
  1375. simplePropertyNode(BINDING_TYPE_KEY,
  1376. builders.memberExpression(
  1377. builders.identifier(EXPRESSION_TYPES),
  1378. builders.identifier(VALUE_EXPRESSION_TYPE),
  1379. false
  1380. ),
  1381. ),
  1382. simplePropertyNode(
  1383. BINDING_EVALUATE_KEY,
  1384. createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode)
  1385. )
  1386. ])
  1387. }
  1388. function createExpression(sourceNode, sourceFile, sourceCode, childNodeIndex, parentNode) {
  1389. switch (true) {
  1390. case isTextNode(sourceNode):
  1391. return createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex)
  1392. // progress nodes value attributes will be rendered as attributes
  1393. // see https://github.com/riot/compiler/issues/122
  1394. case isValueAttribute(sourceNode) && hasValueAttribute(parentNode.name) && !isProgressNode(parentNode):
  1395. return createValueExpression(sourceNode, sourceFile, sourceCode)
  1396. case isEventAttribute(sourceNode):
  1397. return createEventExpression(sourceNode, sourceFile, sourceCode)
  1398. default:
  1399. return createAttributeExpression(sourceNode, sourceFile, sourceCode)
  1400. }
  1401. }
  1402. /**
  1403. * Create the attribute expressions
  1404. * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
  1405. * @param {string} sourceFile - source file path
  1406. * @param {string} sourceCode - original source
  1407. * @returns {Array} array containing all the attribute expressions
  1408. */
  1409. function createAttributeExpressions(sourceNode, sourceFile, sourceCode) {
  1410. return findDynamicAttributes(sourceNode)
  1411. .map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode))
  1412. }
  1413. /**
  1414. * Create the text node expressions
  1415. * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
  1416. * @param {string} sourceFile - source file path
  1417. * @param {string} sourceCode - original source
  1418. * @returns {Array} array containing all the text node expressions
  1419. */
  1420. function createTextNodeExpressions(sourceNode, sourceFile, sourceCode) {
  1421. const childrenNodes = getChildrenNodes(sourceNode);
  1422. return childrenNodes
  1423. .filter(isTextNode)
  1424. .filter(hasExpressions)
  1425. .map(node => createExpression(
  1426. node,
  1427. sourceFile,
  1428. sourceCode,
  1429. childrenNodes.indexOf(node),
  1430. sourceNode
  1431. ))
  1432. }
  1433. /**
  1434. * Add a simple binding to a riot parser node
  1435. * @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute
  1436. * @param { string } selectorAttribute - attribute needed to select the target node
  1437. * @param { string } sourceFile - source file path
  1438. * @param { string } sourceCode - original source
  1439. * @returns { AST.Node } an each binding node
  1440. */
  1441. function createSimpleBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) {
  1442. return builders.objectExpression([
  1443. ...createSelectorProperties(selectorAttribute),
  1444. simplePropertyNode(
  1445. BINDING_EXPRESSIONS_KEY,
  1446. builders.arrayExpression([
  1447. ...createTextNodeExpressions(sourceNode, sourceFile, sourceCode),
  1448. ...createAttributeExpressions(sourceNode, sourceFile, sourceCode)
  1449. ])
  1450. )
  1451. ])
  1452. }
  1453. /**
  1454. * Transform a RiotParser.Node.Tag of type slot into a slot binding
  1455. * @param { RiotParser.Node.Tag } sourceNode - slot node
  1456. * @param { string } selectorAttribute - attribute needed to select the target node
  1457. * @returns { AST.Node } a slot binding node
  1458. */
  1459. function createSlotBinding(sourceNode, selectorAttribute) {
  1460. const slotNameAttribute = findAttribute(NAME_ATTRIBUTE, sourceNode);
  1461. const slotName = slotNameAttribute ? slotNameAttribute.value : DEFAULT_SLOT_NAME;
  1462. return builders.objectExpression([
  1463. simplePropertyNode(BINDING_TYPE_KEY,
  1464. builders.memberExpression(
  1465. builders.identifier(BINDING_TYPES),
  1466. builders.identifier(SLOT_BINDING_TYPE),
  1467. false
  1468. ),
  1469. ),
  1470. simplePropertyNode(
  1471. BINDING_NAME_KEY,
  1472. builders.literal(slotName)
  1473. ),
  1474. ...createSelectorProperties(selectorAttribute)
  1475. ])
  1476. }
  1477. /**
  1478. * Find the slots in the current component and group them under the same id
  1479. * @param {RiotParser.Node.Tag} sourceNode - the custom tag
  1480. * @returns {Object} object containing all the slots grouped by name
  1481. */
  1482. function groupSlots(sourceNode) {
  1483. return getChildrenNodes(sourceNode).reduce((acc, node) => {
  1484. const slotAttribute = findSlotAttribute(node);
  1485. if (slotAttribute) {
  1486. acc[slotAttribute.value] = node;
  1487. } else {
  1488. acc.default = createRootNode({
  1489. nodes: [...getChildrenNodes(acc.default), node]
  1490. });
  1491. }
  1492. return acc
  1493. }, {
  1494. default: null
  1495. })
  1496. }
  1497. /**
  1498. * Create the slot entity to pass to the riot-dom bindings
  1499. * @param {string} id - slot id
  1500. * @param {RiotParser.Node.Tag} sourceNode - slot root node
  1501. * @param {string} sourceFile - source file path
  1502. * @param {string} sourceCode - original source
  1503. * @returns {AST.Node} ast node containing the slot object properties
  1504. */
  1505. function buildSlot(id, sourceNode, sourceFile, sourceCode) {
  1506. const cloneNode = {
  1507. ...sourceNode,
  1508. // avoid to render the slot attribute
  1509. attributes: getNodeAttributes(sourceNode).filter(attribute => attribute.name !== SLOT_ATTRIBUTE)
  1510. };
  1511. const [html, bindings] = build(cloneNode, sourceFile, sourceCode);
  1512. return builders.objectExpression([
  1513. simplePropertyNode(BINDING_ID_KEY, builders.literal(id)),
  1514. simplePropertyNode(BINDING_HTML_KEY, builders.literal(html)),
  1515. simplePropertyNode(BINDING_BINDINGS_KEY, builders.arrayExpression(bindings))
  1516. ])
  1517. }
  1518. /**
  1519. * Create the AST array containing the slots
  1520. * @param { RiotParser.Node.Tag } sourceNode - the custom tag
  1521. * @param { string } sourceFile - source file path
  1522. * @param { string } sourceCode - original source
  1523. * @returns {AST.ArrayExpression} array containing the attributes to bind
  1524. */
  1525. function createSlotsArray(sourceNode, sourceFile, sourceCode) {
  1526. return builders.arrayExpression([
  1527. ...compose(
  1528. slots => slots.map(([key, value]) => buildSlot(key, value, sourceFile, sourceCode)),
  1529. slots => slots.filter(([,value]) => value),
  1530. Object.entries,
  1531. groupSlots
  1532. )(sourceNode)
  1533. ])
  1534. }
  1535. /**
  1536. * Create the AST array containing the attributes to bind to this node
  1537. * @param { RiotParser.Node.Tag } sourceNode - the custom tag
  1538. * @param { string } selectorAttribute - attribute needed to select the target node
  1539. * @param { string } sourceFile - source file path
  1540. * @param { string } sourceCode - original source
  1541. * @returns {AST.ArrayExpression} array containing the slot objects
  1542. */
  1543. function createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) {
  1544. return builders.arrayExpression([
  1545. ...compose(
  1546. attributes => attributes.map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)),
  1547. attributes => getAttributesWithoutSelector(attributes, selectorAttribute), // eslint-disable-line
  1548. cleanAttributes
  1549. )(sourceNode)
  1550. ])
  1551. }
  1552. /**
  1553. * Find the slot attribute if it exists
  1554. * @param {RiotParser.Node.Tag} sourceNode - the custom tag
  1555. * @returns {RiotParser.Node.Attr|undefined} the slot attribute found
  1556. */
  1557. function findSlotAttribute(sourceNode) {
  1558. return getNodeAttributes(sourceNode).find(attribute => attribute.name === SLOT_ATTRIBUTE)
  1559. }
  1560. /**
  1561. * Transform a RiotParser.Node.Tag into a tag binding
  1562. * @param { RiotParser.Node.Tag } sourceNode - the custom tag
  1563. * @param { string } selectorAttribute - attribute needed to select the target node
  1564. * @param { string } sourceFile - source file path
  1565. * @param { string } sourceCode - original source
  1566. * @returns { AST.Node } tag binding node
  1567. */
  1568. function createTagBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) {
  1569. return builders.objectExpression([
  1570. simplePropertyNode(BINDING_TYPE_KEY,
  1571. builders.memberExpression(
  1572. builders.identifier(BINDING_TYPES),
  1573. builders.identifier(TAG_BINDING_TYPE),
  1574. false
  1575. ),
  1576. ),
  1577. simplePropertyNode(BINDING_GET_COMPONENT_KEY, builders.identifier(GET_COMPONENT_FN)),
  1578. simplePropertyNode(
  1579. BINDING_EVALUATE_KEY,
  1580. toScopedFunction(getCustomNodeNameAsExpression(sourceNode), sourceFile, sourceCode)
  1581. ),
  1582. simplePropertyNode(BINDING_SLOTS_KEY, createSlotsArray(sourceNode, sourceFile, sourceCode)),
  1583. simplePropertyNode(
  1584. BINDING_ATTRIBUTES_KEY,
  1585. createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode)
  1586. ),
  1587. ...createSelectorProperties(selectorAttribute)
  1588. ])
  1589. }
  1590. const BuildingState = Object.freeze({
  1591. html: [],
  1592. bindings: [],
  1593. parent: null
  1594. });
  1595. /**
  1596. * Nodes having bindings should be cloned and new selector properties should be added to them
  1597. * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
  1598. * @param {string} bindingsSelector - temporary string to identify the current node
  1599. * @returns {RiotParser.Node} the original node parsed having the new binding selector attribute
  1600. */
  1601. function createBindingsTag(sourceNode, bindingsSelector) {
  1602. if (!bindingsSelector) return sourceNode
  1603. return {
  1604. ...sourceNode,
  1605. // inject the selector bindings into the node attributes
  1606. attributes: [{
  1607. name: bindingsSelector,
  1608. value: bindingsSelector
  1609. }, ...getNodeAttributes(sourceNode)]
  1610. }
  1611. }
  1612. /**
  1613. * Create a generic dynamic node (text or tag) and generate its bindings
  1614. * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
  1615. * @param {string} sourceFile - source file path
  1616. * @param {string} sourceCode - original source
  1617. * @param {BuildingState} state - state representing the current building tree state during the recursion
  1618. * @returns {Array} array containing the html output and bindings for the current node
  1619. */
  1620. function createDynamicNode(sourceNode, sourceFile, sourceCode, state) {
  1621. switch (true) {
  1622. case isTextNode(sourceNode):
  1623. // text nodes will not have any bindings
  1624. return [nodeToString(sourceNode), []]
  1625. default:
  1626. return createTagWithBindings(sourceNode, sourceFile, sourceCode)
  1627. }
  1628. }
  1629. /**
  1630. * Create only a dynamic tag node with generating a custom selector and its bindings
  1631. * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
  1632. * @param {string} sourceFile - source file path
  1633. * @param {string} sourceCode - original source
  1634. * @param {BuildingState} state - state representing the current building tree state during the recursion
  1635. * @returns {Array} array containing the html output and bindings for the current node
  1636. */
  1637. function createTagWithBindings(sourceNode, sourceFile, sourceCode) {
  1638. const bindingsSelector = isRootNode(sourceNode) ? null : createBindingSelector();
  1639. const cloneNode = createBindingsTag(sourceNode, bindingsSelector);
  1640. const tagOpeningHTML = nodeToString(cloneNode);
  1641. switch(true) {
  1642. // EACH bindings have prio 1
  1643. case hasEachAttribute(cloneNode):
  1644. return [tagOpeningHTML, [createEachBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]]
  1645. // IF bindings have prio 2
  1646. case hasIfAttribute(cloneNode):
  1647. return [tagOpeningHTML, [createIfBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]]
  1648. // TAG bindings have prio 3
  1649. case isCustomNode(cloneNode):
  1650. return [tagOpeningHTML, [createTagBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]]
  1651. // slot tag
  1652. case isSlotNode(cloneNode):
  1653. return [tagOpeningHTML, [createSlotBinding(cloneNode, bindingsSelector)]]
  1654. // this node has expressions bound to it
  1655. default:
  1656. return [tagOpeningHTML, [createSimpleBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]]
  1657. }
  1658. }
  1659. /**
  1660. * Parse a node trying to extract its template and bindings
  1661. * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
  1662. * @param {string} sourceFile - source file path
  1663. * @param {string} sourceCode - original source
  1664. * @param {BuildingState} state - state representing the current building tree state during the recursion
  1665. * @returns {Array} array containing the html output and bindings for the current node
  1666. */
  1667. function parseNode(sourceNode, sourceFile, sourceCode, state) {
  1668. // static nodes have no bindings
  1669. if (isStaticNode(sourceNode)) return [nodeToString(sourceNode), []]
  1670. return createDynamicNode(sourceNode, sourceFile, sourceCode)
  1671. }
  1672. /**
  1673. * Create the tag binding
  1674. * @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute
  1675. * @param { string } sourceFile - source file path
  1676. * @param { string } sourceCode - original source
  1677. * @param { string } selector - binding selector
  1678. * @returns { Array } array with only the tag binding AST
  1679. */
  1680. function createNestedBindings(sourceNode, sourceFile, sourceCode, selector) {
  1681. const mightBeARiotComponent = isCustomNode(sourceNode);
  1682. return mightBeARiotComponent ? [null, [
  1683. createTagBinding(
  1684. cloneNodeWithoutSelectorAttribute(sourceNode, selector),
  1685. null,
  1686. sourceFile,
  1687. sourceCode
  1688. )]
  1689. ] : build(createRootNode(sourceNode), sourceFile, sourceCode)
  1690. }
  1691. /**
  1692. * Build the template and the bindings
  1693. * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser
  1694. * @param {string} sourceFile - source file path
  1695. * @param {string} sourceCode - original source
  1696. * @param {BuildingState} state - state representing the current building tree state during the recursion
  1697. * @returns {Array} array containing the html output and the dom bindings
  1698. */
  1699. function build(
  1700. sourceNode,
  1701. sourceFile,
  1702. sourceCode,
  1703. state
  1704. ) {
  1705. if (!sourceNode) panic('Something went wrong with your tag DOM parsing, your tag template can\'t be created');
  1706. const [nodeHTML, nodeBindings] = parseNode(sourceNode, sourceFile, sourceCode);
  1707. const childrenNodes = getChildrenNodes(sourceNode);
  1708. const currentState = { ...cloneDeep(BuildingState), ...state };
  1709. // mutate the original arrays
  1710. currentState.html.push(...nodeHTML);
  1711. currentState.bindings.push(...nodeBindings);
  1712. // do recursion if
  1713. // this tag has children and it has no special directives bound to it
  1714. if (childrenNodes.length && !hasItsOwnTemplate(sourceNode)) {
  1715. childrenNodes.forEach(node => build(node, sourceFile, sourceCode, { parent: sourceNode, ...currentState }));
  1716. }
  1717. // close the tag if it's not a void one
  1718. if (isTagNode(sourceNode) && !isVoidNode(sourceNode)) {
  1719. currentState.html.push(closeTag(sourceNode));
  1720. }
  1721. return [
  1722. currentState.html.join(''),
  1723. currentState.bindings
  1724. ]
  1725. }
  1726. const templateFunctionArguments = [
  1727. TEMPLATE_FN,
  1728. EXPRESSION_TYPES,
  1729. BINDING_TYPES,
  1730. GET_COMPONENT_FN
  1731. ].map(builders.identifier);
  1732. /**
  1733. * Create the content of the template function
  1734. * @param { RiotParser.Node } sourceNode - node generated by the riot compiler
  1735. * @param { string } sourceFile - source file path
  1736. * @param { string } sourceCode - original source
  1737. * @returns {AST.BlockStatement} the content of the template function
  1738. */
  1739. function createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) {
  1740. return builders.blockStatement([
  1741. builders.returnStatement(
  1742. callTemplateFunction(
  1743. ...build(
  1744. createRootNode(sourceNode),
  1745. sourceFile,
  1746. sourceCode
  1747. )
  1748. )
  1749. )
  1750. ])
  1751. }
  1752. /**
  1753. * Extend the AST adding the new template property containing our template call to render the component
  1754. * @param { Object } ast - current output ast
  1755. * @param { string } sourceFile - source file path
  1756. * @param { string } sourceCode - original source
  1757. * @param { RiotParser.Node } sourceNode - node generated by the riot compiler
  1758. * @returns { Object } the output ast having the "template" key
  1759. */
  1760. function extendTemplateProperty(ast, sourceFile, sourceCode, sourceNode) {
  1761. types.visit(ast, {
  1762. visitProperty(path) {
  1763. if (path.value.key.value === TAG_TEMPLATE_PROPERTY) {
  1764. path.value.value = builders.functionExpression(
  1765. null,
  1766. templateFunctionArguments,
  1767. createTemplateFunctionContent(sourceNode, sourceFile, sourceCode)
  1768. );
  1769. return false
  1770. }
  1771. this.traverse(path);
  1772. }
  1773. });
  1774. return ast
  1775. }
  1776. /**
  1777. * Generate the component template logic
  1778. * @param { RiotParser.Node } sourceNode - node generated by the riot compiler
  1779. * @param { string } source - original component source code
  1780. * @param { Object } meta - compilation meta information
  1781. * @param { AST } ast - current AST output
  1782. * @returns { AST } the AST generated
  1783. */
  1784. function template(sourceNode, source, meta, ast) {
  1785. const { options } = meta;
  1786. return extendTemplateProperty(ast, options.file, source, sourceNode)
  1787. }
  1788. const DEFAULT_OPTIONS = {
  1789. template: 'default',
  1790. file: '[unknown-source-file]',
  1791. scopedCss: true
  1792. };
  1793. /**
  1794. * Create the initial AST
  1795. * @param {string} tagName - the name of the component we have compiled
  1796. * @returns { AST } the initial AST
  1797. *
  1798. * @example
  1799. * // the output represents the following string in AST
  1800. */
  1801. function createInitialInput({tagName}) {
  1802. /*
  1803. generates
  1804. export default {
  1805. ${TAG_CSS_PROPERTY}: null,
  1806. ${TAG_LOGIC_PROPERTY}: null,
  1807. ${TAG_TEMPLATE_PROPERTY}: null
  1808. }
  1809. */
  1810. return builders.program([
  1811. builders.exportDefaultDeclaration(
  1812. builders.objectExpression([
  1813. simplePropertyNode(TAG_CSS_PROPERTY, nullNode()),
  1814. simplePropertyNode(TAG_LOGIC_PROPERTY, nullNode()),
  1815. simplePropertyNode(TAG_TEMPLATE_PROPERTY, nullNode()),
  1816. simplePropertyNode(TAG_NAME_PROPERTY, builders.literal(tagName))
  1817. ])
  1818. )]
  1819. )
  1820. }
  1821. /**
  1822. * Make sure the input sourcemap is valid otherwise we ignore it
  1823. * @param {SourceMapGenerator} map - preprocessor source map
  1824. * @returns {Object} sourcemap as json or nothing
  1825. */
  1826. function normaliseInputSourceMap(map) {
  1827. const inputSourceMap = sourcemapAsJSON(map);
  1828. return isEmptySourcemap(inputSourceMap) ? null : inputSourceMap
  1829. }
  1830. /**
  1831. * Override the sourcemap content making sure it will always contain the tag source code
  1832. * @param {Object} map - sourcemap as json
  1833. * @param {string} source - component source code
  1834. * @returns {Object} original source map with the "sourcesContent" property overriden
  1835. */
  1836. function overrideSourcemapContent(map, source) {
  1837. return {
  1838. ...map,
  1839. sourcesContent: [source]
  1840. }
  1841. }
  1842. /**
  1843. * Create the compilation meta object
  1844. * @param { string } source - source code of the tag we will need to compile
  1845. * @param { string } options - compiling options
  1846. * @returns {Object} meta object
  1847. */
  1848. function createMeta(source, options) {
  1849. return {
  1850. tagName: null,
  1851. fragments: null,
  1852. options: {
  1853. ...DEFAULT_OPTIONS,
  1854. ...options
  1855. },
  1856. source
  1857. }
  1858. }
  1859. /**
  1860. * Generate the output code source together with the sourcemap
  1861. * @param { string } source - source code of the tag we will need to compile
  1862. * @param { string } opts - compiling options
  1863. * @returns { Output } object containing output code and source map
  1864. */
  1865. function compile(source, opts = {}) {
  1866. const meta = createMeta(source, opts);
  1867. const {options} = meta;
  1868. const { code, map } = execute$1('template', options.template, meta, source);
  1869. const { template: template$1, css: css$1, javascript: javascript$1 } = riotParser(options).parse(code).output;
  1870. // extend the meta object with the result of the parsing
  1871. Object.assign(meta, {
  1872. tagName: template$1.name,
  1873. fragments: { template: template$1, css: css$1, javascript: javascript$1 }
  1874. });
  1875. return compose(
  1876. result => ({ ...result, meta }),
  1877. result => execute(result, meta),
  1878. result => ({
  1879. ...result,
  1880. map: overrideSourcemapContent(result.map, source)
  1881. }),
  1882. ast => meta.ast = ast && generateJavascript(ast, {
  1883. sourceMapName: `${options.file}.map`,
  1884. inputSourceMap: normaliseInputSourceMap(map)
  1885. }),
  1886. hookGenerator(template, template$1, code, meta),
  1887. hookGenerator(javascript, javascript$1, code, meta),
  1888. hookGenerator(css, css$1, code, meta),
  1889. )(createInitialInput(meta))
  1890. }
  1891. /**
  1892. * Prepare the riot parser node transformers
  1893. * @param { Function } transformer - transformer function
  1894. * @param { Object } sourceNode - riot parser node
  1895. * @param { string } source - component source code
  1896. * @param { Object } meta - compilation meta information
  1897. * @returns { Promise<Output> } object containing output code and source map
  1898. */
  1899. function hookGenerator(transformer, sourceNode, source, meta) {
  1900. if (
  1901. // filter missing nodes
  1902. !sourceNode ||
  1903. // filter nodes without children
  1904. (sourceNode.nodes && !sourceNode.nodes.length) ||
  1905. // filter empty javascript and css nodes
  1906. (!sourceNode.nodes && !sourceNode.text)) {
  1907. return result => result
  1908. }
  1909. return curry(transformer)(sourceNode, source, meta)
  1910. }
  1911. // This function can be used to register new preprocessors
  1912. // a preprocessor can target either only the css or javascript nodes
  1913. // or the complete tag source file ('template')
  1914. const registerPreprocessor = register$1;
  1915. // This function can allow you to register postprocessors that will parse the output code
  1916. // here we can run prettifiers, eslint fixes...
  1917. const registerPostprocessor = register;
  1918. export { compile, createInitialInput, registerPostprocessor, registerPreprocessor };