Skip to content

Commit 568d53b

Browse files
authoredNov 8, 2024··
Add content_for tag parser support + ValidContentForArguments check (#568)
Fixes #466

File tree

29 files changed

+499
-62
lines changed

29 files changed

+499
-62
lines changed
 

‎.changeset/purple-pears-report.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@shopify/theme-check-browser': minor
3+
'@shopify/theme-check-common': minor
4+
'@shopify/theme-check-node': minor
5+
---
6+
7+
Add the `ValidContentForArguments` check

‎.changeset/tasty-mugs-behave.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@shopify/liquid-html-parser': minor
3+
'@shopify/prettier-plugin-liquid': minor
4+
'@shopify/theme-language-server-common': minor
5+
---
6+
7+
Add support for the `content_for` Liquid tag

‎packages/liquid-html-parser/grammar/liquid-html.ohm

+6
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ Liquid <: Helpers {
5050
| liquidTagBreak
5151
| liquidTagContinue
5252
| liquidTagCycle
53+
| liquidTagContentFor
5354
| liquidTagDecrement
5455
| liquidTagEcho
5556
| liquidTagElse
@@ -125,6 +126,11 @@ Liquid <: Helpers {
125126
liquidTagLiquid = liquidTagRule<"liquid", liquidTagLiquidMarkup>
126127
liquidTagLiquidMarkup = tagMarkup
127128

129+
liquidTagContentFor = liquidTagRule<"content_for", liquidTagContentForMarkup>
130+
liquidTagContentForMarkup =
131+
contentForType (argumentSeparatorOptionalComma tagArguments) (space* ",")? space*
132+
contentForType = liquidString<delimTag>
133+
128134
liquidTagInclude = liquidTagRule<"include", liquidTagRenderMarkup>
129135
liquidTagRender = liquidTagRule<"render", liquidTagRenderMarkup>
130136
liquidTagRenderMarkup =

‎packages/liquid-html-parser/src/stage-1-cst.spec.ts

+39
Original file line numberDiff line numberDiff line change
@@ -586,6 +586,45 @@ describe('Unit: Stage 1 (CST)', () => {
586586
expectPath(cst, '0.markup.expression.lookups').to.eql([]);
587587
}
588588
});
589+
590+
it('should parse content_for "blocks"', () => {
591+
for (const { toCST, expectPath } of testCases) {
592+
cst = toCST(`{% content_for "blocks" -%}`);
593+
expectPath(cst, '0.type').to.equal('LiquidTag');
594+
expectPath(cst, '0.name').to.equal('content_for');
595+
expectPath(cst, '0.markup.type').to.equal('ContentForMarkup');
596+
expectPath(cst, '0.markup.contentForType.type').to.equal('String');
597+
expectPath(cst, '0.markup.contentForType.value').to.equal('blocks');
598+
expectPath(cst, '0.markup.contentForType.single').to.equal(false);
599+
expectPath(cst, '0.markup.args').to.have.lengthOf(0);
600+
expectPath(cst, '0.whitespaceStart').to.equal(null);
601+
expectPath(cst, '0.whitespaceEnd').to.equal('-');
602+
}
603+
});
604+
605+
it('should parse content_for "block", id: "my-id", type: "my-block"', () => {
606+
for (const { toCST, expectPath } of testCases) {
607+
cst = toCST(`{% content_for "block", id: "block-id", type: "block-type" -%}`);
608+
expectPath(cst, '0.type').to.equal('LiquidTag');
609+
expectPath(cst, '0.name').to.equal('content_for');
610+
expectPath(cst, '0.markup.type').to.equal('ContentForMarkup');
611+
expectPath(cst, '0.markup.contentForType.type').to.equal('String');
612+
expectPath(cst, '0.markup.contentForType.value').to.equal('block');
613+
expectPath(cst, '0.markup.contentForType.single').to.equal(false);
614+
expectPath(cst, '0.markup.args').to.have.lengthOf(2);
615+
const namedArguments = [
616+
{ name: 'id', valueType: 'String' },
617+
{ name: 'type', valueType: 'String' },
618+
];
619+
namedArguments.forEach(({ name, valueType }, i) => {
620+
expectPath(cst, `0.markup.args.${i}.type`).to.equal('NamedArgument');
621+
expectPath(cst, `0.markup.args.${i}.name`).to.equal(name);
622+
expectPath(cst, `0.markup.args.${i}.value.type`).to.equal(valueType);
623+
});
624+
expectPath(cst, '0.whitespaceStart').to.equal(null);
625+
expectPath(cst, '0.whitespaceEnd').to.equal('-');
626+
}
627+
});
589628
});
590629

