diff --git a/package-lock.json b/package-lock.json index c75abfa..601982b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -45,7 +45,7 @@ "rollup": "^4.40.0", "rollup-plugin-node-builtins": "^2.1.2", "rollup-plugin-visualizer": "^5.14.0", - "sass": "^1.86.3", + "sass": "^1.87.0", "serve": "^14.2.4", "shelljs": "^0.9.2", "start-server-and-test": "^2.0.11", @@ -7615,9 +7615,9 @@ "dev": true }, "node_modules/sass": { - "version": "1.86.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.86.3.tgz", - "integrity": "sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw==", + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz", + "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", "dev": true, "license": "MIT", "dependencies": { @@ -14044,9 +14044,9 @@ "dev": true }, "sass": { - "version": "1.86.3", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.86.3.tgz", - "integrity": "sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw==", + "version": "1.87.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz", + "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==", "dev": true, "requires": { "@parcel/watcher": "^2.4.1", diff --git a/package.json b/package.json index 2a77e08..aab3a70 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "rollup": "^4.40.0", "rollup-plugin-node-builtins": "^2.1.2", "rollup-plugin-visualizer": "^5.14.0", - "sass": "^1.86.3", + "sass": "^1.87.0", "serve": "^14.2.4", "shelljs": "^0.9.2", "start-server-and-test": "^2.0.11", diff --git a/src/generators/template/builder.js b/src/generators/template/builder.js index 4cd0838..98fd69d 100644 --- a/src/generators/template/builder.js +++ b/src/generators/template/builder.js @@ -87,7 +87,7 @@ function createTagWithBindings(sourceNode, sourceFile, sourceCode) { ? null : createBindingSelector() const cloneNode = createBindingsTag(sourceNode, bindingsSelector) - const tagOpeningHTML = nodeToString(cloneNode) + const tagOpeningHTML = nodeToString(cloneNode, bindingsSelector) switch (true) { case hasEachAttribute(cloneNode): diff --git a/src/generators/template/utils.js b/src/generators/template/utils.js index 412e759..7180d71 100644 --- a/src/generators/template/utils.js +++ b/src/generators/template/utils.js @@ -20,6 +20,7 @@ import { builders, types } from '../../utils/build-types.js' import { findIsAttribute, findStaticAttributes } from './find.js' import { hasExpressions, + isCustomNode, isGlobal, isTagNode, isTextNode, @@ -557,13 +558,32 @@ export function unescapeNode(node, key) { return node } +/** + * Custom nodes can only render a small subset of their static attributes + * only the is and the expr attribute can be rendered as static attributes + * @param {RiotParser.Node} node - a custom element node + * @param {string} bindingsSelector - temporary string to identify the current node + * @returns {Array} list of the attributes that can be statically rendered + */ +export function filterCustomNodeStaticAttributes(node, bindingsSelector) { + return node.attributes.filter( + (attribute) => + attribute.name === bindingsSelector || attribute.name === IS_DIRECTIVE, + ) +} + /** * Convert a riot parser opening node into a string * @param {RiotParser.Node} node - riot parser node + * @param {string} bindingsSelector - temporary string to identify the current node * @returns {string} the node as string */ -export function nodeToString(node) { - const attributes = staticAttributesToString(node) +export function nodeToString(node, bindingsSelector) { + const attributes = staticAttributesToString( + isCustomNode(node) + ? { attributes: filterCustomNodeStaticAttributes(node, bindingsSelector) } + : node, + ) switch (true) { case isTagNode(node): @@ -606,30 +626,41 @@ export function createArrayString(stringsArray) { /** * 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 + * This helper aims to merge them into 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) + switch (true) { + // static attributes don't need to be merged, nor expression transformations are needed + case !hasExpressions(node) && node.parts.length === 1: + return builders.literal(node.parts[0]) + // if there are no node parts or there is just one item we just create a simple expression literal + case !node.parts || node.parts.length === 1: + return transformExpression(node.expressions[0], sourceFile, sourceCode) + default: + // merge the siblings expressions into a single array literal + return createArrayString( + [ + // fold the expression parts into a single array + ...node.parts.reduce((acc, str) => { + const expression = node.expressions.find( + (e) => e.text.trim() === str, + ) + + return [ + ...acc, + expression + ? transformExpression(expression, sourceFile, sourceCode) + : builders.literal(encodeHTMLEntities(str)), + ] + }, []), + // filter out invalid items that are not literal or have no value + ].filter((expr) => !isLiteral(expr) || expr.value), + ) } - const stringsArray = [ - ...node.parts.reduce((acc, str) => { - const expression = node.expressions.find((e) => e.text.trim() === str) - - return [ - ...acc, - expression - ? transformExpression(expression, sourceFile, sourceCode) - : builders.literal(encodeHTMLEntities(str)), - ] - }, []), - ].filter((expr) => !isLiteral(expr) || expr.value) - - return createArrayString(stringsArray) } /** @@ -661,7 +692,6 @@ export function createBindingAttributes( attributes.map((attribute) => createExpression(attribute, sourceFile, sourceCode, 0, sourceNode), ), - (attributes) => attributes.filter(hasExpressions), (attributes) => getAttributesWithoutSelector(attributes, selectorAttribute), cleanAttributes, diff --git a/test/generators/template.spec.js b/test/generators/template.spec.js index 2afe681..834ee32 100644 --- a/test/generators/template.spec.js +++ b/test/generators/template.spec.js @@ -805,10 +805,14 @@ describe('Generators - Template', () => { expect(output[BINDING_TYPE_KEY]).to.be.equal(bindingTypes.TAG) expect(output[BINDING_EVALUATE_KEY]()).to.be.equal('my-tag') expect(output.slots).to.have.length(0) - expect(output[BINDING_ATTRIBUTES_KEY]).to.have.length(1) + expect(output[BINDING_ATTRIBUTES_KEY]).to.have.length(2) expect( output[BINDING_ATTRIBUTES_KEY][0][BINDING_EVALUATE_KEY]({ foo: 'foo' }), ).to.be.equal('foo') + + expect( + output[BINDING_ATTRIBUTES_KEY][1][BINDING_EVALUATE_KEY](), + ).to.be.equal('my-id') }) it('Children tags do not inherit "expr" and "is" attributes', () => { @@ -823,12 +827,15 @@ describe('Generators - Template', () => { expect(tagBinding[BINDING_TYPE_KEY]).to.be.equal(bindingTypes.TAG) expect(tagBinding[BINDING_EVALUATE_KEY]()).to.be.equal('my-tag') expect(tagBinding.slots).to.have.length(0) - expect(tagBinding[BINDING_ATTRIBUTES_KEY]).to.have.length(1) + expect(tagBinding[BINDING_ATTRIBUTES_KEY]).to.have.length(2) expect( tagBinding[BINDING_ATTRIBUTES_KEY][0][BINDING_EVALUATE_KEY]({ foo: 'foo', }), ).to.be.equal('foo') + expect( + tagBinding[BINDING_ATTRIBUTES_KEY][1][BINDING_EVALUATE_KEY](), + ).to.be.equal('my-id') }) it('Tag bindings can be computed', () => { @@ -873,7 +880,7 @@ describe('Generators - Template', () => { expect(defaultSlot[BINDING_HTML_KEY]).to.be.equal('
hello
') expect(defaultSlot[BINDING_BINDINGS_KEY]).to.be.deep.equal([]) expect(defaultSlot[BINDING_ID_KEY]).to.be.equal('default') - expect(output[BINDING_ATTRIBUTES_KEY]).to.have.length(1) + expect(output[BINDING_ATTRIBUTES_KEY]).to.have.length(2) expect( output[BINDING_ATTRIBUTES_KEY][0][BINDING_EVALUATE_KEY]({ foo: 'foo' }), ).to.be.equal('foo') @@ -961,7 +968,7 @@ describe('Generators - Template', () => { const output = evaluateOutput(input) expect(output[BINDING_SELECTOR_KEY]).to.be.equal('[expr0]') - expect(output.attributes).to.have.length(0) + expect(output.attributes).to.have.length(1) }) it('Tag binding with multiple slots with expressions', () => { @@ -1220,8 +1227,8 @@ describe('Generators - Template', () => { expect( output.template.bindingsData[0].attributes, - "static attributes should't be parsed as expressions", - ).to.have.length(1) + 'static attributes should also be parsed as expressions', + ).to.have.length(2) expect(output[BINDING_SELECTOR_KEY]).to.be.equal('[expr0]') expect(expression[BINDING_EVALUATE_KEY]).to.be.a('function') @@ -1597,9 +1604,7 @@ describe('Generators - Template', () => { const source = '' const { template } = parse(source) const html = buildSimpleTemplate(template, 'expr0', FAKE_SRC_FILE, source) - expect(html).to.be.equal( - '', - ) + expect(html).to.be.equal('') }) it('Event attributes on custom tags do not break the compiler (issue #124)', () => { @@ -1607,9 +1612,7 @@ describe('Generators - Template', () => { '' const { template } = parse(source) const html = buildSimpleTemplate(template, 'expr0', FAKE_SRC_FILE, source) - expect(html).to.be.equal( - '', - ) + expect(html).to.be.equal('') }) }) })