commit be2ee1311ea98537ffc70299fca0623eb0241512 Author: bjoern Date: Sun Sep 8 20:04:59 2019 +0200 adding diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..952c39b --- /dev/null +++ b/dist/index.html @@ -0,0 +1,34 @@ + + + + + + Tiny Hamburger | Demo 386 Top + + + + + + + + + +
+ + + +
+ + + + diff --git a/node_modules/.bin/acorn b/node_modules/.bin/acorn new file mode 120000 index 0000000..cf76760 --- /dev/null +++ b/node_modules/.bin/acorn @@ -0,0 +1 @@ +../acorn/bin/acorn \ No newline at end of file diff --git a/node_modules/.bin/cssesc b/node_modules/.bin/cssesc new file mode 120000 index 0000000..487b689 --- /dev/null +++ b/node_modules/.bin/cssesc @@ -0,0 +1 @@ +../cssesc/bin/cssesc \ No newline at end of file diff --git a/node_modules/.bin/esparse b/node_modules/.bin/esparse new file mode 120000 index 0000000..7423b18 --- /dev/null +++ b/node_modules/.bin/esparse @@ -0,0 +1 @@ +../esprima/bin/esparse.js \ No newline at end of file diff --git a/node_modules/.bin/esvalidate b/node_modules/.bin/esvalidate new file mode 120000 index 0000000..16069ef --- /dev/null +++ b/node_modules/.bin/esvalidate @@ -0,0 +1 @@ +../esprima/bin/esvalidate.js \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/CHANGELOG.md b/node_modules/@riotjs/compiler/CHANGELOG.md new file mode 100644 index 0000000..17d4729 --- /dev/null +++ b/node_modules/@riotjs/compiler/CHANGELOG.md @@ -0,0 +1,378 @@ +# Compiler Changes + +### v4.3.11 +- Fix https://github.com/riot/compiler/issues/127 +- Fix https://github.com/riot/compiler/issues/126 +- Improve the code maintainability refactoring big files into smaller ones + +### v4.3.10 +- Fix https://github.com/riot/riot/issues/2753 +- Fix https://github.com/riot/riot/issues/2748 +- Update acorn to v7.0.0 + +### v4.3.9 +- Fix https://github.com/riot/compiler/issues/125 +- Fix https://github.com/riot/compiler/issues/124 + +### v4.3.8 +- Fix make sure that the `createExpression` internal function receives always all its arguments + +### v4.3.7 +- Fix https://github.com/riot/compiler/issues/121 + +### v4.3.6 +- Fix https://github.com/riot/compiler/issues/122 +- Fix https://github.com/riot/compiler/pull/118 + +### v4.3.5 +- Fix backslashed unicode css properties + +### v4.3.4 +- Fix escape backslashes in css strings https://github.com/riot/riot/issues/2726 + +### v4.3.3 +- Fix https://github.com/riot/compiler/issues/119 +- Fix https://github.com/riot/riot/issues/2726 + +### v4.3.2 +- Fix void tags will be automatically corrected for example: + ```html + + + + ``` + Will be transfromed to + + ```html + + + + ``` + +### v4.3.1 +- Fix https://github.com/riot/riot/issues/2719 +- Fix https://github.com/riot/riot/issues/2723 + +### v4.3.0 +- Add support for dynamic import + +### v4.2.6 +- Fix expression parts issues https://github.com/riot/riot/issues/2701 + +### v4.2.5 +- Fix https://github.com/riot/riot/issues/2700 replacing the `esprima` parser with `acorn` + +### v4.2.4 +- Fix attributes on custom tags having `if` or `each` directives + +### v4.2.3 +- Update `@riotjs/dom-bindings` using v4.0.0 +- Update npm dependencies + +### v4.2.2 +- Fix [riot#2691](https://github.com/riot/riot/issues/2691) + +### v4.2.1 +- Fix css generation with `@media` queries + +### v4.2.0 +- Add support for `` shortcut expressions +- Fix spread expressions issue [riot/2679](https://github.com/riot/riot/issues/2679) + +### v4.1.1 +- Fix commonjs imports + +### v4.1.0 +- Add support for the slot attribute binding + +### v4.0.4 +- Fix avoid removing selector attributes twice on custom tags + +### v4.0.3 +- Fix attributes handling on custom children nodes [riot#2680](https://github.com/riot/riot/issues/2680) + +### v4.0.2 +- Fix spread operator on each directives [riot#2679](https://github.com/riot/riot/issues/2679) + +### v4.0.1 +- Fix attributes mixed with expressions [riot#2681](https://github.com/riot/riot/issues/2681) + +### v4.0.0 +- Complete rewrite **not backward compatible** +- New output compatible only for Riot.js 4.0.0 +- Add better sourcemaps generation +- Add the `@riotjs/parser` fixing odd issues with regex like [#114](https://github.com/riot/compiler/issues/114) +- Improve the code generation strategy preferring AST to regex parsing +- Remove all the preprocessors from the core in favor of `registerPreprocessor` and `registerPostprocessor` instead + +### v4.0.0-beta.5 +- Fix https://github.com/riot/riot/issues/2669 + +### v4.0.0-beta.3 +- Fix https://github.com/riot/compiler/issues/115 + +### v4.0.0-beta.2 +- Add support for multiple expressions on the same attribute node + +### v4.0.0-beta.1 +- Update rename the `tag` property `exports` + +### v4.0.0-alpha.20 +- Fix handle escaped chars + +### v4.0.0-alpha.19 +- Fix bug in nodes with a single expression + +### v4.0.0-alpha.18 +- Add the `name` key to the tag exports +- Fix self-closed tag bindings + +### v4.0.0-alpha.17 +- Remove unused dev dependencies + +### v4.0.0-alpha.16 +- *Breaking change*: make the compiler API synchronous + +### v4.0.0-alpha.15 +- Fix slots root nodes handling + +### v4.0.0-alpha.14 +- Add sourcemap tests for babel preprocessor +- Update handling of multiple line text expressions, from template literal to array +- Update output format + +### v4.0.0-alpha.13 +- Fix sourcemap for the multiple text expressions +- Check make sure that `slot` tags will not be considered custom tags + +### v4.0.0-alpha.12 +- Fix sourcemap `sourcesContent` property +- Update sourcemap filename + +### v4.0.0-alpha.11 +- Fix sourcemap generation for the `if` and `each` tag bindings + +### v4.0.0-alpha.10 +- Update enhance sourcemaps generation +- Change second arguments for the pre/post processors. The `meta` object will contain info about the current compilation + +### v4.0.0-alpha.9 +- Fix move `recast` into the package dependencies + +### v4.0.0-alpha.8 +- Enhance the source map generation +- Improve performance +- Update npm dependencies + +### v4.0.0-alpha.7 +- Add support for the scoped css +- Update the Tag bindings output to support dynamic tags + +### v4.0.0-alpha.6 +- Fix issue with the object expressions scoping + +### v4.0.0-alpha.5 +- Update the tag bindings API to get the component implementation via function + +### v4.0.0-alpha.4 +- Fix issues related to the member expressions traversal and the scope + +### v4.0.0-alpha.3 +- Fix issue with custom tags and no slots + +### v4.0.0-alpha.2 +- Add support for the spread attributes `` +- Add the `tagName` to the compiler options in runtime +- Fix the options were not passed to the postprocessor + +### v4.0.0-alpha.1 + +- New complete rewrite from scratch +- Change npm name from `riot-compiler` to `@riotjs/compiler` +- First alpha release not backward compatible + +### v3.5.2 +- Fix es6 dynamic imports https://github.com/riot/riot/issues/2641 + +### v3.5.1 +- Fix try importing `@babel/core` first and then fallback to `babel-core` for the `es6` parser + +### v3.5.0 +- Add support for Babel 7 + +### v3.4.0 +- Add inline sourcemap support via `sourcemap='inline'` option + +### v3.3.1 +- Improve the sourcemap generation adding the `sourceContent` key + +### v3.3.0 +- Add initial experimental sourcemaps support via `sourcemap: true` option + +### v3.2.6 +- Fix #105 +- Fix #104 + +### v3.2.5 +- Update dependencies and refactor some internal code avoiding bitwise operators +- Fix coffeescript parser require https://github.com/riot/compiler/pull/102 + +### v3.2.4 +- Fix [riot#2369](https://github.com/riot/riot/issues/2369) : Possible bug involving compilation of tags containing regex. +- Using the `skip-regex` function from npm for sharing bwteen modules (at future). +- Using the `jsSplitter` function for safer replacement of JS code, part of the next compiler. + +### v3.2.3 +- Fixes various issues with literal regexes. + +### v3.1.4 +- Fix avoid the `filename` option for the babel-standalone parser + +### v3.1.3 +- Fix babel in browser runtime parser https://github.com/riot/examples/issues/51 + +### v3.1.2 +- Fix [riot#2210](https://github.com/riot/riot/issues/2210) : Style tag get stripped from riot tag even if it's in a javascript string. +- Updated devDependencies. + +### v3.1.0 +- Adds support for css @apply rule: now ScopedCSS parser can handle it properly + +### v3.0.0 +- Deprecate old `babel` support, now the `es6` parser will use Babel 6 by default +- Change css always scoped by default +- Fix all the `value` attributes using expressions will be output as `riot-value` to [riot#1957](https://github.com/riot/riot/issues/1957) + +### v2.5.5 +- Fix to erroneous version number in the package.json, v2.5.4 was released before. +- Removed unuseful files from the npm package. +- Updated credits in package.json +- Updated devDependencies, skip ESLint in CI test for node v0.12 +- BuGless-hack for [riot#1966](https://github.com/riot/riot/issues/1966) - You can use `<-/>` to signal the end of the html if your html is ending with an expression. + +### v2.5.4 +- Fix #68 : SASS inside Pug template gives Invalid CSS. +- Added parser for [bublé](https://buble.surge.sh) as `buble` in the browser. Option `modules` is `false` in all versions. +- Added parser for [bublé](https://buble.surge.sh) as `buble`. +- Added support for es6 `import` statements. Thanks to @kuashe! - Related to [riot#1715](https://github.com/riot/riot/issues/1715), [riot#1784](https://github.com/riot/riot/issues/1784), and [riot#1864](https://github.com/riot/riot/issues/1864). + +### v2.5.3 +- Fix #73 : resolveModuleSource must be a function - Option removed from the default Babel options. +- Updated node.js to 4.4 in the Travis environment. +- Downgraded ESLint to 2.x for using with node v0.12.x + +### v2.5.2 +- Fix #72: `undefined` is not a function when evaluating `parsers._req`. +- Updated node versions for travis, including v5.x + +### v2.4.1 + +- Add the `pug` parser (it will replace completely `jade` in the next major release) +- Add the possibility to pass custom parsers options directly via the `compiler.compile` method through the `parserOptions: {js: {}, template: {}, style: {}}` key [more info](https://github.com/riot/compiler/issues/64) +- Fix un-escape parser options in html [more info](https://github.com/riot/compiler/issues/63) + +### v2.3.23 +- The parsers are moved to its own directory in the node version. The load is on first use. +- Fix [riot#1325](https://github.com/riot/riot/issues/1325) : Gulp + Browserify + Babelify + type="es6" error. +- Fix [riot#1342](https://github.com/riot/riot/issues/1342), [riot#1636](https://github.com/riot/riot/issues/1636) and request from [dwyl/learn-riot#8](https://github.com/dwyl/learn-riot/issues/8) : Server-Side Rendered Page Fails W3C Check. The new `data-is` attribute is used for scoped styles in addition to `riot-tag` (the later will be removed in compiler v3.x) +- The keyword `defer` in ` or + pushTag(state, `/${name}`, start, end); + break + } + case data[pos] === '<': + state.pos++; + return TAG + default: + expr(state, null, '<', pos); + } + + return TEXT + } + + /** + * Parse the text content depending on the name + * @param {ParserState} state - Parser state + * @param {string} name - one of the tags matched by the RE_SCRYLE regex + * @param {Array} match - result of the regex matching the content of the parsed tag + * @returns {undefined} void function + */ + function parseSpecialTagsContent(state, name, match) { + const { pos } = state; + const start = match.index; + + if (name === TEXTAREA_TAG) { + expr(state, null, match[0], pos); + } else { + pushText(state, pos, start); + } + } + + /*--------------------------------------------------------------------- + * Tree builder for the riot tag parser. + * + * The output has a root property and separate arrays for `html`, `css`, + * and `js` tags. + * + * The root tag is included as first element in the `html` array. + * Script tags marked with "defer" are included in `html` instead `js`. + * + * - Mark SVG tags + * - Mark raw tags + * - Mark void tags + * - Split prefixes from expressions + * - Unescape escaped brackets and escape EOLs and backslashes + * - Compact whitespace (option `compact`) for non-raw tags + * - Create an array `parts` for text nodes and attributes + * + * Throws on unclosed tags or closing tags without start tag. + * Selfclosing and void tags has no nodes[] property. + */ + + /** + * Escape the carriage return and the line feed from a string + * @param {string} string - input string + * @returns {string} output string escaped + */ + function escapeReturn(string) { + return string + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n') + } + + /** + * Escape double slashes in a string + * @param {string} string - input string + * @returns {string} output string escaped + */ + function escapeSlashes(string) { + return string.replace(/\\/g, '\\\\') + } + + /** + * Replace the multiple spaces with only one + * @param {string} string - input string + * @returns {string} string without trailing spaces + */ + function cleanSpaces(string) { + return string.replace(/\s+/g, ' ') + } + + const TREE_BUILDER_STRUCT = Object.seal({ + get() { + const store = this.store; + // The real root tag is in store.root.nodes[0] + return { + [TEMPLATE_OUTPUT_NAME]: store.root.nodes[0], + [CSS_OUTPUT_NAME]: store[STYLE_TAG], + [JAVASCRIPT_OUTPUT_NAME]: store[JAVASCRIPT_TAG] + } + }, + + /** + * Process the current tag or text. + * @param {Object} node - Raw pseudo-node from the parser + * @returns {undefined} void function + */ + push(node) { + const store = this.store; + + switch (node.type) { + case TEXT: + this.pushText(store, node); + break + case TAG: { + const name = node.name; + const closingTagChar = '/'; + const [firstChar] = name; + + if (firstChar === closingTagChar && !node.isVoid) { + this.closeTag(store, node, name); + } else if (firstChar !== closingTagChar) { + this.openTag(store, node); + } + break + } + } + }, + closeTag(store, node) { + const last = store.scryle || store.last; + + last.end = node.end; + + if (store.scryle) { + store.scryle = null; + } else { + store.last = store.stack.pop(); + } + }, + + openTag(store, node) { + const name = node.name; + const attrs = node.attributes; + + if ([JAVASCRIPT_TAG, STYLE_TAG].includes(name)) { + // Only accept one of each + if (store[name]) { + panic$1(this.store.data, duplicatedNamedTag.replace('%1', name), node.start); + } + + store[name] = node; + store.scryle = store[name]; + + } else { + // store.last holds the last tag pushed in the stack and this are + // non-void, non-empty tags, so we are sure the `lastTag` here + // have a `nodes` property. + const lastTag = store.last; + const newNode = node; + + lastTag.nodes.push(newNode); + + if (lastTag[IS_RAW] || RAW_TAGS.test(name)) { + node[IS_RAW] = true; + } + + if (!node[IS_SELF_CLOSING] && !node[IS_VOID]) { + store.stack.push(lastTag); + newNode.nodes = []; + store.last = newNode; + } + } + + if (attrs) { + this.attrs(attrs); + } + }, + attrs(attributes) { + attributes.forEach(attr => { + if (attr.value) { + this.split(attr, attr.value, attr.valueStart, true); + } + }); + }, + pushText(store, node) { + const text = node.text; + const empty = !/\S/.test(text); + const scryle = store.scryle; + if (!scryle) { + // store.last always have a nodes property + const parent = store.last; + + const pack = this.compact && !parent[IS_RAW]; + if (pack && empty) { + return + } + this.split(node, text, node.start, pack); + parent.nodes.push(node); + } else if (!empty) { + scryle.text = node; + } + }, + split(node, source, start, pack) { + const expressions = node.expressions; + const parts = []; + + if (expressions) { + let pos = 0; + + expressions.forEach(expr => { + const text = source.slice(pos, expr.start - start); + const code = expr.text; + parts.push(this.sanitise(node, text, pack), escapeReturn(escapeSlashes(code).trim())); + pos = expr.end - start; + }); + + if (pos < node.end) { + parts.push(this.sanitise(node, source.slice(pos), pack)); + } + } else { + parts[0] = this.sanitise(node, source, pack); + } + + node.parts = parts.filter(p => p); // remove the empty strings + }, + // unescape escaped brackets and split prefixes of expressions + sanitise(node, text, pack) { + let rep = node.unescape; + if (rep) { + let idx = 0; + rep = `\\${rep}`; + while ((idx = text.indexOf(rep, idx)) !== -1) { + text = text.substr(0, idx) + text.substr(idx + 1); + idx++; + } + } + + text = escapeSlashes(text); + + return pack ? cleanSpaces(text) : escapeReturn(text) + } + }); + + function createTreeBuilder(data, options) { + const root = { + type: TAG, + name: '', + start: 0, + end: 0, + nodes: [] + }; + + return Object.assign(Object.create(TREE_BUILDER_STRUCT), { + compact: options.compact !== false, + store: { + last: root, + stack: [], + scryle: null, + root, + style: null, + script: null, + data + } + }) + } + + /** + * Factory for the Parser class, exposing only the `parse` method. + * The export adds the Parser class as property. + * + * @param {Object} options - User Options + * @param {Function} customBuilder - Tree builder factory + * @returns {Function} Public Parser implementation. + */ + function parser$1(options, customBuilder) { + const state = curry(createParserState)(options, customBuilder || createTreeBuilder); + return { + parse: (data) => parse(state(data)) + } + } + + /** + * Create a new state object + * @param {Object} userOptions - parser options + * @param {Function} builder - Tree builder factory + * @param {string} data - data to parse + * @returns {ParserState} it represents the current parser state + */ + function createParserState(userOptions, builder, data) { + const options = Object.assign({ + brackets: ['{', '}'] + }, userOptions); + + return { + options, + regexCache: {}, + pos: 0, + count: -1, + root: null, + last: null, + scryle: null, + builder: builder(data, options), + data + } + } + + /** + * It creates a raw output of pseudo-nodes with one of three different types, + * all of them having a start/end position: + * + * - TAG -- Opening or closing tags + * - TEXT -- Raw text + * - COMMENT -- Comments + * + * @param {ParserState} state - Current parser state + * @returns {ParserResult} Result, contains data and output properties. + */ + function parse(state) { + const { data } = state; + + walk(state); + flush(state); + + if (state.count) { + panic$1(data, state.count > 0 ? unexpectedEndOfFile : rootTagNotFound, state.pos); + } + + return { + data, + output: state.builder.get() + } + } + + /** + * Parser walking recursive function + * @param {ParserState} state - Current parser state + * @param {string} type - current parsing context + * @returns {undefined} void function + */ + function walk(state, type) { + const { data } = state; + // extend the state adding the tree builder instance and the initial data + const length = data.length; + + // The "count" property is set to 1 when the first tag is found. + // This becomes the root and precedent text or comments are discarded. + // So, at the end of the parsing count must be zero. + if (state.pos < length && state.count) { + walk(state, eat(state, type)); + } + } + + /** + * Function to help iterating on the current parser state + * @param {ParserState} state - Current parser state + * @param {string} type - current parsing context + * @returns {string} parsing context + */ + function eat(state, type) { + switch (type) { + case TAG: + return tag(state) + case ATTR: + return attr(state) + default: + return text(state) + } + } + + /** + * The nodeTypes definition + */ + const nodeTypes = types$3; + + // import {IS_BOOLEAN,IS_CUSTOM,IS_RAW,IS_SPREAD,IS_VOID} from '@riotjs/parser/src/constants' + + const BINDING_TYPES = 'bindingTypes'; + const EACH_BINDING_TYPE = 'EACH'; + const IF_BINDING_TYPE = 'IF'; + const TAG_BINDING_TYPE = 'TAG'; + const SLOT_BINDING_TYPE = 'SLOT'; + + + const EXPRESSION_TYPES = 'expressionTypes'; + const ATTRIBUTE_EXPRESSION_TYPE = 'ATTRIBUTE'; + const VALUE_EXPRESSION_TYPE = 'VALUE'; + const TEXT_EXPRESSION_TYPE = 'TEXT'; + const EVENT_EXPRESSION_TYPE = 'EVENT'; + + const TEMPLATE_FN = 'template'; + const SCOPE = 'scope'; + const GET_COMPONENT_FN = 'getComponent'; + + // keys needed to create the DOM bindings + const BINDING_SELECTOR_KEY = 'selector'; + const BINDING_GET_COMPONENT_KEY = 'getComponent'; + const BINDING_TEMPLATE_KEY = 'template'; + const BINDING_TYPE_KEY = 'type'; + const BINDING_REDUNDANT_ATTRIBUTE_KEY = 'redundantAttribute'; + const BINDING_CONDITION_KEY = 'condition'; + const BINDING_ITEM_NAME_KEY = 'itemName'; + const BINDING_GET_KEY_KEY = 'getKey'; + const BINDING_INDEX_NAME_KEY = 'indexName'; + const BINDING_EVALUATE_KEY = 'evaluate'; + const BINDING_NAME_KEY = 'name'; + const BINDING_SLOTS_KEY = 'slots'; + const BINDING_EXPRESSIONS_KEY = 'expressions'; + const BINDING_CHILD_NODE_INDEX_KEY = 'childNodeIndex'; + // slots keys + const BINDING_BINDINGS_KEY = 'bindings'; + const BINDING_ID_KEY = 'id'; + const BINDING_HTML_KEY = 'html'; + const BINDING_ATTRIBUTES_KEY = 'attributes'; + + // DOM directives + const IF_DIRECTIVE = 'if'; + const EACH_DIRECTIVE = 'each'; + const KEY_ATTRIBUTE = 'key'; + const SLOT_ATTRIBUTE = 'slot'; + const NAME_ATTRIBUTE = 'name'; + const IS_DIRECTIVE = 'is'; + + // Misc + const DEFAULT_SLOT_NAME = 'default'; + const TEXT_NODE_EXPRESSION_PLACEHOLDER = ''; + const BINDING_SELECTOR_PREFIX = 'expr'; + const SLOT_TAG_NODE_NAME = 'slot'; + const PROGRESS_TAG_NODE_NAME = 'progress'; + const IS_VOID_NODE = 'isVoid'; + const IS_CUSTOM_NODE = 'isCustom'; + const IS_BOOLEAN_ATTRIBUTE = 'isBoolean'; + const IS_SPREAD_ATTRIBUTE = 'isSpread'; + + /** + * True if the node has not expression set nor bindings directives + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only if it's a static node that doesn't need bindings or expressions + */ + function isStaticNode(node) { + return [ + hasExpressions, + findEachAttribute, + findIfAttribute, + isCustomNode, + isSlotNode + ].every(test => !test(node)) + } + + /** + * Check if a node name is part of the browser or builtin javascript api or it belongs to the current scope + * @param { types.NodePath } path - containing the current node visited + * @returns {boolean} true if it's a global api variable + */ + function isGlobal({ scope, node }) { + return Boolean( + isRaw(node) || + isBuiltinAPI(node) || + isBrowserAPI(node) || + isNewExpression(node) || + isNodeInScope(scope, node), + ) + } + + /** + * Checks if the identifier of a given node exists in a scope + * @param {Scope} scope - scope where to search for the identifier + * @param {types.Node} node - node to search for the identifier + * @returns {boolean} true if the node identifier is defined in the given scope + */ + function isNodeInScope(scope, node) { + const traverse = (isInScope = false) => { + types$1.visit(node, { + visitIdentifier(path) { + if (scope.lookup(getName$1(path.node))) { + isInScope = true; + } + + this.abort(); + } + }); + + return isInScope + }; + + return traverse() + } + + /** + * True if the node has the isCustom attribute set + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if either it's a riot component or a custom element + */ + function isCustomNode(node) { + return !!(node[IS_CUSTOM_NODE] || hasIsAttribute(node)) + } + + /** + * True the node is + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if it's a slot node + */ + function isSlotNode(node) { + return node.name === SLOT_TAG_NODE_NAME + } + + /** + * True if the node has the isVoid attribute set + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if the node is self closing + */ + function isVoidNode(node) { + return !!node[IS_VOID_NODE] + } + + /** + * True if the riot parser did find a tag node + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the tag nodes + */ + function isTagNode(node) { + return node.type === nodeTypes.TAG + } + + /** + * True if the riot parser did find a text node + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the text nodes + */ + function isTextNode(node) { + return node.type === nodeTypes.TEXT + } + + /** + * True if the node parsed is the root one + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the root nodes + */ + function isRootNode(node) { + return node.isRoot + } + + /** + * True if the attribute parsed is of type spread one + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if the attribute node is of type spread + */ + function isSpreadAttribute$1(node) { + return node[IS_SPREAD_ATTRIBUTE] + } + + /** + * True if the node is an attribute and its name is "value" + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for value attribute nodes + */ + function isValueAttribute(node) { + return node.name === 'value' + } + + /** + * True if the DOM node is a progress tag + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true for the progress tags + */ + function isProgressNode(node) { + return node.name === PROGRESS_TAG_NODE_NAME + } + + /** + * True if the node is an attribute and a DOM handler + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for dom listener attribute nodes + */ + const isEventAttribute = (() => { + const EVENT_ATTR_RE = /^on/; + return node => EVENT_ATTR_RE.test(node.name) + })(); + + /** + * True if the node has expressions or expression attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} ditto + */ + function hasExpressions(node) { + return !!( + node.expressions || + // has expression attributes + (getNodeAttributes(node).some(attribute => hasExpressions(attribute))) || + // has child text nodes with expressions + (node.nodes && node.nodes.some(node => isTextNode(node) && hasExpressions(node))) + ) + } + + /** + * True if the node is a directive having its own template + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the IF EACH and TAG bindings + */ + function hasItsOwnTemplate(node) { + return [ + findEachAttribute, + findIfAttribute, + isCustomNode + ].some(test => test(node)) + } + + const hasIfAttribute = compose(Boolean, findIfAttribute); + const hasEachAttribute = compose(Boolean, findEachAttribute); + const hasIsAttribute = compose(Boolean, findIsAttribute); + const hasKeyAttribute = compose(Boolean, findKeyAttribute); + + /** + * Find the attribute node + * @param { string } name - name of the attribute we want to find + * @param { riotParser.nodeTypes.TAG } node - a tag node + * @returns { riotParser.nodeTypes.ATTR } attribute node + */ + function findAttribute(name, node) { + return node.attributes && node.attributes.find(attr => getName$1(attr) === name) + } + + function findIfAttribute(node) { + return findAttribute(IF_DIRECTIVE, node) + } + + function findEachAttribute(node) { + return findAttribute(EACH_DIRECTIVE, node) + } + + function findKeyAttribute(node) { + return findAttribute(KEY_ATTRIBUTE, node) + } + + function findIsAttribute(node) { + return findAttribute(IS_DIRECTIVE, node) + } + + /** + * Find all the node attributes that are not expressions + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} list of all the static attributes + */ + function findStaticAttributes(node) { + return getNodeAttributes(node).filter(attribute => !hasExpressions(attribute)) + } + + /** + * Find all the node attributes that have expressions + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} list of all the dynamic attributes + */ + function findDynamicAttributes(node) { + return getNodeAttributes(node).filter(hasExpressions) + } + + /** + * Unescape the user escaped chars + * @param {string} string - input string + * @param {string} char - probably a '{' or anything the user want's to escape + * @returns {string} cleaned up string + */ + function unescapeChar(string, char) { + return string.replace(RegExp(`\\\\${char}`, 'gm'), char) + } + + const scope$1 = builders.identifier(SCOPE); + const getName$1 = node => node && node.name ? node.name : node; + + /** + * Replace the path scope with a member Expression + * @param { types.NodePath } path - containing the current node visited + * @param { types.Node } property - node we want to prefix with the scope identifier + * @returns {undefined} this is a void function + */ + function replacePathScope(path, property) { + path.replace(builders.memberExpression( + scope$1, + property, + false + )); + } + + /** + * Change the nodes scope adding the `scope` prefix + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return false if we want to stop the tree traversal + * @context { types.visit } + */ + function updateNodeScope(path) { + if (!isGlobal(path)) { + replacePathScope(path, path.node); + + return false + } + + this.traverse(path); + } + + /** + * Change the scope of the member expressions + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return always false because we want to check only the first node object + */ + function visitMemberExpression(path) { + if (!isGlobal(path) && !isGlobal({ node: path.node.object, scope: path.scope })) { + if (path.value.computed) { + this.traverse(path); + } else if (isBinaryExpression(path.node.object) || path.node.object.computed) { + this.traverse(path.get('object')); + } else if (!path.node.object.callee) { + replacePathScope(path, isThisExpression(path.node.object) ? path.node.property : path.node); + } else { + this.traverse(path.get('object')); + } + } + + return false + } + + + /** + * Objects properties should be handled a bit differently from the Identifier + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return false if we want to stop the tree traversal + */ + function visitProperty(path) { + const value = path.node.value; + + if (isIdentifier(value)) { + updateNodeScope(path.get('value')); + } else { + this.traverse(path.get('value')); + } + + return false + } + + /** + * The this expressions should be replaced with the scope + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean|undefined } return false if we want to stop the tree traversal + */ + function visitThisExpression(path) { + path.replace(scope$1); + this.traverse(path); + } + + + /** + * Update the scope of the global nodes + * @param { Object } ast - ast program + * @returns { Object } the ast program with all the global nodes updated + */ + function updateNodesScope(ast) { + const ignorePath = () => false; + + types$1.visit(ast, { + visitIdentifier: updateNodeScope, + visitMemberExpression, + visitProperty, + visitThisExpression, + visitClassExpression: ignorePath + }); + + return ast + } + + /** + * Convert any expression to an AST tree + * @param { Object } expression - expression parsed by the riot parser + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Object } the ast generated + */ + function createASTFromExpression(expression, sourceFile, sourceCode) { + const code = sourceFile ? + addLineOffset(expression.text, sourceCode, expression) : + expression.text; + + return generateAST(`(${code})`, { + sourceFileName: sourceFile + }) + } + + /** + * Create the bindings template property + * @param {Array} args - arguments to pass to the template function + * @returns {ASTNode} a binding template key + */ + function createTemplateProperty(args) { + return simplePropertyNode( + BINDING_TEMPLATE_KEY, + args ? callTemplateFunction(...args) : nullNode() + ) + } + + /** + * Try to get the expression of an attribute node + * @param { RiotParser.Node.Attribute } attribute - riot parser attribute node + * @returns { RiotParser.Node.Expression } attribute expression value + */ + function getAttributeExpression(attribute) { + return attribute.expressions ? attribute.expressions[0] : { + // if no expression was found try to typecast the attribute value + ...attribute, + text: attribute.value + } + } + + /** + * Wrap the ast generated in a function call providing the scope argument + * @param {Object} ast - function body + * @returns {FunctionExpresion} function having the scope argument injected + */ + function wrapASTInFunctionWithScope(ast) { + return builders.functionExpression( + null, + [scope$1], + builders.blockStatement([builders.returnStatement( + ast + )]) + ) + } + + /** + * Convert any parser option to a valid template one + * @param { RiotParser.Node.Expression } expression - expression parsed by the riot parser + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Object } a FunctionExpression object + * + * @example + * toScopedFunction('foo + bar') // scope.foo + scope.bar + * + * @example + * toScopedFunction('foo.baz + bar') // scope.foo.baz + scope.bar + */ + function toScopedFunction(expression, sourceFile, sourceCode) { + return compose( + wrapASTInFunctionWithScope, + transformExpression, + )(expression, sourceFile, sourceCode) + } + + /** + * Transform an expression node updating its global scope + * @param {RiotParser.Node.Expr} expression - riot parser expression node + * @param {string} sourceFile - source file + * @param {string} sourceCode - source code + * @returns {ASTExpression} ast expression generated from the riot parser expression node + */ + function transformExpression(expression, sourceFile, sourceCode) { + return compose( + getExpressionAST, + updateNodesScope, + createASTFromExpression + )(expression, sourceFile, sourceCode) + } + + /** + * Get the parsed AST expression of riot expression node + * @param {AST.Program} sourceAST - raw node parsed + * @returns {AST.Expression} program expression output + */ + function getExpressionAST(sourceAST) { + const astBody = sourceAST.program.body; + + return astBody[0] ? astBody[0].expression : astBody + } + + /** + * Create the template call function + * @param {Array|string|Node.Literal} template - template string + * @param {Array} bindings - template bindings provided as AST nodes + * @returns {Node.CallExpression} template call expression + */ + function callTemplateFunction(template, bindings) { + return builders.callExpression(builders.identifier(TEMPLATE_FN), [ + template ? builders.literal(template) : nullNode(), + bindings ? builders.arrayExpression(bindings) : nullNode() + ]) + } + + /** + * Convert any DOM attribute into a valid DOM selector useful for the querySelector API + * @param { string } attributeName - name of the attribute to query + * @returns { string } the attribute transformed to a query selector + */ + const attributeNameToDOMQuerySelector = attributeName => `[${attributeName}]`; + + /** + * Create the properties to query a DOM node + * @param { string } attributeName - attribute name needed to identify a DOM node + * @returns { Array } array containing the selector properties needed for the binding + */ + function createSelectorProperties(attributeName) { + return attributeName ? [ + simplePropertyNode(BINDING_REDUNDANT_ATTRIBUTE_KEY, builders.literal(attributeName)), + simplePropertyNode(BINDING_SELECTOR_KEY, + compose(builders.literal, attributeNameToDOMQuerySelector)(attributeName) + ) + ] : [] + } + + /** + * Clone the node filtering out the selector attribute from the attributes list + * @param {RiotParser.Node} node - riot parser node + * @param {string} selectorAttribute - name of the selector attribute to filter out + * @returns {RiotParser.Node} the node with the attribute cleaned up + */ + function cloneNodeWithoutSelectorAttribute(node, selectorAttribute) { + return { + ...node, + attributes: getAttributesWithoutSelector(getNodeAttributes(node), selectorAttribute) + } + } + + + /** + * Get the node attributes without the selector one + * @param {Array} attributes - attributes list + * @param {string} selectorAttribute - name of the selector attribute to filter out + * @returns {Array} filtered attributes + */ + function getAttributesWithoutSelector(attributes, selectorAttribute) { + if (selectorAttribute) + return attributes.filter(attribute => attribute.name !== selectorAttribute) + + return attributes + } + + /** + * Clean binding or custom attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} only the attributes that are not bindings or directives + */ + function cleanAttributes(node) { + return getNodeAttributes(node).filter(attribute => ![ + IF_DIRECTIVE, + EACH_DIRECTIVE, + KEY_ATTRIBUTE, + SLOT_ATTRIBUTE, + IS_DIRECTIVE + ].includes(attribute.name)) + } + + /** + * Create a root node proxing only its nodes and attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {RiotParser.Node} root node + */ + function createRootNode(node) { + return { + nodes: getChildrenNodes(node), + isRoot: true, + // root nodes shuold't have directives + attributes: cleanAttributes(node) + } + } + + /** + * Get all the child nodes of a RiotParser.Node + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} all the child nodes found + */ + function getChildrenNodes(node) { + return node && node.nodes ? node.nodes : [] + } + + /** + * Get all the attributes of a riot parser node + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} all the attributes find + */ + function getNodeAttributes(node) { + return node.attributes ? node.attributes : [] + } + /** + * Get the name of a custom node transforming it into an expression node + * @param {RiotParser.Node} node - riot parser node + * @returns {RiotParser.Node.Attr} the node name as expression attribute + */ + function getCustomNodeNameAsExpression(node) { + const isAttribute = findIsAttribute(node); + const toRawString = val => `'${val}'`; + + if (isAttribute) { + return isAttribute.expressions ? isAttribute.expressions[0] : { + ...isAttribute, + text: toRawString(isAttribute.value) + } + } + + return { ...node, text: toRawString(getName$1(node)) } + } + + /** + * Convert all the node static attributes to strings + * @param {RiotParser.Node} node - riot parser node + * @returns {string} all the node static concatenated as string + */ + function staticAttributesToString(node) { + return findStaticAttributes(node) + .map(attribute => attribute[IS_BOOLEAN_ATTRIBUTE] || !attribute.value ? + attribute.name : + `${attribute.name}="${unescapeNode(attribute, 'value').value}"` + ).join(' ') + } + + /** + * Make sure that node escaped chars will be unescaped + * @param {RiotParser.Node} node - riot parser node + * @param {string} key - key property to unescape + * @returns {RiotParser.Node} node with the text property unescaped + */ + function unescapeNode(node, key) { + if (node.unescape) { + return { + ...node, + [key]: unescapeChar(node[key], node.unescape) + } + } + + return node + } + + + /** + * Convert a riot parser opening node into a string + * @param {RiotParser.Node} node - riot parser node + * @returns {string} the node as string + */ + function nodeToString(node) { + const attributes = staticAttributesToString(node); + + switch(true) { + case isTagNode(node): + return `<${node.name}${attributes ? ` ${attributes}` : ''}${isVoidNode(node) ? '/' : ''}>` + case isTextNode(node): + return hasExpressions(node) ? TEXT_NODE_EXPRESSION_PLACEHOLDER : unescapeNode(node, 'text').text + default: + return '' + } + } + + /** + * Close an html node + * @param {RiotParser.Node} node - riot parser node + * @returns {string} the closing tag of the html tag node passed to this function + */ + function closeTag(node) { + return node.name ? `` : '' + } + + /** + * Create a strings array with the `join` call to transform it into a string + * @param {Array} stringsArray - array containing all the strings to concatenate + * @returns {AST.CallExpression} array with a `join` call + */ + function createArrayString(stringsArray) { + return builders.callExpression( + builders.memberExpression( + builders.arrayExpression(stringsArray), + builders.identifier('join'), + false + ), + [builders.literal('')], + ) + } + + /** + * Simple expression bindings might contain multiple expressions like for example: "class="{foo} red {bar}"" + * This helper aims to merge them in a template literal if it's necessary + * @param {RiotParser.Attr} node - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { Object } a template literal expression object + */ + function mergeAttributeExpressions(node, sourceFile, sourceCode) { + if (!node.parts || node.parts.length === 1) { + return transformExpression(node.expressions[0], sourceFile, sourceCode) + } + const stringsArray = [ + ...node.parts.reduce((acc, str) => { + const expression = node.expressions.find(e => e.text.trim() === str); + + return [ + ...acc, + expression ? transformExpression(expression, sourceFile, sourceCode) : builders.literal(str) + ] + }, []) + ].filter(expr => !isLiteral(expr) || expr.value); + + + return createArrayString(stringsArray) + } + + /** + * Create a selector that will be used to find the node via dom-bindings + * @param {number} id - temporary variable that will be increased anytime this function will be called + * @returns {string} selector attribute needed to bind a riot expression + */ + const createBindingSelector = (function createSelector(id = 0) { + return () => `${BINDING_SELECTOR_PREFIX}${id++}` + }()); + + /** + * Create an attribute evaluation function + * @param {RiotParser.Attr} sourceNode - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { AST.Node } an AST function expression to evaluate the attribute value + */ + function createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) { + return hasExpressions(sourceNode) ? + // dynamic attribute + wrapASTInFunctionWithScope(mergeAttributeExpressions(sourceNode, sourceFile, sourceCode)) : + // static attribute + builders.functionExpression( + null, + [], + builders.blockStatement([ + builders.returnStatement(builders.literal(sourceNode.value || true)) + ]), + ) + } + + /** + * Simple clone deep function, do not use it for classes or recursive objects! + * @param {*} source - possibily an object to clone + * @returns {*} the object we wanted to clone + */ + function cloneDeep(source) { + return JSON.parse(JSON.stringify(source)) + } + + const getEachItemName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[0] : expression.left; + const getEachIndexName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[1] : null; + const getEachValue = expression => expression.right; + const nameToliteral = compose(builders.literal, getName$1); + + const generateEachItemNameKey = expression => simplePropertyNode( + BINDING_ITEM_NAME_KEY, + compose(nameToliteral, getEachItemName)(expression) + ); + + const generateEachIndexNameKey = expression => simplePropertyNode( + BINDING_INDEX_NAME_KEY, + compose(nameToliteral, getEachIndexName)(expression) + ); + + const generateEachEvaluateKey = (expression, eachExpression, sourceFile, sourceCode) => simplePropertyNode( + BINDING_EVALUATE_KEY, + compose( + e => toScopedFunction(e, sourceFile, sourceCode), + e => ({ + ...eachExpression, + text: generateJavascript(e).code + }), + getEachValue + )(expression) + ); + + /** + * Get the each expression properties to create properly the template binding + * @param { DomBinding.Expression } eachExpression - original each expression data + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Array } AST nodes that are needed to build an each binding + */ + function generateEachExpressionProperties(eachExpression, sourceFile, sourceCode) { + const ast = createASTFromExpression(eachExpression, sourceFile, sourceCode); + const body = ast.program.body; + const firstNode = body[0]; + + if (!isExpressionStatement(firstNode)) { + panic(`The each directives supported should be of type "ExpressionStatement",you have provided a "${firstNode.type}"`); + } + + const { expression } = firstNode; + + return [ + generateEachItemNameKey(expression), + generateEachIndexNameKey(expression), + generateEachEvaluateKey(expression, eachExpression, sourceFile, sourceCode) + ] + } + + /** + * Transform a RiotParser.Node.Tag into an each binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an each binding node + */ + function createEachBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + const [ifAttribute, eachAttribute, keyAttribute] = [ + findIfAttribute, + findEachAttribute, + findKeyAttribute + ].map(f => f(sourceNode)); + const attributeOrNull = attribute => attribute ? toScopedFunction(getAttributeExpression(attribute), sourceFile, sourceCode) : nullNode(); + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(EACH_BINDING_TYPE), + false + ), + ), + simplePropertyNode(BINDING_GET_KEY_KEY, attributeOrNull(keyAttribute)), + simplePropertyNode(BINDING_CONDITION_KEY, attributeOrNull(ifAttribute)), + createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)), + ...createSelectorProperties(selectorAttribute), + ...compose(generateEachExpressionProperties, getAttributeExpression)(eachAttribute) + ]) + } + + /** + * Transform a RiotParser.Node.Tag into an if binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { stiring } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an if binding node + */ + function createIfBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + const ifAttribute = findIfAttribute(sourceNode); + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(IF_BINDING_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + toScopedFunction(ifAttribute.expressions[0], sourceFile, sourceCode) + ), + ...createSelectorProperties(selectorAttribute), + createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)) + ]) + } + + /** + * Create a simple attribute expression + * @param {RiotParser.Node.Attr} sourceNode - the custom tag + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} object containing the expression binding keys + */ + function createAttributeExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(ATTRIBUTE_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode(BINDING_NAME_KEY, isSpreadAttribute$1(sourceNode) ? nullNode() : builders.literal(sourceNode.name)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) + } + + /** + * Create a simple event expression + * @param {RiotParser.Node.Attr} sourceNode - attribute containing the event handlers + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} object containing the expression binding keys + */ + function createEventExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(EVENT_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode(BINDING_NAME_KEY, builders.literal(sourceNode.name)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) + } + + /** + * Generate the pure immutable string chunks from a RiotParser.Node.Text + * @param {RiotParser.Node.Text} node - riot parser text node + * @param {string} sourceCode sourceCode - source code + * @returns {Array} array containing the immutable string chunks + */ + function generateLiteralStringChunksFromNode(node, sourceCode) { + return node.expressions.reduce((chunks, expression, index) => { + const start = index ? node.expressions[index - 1].end : node.start; + + chunks.push(sourceCode.substring(start, expression.start)); + + // add the tail to the string + if (index === node.expressions.length - 1) + chunks.push(sourceCode.substring(expression.end, node.end)); + + return chunks + }, []).map(str => node.unescape ? unescapeChar(str, node.unescape) : str) + } + + /** + * Simple bindings might contain multiple expressions like for example: "{foo} and {bar}" + * This helper aims to merge them in a template literal if it's necessary + * @param {RiotParser.Node} node - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { Object } a template literal expression object + */ + function mergeNodeExpressions(node, sourceFile, sourceCode) { + if (node.parts.length === 1) + return transformExpression(node.expressions[0], sourceFile, sourceCode) + + const pureStringChunks = generateLiteralStringChunksFromNode(node, sourceCode); + const stringsArray = pureStringChunks.reduce((acc, str, index) => { + const expr = node.expressions[index]; + + return [ + ...acc, + builders.literal(str), + expr ? transformExpression(expr, sourceFile, sourceCode) : nullNode() + ] + }, []) + // filter the empty literal expressions + .filter(expr => !isLiteral(expr) || expr.value); + + return createArrayString(stringsArray) + } + + /** + * Create a text expression + * @param {RiotParser.Node.Text} sourceNode - text node to parse + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {number} childNodeIndex - position of the child text node in its parent children nodes + * @returns {AST.Node} object containing the expression binding keys + */ + function createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(TEXT_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_CHILD_NODE_INDEX_KEY, + builders.literal(childNodeIndex) + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + wrapASTInFunctionWithScope( + mergeNodeExpressions(sourceNode, sourceFile, sourceCode) + ) + ) + ]) + } + + function createValueExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(VALUE_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) + } + + function createExpression(sourceNode, sourceFile, sourceCode, childNodeIndex, parentNode) { + switch (true) { + case isTextNode(sourceNode): + return createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) + // progress nodes value attributes will be rendered as attributes + // see https://github.com/riot/compiler/issues/122 + case isValueAttribute(sourceNode) && hasValueAttribute(parentNode.name) && !isProgressNode(parentNode): + return createValueExpression(sourceNode, sourceFile, sourceCode) + case isEventAttribute(sourceNode): + return createEventExpression(sourceNode, sourceFile, sourceCode) + default: + return createAttributeExpression(sourceNode, sourceFile, sourceCode) + } + } + + /** + * Create the attribute expressions + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {Array} array containing all the attribute expressions + */ + function createAttributeExpressions(sourceNode, sourceFile, sourceCode) { + return findDynamicAttributes(sourceNode) + .map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)) + } + + /** + * Create the text node expressions + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {Array} array containing all the text node expressions + */ + function createTextNodeExpressions(sourceNode, sourceFile, sourceCode) { + const childrenNodes = getChildrenNodes(sourceNode); + + return childrenNodes + .filter(isTextNode) + .filter(hasExpressions) + .map(node => createExpression( + node, + sourceFile, + sourceCode, + childrenNodes.indexOf(node), + sourceNode + )) + } + + /** + * Add a simple binding to a riot parser node + * @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an each binding node + */ + function createSimpleBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.objectExpression([ + ...createSelectorProperties(selectorAttribute), + simplePropertyNode( + BINDING_EXPRESSIONS_KEY, + builders.arrayExpression([ + ...createTextNodeExpressions(sourceNode, sourceFile, sourceCode), + ...createAttributeExpressions(sourceNode, sourceFile, sourceCode) + ]) + ) + ]) + } + + /** + * Transform a RiotParser.Node.Tag of type slot into a slot binding + * @param { RiotParser.Node.Tag } sourceNode - slot node + * @param { string } selectorAttribute - attribute needed to select the target node + * @returns { AST.Node } a slot binding node + */ + function createSlotBinding(sourceNode, selectorAttribute) { + const slotNameAttribute = findAttribute(NAME_ATTRIBUTE, sourceNode); + const slotName = slotNameAttribute ? slotNameAttribute.value : DEFAULT_SLOT_NAME; + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(SLOT_BINDING_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_NAME_KEY, + builders.literal(slotName) + ), + ...createSelectorProperties(selectorAttribute) + ]) + } + + /** + * Find the slots in the current component and group them under the same id + * @param {RiotParser.Node.Tag} sourceNode - the custom tag + * @returns {Object} object containing all the slots grouped by name + */ + function groupSlots(sourceNode) { + return getChildrenNodes(sourceNode).reduce((acc, node) => { + const slotAttribute = findSlotAttribute(node); + + if (slotAttribute) { + acc[slotAttribute.value] = node; + } else { + acc.default = createRootNode({ + nodes: [...getChildrenNodes(acc.default), node] + }); + } + + return acc + }, { + default: null + }) + } + + /** + * Create the slot entity to pass to the riot-dom bindings + * @param {string} id - slot id + * @param {RiotParser.Node.Tag} sourceNode - slot root node + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} ast node containing the slot object properties + */ + function buildSlot(id, sourceNode, sourceFile, sourceCode) { + const cloneNode = { + ...sourceNode, + // avoid to render the slot attribute + attributes: getNodeAttributes(sourceNode).filter(attribute => attribute.name !== SLOT_ATTRIBUTE) + }; + const [html, bindings] = build(cloneNode, sourceFile, sourceCode); + + return builders.objectExpression([ + simplePropertyNode(BINDING_ID_KEY, builders.literal(id)), + simplePropertyNode(BINDING_HTML_KEY, builders.literal(html)), + simplePropertyNode(BINDING_BINDINGS_KEY, builders.arrayExpression(bindings)) + ]) + } + + /** + * Create the AST array containing the slots + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.ArrayExpression} array containing the attributes to bind + */ + function createSlotsArray(sourceNode, sourceFile, sourceCode) { + return builders.arrayExpression([ + ...compose( + slots => slots.map(([key, value]) => buildSlot(key, value, sourceFile, sourceCode)), + slots => slots.filter(([,value]) => value), + Object.entries, + groupSlots + )(sourceNode) + ]) + } + + /** + * Create the AST array containing the attributes to bind to this node + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.ArrayExpression} array containing the slot objects + */ + function createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.arrayExpression([ + ...compose( + attributes => attributes.map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)), + attributes => getAttributesWithoutSelector(attributes, selectorAttribute), // eslint-disable-line + cleanAttributes + )(sourceNode) + ]) + } + + /** + * Find the slot attribute if it exists + * @param {RiotParser.Node.Tag} sourceNode - the custom tag + * @returns {RiotParser.Node.Attr|undefined} the slot attribute found + */ + function findSlotAttribute(sourceNode) { + return getNodeAttributes(sourceNode).find(attribute => attribute.name === SLOT_ATTRIBUTE) + } + + /** + * Transform a RiotParser.Node.Tag into a tag binding + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } tag binding node + */ + function createTagBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(TAG_BINDING_TYPE), + false + ), + ), + simplePropertyNode(BINDING_GET_COMPONENT_KEY, builders.identifier(GET_COMPONENT_FN)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + toScopedFunction(getCustomNodeNameAsExpression(sourceNode), sourceFile, sourceCode) + ), + simplePropertyNode(BINDING_SLOTS_KEY, createSlotsArray(sourceNode, sourceFile, sourceCode)), + simplePropertyNode( + BINDING_ATTRIBUTES_KEY, + createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) + ), + ...createSelectorProperties(selectorAttribute) + ]) + } + + const BuildingState = Object.freeze({ + html: [], + bindings: [], + parent: null + }); + + /** + * Nodes having bindings should be cloned and new selector properties should be added to them + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} bindingsSelector - temporary string to identify the current node + * @returns {RiotParser.Node} the original node parsed having the new binding selector attribute + */ + function createBindingsTag(sourceNode, bindingsSelector) { + if (!bindingsSelector) return sourceNode + + return { + ...sourceNode, + // inject the selector bindings into the node attributes + attributes: [{ + name: bindingsSelector, + value: bindingsSelector + }, ...getNodeAttributes(sourceNode)] + } + } + + /** + * Create a generic dynamic node (text or tag) and generate its bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ + function createDynamicNode(sourceNode, sourceFile, sourceCode, state) { + switch (true) { + case isTextNode(sourceNode): + // text nodes will not have any bindings + return [nodeToString(sourceNode), []] + default: + return createTagWithBindings(sourceNode, sourceFile, sourceCode) + } + } + + /** + * Create only a dynamic tag node with generating a custom selector and its bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ + function createTagWithBindings(sourceNode, sourceFile, sourceCode) { + const bindingsSelector = isRootNode(sourceNode) ? null : createBindingSelector(); + const cloneNode = createBindingsTag(sourceNode, bindingsSelector); + const tagOpeningHTML = nodeToString(cloneNode); + + switch(true) { + // EACH bindings have prio 1 + case hasEachAttribute(cloneNode): + return [tagOpeningHTML, [createEachBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // IF bindings have prio 2 + case hasIfAttribute(cloneNode): + return [tagOpeningHTML, [createIfBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // TAG bindings have prio 3 + case isCustomNode(cloneNode): + return [tagOpeningHTML, [createTagBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // slot tag + case isSlotNode(cloneNode): + return [tagOpeningHTML, [createSlotBinding(cloneNode, bindingsSelector)]] + // this node has expressions bound to it + default: + return [tagOpeningHTML, [createSimpleBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + } + } + + /** + * Parse a node trying to extract its template and bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ + function parseNode(sourceNode, sourceFile, sourceCode, state) { + // static nodes have no bindings + if (isStaticNode(sourceNode)) return [nodeToString(sourceNode), []] + return createDynamicNode(sourceNode, sourceFile, sourceCode) + } + + /** + * Create the tag binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @param { string } selector - binding selector + * @returns { Array } array with only the tag binding AST + */ + function createNestedBindings(sourceNode, sourceFile, sourceCode, selector) { + const mightBeARiotComponent = isCustomNode(sourceNode); + + return mightBeARiotComponent ? [null, [ + createTagBinding( + cloneNodeWithoutSelectorAttribute(sourceNode, selector), + null, + sourceFile, + sourceCode + )] + ] : build(createRootNode(sourceNode), sourceFile, sourceCode) + } + + /** + * Build the template and the bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and the dom bindings + */ + function build( + sourceNode, + sourceFile, + sourceCode, + state + ) { + if (!sourceNode) panic('Something went wrong with your tag DOM parsing, your tag template can\'t be created'); + + const [nodeHTML, nodeBindings] = parseNode(sourceNode, sourceFile, sourceCode); + const childrenNodes = getChildrenNodes(sourceNode); + const currentState = { ...cloneDeep(BuildingState), ...state }; + + // mutate the original arrays + currentState.html.push(...nodeHTML); + currentState.bindings.push(...nodeBindings); + + // do recursion if + // this tag has children and it has no special directives bound to it + if (childrenNodes.length && !hasItsOwnTemplate(sourceNode)) { + childrenNodes.forEach(node => build(node, sourceFile, sourceCode, { parent: sourceNode, ...currentState })); + } + + // close the tag if it's not a void one + if (isTagNode(sourceNode) && !isVoidNode(sourceNode)) { + currentState.html.push(closeTag(sourceNode)); + } + + return [ + currentState.html.join(''), + currentState.bindings + ] + } + + const templateFunctionArguments = [ + TEMPLATE_FN, + EXPRESSION_TYPES, + BINDING_TYPES, + GET_COMPONENT_FN + ].map(builders.identifier); + + /** + * Create the content of the template function + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.BlockStatement} the content of the template function + */ + function createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) { + return builders.blockStatement([ + builders.returnStatement( + callTemplateFunction( + ...build( + createRootNode(sourceNode), + sourceFile, + sourceCode + ) + ) + ) + ]) + } + + /** + * Extend the AST adding the new template property containing our template call to render the component + * @param { Object } ast - current output ast + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @returns { Object } the output ast having the "template" key + */ + function extendTemplateProperty(ast, sourceFile, sourceCode, sourceNode) { + types$1.visit(ast, { + visitProperty(path) { + if (path.value.key.value === TAG_TEMPLATE_PROPERTY) { + path.value.value = builders.functionExpression( + null, + templateFunctionArguments, + createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) + ); + + return false + } + + this.traverse(path); + } + }); + + return ast + } + + /** + * Generate the component template logic + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @param { string } source - original component source code + * @param { Object } meta - compilation meta information + * @param { AST } ast - current AST output + * @returns { AST } the AST generated + */ + function template(sourceNode, source, meta, ast) { + const { options } = meta; + return extendTemplateProperty(ast, options.file, source, sourceNode) + } + + const DEFAULT_OPTIONS = { + template: 'default', + file: '[unknown-source-file]', + scopedCss: true + }; + + /** + * Create the initial AST + * @param {string} tagName - the name of the component we have compiled + * @returns { AST } the initial AST + * + * @example + * // the output represents the following string in AST + */ + function createInitialInput({tagName}) { + /* + generates + export default { + ${TAG_CSS_PROPERTY}: null, + ${TAG_LOGIC_PROPERTY}: null, + ${TAG_TEMPLATE_PROPERTY}: null + } + */ + return builders.program([ + builders.exportDefaultDeclaration( + builders.objectExpression([ + simplePropertyNode(TAG_CSS_PROPERTY, nullNode()), + simplePropertyNode(TAG_LOGIC_PROPERTY, nullNode()), + simplePropertyNode(TAG_TEMPLATE_PROPERTY, nullNode()), + simplePropertyNode(TAG_NAME_PROPERTY, builders.literal(tagName)) + ]) + )] + ) + } + + /** + * Make sure the input sourcemap is valid otherwise we ignore it + * @param {SourceMapGenerator} map - preprocessor source map + * @returns {Object} sourcemap as json or nothing + */ + function normaliseInputSourceMap(map) { + const inputSourceMap = sourcemapAsJSON(map); + return isEmptySourcemap(inputSourceMap) ? null : inputSourceMap + } + + /** + * Override the sourcemap content making sure it will always contain the tag source code + * @param {Object} map - sourcemap as json + * @param {string} source - component source code + * @returns {Object} original source map with the "sourcesContent" property overriden + */ + function overrideSourcemapContent(map, source) { + return { + ...map, + sourcesContent: [source] + } + } + + /** + * Create the compilation meta object + * @param { string } source - source code of the tag we will need to compile + * @param { string } options - compiling options + * @returns {Object} meta object + */ + function createMeta(source, options) { + return { + tagName: null, + fragments: null, + options: { + ...DEFAULT_OPTIONS, + ...options + }, + source + } + } + + /** + * Generate the output code source together with the sourcemap + * @param { string } source - source code of the tag we will need to compile + * @param { string } opts - compiling options + * @returns { Output } object containing output code and source map + */ + function compile(source, opts = {}) { + const meta = createMeta(source, opts); + const {options} = meta; + const { code, map } = execute$1('template', options.template, meta, source); + const { template: template$1, css: css$1, javascript: javascript$1 } = parser$1(options).parse(code).output; + + // extend the meta object with the result of the parsing + Object.assign(meta, { + tagName: template$1.name, + fragments: { template: template$1, css: css$1, javascript: javascript$1 } + }); + + return compose( + result => ({ ...result, meta }), + result => execute(result, meta), + result => ({ + ...result, + map: overrideSourcemapContent(result.map, source) + }), + ast => meta.ast = ast && generateJavascript(ast, { + sourceMapName: `${options.file}.map`, + inputSourceMap: normaliseInputSourceMap(map) + }), + hookGenerator(template, template$1, code, meta), + hookGenerator(javascript, javascript$1, code, meta), + hookGenerator(css, css$1, code, meta), + )(createInitialInput(meta)) + } + + /** + * Prepare the riot parser node transformers + * @param { Function } transformer - transformer function + * @param { Object } sourceNode - riot parser node + * @param { string } source - component source code + * @param { Object } meta - compilation meta information + * @returns { Promise } object containing output code and source map + */ + function hookGenerator(transformer, sourceNode, source, meta) { + if ( + // filter missing nodes + !sourceNode || + // filter nodes without children + (sourceNode.nodes && !sourceNode.nodes.length) || + // filter empty javascript and css nodes + (!sourceNode.nodes && !sourceNode.text)) { + return result => result + } + + return curry(transformer)(sourceNode, source, meta) + } + + // This function can be used to register new preprocessors + // a preprocessor can target either only the css or javascript nodes + // or the complete tag source file ('template') + const registerPreprocessor = register$1; + + // This function can allow you to register postprocessors that will parse the output code + // here we can run prettifiers, eslint fixes... + const registerPostprocessor = register; + + exports.compile = compile; + exports.createInitialInput = createInitialInput; + exports.registerPostprocessor = registerPostprocessor; + exports.registerPreprocessor = registerPreprocessor; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/node_modules/@riotjs/compiler/dist/index.esm.js b/node_modules/@riotjs/compiler/dist/index.esm.js new file mode 100644 index 0000000..fc58b59 --- /dev/null +++ b/node_modules/@riotjs/compiler/dist/index.esm.js @@ -0,0 +1,2157 @@ +/* Riot Compiler WIP, @license MIT */ +import { types as types$1, print, parse } from 'recast'; +import { composeSourceMaps } from 'recast/lib/util'; +import { SourceMapGenerator } from 'source-map'; +import compose from 'cumpa'; +import cssEscape from 'cssesc'; +import curry from 'curri'; +import { Parser } from 'acorn'; +import globalScope from 'globals'; +import riotParser, { nodeTypes } from '@riotjs/parser'; +import { hasValueAttribute } from 'dom-nodes'; + +const TAG_LOGIC_PROPERTY = 'exports'; +const TAG_CSS_PROPERTY = 'css'; +const TAG_TEMPLATE_PROPERTY = 'template'; +const TAG_NAME_PROPERTY = 'name'; + +const types = types$1; +const builders = types$1.builders; +const namedTypes = types$1.namedTypes; + +function nullNode() { + return builders.literal(null) +} + +function simplePropertyNode(key, value) { + return builders.property('init', builders.literal(key), value, false) +} + +/** + * Return a source map as JSON, it it has not the toJSON method it means it can + * be used right the way + * @param { SourceMapGenerator|Object } map - a sourcemap generator or simply an json object + * @returns { Object } the source map as JSON + */ +function sourcemapAsJSON(map) { + if (map && map.toJSON) return map.toJSON() + return map +} + +/** + * Detect node js environements + * @returns { boolean } true if the runtime is node + */ +function isNode() { + return typeof process !== 'undefined' +} + +/** + * Compose two sourcemaps + * @param { SourceMapGenerator } formerMap - original sourcemap + * @param { SourceMapGenerator } latterMap - target sourcemap + * @returns { Object } sourcemap json + */ +function composeSourcemaps(formerMap, latterMap) { + if ( + isNode() && + formerMap && latterMap && latterMap.mappings + ) { + return composeSourceMaps(sourcemapAsJSON(formerMap), sourcemapAsJSON(latterMap)) + } else if (isNode() && formerMap) { + return sourcemapAsJSON(formerMap) + } + + return {} +} + +/** + * Create a new sourcemap generator + * @param { Object } options - sourcemap options + * @returns { SourceMapGenerator } SourceMapGenerator instance + */ +function createSourcemap(options) { + return new SourceMapGenerator(options) +} + +const Output = Object.freeze({ + code: '', + ast: [], + meta: {}, + map: null +}); + +/** + * Create the right output data result of a parsing + * @param { Object } data - output data + * @param { string } data.code - code generated + * @param { AST } data.ast - ast representing the code + * @param { SourceMapGenerator } data.map - source map generated along with the code + * @param { Object } meta - compilation meta infomration + * @returns { Output } output container object + */ +function createOutput(data, meta) { + const output = { + ...Output, + ...data, + meta + }; + + if (!output.map && meta && meta.options && meta.options.file) + return { + ...output, + map: createSourcemap({ file: meta.options.file }) + } + + return output +} + +/** + * Transform the source code received via a compiler function + * @param { Function } compiler - function needed to generate the output code + * @param { Object } meta - compilation meta information + * @param { string } source - source code + * @returns { Output } output - the result of the compiler + */ +function transform(compiler, meta, source) { + const result = (compiler ? compiler(source, meta) : { code: source }); + return createOutput(result, meta) +} + +/** + * Throw an error with a descriptive message + * @param { string } message - error message + * @returns { undefined } hoppla.. at this point the program should stop working + */ +function panic(message) { + throw new Error(message) +} + +const postprocessors = new Set(); + +/** + * Register a postprocessor that will be used after the parsing and compilation of the riot tags + * @param { Function } postprocessor - transformer that will receive the output code ans sourcemap + * @returns { Set } the postprocessors collection + */ +function register(postprocessor) { + if (postprocessors.has(postprocessor)) { + panic(`This postprocessor "${postprocessor.name || postprocessor.toString()}" was already registered`); + } + + postprocessors.add(postprocessor); + + return postprocessors +} + +/** + * Exec all the postprocessors in sequence combining the sourcemaps generated + * @param { Output } compilerOutput - output generated by the compiler + * @param { Object } meta - compiling meta information + * @returns { Output } object containing output code and source map + */ +function execute(compilerOutput, meta) { + return Array.from(postprocessors).reduce(function(acc, postprocessor) { + const { code, map } = acc; + const output = postprocessor(code, meta); + + return { + code: output.code, + map: composeSourcemaps(map, output.map) + } + }, createOutput(compilerOutput, meta)) +} + +/** + * Parsers that can be registered by users to preparse components fragments + * @type { Object } + */ +const preprocessors = Object.freeze({ + javascript: new Map(), + css: new Map(), + template: new Map().set('default', code => ({ code })) +}); + +// throw a processor type error +function preprocessorTypeError(type) { + panic(`No preprocessor of type "${type}" was found, please make sure to use one of these: 'javascript', 'css' or 'template'`); +} + +// throw an error if the preprocessor was not registered +function preprocessorNameNotFoundError(name) { + panic(`No preprocessor named "${name}" was found, are you sure you have registered it?'`); +} + +/** + * Register a custom preprocessor + * @param { string } type - preprocessor type either 'js', 'css' or 'template' + * @param { string } name - unique preprocessor id + * @param { Function } preprocessor - preprocessor function + * @returns { Map } - the preprocessors map + */ +function register$1(type, name, preprocessor) { + if (!type) panic('Please define the type of preprocessor you want to register \'javascript\', \'css\' or \'template\''); + if (!name) panic('Please define a name for your preprocessor'); + if (!preprocessor) panic('Please provide a preprocessor function'); + if (!preprocessors[type]) preprocessorTypeError(type); + if (preprocessors[type].has(name)) panic(`The preprocessor ${name} was already registered before`); + + preprocessors[type].set(name, preprocessor); + + return preprocessors +} + +/** + * Exec the compilation of a preprocessor + * @param { string } type - preprocessor type either 'js', 'css' or 'template' + * @param { string } name - unique preprocessor id + * @param { Object } meta - preprocessor meta information + * @param { string } source - source code + * @returns { Output } object containing a sourcemap and a code string + */ +function execute$1(type, name, meta, source) { + if (!preprocessors[type]) preprocessorTypeError(type); + if (!preprocessors[type].has(name)) preprocessorNameNotFoundError(name); + + return transform(preprocessors[type].get(name), meta, source) +} + +const ATTRIBUTE_TYPE_NAME = 'type'; + +/** + * Get the type attribute from a node generated by the riot parser + * @param { Object} sourceNode - riot parser node + * @returns { string|null } a valid type to identify the preprocessor to use or nothing + */ +function getPreprocessorTypeByAttribute(sourceNode) { + const typeAttribute = sourceNode.attributes ? + sourceNode.attributes.find(attribute => attribute.name === ATTRIBUTE_TYPE_NAME) : + null; + + return typeAttribute ? normalize(typeAttribute.value) : null +} + + +/** + * Remove the noise in case a user has defined the preprocessor type='text/scss' + * @param { string } value - input string + * @returns { string } normalized string + */ +function normalize(value) { + return value.replace('text/', '') +} + +/** + * Preprocess a riot parser node + * @param { string } preprocessorType - either css, js + * @param { string } preprocessorName - preprocessor id + * @param { Object } meta - compilation meta information + * @param { RiotParser.nodeTypes } node - css node detected by the parser + * @returns { Output } code and sourcemap generated by the preprocessor + */ +function preprocess(preprocessorType, preprocessorName, meta, node) { + const code = node.text; + + return (preprocessorName ? + execute$1(preprocessorType, preprocessorName, meta, code) : + { code } + ) +} + +/** + * Matches valid, multiline JavaScript comments in almost all its forms. + * @const {RegExp} + * @static + */ +const R_MLCOMMS = /\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//g; + +/** + * Source for creating regexes matching valid quoted, single-line JavaScript strings. + * It recognizes escape characters, including nested quotes and line continuation. + * @const {string} + */ +const S_LINESTR = /"[^"\n\\]*(?:\\[\S\s][^"\n\\]*)*"|'[^'\n\\]*(?:\\[\S\s][^'\n\\]*)*'/.source; + +/** + * Matches CSS selectors, excluding those beginning with '@' and quoted strings. + * @const {RegExp} + */ + +const CSS_SELECTOR = RegExp(`([{}]|^)[; ]*((?:[^@ ;{}][^{}]*)?[^@ ;{}:] ?)(?={)|${S_LINESTR}`, 'g'); + +/** + * Parses styles enclosed in a "scoped" tag + * The "css" string is received without comments or surrounding spaces. + * + * @param {string} tag - Tag name of the root element + * @param {string} css - The CSS code + * @returns {string} CSS with the styles scoped to the root element + */ +function scopedCSS(tag, css) { + const host = ':host'; + const selectorsBlacklist = ['from', 'to']; + + return css.replace(CSS_SELECTOR, function(m, p1, p2) { + // skip quoted strings + if (!p2) return m + + // we have a selector list, parse each individually + p2 = p2.replace(/[^,]+/g, function(sel) { + const s = sel.trim(); + + // skip selectors already using the tag name + if (s.indexOf(tag) === 0) { + return sel + } + + // skips the keywords and percents of css animations + if (!s || selectorsBlacklist.indexOf(s) > -1 || s.slice(-1) === '%') { + return sel + } + + // replace the `:host` pseudo-selector, where it is, with the root tag name; + // if `:host` was not included, add the tag name as prefix, and mirror all + // `[data-is]` + if (s.indexOf(host) < 0) { + return `${tag} ${s},[is="${tag}"] ${s}` + } else { + return `${s.replace(host, tag)},${ + s.replace(host, `[is="${tag}"]`)}` + } + }); + + // add the danling bracket char and return the processed selector list + return p1 ? `${p1} ${p2}` : p2 + }) +} + +/** + * Remove comments, compact and trim whitespace + * @param { string } code - compiled css code + * @returns { string } css code normalized + */ +function compactCss(code) { + return code.replace(R_MLCOMMS, '').replace(/\s+/g, ' ').trim() +} + +const escapeBackslashes = s => s.replace(/\\/g, '\\\\'); +const escapeIdentifier = identifier => escapeBackslashes(cssEscape(identifier, { + isIdentifier: true +})); + +/** + * Generate the component css + * @param { Object } sourceNode - node generated by the riot compiler + * @param { string } source - original component source code + * @param { Object } meta - compilation meta information + * @param { AST } ast - current AST output + * @returns { AST } the AST generated + */ +function css(sourceNode, source, meta, ast) { + const preprocessorName = getPreprocessorTypeByAttribute(sourceNode); + const { options } = meta; + const preprocessorOutput = preprocess('css', preprocessorName, meta, sourceNode.text); + const normalizedCssCode = compactCss(preprocessorOutput.code); + const escapedCssIdentifier = escapeIdentifier(meta.tagName); + + const cssCode = (options.scopedCss ? + scopedCSS(escapedCssIdentifier, escapeBackslashes(normalizedCssCode)) : + escapeBackslashes(normalizedCssCode) + ).trim(); + + types.visit(ast, { + visitProperty(path) { + if (path.value.key.value === TAG_CSS_PROPERTY) { + path.value.value = builders.templateLiteral( + [builders.templateElement({ raw: cssCode, cooked: '' }, false)], + [] + ); + + return false + } + + this.traverse(path); + } + }); + + return ast +} + +/** + * Generate the javascript from an ast source + * @param {AST} ast - ast object + * @param {Object} options - printer options + * @returns {Object} code + map + */ +function generateJavascript(ast, options) { + return print(ast, { + ...options, + tabWidth: 2, + quote: 'single' + }) +} + +/** + * True if the sourcemap has no mappings, it is empty + * @param {Object} map - sourcemap json + * @returns {boolean} true if empty + */ +function isEmptySourcemap(map) { + return !map || !map.mappings || !map.mappings.length +} + +const LINES_RE = /\r\n?|\n/g; + +/** + * Split a string into a rows array generated from its EOL matches + * @param { string } string [description] + * @returns { Array } array containing all the string rows + */ +function splitStringByEOL(string) { + return string.split(LINES_RE) +} + +/** + * Get the line and the column of a source text based on its position in the string + * @param { string } string - target string + * @param { number } position - target position + * @returns { Object } object containing the source text line and column + */ +function getLineAndColumnByPosition(string, position) { + const lines = splitStringByEOL(string.slice(0, position)); + + return { + line: lines.length, + column: lines[lines.length - 1].length + } +} + +/** + * Add the offset to the code that must be parsed in order to generate properly the sourcemaps + * @param {string} input - input string + * @param {string} source - original source code + * @param {RiotParser.Node} node - node that we are going to transform + * @return {string} the input string with the offset properly set + */ +function addLineOffset(input, source, node) { + const {column, line} = getLineAndColumnByPosition(source, node.start); + return `${'\n'.repeat(line - 1)}${' '.repeat(column + 1)}${input}` +} + +/** + * Parse a js source to generate the AST + * @param {string} source - javascript source + * @param {Object} options - parser options + * @returns {AST} AST tree + */ +function generateAST(source, options) { + return parse(source, { + parser: { + parse(source, opts) { + return Parser.parse(source, { + ...opts, + ecmaVersion: 2020 + }) + } + }, + ...options + }) +} + +const browserAPIs = Object.keys(globalScope.browser); +const builtinAPIs = Object.keys(globalScope.builtin); + +const isIdentifier = namedTypes.Identifier.check.bind(namedTypes.Identifier); +const isLiteral = namedTypes.Literal.check.bind(namedTypes.Literal); +const isExpressionStatement = namedTypes.ExpressionStatement.check.bind(namedTypes.ExpressionStatement); +const isObjectExpression = namedTypes.ObjectExpression.check.bind(namedTypes.ObjectExpression); +const isThisExpression = namedTypes.ThisExpression.check.bind(namedTypes.ThisExpression); +const isNewExpression = namedTypes.NewExpression.check.bind(namedTypes.NewExpression); +const isSequenceExpression = namedTypes.SequenceExpression.check.bind(namedTypes.SequenceExpression); +const isBinaryExpression = namedTypes.BinaryExpression.check.bind(namedTypes.BinaryExpression); +const isExportDefaultStatement = namedTypes.ExportDefaultDeclaration.check.bind(namedTypes.ExportDefaultDeclaration); + +const isBrowserAPI = ({name}) => browserAPIs.includes(name); +const isBuiltinAPI = ({name}) => builtinAPIs.includes(name); +const isRaw = (node) => node && node.raw; // eslint-disable-line + +/** + * Find the export default statement + * @param { Array } body - tree structure containing the program code + * @returns { Object } node containing only the code of the export default statement + */ +function findExportDefaultStatement(body) { + return body.find(isExportDefaultStatement) +} + +/** + * Find all the code in an ast program except for the export default statements + * @param { Array } body - tree structure containing the program code + * @returns { Array } array containing all the program code except the export default expressions + */ +function filterNonExportDefaultStatements(body) { + return body.filter(node => !isExportDefaultStatement(node)) +} + +/** + * Get the body of the AST structure + * @param { Object } ast - ast object generated by recast + * @returns { Array } array containing the program code + */ +function getProgramBody(ast) { + return ast.body || ast.program.body +} + +/** + * Extend the AST adding the new tag method containing our tag sourcecode + * @param { Object } ast - current output ast + * @param { Object } exportDefaultNode - tag export default node + * @returns { Object } the output ast having the "tag" key extended with the content of the export default + */ +function extendTagProperty(ast, exportDefaultNode) { + types.visit(ast, { + visitProperty(path) { + if (path.value.key.value === TAG_LOGIC_PROPERTY) { + path.value.value = exportDefaultNode.declaration; + return false + } + + this.traverse(path); + } + }); + + return ast +} + +/** + * Generate the component javascript logic + * @param { Object } sourceNode - node generated by the riot compiler + * @param { string } source - original component source code + * @param { Object } meta - compilation meta information + * @param { AST } ast - current AST output + * @returns { AST } the AST generated + */ +function javascript(sourceNode, source, meta, ast) { + const preprocessorName = getPreprocessorTypeByAttribute(sourceNode); + const javascriptNode = addLineOffset(sourceNode.text.text, source, sourceNode); + const { options } = meta; + const preprocessorOutput = preprocess('javascript', preprocessorName, meta, { + ...sourceNode, + text: javascriptNode + }); + const inputSourceMap = sourcemapAsJSON(preprocessorOutput.map); + const generatedAst = generateAST(preprocessorOutput.code, { + sourceFileName: options.file, + inputSourceMap: isEmptySourcemap(inputSourceMap) ? null : inputSourceMap + }); + const generatedAstBody = getProgramBody(generatedAst); + const bodyWithoutExportDefault = filterNonExportDefaultStatements(generatedAstBody); + const exportDefaultNode = findExportDefaultStatement(generatedAstBody); + const outputBody = getProgramBody(ast); + + // add to the ast the "private" javascript content of our tag script node + outputBody.unshift(...bodyWithoutExportDefault); + + // convert the export default adding its content to the "tag" property exported + if (exportDefaultNode) extendTagProperty(ast, exportDefaultNode); + + return ast +} + +// import {IS_BOOLEAN,IS_CUSTOM,IS_RAW,IS_SPREAD,IS_VOID} from '@riotjs/parser/src/constants' + +const BINDING_TYPES = 'bindingTypes'; +const EACH_BINDING_TYPE = 'EACH'; +const IF_BINDING_TYPE = 'IF'; +const TAG_BINDING_TYPE = 'TAG'; +const SLOT_BINDING_TYPE = 'SLOT'; + + +const EXPRESSION_TYPES = 'expressionTypes'; +const ATTRIBUTE_EXPRESSION_TYPE = 'ATTRIBUTE'; +const VALUE_EXPRESSION_TYPE = 'VALUE'; +const TEXT_EXPRESSION_TYPE = 'TEXT'; +const EVENT_EXPRESSION_TYPE = 'EVENT'; + +const TEMPLATE_FN = 'template'; +const SCOPE = 'scope'; +const GET_COMPONENT_FN = 'getComponent'; + +// keys needed to create the DOM bindings +const BINDING_SELECTOR_KEY = 'selector'; +const BINDING_GET_COMPONENT_KEY = 'getComponent'; +const BINDING_TEMPLATE_KEY = 'template'; +const BINDING_TYPE_KEY = 'type'; +const BINDING_REDUNDANT_ATTRIBUTE_KEY = 'redundantAttribute'; +const BINDING_CONDITION_KEY = 'condition'; +const BINDING_ITEM_NAME_KEY = 'itemName'; +const BINDING_GET_KEY_KEY = 'getKey'; +const BINDING_INDEX_NAME_KEY = 'indexName'; +const BINDING_EVALUATE_KEY = 'evaluate'; +const BINDING_NAME_KEY = 'name'; +const BINDING_SLOTS_KEY = 'slots'; +const BINDING_EXPRESSIONS_KEY = 'expressions'; +const BINDING_CHILD_NODE_INDEX_KEY = 'childNodeIndex'; +// slots keys +const BINDING_BINDINGS_KEY = 'bindings'; +const BINDING_ID_KEY = 'id'; +const BINDING_HTML_KEY = 'html'; +const BINDING_ATTRIBUTES_KEY = 'attributes'; + +// DOM directives +const IF_DIRECTIVE = 'if'; +const EACH_DIRECTIVE = 'each'; +const KEY_ATTRIBUTE = 'key'; +const SLOT_ATTRIBUTE = 'slot'; +const NAME_ATTRIBUTE = 'name'; +const IS_DIRECTIVE = 'is'; + +// Misc +const DEFAULT_SLOT_NAME = 'default'; +const TEXT_NODE_EXPRESSION_PLACEHOLDER = ''; +const BINDING_SELECTOR_PREFIX = 'expr'; +const SLOT_TAG_NODE_NAME = 'slot'; +const PROGRESS_TAG_NODE_NAME = 'progress'; +const IS_VOID_NODE = 'isVoid'; +const IS_CUSTOM_NODE = 'isCustom'; +const IS_BOOLEAN_ATTRIBUTE = 'isBoolean'; +const IS_SPREAD_ATTRIBUTE = 'isSpread'; + +/** + * True if the node has not expression set nor bindings directives + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only if it's a static node that doesn't need bindings or expressions + */ +function isStaticNode(node) { + return [ + hasExpressions, + findEachAttribute, + findIfAttribute, + isCustomNode, + isSlotNode + ].every(test => !test(node)) +} + +/** + * Check if a node name is part of the browser or builtin javascript api or it belongs to the current scope + * @param { types.NodePath } path - containing the current node visited + * @returns {boolean} true if it's a global api variable + */ +function isGlobal({ scope, node }) { + return Boolean( + isRaw(node) || + isBuiltinAPI(node) || + isBrowserAPI(node) || + isNewExpression(node) || + isNodeInScope(scope, node), + ) +} + +/** + * Checks if the identifier of a given node exists in a scope + * @param {Scope} scope - scope where to search for the identifier + * @param {types.Node} node - node to search for the identifier + * @returns {boolean} true if the node identifier is defined in the given scope + */ +function isNodeInScope(scope, node) { + const traverse = (isInScope = false) => { + types.visit(node, { + visitIdentifier(path) { + if (scope.lookup(getName(path.node))) { + isInScope = true; + } + + this.abort(); + } + }); + + return isInScope + }; + + return traverse() +} + +/** + * True if the node has the isCustom attribute set + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if either it's a riot component or a custom element + */ +function isCustomNode(node) { + return !!(node[IS_CUSTOM_NODE] || hasIsAttribute(node)) +} + +/** + * True the node is + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if it's a slot node + */ +function isSlotNode(node) { + return node.name === SLOT_TAG_NODE_NAME +} + +/** + * True if the node has the isVoid attribute set + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if the node is self closing + */ +function isVoidNode(node) { + return !!node[IS_VOID_NODE] +} + +/** + * True if the riot parser did find a tag node + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the tag nodes + */ +function isTagNode(node) { + return node.type === nodeTypes.TAG +} + +/** + * True if the riot parser did find a text node + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the text nodes + */ +function isTextNode(node) { + return node.type === nodeTypes.TEXT +} + +/** + * True if the node parsed is the root one + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the root nodes + */ +function isRootNode(node) { + return node.isRoot +} + +/** + * True if the attribute parsed is of type spread one + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if the attribute node is of type spread + */ +function isSpreadAttribute(node) { + return node[IS_SPREAD_ATTRIBUTE] +} + +/** + * True if the node is an attribute and its name is "value" + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for value attribute nodes + */ +function isValueAttribute(node) { + return node.name === 'value' +} + +/** + * True if the DOM node is a progress tag + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true for the progress tags + */ +function isProgressNode(node) { + return node.name === PROGRESS_TAG_NODE_NAME +} + +/** + * True if the node is an attribute and a DOM handler + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for dom listener attribute nodes + */ +const isEventAttribute = (() => { + const EVENT_ATTR_RE = /^on/; + return node => EVENT_ATTR_RE.test(node.name) +})(); + +/** + * True if the node has expressions or expression attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} ditto + */ +function hasExpressions(node) { + return !!( + node.expressions || + // has expression attributes + (getNodeAttributes(node).some(attribute => hasExpressions(attribute))) || + // has child text nodes with expressions + (node.nodes && node.nodes.some(node => isTextNode(node) && hasExpressions(node))) + ) +} + +/** + * True if the node is a directive having its own template + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the IF EACH and TAG bindings + */ +function hasItsOwnTemplate(node) { + return [ + findEachAttribute, + findIfAttribute, + isCustomNode + ].some(test => test(node)) +} + +const hasIfAttribute = compose(Boolean, findIfAttribute); +const hasEachAttribute = compose(Boolean, findEachAttribute); +const hasIsAttribute = compose(Boolean, findIsAttribute); +const hasKeyAttribute = compose(Boolean, findKeyAttribute); + +/** + * Find the attribute node + * @param { string } name - name of the attribute we want to find + * @param { riotParser.nodeTypes.TAG } node - a tag node + * @returns { riotParser.nodeTypes.ATTR } attribute node + */ +function findAttribute(name, node) { + return node.attributes && node.attributes.find(attr => getName(attr) === name) +} + +function findIfAttribute(node) { + return findAttribute(IF_DIRECTIVE, node) +} + +function findEachAttribute(node) { + return findAttribute(EACH_DIRECTIVE, node) +} + +function findKeyAttribute(node) { + return findAttribute(KEY_ATTRIBUTE, node) +} + +function findIsAttribute(node) { + return findAttribute(IS_DIRECTIVE, node) +} + +/** + * Find all the node attributes that are not expressions + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} list of all the static attributes + */ +function findStaticAttributes(node) { + return getNodeAttributes(node).filter(attribute => !hasExpressions(attribute)) +} + +/** + * Find all the node attributes that have expressions + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} list of all the dynamic attributes + */ +function findDynamicAttributes(node) { + return getNodeAttributes(node).filter(hasExpressions) +} + +/** + * Unescape the user escaped chars + * @param {string} string - input string + * @param {string} char - probably a '{' or anything the user want's to escape + * @returns {string} cleaned up string + */ +function unescapeChar(string, char) { + return string.replace(RegExp(`\\\\${char}`, 'gm'), char) +} + +const scope = builders.identifier(SCOPE); +const getName = node => node && node.name ? node.name : node; + +/** + * Replace the path scope with a member Expression + * @param { types.NodePath } path - containing the current node visited + * @param { types.Node } property - node we want to prefix with the scope identifier + * @returns {undefined} this is a void function + */ +function replacePathScope(path, property) { + path.replace(builders.memberExpression( + scope, + property, + false + )); +} + +/** + * Change the nodes scope adding the `scope` prefix + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return false if we want to stop the tree traversal + * @context { types.visit } + */ +function updateNodeScope(path) { + if (!isGlobal(path)) { + replacePathScope(path, path.node); + + return false + } + + this.traverse(path); +} + +/** + * Change the scope of the member expressions + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return always false because we want to check only the first node object + */ +function visitMemberExpression(path) { + if (!isGlobal(path) && !isGlobal({ node: path.node.object, scope: path.scope })) { + if (path.value.computed) { + this.traverse(path); + } else if (isBinaryExpression(path.node.object) || path.node.object.computed) { + this.traverse(path.get('object')); + } else if (!path.node.object.callee) { + replacePathScope(path, isThisExpression(path.node.object) ? path.node.property : path.node); + } else { + this.traverse(path.get('object')); + } + } + + return false +} + + +/** + * Objects properties should be handled a bit differently from the Identifier + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return false if we want to stop the tree traversal + */ +function visitProperty(path) { + const value = path.node.value; + + if (isIdentifier(value)) { + updateNodeScope(path.get('value')); + } else { + this.traverse(path.get('value')); + } + + return false +} + +/** + * The this expressions should be replaced with the scope + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean|undefined } return false if we want to stop the tree traversal + */ +function visitThisExpression(path) { + path.replace(scope); + this.traverse(path); +} + + +/** + * Update the scope of the global nodes + * @param { Object } ast - ast program + * @returns { Object } the ast program with all the global nodes updated + */ +function updateNodesScope(ast) { + const ignorePath = () => false; + + types.visit(ast, { + visitIdentifier: updateNodeScope, + visitMemberExpression, + visitProperty, + visitThisExpression, + visitClassExpression: ignorePath + }); + + return ast +} + +/** + * Convert any expression to an AST tree + * @param { Object } expression - expression parsed by the riot parser + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Object } the ast generated + */ +function createASTFromExpression(expression, sourceFile, sourceCode) { + const code = sourceFile ? + addLineOffset(expression.text, sourceCode, expression) : + expression.text; + + return generateAST(`(${code})`, { + sourceFileName: sourceFile + }) +} + +/** + * Create the bindings template property + * @param {Array} args - arguments to pass to the template function + * @returns {ASTNode} a binding template key + */ +function createTemplateProperty(args) { + return simplePropertyNode( + BINDING_TEMPLATE_KEY, + args ? callTemplateFunction(...args) : nullNode() + ) +} + +/** + * Try to get the expression of an attribute node + * @param { RiotParser.Node.Attribute } attribute - riot parser attribute node + * @returns { RiotParser.Node.Expression } attribute expression value + */ +function getAttributeExpression(attribute) { + return attribute.expressions ? attribute.expressions[0] : { + // if no expression was found try to typecast the attribute value + ...attribute, + text: attribute.value + } +} + +/** + * Wrap the ast generated in a function call providing the scope argument + * @param {Object} ast - function body + * @returns {FunctionExpresion} function having the scope argument injected + */ +function wrapASTInFunctionWithScope(ast) { + return builders.functionExpression( + null, + [scope], + builders.blockStatement([builders.returnStatement( + ast + )]) + ) +} + +/** + * Convert any parser option to a valid template one + * @param { RiotParser.Node.Expression } expression - expression parsed by the riot parser + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Object } a FunctionExpression object + * + * @example + * toScopedFunction('foo + bar') // scope.foo + scope.bar + * + * @example + * toScopedFunction('foo.baz + bar') // scope.foo.baz + scope.bar + */ +function toScopedFunction(expression, sourceFile, sourceCode) { + return compose( + wrapASTInFunctionWithScope, + transformExpression, + )(expression, sourceFile, sourceCode) +} + +/** + * Transform an expression node updating its global scope + * @param {RiotParser.Node.Expr} expression - riot parser expression node + * @param {string} sourceFile - source file + * @param {string} sourceCode - source code + * @returns {ASTExpression} ast expression generated from the riot parser expression node + */ +function transformExpression(expression, sourceFile, sourceCode) { + return compose( + getExpressionAST, + updateNodesScope, + createASTFromExpression + )(expression, sourceFile, sourceCode) +} + +/** + * Get the parsed AST expression of riot expression node + * @param {AST.Program} sourceAST - raw node parsed + * @returns {AST.Expression} program expression output + */ +function getExpressionAST(sourceAST) { + const astBody = sourceAST.program.body; + + return astBody[0] ? astBody[0].expression : astBody +} + +/** + * Create the template call function + * @param {Array|string|Node.Literal} template - template string + * @param {Array} bindings - template bindings provided as AST nodes + * @returns {Node.CallExpression} template call expression + */ +function callTemplateFunction(template, bindings) { + return builders.callExpression(builders.identifier(TEMPLATE_FN), [ + template ? builders.literal(template) : nullNode(), + bindings ? builders.arrayExpression(bindings) : nullNode() + ]) +} + +/** + * Convert any DOM attribute into a valid DOM selector useful for the querySelector API + * @param { string } attributeName - name of the attribute to query + * @returns { string } the attribute transformed to a query selector + */ +const attributeNameToDOMQuerySelector = attributeName => `[${attributeName}]`; + +/** + * Create the properties to query a DOM node + * @param { string } attributeName - attribute name needed to identify a DOM node + * @returns { Array } array containing the selector properties needed for the binding + */ +function createSelectorProperties(attributeName) { + return attributeName ? [ + simplePropertyNode(BINDING_REDUNDANT_ATTRIBUTE_KEY, builders.literal(attributeName)), + simplePropertyNode(BINDING_SELECTOR_KEY, + compose(builders.literal, attributeNameToDOMQuerySelector)(attributeName) + ) + ] : [] +} + +/** + * Clone the node filtering out the selector attribute from the attributes list + * @param {RiotParser.Node} node - riot parser node + * @param {string} selectorAttribute - name of the selector attribute to filter out + * @returns {RiotParser.Node} the node with the attribute cleaned up + */ +function cloneNodeWithoutSelectorAttribute(node, selectorAttribute) { + return { + ...node, + attributes: getAttributesWithoutSelector(getNodeAttributes(node), selectorAttribute) + } +} + + +/** + * Get the node attributes without the selector one + * @param {Array} attributes - attributes list + * @param {string} selectorAttribute - name of the selector attribute to filter out + * @returns {Array} filtered attributes + */ +function getAttributesWithoutSelector(attributes, selectorAttribute) { + if (selectorAttribute) + return attributes.filter(attribute => attribute.name !== selectorAttribute) + + return attributes +} + +/** + * Clean binding or custom attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} only the attributes that are not bindings or directives + */ +function cleanAttributes(node) { + return getNodeAttributes(node).filter(attribute => ![ + IF_DIRECTIVE, + EACH_DIRECTIVE, + KEY_ATTRIBUTE, + SLOT_ATTRIBUTE, + IS_DIRECTIVE + ].includes(attribute.name)) +} + +/** + * Create a root node proxing only its nodes and attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {RiotParser.Node} root node + */ +function createRootNode(node) { + return { + nodes: getChildrenNodes(node), + isRoot: true, + // root nodes shuold't have directives + attributes: cleanAttributes(node) + } +} + +/** + * Get all the child nodes of a RiotParser.Node + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} all the child nodes found + */ +function getChildrenNodes(node) { + return node && node.nodes ? node.nodes : [] +} + +/** + * Get all the attributes of a riot parser node + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} all the attributes find + */ +function getNodeAttributes(node) { + return node.attributes ? node.attributes : [] +} +/** + * Get the name of a custom node transforming it into an expression node + * @param {RiotParser.Node} node - riot parser node + * @returns {RiotParser.Node.Attr} the node name as expression attribute + */ +function getCustomNodeNameAsExpression(node) { + const isAttribute = findIsAttribute(node); + const toRawString = val => `'${val}'`; + + if (isAttribute) { + return isAttribute.expressions ? isAttribute.expressions[0] : { + ...isAttribute, + text: toRawString(isAttribute.value) + } + } + + return { ...node, text: toRawString(getName(node)) } +} + +/** + * Convert all the node static attributes to strings + * @param {RiotParser.Node} node - riot parser node + * @returns {string} all the node static concatenated as string + */ +function staticAttributesToString(node) { + return findStaticAttributes(node) + .map(attribute => attribute[IS_BOOLEAN_ATTRIBUTE] || !attribute.value ? + attribute.name : + `${attribute.name}="${unescapeNode(attribute, 'value').value}"` + ).join(' ') +} + +/** + * Make sure that node escaped chars will be unescaped + * @param {RiotParser.Node} node - riot parser node + * @param {string} key - key property to unescape + * @returns {RiotParser.Node} node with the text property unescaped + */ +function unescapeNode(node, key) { + if (node.unescape) { + return { + ...node, + [key]: unescapeChar(node[key], node.unescape) + } + } + + return node +} + + +/** + * Convert a riot parser opening node into a string + * @param {RiotParser.Node} node - riot parser node + * @returns {string} the node as string + */ +function nodeToString(node) { + const attributes = staticAttributesToString(node); + + switch(true) { + case isTagNode(node): + return `<${node.name}${attributes ? ` ${attributes}` : ''}${isVoidNode(node) ? '/' : ''}>` + case isTextNode(node): + return hasExpressions(node) ? TEXT_NODE_EXPRESSION_PLACEHOLDER : unescapeNode(node, 'text').text + default: + return '' + } +} + +/** + * Close an html node + * @param {RiotParser.Node} node - riot parser node + * @returns {string} the closing tag of the html tag node passed to this function + */ +function closeTag(node) { + return node.name ? `` : '' +} + +/** + * Create a strings array with the `join` call to transform it into a string + * @param {Array} stringsArray - array containing all the strings to concatenate + * @returns {AST.CallExpression} array with a `join` call + */ +function createArrayString(stringsArray) { + return builders.callExpression( + builders.memberExpression( + builders.arrayExpression(stringsArray), + builders.identifier('join'), + false + ), + [builders.literal('')], + ) +} + +/** + * Simple expression bindings might contain multiple expressions like for example: "class="{foo} red {bar}"" + * This helper aims to merge them in a template literal if it's necessary + * @param {RiotParser.Attr} node - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { Object } a template literal expression object + */ +function mergeAttributeExpressions(node, sourceFile, sourceCode) { + if (!node.parts || node.parts.length === 1) { + return transformExpression(node.expressions[0], sourceFile, sourceCode) + } + const stringsArray = [ + ...node.parts.reduce((acc, str) => { + const expression = node.expressions.find(e => e.text.trim() === str); + + return [ + ...acc, + expression ? transformExpression(expression, sourceFile, sourceCode) : builders.literal(str) + ] + }, []) + ].filter(expr => !isLiteral(expr) || expr.value); + + + return createArrayString(stringsArray) +} + +/** + * Create a selector that will be used to find the node via dom-bindings + * @param {number} id - temporary variable that will be increased anytime this function will be called + * @returns {string} selector attribute needed to bind a riot expression + */ +const createBindingSelector = (function createSelector(id = 0) { + return () => `${BINDING_SELECTOR_PREFIX}${id++}` +}()); + +/** + * Create an attribute evaluation function + * @param {RiotParser.Attr} sourceNode - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { AST.Node } an AST function expression to evaluate the attribute value + */ +function createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) { + return hasExpressions(sourceNode) ? + // dynamic attribute + wrapASTInFunctionWithScope(mergeAttributeExpressions(sourceNode, sourceFile, sourceCode)) : + // static attribute + builders.functionExpression( + null, + [], + builders.blockStatement([ + builders.returnStatement(builders.literal(sourceNode.value || true)) + ]), + ) +} + +/** + * Simple clone deep function, do not use it for classes or recursive objects! + * @param {*} source - possibily an object to clone + * @returns {*} the object we wanted to clone + */ +function cloneDeep(source) { + return JSON.parse(JSON.stringify(source)) +} + +const getEachItemName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[0] : expression.left; +const getEachIndexName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[1] : null; +const getEachValue = expression => expression.right; +const nameToliteral = compose(builders.literal, getName); + +const generateEachItemNameKey = expression => simplePropertyNode( + BINDING_ITEM_NAME_KEY, + compose(nameToliteral, getEachItemName)(expression) +); + +const generateEachIndexNameKey = expression => simplePropertyNode( + BINDING_INDEX_NAME_KEY, + compose(nameToliteral, getEachIndexName)(expression) +); + +const generateEachEvaluateKey = (expression, eachExpression, sourceFile, sourceCode) => simplePropertyNode( + BINDING_EVALUATE_KEY, + compose( + e => toScopedFunction(e, sourceFile, sourceCode), + e => ({ + ...eachExpression, + text: generateJavascript(e).code + }), + getEachValue + )(expression) +); + +/** + * Get the each expression properties to create properly the template binding + * @param { DomBinding.Expression } eachExpression - original each expression data + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Array } AST nodes that are needed to build an each binding + */ +function generateEachExpressionProperties(eachExpression, sourceFile, sourceCode) { + const ast = createASTFromExpression(eachExpression, sourceFile, sourceCode); + const body = ast.program.body; + const firstNode = body[0]; + + if (!isExpressionStatement(firstNode)) { + panic(`The each directives supported should be of type "ExpressionStatement",you have provided a "${firstNode.type}"`); + } + + const { expression } = firstNode; + + return [ + generateEachItemNameKey(expression), + generateEachIndexNameKey(expression), + generateEachEvaluateKey(expression, eachExpression, sourceFile, sourceCode) + ] +} + +/** + * Transform a RiotParser.Node.Tag into an each binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an each binding node + */ +function createEachBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + const [ifAttribute, eachAttribute, keyAttribute] = [ + findIfAttribute, + findEachAttribute, + findKeyAttribute + ].map(f => f(sourceNode)); + const attributeOrNull = attribute => attribute ? toScopedFunction(getAttributeExpression(attribute), sourceFile, sourceCode) : nullNode(); + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(EACH_BINDING_TYPE), + false + ), + ), + simplePropertyNode(BINDING_GET_KEY_KEY, attributeOrNull(keyAttribute)), + simplePropertyNode(BINDING_CONDITION_KEY, attributeOrNull(ifAttribute)), + createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)), + ...createSelectorProperties(selectorAttribute), + ...compose(generateEachExpressionProperties, getAttributeExpression)(eachAttribute) + ]) +} + +/** + * Transform a RiotParser.Node.Tag into an if binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { stiring } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an if binding node + */ +function createIfBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + const ifAttribute = findIfAttribute(sourceNode); + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(IF_BINDING_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + toScopedFunction(ifAttribute.expressions[0], sourceFile, sourceCode) + ), + ...createSelectorProperties(selectorAttribute), + createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)) + ]) +} + +/** + * Create a simple attribute expression + * @param {RiotParser.Node.Attr} sourceNode - the custom tag + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} object containing the expression binding keys + */ +function createAttributeExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(ATTRIBUTE_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode(BINDING_NAME_KEY, isSpreadAttribute(sourceNode) ? nullNode() : builders.literal(sourceNode.name)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) +} + +/** + * Create a simple event expression + * @param {RiotParser.Node.Attr} sourceNode - attribute containing the event handlers + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} object containing the expression binding keys + */ +function createEventExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(EVENT_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode(BINDING_NAME_KEY, builders.literal(sourceNode.name)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) +} + +/** + * Generate the pure immutable string chunks from a RiotParser.Node.Text + * @param {RiotParser.Node.Text} node - riot parser text node + * @param {string} sourceCode sourceCode - source code + * @returns {Array} array containing the immutable string chunks + */ +function generateLiteralStringChunksFromNode(node, sourceCode) { + return node.expressions.reduce((chunks, expression, index) => { + const start = index ? node.expressions[index - 1].end : node.start; + + chunks.push(sourceCode.substring(start, expression.start)); + + // add the tail to the string + if (index === node.expressions.length - 1) + chunks.push(sourceCode.substring(expression.end, node.end)); + + return chunks + }, []).map(str => node.unescape ? unescapeChar(str, node.unescape) : str) +} + +/** + * Simple bindings might contain multiple expressions like for example: "{foo} and {bar}" + * This helper aims to merge them in a template literal if it's necessary + * @param {RiotParser.Node} node - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { Object } a template literal expression object + */ +function mergeNodeExpressions(node, sourceFile, sourceCode) { + if (node.parts.length === 1) + return transformExpression(node.expressions[0], sourceFile, sourceCode) + + const pureStringChunks = generateLiteralStringChunksFromNode(node, sourceCode); + const stringsArray = pureStringChunks.reduce((acc, str, index) => { + const expr = node.expressions[index]; + + return [ + ...acc, + builders.literal(str), + expr ? transformExpression(expr, sourceFile, sourceCode) : nullNode() + ] + }, []) + // filter the empty literal expressions + .filter(expr => !isLiteral(expr) || expr.value); + + return createArrayString(stringsArray) +} + +/** + * Create a text expression + * @param {RiotParser.Node.Text} sourceNode - text node to parse + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {number} childNodeIndex - position of the child text node in its parent children nodes + * @returns {AST.Node} object containing the expression binding keys + */ +function createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(TEXT_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_CHILD_NODE_INDEX_KEY, + builders.literal(childNodeIndex) + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + wrapASTInFunctionWithScope( + mergeNodeExpressions(sourceNode, sourceFile, sourceCode) + ) + ) + ]) +} + +function createValueExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(VALUE_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) +} + +function createExpression(sourceNode, sourceFile, sourceCode, childNodeIndex, parentNode) { + switch (true) { + case isTextNode(sourceNode): + return createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) + // progress nodes value attributes will be rendered as attributes + // see https://github.com/riot/compiler/issues/122 + case isValueAttribute(sourceNode) && hasValueAttribute(parentNode.name) && !isProgressNode(parentNode): + return createValueExpression(sourceNode, sourceFile, sourceCode) + case isEventAttribute(sourceNode): + return createEventExpression(sourceNode, sourceFile, sourceCode) + default: + return createAttributeExpression(sourceNode, sourceFile, sourceCode) + } +} + +/** + * Create the attribute expressions + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {Array} array containing all the attribute expressions + */ +function createAttributeExpressions(sourceNode, sourceFile, sourceCode) { + return findDynamicAttributes(sourceNode) + .map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)) +} + +/** + * Create the text node expressions + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {Array} array containing all the text node expressions + */ +function createTextNodeExpressions(sourceNode, sourceFile, sourceCode) { + const childrenNodes = getChildrenNodes(sourceNode); + + return childrenNodes + .filter(isTextNode) + .filter(hasExpressions) + .map(node => createExpression( + node, + sourceFile, + sourceCode, + childrenNodes.indexOf(node), + sourceNode + )) +} + +/** + * Add a simple binding to a riot parser node + * @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an each binding node + */ +function createSimpleBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.objectExpression([ + ...createSelectorProperties(selectorAttribute), + simplePropertyNode( + BINDING_EXPRESSIONS_KEY, + builders.arrayExpression([ + ...createTextNodeExpressions(sourceNode, sourceFile, sourceCode), + ...createAttributeExpressions(sourceNode, sourceFile, sourceCode) + ]) + ) + ]) +} + +/** + * Transform a RiotParser.Node.Tag of type slot into a slot binding + * @param { RiotParser.Node.Tag } sourceNode - slot node + * @param { string } selectorAttribute - attribute needed to select the target node + * @returns { AST.Node } a slot binding node + */ +function createSlotBinding(sourceNode, selectorAttribute) { + const slotNameAttribute = findAttribute(NAME_ATTRIBUTE, sourceNode); + const slotName = slotNameAttribute ? slotNameAttribute.value : DEFAULT_SLOT_NAME; + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(SLOT_BINDING_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_NAME_KEY, + builders.literal(slotName) + ), + ...createSelectorProperties(selectorAttribute) + ]) +} + +/** + * Find the slots in the current component and group them under the same id + * @param {RiotParser.Node.Tag} sourceNode - the custom tag + * @returns {Object} object containing all the slots grouped by name + */ +function groupSlots(sourceNode) { + return getChildrenNodes(sourceNode).reduce((acc, node) => { + const slotAttribute = findSlotAttribute(node); + + if (slotAttribute) { + acc[slotAttribute.value] = node; + } else { + acc.default = createRootNode({ + nodes: [...getChildrenNodes(acc.default), node] + }); + } + + return acc + }, { + default: null + }) +} + +/** + * Create the slot entity to pass to the riot-dom bindings + * @param {string} id - slot id + * @param {RiotParser.Node.Tag} sourceNode - slot root node + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} ast node containing the slot object properties + */ +function buildSlot(id, sourceNode, sourceFile, sourceCode) { + const cloneNode = { + ...sourceNode, + // avoid to render the slot attribute + attributes: getNodeAttributes(sourceNode).filter(attribute => attribute.name !== SLOT_ATTRIBUTE) + }; + const [html, bindings] = build(cloneNode, sourceFile, sourceCode); + + return builders.objectExpression([ + simplePropertyNode(BINDING_ID_KEY, builders.literal(id)), + simplePropertyNode(BINDING_HTML_KEY, builders.literal(html)), + simplePropertyNode(BINDING_BINDINGS_KEY, builders.arrayExpression(bindings)) + ]) +} + +/** + * Create the AST array containing the slots + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.ArrayExpression} array containing the attributes to bind + */ +function createSlotsArray(sourceNode, sourceFile, sourceCode) { + return builders.arrayExpression([ + ...compose( + slots => slots.map(([key, value]) => buildSlot(key, value, sourceFile, sourceCode)), + slots => slots.filter(([,value]) => value), + Object.entries, + groupSlots + )(sourceNode) + ]) +} + +/** + * Create the AST array containing the attributes to bind to this node + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.ArrayExpression} array containing the slot objects + */ +function createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.arrayExpression([ + ...compose( + attributes => attributes.map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)), + attributes => getAttributesWithoutSelector(attributes, selectorAttribute), // eslint-disable-line + cleanAttributes + )(sourceNode) + ]) +} + +/** + * Find the slot attribute if it exists + * @param {RiotParser.Node.Tag} sourceNode - the custom tag + * @returns {RiotParser.Node.Attr|undefined} the slot attribute found + */ +function findSlotAttribute(sourceNode) { + return getNodeAttributes(sourceNode).find(attribute => attribute.name === SLOT_ATTRIBUTE) +} + +/** + * Transform a RiotParser.Node.Tag into a tag binding + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } tag binding node + */ +function createTagBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(TAG_BINDING_TYPE), + false + ), + ), + simplePropertyNode(BINDING_GET_COMPONENT_KEY, builders.identifier(GET_COMPONENT_FN)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + toScopedFunction(getCustomNodeNameAsExpression(sourceNode), sourceFile, sourceCode) + ), + simplePropertyNode(BINDING_SLOTS_KEY, createSlotsArray(sourceNode, sourceFile, sourceCode)), + simplePropertyNode( + BINDING_ATTRIBUTES_KEY, + createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) + ), + ...createSelectorProperties(selectorAttribute) + ]) +} + +const BuildingState = Object.freeze({ + html: [], + bindings: [], + parent: null +}); + +/** + * Nodes having bindings should be cloned and new selector properties should be added to them + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} bindingsSelector - temporary string to identify the current node + * @returns {RiotParser.Node} the original node parsed having the new binding selector attribute + */ +function createBindingsTag(sourceNode, bindingsSelector) { + if (!bindingsSelector) return sourceNode + + return { + ...sourceNode, + // inject the selector bindings into the node attributes + attributes: [{ + name: bindingsSelector, + value: bindingsSelector + }, ...getNodeAttributes(sourceNode)] + } +} + +/** + * Create a generic dynamic node (text or tag) and generate its bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ +function createDynamicNode(sourceNode, sourceFile, sourceCode, state) { + switch (true) { + case isTextNode(sourceNode): + // text nodes will not have any bindings + return [nodeToString(sourceNode), []] + default: + return createTagWithBindings(sourceNode, sourceFile, sourceCode) + } +} + +/** + * Create only a dynamic tag node with generating a custom selector and its bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ +function createTagWithBindings(sourceNode, sourceFile, sourceCode) { + const bindingsSelector = isRootNode(sourceNode) ? null : createBindingSelector(); + const cloneNode = createBindingsTag(sourceNode, bindingsSelector); + const tagOpeningHTML = nodeToString(cloneNode); + + switch(true) { + // EACH bindings have prio 1 + case hasEachAttribute(cloneNode): + return [tagOpeningHTML, [createEachBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // IF bindings have prio 2 + case hasIfAttribute(cloneNode): + return [tagOpeningHTML, [createIfBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // TAG bindings have prio 3 + case isCustomNode(cloneNode): + return [tagOpeningHTML, [createTagBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // slot tag + case isSlotNode(cloneNode): + return [tagOpeningHTML, [createSlotBinding(cloneNode, bindingsSelector)]] + // this node has expressions bound to it + default: + return [tagOpeningHTML, [createSimpleBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + } +} + +/** + * Parse a node trying to extract its template and bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ +function parseNode(sourceNode, sourceFile, sourceCode, state) { + // static nodes have no bindings + if (isStaticNode(sourceNode)) return [nodeToString(sourceNode), []] + return createDynamicNode(sourceNode, sourceFile, sourceCode) +} + +/** + * Create the tag binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @param { string } selector - binding selector + * @returns { Array } array with only the tag binding AST + */ +function createNestedBindings(sourceNode, sourceFile, sourceCode, selector) { + const mightBeARiotComponent = isCustomNode(sourceNode); + + return mightBeARiotComponent ? [null, [ + createTagBinding( + cloneNodeWithoutSelectorAttribute(sourceNode, selector), + null, + sourceFile, + sourceCode + )] + ] : build(createRootNode(sourceNode), sourceFile, sourceCode) +} + +/** + * Build the template and the bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and the dom bindings + */ +function build( + sourceNode, + sourceFile, + sourceCode, + state +) { + if (!sourceNode) panic('Something went wrong with your tag DOM parsing, your tag template can\'t be created'); + + const [nodeHTML, nodeBindings] = parseNode(sourceNode, sourceFile, sourceCode); + const childrenNodes = getChildrenNodes(sourceNode); + const currentState = { ...cloneDeep(BuildingState), ...state }; + + // mutate the original arrays + currentState.html.push(...nodeHTML); + currentState.bindings.push(...nodeBindings); + + // do recursion if + // this tag has children and it has no special directives bound to it + if (childrenNodes.length && !hasItsOwnTemplate(sourceNode)) { + childrenNodes.forEach(node => build(node, sourceFile, sourceCode, { parent: sourceNode, ...currentState })); + } + + // close the tag if it's not a void one + if (isTagNode(sourceNode) && !isVoidNode(sourceNode)) { + currentState.html.push(closeTag(sourceNode)); + } + + return [ + currentState.html.join(''), + currentState.bindings + ] +} + +const templateFunctionArguments = [ + TEMPLATE_FN, + EXPRESSION_TYPES, + BINDING_TYPES, + GET_COMPONENT_FN +].map(builders.identifier); + +/** + * Create the content of the template function + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.BlockStatement} the content of the template function + */ +function createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) { + return builders.blockStatement([ + builders.returnStatement( + callTemplateFunction( + ...build( + createRootNode(sourceNode), + sourceFile, + sourceCode + ) + ) + ) + ]) +} + +/** + * Extend the AST adding the new template property containing our template call to render the component + * @param { Object } ast - current output ast + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @returns { Object } the output ast having the "template" key + */ +function extendTemplateProperty(ast, sourceFile, sourceCode, sourceNode) { + types.visit(ast, { + visitProperty(path) { + if (path.value.key.value === TAG_TEMPLATE_PROPERTY) { + path.value.value = builders.functionExpression( + null, + templateFunctionArguments, + createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) + ); + + return false + } + + this.traverse(path); + } + }); + + return ast +} + +/** + * Generate the component template logic + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @param { string } source - original component source code + * @param { Object } meta - compilation meta information + * @param { AST } ast - current AST output + * @returns { AST } the AST generated + */ +function template(sourceNode, source, meta, ast) { + const { options } = meta; + return extendTemplateProperty(ast, options.file, source, sourceNode) +} + +const DEFAULT_OPTIONS = { + template: 'default', + file: '[unknown-source-file]', + scopedCss: true +}; + +/** + * Create the initial AST + * @param {string} tagName - the name of the component we have compiled + * @returns { AST } the initial AST + * + * @example + * // the output represents the following string in AST + */ +function createInitialInput({tagName}) { + /* + generates + export default { + ${TAG_CSS_PROPERTY}: null, + ${TAG_LOGIC_PROPERTY}: null, + ${TAG_TEMPLATE_PROPERTY}: null + } + */ + return builders.program([ + builders.exportDefaultDeclaration( + builders.objectExpression([ + simplePropertyNode(TAG_CSS_PROPERTY, nullNode()), + simplePropertyNode(TAG_LOGIC_PROPERTY, nullNode()), + simplePropertyNode(TAG_TEMPLATE_PROPERTY, nullNode()), + simplePropertyNode(TAG_NAME_PROPERTY, builders.literal(tagName)) + ]) + )] + ) +} + +/** + * Make sure the input sourcemap is valid otherwise we ignore it + * @param {SourceMapGenerator} map - preprocessor source map + * @returns {Object} sourcemap as json or nothing + */ +function normaliseInputSourceMap(map) { + const inputSourceMap = sourcemapAsJSON(map); + return isEmptySourcemap(inputSourceMap) ? null : inputSourceMap +} + +/** + * Override the sourcemap content making sure it will always contain the tag source code + * @param {Object} map - sourcemap as json + * @param {string} source - component source code + * @returns {Object} original source map with the "sourcesContent" property overriden + */ +function overrideSourcemapContent(map, source) { + return { + ...map, + sourcesContent: [source] + } +} + +/** + * Create the compilation meta object + * @param { string } source - source code of the tag we will need to compile + * @param { string } options - compiling options + * @returns {Object} meta object + */ +function createMeta(source, options) { + return { + tagName: null, + fragments: null, + options: { + ...DEFAULT_OPTIONS, + ...options + }, + source + } +} + +/** + * Generate the output code source together with the sourcemap + * @param { string } source - source code of the tag we will need to compile + * @param { string } opts - compiling options + * @returns { Output } object containing output code and source map + */ +function compile(source, opts = {}) { + const meta = createMeta(source, opts); + const {options} = meta; + const { code, map } = execute$1('template', options.template, meta, source); + const { template: template$1, css: css$1, javascript: javascript$1 } = riotParser(options).parse(code).output; + + // extend the meta object with the result of the parsing + Object.assign(meta, { + tagName: template$1.name, + fragments: { template: template$1, css: css$1, javascript: javascript$1 } + }); + + return compose( + result => ({ ...result, meta }), + result => execute(result, meta), + result => ({ + ...result, + map: overrideSourcemapContent(result.map, source) + }), + ast => meta.ast = ast && generateJavascript(ast, { + sourceMapName: `${options.file}.map`, + inputSourceMap: normaliseInputSourceMap(map) + }), + hookGenerator(template, template$1, code, meta), + hookGenerator(javascript, javascript$1, code, meta), + hookGenerator(css, css$1, code, meta), + )(createInitialInput(meta)) +} + +/** + * Prepare the riot parser node transformers + * @param { Function } transformer - transformer function + * @param { Object } sourceNode - riot parser node + * @param { string } source - component source code + * @param { Object } meta - compilation meta information + * @returns { Promise } object containing output code and source map + */ +function hookGenerator(transformer, sourceNode, source, meta) { + if ( + // filter missing nodes + !sourceNode || + // filter nodes without children + (sourceNode.nodes && !sourceNode.nodes.length) || + // filter empty javascript and css nodes + (!sourceNode.nodes && !sourceNode.text)) { + return result => result + } + + return curry(transformer)(sourceNode, source, meta) +} + +// This function can be used to register new preprocessors +// a preprocessor can target either only the css or javascript nodes +// or the complete tag source file ('template') +const registerPreprocessor = register$1; + +// This function can allow you to register postprocessors that will parse the output code +// here we can run prettifiers, eslint fixes... +const registerPostprocessor = register; + +export { compile, createInitialInput, registerPostprocessor, registerPreprocessor }; diff --git a/node_modules/@riotjs/compiler/dist/index.js b/node_modules/@riotjs/compiler/dist/index.js new file mode 100644 index 0000000..12bcdbd --- /dev/null +++ b/node_modules/@riotjs/compiler/dist/index.js @@ -0,0 +1,2167 @@ +/* Riot Compiler WIP, @license MIT */ +'use strict'; + +Object.defineProperty(exports, '__esModule', { value: true }); + +function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } + +var recast = require('recast'); +var util = require('recast/lib/util'); +var sourceMap = require('source-map'); +var compose = _interopDefault(require('cumpa')); +var cssEscape = _interopDefault(require('cssesc')); +var curry = _interopDefault(require('curri')); +var acorn = require('acorn'); +var globalScope = _interopDefault(require('globals')); +var riotParser = require('@riotjs/parser'); +var riotParser__default = _interopDefault(riotParser); +var domNodes = require('dom-nodes'); + +const TAG_LOGIC_PROPERTY = 'exports'; +const TAG_CSS_PROPERTY = 'css'; +const TAG_TEMPLATE_PROPERTY = 'template'; +const TAG_NAME_PROPERTY = 'name'; + +const types = recast.types; +const builders = recast.types.builders; +const namedTypes = recast.types.namedTypes; + +function nullNode() { + return builders.literal(null) +} + +function simplePropertyNode(key, value) { + return builders.property('init', builders.literal(key), value, false) +} + +/** + * Return a source map as JSON, it it has not the toJSON method it means it can + * be used right the way + * @param { SourceMapGenerator|Object } map - a sourcemap generator or simply an json object + * @returns { Object } the source map as JSON + */ +function sourcemapAsJSON(map) { + if (map && map.toJSON) return map.toJSON() + return map +} + +/** + * Detect node js environements + * @returns { boolean } true if the runtime is node + */ +function isNode() { + return typeof process !== 'undefined' +} + +/** + * Compose two sourcemaps + * @param { SourceMapGenerator } formerMap - original sourcemap + * @param { SourceMapGenerator } latterMap - target sourcemap + * @returns { Object } sourcemap json + */ +function composeSourcemaps(formerMap, latterMap) { + if ( + isNode() && + formerMap && latterMap && latterMap.mappings + ) { + return util.composeSourceMaps(sourcemapAsJSON(formerMap), sourcemapAsJSON(latterMap)) + } else if (isNode() && formerMap) { + return sourcemapAsJSON(formerMap) + } + + return {} +} + +/** + * Create a new sourcemap generator + * @param { Object } options - sourcemap options + * @returns { SourceMapGenerator } SourceMapGenerator instance + */ +function createSourcemap(options) { + return new sourceMap.SourceMapGenerator(options) +} + +const Output = Object.freeze({ + code: '', + ast: [], + meta: {}, + map: null +}); + +/** + * Create the right output data result of a parsing + * @param { Object } data - output data + * @param { string } data.code - code generated + * @param { AST } data.ast - ast representing the code + * @param { SourceMapGenerator } data.map - source map generated along with the code + * @param { Object } meta - compilation meta infomration + * @returns { Output } output container object + */ +function createOutput(data, meta) { + const output = { + ...Output, + ...data, + meta + }; + + if (!output.map && meta && meta.options && meta.options.file) + return { + ...output, + map: createSourcemap({ file: meta.options.file }) + } + + return output +} + +/** + * Transform the source code received via a compiler function + * @param { Function } compiler - function needed to generate the output code + * @param { Object } meta - compilation meta information + * @param { string } source - source code + * @returns { Output } output - the result of the compiler + */ +function transform(compiler, meta, source) { + const result = (compiler ? compiler(source, meta) : { code: source }); + return createOutput(result, meta) +} + +/** + * Throw an error with a descriptive message + * @param { string } message - error message + * @returns { undefined } hoppla.. at this point the program should stop working + */ +function panic(message) { + throw new Error(message) +} + +const postprocessors = new Set(); + +/** + * Register a postprocessor that will be used after the parsing and compilation of the riot tags + * @param { Function } postprocessor - transformer that will receive the output code ans sourcemap + * @returns { Set } the postprocessors collection + */ +function register(postprocessor) { + if (postprocessors.has(postprocessor)) { + panic(`This postprocessor "${postprocessor.name || postprocessor.toString()}" was already registered`); + } + + postprocessors.add(postprocessor); + + return postprocessors +} + +/** + * Exec all the postprocessors in sequence combining the sourcemaps generated + * @param { Output } compilerOutput - output generated by the compiler + * @param { Object } meta - compiling meta information + * @returns { Output } object containing output code and source map + */ +function execute(compilerOutput, meta) { + return Array.from(postprocessors).reduce(function(acc, postprocessor) { + const { code, map } = acc; + const output = postprocessor(code, meta); + + return { + code: output.code, + map: composeSourcemaps(map, output.map) + } + }, createOutput(compilerOutput, meta)) +} + +/** + * Parsers that can be registered by users to preparse components fragments + * @type { Object } + */ +const preprocessors = Object.freeze({ + javascript: new Map(), + css: new Map(), + template: new Map().set('default', code => ({ code })) +}); + +// throw a processor type error +function preprocessorTypeError(type) { + panic(`No preprocessor of type "${type}" was found, please make sure to use one of these: 'javascript', 'css' or 'template'`); +} + +// throw an error if the preprocessor was not registered +function preprocessorNameNotFoundError(name) { + panic(`No preprocessor named "${name}" was found, are you sure you have registered it?'`); +} + +/** + * Register a custom preprocessor + * @param { string } type - preprocessor type either 'js', 'css' or 'template' + * @param { string } name - unique preprocessor id + * @param { Function } preprocessor - preprocessor function + * @returns { Map } - the preprocessors map + */ +function register$1(type, name, preprocessor) { + if (!type) panic('Please define the type of preprocessor you want to register \'javascript\', \'css\' or \'template\''); + if (!name) panic('Please define a name for your preprocessor'); + if (!preprocessor) panic('Please provide a preprocessor function'); + if (!preprocessors[type]) preprocessorTypeError(type); + if (preprocessors[type].has(name)) panic(`The preprocessor ${name} was already registered before`); + + preprocessors[type].set(name, preprocessor); + + return preprocessors +} + +/** + * Exec the compilation of a preprocessor + * @param { string } type - preprocessor type either 'js', 'css' or 'template' + * @param { string } name - unique preprocessor id + * @param { Object } meta - preprocessor meta information + * @param { string } source - source code + * @returns { Output } object containing a sourcemap and a code string + */ +function execute$1(type, name, meta, source) { + if (!preprocessors[type]) preprocessorTypeError(type); + if (!preprocessors[type].has(name)) preprocessorNameNotFoundError(name); + + return transform(preprocessors[type].get(name), meta, source) +} + +const ATTRIBUTE_TYPE_NAME = 'type'; + +/** + * Get the type attribute from a node generated by the riot parser + * @param { Object} sourceNode - riot parser node + * @returns { string|null } a valid type to identify the preprocessor to use or nothing + */ +function getPreprocessorTypeByAttribute(sourceNode) { + const typeAttribute = sourceNode.attributes ? + sourceNode.attributes.find(attribute => attribute.name === ATTRIBUTE_TYPE_NAME) : + null; + + return typeAttribute ? normalize(typeAttribute.value) : null +} + + +/** + * Remove the noise in case a user has defined the preprocessor type='text/scss' + * @param { string } value - input string + * @returns { string } normalized string + */ +function normalize(value) { + return value.replace('text/', '') +} + +/** + * Preprocess a riot parser node + * @param { string } preprocessorType - either css, js + * @param { string } preprocessorName - preprocessor id + * @param { Object } meta - compilation meta information + * @param { RiotParser.nodeTypes } node - css node detected by the parser + * @returns { Output } code and sourcemap generated by the preprocessor + */ +function preprocess(preprocessorType, preprocessorName, meta, node) { + const code = node.text; + + return (preprocessorName ? + execute$1(preprocessorType, preprocessorName, meta, code) : + { code } + ) +} + +/** + * Matches valid, multiline JavaScript comments in almost all its forms. + * @const {RegExp} + * @static + */ +const R_MLCOMMS = /\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//g; + +/** + * Source for creating regexes matching valid quoted, single-line JavaScript strings. + * It recognizes escape characters, including nested quotes and line continuation. + * @const {string} + */ +const S_LINESTR = /"[^"\n\\]*(?:\\[\S\s][^"\n\\]*)*"|'[^'\n\\]*(?:\\[\S\s][^'\n\\]*)*'/.source; + +/** + * Matches CSS selectors, excluding those beginning with '@' and quoted strings. + * @const {RegExp} + */ + +const CSS_SELECTOR = RegExp(`([{}]|^)[; ]*((?:[^@ ;{}][^{}]*)?[^@ ;{}:] ?)(?={)|${S_LINESTR}`, 'g'); + +/** + * Parses styles enclosed in a "scoped" tag + * The "css" string is received without comments or surrounding spaces. + * + * @param {string} tag - Tag name of the root element + * @param {string} css - The CSS code + * @returns {string} CSS with the styles scoped to the root element + */ +function scopedCSS(tag, css) { + const host = ':host'; + const selectorsBlacklist = ['from', 'to']; + + return css.replace(CSS_SELECTOR, function(m, p1, p2) { + // skip quoted strings + if (!p2) return m + + // we have a selector list, parse each individually + p2 = p2.replace(/[^,]+/g, function(sel) { + const s = sel.trim(); + + // skip selectors already using the tag name + if (s.indexOf(tag) === 0) { + return sel + } + + // skips the keywords and percents of css animations + if (!s || selectorsBlacklist.indexOf(s) > -1 || s.slice(-1) === '%') { + return sel + } + + // replace the `:host` pseudo-selector, where it is, with the root tag name; + // if `:host` was not included, add the tag name as prefix, and mirror all + // `[data-is]` + if (s.indexOf(host) < 0) { + return `${tag} ${s},[is="${tag}"] ${s}` + } else { + return `${s.replace(host, tag)},${ + s.replace(host, `[is="${tag}"]`)}` + } + }); + + // add the danling bracket char and return the processed selector list + return p1 ? `${p1} ${p2}` : p2 + }) +} + +/** + * Remove comments, compact and trim whitespace + * @param { string } code - compiled css code + * @returns { string } css code normalized + */ +function compactCss(code) { + return code.replace(R_MLCOMMS, '').replace(/\s+/g, ' ').trim() +} + +const escapeBackslashes = s => s.replace(/\\/g, '\\\\'); +const escapeIdentifier = identifier => escapeBackslashes(cssEscape(identifier, { + isIdentifier: true +})); + +/** + * Generate the component css + * @param { Object } sourceNode - node generated by the riot compiler + * @param { string } source - original component source code + * @param { Object } meta - compilation meta information + * @param { AST } ast - current AST output + * @returns { AST } the AST generated + */ +function css(sourceNode, source, meta, ast) { + const preprocessorName = getPreprocessorTypeByAttribute(sourceNode); + const { options } = meta; + const preprocessorOutput = preprocess('css', preprocessorName, meta, sourceNode.text); + const normalizedCssCode = compactCss(preprocessorOutput.code); + const escapedCssIdentifier = escapeIdentifier(meta.tagName); + + const cssCode = (options.scopedCss ? + scopedCSS(escapedCssIdentifier, escapeBackslashes(normalizedCssCode)) : + escapeBackslashes(normalizedCssCode) + ).trim(); + + types.visit(ast, { + visitProperty(path) { + if (path.value.key.value === TAG_CSS_PROPERTY) { + path.value.value = builders.templateLiteral( + [builders.templateElement({ raw: cssCode, cooked: '' }, false)], + [] + ); + + return false + } + + this.traverse(path); + } + }); + + return ast +} + +/** + * Generate the javascript from an ast source + * @param {AST} ast - ast object + * @param {Object} options - printer options + * @returns {Object} code + map + */ +function generateJavascript(ast, options) { + return recast.print(ast, { + ...options, + tabWidth: 2, + quote: 'single' + }) +} + +/** + * True if the sourcemap has no mappings, it is empty + * @param {Object} map - sourcemap json + * @returns {boolean} true if empty + */ +function isEmptySourcemap(map) { + return !map || !map.mappings || !map.mappings.length +} + +const LINES_RE = /\r\n?|\n/g; + +/** + * Split a string into a rows array generated from its EOL matches + * @param { string } string [description] + * @returns { Array } array containing all the string rows + */ +function splitStringByEOL(string) { + return string.split(LINES_RE) +} + +/** + * Get the line and the column of a source text based on its position in the string + * @param { string } string - target string + * @param { number } position - target position + * @returns { Object } object containing the source text line and column + */ +function getLineAndColumnByPosition(string, position) { + const lines = splitStringByEOL(string.slice(0, position)); + + return { + line: lines.length, + column: lines[lines.length - 1].length + } +} + +/** + * Add the offset to the code that must be parsed in order to generate properly the sourcemaps + * @param {string} input - input string + * @param {string} source - original source code + * @param {RiotParser.Node} node - node that we are going to transform + * @return {string} the input string with the offset properly set + */ +function addLineOffset(input, source, node) { + const {column, line} = getLineAndColumnByPosition(source, node.start); + return `${'\n'.repeat(line - 1)}${' '.repeat(column + 1)}${input}` +} + +/** + * Parse a js source to generate the AST + * @param {string} source - javascript source + * @param {Object} options - parser options + * @returns {AST} AST tree + */ +function generateAST(source, options) { + return recast.parse(source, { + parser: { + parse(source, opts) { + return acorn.Parser.parse(source, { + ...opts, + ecmaVersion: 2020 + }) + } + }, + ...options + }) +} + +const browserAPIs = Object.keys(globalScope.browser); +const builtinAPIs = Object.keys(globalScope.builtin); + +const isIdentifier = namedTypes.Identifier.check.bind(namedTypes.Identifier); +const isLiteral = namedTypes.Literal.check.bind(namedTypes.Literal); +const isExpressionStatement = namedTypes.ExpressionStatement.check.bind(namedTypes.ExpressionStatement); +const isObjectExpression = namedTypes.ObjectExpression.check.bind(namedTypes.ObjectExpression); +const isThisExpression = namedTypes.ThisExpression.check.bind(namedTypes.ThisExpression); +const isNewExpression = namedTypes.NewExpression.check.bind(namedTypes.NewExpression); +const isSequenceExpression = namedTypes.SequenceExpression.check.bind(namedTypes.SequenceExpression); +const isBinaryExpression = namedTypes.BinaryExpression.check.bind(namedTypes.BinaryExpression); +const isExportDefaultStatement = namedTypes.ExportDefaultDeclaration.check.bind(namedTypes.ExportDefaultDeclaration); + +const isBrowserAPI = ({name}) => browserAPIs.includes(name); +const isBuiltinAPI = ({name}) => builtinAPIs.includes(name); +const isRaw = (node) => node && node.raw; // eslint-disable-line + +/** + * Find the export default statement + * @param { Array } body - tree structure containing the program code + * @returns { Object } node containing only the code of the export default statement + */ +function findExportDefaultStatement(body) { + return body.find(isExportDefaultStatement) +} + +/** + * Find all the code in an ast program except for the export default statements + * @param { Array } body - tree structure containing the program code + * @returns { Array } array containing all the program code except the export default expressions + */ +function filterNonExportDefaultStatements(body) { + return body.filter(node => !isExportDefaultStatement(node)) +} + +/** + * Get the body of the AST structure + * @param { Object } ast - ast object generated by recast + * @returns { Array } array containing the program code + */ +function getProgramBody(ast) { + return ast.body || ast.program.body +} + +/** + * Extend the AST adding the new tag method containing our tag sourcecode + * @param { Object } ast - current output ast + * @param { Object } exportDefaultNode - tag export default node + * @returns { Object } the output ast having the "tag" key extended with the content of the export default + */ +function extendTagProperty(ast, exportDefaultNode) { + types.visit(ast, { + visitProperty(path) { + if (path.value.key.value === TAG_LOGIC_PROPERTY) { + path.value.value = exportDefaultNode.declaration; + return false + } + + this.traverse(path); + } + }); + + return ast +} + +/** + * Generate the component javascript logic + * @param { Object } sourceNode - node generated by the riot compiler + * @param { string } source - original component source code + * @param { Object } meta - compilation meta information + * @param { AST } ast - current AST output + * @returns { AST } the AST generated + */ +function javascript(sourceNode, source, meta, ast) { + const preprocessorName = getPreprocessorTypeByAttribute(sourceNode); + const javascriptNode = addLineOffset(sourceNode.text.text, source, sourceNode); + const { options } = meta; + const preprocessorOutput = preprocess('javascript', preprocessorName, meta, { + ...sourceNode, + text: javascriptNode + }); + const inputSourceMap = sourcemapAsJSON(preprocessorOutput.map); + const generatedAst = generateAST(preprocessorOutput.code, { + sourceFileName: options.file, + inputSourceMap: isEmptySourcemap(inputSourceMap) ? null : inputSourceMap + }); + const generatedAstBody = getProgramBody(generatedAst); + const bodyWithoutExportDefault = filterNonExportDefaultStatements(generatedAstBody); + const exportDefaultNode = findExportDefaultStatement(generatedAstBody); + const outputBody = getProgramBody(ast); + + // add to the ast the "private" javascript content of our tag script node + outputBody.unshift(...bodyWithoutExportDefault); + + // convert the export default adding its content to the "tag" property exported + if (exportDefaultNode) extendTagProperty(ast, exportDefaultNode); + + return ast +} + +// import {IS_BOOLEAN,IS_CUSTOM,IS_RAW,IS_SPREAD,IS_VOID} from '@riotjs/parser/src/constants' + +const BINDING_TYPES = 'bindingTypes'; +const EACH_BINDING_TYPE = 'EACH'; +const IF_BINDING_TYPE = 'IF'; +const TAG_BINDING_TYPE = 'TAG'; +const SLOT_BINDING_TYPE = 'SLOT'; + + +const EXPRESSION_TYPES = 'expressionTypes'; +const ATTRIBUTE_EXPRESSION_TYPE = 'ATTRIBUTE'; +const VALUE_EXPRESSION_TYPE = 'VALUE'; +const TEXT_EXPRESSION_TYPE = 'TEXT'; +const EVENT_EXPRESSION_TYPE = 'EVENT'; + +const TEMPLATE_FN = 'template'; +const SCOPE = 'scope'; +const GET_COMPONENT_FN = 'getComponent'; + +// keys needed to create the DOM bindings +const BINDING_SELECTOR_KEY = 'selector'; +const BINDING_GET_COMPONENT_KEY = 'getComponent'; +const BINDING_TEMPLATE_KEY = 'template'; +const BINDING_TYPE_KEY = 'type'; +const BINDING_REDUNDANT_ATTRIBUTE_KEY = 'redundantAttribute'; +const BINDING_CONDITION_KEY = 'condition'; +const BINDING_ITEM_NAME_KEY = 'itemName'; +const BINDING_GET_KEY_KEY = 'getKey'; +const BINDING_INDEX_NAME_KEY = 'indexName'; +const BINDING_EVALUATE_KEY = 'evaluate'; +const BINDING_NAME_KEY = 'name'; +const BINDING_SLOTS_KEY = 'slots'; +const BINDING_EXPRESSIONS_KEY = 'expressions'; +const BINDING_CHILD_NODE_INDEX_KEY = 'childNodeIndex'; +// slots keys +const BINDING_BINDINGS_KEY = 'bindings'; +const BINDING_ID_KEY = 'id'; +const BINDING_HTML_KEY = 'html'; +const BINDING_ATTRIBUTES_KEY = 'attributes'; + +// DOM directives +const IF_DIRECTIVE = 'if'; +const EACH_DIRECTIVE = 'each'; +const KEY_ATTRIBUTE = 'key'; +const SLOT_ATTRIBUTE = 'slot'; +const NAME_ATTRIBUTE = 'name'; +const IS_DIRECTIVE = 'is'; + +// Misc +const DEFAULT_SLOT_NAME = 'default'; +const TEXT_NODE_EXPRESSION_PLACEHOLDER = ''; +const BINDING_SELECTOR_PREFIX = 'expr'; +const SLOT_TAG_NODE_NAME = 'slot'; +const PROGRESS_TAG_NODE_NAME = 'progress'; +const IS_VOID_NODE = 'isVoid'; +const IS_CUSTOM_NODE = 'isCustom'; +const IS_BOOLEAN_ATTRIBUTE = 'isBoolean'; +const IS_SPREAD_ATTRIBUTE = 'isSpread'; + +/** + * True if the node has not expression set nor bindings directives + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only if it's a static node that doesn't need bindings or expressions + */ +function isStaticNode(node) { + return [ + hasExpressions, + findEachAttribute, + findIfAttribute, + isCustomNode, + isSlotNode + ].every(test => !test(node)) +} + +/** + * Check if a node name is part of the browser or builtin javascript api or it belongs to the current scope + * @param { types.NodePath } path - containing the current node visited + * @returns {boolean} true if it's a global api variable + */ +function isGlobal({ scope, node }) { + return Boolean( + isRaw(node) || + isBuiltinAPI(node) || + isBrowserAPI(node) || + isNewExpression(node) || + isNodeInScope(scope, node), + ) +} + +/** + * Checks if the identifier of a given node exists in a scope + * @param {Scope} scope - scope where to search for the identifier + * @param {types.Node} node - node to search for the identifier + * @returns {boolean} true if the node identifier is defined in the given scope + */ +function isNodeInScope(scope, node) { + const traverse = (isInScope = false) => { + types.visit(node, { + visitIdentifier(path) { + if (scope.lookup(getName(path.node))) { + isInScope = true; + } + + this.abort(); + } + }); + + return isInScope + }; + + return traverse() +} + +/** + * True if the node has the isCustom attribute set + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if either it's a riot component or a custom element + */ +function isCustomNode(node) { + return !!(node[IS_CUSTOM_NODE] || hasIsAttribute(node)) +} + +/** + * True the node is + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if it's a slot node + */ +function isSlotNode(node) { + return node.name === SLOT_TAG_NODE_NAME +} + +/** + * True if the node has the isVoid attribute set + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if the node is self closing + */ +function isVoidNode(node) { + return !!node[IS_VOID_NODE] +} + +/** + * True if the riot parser did find a tag node + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the tag nodes + */ +function isTagNode(node) { + return node.type === riotParser.nodeTypes.TAG +} + +/** + * True if the riot parser did find a text node + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the text nodes + */ +function isTextNode(node) { + return node.type === riotParser.nodeTypes.TEXT +} + +/** + * True if the node parsed is the root one + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the root nodes + */ +function isRootNode(node) { + return node.isRoot +} + +/** + * True if the attribute parsed is of type spread one + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if the attribute node is of type spread + */ +function isSpreadAttribute(node) { + return node[IS_SPREAD_ATTRIBUTE] +} + +/** + * True if the node is an attribute and its name is "value" + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for value attribute nodes + */ +function isValueAttribute(node) { + return node.name === 'value' +} + +/** + * True if the DOM node is a progress tag + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true for the progress tags + */ +function isProgressNode(node) { + return node.name === PROGRESS_TAG_NODE_NAME +} + +/** + * True if the node is an attribute and a DOM handler + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for dom listener attribute nodes + */ +const isEventAttribute = (() => { + const EVENT_ATTR_RE = /^on/; + return node => EVENT_ATTR_RE.test(node.name) +})(); + +/** + * True if the node has expressions or expression attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} ditto + */ +function hasExpressions(node) { + return !!( + node.expressions || + // has expression attributes + (getNodeAttributes(node).some(attribute => hasExpressions(attribute))) || + // has child text nodes with expressions + (node.nodes && node.nodes.some(node => isTextNode(node) && hasExpressions(node))) + ) +} + +/** + * True if the node is a directive having its own template + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the IF EACH and TAG bindings + */ +function hasItsOwnTemplate(node) { + return [ + findEachAttribute, + findIfAttribute, + isCustomNode + ].some(test => test(node)) +} + +const hasIfAttribute = compose(Boolean, findIfAttribute); +const hasEachAttribute = compose(Boolean, findEachAttribute); +const hasIsAttribute = compose(Boolean, findIsAttribute); +const hasKeyAttribute = compose(Boolean, findKeyAttribute); + +/** + * Find the attribute node + * @param { string } name - name of the attribute we want to find + * @param { riotParser.nodeTypes.TAG } node - a tag node + * @returns { riotParser.nodeTypes.ATTR } attribute node + */ +function findAttribute(name, node) { + return node.attributes && node.attributes.find(attr => getName(attr) === name) +} + +function findIfAttribute(node) { + return findAttribute(IF_DIRECTIVE, node) +} + +function findEachAttribute(node) { + return findAttribute(EACH_DIRECTIVE, node) +} + +function findKeyAttribute(node) { + return findAttribute(KEY_ATTRIBUTE, node) +} + +function findIsAttribute(node) { + return findAttribute(IS_DIRECTIVE, node) +} + +/** + * Find all the node attributes that are not expressions + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} list of all the static attributes + */ +function findStaticAttributes(node) { + return getNodeAttributes(node).filter(attribute => !hasExpressions(attribute)) +} + +/** + * Find all the node attributes that have expressions + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} list of all the dynamic attributes + */ +function findDynamicAttributes(node) { + return getNodeAttributes(node).filter(hasExpressions) +} + +/** + * Unescape the user escaped chars + * @param {string} string - input string + * @param {string} char - probably a '{' or anything the user want's to escape + * @returns {string} cleaned up string + */ +function unescapeChar(string, char) { + return string.replace(RegExp(`\\\\${char}`, 'gm'), char) +} + +const scope = builders.identifier(SCOPE); +const getName = node => node && node.name ? node.name : node; + +/** + * Replace the path scope with a member Expression + * @param { types.NodePath } path - containing the current node visited + * @param { types.Node } property - node we want to prefix with the scope identifier + * @returns {undefined} this is a void function + */ +function replacePathScope(path, property) { + path.replace(builders.memberExpression( + scope, + property, + false + )); +} + +/** + * Change the nodes scope adding the `scope` prefix + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return false if we want to stop the tree traversal + * @context { types.visit } + */ +function updateNodeScope(path) { + if (!isGlobal(path)) { + replacePathScope(path, path.node); + + return false + } + + this.traverse(path); +} + +/** + * Change the scope of the member expressions + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return always false because we want to check only the first node object + */ +function visitMemberExpression(path) { + if (!isGlobal(path) && !isGlobal({ node: path.node.object, scope: path.scope })) { + if (path.value.computed) { + this.traverse(path); + } else if (isBinaryExpression(path.node.object) || path.node.object.computed) { + this.traverse(path.get('object')); + } else if (!path.node.object.callee) { + replacePathScope(path, isThisExpression(path.node.object) ? path.node.property : path.node); + } else { + this.traverse(path.get('object')); + } + } + + return false +} + + +/** + * Objects properties should be handled a bit differently from the Identifier + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return false if we want to stop the tree traversal + */ +function visitProperty(path) { + const value = path.node.value; + + if (isIdentifier(value)) { + updateNodeScope(path.get('value')); + } else { + this.traverse(path.get('value')); + } + + return false +} + +/** + * The this expressions should be replaced with the scope + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean|undefined } return false if we want to stop the tree traversal + */ +function visitThisExpression(path) { + path.replace(scope); + this.traverse(path); +} + + +/** + * Update the scope of the global nodes + * @param { Object } ast - ast program + * @returns { Object } the ast program with all the global nodes updated + */ +function updateNodesScope(ast) { + const ignorePath = () => false; + + types.visit(ast, { + visitIdentifier: updateNodeScope, + visitMemberExpression, + visitProperty, + visitThisExpression, + visitClassExpression: ignorePath + }); + + return ast +} + +/** + * Convert any expression to an AST tree + * @param { Object } expression - expression parsed by the riot parser + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Object } the ast generated + */ +function createASTFromExpression(expression, sourceFile, sourceCode) { + const code = sourceFile ? + addLineOffset(expression.text, sourceCode, expression) : + expression.text; + + return generateAST(`(${code})`, { + sourceFileName: sourceFile + }) +} + +/** + * Create the bindings template property + * @param {Array} args - arguments to pass to the template function + * @returns {ASTNode} a binding template key + */ +function createTemplateProperty(args) { + return simplePropertyNode( + BINDING_TEMPLATE_KEY, + args ? callTemplateFunction(...args) : nullNode() + ) +} + +/** + * Try to get the expression of an attribute node + * @param { RiotParser.Node.Attribute } attribute - riot parser attribute node + * @returns { RiotParser.Node.Expression } attribute expression value + */ +function getAttributeExpression(attribute) { + return attribute.expressions ? attribute.expressions[0] : { + // if no expression was found try to typecast the attribute value + ...attribute, + text: attribute.value + } +} + +/** + * Wrap the ast generated in a function call providing the scope argument + * @param {Object} ast - function body + * @returns {FunctionExpresion} function having the scope argument injected + */ +function wrapASTInFunctionWithScope(ast) { + return builders.functionExpression( + null, + [scope], + builders.blockStatement([builders.returnStatement( + ast + )]) + ) +} + +/** + * Convert any parser option to a valid template one + * @param { RiotParser.Node.Expression } expression - expression parsed by the riot parser + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Object } a FunctionExpression object + * + * @example + * toScopedFunction('foo + bar') // scope.foo + scope.bar + * + * @example + * toScopedFunction('foo.baz + bar') // scope.foo.baz + scope.bar + */ +function toScopedFunction(expression, sourceFile, sourceCode) { + return compose( + wrapASTInFunctionWithScope, + transformExpression, + )(expression, sourceFile, sourceCode) +} + +/** + * Transform an expression node updating its global scope + * @param {RiotParser.Node.Expr} expression - riot parser expression node + * @param {string} sourceFile - source file + * @param {string} sourceCode - source code + * @returns {ASTExpression} ast expression generated from the riot parser expression node + */ +function transformExpression(expression, sourceFile, sourceCode) { + return compose( + getExpressionAST, + updateNodesScope, + createASTFromExpression + )(expression, sourceFile, sourceCode) +} + +/** + * Get the parsed AST expression of riot expression node + * @param {AST.Program} sourceAST - raw node parsed + * @returns {AST.Expression} program expression output + */ +function getExpressionAST(sourceAST) { + const astBody = sourceAST.program.body; + + return astBody[0] ? astBody[0].expression : astBody +} + +/** + * Create the template call function + * @param {Array|string|Node.Literal} template - template string + * @param {Array} bindings - template bindings provided as AST nodes + * @returns {Node.CallExpression} template call expression + */ +function callTemplateFunction(template, bindings) { + return builders.callExpression(builders.identifier(TEMPLATE_FN), [ + template ? builders.literal(template) : nullNode(), + bindings ? builders.arrayExpression(bindings) : nullNode() + ]) +} + +/** + * Convert any DOM attribute into a valid DOM selector useful for the querySelector API + * @param { string } attributeName - name of the attribute to query + * @returns { string } the attribute transformed to a query selector + */ +const attributeNameToDOMQuerySelector = attributeName => `[${attributeName}]`; + +/** + * Create the properties to query a DOM node + * @param { string } attributeName - attribute name needed to identify a DOM node + * @returns { Array } array containing the selector properties needed for the binding + */ +function createSelectorProperties(attributeName) { + return attributeName ? [ + simplePropertyNode(BINDING_REDUNDANT_ATTRIBUTE_KEY, builders.literal(attributeName)), + simplePropertyNode(BINDING_SELECTOR_KEY, + compose(builders.literal, attributeNameToDOMQuerySelector)(attributeName) + ) + ] : [] +} + +/** + * Clone the node filtering out the selector attribute from the attributes list + * @param {RiotParser.Node} node - riot parser node + * @param {string} selectorAttribute - name of the selector attribute to filter out + * @returns {RiotParser.Node} the node with the attribute cleaned up + */ +function cloneNodeWithoutSelectorAttribute(node, selectorAttribute) { + return { + ...node, + attributes: getAttributesWithoutSelector(getNodeAttributes(node), selectorAttribute) + } +} + + +/** + * Get the node attributes without the selector one + * @param {Array} attributes - attributes list + * @param {string} selectorAttribute - name of the selector attribute to filter out + * @returns {Array} filtered attributes + */ +function getAttributesWithoutSelector(attributes, selectorAttribute) { + if (selectorAttribute) + return attributes.filter(attribute => attribute.name !== selectorAttribute) + + return attributes +} + +/** + * Clean binding or custom attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} only the attributes that are not bindings or directives + */ +function cleanAttributes(node) { + return getNodeAttributes(node).filter(attribute => ![ + IF_DIRECTIVE, + EACH_DIRECTIVE, + KEY_ATTRIBUTE, + SLOT_ATTRIBUTE, + IS_DIRECTIVE + ].includes(attribute.name)) +} + +/** + * Create a root node proxing only its nodes and attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {RiotParser.Node} root node + */ +function createRootNode(node) { + return { + nodes: getChildrenNodes(node), + isRoot: true, + // root nodes shuold't have directives + attributes: cleanAttributes(node) + } +} + +/** + * Get all the child nodes of a RiotParser.Node + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} all the child nodes found + */ +function getChildrenNodes(node) { + return node && node.nodes ? node.nodes : [] +} + +/** + * Get all the attributes of a riot parser node + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} all the attributes find + */ +function getNodeAttributes(node) { + return node.attributes ? node.attributes : [] +} +/** + * Get the name of a custom node transforming it into an expression node + * @param {RiotParser.Node} node - riot parser node + * @returns {RiotParser.Node.Attr} the node name as expression attribute + */ +function getCustomNodeNameAsExpression(node) { + const isAttribute = findIsAttribute(node); + const toRawString = val => `'${val}'`; + + if (isAttribute) { + return isAttribute.expressions ? isAttribute.expressions[0] : { + ...isAttribute, + text: toRawString(isAttribute.value) + } + } + + return { ...node, text: toRawString(getName(node)) } +} + +/** + * Convert all the node static attributes to strings + * @param {RiotParser.Node} node - riot parser node + * @returns {string} all the node static concatenated as string + */ +function staticAttributesToString(node) { + return findStaticAttributes(node) + .map(attribute => attribute[IS_BOOLEAN_ATTRIBUTE] || !attribute.value ? + attribute.name : + `${attribute.name}="${unescapeNode(attribute, 'value').value}"` + ).join(' ') +} + +/** + * Make sure that node escaped chars will be unescaped + * @param {RiotParser.Node} node - riot parser node + * @param {string} key - key property to unescape + * @returns {RiotParser.Node} node with the text property unescaped + */ +function unescapeNode(node, key) { + if (node.unescape) { + return { + ...node, + [key]: unescapeChar(node[key], node.unescape) + } + } + + return node +} + + +/** + * Convert a riot parser opening node into a string + * @param {RiotParser.Node} node - riot parser node + * @returns {string} the node as string + */ +function nodeToString(node) { + const attributes = staticAttributesToString(node); + + switch(true) { + case isTagNode(node): + return `<${node.name}${attributes ? ` ${attributes}` : ''}${isVoidNode(node) ? '/' : ''}>` + case isTextNode(node): + return hasExpressions(node) ? TEXT_NODE_EXPRESSION_PLACEHOLDER : unescapeNode(node, 'text').text + default: + return '' + } +} + +/** + * Close an html node + * @param {RiotParser.Node} node - riot parser node + * @returns {string} the closing tag of the html tag node passed to this function + */ +function closeTag(node) { + return node.name ? `` : '' +} + +/** + * Create a strings array with the `join` call to transform it into a string + * @param {Array} stringsArray - array containing all the strings to concatenate + * @returns {AST.CallExpression} array with a `join` call + */ +function createArrayString(stringsArray) { + return builders.callExpression( + builders.memberExpression( + builders.arrayExpression(stringsArray), + builders.identifier('join'), + false + ), + [builders.literal('')], + ) +} + +/** + * Simple expression bindings might contain multiple expressions like for example: "class="{foo} red {bar}"" + * This helper aims to merge them in a template literal if it's necessary + * @param {RiotParser.Attr} node - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { Object } a template literal expression object + */ +function mergeAttributeExpressions(node, sourceFile, sourceCode) { + if (!node.parts || node.parts.length === 1) { + return transformExpression(node.expressions[0], sourceFile, sourceCode) + } + const stringsArray = [ + ...node.parts.reduce((acc, str) => { + const expression = node.expressions.find(e => e.text.trim() === str); + + return [ + ...acc, + expression ? transformExpression(expression, sourceFile, sourceCode) : builders.literal(str) + ] + }, []) + ].filter(expr => !isLiteral(expr) || expr.value); + + + return createArrayString(stringsArray) +} + +/** + * Create a selector that will be used to find the node via dom-bindings + * @param {number} id - temporary variable that will be increased anytime this function will be called + * @returns {string} selector attribute needed to bind a riot expression + */ +const createBindingSelector = (function createSelector(id = 0) { + return () => `${BINDING_SELECTOR_PREFIX}${id++}` +}()); + +/** + * Create an attribute evaluation function + * @param {RiotParser.Attr} sourceNode - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { AST.Node } an AST function expression to evaluate the attribute value + */ +function createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) { + return hasExpressions(sourceNode) ? + // dynamic attribute + wrapASTInFunctionWithScope(mergeAttributeExpressions(sourceNode, sourceFile, sourceCode)) : + // static attribute + builders.functionExpression( + null, + [], + builders.blockStatement([ + builders.returnStatement(builders.literal(sourceNode.value || true)) + ]), + ) +} + +/** + * Simple clone deep function, do not use it for classes or recursive objects! + * @param {*} source - possibily an object to clone + * @returns {*} the object we wanted to clone + */ +function cloneDeep(source) { + return JSON.parse(JSON.stringify(source)) +} + +const getEachItemName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[0] : expression.left; +const getEachIndexName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[1] : null; +const getEachValue = expression => expression.right; +const nameToliteral = compose(builders.literal, getName); + +const generateEachItemNameKey = expression => simplePropertyNode( + BINDING_ITEM_NAME_KEY, + compose(nameToliteral, getEachItemName)(expression) +); + +const generateEachIndexNameKey = expression => simplePropertyNode( + BINDING_INDEX_NAME_KEY, + compose(nameToliteral, getEachIndexName)(expression) +); + +const generateEachEvaluateKey = (expression, eachExpression, sourceFile, sourceCode) => simplePropertyNode( + BINDING_EVALUATE_KEY, + compose( + e => toScopedFunction(e, sourceFile, sourceCode), + e => ({ + ...eachExpression, + text: generateJavascript(e).code + }), + getEachValue + )(expression) +); + +/** + * Get the each expression properties to create properly the template binding + * @param { DomBinding.Expression } eachExpression - original each expression data + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Array } AST nodes that are needed to build an each binding + */ +function generateEachExpressionProperties(eachExpression, sourceFile, sourceCode) { + const ast = createASTFromExpression(eachExpression, sourceFile, sourceCode); + const body = ast.program.body; + const firstNode = body[0]; + + if (!isExpressionStatement(firstNode)) { + panic(`The each directives supported should be of type "ExpressionStatement",you have provided a "${firstNode.type}"`); + } + + const { expression } = firstNode; + + return [ + generateEachItemNameKey(expression), + generateEachIndexNameKey(expression), + generateEachEvaluateKey(expression, eachExpression, sourceFile, sourceCode) + ] +} + +/** + * Transform a RiotParser.Node.Tag into an each binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an each binding node + */ +function createEachBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + const [ifAttribute, eachAttribute, keyAttribute] = [ + findIfAttribute, + findEachAttribute, + findKeyAttribute + ].map(f => f(sourceNode)); + const attributeOrNull = attribute => attribute ? toScopedFunction(getAttributeExpression(attribute), sourceFile, sourceCode) : nullNode(); + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(EACH_BINDING_TYPE), + false + ), + ), + simplePropertyNode(BINDING_GET_KEY_KEY, attributeOrNull(keyAttribute)), + simplePropertyNode(BINDING_CONDITION_KEY, attributeOrNull(ifAttribute)), + createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)), + ...createSelectorProperties(selectorAttribute), + ...compose(generateEachExpressionProperties, getAttributeExpression)(eachAttribute) + ]) +} + +/** + * Transform a RiotParser.Node.Tag into an if binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { stiring } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an if binding node + */ +function createIfBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + const ifAttribute = findIfAttribute(sourceNode); + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(IF_BINDING_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + toScopedFunction(ifAttribute.expressions[0], sourceFile, sourceCode) + ), + ...createSelectorProperties(selectorAttribute), + createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)) + ]) +} + +/** + * Create a simple attribute expression + * @param {RiotParser.Node.Attr} sourceNode - the custom tag + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} object containing the expression binding keys + */ +function createAttributeExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(ATTRIBUTE_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode(BINDING_NAME_KEY, isSpreadAttribute(sourceNode) ? nullNode() : builders.literal(sourceNode.name)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) +} + +/** + * Create a simple event expression + * @param {RiotParser.Node.Attr} sourceNode - attribute containing the event handlers + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} object containing the expression binding keys + */ +function createEventExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(EVENT_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode(BINDING_NAME_KEY, builders.literal(sourceNode.name)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) +} + +/** + * Generate the pure immutable string chunks from a RiotParser.Node.Text + * @param {RiotParser.Node.Text} node - riot parser text node + * @param {string} sourceCode sourceCode - source code + * @returns {Array} array containing the immutable string chunks + */ +function generateLiteralStringChunksFromNode(node, sourceCode) { + return node.expressions.reduce((chunks, expression, index) => { + const start = index ? node.expressions[index - 1].end : node.start; + + chunks.push(sourceCode.substring(start, expression.start)); + + // add the tail to the string + if (index === node.expressions.length - 1) + chunks.push(sourceCode.substring(expression.end, node.end)); + + return chunks + }, []).map(str => node.unescape ? unescapeChar(str, node.unescape) : str) +} + +/** + * Simple bindings might contain multiple expressions like for example: "{foo} and {bar}" + * This helper aims to merge them in a template literal if it's necessary + * @param {RiotParser.Node} node - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { Object } a template literal expression object + */ +function mergeNodeExpressions(node, sourceFile, sourceCode) { + if (node.parts.length === 1) + return transformExpression(node.expressions[0], sourceFile, sourceCode) + + const pureStringChunks = generateLiteralStringChunksFromNode(node, sourceCode); + const stringsArray = pureStringChunks.reduce((acc, str, index) => { + const expr = node.expressions[index]; + + return [ + ...acc, + builders.literal(str), + expr ? transformExpression(expr, sourceFile, sourceCode) : nullNode() + ] + }, []) + // filter the empty literal expressions + .filter(expr => !isLiteral(expr) || expr.value); + + return createArrayString(stringsArray) +} + +/** + * Create a text expression + * @param {RiotParser.Node.Text} sourceNode - text node to parse + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {number} childNodeIndex - position of the child text node in its parent children nodes + * @returns {AST.Node} object containing the expression binding keys + */ +function createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(TEXT_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_CHILD_NODE_INDEX_KEY, + builders.literal(childNodeIndex) + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + wrapASTInFunctionWithScope( + mergeNodeExpressions(sourceNode, sourceFile, sourceCode) + ) + ) + ]) +} + +function createValueExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(VALUE_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) +} + +function createExpression(sourceNode, sourceFile, sourceCode, childNodeIndex, parentNode) { + switch (true) { + case isTextNode(sourceNode): + return createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) + // progress nodes value attributes will be rendered as attributes + // see https://github.com/riot/compiler/issues/122 + case isValueAttribute(sourceNode) && domNodes.hasValueAttribute(parentNode.name) && !isProgressNode(parentNode): + return createValueExpression(sourceNode, sourceFile, sourceCode) + case isEventAttribute(sourceNode): + return createEventExpression(sourceNode, sourceFile, sourceCode) + default: + return createAttributeExpression(sourceNode, sourceFile, sourceCode) + } +} + +/** + * Create the attribute expressions + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {Array} array containing all the attribute expressions + */ +function createAttributeExpressions(sourceNode, sourceFile, sourceCode) { + return findDynamicAttributes(sourceNode) + .map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)) +} + +/** + * Create the text node expressions + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {Array} array containing all the text node expressions + */ +function createTextNodeExpressions(sourceNode, sourceFile, sourceCode) { + const childrenNodes = getChildrenNodes(sourceNode); + + return childrenNodes + .filter(isTextNode) + .filter(hasExpressions) + .map(node => createExpression( + node, + sourceFile, + sourceCode, + childrenNodes.indexOf(node), + sourceNode + )) +} + +/** + * Add a simple binding to a riot parser node + * @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an each binding node + */ +function createSimpleBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.objectExpression([ + ...createSelectorProperties(selectorAttribute), + simplePropertyNode( + BINDING_EXPRESSIONS_KEY, + builders.arrayExpression([ + ...createTextNodeExpressions(sourceNode, sourceFile, sourceCode), + ...createAttributeExpressions(sourceNode, sourceFile, sourceCode) + ]) + ) + ]) +} + +/** + * Transform a RiotParser.Node.Tag of type slot into a slot binding + * @param { RiotParser.Node.Tag } sourceNode - slot node + * @param { string } selectorAttribute - attribute needed to select the target node + * @returns { AST.Node } a slot binding node + */ +function createSlotBinding(sourceNode, selectorAttribute) { + const slotNameAttribute = findAttribute(NAME_ATTRIBUTE, sourceNode); + const slotName = slotNameAttribute ? slotNameAttribute.value : DEFAULT_SLOT_NAME; + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(SLOT_BINDING_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_NAME_KEY, + builders.literal(slotName) + ), + ...createSelectorProperties(selectorAttribute) + ]) +} + +/** + * Find the slots in the current component and group them under the same id + * @param {RiotParser.Node.Tag} sourceNode - the custom tag + * @returns {Object} object containing all the slots grouped by name + */ +function groupSlots(sourceNode) { + return getChildrenNodes(sourceNode).reduce((acc, node) => { + const slotAttribute = findSlotAttribute(node); + + if (slotAttribute) { + acc[slotAttribute.value] = node; + } else { + acc.default = createRootNode({ + nodes: [...getChildrenNodes(acc.default), node] + }); + } + + return acc + }, { + default: null + }) +} + +/** + * Create the slot entity to pass to the riot-dom bindings + * @param {string} id - slot id + * @param {RiotParser.Node.Tag} sourceNode - slot root node + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} ast node containing the slot object properties + */ +function buildSlot(id, sourceNode, sourceFile, sourceCode) { + const cloneNode = { + ...sourceNode, + // avoid to render the slot attribute + attributes: getNodeAttributes(sourceNode).filter(attribute => attribute.name !== SLOT_ATTRIBUTE) + }; + const [html, bindings] = build(cloneNode, sourceFile, sourceCode); + + return builders.objectExpression([ + simplePropertyNode(BINDING_ID_KEY, builders.literal(id)), + simplePropertyNode(BINDING_HTML_KEY, builders.literal(html)), + simplePropertyNode(BINDING_BINDINGS_KEY, builders.arrayExpression(bindings)) + ]) +} + +/** + * Create the AST array containing the slots + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.ArrayExpression} array containing the attributes to bind + */ +function createSlotsArray(sourceNode, sourceFile, sourceCode) { + return builders.arrayExpression([ + ...compose( + slots => slots.map(([key, value]) => buildSlot(key, value, sourceFile, sourceCode)), + slots => slots.filter(([,value]) => value), + Object.entries, + groupSlots + )(sourceNode) + ]) +} + +/** + * Create the AST array containing the attributes to bind to this node + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.ArrayExpression} array containing the slot objects + */ +function createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.arrayExpression([ + ...compose( + attributes => attributes.map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)), + attributes => getAttributesWithoutSelector(attributes, selectorAttribute), // eslint-disable-line + cleanAttributes + )(sourceNode) + ]) +} + +/** + * Find the slot attribute if it exists + * @param {RiotParser.Node.Tag} sourceNode - the custom tag + * @returns {RiotParser.Node.Attr|undefined} the slot attribute found + */ +function findSlotAttribute(sourceNode) { + return getNodeAttributes(sourceNode).find(attribute => attribute.name === SLOT_ATTRIBUTE) +} + +/** + * Transform a RiotParser.Node.Tag into a tag binding + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } tag binding node + */ +function createTagBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(TAG_BINDING_TYPE), + false + ), + ), + simplePropertyNode(BINDING_GET_COMPONENT_KEY, builders.identifier(GET_COMPONENT_FN)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + toScopedFunction(getCustomNodeNameAsExpression(sourceNode), sourceFile, sourceCode) + ), + simplePropertyNode(BINDING_SLOTS_KEY, createSlotsArray(sourceNode, sourceFile, sourceCode)), + simplePropertyNode( + BINDING_ATTRIBUTES_KEY, + createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) + ), + ...createSelectorProperties(selectorAttribute) + ]) +} + +const BuildingState = Object.freeze({ + html: [], + bindings: [], + parent: null +}); + +/** + * Nodes having bindings should be cloned and new selector properties should be added to them + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} bindingsSelector - temporary string to identify the current node + * @returns {RiotParser.Node} the original node parsed having the new binding selector attribute + */ +function createBindingsTag(sourceNode, bindingsSelector) { + if (!bindingsSelector) return sourceNode + + return { + ...sourceNode, + // inject the selector bindings into the node attributes + attributes: [{ + name: bindingsSelector, + value: bindingsSelector + }, ...getNodeAttributes(sourceNode)] + } +} + +/** + * Create a generic dynamic node (text or tag) and generate its bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ +function createDynamicNode(sourceNode, sourceFile, sourceCode, state) { + switch (true) { + case isTextNode(sourceNode): + // text nodes will not have any bindings + return [nodeToString(sourceNode), []] + default: + return createTagWithBindings(sourceNode, sourceFile, sourceCode) + } +} + +/** + * Create only a dynamic tag node with generating a custom selector and its bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ +function createTagWithBindings(sourceNode, sourceFile, sourceCode) { + const bindingsSelector = isRootNode(sourceNode) ? null : createBindingSelector(); + const cloneNode = createBindingsTag(sourceNode, bindingsSelector); + const tagOpeningHTML = nodeToString(cloneNode); + + switch(true) { + // EACH bindings have prio 1 + case hasEachAttribute(cloneNode): + return [tagOpeningHTML, [createEachBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // IF bindings have prio 2 + case hasIfAttribute(cloneNode): + return [tagOpeningHTML, [createIfBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // TAG bindings have prio 3 + case isCustomNode(cloneNode): + return [tagOpeningHTML, [createTagBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // slot tag + case isSlotNode(cloneNode): + return [tagOpeningHTML, [createSlotBinding(cloneNode, bindingsSelector)]] + // this node has expressions bound to it + default: + return [tagOpeningHTML, [createSimpleBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + } +} + +/** + * Parse a node trying to extract its template and bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ +function parseNode(sourceNode, sourceFile, sourceCode, state) { + // static nodes have no bindings + if (isStaticNode(sourceNode)) return [nodeToString(sourceNode), []] + return createDynamicNode(sourceNode, sourceFile, sourceCode) +} + +/** + * Create the tag binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @param { string } selector - binding selector + * @returns { Array } array with only the tag binding AST + */ +function createNestedBindings(sourceNode, sourceFile, sourceCode, selector) { + const mightBeARiotComponent = isCustomNode(sourceNode); + + return mightBeARiotComponent ? [null, [ + createTagBinding( + cloneNodeWithoutSelectorAttribute(sourceNode, selector), + null, + sourceFile, + sourceCode + )] + ] : build(createRootNode(sourceNode), sourceFile, sourceCode) +} + +/** + * Build the template and the bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and the dom bindings + */ +function build( + sourceNode, + sourceFile, + sourceCode, + state +) { + if (!sourceNode) panic('Something went wrong with your tag DOM parsing, your tag template can\'t be created'); + + const [nodeHTML, nodeBindings] = parseNode(sourceNode, sourceFile, sourceCode); + const childrenNodes = getChildrenNodes(sourceNode); + const currentState = { ...cloneDeep(BuildingState), ...state }; + + // mutate the original arrays + currentState.html.push(...nodeHTML); + currentState.bindings.push(...nodeBindings); + + // do recursion if + // this tag has children and it has no special directives bound to it + if (childrenNodes.length && !hasItsOwnTemplate(sourceNode)) { + childrenNodes.forEach(node => build(node, sourceFile, sourceCode, { parent: sourceNode, ...currentState })); + } + + // close the tag if it's not a void one + if (isTagNode(sourceNode) && !isVoidNode(sourceNode)) { + currentState.html.push(closeTag(sourceNode)); + } + + return [ + currentState.html.join(''), + currentState.bindings + ] +} + +const templateFunctionArguments = [ + TEMPLATE_FN, + EXPRESSION_TYPES, + BINDING_TYPES, + GET_COMPONENT_FN +].map(builders.identifier); + +/** + * Create the content of the template function + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.BlockStatement} the content of the template function + */ +function createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) { + return builders.blockStatement([ + builders.returnStatement( + callTemplateFunction( + ...build( + createRootNode(sourceNode), + sourceFile, + sourceCode + ) + ) + ) + ]) +} + +/** + * Extend the AST adding the new template property containing our template call to render the component + * @param { Object } ast - current output ast + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @returns { Object } the output ast having the "template" key + */ +function extendTemplateProperty(ast, sourceFile, sourceCode, sourceNode) { + types.visit(ast, { + visitProperty(path) { + if (path.value.key.value === TAG_TEMPLATE_PROPERTY) { + path.value.value = builders.functionExpression( + null, + templateFunctionArguments, + createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) + ); + + return false + } + + this.traverse(path); + } + }); + + return ast +} + +/** + * Generate the component template logic + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @param { string } source - original component source code + * @param { Object } meta - compilation meta information + * @param { AST } ast - current AST output + * @returns { AST } the AST generated + */ +function template(sourceNode, source, meta, ast) { + const { options } = meta; + return extendTemplateProperty(ast, options.file, source, sourceNode) +} + +const DEFAULT_OPTIONS = { + template: 'default', + file: '[unknown-source-file]', + scopedCss: true +}; + +/** + * Create the initial AST + * @param {string} tagName - the name of the component we have compiled + * @returns { AST } the initial AST + * + * @example + * // the output represents the following string in AST + */ +function createInitialInput({tagName}) { + /* + generates + export default { + ${TAG_CSS_PROPERTY}: null, + ${TAG_LOGIC_PROPERTY}: null, + ${TAG_TEMPLATE_PROPERTY}: null + } + */ + return builders.program([ + builders.exportDefaultDeclaration( + builders.objectExpression([ + simplePropertyNode(TAG_CSS_PROPERTY, nullNode()), + simplePropertyNode(TAG_LOGIC_PROPERTY, nullNode()), + simplePropertyNode(TAG_TEMPLATE_PROPERTY, nullNode()), + simplePropertyNode(TAG_NAME_PROPERTY, builders.literal(tagName)) + ]) + )] + ) +} + +/** + * Make sure the input sourcemap is valid otherwise we ignore it + * @param {SourceMapGenerator} map - preprocessor source map + * @returns {Object} sourcemap as json or nothing + */ +function normaliseInputSourceMap(map) { + const inputSourceMap = sourcemapAsJSON(map); + return isEmptySourcemap(inputSourceMap) ? null : inputSourceMap +} + +/** + * Override the sourcemap content making sure it will always contain the tag source code + * @param {Object} map - sourcemap as json + * @param {string} source - component source code + * @returns {Object} original source map with the "sourcesContent" property overriden + */ +function overrideSourcemapContent(map, source) { + return { + ...map, + sourcesContent: [source] + } +} + +/** + * Create the compilation meta object + * @param { string } source - source code of the tag we will need to compile + * @param { string } options - compiling options + * @returns {Object} meta object + */ +function createMeta(source, options) { + return { + tagName: null, + fragments: null, + options: { + ...DEFAULT_OPTIONS, + ...options + }, + source + } +} + +/** + * Generate the output code source together with the sourcemap + * @param { string } source - source code of the tag we will need to compile + * @param { string } opts - compiling options + * @returns { Output } object containing output code and source map + */ +function compile(source, opts = {}) { + const meta = createMeta(source, opts); + const {options} = meta; + const { code, map } = execute$1('template', options.template, meta, source); + const { template: template$1, css: css$1, javascript: javascript$1 } = riotParser__default(options).parse(code).output; + + // extend the meta object with the result of the parsing + Object.assign(meta, { + tagName: template$1.name, + fragments: { template: template$1, css: css$1, javascript: javascript$1 } + }); + + return compose( + result => ({ ...result, meta }), + result => execute(result, meta), + result => ({ + ...result, + map: overrideSourcemapContent(result.map, source) + }), + ast => meta.ast = ast && generateJavascript(ast, { + sourceMapName: `${options.file}.map`, + inputSourceMap: normaliseInputSourceMap(map) + }), + hookGenerator(template, template$1, code, meta), + hookGenerator(javascript, javascript$1, code, meta), + hookGenerator(css, css$1, code, meta), + )(createInitialInput(meta)) +} + +/** + * Prepare the riot parser node transformers + * @param { Function } transformer - transformer function + * @param { Object } sourceNode - riot parser node + * @param { string } source - component source code + * @param { Object } meta - compilation meta information + * @returns { Promise } object containing output code and source map + */ +function hookGenerator(transformer, sourceNode, source, meta) { + if ( + // filter missing nodes + !sourceNode || + // filter nodes without children + (sourceNode.nodes && !sourceNode.nodes.length) || + // filter empty javascript and css nodes + (!sourceNode.nodes && !sourceNode.text)) { + return result => result + } + + return curry(transformer)(sourceNode, source, meta) +} + +// This function can be used to register new preprocessors +// a preprocessor can target either only the css or javascript nodes +// or the complete tag source file ('template') +const registerPreprocessor = register$1; + +// This function can allow you to register postprocessors that will parse the output code +// here we can run prettifiers, eslint fixes... +const registerPostprocessor = register; + +exports.compile = compile; +exports.createInitialInput = createInitialInput; +exports.registerPostprocessor = registerPostprocessor; +exports.registerPreprocessor = registerPreprocessor; diff --git a/node_modules/@riotjs/compiler/package.json b/node_modules/@riotjs/compiler/package.json new file mode 100644 index 0000000..6fbe992 --- /dev/null +++ b/node_modules/@riotjs/compiler/package.json @@ -0,0 +1,99 @@ +{ + "_from": "@riotjs/compiler@^4.3.11", + "_id": "@riotjs/compiler@4.3.11", + "_inBundle": false, + "_integrity": "sha512-3TpOuoiXWSLGvcvZRfhJLdpRpwJihmT+J+NB2nWXA5/8+23x1soUoPmBtRi9Jo2xLInsh3J1/q5tQ8LjXLc2eQ==", + "_location": "/@riotjs/compiler", + "_phantomChildren": {}, + "_requested": { + "type": "range", + "registry": true, + "raw": "@riotjs/compiler@^4.3.11", + "name": "@riotjs/compiler", + "escapedName": "@riotjs%2fcompiler", + "scope": "@riotjs", + "rawSpec": "^4.3.11", + "saveSpec": null, + "fetchSpec": "^4.3.11" + }, + "_requiredBy": [ + "/riot" + ], + "_resolved": "https://registry.npmjs.org/@riotjs/compiler/-/compiler-4.3.11.tgz", + "_shasum": "b068216c19092d524dc0a7fbce1572a23830607b", + "_spec": "@riotjs/compiler@^4.3.11", + "_where": "/home/herrhase/Workspace/tentakelfabrik/tiny-components/tiny-one-page/node_modules/riot", + "author": { + "name": "Gianluca Guarini", + "email": "gianluca.guarini@gmail.com", + "url": "http://gianlucaguarini.com" + }, + "bugs": { + "url": "https://github.com/riot/compiler/issues" + }, + "bundleDependencies": false, + "dependencies": { + "@riotjs/dom-bindings": "^4.2.5", + "@riotjs/parser": "^4.0.3", + "acorn": "^7.0.0", + "cssesc": "^3.0.0", + "cumpa": "^1.0.1", + "curri": "^1.0.1", + "dom-nodes": "^1.1.3", + "globals": "^12.0.0", + "recast": "^0.18.2", + "source-map": "^0.7.3" + }, + "deprecated": false, + "description": "Compiler for riot .tag files", + "devDependencies": { + "chai": "^4.2.0", + "coveralls": "^3.0.6", + "eslint": "^6.2.1", + "eslint-config-riot": "^3.0.0", + "esm": "^3.2.25", + "mocha": "^6.2.0", + "nyc": "^14.1.1", + "rollup": "^1.20.1", + "rollup-plugin-alias": "^2.0.0", + "rollup-plugin-commonjs": "^10.0.2", + "rollup-plugin-json": "^4.0.0", + "rollup-plugin-node-builtins": "^2.1.2", + "rollup-plugin-node-resolve": "^5.2.0", + "shelljs": "^0.8.3" + }, + "files": [ + "dist", + "src" + ], + "homepage": "https://github.com/riot/compiler#readme", + "jsnext:main": "dist/index.esm.js", + "keywords": [ + "riot", + "Riot.js", + "components", + "custom components", + "custom elements", + "compiler" + ], + "license": "MIT", + "main": "dist/index.js", + "module": "dist/index.esm.js", + "name": "@riotjs/compiler", + "repository": { + "type": "git", + "url": "git+https://github.com/riot/compiler.git" + }, + "scripts": { + "build": "rollup -c build/rollup.node.config.js && rollup -c build/rollup.browser.config.js", + "cov": "nyc report --reporter=text-lcov | coveralls", + "cov-html": "nyc report --reporter=html", + "debug": "mocha --inspect --inspect-brk -r esm test/*.spec.js test/**/*.spec.js", + "lint": "eslint src/ test/ build/", + "postest": "npm run cov-html", + "prepare": "npm i pug@2.0.3 node-sass@4.12.0 @babel/core@7 @babel/preset-env@7 --no-save", + "prepublishOnly": "npm run build && npm run test", + "test": "npm run lint && nyc mocha -r esm test/*.spec.js test/**/*.spec.js" + }, + "version": "4.3.11" +} diff --git a/node_modules/@riotjs/compiler/src/.DS_Store b/node_modules/@riotjs/compiler/src/.DS_Store new file mode 100644 index 0000000..57c470d Binary files /dev/null and b/node_modules/@riotjs/compiler/src/.DS_Store differ diff --git a/node_modules/@riotjs/compiler/src/constants.js b/node_modules/@riotjs/compiler/src/constants.js new file mode 100644 index 0000000..0e5b934 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/constants.js @@ -0,0 +1,4 @@ +export const TAG_LOGIC_PROPERTY = 'exports' +export const TAG_CSS_PROPERTY = 'css' +export const TAG_TEMPLATE_PROPERTY = 'template' +export const TAG_NAME_PROPERTY = 'name' \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/generators/.DS_Store b/node_modules/@riotjs/compiler/src/generators/.DS_Store new file mode 100644 index 0000000..eb03002 Binary files /dev/null and b/node_modules/@riotjs/compiler/src/generators/.DS_Store differ diff --git a/node_modules/@riotjs/compiler/src/generators/css/index.js b/node_modules/@riotjs/compiler/src/generators/css/index.js new file mode 100644 index 0000000..77a52ab --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/css/index.js @@ -0,0 +1,124 @@ +import {builders, types} from '../../utils/build-types' +import {TAG_CSS_PROPERTY} from '../../constants' +import cssEscape from 'cssesc' +import getPreprocessorTypeByAttribute from '../../utils/get-preprocessor-type-by-attribute' +import preprocess from '../../utils/preprocess-node' + +/** + * Matches valid, multiline JavaScript comments in almost all its forms. + * @const {RegExp} + * @static + */ +const R_MLCOMMS = /\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//g + +/** + * Source for creating regexes matching valid quoted, single-line JavaScript strings. + * It recognizes escape characters, including nested quotes and line continuation. + * @const {string} + */ +const S_LINESTR = /"[^"\n\\]*(?:\\[\S\s][^"\n\\]*)*"|'[^'\n\\]*(?:\\[\S\s][^'\n\\]*)*'/.source + +/** + * Matches CSS selectors, excluding those beginning with '@' and quoted strings. + * @const {RegExp} + */ + +const CSS_SELECTOR = RegExp(`([{}]|^)[; ]*((?:[^@ ;{}][^{}]*)?[^@ ;{}:] ?)(?={)|${S_LINESTR}`, 'g') + +/** + * Parses styles enclosed in a "scoped" tag + * The "css" string is received without comments or surrounding spaces. + * + * @param {string} tag - Tag name of the root element + * @param {string} css - The CSS code + * @returns {string} CSS with the styles scoped to the root element + */ +function scopedCSS(tag, css) { + const host = ':host' + const selectorsBlacklist = ['from', 'to'] + + return css.replace(CSS_SELECTOR, function(m, p1, p2) { + // skip quoted strings + if (!p2) return m + + // we have a selector list, parse each individually + p2 = p2.replace(/[^,]+/g, function(sel) { + const s = sel.trim() + + // skip selectors already using the tag name + if (s.indexOf(tag) === 0) { + return sel + } + + // skips the keywords and percents of css animations + if (!s || selectorsBlacklist.indexOf(s) > -1 || s.slice(-1) === '%') { + return sel + } + + // replace the `:host` pseudo-selector, where it is, with the root tag name; + // if `:host` was not included, add the tag name as prefix, and mirror all + // `[data-is]` + if (s.indexOf(host) < 0) { + return `${tag} ${s},[is="${tag}"] ${s}` + } else { + return `${s.replace(host, tag)},${ + s.replace(host, `[is="${tag}"]`)}` + } + }) + + // add the danling bracket char and return the processed selector list + return p1 ? `${p1} ${p2}` : p2 + }) +} + +/** + * Remove comments, compact and trim whitespace + * @param { string } code - compiled css code + * @returns { string } css code normalized + */ +function compactCss(code) { + return code.replace(R_MLCOMMS, '').replace(/\s+/g, ' ').trim() +} + +const escapeBackslashes = s => s.replace(/\\/g, '\\\\') +const escapeIdentifier = identifier => escapeBackslashes(cssEscape(identifier, { + isIdentifier: true +})) + +/** + * Generate the component css + * @param { Object } sourceNode - node generated by the riot compiler + * @param { string } source - original component source code + * @param { Object } meta - compilation meta information + * @param { AST } ast - current AST output + * @returns { AST } the AST generated + */ +export default function css(sourceNode, source, meta, ast) { + const preprocessorName = getPreprocessorTypeByAttribute(sourceNode) + const { options } = meta + const preprocessorOutput = preprocess('css', preprocessorName, meta, sourceNode.text) + const normalizedCssCode = compactCss(preprocessorOutput.code) + const escapedCssIdentifier = escapeIdentifier(meta.tagName) + + const cssCode = (options.scopedCss ? + scopedCSS(escapedCssIdentifier, escapeBackslashes(normalizedCssCode)) : + escapeBackslashes(normalizedCssCode) + ).trim() + + types.visit(ast, { + visitProperty(path) { + if (path.value.key.value === TAG_CSS_PROPERTY) { + path.value.value = builders.templateLiteral( + [builders.templateElement({ raw: cssCode, cooked: '' }, false)], + [] + ) + + return false + } + + this.traverse(path) + } + }) + + return ast +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/generators/javascript/index.js b/node_modules/@riotjs/compiler/src/generators/javascript/index.js new file mode 100644 index 0000000..49e4e54 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/javascript/index.js @@ -0,0 +1,92 @@ +import {TAG_LOGIC_PROPERTY} from '../../constants' +import addLinesOffset from '../../utils/add-lines-offset' +import generateAST from '../../utils/generate-ast' +import getPreprocessorTypeByAttribute from '../../utils/get-preprocessor-type-by-attribute' +import isEmptySourcemap from '../../utils/is-empty-sourcemap' +import {isExportDefaultStatement} from '../../utils/ast-nodes-checks' +import preprocess from '../../utils/preprocess-node' +import sourcemapToJSON from '../../utils/sourcemap-as-json' +import {types} from '../../utils/build-types' + +/** + * Find the export default statement + * @param { Array } body - tree structure containing the program code + * @returns { Object } node containing only the code of the export default statement + */ +function findExportDefaultStatement(body) { + return body.find(isExportDefaultStatement) +} + +/** + * Find all the code in an ast program except for the export default statements + * @param { Array } body - tree structure containing the program code + * @returns { Array } array containing all the program code except the export default expressions + */ +function filterNonExportDefaultStatements(body) { + return body.filter(node => !isExportDefaultStatement(node)) +} + +/** + * Get the body of the AST structure + * @param { Object } ast - ast object generated by recast + * @returns { Array } array containing the program code + */ +function getProgramBody(ast) { + return ast.body || ast.program.body +} + +/** + * Extend the AST adding the new tag method containing our tag sourcecode + * @param { Object } ast - current output ast + * @param { Object } exportDefaultNode - tag export default node + * @returns { Object } the output ast having the "tag" key extended with the content of the export default + */ +function extendTagProperty(ast, exportDefaultNode) { + types.visit(ast, { + visitProperty(path) { + if (path.value.key.value === TAG_LOGIC_PROPERTY) { + path.value.value = exportDefaultNode.declaration + return false + } + + this.traverse(path) + } + }) + + return ast +} + +/** + * Generate the component javascript logic + * @param { Object } sourceNode - node generated by the riot compiler + * @param { string } source - original component source code + * @param { Object } meta - compilation meta information + * @param { AST } ast - current AST output + * @returns { AST } the AST generated + */ +export default function javascript(sourceNode, source, meta, ast) { + const preprocessorName = getPreprocessorTypeByAttribute(sourceNode) + const javascriptNode = addLinesOffset(sourceNode.text.text, source, sourceNode) + const { options } = meta + const preprocessorOutput = preprocess('javascript', preprocessorName, meta, { + ...sourceNode, + text: javascriptNode + }) + const inputSourceMap = sourcemapToJSON(preprocessorOutput.map) + const generatedAst = generateAST(preprocessorOutput.code, { + sourceFileName: options.file, + inputSourceMap: isEmptySourcemap(inputSourceMap) ? null : inputSourceMap + }) + const generatedAstBody = getProgramBody(generatedAst) + const bodyWithoutExportDefault = filterNonExportDefaultStatements(generatedAstBody) + const exportDefaultNode = findExportDefaultStatement(generatedAstBody) + const outputBody = getProgramBody(ast) + + // add to the ast the "private" javascript content of our tag script node + outputBody.unshift(...bodyWithoutExportDefault) + + // convert the export default adding its content to the "tag" property exported + if (exportDefaultNode) extendTagProperty(ast, exportDefaultNode) + + return ast +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/generators/template/bindings/each.js b/node_modules/@riotjs/compiler/src/generators/template/bindings/each.js new file mode 100644 index 0000000..240afd1 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/bindings/each.js @@ -0,0 +1,110 @@ +import { + BINDING_CONDITION_KEY, + BINDING_EVALUATE_KEY, + BINDING_GET_KEY_KEY, + BINDING_INDEX_NAME_KEY, + BINDING_ITEM_NAME_KEY, + BINDING_TYPES, + BINDING_TYPE_KEY, + EACH_BINDING_TYPE +} from '../constants' +import { + createASTFromExpression, + createSelectorProperties, + createTemplateProperty, + getAttributeExpression, + getName, + toScopedFunction +} from '../utils' +import { findEachAttribute, findIfAttribute, findKeyAttribute } from '../find' +import {isExpressionStatement, isSequenceExpression} from '../../../utils/ast-nodes-checks' +import {nullNode, simplePropertyNode} from '../../../utils/custom-ast-nodes' +import {builders} from '../../../utils/build-types' +import compose from 'cumpa' +import {createNestedBindings} from '../builder' +import generateJavascript from '../../../utils/generate-javascript' +import panic from '../../../utils/panic' + +const getEachItemName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[0] : expression.left +const getEachIndexName = expression => isSequenceExpression(expression.left) ? expression.left.expressions[1] : null +const getEachValue = expression => expression.right +const nameToliteral = compose(builders.literal, getName) + +const generateEachItemNameKey = expression => simplePropertyNode( + BINDING_ITEM_NAME_KEY, + compose(nameToliteral, getEachItemName)(expression) +) + +const generateEachIndexNameKey = expression => simplePropertyNode( + BINDING_INDEX_NAME_KEY, + compose(nameToliteral, getEachIndexName)(expression) +) + +const generateEachEvaluateKey = (expression, eachExpression, sourceFile, sourceCode) => simplePropertyNode( + BINDING_EVALUATE_KEY, + compose( + e => toScopedFunction(e, sourceFile, sourceCode), + e => ({ + ...eachExpression, + text: generateJavascript(e).code + }), + getEachValue + )(expression) +) + +/** + * Get the each expression properties to create properly the template binding + * @param { DomBinding.Expression } eachExpression - original each expression data + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Array } AST nodes that are needed to build an each binding + */ +export function generateEachExpressionProperties(eachExpression, sourceFile, sourceCode) { + const ast = createASTFromExpression(eachExpression, sourceFile, sourceCode) + const body = ast.program.body + const firstNode = body[0] + + if (!isExpressionStatement(firstNode)) { + panic(`The each directives supported should be of type "ExpressionStatement",you have provided a "${firstNode.type}"`) + } + + const { expression } = firstNode + + return [ + generateEachItemNameKey(expression), + generateEachIndexNameKey(expression), + generateEachEvaluateKey(expression, eachExpression, sourceFile, sourceCode) + ] +} + +/** + * Transform a RiotParser.Node.Tag into an each binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an each binding node + */ +export default function createEachBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + const [ifAttribute, eachAttribute, keyAttribute] = [ + findIfAttribute, + findEachAttribute, + findKeyAttribute + ].map(f => f(sourceNode)) + const attributeOrNull = attribute => attribute ? toScopedFunction(getAttributeExpression(attribute), sourceFile, sourceCode) : nullNode() + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(EACH_BINDING_TYPE), + false + ), + ), + simplePropertyNode(BINDING_GET_KEY_KEY, attributeOrNull(keyAttribute)), + simplePropertyNode(BINDING_CONDITION_KEY, attributeOrNull(ifAttribute)), + createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)), + ...createSelectorProperties(selectorAttribute), + ...compose(generateEachExpressionProperties, getAttributeExpression)(eachAttribute) + ]) +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/bindings/if.js b/node_modules/@riotjs/compiler/src/generators/template/bindings/if.js new file mode 100644 index 0000000..58b5cf1 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/bindings/if.js @@ -0,0 +1,43 @@ +import { + BINDING_EVALUATE_KEY, + BINDING_TYPES, + BINDING_TYPE_KEY, + IF_BINDING_TYPE +} from '../constants' +import { + createSelectorProperties, + createTemplateProperty, + toScopedFunction +} from '../utils' +import {builders} from '../../../utils/build-types' +import {createNestedBindings} from '../builder' +import {findIfAttribute} from '../find' +import {simplePropertyNode} from '../../../utils/custom-ast-nodes' + +/** + * Transform a RiotParser.Node.Tag into an if binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { stiring } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an if binding node + */ +export default function createIfBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + const ifAttribute = findIfAttribute(sourceNode) + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(IF_BINDING_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + toScopedFunction(ifAttribute.expressions[0], sourceFile, sourceCode) + ), + ...createSelectorProperties(selectorAttribute), + createTemplateProperty(createNestedBindings(sourceNode, sourceFile, sourceCode, selectorAttribute)) + ]) +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/bindings/simple.js b/node_modules/@riotjs/compiler/src/generators/template/bindings/simple.js new file mode 100644 index 0000000..b9bf99a --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/bindings/simple.js @@ -0,0 +1,52 @@ +import {createAttributeExpressions, createExpression} from '../expressions/index' +import { + createSelectorProperties, + getChildrenNodes +} from '../utils' +import { hasExpressions, isTextNode } from '../checks' +import {BINDING_EXPRESSIONS_KEY} from '../constants' +import {builders} from '../../../utils/build-types' +import {simplePropertyNode} from '../../../utils/custom-ast-nodes' + +/** + * Create the text node expressions + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {Array} array containing all the text node expressions + */ +function createTextNodeExpressions(sourceNode, sourceFile, sourceCode) { + const childrenNodes = getChildrenNodes(sourceNode) + + return childrenNodes + .filter(isTextNode) + .filter(hasExpressions) + .map(node => createExpression( + node, + sourceFile, + sourceCode, + childrenNodes.indexOf(node), + sourceNode + )) +} + +/** + * Add a simple binding to a riot parser node + * @param { RiotParser.Node.Tag } sourceNode - tag containing the if attribute + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } an each binding node + */ +export default function createSimpleBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.objectExpression([ + ...createSelectorProperties(selectorAttribute), + simplePropertyNode( + BINDING_EXPRESSIONS_KEY, + builders.arrayExpression([ + ...createTextNodeExpressions(sourceNode, sourceFile, sourceCode), + ...createAttributeExpressions(sourceNode, sourceFile, sourceCode) + ]) + ) + ]) +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/bindings/slot.js b/node_modules/@riotjs/compiler/src/generators/template/bindings/slot.js new file mode 100644 index 0000000..7198c31 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/bindings/slot.js @@ -0,0 +1,38 @@ +import { + BINDING_NAME_KEY, + BINDING_TYPES, + BINDING_TYPE_KEY, + DEFAULT_SLOT_NAME, + NAME_ATTRIBUTE, + SLOT_BINDING_TYPE +} from '../constants' +import {builders} from '../../../utils/build-types' +import {createSelectorProperties} from '../utils' +import {findAttribute} from '../find' +import {simplePropertyNode} from '../../../utils/custom-ast-nodes' + +/** + * Transform a RiotParser.Node.Tag of type slot into a slot binding + * @param { RiotParser.Node.Tag } sourceNode - slot node + * @param { string } selectorAttribute - attribute needed to select the target node + * @returns { AST.Node } a slot binding node + */ +export default function createSlotBinding(sourceNode, selectorAttribute) { + const slotNameAttribute = findAttribute(NAME_ATTRIBUTE, sourceNode) + const slotName = slotNameAttribute ? slotNameAttribute.value : DEFAULT_SLOT_NAME + + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(SLOT_BINDING_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_NAME_KEY, + builders.literal(slotName) + ), + ...createSelectorProperties(selectorAttribute) + ]) +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/bindings/tag.js b/node_modules/@riotjs/compiler/src/generators/template/bindings/tag.js new file mode 100644 index 0000000..bab3524 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/bindings/tag.js @@ -0,0 +1,151 @@ +import { + BINDING_ATTRIBUTES_KEY, + BINDING_BINDINGS_KEY, + BINDING_EVALUATE_KEY, + BINDING_GET_COMPONENT_KEY, + BINDING_HTML_KEY, + BINDING_ID_KEY, + BINDING_SLOTS_KEY, + BINDING_TYPES, + BINDING_TYPE_KEY, + GET_COMPONENT_FN, + SLOT_ATTRIBUTE, + TAG_BINDING_TYPE +} from '../constants' +import { + cleanAttributes, + createRootNode, + createSelectorProperties, + getAttributesWithoutSelector, + getChildrenNodes, + getCustomNodeNameAsExpression, + getNodeAttributes, + toScopedFunction +} from '../utils' +import build from '../builder' +import {builders} from '../../../utils/build-types' +import compose from 'cumpa' +import {createExpression} from '../expressions/index' +import {simplePropertyNode} from '../../../utils/custom-ast-nodes' + +/** + * Find the slots in the current component and group them under the same id + * @param {RiotParser.Node.Tag} sourceNode - the custom tag + * @returns {Object} object containing all the slots grouped by name + */ +function groupSlots(sourceNode) { + return getChildrenNodes(sourceNode).reduce((acc, node) => { + const slotAttribute = findSlotAttribute(node) + + if (slotAttribute) { + acc[slotAttribute.value] = node + } else { + acc.default = createRootNode({ + nodes: [...getChildrenNodes(acc.default), node] + }) + } + + return acc + }, { + default: null + }) +} + +/** + * Create the slot entity to pass to the riot-dom bindings + * @param {string} id - slot id + * @param {RiotParser.Node.Tag} sourceNode - slot root node + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} ast node containing the slot object properties + */ +function buildSlot(id, sourceNode, sourceFile, sourceCode) { + const cloneNode = { + ...sourceNode, + // avoid to render the slot attribute + attributes: getNodeAttributes(sourceNode).filter(attribute => attribute.name !== SLOT_ATTRIBUTE) + } + const [html, bindings] = build(cloneNode, sourceFile, sourceCode) + + return builders.objectExpression([ + simplePropertyNode(BINDING_ID_KEY, builders.literal(id)), + simplePropertyNode(BINDING_HTML_KEY, builders.literal(html)), + simplePropertyNode(BINDING_BINDINGS_KEY, builders.arrayExpression(bindings)) + ]) +} + +/** + * Create the AST array containing the slots + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.ArrayExpression} array containing the attributes to bind + */ +function createSlotsArray(sourceNode, sourceFile, sourceCode) { + return builders.arrayExpression([ + ...compose( + slots => slots.map(([key, value]) => buildSlot(key, value, sourceFile, sourceCode)), + slots => slots.filter(([,value]) => value), + Object.entries, + groupSlots + )(sourceNode) + ]) +} + +/** + * Create the AST array containing the attributes to bind to this node + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.ArrayExpression} array containing the slot objects + */ +function createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.arrayExpression([ + ...compose( + attributes => attributes.map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)), + attributes => getAttributesWithoutSelector(attributes, selectorAttribute), // eslint-disable-line + cleanAttributes + )(sourceNode) + ]) +} + +/** + * Find the slot attribute if it exists + * @param {RiotParser.Node.Tag} sourceNode - the custom tag + * @returns {RiotParser.Node.Attr|undefined} the slot attribute found + */ +function findSlotAttribute(sourceNode) { + return getNodeAttributes(sourceNode).find(attribute => attribute.name === SLOT_ATTRIBUTE) +} + +/** + * Transform a RiotParser.Node.Tag into a tag binding + * @param { RiotParser.Node.Tag } sourceNode - the custom tag + * @param { string } selectorAttribute - attribute needed to select the target node + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns { AST.Node } tag binding node + */ +export default function createTagBinding(sourceNode, selectorAttribute, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(BINDING_TYPES), + builders.identifier(TAG_BINDING_TYPE), + false + ), + ), + simplePropertyNode(BINDING_GET_COMPONENT_KEY, builders.identifier(GET_COMPONENT_FN)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + toScopedFunction(getCustomNodeNameAsExpression(sourceNode), sourceFile, sourceCode) + ), + simplePropertyNode(BINDING_SLOTS_KEY, createSlotsArray(sourceNode, sourceFile, sourceCode)), + simplePropertyNode( + BINDING_ATTRIBUTES_KEY, + createBindingAttributes(sourceNode, selectorAttribute, sourceFile, sourceCode) + ), + ...createSelectorProperties(selectorAttribute) + ]) +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/builder.js b/node_modules/@riotjs/compiler/src/generators/template/builder.js new file mode 100644 index 0000000..deb660d --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/builder.js @@ -0,0 +1,178 @@ +import { + cloneNodeWithoutSelectorAttribute, + closeTag, createBindingSelector, + createRootNode, + getChildrenNodes, + getNodeAttributes, + nodeToString +} from './utils' +import { + hasEachAttribute, hasIfAttribute, + hasItsOwnTemplate, + isCustomNode, + isRootNode, + isSlotNode, + isStaticNode, + isTagNode, + isTextNode, + isVoidNode +} from './checks' +import cloneDeep from '../../utils/clone-deep' +import eachBinding from './bindings/each' +import ifBinding from './bindings/if' +import panic from '../../utils/panic' +import simpleBinding from './bindings/simple' +import slotBinding from './bindings/slot' +import tagBinding from './bindings/tag' + + +const BuildingState = Object.freeze({ + html: [], + bindings: [], + parent: null +}) + +/** + * Nodes having bindings should be cloned and new selector properties should be added to them + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} bindingsSelector - temporary string to identify the current node + * @returns {RiotParser.Node} the original node parsed having the new binding selector attribute + */ +function createBindingsTag(sourceNode, bindingsSelector) { + if (!bindingsSelector) return sourceNode + + return { + ...sourceNode, + // inject the selector bindings into the node attributes + attributes: [{ + name: bindingsSelector, + value: bindingsSelector + }, ...getNodeAttributes(sourceNode)] + } +} + +/** + * Create a generic dynamic node (text or tag) and generate its bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ +function createDynamicNode(sourceNode, sourceFile, sourceCode, state) { + switch (true) { + case isTextNode(sourceNode): + // text nodes will not have any bindings + return [nodeToString(sourceNode), []] + default: + return createTagWithBindings(sourceNode, sourceFile, sourceCode, state) + } +} + +/** + * Create only a dynamic tag node with generating a custom selector and its bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ +function createTagWithBindings(sourceNode, sourceFile, sourceCode) { + const bindingsSelector = isRootNode(sourceNode) ? null : createBindingSelector() + const cloneNode = createBindingsTag(sourceNode, bindingsSelector) + const tagOpeningHTML = nodeToString(cloneNode) + + switch(true) { + // EACH bindings have prio 1 + case hasEachAttribute(cloneNode): + return [tagOpeningHTML, [eachBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // IF bindings have prio 2 + case hasIfAttribute(cloneNode): + return [tagOpeningHTML, [ifBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // TAG bindings have prio 3 + case isCustomNode(cloneNode): + return [tagOpeningHTML, [tagBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + // slot tag + case isSlotNode(cloneNode): + return [tagOpeningHTML, [slotBinding(cloneNode, bindingsSelector)]] + // this node has expressions bound to it + default: + return [tagOpeningHTML, [simpleBinding(cloneNode, bindingsSelector, sourceFile, sourceCode)]] + } +} + +/** + * Parse a node trying to extract its template and bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and bindings for the current node + */ +function parseNode(sourceNode, sourceFile, sourceCode, state) { + // static nodes have no bindings + if (isStaticNode(sourceNode)) return [nodeToString(sourceNode), []] + return createDynamicNode(sourceNode, sourceFile, sourceCode, state) +} + +/** + * Create the tag binding + * @param { RiotParser.Node.Tag } sourceNode - tag containing the each attribute + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @param { string } selector - binding selector + * @returns { Array } array with only the tag binding AST + */ +export function createNestedBindings(sourceNode, sourceFile, sourceCode, selector) { + const mightBeARiotComponent = isCustomNode(sourceNode) + + return mightBeARiotComponent ? [null, [ + tagBinding( + cloneNodeWithoutSelectorAttribute(sourceNode, selector), + null, + sourceFile, + sourceCode + )] + ] : build(createRootNode(sourceNode), sourceFile, sourceCode) +} + +/** + * Build the template and the bindings + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {BuildingState} state - state representing the current building tree state during the recursion + * @returns {Array} array containing the html output and the dom bindings + */ +export default function build( + sourceNode, + sourceFile, + sourceCode, + state +) { + if (!sourceNode) panic('Something went wrong with your tag DOM parsing, your tag template can\'t be created') + + const [nodeHTML, nodeBindings] = parseNode(sourceNode, sourceFile, sourceCode, state) + const childrenNodes = getChildrenNodes(sourceNode) + const currentState = { ...cloneDeep(BuildingState), ...state } + + // mutate the original arrays + currentState.html.push(...nodeHTML) + currentState.bindings.push(...nodeBindings) + + // do recursion if + // this tag has children and it has no special directives bound to it + if (childrenNodes.length && !hasItsOwnTemplate(sourceNode)) { + childrenNodes.forEach(node => build(node, sourceFile, sourceCode, { parent: sourceNode, ...currentState })) + } + + // close the tag if it's not a void one + if (isTagNode(sourceNode) && !isVoidNode(sourceNode)) { + currentState.html.push(closeTag(sourceNode)) + } + + return [ + currentState.html.join(''), + currentState.bindings + ] +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/checks.js b/node_modules/@riotjs/compiler/src/generators/template/checks.js new file mode 100644 index 0000000..18f2c44 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/checks.js @@ -0,0 +1,194 @@ +import { + IS_CUSTOM_NODE, + IS_SPREAD_ATTRIBUTE, + IS_VOID_NODE, + PROGRESS_TAG_NODE_NAME, + SLOT_TAG_NODE_NAME +} from './constants' +import { findEachAttribute, findIfAttribute, findIsAttribute, findKeyAttribute } from './find' +import { + getName, + getNodeAttributes +} from './utils' +import { isBrowserAPI, isBuiltinAPI, isNewExpression, isRaw } from '../../utils/ast-nodes-checks' +import compose from 'cumpa' +import { nodeTypes } from '@riotjs/parser' +import { types } from '../../utils/build-types' + +/** + * True if the node has not expression set nor bindings directives + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only if it's a static node that doesn't need bindings or expressions + */ +export function isStaticNode(node) { + return [ + hasExpressions, + findEachAttribute, + findIfAttribute, + isCustomNode, + isSlotNode + ].every(test => !test(node)) +} + +/** + * Check if a node name is part of the browser or builtin javascript api or it belongs to the current scope + * @param { types.NodePath } path - containing the current node visited + * @returns {boolean} true if it's a global api variable + */ +export function isGlobal({ scope, node }) { + return Boolean( + isRaw(node) || + isBuiltinAPI(node) || + isBrowserAPI(node) || + isNewExpression(node) || + isNodeInScope(scope, node), + ) +} + +/** + * Checks if the identifier of a given node exists in a scope + * @param {Scope} scope - scope where to search for the identifier + * @param {types.Node} node - node to search for the identifier + * @returns {boolean} true if the node identifier is defined in the given scope + */ +function isNodeInScope(scope, node) { + const traverse = (isInScope = false) => { + types.visit(node, { + visitIdentifier(path) { + if (scope.lookup(getName(path.node))) { + isInScope = true + } + + this.abort() + } + }) + + return isInScope + } + + return traverse() +} + +/** + * True if the node has the isCustom attribute set + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if either it's a riot component or a custom element + */ +export function isCustomNode(node) { + return !!(node[IS_CUSTOM_NODE] || hasIsAttribute(node)) +} + +/** + * True the node is + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if it's a slot node + */ +export function isSlotNode(node) { + return node.name === SLOT_TAG_NODE_NAME +} + +/** + * True if the node has the isVoid attribute set + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if the node is self closing + */ +export function isVoidNode(node) { + return !!node[IS_VOID_NODE] +} + +/** + * True if the riot parser did find a tag node + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the tag nodes + */ +export function isTagNode(node) { + return node.type === nodeTypes.TAG +} + +/** + * True if the riot parser did find a text node + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the text nodes + */ +export function isTextNode(node) { + return node.type === nodeTypes.TEXT +} + +/** + * True if the node parsed is the root one + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the root nodes + */ +export function isRootNode(node) { + return node.isRoot +} + +/** + * True if the attribute parsed is of type spread one + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true if the attribute node is of type spread + */ +export function isSpreadAttribute(node) { + return node[IS_SPREAD_ATTRIBUTE] +} + +/** + * True if the node is an attribute and its name is "value" + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for value attribute nodes + */ +export function isValueAttribute(node) { + return node.name === 'value' +} + +/** + * True if the DOM node is a progress tag + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true for the progress tags + */ +export function isProgressNode(node) { + return node.name === PROGRESS_TAG_NODE_NAME +} + +/** + * True if the node is an attribute and a DOM handler + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for dom listener attribute nodes + */ +export const isEventAttribute = (() => { + const EVENT_ATTR_RE = /^on/ + return node => EVENT_ATTR_RE.test(node.name) +})() + +/** + * True if the node has expressions or expression attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} ditto + */ +export function hasExpressions(node) { + return !!( + node.expressions || + // has expression attributes + (getNodeAttributes(node).some(attribute => hasExpressions(attribute))) || + // has child text nodes with expressions + (node.nodes && node.nodes.some(node => isTextNode(node) && hasExpressions(node))) + ) +} + +/** + * True if the node is a directive having its own template + * @param {RiotParser.Node} node - riot parser node + * @returns {boolean} true only for the IF EACH and TAG bindings + */ +export function hasItsOwnTemplate(node) { + return [ + findEachAttribute, + findIfAttribute, + isCustomNode + ].some(test => test(node)) +} + +export const hasIfAttribute = compose(Boolean, findIfAttribute) +export const hasEachAttribute = compose(Boolean, findEachAttribute) +export const hasIsAttribute = compose(Boolean, findIsAttribute) +export const hasKeyAttribute = compose(Boolean, findKeyAttribute) diff --git a/node_modules/@riotjs/compiler/src/generators/template/constants.js b/node_modules/@riotjs/compiler/src/generators/template/constants.js new file mode 100644 index 0000000..6c1d9b0 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/constants.js @@ -0,0 +1,64 @@ +// import {IS_BOOLEAN,IS_CUSTOM,IS_RAW,IS_SPREAD,IS_VOID} from '@riotjs/parser/src/constants' + +export const BINDING_TYPES = 'bindingTypes' +export const EACH_BINDING_TYPE = 'EACH' +export const IF_BINDING_TYPE = 'IF' +export const TAG_BINDING_TYPE = 'TAG' +export const SLOT_BINDING_TYPE = 'SLOT' + + +export const EXPRESSION_TYPES = 'expressionTypes' +export const ATTRIBUTE_EXPRESSION_TYPE = 'ATTRIBUTE' +export const VALUE_EXPRESSION_TYPE = 'VALUE' +export const TEXT_EXPRESSION_TYPE = 'TEXT' +export const EVENT_EXPRESSION_TYPE = 'EVENT' + +export const TEMPLATE_FN = 'template' +export const SCOPE = 'scope' +export const GET_COMPONENT_FN = 'getComponent' + +// keys needed to create the DOM bindings +export const BINDING_SELECTOR_KEY = 'selector' +export const BINDING_GET_COMPONENT_KEY = 'getComponent' +export const BINDING_TEMPLATE_KEY = 'template' +export const BINDING_TYPE_KEY = 'type' +export const BINDING_REDUNDANT_ATTRIBUTE_KEY = 'redundantAttribute' +export const BINDING_CONDITION_KEY = 'condition' +export const BINDING_ITEM_NAME_KEY = 'itemName' +export const BINDING_GET_KEY_KEY = 'getKey' +export const BINDING_INDEX_NAME_KEY = 'indexName' +export const BINDING_EVALUATE_KEY = 'evaluate' +export const BINDING_NAME_KEY = 'name' +export const BINDING_SLOTS_KEY = 'slots' +export const BINDING_EXPRESSIONS_KEY = 'expressions' +export const BINDING_CHILD_NODE_INDEX_KEY = 'childNodeIndex' +// slots keys +export const BINDING_BINDINGS_KEY = 'bindings' +export const BINDING_ID_KEY = 'id' +export const BINDING_HTML_KEY = 'html' +export const BINDING_ATTRIBUTES_KEY = 'attributes' + +// DOM directives +export const IF_DIRECTIVE = 'if' +export const EACH_DIRECTIVE = 'each' +export const KEY_ATTRIBUTE = 'key' +export const SLOT_ATTRIBUTE = 'slot' +export const NAME_ATTRIBUTE = 'name' +export const IS_DIRECTIVE = 'is' + +// Misc +export const DEFAULT_SLOT_NAME = 'default' +export const TEXT_NODE_EXPRESSION_PLACEHOLDER = '' +export const BINDING_SELECTOR_PREFIX = 'expr' +export const SLOT_TAG_NODE_NAME = 'slot' +export const PROGRESS_TAG_NODE_NAME = 'progress' + +// Riot Parser constants +// TODO: import these values dynamically +export const IS_RAW_NODE = 'isRaw' +export const IS_VOID_NODE = 'isVoid' +export const IS_CUSTOM_NODE = 'isCustom' +export const IS_BOOLEAN_ATTRIBUTE = 'isBoolean' +export const IS_SPREAD_ATTRIBUTE = 'isSpread' + + diff --git a/node_modules/@riotjs/compiler/src/generators/template/expressions/attribute.js b/node_modules/@riotjs/compiler/src/generators/template/expressions/attribute.js new file mode 100644 index 0000000..c55ed53 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/expressions/attribute.js @@ -0,0 +1,36 @@ +import { + ATTRIBUTE_EXPRESSION_TYPE, + BINDING_EVALUATE_KEY, + BINDING_NAME_KEY, + BINDING_TYPE_KEY, + EXPRESSION_TYPES +} from '../constants' +import {nullNode, simplePropertyNode} from '../../../utils/custom-ast-nodes' +import {builders} from '../../../utils/build-types' +import {createAttributeEvaluationFunction} from '../utils' +import {isSpreadAttribute} from '../checks' + + +/** + * Create a simple attribute expression + * @param {RiotParser.Node.Attr} sourceNode - the custom tag + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} object containing the expression binding keys + */ +export default function createAttributeExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(ATTRIBUTE_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode(BINDING_NAME_KEY, isSpreadAttribute(sourceNode) ? nullNode() : builders.literal(sourceNode.name)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/expressions/event.js b/node_modules/@riotjs/compiler/src/generators/template/expressions/event.js new file mode 100644 index 0000000..c6fc45f --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/expressions/event.js @@ -0,0 +1,34 @@ +import { + BINDING_EVALUATE_KEY, + BINDING_NAME_KEY, + BINDING_TYPE_KEY, + EVENT_EXPRESSION_TYPE, + EXPRESSION_TYPES +} from '../constants' +import {builders} from '../../../utils/build-types' +import {createAttributeEvaluationFunction} from '../utils' +import {simplePropertyNode} from '../../../utils/custom-ast-nodes' + +/** + * Create a simple event expression + * @param {RiotParser.Node.Attr} sourceNode - attribute containing the event handlers + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {AST.Node} object containing the expression binding keys + */ +export default function createEventExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(EVENT_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode(BINDING_NAME_KEY, builders.literal(sourceNode.name)), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/expressions/index.js b/node_modules/@riotjs/compiler/src/generators/template/expressions/index.js new file mode 100644 index 0000000..09d1b42 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/expressions/index.js @@ -0,0 +1,34 @@ +import {isEventAttribute, isProgressNode, isTextNode, isValueAttribute} from '../checks' +import attributeExpression from './attribute' +import eventExpression from './event' +import {findDynamicAttributes} from '../find' +import {hasValueAttribute} from 'dom-nodes' +import textExpression from './text' +import valueExpression from './value' + +export function createExpression(sourceNode, sourceFile, sourceCode, childNodeIndex, parentNode) { + switch (true) { + case isTextNode(sourceNode): + return textExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) + // progress nodes value attributes will be rendered as attributes + // see https://github.com/riot/compiler/issues/122 + case isValueAttribute(sourceNode) && hasValueAttribute(parentNode.name) && !isProgressNode(parentNode): + return valueExpression(sourceNode, sourceFile, sourceCode) + case isEventAttribute(sourceNode): + return eventExpression(sourceNode, sourceFile, sourceCode) + default: + return attributeExpression(sourceNode, sourceFile, sourceCode) + } +} + +/** + * Create the attribute expressions + * @param {RiotParser.Node} sourceNode - any kind of node parsed via riot parser + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @returns {Array} array containing all the attribute expressions + */ +export function createAttributeExpressions(sourceNode, sourceFile, sourceCode) { + return findDynamicAttributes(sourceNode) + .map(attribute => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode)) +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/expressions/text.js b/node_modules/@riotjs/compiler/src/generators/template/expressions/text.js new file mode 100644 index 0000000..cb69cd0 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/expressions/text.js @@ -0,0 +1,90 @@ +import { + BINDING_CHILD_NODE_INDEX_KEY, + BINDING_EVALUATE_KEY, + BINDING_TYPE_KEY, + EXPRESSION_TYPES, + TEXT_EXPRESSION_TYPE +} from '../constants' +import {createArrayString, transformExpression, wrapASTInFunctionWithScope} from '../utils' +import {nullNode,simplePropertyNode} from '../../../utils/custom-ast-nodes' +import {builders} from '../../../utils/build-types' +import {isLiteral} from '../../../utils/ast-nodes-checks' +import unescapeChar from '../../../utils/unescape-char' + +/** + * Generate the pure immutable string chunks from a RiotParser.Node.Text + * @param {RiotParser.Node.Text} node - riot parser text node + * @param {string} sourceCode sourceCode - source code + * @returns {Array} array containing the immutable string chunks + */ +function generateLiteralStringChunksFromNode(node, sourceCode) { + return node.expressions.reduce((chunks, expression, index) => { + const start = index ? node.expressions[index - 1].end : node.start + + chunks.push(sourceCode.substring(start, expression.start)) + + // add the tail to the string + if (index === node.expressions.length - 1) + chunks.push(sourceCode.substring(expression.end, node.end)) + + return chunks + }, []).map(str => node.unescape ? unescapeChar(str, node.unescape) : str) +} + +/** + * Simple bindings might contain multiple expressions like for example: "{foo} and {bar}" + * This helper aims to merge them in a template literal if it's necessary + * @param {RiotParser.Node} node - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { Object } a template literal expression object + */ +export function mergeNodeExpressions(node, sourceFile, sourceCode) { + if (node.parts.length === 1) + return transformExpression(node.expressions[0], sourceFile, sourceCode) + + const pureStringChunks = generateLiteralStringChunksFromNode(node, sourceCode) + const stringsArray = pureStringChunks.reduce((acc, str, index) => { + const expr = node.expressions[index] + + return [ + ...acc, + builders.literal(str), + expr ? transformExpression(expr, sourceFile, sourceCode) : nullNode() + ] + }, []) + // filter the empty literal expressions + .filter(expr => !isLiteral(expr) || expr.value) + + return createArrayString(stringsArray) +} + +/** + * Create a text expression + * @param {RiotParser.Node.Text} sourceNode - text node to parse + * @param {string} sourceFile - source file path + * @param {string} sourceCode - original source + * @param {number} childNodeIndex - position of the child text node in its parent children nodes + * @returns {AST.Node} object containing the expression binding keys + */ +export default function createTextExpression(sourceNode, sourceFile, sourceCode, childNodeIndex) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(TEXT_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_CHILD_NODE_INDEX_KEY, + builders.literal(childNodeIndex) + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + wrapASTInFunctionWithScope( + mergeNodeExpressions(sourceNode, sourceFile, sourceCode) + ) + ) + ]) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/generators/template/expressions/value.js b/node_modules/@riotjs/compiler/src/generators/template/expressions/value.js new file mode 100644 index 0000000..49ed5d6 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/expressions/value.js @@ -0,0 +1,25 @@ +import { + BINDING_EVALUATE_KEY, + BINDING_TYPE_KEY, + EXPRESSION_TYPES, + VALUE_EXPRESSION_TYPE +} from '../constants' +import {builders} from '../../../utils/build-types' +import {createAttributeEvaluationFunction} from '../utils' +import {simplePropertyNode} from '../../../utils/custom-ast-nodes' + +export default function createValueExpression(sourceNode, sourceFile, sourceCode) { + return builders.objectExpression([ + simplePropertyNode(BINDING_TYPE_KEY, + builders.memberExpression( + builders.identifier(EXPRESSION_TYPES), + builders.identifier(VALUE_EXPRESSION_TYPE), + false + ), + ), + simplePropertyNode( + BINDING_EVALUATE_KEY, + createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) + ) + ]) +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/find.js b/node_modules/@riotjs/compiler/src/generators/template/find.js new file mode 100644 index 0000000..e88797e --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/find.js @@ -0,0 +1,47 @@ +import { EACH_DIRECTIVE, IF_DIRECTIVE, IS_DIRECTIVE, KEY_ATTRIBUTE } from './constants' +import { getName, getNodeAttributes } from './utils' +import { hasExpressions } from './checks' + +/** + * Find the attribute node + * @param { string } name - name of the attribute we want to find + * @param { riotParser.nodeTypes.TAG } node - a tag node + * @returns { riotParser.nodeTypes.ATTR } attribute node + */ +export function findAttribute(name, node) { + return node.attributes && node.attributes.find(attr => getName(attr) === name) +} + +export function findIfAttribute(node) { + return findAttribute(IF_DIRECTIVE, node) +} + +export function findEachAttribute(node) { + return findAttribute(EACH_DIRECTIVE, node) +} + +export function findKeyAttribute(node) { + return findAttribute(KEY_ATTRIBUTE, node) +} + +export function findIsAttribute(node) { + return findAttribute(IS_DIRECTIVE, node) +} + +/** + * Find all the node attributes that are not expressions + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} list of all the static attributes + */ +export function findStaticAttributes(node) { + return getNodeAttributes(node).filter(attribute => !hasExpressions(attribute)) +} + +/** + * Find all the node attributes that have expressions + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} list of all the dynamic attributes + */ +export function findDynamicAttributes(node) { + return getNodeAttributes(node).filter(hasExpressions) +} diff --git a/node_modules/@riotjs/compiler/src/generators/template/index.js b/node_modules/@riotjs/compiler/src/generators/template/index.js new file mode 100644 index 0000000..60a7bf0 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/index.js @@ -0,0 +1,74 @@ +import {BINDING_TYPES, EXPRESSION_TYPES, GET_COMPONENT_FN, TEMPLATE_FN} from './constants' +import {builders, types} from '../../utils/build-types' +import {callTemplateFunction, createRootNode} from './utils' +import {TAG_TEMPLATE_PROPERTY} from '../../constants' +import build from './builder' + +const templateFunctionArguments = [ + TEMPLATE_FN, + EXPRESSION_TYPES, + BINDING_TYPES, + GET_COMPONENT_FN +].map(builders.identifier) + +/** + * Create the content of the template function + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @returns {AST.BlockStatement} the content of the template function + */ +function createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) { + return builders.blockStatement([ + builders.returnStatement( + callTemplateFunction( + ...build( + createRootNode(sourceNode), + sourceFile, + sourceCode + ) + ) + ) + ]) +} + +/** + * Extend the AST adding the new template property containing our template call to render the component + * @param { Object } ast - current output ast + * @param { string } sourceFile - source file path + * @param { string } sourceCode - original source + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @returns { Object } the output ast having the "template" key + */ +function extendTemplateProperty(ast, sourceFile, sourceCode, sourceNode) { + types.visit(ast, { + visitProperty(path) { + if (path.value.key.value === TAG_TEMPLATE_PROPERTY) { + path.value.value = builders.functionExpression( + null, + templateFunctionArguments, + createTemplateFunctionContent(sourceNode, sourceFile, sourceCode) + ) + + return false + } + + this.traverse(path) + } + }) + + return ast +} + +/** + * Generate the component template logic + * @param { RiotParser.Node } sourceNode - node generated by the riot compiler + * @param { string } source - original component source code + * @param { Object } meta - compilation meta information + * @param { AST } ast - current AST output + * @returns { AST } the AST generated + */ +export default function template(sourceNode, source, meta, ast) { + const { options } = meta + return extendTemplateProperty(ast, options.file, source, sourceNode) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/generators/template/utils.js b/node_modules/@riotjs/compiler/src/generators/template/utils.js new file mode 100644 index 0000000..f67df1b --- /dev/null +++ b/node_modules/@riotjs/compiler/src/generators/template/utils.js @@ -0,0 +1,486 @@ +import { + BINDING_REDUNDANT_ATTRIBUTE_KEY, + BINDING_SELECTOR_KEY, + BINDING_SELECTOR_PREFIX, + BINDING_TEMPLATE_KEY, + EACH_DIRECTIVE, + IF_DIRECTIVE, + IS_BOOLEAN_ATTRIBUTE, + IS_DIRECTIVE, + KEY_ATTRIBUTE, + SCOPE, + SLOT_ATTRIBUTE, + TEMPLATE_FN, + TEXT_NODE_EXPRESSION_PLACEHOLDER +} from './constants' +import { builders, types } from '../../utils/build-types' +import { findIsAttribute, findStaticAttributes } from './find' +import { hasExpressions, isGlobal, isTagNode, isTextNode, isVoidNode } from './checks' +import { isBinaryExpression, isIdentifier, isLiteral, isThisExpression } from '../../utils/ast-nodes-checks' +import { nullNode, simplePropertyNode } from '../../utils/custom-ast-nodes' +import addLinesOffset from '../../utils/add-lines-offset' +import compose from 'cumpa' +import generateAST from '../../utils/generate-ast' +import unescapeChar from '../../utils/unescape-char' + +const scope = builders.identifier(SCOPE) +export const getName = node => node && node.name ? node.name : node + +/** + * Replace the path scope with a member Expression + * @param { types.NodePath } path - containing the current node visited + * @param { types.Node } property - node we want to prefix with the scope identifier + * @returns {undefined} this is a void function + */ +function replacePathScope(path, property) { + path.replace(builders.memberExpression( + scope, + property, + false + )) +} + +/** + * Change the nodes scope adding the `scope` prefix + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return false if we want to stop the tree traversal + * @context { types.visit } + */ +function updateNodeScope(path) { + if (!isGlobal(path)) { + replacePathScope(path, path.node) + + return false + } + + this.traverse(path) +} + +/** + * Change the scope of the member expressions + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return always false because we want to check only the first node object + */ +function visitMemberExpression(path) { + if (!isGlobal(path) && !isGlobal({ node: path.node.object, scope: path.scope })) { + if (path.value.computed) { + this.traverse(path) + } else if (isBinaryExpression(path.node.object) || path.node.object.computed) { + this.traverse(path.get('object')) + } else if (!path.node.object.callee) { + replacePathScope(path, isThisExpression(path.node.object) ? path.node.property : path.node) + } else { + this.traverse(path.get('object')) + } + } + + return false +} + + +/** + * Objects properties should be handled a bit differently from the Identifier + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean } return false if we want to stop the tree traversal + */ +function visitProperty(path) { + const value = path.node.value + + if (isIdentifier(value)) { + updateNodeScope(path.get('value')) + } else { + this.traverse(path.get('value')) + } + + return false +} + +/** + * The this expressions should be replaced with the scope + * @param { types.NodePath } path - containing the current node visited + * @returns { boolean|undefined } return false if we want to stop the tree traversal + */ +function visitThisExpression(path) { + path.replace(scope) + this.traverse(path) +} + + +/** + * Update the scope of the global nodes + * @param { Object } ast - ast program + * @returns { Object } the ast program with all the global nodes updated + */ +export function updateNodesScope(ast) { + const ignorePath = () => false + + types.visit(ast, { + visitIdentifier: updateNodeScope, + visitMemberExpression, + visitProperty, + visitThisExpression, + visitClassExpression: ignorePath + }) + + return ast +} + +/** + * Convert any expression to an AST tree + * @param { Object } expression - expression parsed by the riot parser + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Object } the ast generated + */ +export function createASTFromExpression(expression, sourceFile, sourceCode) { + const code = sourceFile ? + addLinesOffset(expression.text, sourceCode, expression) : + expression.text + + return generateAST(`(${code})`, { + sourceFileName: sourceFile + }) +} + +/** + * Create the bindings template property + * @param {Array} args - arguments to pass to the template function + * @returns {ASTNode} a binding template key + */ +export function createTemplateProperty(args) { + return simplePropertyNode( + BINDING_TEMPLATE_KEY, + args ? callTemplateFunction(...args) : nullNode() + ) +} + +/** + * Try to get the expression of an attribute node + * @param { RiotParser.Node.Attribute } attribute - riot parser attribute node + * @returns { RiotParser.Node.Expression } attribute expression value + */ +export function getAttributeExpression(attribute) { + return attribute.expressions ? attribute.expressions[0] : { + // if no expression was found try to typecast the attribute value + ...attribute, + text: attribute.value + } +} + +/** + * Wrap the ast generated in a function call providing the scope argument + * @param {Object} ast - function body + * @returns {FunctionExpresion} function having the scope argument injected + */ +export function wrapASTInFunctionWithScope(ast) { + return builders.functionExpression( + null, + [scope], + builders.blockStatement([builders.returnStatement( + ast + )]) + ) +} + +/** + * Convert any parser option to a valid template one + * @param { RiotParser.Node.Expression } expression - expression parsed by the riot parser + * @param { string } sourceFile - original tag file + * @param { string } sourceCode - original tag source code + * @returns { Object } a FunctionExpression object + * + * @example + * toScopedFunction('foo + bar') // scope.foo + scope.bar + * + * @example + * toScopedFunction('foo.baz + bar') // scope.foo.baz + scope.bar + */ +export function toScopedFunction(expression, sourceFile, sourceCode) { + return compose( + wrapASTInFunctionWithScope, + transformExpression, + )(expression, sourceFile, sourceCode) +} + +/** + * Transform an expression node updating its global scope + * @param {RiotParser.Node.Expr} expression - riot parser expression node + * @param {string} sourceFile - source file + * @param {string} sourceCode - source code + * @returns {ASTExpression} ast expression generated from the riot parser expression node + */ +export function transformExpression(expression, sourceFile, sourceCode) { + return compose( + getExpressionAST, + updateNodesScope, + createASTFromExpression + )(expression, sourceFile, sourceCode) +} + +/** + * Get the parsed AST expression of riot expression node + * @param {AST.Program} sourceAST - raw node parsed + * @returns {AST.Expression} program expression output + */ +export function getExpressionAST(sourceAST) { + const astBody = sourceAST.program.body + + return astBody[0] ? astBody[0].expression : astBody +} + +/** + * Create the template call function + * @param {Array|string|Node.Literal} template - template string + * @param {Array} bindings - template bindings provided as AST nodes + * @returns {Node.CallExpression} template call expression + */ +export function callTemplateFunction(template, bindings) { + return builders.callExpression(builders.identifier(TEMPLATE_FN), [ + template ? builders.literal(template) : nullNode(), + bindings ? builders.arrayExpression(bindings) : nullNode() + ]) +} + +/** + * Convert any DOM attribute into a valid DOM selector useful for the querySelector API + * @param { string } attributeName - name of the attribute to query + * @returns { string } the attribute transformed to a query selector + */ +export const attributeNameToDOMQuerySelector = attributeName => `[${attributeName}]` + +/** + * Create the properties to query a DOM node + * @param { string } attributeName - attribute name needed to identify a DOM node + * @returns { Array } array containing the selector properties needed for the binding + */ +export function createSelectorProperties(attributeName) { + return attributeName ? [ + simplePropertyNode(BINDING_REDUNDANT_ATTRIBUTE_KEY, builders.literal(attributeName)), + simplePropertyNode(BINDING_SELECTOR_KEY, + compose(builders.literal, attributeNameToDOMQuerySelector)(attributeName) + ) + ] : [] +} + +/** + * Clone the node filtering out the selector attribute from the attributes list + * @param {RiotParser.Node} node - riot parser node + * @param {string} selectorAttribute - name of the selector attribute to filter out + * @returns {RiotParser.Node} the node with the attribute cleaned up + */ +export function cloneNodeWithoutSelectorAttribute(node, selectorAttribute) { + return { + ...node, + attributes: getAttributesWithoutSelector(getNodeAttributes(node), selectorAttribute) + } +} + + +/** + * Get the node attributes without the selector one + * @param {Array} attributes - attributes list + * @param {string} selectorAttribute - name of the selector attribute to filter out + * @returns {Array} filtered attributes + */ +export function getAttributesWithoutSelector(attributes, selectorAttribute) { + if (selectorAttribute) + return attributes.filter(attribute => attribute.name !== selectorAttribute) + + return attributes +} + +/** + * Clean binding or custom attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} only the attributes that are not bindings or directives + */ +export function cleanAttributes(node) { + return getNodeAttributes(node).filter(attribute => ![ + IF_DIRECTIVE, + EACH_DIRECTIVE, + KEY_ATTRIBUTE, + SLOT_ATTRIBUTE, + IS_DIRECTIVE + ].includes(attribute.name)) +} + +/** + * Create a root node proxing only its nodes and attributes + * @param {RiotParser.Node} node - riot parser node + * @returns {RiotParser.Node} root node + */ +export function createRootNode(node) { + return { + nodes: getChildrenNodes(node), + isRoot: true, + // root nodes shuold't have directives + attributes: cleanAttributes(node) + } +} + +/** + * Get all the child nodes of a RiotParser.Node + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} all the child nodes found + */ +export function getChildrenNodes(node) { + return node && node.nodes ? node.nodes : [] +} + +/** + * Get all the attributes of a riot parser node + * @param {RiotParser.Node} node - riot parser node + * @returns {Array} all the attributes find + */ +export function getNodeAttributes(node) { + return node.attributes ? node.attributes : [] +} +/** + * Get the name of a custom node transforming it into an expression node + * @param {RiotParser.Node} node - riot parser node + * @returns {RiotParser.Node.Attr} the node name as expression attribute + */ +export function getCustomNodeNameAsExpression(node) { + const isAttribute = findIsAttribute(node) + const toRawString = val => `'${val}'` + + if (isAttribute) { + return isAttribute.expressions ? isAttribute.expressions[0] : { + ...isAttribute, + text: toRawString(isAttribute.value) + } + } + + return { ...node, text: toRawString(getName(node)) } +} + +/** + * Convert all the node static attributes to strings + * @param {RiotParser.Node} node - riot parser node + * @returns {string} all the node static concatenated as string + */ +export function staticAttributesToString(node) { + return findStaticAttributes(node) + .map(attribute => attribute[IS_BOOLEAN_ATTRIBUTE] || !attribute.value ? + attribute.name : + `${attribute.name}="${unescapeNode(attribute, 'value').value}"` + ).join(' ') +} + +/** + * Make sure that node escaped chars will be unescaped + * @param {RiotParser.Node} node - riot parser node + * @param {string} key - key property to unescape + * @returns {RiotParser.Node} node with the text property unescaped + */ +export function unescapeNode(node, key) { + if (node.unescape) { + return { + ...node, + [key]: unescapeChar(node[key], node.unescape) + } + } + + return node +} + + +/** + * Convert a riot parser opening node into a string + * @param {RiotParser.Node} node - riot parser node + * @returns {string} the node as string + */ +export function nodeToString(node) { + const attributes = staticAttributesToString(node) + + switch(true) { + case isTagNode(node): + return `<${node.name}${attributes ? ` ${attributes}` : ''}${isVoidNode(node) ? '/' : ''}>` + case isTextNode(node): + return hasExpressions(node) ? TEXT_NODE_EXPRESSION_PLACEHOLDER : unescapeNode(node, 'text').text + default: + return '' + } +} + +/** + * Close an html node + * @param {RiotParser.Node} node - riot parser node + * @returns {string} the closing tag of the html tag node passed to this function + */ +export function closeTag(node) { + return node.name ? `` : '' +} + +/** + * Create a strings array with the `join` call to transform it into a string + * @param {Array} stringsArray - array containing all the strings to concatenate + * @returns {AST.CallExpression} array with a `join` call + */ +export function createArrayString(stringsArray) { + return builders.callExpression( + builders.memberExpression( + builders.arrayExpression(stringsArray), + builders.identifier('join'), + false + ), + [builders.literal('')], + ) +} + +/** + * Simple expression bindings might contain multiple expressions like for example: "class="{foo} red {bar}"" + * This helper aims to merge them in a template literal if it's necessary + * @param {RiotParser.Attr} node - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { Object } a template literal expression object + */ +export function mergeAttributeExpressions(node, sourceFile, sourceCode) { + if (!node.parts || node.parts.length === 1) { + return transformExpression(node.expressions[0], sourceFile, sourceCode) + } + const stringsArray = [ + ...node.parts.reduce((acc, str) => { + const expression = node.expressions.find(e => e.text.trim() === str) + + return [ + ...acc, + expression ? transformExpression(expression, sourceFile, sourceCode) : builders.literal(str) + ] + }, []) + ].filter(expr => !isLiteral(expr) || expr.value) + + + return createArrayString(stringsArray) +} + +/** + * Create a selector that will be used to find the node via dom-bindings + * @param {number} id - temporary variable that will be increased anytime this function will be called + * @returns {string} selector attribute needed to bind a riot expression + */ +export const createBindingSelector = (function createSelector(id = 0) { + return () => `${BINDING_SELECTOR_PREFIX}${id++}` +}()) + +/** + * Create an attribute evaluation function + * @param {RiotParser.Attr} sourceNode - riot parser node + * @param {string} sourceFile - original tag file + * @param {string} sourceCode - original tag source code + * @returns { AST.Node } an AST function expression to evaluate the attribute value + */ +export function createAttributeEvaluationFunction(sourceNode, sourceFile, sourceCode) { + return hasExpressions(sourceNode) ? + // dynamic attribute + wrapASTInFunctionWithScope(mergeAttributeExpressions(sourceNode, sourceFile, sourceCode)) : + // static attribute + builders.functionExpression( + null, + [], + builders.blockStatement([ + builders.returnStatement(builders.literal(sourceNode.value || true)) + ]), + ) +} diff --git a/node_modules/@riotjs/compiler/src/index.js b/node_modules/@riotjs/compiler/src/index.js new file mode 100644 index 0000000..9aab1a2 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/index.js @@ -0,0 +1,156 @@ +import { TAG_CSS_PROPERTY, TAG_LOGIC_PROPERTY, TAG_NAME_PROPERTY, TAG_TEMPLATE_PROPERTY } from './constants' +import { nullNode, simplePropertyNode } from './utils/custom-ast-nodes' +import { register as registerPostproc, execute as runPostprocessors } from './postprocessors' +import { register as registerPreproc, execute as runPreprocessor } from './preprocessors' +import {builders} from './utils/build-types' +import compose from 'cumpa' +import cssGenerator from './generators/css/index' +import curry from 'curri' +import generateJavascript from './utils/generate-javascript' +import isEmptySourcemap from './utils/is-empty-sourcemap' +import javascriptGenerator from './generators/javascript/index' +import riotParser from '@riotjs/parser' +import sourcemapAsJSON from './utils/sourcemap-as-json' +import templateGenerator from './generators/template/index' + +const DEFAULT_OPTIONS = { + template: 'default', + file: '[unknown-source-file]', + scopedCss: true +} + +/** + * Create the initial AST + * @param {string} tagName - the name of the component we have compiled + * @returns { AST } the initial AST + * + * @example + * // the output represents the following string in AST + */ +export function createInitialInput({tagName}) { + /* + generates + export default { + ${TAG_CSS_PROPERTY}: null, + ${TAG_LOGIC_PROPERTY}: null, + ${TAG_TEMPLATE_PROPERTY}: null + } + */ + return builders.program([ + builders.exportDefaultDeclaration( + builders.objectExpression([ + simplePropertyNode(TAG_CSS_PROPERTY, nullNode()), + simplePropertyNode(TAG_LOGIC_PROPERTY, nullNode()), + simplePropertyNode(TAG_TEMPLATE_PROPERTY, nullNode()), + simplePropertyNode(TAG_NAME_PROPERTY, builders.literal(tagName)) + ]) + )] + ) +} + +/** + * Make sure the input sourcemap is valid otherwise we ignore it + * @param {SourceMapGenerator} map - preprocessor source map + * @returns {Object} sourcemap as json or nothing + */ +function normaliseInputSourceMap(map) { + const inputSourceMap = sourcemapAsJSON(map) + return isEmptySourcemap(inputSourceMap) ? null : inputSourceMap +} + +/** + * Override the sourcemap content making sure it will always contain the tag source code + * @param {Object} map - sourcemap as json + * @param {string} source - component source code + * @returns {Object} original source map with the "sourcesContent" property overriden + */ +function overrideSourcemapContent(map, source) { + return { + ...map, + sourcesContent: [source] + } +} + +/** + * Create the compilation meta object + * @param { string } source - source code of the tag we will need to compile + * @param { string } options - compiling options + * @returns {Object} meta object + */ +function createMeta(source, options) { + return { + tagName: null, + fragments: null, + options: { + ...DEFAULT_OPTIONS, + ...options + }, + source + } +} + +/** + * Generate the output code source together with the sourcemap + * @param { string } source - source code of the tag we will need to compile + * @param { string } opts - compiling options + * @returns { Output } object containing output code and source map + */ +export function compile(source, opts = {}) { + const meta = createMeta(source, opts) + const {options} = meta + const { code, map } = runPreprocessor('template', options.template, meta, source) + const { template, css, javascript } = riotParser(options).parse(code).output + + // extend the meta object with the result of the parsing + Object.assign(meta, { + tagName: template.name, + fragments: { template, css, javascript } + }) + + return compose( + result => ({ ...result, meta }), + result => runPostprocessors(result, meta), + result => ({ + ...result, + map: overrideSourcemapContent(result.map, source) + }), + ast => meta.ast = ast && generateJavascript(ast, { + sourceMapName: `${options.file}.map`, + inputSourceMap: normaliseInputSourceMap(map) + }), + hookGenerator(templateGenerator, template, code, meta), + hookGenerator(javascriptGenerator, javascript, code, meta), + hookGenerator(cssGenerator, css, code, meta), + )(createInitialInput(meta)) +} + +/** + * Prepare the riot parser node transformers + * @param { Function } transformer - transformer function + * @param { Object } sourceNode - riot parser node + * @param { string } source - component source code + * @param { Object } meta - compilation meta information + * @returns { Promise } object containing output code and source map + */ +function hookGenerator(transformer, sourceNode, source, meta) { + if ( + // filter missing nodes + !sourceNode || + // filter nodes without children + (sourceNode.nodes && !sourceNode.nodes.length) || + // filter empty javascript and css nodes + (!sourceNode.nodes && !sourceNode.text)) { + return result => result + } + + return curry(transformer)(sourceNode, source, meta) +} + +// This function can be used to register new preprocessors +// a preprocessor can target either only the css or javascript nodes +// or the complete tag source file ('template') +export const registerPreprocessor = registerPreproc + +// This function can allow you to register postprocessors that will parse the output code +// here we can run prettifiers, eslint fixes... +export const registerPostprocessor = registerPostproc \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/postprocessors.js b/node_modules/@riotjs/compiler/src/postprocessors.js new file mode 100644 index 0000000..48b144d --- /dev/null +++ b/node_modules/@riotjs/compiler/src/postprocessors.js @@ -0,0 +1,53 @@ +import composeSourcemaps from './utils/compose-sourcemaps' +import { createOutput } from './transformer' +import panic from './utils/panic' + +export const postprocessors = new Set() + +/** + * Register a postprocessor that will be used after the parsing and compilation of the riot tags + * @param { Function } postprocessor - transformer that will receive the output code ans sourcemap + * @returns { Set } the postprocessors collection + */ +export function register(postprocessor) { + if (postprocessors.has(postprocessor)) { + panic(`This postprocessor "${postprocessor.name || postprocessor.toString()}" was already registered`) + } + + postprocessors.add(postprocessor) + + return postprocessors +} + +/** + * Unregister a postprocessor + * @param { Function } postprocessor - possibly a postprocessor previously registered + * @returns { Set } the postprocessors collection + */ +export function unregister(postprocessor) { + if (!postprocessors.has(postprocessor)) { + panic(`This postprocessor "${postprocessor.name || postprocessor.toString()}" was never registered`) + } + + postprocessors.delete(postprocessor) + + return postprocessors +} + +/** + * Exec all the postprocessors in sequence combining the sourcemaps generated + * @param { Output } compilerOutput - output generated by the compiler + * @param { Object } meta - compiling meta information + * @returns { Output } object containing output code and source map + */ +export function execute(compilerOutput, meta) { + return Array.from(postprocessors).reduce(function(acc, postprocessor) { + const { code, map } = acc + const output = postprocessor(code, meta) + + return { + code: output.code, + map: composeSourcemaps(map, output.map) + } + }, createOutput(compilerOutput, meta)) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/preprocessors.js b/node_modules/@riotjs/compiler/src/preprocessors.js new file mode 100644 index 0000000..648d2dd --- /dev/null +++ b/node_modules/@riotjs/compiler/src/preprocessors.js @@ -0,0 +1,72 @@ +import panic from './utils/panic' +import { transform } from './transformer' +/** + * Parsers that can be registered by users to preparse components fragments + * @type { Object } + */ +export const preprocessors = Object.freeze({ + javascript: new Map(), + css: new Map(), + template: new Map().set('default', code => ({ code })) +}) + +// throw a processor type error +function preprocessorTypeError(type) { + panic(`No preprocessor of type "${type}" was found, please make sure to use one of these: 'javascript', 'css' or 'template'`) +} + +// throw an error if the preprocessor was not registered +function preprocessorNameNotFoundError(name) { + panic(`No preprocessor named "${name}" was found, are you sure you have registered it?'`) +} + +/** + * Register a custom preprocessor + * @param { string } type - preprocessor type either 'js', 'css' or 'template' + * @param { string } name - unique preprocessor id + * @param { Function } preprocessor - preprocessor function + * @returns { Map } - the preprocessors map + */ +export function register(type, name, preprocessor) { + if (!type) panic('Please define the type of preprocessor you want to register \'javascript\', \'css\' or \'template\'') + if (!name) panic('Please define a name for your preprocessor') + if (!preprocessor) panic('Please provide a preprocessor function') + if (!preprocessors[type]) preprocessorTypeError(type) + if (preprocessors[type].has(name)) panic(`The preprocessor ${name} was already registered before`) + + preprocessors[type].set(name, preprocessor) + + return preprocessors +} + +/** + * Register a custom preprocessor + * @param { string } type - preprocessor type either 'js', 'css' or 'template' + * @param { string } name - unique preprocessor id + * @returns { Map } - the preprocessors map + */ +export function unregister(type, name) { + if (!type) panic('Please define the type of preprocessor you want to unregister \'javascript\', \'css\' or \'template\'') + if (!name) panic('Please define the name of the preprocessor you want to unregister') + if (!preprocessors[type]) preprocessorTypeError(type) + if (!preprocessors[type].has(name)) preprocessorNameNotFoundError(name) + + preprocessors[type].delete(name) + + return preprocessors +} + +/** + * Exec the compilation of a preprocessor + * @param { string } type - preprocessor type either 'js', 'css' or 'template' + * @param { string } name - unique preprocessor id + * @param { Object } meta - preprocessor meta information + * @param { string } source - source code + * @returns { Output } object containing a sourcemap and a code string + */ +export function execute(type, name, meta, source) { + if (!preprocessors[type]) preprocessorTypeError(type) + if (!preprocessors[type].has(name)) preprocessorNameNotFoundError(name) + + return transform(preprocessors[type].get(name), meta, source) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/transformer.js b/node_modules/@riotjs/compiler/src/transformer.js new file mode 100644 index 0000000..c5cae26 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/transformer.js @@ -0,0 +1,45 @@ +import createSourcemap from './utils/create-sourcemap' + +export const Output = Object.freeze({ + code: '', + ast: [], + meta: {}, + map: null +}) + +/** + * Create the right output data result of a parsing + * @param { Object } data - output data + * @param { string } data.code - code generated + * @param { AST } data.ast - ast representing the code + * @param { SourceMapGenerator } data.map - source map generated along with the code + * @param { Object } meta - compilation meta infomration + * @returns { Output } output container object + */ +export function createOutput(data, meta) { + const output = { + ...Output, + ...data, + meta + } + + if (!output.map && meta && meta.options && meta.options.file) + return { + ...output, + map: createSourcemap({ file: meta.options.file }) + } + + return output +} + +/** + * Transform the source code received via a compiler function + * @param { Function } compiler - function needed to generate the output code + * @param { Object } meta - compilation meta information + * @param { string } source - source code + * @returns { Output } output - the result of the compiler + */ +export function transform(compiler, meta, source) { + const result = (compiler ? compiler(source, meta) : { code: source }) + return createOutput(result, meta) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/add-lines-offset.js b/node_modules/@riotjs/compiler/src/utils/add-lines-offset.js new file mode 100644 index 0000000..4e9028e --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/add-lines-offset.js @@ -0,0 +1,13 @@ +import getLineAndColumnByPosition from './get-line-and-column-by-position' + +/** + * Add the offset to the code that must be parsed in order to generate properly the sourcemaps + * @param {string} input - input string + * @param {string} source - original source code + * @param {RiotParser.Node} node - node that we are going to transform + * @return {string} the input string with the offset properly set + */ +export default function addLineOffset(input, source, node) { + const {column, line} = getLineAndColumnByPosition(source, node.start) + return `${'\n'.repeat(line - 1)}${' '.repeat(column + 1)}${input}` +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/ast-nodes-checks.js b/node_modules/@riotjs/compiler/src/utils/ast-nodes-checks.js new file mode 100644 index 0000000..699d2d9 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/ast-nodes-checks.js @@ -0,0 +1,19 @@ +import globalScope from 'globals' +import {namedTypes} from './build-types' + +const browserAPIs = Object.keys(globalScope.browser) +const builtinAPIs = Object.keys(globalScope.builtin) + +export const isIdentifier = namedTypes.Identifier.check.bind(namedTypes.Identifier) +export const isLiteral = namedTypes.Literal.check.bind(namedTypes.Literal) +export const isExpressionStatement = namedTypes.ExpressionStatement.check.bind(namedTypes.ExpressionStatement) +export const isObjectExpression = namedTypes.ObjectExpression.check.bind(namedTypes.ObjectExpression) +export const isThisExpression = namedTypes.ThisExpression.check.bind(namedTypes.ThisExpression) +export const isNewExpression = namedTypes.NewExpression.check.bind(namedTypes.NewExpression) +export const isSequenceExpression = namedTypes.SequenceExpression.check.bind(namedTypes.SequenceExpression) +export const isBinaryExpression = namedTypes.BinaryExpression.check.bind(namedTypes.BinaryExpression) +export const isExportDefaultStatement = namedTypes.ExportDefaultDeclaration.check.bind(namedTypes.ExportDefaultDeclaration) + +export const isBrowserAPI = ({name}) => browserAPIs.includes(name) +export const isBuiltinAPI = ({name}) => builtinAPIs.includes(name) +export const isRaw = (node) => node && node.raw // eslint-disable-line \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/build-types.js b/node_modules/@riotjs/compiler/src/utils/build-types.js new file mode 100644 index 0000000..67573df --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/build-types.js @@ -0,0 +1,5 @@ +import {types as astTypes} from 'recast' + +export const types = astTypes +export const builders = astTypes.builders +export const namedTypes = astTypes.namedTypes \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/clone-deep.js b/node_modules/@riotjs/compiler/src/utils/clone-deep.js new file mode 100644 index 0000000..9a118b3 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/clone-deep.js @@ -0,0 +1,8 @@ +/** + * Simple clone deep function, do not use it for classes or recursive objects! + * @param {*} source - possibily an object to clone + * @returns {*} the object we wanted to clone + */ +export default function cloneDeep(source) { + return JSON.parse(JSON.stringify(source)) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/compose-sourcemaps.js b/node_modules/@riotjs/compiler/src/utils/compose-sourcemaps.js new file mode 100644 index 0000000..c83395a --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/compose-sourcemaps.js @@ -0,0 +1,22 @@ +import asJSON from './sourcemap-as-json' +import {composeSourceMaps} from 'recast/lib/util' +import isNode from './is-node' + +/** + * Compose two sourcemaps + * @param { SourceMapGenerator } formerMap - original sourcemap + * @param { SourceMapGenerator } latterMap - target sourcemap + * @returns { Object } sourcemap json + */ +export default function composeSourcemaps(formerMap, latterMap) { + if ( + isNode() && + formerMap && latterMap && latterMap.mappings + ) { + return composeSourceMaps(asJSON(formerMap), asJSON(latterMap)) + } else if (isNode() && formerMap) { + return asJSON(formerMap) + } + + return {} +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/create-sourcemap.js b/node_modules/@riotjs/compiler/src/utils/create-sourcemap.js new file mode 100644 index 0000000..95d92ff --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/create-sourcemap.js @@ -0,0 +1,10 @@ +import { SourceMapGenerator } from 'source-map' + +/** + * Create a new sourcemap generator + * @param { Object } options - sourcemap options + * @returns { SourceMapGenerator } SourceMapGenerator instance + */ +export default function createSourcemap(options) { + return new SourceMapGenerator(options) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/custom-ast-nodes.js b/node_modules/@riotjs/compiler/src/utils/custom-ast-nodes.js new file mode 100644 index 0000000..b817d38 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/custom-ast-nodes.js @@ -0,0 +1,9 @@ +import {builders} from './build-types' + +export function nullNode() { + return builders.literal(null) +} + +export function simplePropertyNode(key, value) { + return builders.property('init', builders.literal(key), value, false) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/generate-ast.js b/node_modules/@riotjs/compiler/src/utils/generate-ast.js new file mode 100644 index 0000000..8295dab --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/generate-ast.js @@ -0,0 +1,22 @@ +import {Parser} from 'acorn' +import {parse} from 'recast' + +/** + * Parse a js source to generate the AST + * @param {string} source - javascript source + * @param {Object} options - parser options + * @returns {AST} AST tree + */ +export default function generateAST(source, options) { + return parse(source, { + parser: { + parse(source, opts) { + return Parser.parse(source, { + ...opts, + ecmaVersion: 2020 + }) + } + }, + ...options + }) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/generate-javascript.js b/node_modules/@riotjs/compiler/src/utils/generate-javascript.js new file mode 100644 index 0000000..5eaec9b --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/generate-javascript.js @@ -0,0 +1,15 @@ +import {print} from 'recast' + +/** + * Generate the javascript from an ast source + * @param {AST} ast - ast object + * @param {Object} options - printer options + * @returns {Object} code + map + */ +export default function generateJavascript(ast, options) { + return print(ast, { + ...options, + tabWidth: 2, + quote: 'single' + }) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/get-line-and-column-by-position.js b/node_modules/@riotjs/compiler/src/utils/get-line-and-column-by-position.js new file mode 100644 index 0000000..057409f --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/get-line-and-column-by-position.js @@ -0,0 +1,16 @@ +import splitStringByEOL from './split-string-by-EOL' + +/** + * Get the line and the column of a source text based on its position in the string + * @param { string } string - target string + * @param { number } position - target position + * @returns { Object } object containing the source text line and column + */ +export default function getLineAndColumnByPosition(string, position) { + const lines = splitStringByEOL(string.slice(0, position)) + + return { + line: lines.length, + column: lines[lines.length - 1].length + } +} diff --git a/node_modules/@riotjs/compiler/src/utils/get-preprocessor-type-by-attribute.js b/node_modules/@riotjs/compiler/src/utils/get-preprocessor-type-by-attribute.js new file mode 100644 index 0000000..582232e --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/get-preprocessor-type-by-attribute.js @@ -0,0 +1,24 @@ +const ATTRIBUTE_TYPE_NAME = 'type' + +/** + * Get the type attribute from a node generated by the riot parser + * @param { Object} sourceNode - riot parser node + * @returns { string|null } a valid type to identify the preprocessor to use or nothing + */ +export default function getPreprocessorTypeByAttribute(sourceNode) { + const typeAttribute = sourceNode.attributes ? + sourceNode.attributes.find(attribute => attribute.name === ATTRIBUTE_TYPE_NAME) : + null + + return typeAttribute ? normalize(typeAttribute.value) : null +} + + +/** + * Remove the noise in case a user has defined the preprocessor type='text/scss' + * @param { string } value - input string + * @returns { string } normalized string + */ +function normalize(value) { + return value.replace('text/', '') +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/is-empty-sourcemap.js b/node_modules/@riotjs/compiler/src/utils/is-empty-sourcemap.js new file mode 100644 index 0000000..b96e078 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/is-empty-sourcemap.js @@ -0,0 +1,8 @@ +/** + * True if the sourcemap has no mappings, it is empty + * @param {Object} map - sourcemap json + * @returns {boolean} true if empty + */ +export default function isEmptySourcemap(map) { + return !map || !map.mappings || !map.mappings.length +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/is-node.js b/node_modules/@riotjs/compiler/src/utils/is-node.js new file mode 100644 index 0000000..176fab7 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/is-node.js @@ -0,0 +1,7 @@ +/** + * Detect node js environements + * @returns { boolean } true if the runtime is node + */ +export default function isNode() { + return typeof process !== 'undefined' +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/panic.js b/node_modules/@riotjs/compiler/src/utils/panic.js new file mode 100644 index 0000000..5c966ac --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/panic.js @@ -0,0 +1,8 @@ +/** + * Throw an error with a descriptive message + * @param { string } message - error message + * @returns { undefined } hoppla.. at this point the program should stop working + */ +export default function panic(message) { + throw new Error(message) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/preprocess-node.js b/node_modules/@riotjs/compiler/src/utils/preprocess-node.js new file mode 100644 index 0000000..d9ef32a --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/preprocess-node.js @@ -0,0 +1,18 @@ +import {execute as runPreprocessor} from '../preprocessors' + +/** + * Preprocess a riot parser node + * @param { string } preprocessorType - either css, js + * @param { string } preprocessorName - preprocessor id + * @param { Object } meta - compilation meta information + * @param { RiotParser.nodeTypes } node - css node detected by the parser + * @returns { Output } code and sourcemap generated by the preprocessor + */ +export default function preprocess(preprocessorType, preprocessorName, meta, node) { + const code = node.text + + return (preprocessorName ? + runPreprocessor(preprocessorType, preprocessorName, meta, code) : + { code } + ) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/sourcemap-as-json.js b/node_modules/@riotjs/compiler/src/utils/sourcemap-as-json.js new file mode 100644 index 0000000..7395d00 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/sourcemap-as-json.js @@ -0,0 +1,10 @@ +/** + * Return a source map as JSON, it it has not the toJSON method it means it can + * be used right the way + * @param { SourceMapGenerator|Object } map - a sourcemap generator or simply an json object + * @returns { Object } the source map as JSON + */ +export default function sourcemapAsJSON(map) { + if (map && map.toJSON) return map.toJSON() + return map +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/split-string-by-EOL.js b/node_modules/@riotjs/compiler/src/utils/split-string-by-EOL.js new file mode 100644 index 0000000..2e34e63 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/split-string-by-EOL.js @@ -0,0 +1,10 @@ +const LINES_RE = /\r\n?|\n/g + +/** + * Split a string into a rows array generated from its EOL matches + * @param { string } string [description] + * @returns { Array } array containing all the string rows + */ +export default function splitStringByEOL(string) { + return string.split(LINES_RE) +} \ No newline at end of file diff --git a/node_modules/@riotjs/compiler/src/utils/unescape-char.js b/node_modules/@riotjs/compiler/src/utils/unescape-char.js new file mode 100644 index 0000000..9cab2d3 --- /dev/null +++ b/node_modules/@riotjs/compiler/src/utils/unescape-char.js @@ -0,0 +1,9 @@ +/** + * Unescape the user escaped chars + * @param {string} string - input string + * @param {string} char - probably a '{' or anything the user want's to escape + * @returns {string} cleaned up string + */ +export default function unescapeChar(string, char) { + return string.replace(RegExp(`\\\\${char}`, 'gm'), char) +} \ No newline at end of file diff --git a/node_modules/@riotjs/dom-bindings/LICENSE b/node_modules/@riotjs/dom-bindings/LICENSE new file mode 100644 index 0000000..74e6791 --- /dev/null +++ b/node_modules/@riotjs/dom-bindings/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Gianluca Guarini + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/node_modules/@riotjs/dom-bindings/README.md b/node_modules/@riotjs/dom-bindings/README.md new file mode 100644 index 0000000..1e00c5e --- /dev/null +++ b/node_modules/@riotjs/dom-bindings/README.md @@ -0,0 +1,546 @@ +# dom-bindings + +[![Build Status][travis-image]][travis-url] +[![Code Quality][codeclimate-image]][codeclimate-url] +[![NPM version][npm-version-image]][npm-url] +[![NPM downloads][npm-downloads-image]][npm-url] +[![MIT License][license-image]][license-url] +[![Coverage Status][coverage-image]][coverage-url] + +## Usage + +```js +import { template, expressionTypes } from '@riotjs/dom-bindings' + +// Create the app template +const tmpl = template('

