Skip to content

Commit

Permalink
Merge branch 'fix-mappings-expression' into master
Browse files Browse the repository at this point in the history
Close GH-16.

Fix handling mapping expression when the key parts is being omitted.
  • Loading branch information
zackad committed Aug 19, 2024
2 parents 1b69010 + b0dccf8 commit 5f81b65
Show file tree
Hide file tree
Showing 7 changed files with 148 additions and 9 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## unreleased

### Bugfixes
- Fix handling mapping that omit key part

---
## 0.8.0 (2024-08-09)

Expand Down
31 changes: 23 additions & 8 deletions src/melody/melody-parser/Parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {};
Expand Down Expand Up @@ -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(
{
Expand Down Expand Up @@ -751,7 +755,7 @@ export default class Parser {
return array;
}

matchMap() {
matchMapping() {
const tokens = this.tokens;
let token;
const obj = new n.ObjectExpression();
Expand All @@ -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))) {
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion src/melody/melody-types/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
4 changes: 4 additions & 0 deletions src/print/ObjectProperty.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
70 changes: 70 additions & 0 deletions tests/Expressions/__snapshots__/mappingExpression.snap.twig
Original file line number Diff line number Diff line change
@@ -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 %}

<div {{
attributes.defaults({
class: cva({
base: 'alert shadow-md',
variants: {
theme: {
info: 'alert-info',
success: 'alert-success',
warning: 'alert-warning',
error: 'alert-error'
}
}
}).apply({
theme
})
})
}}></div>
6 changes: 6 additions & 0 deletions tests/Expressions/jsfmt.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
39 changes: 39 additions & 0 deletions tests/Expressions/mappingExpression.twig
Original file line number Diff line number Diff line change
@@ -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 %}

<div
{{ attributes.defaults({
class: cva({
base: "alert shadow-md",
variants: {
theme: {
info: "alert-info",
success: "alert-success",
warning: "alert-warning",
error: "alert-error",
},
},
}).apply({ theme }),
}) }}
></div>

0 comments on commit 5f81b65

Please sign in to comment.