diff --git a/CHANGELOG.md b/CHANGELOG.md index bea16d68..173301d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## unreleased +### Bugfixes +- Fix handling mapping that omit key part + --- ## 0.8.0 (2024-08-09) diff --git a/src/melody/melody-parser/Parser.js b/src/melody/melody-parser/Parser.js index 71f29ab1..ba2cab1c 100644 --- a/src/melody/melody-parser/Parser.js +++ b/src/melody/melody-parser/Parser.js @@ -52,6 +52,10 @@ const TAG = Symbol("TAG"); const TEST = Symbol("TEST"); export default class Parser { + /** + * @param {TokenStream} tokenStream + * @param {Object} options + */ constructor(tokenStream, options) { this.tokens = tokenStream; this[UNARY] = {}; @@ -625,7 +629,7 @@ export default class Parser { if (token.type === Types.LBRACE) { node = this.matchArray(); } else if (token.type === Types.LBRACKET) { - node = this.matchMap(); + node = this.matchMapping(); } else { this.error( { @@ -751,7 +755,7 @@ export default class Parser { return array; } - matchMap() { + matchMapping() { const tokens = this.tokens; let token; const obj = new n.ObjectExpression(); @@ -765,6 +769,9 @@ export default class Parser { if (!n.is(key, "StringLiteral")) { computed = true; } + } else if ((token = tokens.nextIf(Types.EXPRESSION_START))) { + key = this.matchExpression(); + computed = true; } else if ((token = tokens.nextIf(Types.SYMBOL))) { key = createNode(n.Identifier, token, token.text); } else if ((token = tokens.nextIf(Types.NUMBER))) { @@ -781,12 +788,20 @@ export default class Parser { tokens.next() }); } - tokens.expect(Types.COLON); - const value = this.matchExpression(); - const prop = new n.ObjectProperty(key, value, computed); - copyStart(prop, key); - copyEnd(prop, value); - obj.properties.push(prop); + if (tokens.test(Types.COLON)) { + tokens.expect(Types.COLON); + const value = this.matchExpression(); + const prop = new n.ObjectProperty(key, value, computed); + copyStart(prop, key); + copyEnd(prop, value); + obj.properties.push(prop); + } else { + const value = key; + const prop = new n.ObjectProperty(key, value, computed, true); + copyStart(prop, key); + copyEnd(prop, value); + obj.properties.push(prop); + } if (!tokens.test(Types.RBRACKET)) { tokens.expect(Types.COMMA); // support trailing comma diff --git a/src/melody/melody-types/index.js b/src/melody/melody-types/index.js index 60762b3a..4f911ba2 100644 --- a/src/melody/melody-types/index.js +++ b/src/melody/melody-types/index.js @@ -356,12 +356,14 @@ export class ObjectProperty extends Node { * @param {Node} key * @param {Node} value * @param {boolean} computed + * @param {boolean} omitKey */ - constructor(key, value, computed) { + constructor(key, value, computed, omitKey = false) { super(); this.key = key; this.value = value; this.computed = computed; + this.omitKey = omitKey; } } type(ObjectProperty, "ObjectProperty"); diff --git a/src/print/ObjectProperty.js b/src/print/ObjectProperty.js index 0e53b122..4c423834 100644 --- a/src/print/ObjectProperty.js +++ b/src/print/ObjectProperty.js @@ -16,6 +16,10 @@ const p = (node, path, print, options) => { if (needsParentheses) { parts.push(")"); } + // handle property that omit key + if (node.omitKey) { + return parts; + } parts.push(": "); node[STRING_NEEDS_QUOTES] = true; parts.push(path.call(print, "value")); diff --git a/tests/Expressions/__snapshots__/mappingExpression.snap.twig b/tests/Expressions/__snapshots__/mappingExpression.snap.twig new file mode 100644 index 00000000..bcac4929 --- /dev/null +++ b/tests/Expressions/__snapshots__/mappingExpression.snap.twig @@ -0,0 +1,70 @@ +{# Example mappings literal expression taken from https://twig.symfony.com/doc/3.x/templates.html#literals #} +{# keys as string #} +{{ + { + foo: 'foo', + bar: 'bar' + } +}} + +{# keys as names (equivalent to the previous mapping) #} +{{ + { + foo: 'foo', + bar: 'bar' + } +}} + +{# keys as integer #} +{{ + { + 2: 'foo', + 4: 'bar' + } +}} + +{# keys can be omitted if it is the same as the variable name #} +{% set nokey = { + nokey +} %} + +{% set nokey = { + no, + key +} %} +{# is equivalent to the following #} +{% set no_key = { + no: no, + key: key +} %} + +{% set foo = 'foo' %} + +{# keys as expressions (the expression must be enclosed into parentheses) #} +{{ + { + (foo): 'foo', + (1 + 1): 'bar', + (foo ~ 'b'): 'baz' + } +}} + +{% props theme = null, text = null %} + +
diff --git a/tests/Expressions/jsfmt.spec.js b/tests/Expressions/jsfmt.spec.js index e18d5373..542e7e87 100644 --- a/tests/Expressions/jsfmt.spec.js +++ b/tests/Expressions/jsfmt.spec.js @@ -38,6 +38,12 @@ describe("Expressions", () => { }); await expect(actual).toMatchFileSnapshot(snapshotFile); }); + it("should handle mapping expressions", async () => { + const { actual, snapshotFile } = await run_spec(import.meta.url, { + source: "mappingExpression.twig" + }); + await expect(actual).toMatchFileSnapshot(snapshotFile); + }); it("should handle member expressions", async () => { const { actual, snapshotFile } = await run_spec(import.meta.url, { source: "memberExpression.twig" diff --git a/tests/Expressions/mappingExpression.twig b/tests/Expressions/mappingExpression.twig new file mode 100644 index 00000000..e91494af --- /dev/null +++ b/tests/Expressions/mappingExpression.twig @@ -0,0 +1,39 @@ +{# Example mappings literal expression taken from https://twig.symfony.com/doc/3.x/templates.html#literals #} +{# keys as string #} +{{ { 'foo': 'foo', 'bar': 'bar' } }} + +{# keys as names (equivalent to the previous mapping) #} +{{ { foo: 'foo', bar: 'bar' } }} + +{# keys as integer #} +{{ { 2: 'foo', 4: 'bar' } }} + +{# keys can be omitted if it is the same as the variable name #} +{% set nokey = { nokey } %} + +{% set nokey = { no, key } %} +{# is equivalent to the following #} +{% set no_key = { 'no': no, 'key': key } %} + +{% set foo = 'foo' %} + +{# keys as expressions (the expression must be enclosed into parentheses) #} +{{ { (foo): 'foo', (1 + 1): 'bar', (foo ~ 'b'): 'baz' } }} + +{% props theme = null, text = null %} + +