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.

2167 lines
70 KiB

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