591630
describe('Case: LiquidTagOpen', () => {

‎packages/liquid-html-parser/src/stage-1-cst.ts

+22
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export enum ConcreteNodeTypes {
7575
Condition = 'Condition',
7676

7777
AssignMarkup = 'AssignMarkup',
78+
ContentForMarkup = 'ContentForMarkup',
7879
CycleMarkup = 'CycleMarkup',
7980
ForMarkup = 'ForMarkup',
8081
RenderMarkup = 'RenderMarkup',
@@ -265,6 +266,7 @@ export type ConcreteLiquidTag = ConcreteLiquidTagNamed | ConcreteLiquidTagBaseCa
265266
export type ConcreteLiquidTagNamed =
266267
| ConcreteLiquidTagAssign
267268
| ConcreteLiquidTagCycle
269+
| ConcreteLiquidTagContentFor
268270
| ConcreteLiquidTagEcho
269271
| ConcreteLiquidTagIncrement
270272
| ConcreteLiquidTagDecrement
@@ -321,11 +323,20 @@ export interface ConcreteLiquidTagCycleMarkup
321323
args: ConcreteLiquidExpression[];
322324
}
323325

326+
export interface ConcreteLiquidTagContentFor
327+
extends ConcreteLiquidTagNode<NamedTags.content_for, ConcreteLiquidTagContentForMarkup> {}
328+
324329
export interface ConcreteLiquidTagRender
325330
extends ConcreteLiquidTagNode<NamedTags.render, ConcreteLiquidTagRenderMarkup> {}
326331
export interface ConcreteLiquidTagInclude
327332
extends ConcreteLiquidTagNode<NamedTags.include, ConcreteLiquidTagRenderMarkup> {}
328333

334+
export interface ConcreteLiquidTagContentForMarkup
335+
extends ConcreteBasicNode<ConcreteNodeTypes.ContentForMarkup> {
336+
contentForType: ConcreteStringLiteral;
337+
args: ConcreteLiquidNamedArgument[];
338+
}
339+
329340
export interface ConcreteLiquidTagRenderMarkup
330341
extends ConcreteBasicNode<ConcreteNodeTypes.RenderMarkup> {
331342
snippet: ConcreteStringLiteral | ConcreteLiquidVariableLookup;
@@ -715,6 +726,7 @@ function toCST<T>(
715726
liquidTagBaseCase: 0,
716727
liquidTagAssign: 0,
717728
liquidTagEcho: 0,
729+
liquidTagContentFor: 0,
718730
liquidTagCycle: 0,
719731
liquidTagIncrement: 0,
720732
liquidTagDecrement: 0,
@@ -775,6 +787,16 @@ function toCST<T>(
775787
source,
776788
},
777789

790+
liquidTagContentForMarkup: {
791+
type: ConcreteNodeTypes.ContentForMarkup,
792+
contentForType: 0,
793+
args: 2,
794+
locStart,
795+
locEnd,
796+
source,
797+
},
798+
contentForType: 0,
799+
778800
liquidTagRenderMarkup: {
779801
type: ConcreteNodeTypes.RenderMarkup,
780802
snippet: 0,

‎packages/liquid-html-parser/src/stage-2-ast.spec.ts

+41
Original file line numberDiff line numberDiff line change
@@ -579,6 +579,47 @@ describe('Unit: Stage 2 (AST)', () => {
579579
});
580580
});
581581
});
582+
583+
describe('Case: content_for', () => {
584+
it('should parse content_for tags with no arguments', () => {
585+
for (const { toAST, expectPath, expectPosition } of testCases) {
586+
ast = toAST(`{% content_for "blocks" %}`);
587+
expectPath(ast, 'children.0.type').to.equal('LiquidTag');
588+
expectPath(ast, 'children.0.name').to.equal('content_for');
589+
expectPath(ast, 'children.0.markup.type').to.equal('ContentForMarkup');
590+
expectPath(ast, 'children.0.markup.contentForType.type').to.equal('String');
591+
expectPath(ast, 'children.0.markup.contentForType.value').to.equal('blocks');
592+
expectPosition(ast, 'children.0');
593+
expectPosition(ast, 'children.0.markup');
594+
}
595+
});
596+
597+
it('should parse content_for named expression arguments', () => {
598+
for (const { toAST, expectPath, expectPosition } of testCases) {
599+
ast = toAST(`{% content_for "snippet", s: 'string', n: 10, r: (1..2), v: variable %}`);
600+
expectPath(ast, 'children.0.type').to.equal('LiquidTag');
601+
expectPath(ast, 'children.0.name').to.equal('content_for');
602+
expectPath(ast, 'children.0.markup.type').to.equal('ContentForMarkup');
603+
expectPath(ast, 'children.0.markup.contentForType.type').to.equal('String');
604+
expectPath(ast, 'children.0.markup.contentForType.value').to.equal('snippet');
605+
expectPath(ast, 'children.0.markup.args').to.have.lengthOf(4);
606+
expectPath(ast, 'children.0.markup.args.0.type').to.equal('NamedArgument');
607+
expectPath(ast, 'children.0.markup.args.0.name').to.equal('s');
608+
expectPath(ast, 'children.0.markup.args.0.value.type').to.equal('String');
609+
expectPath(ast, 'children.0.markup.args.1.type').to.equal('NamedArgument');
610+
expectPath(ast, 'children.0.markup.args.1.name').to.equal('n');
611+
expectPath(ast, 'children.0.markup.args.1.value.type').to.equal('Number');
612+
expectPath(ast, 'children.0.markup.args.2.type').to.equal('NamedArgument');
613+
expectPath(ast, 'children.0.markup.args.2.name').to.equal('r');
614+
expectPath(ast, 'children.0.markup.args.2.value.type').to.equal('Range');
615+
expectPath(ast, 'children.0.markup.args.3.type').to.equal('NamedArgument');
616+
expectPath(ast, 'children.0.markup.args.3.name').to.equal('v');
617+
expectPath(ast, 'children.0.markup.args.3.value.type').to.equal('VariableLookup');
618+
expectPosition(ast, 'children.0');
619+
expectPosition(ast, 'children.0.markup');
620+
}
621+
});
622+
});
582623
});
583624