', [{ + selector: 'p', + expressions: [ + { + type: expressionTypes.TEXT, + childNodeIndex: 0, + evaluate: scope => scope.greeting, + }, + ], +}]) + +// Mount the template to any DOM node +const target = document.getElementById('app') + +const app = tmpl.mount(target, { + greeting: 'Hello World' +}) +``` + +[travis-image]:https://img.shields.io/travis/riot/dom-bindings.svg?style=flat-square +[travis-url]:https://travis-ci.org/riot/dom-bindings + +[license-image]:http://img.shields.io/badge/license-MIT-000000.svg?style=flat-square +[license-url]:LICENSE + +[npm-version-image]:http://img.shields.io/npm/v/@riotjs/dom-bindings.svg?style=flat-square +[npm-downloads-image]:http://img.shields.io/npm/dm/@riotjs/dom-bindings.svg?style=flat-square +[npm-url]:https://npmjs.org/package/@riotjs/dom-bindings + +[coverage-image]:https://img.shields.io/coveralls/riot/dom-bindings/master.svg?style=flat-square +[coverage-url]:https://coveralls.io/r/riot/dom-bindings/?branch=master + +[codeclimate-image]:https://api.codeclimate.com/v1/badges/d0b7c555a1673354d66f/maintainability +[codeclimate-url]:https://codeclimate.com/github/riot/dom-bindings/maintainability + +## API + +### template(String, Array) + +The template method is the most important of this package. +It will create a `TemplateChunk` that could be mounted, updated and unmounted to any DOM node. + +
+ Details + +A template will always need a string as first argument and a list of `Bindings` to work properly. +Consider the following example: + +```js +const tmpl = template('

', [{ + selector: 'p', + expressions: [ + { + type: expressionTypes.TEXT, + childNodeIndex: 0, + evaluate: scope => scope.greeting + } + ], +}]) +``` + +The template object above will bind a [simple binding](#simple-binding) to the `

` tag. + +

+ +### bindingTypes + +Object containing all the type of bindings supported + +### expressionTypes + +Object containing all the expressions types supported + +## Bindings + +A binding is simply an object that will be used internally to map the data structure provided to a DOM tree. + +
+ Details +To create a binding object you might use the following properties: + +- `expressions` + - type: `Array` + - required: `true` + - description: array containing instructions to execute DOM manipulation on the node queried +- `type` + - type: `Number` + - default:`bindingTypes.SIMPLE` + - optional: `true` + - description: id of the binding to use on the node queried. This id must be one of the keys available in the `bindingTypes` object +- `selector` + - type: `String` + - default: binding root **HTMLElement** + - optional: `true` + - description: property to query the node element that needs to updated + +The bindings supported are only of 4 different types: + +- [`simple`](#simple-binding) to bind simply the expressions to a DOM structure +- [`each`](#each-binding) to render DOM lists +- [`if`](#if-binding) to handle conditional DOM structures +- [`tag`](#tag-binding) to mount a coustom tag template to any DOM node + +Combining the bindings above we can map any javascript object to a DOM template. + +
+ +### Simple Binding + +These kind of bindings will be only used to connect the expressions to DOM nodes in order to manipulate them. + +
+ Details + +**Simple bindings will never modify the DOM tree structure, they will only target a single node.**
+A simple binding must always contain at least one of the following expression: + +- `attribute` to update the node attributes +- `event` to set the event handling +- `text` to update the node content +- `value` to update the node value + +For example, let's consider the following binding: + +```js +const pGreetingBinding = { + selector: 'p', + expressions: [{ + type: expressionTypes.Text, + childNodeIndex: 0, + evaluate: scope => scope.greeting, + }] +} + +template('

', [pGreeting]) +``` + +In this case we have created a binding to update only the content of a `p` tag.
+*Notice that the `p` tag has an empty comment that will be replaced with the value of the binding expression whenever the template will be mounted* + +
+ +#### Simple Binding Expressions + +The simple binding supports DOM manipulations only via expressions. + +
+ Details +An expression object must have always at least the following properties: + +- `evaluate` + - type: `Function` + - description: function that will receive the current template scope and will return the current expression value +- `type` + - type: `Number` + - description: id to find the expression we need to apply to the node. This id must be one of the keys available in the `expressionTypes` object + +
+ +##### Attribute Expression + +The attribute expression allows to update all the DOM node attributes. + +
+ Details + This expression might contain the optional `name` key to update a single attribute for example: + + ```js + // update only the class attribute + { type: expressionTypes.ATTRIBUTE, name: 'class', evaluate(scope) { return scope.attr }} + ``` + + If the `name` key will not be defined and the return of the `evaluate` function will be an object, this expression will set all the pairs `key, value` as DOM attributes.
+ Given the current scope `{ attr: { class: 'hello', 'name': 'world' }}`, the following expression will allow to set all the object attributes: + + ```js + { type: expressionTypes.ATTRIBUTE, evaluate(scope) { return scope.attr }} + ``` + + If the return value of the evaluate function will be a `Boolean` the attribute will be considered a boolean attribute like `checked` or `selected`... +
+ +##### Event Expression + +The event expression is really simple, It must contain the `name` attribute and it will set the callback as `dom[name] = callback`. + +
+ Details +For example: + +```js +// add an event listener +{ type: expressionTypes.EVENT, name: 'onclick', evaluate(scope) { return function() { console.log('Hello There') } }} +``` + +To remove an event listener you should only `return null` via evaluate function: + +```js +// remove an event listener +{ type: expressionTypes.EVENT, name: 'onclick', evaluate(scope) { return null } }} +``` + +
+ +##### Text Expression + +The text expression must contain the `childNodeIndex` that will be used to identify which childNode from the `element.childNodes` collection will need to update its text content. + +
+ Details +Given for example the following template: + +```html +

Your name is:user_icon

+``` + +we could use the following text expression to replace the CommentNode with a TextNode + +```js +{ type: expressionTypes.TEXT, childNodeIndex: 2, evaluate(scope) { return 'Gianluca' } }} +``` +
+ +##### Value Expression + +The value expression will just set the `element.value` with the value received from the evaluate function. + +
+ Details +It should be used only for form elements and it might look like the example below: + +```js +{ type: expressionTypes.VALUE, evaluate(scope) { return scope.val }} +``` + +
+ +### Each Binding + +The `each` binding is used to create multiple DOM nodes of the same type. This binding is typically used in to render javascript collections. + +
+ Details + +**`each` bindings will need a template that will be cloned, mounted and updated for all the instances of the collection.**
+An each binding should contain the following properties: + +- `itemName` + - type: `String` + - required: `true` + - description: name to identify the item object of the current iteration +- `indexName` + - type: `Number` + - optional: `true` + - description: name to identify the current item index +- `evaluate` + - type: `Function` + - required: `true` + - description: function that will return the collection to iterate +- `template` + - type: `TemplateChunk` + - required: `true` + - description: a dom-bindings template that will be used as skeleton for the DOM elements created +- `condition` + - type: `Function` + - optional: `true` + - description: function that can be used to filter the items from the collection + +The each bindings have the highest [hierarchical priority](#bindings-hierarchy) compared to the other riot bindings. +The following binding will loop through the `scope.items` collection creating several `p` tags having as TextNode child value dependent loop item received + +```js +const eachBinding = { + type: bindingTypes.EACH, + itemName: 'val', + indexName: 'index' + evaluate: scope => scope.items, + template: template('', [{ + expressions: [ + { + type: expressionTypes.TEXT, + childNodeIndex: 0, + evaluate: scope => `${scope.val} - ${scope.index}` + } + ] + } +} + +template('

', [eachBinding]) +``` +
+ +### If Binding + +The `if` bindings are needed to handle conditionally entire parts of your components templates + +
+ Details + +**`if` bindings will need a template that will be mounted and unmounted depending on the return value of the evaluate function.**
+An if binding should contain the following properties: + +- `evaluate` + - type: `Function` + - required: `true` + - description: if this function will return truthy values the template will be mounted otherwise unmounted +- `template` + - type: `TemplateChunk` + - required: `true` + - description: a dom-bindings template that will be used as skeleton for the DOM element created + +The following binding will render the `b` tag only if the `scope.isVisible` property will be truthy. Otherwise the `b` tag will be removed from the template + +```js +const ifBinding = { + type: bindingTypes.IF, + evaluate: scope => scope.isVisible, + selector: 'b' + template: template('', [{ + expressions: [ + { + type: expressionTypes.TEXT, + childNodeIndex: 0, + evaluate: scope => scope.name + } + ] + }]) +} + +template('

Hello there

', [ifBinding]) +``` +
+ +### Tag Binding + +The `tag` bindings are needed to mount custom components implementations + +
+ Details + +`tag` bindings will enhance any child node with a custom component factory function. These bindings are likely riot components that must be mounted as children in a parent component template + +A tag binding might contain the following properties: + +- `getComponent` + - type: `Function` + - required: `true` + - description: the factory function responsible for the tag creation +- `evaluate` + - type: `Function` + - required: `true` + - description: it will receive the current scope and it must return the component id that will be passed as first argument to the `getComponent` function +- `slots` + - type: `Array` + - optional: `true` + - description: array containing the slots that must be mounted into the child tag +- `attributes` + - type: `Array` + - optional: `true` + - description: array containing the attribute values that should be passed to the child tag + +The following tag binding will upgrade the `time` tag using the `human-readable-time` template. +This is how the `human-readable-time` template might look like + +```js +import moment from 'moment' + +export default function HumanReadableTime({ attributes }) { + const dateTimeAttr = attributes.find(({ name }) => name === 'datetime') + + return template('', [{ + expressions: [{ + type: expressionTypes.TEXT, + childNodeIndex: 0, + evaluate(scope) { + const dateTimeValue = dateTimeAttr.evaluate(scope) + return moment(new Date(dateTimeValue)).fromNow() + } + }, ...attributes.map(attr => { + return { + ...attr, + type: expressionTypes.ATTRIBUTE + } + })] + }]) +} +``` + +Here it's how the previous tag might be used in a `tag` binding +```js +import HumanReadableTime from './human-readable-time' + +const tagBinding = { + type: bindingTypes.TAG, + evaluate: () => 'human-readable-time', + getComponent: () => HumanReadableTime, + selector: 'time', + attributes: [{ + evaluate: scope => scope.time, + name: 'datetime' + }] +} + +template('

Your last commit was:

', [tagBinding]).mount(app, { + time: '2017-02-14' +}) +``` + +The `tag` bindings have always a lower priority compared to the `if` and `each` bindings +
+ +#### Slot Binding + +The slot binding will be used to manage nested slotted templates that will be update using parent scope + +
+ Details +An expression object must have always at least the following properties: + +- `evaluate` + - type: `Function` + - description: function that will receive the current template scope and will return the current expression value +- `type` + - type: `Number` + - description: id to find the expression we need to apply to the node. This id must be one of the keys available in the `expressionTypes` object +- `name` + - type: `String` + - description: the name to identify the binding html we need to mount in this node + + +```js +// slots array that will be mounted receiving the scope of the parent template +const slots = [{ + id: 'foo', + bindings: [{ + selector: '[expr1]', + expressions: [{ + type: expressionTypes.TEXT, + childNodeIndex: 0, + evaluate: scope => scope.text + }] + }], + html: '

' +}] + +const el = template('
', [{ + type: bindingTypes.SLOT, + selector: '[expr0]', + name: 'foo' +}]).mount(app, { + slots +}, { text: 'hello' }) +``` + +
+ +## Bindings Hierarchy + +If the same DOM node has multiple bindings bound to it, they should be created following the order below: + +1. Each Binding +2. If Binding +3. Tag Binding + +
+ Details + +Let's see some cases where we might combine multiple bindings on the same DOM node and how to handle them properly. + +### Each and If Bindings +Let's consider for example a DOM node that sould handle in parallel the Each and If bindings. +In that case we could skip the `If Binding` and just use the `condition` function provided by the [`Each Binding`](#each-binding) +Each bindings will handle conditional rendering internally without the need of extra logic. + +### Each and Tag Bindings +A custom tag having an Each Binding bound to it should be handled giving the priority to the Eeach Binding. For example: + +```js +const components = { + 'my-tag': function({ slots, attributes }) { + return { + mount(el, scope) { + // do stuff on the mount + }, + unmount() { + // do stuff on the unmount + } + } + } +} +const el = template('
', [{ + type: bindingTypes.EACH, + itemName: 'val', + selector: '[expr0]', + evaluate: scope => scope.items, + template: template(null, [{ + type: bindingTypes.TAG, + name: 'my-tag', + getComponent(name) { + // name here will be 'my-tag' + return components[name] + } + }]) +}]).mount(target, { items: [1, 2] }) +``` + +The template for the Each Binding above will be created receiving `null` as first argument because we suppose that the custom tag template was already stored and registered somewhere else. + +### If and Tag Bindings +Similar to the previous example, If Bindings have always the priority on the Tag Bindings. For example: + +```js +const el = template('
', [{ + type: bindingTypes.IF, + selector: '[expr0]', + evaluate: scope => scope.isVisible, + template: template(null, [{ + type: bindingTypes.TAG, + evaluate: () => 'my-tag', + getComponent(name) { + // name here will be 'my-tag' + return components[name] + } + }]) +}]).mount(target, { isVisible: true }) +``` + +The template for the IF Binding will mount/unmount the Tag Binding on its own DOM node. + +
+ diff --git a/node_modules/@riotjs/dom-bindings/dist/esm.dom-bindings.js b/node_modules/@riotjs/dom-bindings/dist/esm.dom-bindings.js new file mode 100644 index 0000000..9d677fb --- /dev/null +++ b/node_modules/@riotjs/dom-bindings/dist/esm.dom-bindings.js @@ -0,0 +1,1657 @@ +/** + * Remove the child nodes from any DOM node + * @param {HTMLElement} node - target node + * @returns {undefined} + */ +function cleanNode(node) { + clearChildren(node.childNodes); +} + +/** + * Clear multiple children in a node + * @param {HTMLElement[]} children - direct children nodes + * @returns {undefined} + */ +function clearChildren(children) { + Array.from(children).forEach(n => n.parentNode && n.parentNode.removeChild(n)); +} + +const EACH = 0; +const IF = 1; +const SIMPLE = 2; +const TAG = 3; +const SLOT = 4; + +var bindingTypes = { + EACH, + IF, + SIMPLE, + TAG, + SLOT +}; + +/** + * Create the template meta object in case of