diff --git a/.changeset/dull-jobs-tap.md b/.changeset/dull-jobs-tap.md new file mode 100644 index 000000000..8b7fdbc95 --- /dev/null +++ b/.changeset/dull-jobs-tap.md @@ -0,0 +1,6 @@ +--- +'@shopify/prettier-plugin-liquid': minor +'@shopify/liquid-html-parser': minor +--- + +Added parsing support for the `snippet` tag by updating the ohm rules diff --git a/packages/liquid-html-parser/grammar/liquid-html.ohm b/packages/liquid-html-parser/grammar/liquid-html.ohm index c60759c29..933f914c3 100644 --- a/packages/liquid-html-parser/grammar/liquid-html.ohm +++ b/packages/liquid-html-parser/grammar/liquid-html.ohm @@ -78,6 +78,7 @@ Liquid <: Helpers { | liquidTagOpenIf | liquidTagOpenPaginate | liquidTagOpenUnless + | liquidTagOpenSnippet liquidTagOpen = | liquidTagOpenStrict @@ -105,6 +106,7 @@ Liquid <: Helpers { liquidTagIncrement = liquidTagRule<"increment", variableSegmentAsLookupMarkup> liquidTagDecrement = liquidTagRule<"decrement", variableSegmentAsLookupMarkup> liquidTagOpenCapture = liquidTagOpenRule<"capture", variableSegmentAsLookupMarkup> + liquidTagOpenSnippet = liquidTagOpenRule<"snippet", variableSegmentAsLookupMarkup> variableSegmentAsLookupMarkup = variableSegmentAsLookup space* liquidTagSection = liquidTagRule<"section", liquidTagSectionMarkup> @@ -322,6 +324,7 @@ Liquid <: Helpers { | "if" | "unless" | "tablerow" + | "snippet" ) endOfIdentifier delimTag = "-%}" | "%}" diff --git a/packages/liquid-html-parser/src/stage-1-cst.spec.ts b/packages/liquid-html-parser/src/stage-1-cst.spec.ts index c5b20f1e8..8a47dda2c 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts @@ -794,6 +794,17 @@ describe('Unit: Stage 1 (CST)', () => { }); }); + it('should parse snippet arguments as a singular liquid variable lookup', () => { + const expression = `var`; + const type = 'VariableLookup'; + for (const { toCST, expectPath } of testCases) { + cst = toCST(`{% snippet ${expression} -%}`); + expectPath(cst, '0.type').to.equal('LiquidTagOpen'); + expectPath(cst, '0.name').to.equal('snippet'); + expectPath(cst, '0.markup.type').to.equal(type); + } + }); + it('should parse when arguments as an array of liquid expressions', () => { [ { expression: `"string"`, args: [{ type: 'String' }] }, diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index f28bd0891..dafdc6cd8 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -237,6 +237,7 @@ export type ConcreteLiquidTagOpen = ConcreteLiquidTagOpenBaseCase | ConcreteLiqu export type ConcreteLiquidTagOpenNamed = | ConcreteLiquidTagOpenCase | ConcreteLiquidTagOpenCapture + | ConcreteLiquidTagOpenSnippet | ConcreteLiquidTagOpenIf | ConcreteLiquidTagOpenUnless | ConcreteLiquidTagOpenForm @@ -254,6 +255,8 @@ export interface ConcreteLiquidTagOpenBaseCase extends ConcreteLiquidTagOpenNode export interface ConcreteLiquidTagOpenCapture extends ConcreteLiquidTagOpenNode {} +export interface ConcreteLiquidTagOpenSnippet + extends ConcreteLiquidTagOpenNode {} export interface ConcreteLiquidTagOpenCase extends ConcreteLiquidTagOpenNode {} @@ -746,6 +749,7 @@ function toCST( }, liquidTagOpenCapture: 0, + liquidTagOpenSnippet: 0, liquidTagOpenForm: 0, liquidTagOpenFormMarkup: 0, liquidTagOpenFor: 0, diff --git a/packages/liquid-html-parser/src/stage-2-ast.spec.ts b/packages/liquid-html-parser/src/stage-2-ast.spec.ts index 91b2188a9..7fb711d3a 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.spec.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.spec.ts @@ -637,6 +637,25 @@ describe('Unit: Stage 2 (AST)', () => { }); }); + it('should parse snippet blocks', () => { + for (const { toAST, expectPath, expectPosition } of testCases) { + ast = toAST(`{% snippet hello_snippet %}{% echo "Hello content" %}{% endsnippet %}`); + expectPath(ast, 'children.0').to.exist; + expectPath(ast, 'children.0.type').to.eql('LiquidTag'); + expectPath(ast, 'children.0.name').to.eql('snippet'); + expectPath(ast, 'children.0.markup.type').to.eql('VariableLookup'); + expectPath(ast, 'children.0.markup.name').to.eql('hello_snippet'); + + expectPath(ast, 'children.0.children.0.type').to.eql('LiquidTag'); + expectPath(ast, 'children.0.children.0.name').to.eql('echo'); + expectPath(ast, 'children.0.children.0.markup.type').to.eql('LiquidVariable'); + expectPath(ast, 'children.0.children.0.markup.expression.value').to.eql('Hello content'); + + expectPosition(ast, 'children.0'); + expectPosition(ast, 'children.0.markup'); + } + }); + describe('Case: content_for', () => { it('should parse content_for tags with no arguments', () => { for (const { toAST, expectPath, expectPosition } of testCases) { @@ -1230,6 +1249,25 @@ describe('Unit: Stage 2 (AST)', () => { expectPosition(ast, 'children.0.body.nodes.2').toEqual('}'); expectPosition(ast, 'children.0'); }); + + it('should parse snippet blocks with HTML content', () => { + ast = toLiquidHtmlAST( + `{% snippet hello_snippet %}

Hello

{% endsnippet %}`, + ); + expectPath(ast, 'children.0.type').to.eql('LiquidTag'); + expectPath(ast, 'children.0.name').to.eql('snippet'); + expectPath(ast, 'children.0.markup.type').to.eql('VariableLookup'); + expectPath(ast, 'children.0.markup.name').to.eql('hello_snippet'); + expectPath(ast, 'children.0.children.0.type').to.eql('HtmlElement'); + expectPath(ast, 'children.0.children.0.name.0.value').to.eql('div'); + expectPath(ast, 'children.0.children.0.attributes.0.name.0.value').to.eql('class'); + expectPath(ast, 'children.0.children.0.attributes.0.value.0.value').to.eql('component'); + expectPath(ast, 'children.0.children.0.children.0.type').to.eql('HtmlElement'); + expectPath(ast, 'children.0.children.0.children.0.name.0.value').to.eql('p'); + expectPath(ast, 'children.0.children.0.children.0.children.0.type').to.eql('TextNode'); + expectPath(ast, 'children.0.children.0.children.0.children.0.value').to.eql('Hello'); + expectPosition(ast, 'children.0'); + }); }); describe('Unit: toLiquidAST(text)', () => { diff --git a/packages/liquid-html-parser/src/stage-2-ast.ts b/packages/liquid-html-parser/src/stage-2-ast.ts index e05c298a0..845c42846 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.ts @@ -212,6 +212,7 @@ export type LiquidTagNamed = | LiquidTagRender | LiquidTagSection | LiquidTagSections + | LiquidTagSnippet | LiquidTagTablerow | LiquidTagUnless; @@ -278,6 +279,9 @@ export interface LiquidTagDecrement /** https://shopify.dev/docs/api/liquid/tags#capture */ export interface LiquidTagCapture extends LiquidTagNode {} +/** https://shopify.dev/docs/api/liquid/tags#snippet */ +export interface LiquidTagSnippet extends LiquidTagNode {} + /** https://shopify.dev/docs/api/liquid/tags#cycle */ export interface LiquidTagCycle extends LiquidTagNode {} @@ -1544,6 +1548,15 @@ function toNamedLiquidTag( }; } + case NamedTags.snippet: { + return { + ...liquidTagBaseAttributes(node), + name: node.name, + markup: toExpression(node.markup) as LiquidVariableLookup, + children: [], + }; + } + case NamedTags.content_for: { return { ...liquidTagBaseAttributes(node), diff --git a/packages/liquid-html-parser/src/types.ts b/packages/liquid-html-parser/src/types.ts index 961a3dcd5..9e68bd276 100644 --- a/packages/liquid-html-parser/src/types.ts +++ b/packages/liquid-html-parser/src/types.ts @@ -70,6 +70,7 @@ export enum NamedTags { layout = 'layout', liquid = 'liquid', paginate = 'paginate', + snippet = 'snippet', render = 'render', section = 'section', sections = 'sections', diff --git a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts index 7e8628311..b6120559c 100644 --- a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts +++ b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts @@ -189,6 +189,7 @@ function printNamedLiquidBlockStart( } case NamedTags.capture: + case NamedTags.snippet: case NamedTags.increment: case NamedTags.decrement: case NamedTags.layout: diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts index 566b273c8..f4f858e96 100644 --- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts +++ b/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts @@ -86,7 +86,7 @@ describe('Module: LiquidHTMLSyntaxError', () => { const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode); expect(offenses).to.have.length(1); expect(offenses[0].message).to.equal( - `SyntaxError: expected "#", a letter, "when", "sections", "section", "render", "liquid", "layout", "increment", "include", "elsif", "else", "echo", "decrement", "content_for", "cycle", "continue", "break", "assign", "tablerow", "unless", "if", "ifchanged", "for", "case", "capture", "paginate", "form", "end", "style", "stylesheet", "schema", "javascript", "raw", "comment", or "doc"`, + `SyntaxError: expected "#", a letter, "when", "sections", "section", "render", "liquid", "layout", "increment", "include", "elsif", "else", "echo", "decrement", "content_for", "cycle", "continue", "break", "assign", "snippet", "tablerow", "unless", "if", "ifchanged", "for", "case", "capture", "paginate", "form", "end", "style", "stylesheet", "schema", "javascript", "raw", "comment", or "doc"`, ); });