584625
it(`should parse liquid inline comments`, () => {

‎packages/liquid-html-parser/src/stage-2-ast.ts

+33
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
ConcreteLiquidRawTag,
7373
LiquidHtmlConcreteNode,
7474
ConcreteLiquidTagBaseCase,
75+
ConcreteLiquidTagContentForMarkup,
7576
} from './stage-1-cst';
7677
import { Comparators, NamedTags, NodeTypes, nonTraversableProperties, Position } from './types';
7778
import { assertNever, deepGet, dropLast } from './utils';
@@ -97,6 +98,7 @@ export type LiquidHtmlNode =
9798
| LiquidFilter
9899
| LiquidNamedArgument
99100
| AssignMarkup
101+
| ContentForMarkup
100102
| CycleMarkup
101103
| ForMarkup
102104
| RenderMarkup
@@ -188,6 +190,7 @@ export type LiquidTagNamed =
188190
| LiquidTagAssign
189191
| LiquidTagCase
190192
| LiquidTagCapture
193+
| LiquidTagContentFor
191194
| LiquidTagCycle
192195
| LiquidTagDecrement
193196
| LiquidTagEcho
@@ -359,6 +362,10 @@ export interface PaginateMarkup extends ASTNode<NodeTypes.PaginateMarkup> {
359362
args: LiquidNamedArgument[];
360363
}
361364

365+
/** https://shopify.dev/docs/api/liquid/tags#content_for */
366+
export interface LiquidTagContentFor
367+
extends LiquidTagNode<NamedTags.content_for, ContentForMarkup> {}
368+
362369
/** https://shopify.dev/docs/api/liquid/tags#render */
363370
export interface LiquidTagRender extends LiquidTagNode<NamedTags.render, RenderMarkup> {}
364371

@@ -377,6 +384,14 @@ export interface LiquidTagLayout extends LiquidTagNode<NamedTags.layout, LiquidE
377384
/** https://shopify.dev/docs/api/liquid/tags#liquid */
378385
export interface LiquidTagLiquid extends LiquidTagNode<NamedTags.liquid, LiquidStatement[]> {}
379386

387+
/** {% content_for 'contentForType' [...namedArguments] %} */
388+
export interface ContentForMarkup extends ASTNode<NodeTypes.ContentForMarkup> {
389+
/** {% content_for 'contentForType' %} */
390+
contentForType: LiquidString;
391+
/** {% content_for 'contentForType', arg1: value1, arg2: value2 %} */
392+
args: LiquidNamedArgument[];
393+
}
394+
380395
/** {% render 'snippet' [(with|for) variable [as alias]], [...namedArguments] %} */
381396
export interface RenderMarkup extends ASTNode<NodeTypes.RenderMarkup> {
382397
/** {% render snippet %} */
@@ -1408,6 +1423,14 @@ function toNamedLiquidTag(
14081423
};
14091424
}
14101425

1426+
case NamedTags.content_for: {
1427+
return {
1428+
...liquidTagBaseAttributes(node),
1429+
name: node.name,
1430+
markup: toContentForMarkup(node.markup),
1431+
};
1432+
}
1433+
14111434
case NamedTags.include:
14121435
case NamedTags.render: {
14131436
return {
@@ -1684,6 +1707,16 @@ function toRawMarkupKindFromLiquidNode(node: ConcreteLiquidRawTag): RawMarkupKin
16841707
}
16851708
}
16861709

1710+
function toContentForMarkup(node: ConcreteLiquidTagContentForMarkup): ContentForMarkup {
1711+
return {
1712+
type: NodeTypes.ContentForMarkup,
1713+
contentForType: toExpression(node.contentForType) as LiquidString,
1714+
args: node.args.map(toNamedArgument),
1715+
position: position(node),
1716+
source: node.source,
1717+
};
1718+
}
1719+
16871720
function toRenderMarkup(node: ConcreteLiquidTagRenderMarkup): RenderMarkup {
16881721
return {
16891722
type: NodeTypes.RenderMarkup,

‎packages/liquid-html-parser/src/types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export enum NodeTypes {
3737
LogicalExpression = 'LogicalExpression',
3838

3939
AssignMarkup = 'AssignMarkup',
40+
ContentForMarkup = 'ContentForMarkup',
4041
CycleMarkup = 'CycleMarkup',
4142
ForMarkup = 'ForMarkup',
4243
PaginateMarkup = 'PaginateMarkup',
@@ -50,6 +51,7 @@ export enum NamedTags {
5051
assign = 'assign',
5152
capture = 'capture',
5253
case = 'case',
54+
content_for = 'content_for',
5355
cycle = 'cycle',
5456
decrement = 'decrement',
5557
echo = 'echo',

‎packages/prettier-plugin-liquid/src/printer/preprocess/augment-with-css-properties.ts

+2
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ function getCssDisplay(node: AugmentedNode<WithSiblings>, options: LiquidParserO
121121
case NodeTypes.VariableLookup:
122122
case NodeTypes.AssignMarkup:
123123
case NodeTypes.CycleMarkup:
124+
case NodeTypes.ContentForMarkup:
124125
case NodeTypes.ForMarkup:
125126
case NodeTypes.PaginateMarkup:
126127
case NodeTypes.RenderMarkup:
@@ -225,6 +226,7 @@ function getNodeCssStyleWhiteSpace(
225226
case NodeTypes.VariableLookup:
226227
case NodeTypes.AssignMarkup:
227228
case NodeTypes.CycleMarkup:
229+
case NodeTypes.ContentForMarkup:
228230
case NodeTypes.ForMarkup:
229231
case NodeTypes.PaginateMarkup:
230232
case NodeTypes.RenderMarkup:

‎packages/prettier-plugin-liquid/src/printer/print/liquid.ts

+6
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ function printNamedLiquidBlockStart(
165165
]);
166166
}
167167

168+
case NamedTags.content_for: {
169+
const markup = node.markup;
170+
const trailingWhitespace = markup.args.length > 0 ? line : ' ';
171+
return tag(trailingWhitespace);
172+
}
173+
168174
case NamedTags.include:
169175
case NamedTags.render: {
170176
const markup = node.markup;

‎packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts

+17
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,23 @@ function printNode(
378378
return doc;
379379
}
380380

381+
case NodeTypes.ContentForMarkup: {
382+
const contentForType = path.call((p: any) => print(p), 'contentForType');
383+
const doc: Doc = [contentForType];
384+
if (node.args.length > 0) {
385+
doc.push(
386+
',',
387+
line,
388+
join(
389+
[',', line],
390+
path.map((p) => print(p), 'args'),
391+
),
392+
);
393+
}
394+
395+
return doc;
396+
}
397+
381398
case NodeTypes.RenderMarkup: {
382399
const snippet = path.call((p: any) => print(p), 'snippet');
383400
const doc: Doc = [snippet];
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
It should never break a name
2+
printWidth: 1
3+
{% content_for 'block' %}
4+
{%- content_for 'blocks' -%}
5+
6+
It should break on named args
7+
printWidth: 1
8+
{% content_for 'block',
9+
key1: val1,
10+
key2: (0..1)
11+
%}
12+
13+
It should not require commas (as per syntax) but add them when pretty printed
14+
{% content_for 'block', key1: val1, key2: (0..1) %}
15+
16+
It should not require spaces (as per syntax) but add them when pretty printed
17+
{% content_for 'block', key1: val1, key2: (0..1) %}
18+
19+
It should strip trailing commas by default
20+
{% content_for 'foo' %}
21+
{% content_for 'foo', product: product %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
It should never break a name
2+
printWidth: 1
3+
{% content_for "block" %}
4+
{%- content_for "blocks" -%}
5+
6+
It should break on named args
7+
printWidth: 1
8+
{% content_for "block", key1: val1, key2: (0..1) %}
9+
10+
It should not require commas (as per syntax) but add them when pretty printed
11+
{% content_for "block" key1: val1 key2: (0..1) %}
12+
13+
It should not require spaces (as per syntax) but add them when pretty printed
14+
{%content_for "block",key1:val1,key2:(0..1)%}
15+
16+
It should strip trailing commas by default
17+
{% content_for "foo", %}
18+
{% content_for "foo",product:product, %}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { test } from 'vitest';
2+
import { assertFormattedEqualsFixed } from '../test-helpers';
3+
4+
test('Unit: liquid-tag-content-for', async () => {
5+
await assertFormattedEqualsFixed(__dirname);
6+
});

0 commit comments

Comments
 (0)
Please sign in to comment.