diff --git a/packages/liquid-html-parser/package.json b/packages/liquid-html-parser/package.json index 989e8a6f2..578d2af93 100644 --- a/packages/liquid-html-parser/package.json +++ b/packages/liquid-html-parser/package.json @@ -35,6 +35,7 @@ }, "devDependencies": { "@types/line-column": "^1.0.0", - "@types/node": "^22.18.8" + "@types/node": "^22.18.8", + "yaml": "^2.8.3" } } diff --git a/packages/liquid-html-parser/specs/architecture.md b/packages/liquid-html-parser/specs/architecture.md index 4dfafd726..428c1d92e 100644 --- a/packages/liquid-html-parser/specs/architecture.md +++ b/packages/liquid-html-parser/specs/architecture.md @@ -41,7 +41,7 @@ src/ liquid-raw.ts # Raw tag body parsing, Liquid-in-range parsing liquid-tags.ts # Tag dispatch: environment lookup, MarkupParser creation, tolerant fallback liquid-variable-output.ts # {{ }} variable output parsing - test-helpers.ts # expectPath, expectPosition, expectBlockStartPosition test utilities + test-helpers.ts # expectPath, expectPosition test utilities # Markup domain -- Liquid expression grammar markup/ @@ -158,7 +158,7 @@ The parser calls these utilities while building the AST. | `document/liquid-raw.ts` | Raw tag body extraction (scan forward for end tag). `parseLiquidInRange` for tags that parse Liquid inside their body. Defines `RawParserDelegate` interface. | Expression parsing. Tag dispatch. | | `document/liquid-tags.ts` | Tag dispatch: environment lookup, `MarkupParser` creation from envelope, tolerant-mode try/catch fallback to base case. Defines `TagParserDelegate` interface. | Block body parsing. HTML parsing. | | `document/liquid-variable-output.ts` | `{{ }}` variable output parsing. | Everything else. | -| `document/test-helpers.ts` | Test utilities: `expectPath`, `expectPosition`, `expectBlockStartPosition`, `expectBlockEndPosition`, `sourceAt`. | Production code. | +| `document/test-helpers.ts` | Test utilities: `expectPath`, `expectPosition`. | Production code. | | `shared.ts` | `envelopeFromLine()`: constructs `LiquidTagEnvelope` from a `{% liquid %}` line. Shared so both `document/parser.ts` and `tags/liquid.ts` can import it. | Everything else. | | `tag-definitions.ts` | `TagKind` enum, `TagDefinition` discriminated union, `Parser` interface, `BranchName` type, `LiquidLine` and `LiquidLineContext` types. | Parsing logic. Environment management. | | `environment.ts` | `Environment` class (tag registry, `tagForName`, `registerTag`, `Environment.default()` singleton). Re-exports all types from `tag-definitions.ts`. | Parsing logic. | diff --git a/packages/liquid-html-parser/specs/recursive-descent.md b/packages/liquid-html-parser/specs/recursive-descent.md index aad52cf90..7265b55f9 100644 --- a/packages/liquid-html-parser/specs/recursive-descent.md +++ b/packages/liquid-html-parser/specs/recursive-descent.md @@ -2,7 +2,7 @@ ## Public API -`toLiquidHtmlAST(source, options)` and `toLiquidAST(source, options)` parse Liquid (and optionally HTML) source into a typed AST. All oracle tests pass. `ast.test.ts` unit tests pass. +`toLiquidHtmlAST(source, options)` and `toLiquidAST(source, options)` parse Liquid (and optionally HTML) source into a typed AST. All oracle tests and parser specs pass. Performance is not a constraint. Correctness first. @@ -481,19 +481,23 @@ For each `.liquid` file in `fixtures/theme/{base-theme,dawn,horizon}/`: The golden files represent the ground truth AST output for each fixture. Fixtures are generated locally per `fixtures/README.md`. -### Unit tests (`ast.test.ts`) +### Parser specs (`specs/parser` at the repo root) -Fine-grained tests that assert specific properties at specific AST paths using `deepGet`. Tests cover: +Language-neutral specs: each case is a template and the complete tree, or the error, that +`toLiquidHtmlAST` or `toLiquidAST` gives for it. `src/specs.test.ts` runs them, and +`specs/parser/README.md` describes the format. They cover: - All expression types (strings, numbers, literals, ranges, lookups, comparisons, logical ops) - All named tags (assign, echo, render, for, if, case, cycle, paginate, etc.) - HTML elements, attributes, void elements, raw tags - Error cases (unclosed nodes, mismatched tags, etc.) -- Completion mode (dangling nodes, placeholder character) - Branch position tracking - Whitespace trim markers -These tests import from `./ast`. +### Unit tests + +The other tests, like `ast.test.ts` and `document/*.test.ts`, cover what a spec can't state: the +tokenizers, the node factories, and parses with options, like completion mode. --- @@ -510,6 +514,6 @@ These tests import from `./ast`. ```bash cd packages/liquid-html-parser pnpm vitest run src/parser-oracle.test.ts # oracle golden tests -pnpm vitest run src/ast.test.ts # unit tests +pnpm vitest run src/specs.test.ts # parser specs pnpm vitest run # all tests ``` diff --git a/packages/liquid-html-parser/src/ast.test.ts b/packages/liquid-html-parser/src/ast.test.ts index 5909e1ade..1aac72c37 100644 --- a/packages/liquid-html-parser/src/ast.test.ts +++ b/packages/liquid-html-parser/src/ast.test.ts @@ -1,2252 +1,11 @@ import { expect, it, describe } from 'vitest'; -import { toLiquidHtmlAST, toLiquidAST, RawMarkupKinds } from './ast'; -import type { LiquidHtmlNode, DocumentNode } from './ast'; +import { toLiquidHtmlAST } from './ast'; +import type { LiquidHtmlNode } from './ast'; import { deepGet } from './utils'; describe('Unit: Stage 2 (AST)', () => { let ast: any; - describe('Unit: toLiquidHtmlAST(text) and toLiquidAST(text)', () => { - const testCases = [ - { - expectPath: makeExpectPath('toLiquidHtmlAST(text)'), - expectPosition: makeExpectPosition('toLiquidHtmlAST(text)'), - toAST: toLiquidHtmlAST, - }, - { - expectPath: makeExpectPath('toLiquidAST(text)'), - expectPosition: makeExpectPosition('toLiquidAST(text)'), - toAST: toLiquidAST, - }, - ]; - - describe('Unit: LiquidVariableOutput', () => { - it('should transform a base case Liquid Drop into a LiquidVariableOutput', () => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST('{{ !-asd }}'); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup').to.eql('!-asd'); - expectPosition(ast, 'children.0'); - } - }); - - it('should set LiquidVariable end position to the markup boundary (Bug 7 regression)', () => { - for (const { toAST, expectPath } of testCases) { - // For '{{ product.title }}': - // {{ = positions 0..1, openToken.end = 2 - // ' product.title ' = positions 2..17 (rawMarkup) - // }} = positions 17..18, closeToken.start = 17 - // LiquidVariable.start = 3 (first char of 'product') - // LiquidVariable.end = 17 (closeToken.start, the markup boundary) - ast = toAST('{{ product.title }}'); - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.position.start').to.eql(3); - expectPath(ast, 'children.0.markup.position.end').to.eql(17); - expectPath(ast, 'children.0.markup.rawSource').to.eql('product.title'); - - // With filters: end should still be the markup boundary - ast = toAST('{{ price | money }}'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.position.start').to.eql(3); - expectPath(ast, 'children.0.markup.position.end').to.eql(17); - } - }); - - it('should set rawSource for LiquidVariable inside {% liquid %} blocks (Bug 21 fix)', () => { - for (const { toAST, expectPath } of testCases) { - // echo inside {% liquid %} should have correct rawSource - ast = toAST('{% liquid\n echo product.title\n%}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('liquid'); - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('echo'); - expectPath(ast, 'children.0.markup.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.0.markup.rawSource').to.eql('product.title'); - - // echo with filter inside {% liquid %} should have correct rawSource - ast = toAST('{% liquid\n echo product.title | upcase\n%}'); - expectPath(ast, 'children.0.markup.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.0.markup.rawSource').to.eql('product.title | upcase'); - - // Standalone {{ }} drops should still have rawSource populated - ast = toAST('{{ product.title }}'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql('product.title'); - - // Standalone {% echo %} outside liquid block should still have rawSource populated - ast = toAST('{% echo product.title %}'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql('product.title'); - } - }); - - it('should populate rawSource for LiquidVariable inside raw tag bodies (Bug 28)', () => { - for (const { toAST, expectPath } of testCases) { - // {{ }} inside {% style %} should have correct rawSource - ast = toAST('{% style %}{{ product.title }}{% endstyle %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('style'); - expectPath(ast, 'children.0.body.type').to.eql('RawMarkup'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.body.nodes.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.body.nodes.0.markup.rawSource').to.eql('product.title'); - - // {{ }} inside {% javascript %} should have correct rawSource - ast = toAST('{% javascript %}{{ settings.color }}{% endjavascript %}'); - expectPath(ast, 'children.0.body.nodes.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.body.nodes.0.markup.rawSource').to.eql('settings.color'); - - // Standalone {{ }} drops should still have rawSource populated - ast = toAST('{{ product.title }}'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql('product.title'); - - // {% echo %} inside {% style %} should have correct rawSource - ast = toAST('{% style %}{% echo product.title %}{% endstyle %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.body.nodes.0.name').to.eql('echo'); - expectPath(ast, 'children.0.body.nodes.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.body.nodes.0.markup.rawSource').to.eql('product.title'); - - // {% assign %} inside {% style %} should have correct rawSource on the value - ast = toAST('{% style %}{% assign x = product.title %}{% endstyle %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.body.nodes.0.name').to.eql('assign'); - expectPath(ast, 'children.0.body.nodes.0.markup.type').to.eql('AssignMarkup'); - expectPath(ast, 'children.0.body.nodes.0.markup.value.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.body.nodes.0.markup.value.rawSource').to.eql('product.title'); - - // Standalone {% echo %} should still have rawSource populated - ast = toAST('{% echo product.title %}'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql('product.title'); - } - }); - - it('should populate rawSource for {{ }} drops inside HTML '); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.name').to.eql('style'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.body.nodes.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.body.nodes.0.markup.rawSource').to.eql('section.id'); - - // With filters - ast = toLiquidHtmlAST( - "", - ); - expectPath(ast, 'children.0.body.nodes.0.markup.rawSource').to.eql( - "settings.type_body_font | font_face: font_display: 'swap'", - ); - }); - - it('should not truncate rawSource with filters inside {% liquid %} blocks (Bug 21 regression)', () => { - for (const { toAST, expectPath } of testCases) { - // The previous (Ohm) parser had a bug where rawSource inside {% liquid %} - // blocks was truncated because locStart applied the block offset but - // endIdx did not. This caused values like "'home.hero.title' | t" to - // become "'home.he" (truncated by the offset difference). - ast = toAST("{% liquid\n echo 'home.hero.title' | t\n%}"); - expectPath(ast, 'children.0.markup.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.0.markup.rawSource').to.eql("'home.hero.title' | t"); - - // Multiple lines inside liquid block - ast = toAST("{% liquid\n assign x = 'foo'\n echo x | upcase | append: '.bar'\n%}"); - expectPath(ast, 'children.0.markup.1.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.1.markup.rawSource').to.eql( - "x | upcase | append: '.bar'", - ); - } - }); - - it('should parse multiline render inside {% liquid %} as a single statement (Bug 44)', () => { - for (const { toAST, expectPath } of testCases) { - ast = toAST("{% liquid\n render 'foo',\n bar: baz,\n qux: quux\n%}"); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('liquid'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.name').to.eql('render'); - } - }); - - it('should set LogicalExpression position.end to the markup boundary (Bug 9 regression)', () => { - for (const { toAST, expectPath } of testCases) { - // For '{% if a and b %}': - // {% = positions 0..1, openToken.end = 2 - // ' if a and b ' = positions 2..14 - // %} = positions 14..15, closeToken.start = 14 - // 'a' at 6, 'b' at 12 (end=13). eosStart = 14 (closeToken.start). - // LogicalExpression.end extends to eosStart (14), matching original parser. - ast = toAST('{% if a and b %}{% endif %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('if'); - expectPath(ast, 'children.0.markup.type').to.eql('LogicalExpression'); - expectPath(ast, 'children.0.markup.position.start').to.eql(6); - expectPath(ast, 'children.0.markup.position.end').to.eql(14); - - // Comparison (without logical) keeps tight position at the last token's end. - // 'x' at 6, '1' at 11 (end=12). Comparison.end = 12, not eosStart(13). - ast = toAST('{% if x == 1 %}{% endif %}'); - expectPath(ast, 'children.0.markup.type').to.eql('Comparison'); - expectPath(ast, 'children.0.markup.position.start').to.eql(6); - expectPath(ast, 'children.0.markup.position.end').to.eql(12); - } - }); - - it('should set nested LogicalExpression right.position.start to include operator keyword (Bug 19 regression)', () => { - for (const { toAST, expectPath } of testCases) { - // For '{% if a == 1 and b == 2 or c == 3 %}': - // 'a' at 6, '1' at 11 (end 12) - // 'and' at 13 - // 'b' at 17, '2' at 22 (end 23) - // 'or' at 24 - // 'c' at 27, '3' at 32 (end 33) - // - // Structure: LogicalExpression(left=Comparison(a==1), and, right=LogicalExpression(left=Comparison(b==2), or, right=Comparison(c==3))) - // The nested LogicalExpression (right) should have position.start at 'and' (13), not 'b' (17). - // The innermost right (Comparison c==3) should NOT include 'or' — only LogicalExpression children get the operator. - ast = toAST('{% if a == 1 and b == 2 or c == 3 %}{% endif %}'); - expectPath(ast, 'children.0.markup.type').to.eql('LogicalExpression'); - // Outer LogicalExpression starts at 'a' (6) - expectPath(ast, 'children.0.markup.position.start').to.eql(6); - // right is a nested LogicalExpression; its start includes the 'and' keyword - expectPath(ast, 'children.0.markup.right.type').to.eql('LogicalExpression'); - expectPath(ast, 'children.0.markup.right.position.start').to.eql(13); - // The nested LogicalExpression's left starts at 'b' (17), not at the operator - expectPath(ast, 'children.0.markup.right.left.type').to.eql('Comparison'); - expectPath(ast, 'children.0.markup.right.left.position.start').to.eql(17); - // The nested LogicalExpression's right is a Comparison, starts at 'c' (27) - expectPath(ast, 'children.0.markup.right.right.type').to.eql('Comparison'); - expectPath(ast, 'children.0.markup.right.right.position.start').to.eql(27); - - // Simple two-operand case: right is not a LogicalExpression, so no operator included - // '{% if a and b %}' — right is VariableLookup, starts at 'b' (12) - ast = toAST('{% if a and b %}{% endif %}'); - expectPath(ast, 'children.0.markup.type').to.eql('LogicalExpression'); - expectPath(ast, 'children.0.markup.right.type').to.eql('VariableLookup'); - expectPath(ast, 'children.0.markup.right.position.start').to.eql(12); - - // Nested LogicalExpressions extend position.end to eosStart (markup boundary) - ast = toAST('{% if a == 1 and b == 2 or c == 3 %}{% endif %}'); - const eosStart = 34; // position of '%}' close token start (space before %}) - expectPath(ast, 'children.0.markup.position.end').to.eql(eosStart); - expectPath(ast, 'children.0.markup.right.position.end').to.eql(eosStart); - } - }); - - it('should parse comparisons as LiquidVariable > BooleanExpression > Comparison', () => { - [ - { expression: `1 == 1` }, - { expression: `1 != 1` }, - { expression: `1 > 1` }, - { expression: `1 < 1` }, - { expression: `1 >= 1` }, - { expression: `1 <= 1` }, - ].forEach(({ expression }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{{ ${expression} }}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql(expression); - expectPath(ast, 'children.0.markup.expression.type').to.eql('BooleanExpression'); - expectPath(ast, 'children.0.markup.expression.condition.type').to.eql('Comparison'); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - expectPosition(ast, 'children.0.markup.expression'); - } - }); - }); - - it('should parse logical operations as LiquidVariable > BooleanExpression > LogicalExpression', () => { - [ - { expression: `1 == 1 and 2 == 2` }, - { expression: `1 == 1 or 2 == 2` }, - { expression: `1 == 1 and 2 == 2 or 3 == 3` }, - { expression: `1 == 1 and some_variable or 3 == 3` }, - { expression: `some_var and 'raw string'` }, - ].forEach(({ expression }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{{ ${expression} }}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql(expression); - expectPath(ast, 'children.0.markup.expression.type').to.eql('BooleanExpression'); - expectPath(ast, 'children.0.markup.expression.condition.type').to.eql( - 'LogicalExpression', - ); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - expectPosition(ast, 'children.0.markup.expression'); - } - }); - }); - - it('should parse strings as LiquidVariable > String', () => { - [ - { expression: `"string o' string"`, value: `string o' string`, single: false }, - { expression: `'He said: "hi!"'`, value: `He said: "hi!"`, single: true }, - ].forEach(({ expression, value, single }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{{ ${expression} }}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql(expression); - expectPath(ast, 'children.0.markup.expression.type').to.eql('String'); - expectPath(ast, 'children.0.markup.expression.value').to.eql(value); - expectPath(ast, 'children.0.markup.expression.single').to.eql(single); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - expectPosition(ast, 'children.0.markup.expression'); - } - }); - }); - - it('should parse numbers as LiquidVariable > Number', () => { - [ - { expression: `1`, value: '1' }, - { expression: `1.02`, value: '1.02' }, - { expression: `0`, value: '0' }, - { expression: `-0`, value: '-0' }, - { expression: `-0.0`, value: '-0.0' }, - ].forEach(({ expression, value }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{{ ${expression} }}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql(expression); - expectPath(ast, 'children.0.markup.expression.type').to.eql('Number'); - expectPath(ast, 'children.0.markup.expression.value').to.eql(value); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - expectPosition(ast, 'children.0.markup.expression'); - } - }); - }); - - it('should parse numbers as LiquidVariable > LiquidLiteral', () => { - [ - { expression: `nil`, value: null }, - { expression: `null`, value: null }, - { expression: `true`, value: true }, - { expression: `blank`, value: '' }, - { expression: `empty`, value: '' }, - ].forEach(({ expression, value }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{{ ${expression} }}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql(expression); - expectPath(ast, 'children.0.markup.expression.type').to.eql('LiquidLiteral'); - expectPath(ast, 'children.0.markup.expression.keyword').to.eql(expression); - expectPath(ast, 'children.0.markup.expression.value').to.eql(value); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - expectPosition(ast, 'children.0.markup.expression'); - } - }); - }); - - it('should parse ranges as LiquidVariable > Range', () => { - [ - { - expression: `(0..5)`, - start: { value: '0', type: 'Number' }, - end: { value: '5', type: 'Number' }, - }, - { - expression: `( 0 .. 5 )`, - start: { value: '0', type: 'Number' }, - end: { value: '5', type: 'Number' }, - }, - { - expression: `(true..false)`, - start: { value: true, type: 'LiquidLiteral' }, - end: { value: false, type: 'LiquidLiteral' }, - }, - ].forEach(({ expression, start, end }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{{ ${expression} }}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql(expression); - expectPath(ast, 'children.0.markup.expression.type').to.eql('Range'); - expectPath(ast, 'children.0.markup.expression.start.type').to.eql(start.type); - expectPath(ast, 'children.0.markup.expression.start.value').to.eql(start.value); - expectPath(ast, 'children.0.markup.expression.end.type').to.eql(end.type); - expectPath(ast, 'children.0.markup.expression.end.value').to.eql(end.value); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - expectPosition(ast, 'children.0.markup.expression'); - expectPosition(ast, 'children.0.markup.expression.start'); - expectPosition(ast, 'children.0.markup.expression.end'); - } - }); - }); - - interface Lookup { - type: 'VariableLookup'; - lookups: (string | number | Lookup)[]; - name: string | undefined; - } - - it('should parse variable lookups as LiquidVariable > VariableLookup', () => { - const v = (name: string, lookups: (string | number | Lookup)[] = []): Lookup => ({ - type: 'VariableLookup', - name, - lookups, - }); - [ - { expression: `x`, name: 'x', lookups: [] }, - { expression: `x.y`, name: 'x', lookups: ['y'] }, - { expression: `x["y"]`, name: 'x', lookups: ['y'] }, - { expression: `x['y']`, name: 'x', lookups: ['y'] }, - { expression: `x[1]`, name: 'x', lookups: [1] }, - { expression: `x.y.z`, name: 'x', lookups: ['y', 'z'] }, - { expression: `x["y"]["z"]`, name: 'x', lookups: ['y', 'z'] }, - { expression: `x["y"].z`, name: 'x', lookups: ['y', 'z'] }, - { expression: `["product"]`, name: null, lookups: ['product'] }, - { expression: `page.about-us`, name: 'page', lookups: ['about-us'] }, - { expression: `["x"].y`, name: null, lookups: ['x', 'y'] }, - { expression: `["x"]["y"]`, name: null, lookups: ['x', 'y'] }, - { expression: `x[y]`, name: 'x', lookups: [v('y')] }, - { expression: `x[y.z]`, name: 'x', lookups: [v('y', ['z'])] }, - ].forEach(({ expression, name, lookups }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{{ ${expression} }}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.eql(expression); - expectPath(ast, 'children.0.markup.expression.type').to.eql('VariableLookup'); - expectPath(ast, 'children.0.markup.expression.name').to.eql(name); - - lookups.forEach((lookup: string | number | Lookup, i: number) => { - switch (typeof lookup) { - case 'string': { - expectPath(ast, `children.0.markup.expression.lookups.${i}.type`).to.equal( - 'String', - ); - expectPath(ast, `children.0.markup.expression.lookups.${i}.value`).to.equal( - lookup, - ); - break; - } - case 'number': { - expectPath(ast, `children.0.markup.expression.lookups.${i}.type`).to.equal( - 'Number', - ); - expectPath(ast, `children.0.markup.expression.lookups.${i}.value`).to.equal( - lookup.toString(), - ); - break; - } - default: { - expectPath(ast, `children.0.markup.expression.lookups.${i}.type`).to.equal( - 'VariableLookup', - ); - expectPath(ast, `children.0.markup.expression.lookups.${i}.name`).to.equal( - lookup.name, - ); - lookup.lookups.forEach((val, j) => { - // Being lazy here... Assuming string properties. - expectPath( - ast, - `children.0.markup.expression.lookups.${i}.lookups.${j}.type`, - ).to.equal('String'); - expectPath( - ast, - `children.0.markup.expression.lookups.${i}.lookups.${j}.value`, - ).to.equal(val); - }); - break; - } - } - }); - - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - expectPosition(ast, 'children.0.markup.expression'); - } - }); - }); - - it('should parse filters', () => { - interface Filter { - name: string; - args: FilterArgument[]; - } - type FilterArgument = any; - - const filter = (name: string, args: FilterArgument[] = []): Filter => ({ name, args }); - const arg = (type: string, value: string) => ({ type, value }); - const namedArg = (name: string, valueType: string) => ({ - type: 'NamedArgument', - name, - valueType, - }); - [ - { expression: `| filter1`, filters: [filter('filter1')] }, - { expression: `| filter1 | filter2`, filters: [filter('filter1'), filter('filter2')] }, - { - expression: `| filter1: 'hi', 'there'`, - filters: [filter('filter1', [arg('String', 'hi'), arg('String', 'there')])], - }, - { - expression: `| filter1: key: value, kind: 'string'`, - filters: [ - filter('filter1', [namedArg('key', 'VariableLookup'), namedArg('kind', 'String')]), - ], - }, - { - expression: `| f1: 'hi', key: (0..1) | f2: key: value, kind: 'string'`, - filters: [ - filter('f1', [arg('String', 'hi'), namedArg('key', 'Range')]), - filter('f2', [namedArg('key', 'VariableLookup'), namedArg('kind', 'String')]), - ], - }, - ].forEach(({ expression, filters }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{{ 'hello' ${expression} }}`); - expectPath(ast, 'children.0.type').to.equal('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup.type').to.equal('LiquidVariable'); - expectPath(ast, 'children.0.markup.rawSource').to.equal(`'hello' ` + expression); - expectPath(ast, 'children.0.markup.filters').to.have.lengthOf(filters.length); - filters.forEach((filter, i) => { - expectPath(ast, `children.0.markup.filters.${i}`).to.exist; - expectPath(ast, `children.0.markup.filters.${i}.type`).to.equal( - 'LiquidFilter', - expression, - ); - expectPath(ast, `children.0.markup.filters.${i}.name`).to.equal(filter.name); - expectPath(ast, `children.0.markup.filters.${i}.args`).to.exist; - expectPath(ast, `children.0.markup.filters.${i}.args`).to.have.lengthOf( - filter.args.length, - ); - filter.args.forEach((arg: any, j) => { - expectPath(ast, `children.0.markup.filters.${i}.args`).to.exist; - switch (arg.type) { - case 'String': { - expectPath(ast, `children.0.markup.filters.${i}.args.${j}.type`).to.equal( - 'String', - ); - expectPath(ast, `children.0.markup.filters.${i}.args.${j}.value`).to.equal( - arg.value, - ); - break; - } - case 'NamedArgument': { - expectPath(ast, `children.0.markup.filters.${i}.args`).to.not.be.empty; - expectPath(ast, `children.0.markup.filters.${i}.args.${j}.type`).to.equal( - 'NamedArgument', - ); - expectPath(ast, `children.0.markup.filters.${i}.args.${j}.name`).to.equal( - arg.name, - ); - expectPath(ast, `children.0.markup.filters.${i}.args.${j}.value.type`).to.equal( - arg.valueType, - ); - break; - } - } - }); - }); - expectPath(ast, 'children.0.whitespaceStart').to.equal(''); - expectPath(ast, 'children.0.whitespaceEnd').to.equal(''); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - expectPosition(ast, 'children.0.markup.expression'); - } - }); - }); - }); - - describe('Case: LiquidTag', () => { - it('should transform a basic Liquid Tag into a LiquidTag', () => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST('{% name %}{% if -%}{%- endif %}'); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('name'); - expectPath(ast, 'children.0.markup').to.eql(''); - expectPath(ast, 'children.0.children').to.be.undefined; - expectPath(ast, 'children.1.whitespaceStart').to.eql(''); - expectPath(ast, 'children.1.whitespaceEnd').to.eql('-'); - expectPath(ast, 'children.1.delimiterWhitespaceStart').to.eql('-'); - expectPath(ast, 'children.1.delimiterWhitespaceEnd').to.eql(''); - expectPosition(ast, 'children.0'); - } - }); - - it('should parse echo tags', () => { - [ - { expression: `"hi"`, expressionType: 'String', expressionValue: 'hi', filters: [] }, - { expression: `x | f`, expressionType: 'VariableLookup', filters: ['f'] }, - ].forEach(({ expression, expressionType, expressionValue, filters }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{% echo ${expression} -%}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('echo'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.expression.type').to.eql(expressionType); - if (expressionValue) - expectPath(ast, 'children.0.markup.expression.value').to.eql(expressionValue); - expectPath(ast, 'children.0.markup.filters').to.have.lengthOf(filters.length); - expectPath(ast, 'children.0.children').to.be.undefined; - expectPath(ast, 'children.0.whitespaceStart').to.eql(''); - expectPath(ast, 'children.0.whitespaceEnd').to.eql('-'); - expectPath(ast, 'children.0.delimiterWhitespaceStart').to.eql(undefined); - expectPath(ast, 'children.0.delimiterWhitespaceEnd').to.eql(undefined); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - expectPosition(ast, 'children.0.markup.expression'); - } - }); - }); - - it('should parse assign tags', () => { - [ - { - expression: `x = "hi"`, - name: 'x', - expressionType: 'String', - expressionValue: 'hi', - filters: [], - }, - { - expression: `z = y | f`, - name: 'z', - expressionType: 'VariableLookup', - filters: ['f'], - }, - ].forEach(({ expression, name, expressionType, expressionValue, filters }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{% assign ${expression} -%}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('assign'); - expectPath(ast, 'children.0.markup.type').to.eql('AssignMarkup'); - expectPath(ast, 'children.0.markup.name').to.eql(name); - expectPath(ast, 'children.0.markup.value.expression.type').to.eql(expressionType); - if (expressionValue) - expectPath(ast, 'children.0.markup.value.expression.value').to.eql(expressionValue); - expectPath(ast, 'children.0.markup.value.filters').to.have.lengthOf(filters.length); - expectPath(ast, 'children.0.children').to.be.undefined; - expectPath(ast, 'children.0.whitespaceStart').to.eql(''); - expectPath(ast, 'children.0.whitespaceEnd').to.eql('-'); - expectPath(ast, 'children.0.delimiterWhitespaceStart').to.eql(undefined); - expectPath(ast, 'children.0.delimiterWhitespaceEnd').to.eql(undefined); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - expectPosition(ast, 'children.0.markup.value'); - expectPosition(ast, 'children.0.markup.value.expression'); - } - }); - }); - - it('should parse render tags', () => { - [ - { - expression: `"snippet"`, - snippetType: 'String', - alias: null, - renderVariableExpression: null, - namedArguments: [], - }, - { - expression: `"snippet" as foo`, - snippetType: 'String', - alias: { - value: 'foo', - }, - renderVariableExpression: null, - namedArguments: [], - }, - { - expression: `"snippet" with "string" as foo`, - snippetType: 'String', - alias: { - value: 'foo', - }, - renderVariableExpression: { - kind: 'with', - name: { - type: 'String', - }, - }, - namedArguments: [], - }, - { - expression: `"snippet" for products as product`, - snippetType: 'String', - alias: { - value: 'product', - }, - renderVariableExpression: { - kind: 'for', - name: { - type: 'VariableLookup', - }, - }, - namedArguments: [], - }, - { - expression: `variable with "string" as foo, key1: val1, key2: "hi"`, - snippetType: 'VariableLookup', - alias: { - value: 'foo', - }, - renderVariableExpression: { - kind: 'with', - name: { - type: 'String', - }, - }, - namedArguments: [ - { name: 'key1', valueType: 'VariableLookup' }, - { name: 'key2', valueType: 'String' }, - ], - }, - ].forEach( - ({ expression, snippetType, renderVariableExpression, alias, namedArguments }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{% render ${expression} -%}`); - expectPath(ast, 'children.0.type').to.equal('LiquidTag'); - expectPath(ast, 'children.0.name').to.equal('render'); - expectPath(ast, 'children.0.markup.type').to.equal('RenderMarkup'); - expectPath(ast, 'children.0.markup.snippet.type').to.equal(snippetType); - if (renderVariableExpression) { - expectPath(ast, 'children.0.markup.variable.type').to.equal( - 'RenderVariableExpression', - ); - expectPath(ast, 'children.0.markup.variable.kind').to.equal( - renderVariableExpression.kind, - ); - expectPath(ast, 'children.0.markup.variable.name.type').to.equal( - renderVariableExpression.name.type, - ); - expectPosition(ast, 'children.0.markup.variable'); - expectPosition(ast, 'children.0.markup.variable.name'); - } else { - expectPath(ast, 'children.0.markup.variable').to.equal(null); - } - expectPath(ast, 'children.0.markup.alias.value').to.equal(alias?.value); - expectPath(ast, 'children.0.markup.args').to.have.lengthOf(namedArguments.length); - namedArguments.forEach(({ name, valueType }, i) => { - expectPath(ast, `children.0.markup.args.${i}.type`).to.equal('NamedArgument'); - expectPath(ast, `children.0.markup.args.${i}.name`).to.equal(name); - expectPath(ast, `children.0.markup.args.${i}.value.type`).to.equal(valueType); - expectPosition(ast, `children.0.markup.args.${i}`); - expectPosition(ast, `children.0.markup.args.${i}.value`); - }); - expectPath(ast, 'children.0.whitespaceStart').to.equal(''); - expectPath(ast, 'children.0.whitespaceEnd').to.equal('-'); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - } - }, - ); - }); - - it('should parse render tags with named args and no comma (Bug 23 regression)', () => { - for (const { toAST, expectPath } of testCases) { - // No comma before named args - ast = toAST(`{% render 'snippet' section: section %}`); - expectPath(ast, 'children.0.type').to.equal('LiquidTag'); - expectPath(ast, 'children.0.name').to.equal('render'); - expectPath(ast, 'children.0.markup.type').to.equal('RenderMarkup'); - expectPath(ast, 'children.0.markup.snippet.type').to.equal('String'); - expectPath(ast, 'children.0.markup.args').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.args.0.name').to.equal('section'); - expectPath(ast, 'children.0.markup.args.0.value.type').to.equal('VariableLookup'); - - // With comma still works - ast = toAST(`{% render 'snippet', section: section %}`); - expectPath(ast, 'children.0.markup.args').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.args.0.name').to.equal('section'); - - // Multiple named args without comma before first - ast = toAST(`{% render 'snippet' key1: val1, key2: val2 %}`); - expectPath(ast, 'children.0.markup.args').to.have.lengthOf(2); - expectPath(ast, 'children.0.markup.args.0.name').to.equal('key1'); - expectPath(ast, 'children.0.markup.args.1.name').to.equal('key2'); - - // With 'for' and no comma before named args - ast = toAST(`{% render 'snippet' for products section: section %}`); - expectPath(ast, 'children.0.markup.variable.kind').to.equal('for'); - expectPath(ast, 'children.0.markup.args').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.args.0.name').to.equal('section'); - } - }); - - it('should parse conditional tags into conditional expressions', () => { - ['if', 'unless'].forEach((tagName) => { - [ - { - expression: 'a', - markup: { - type: 'VariableLookup', - }, - }, - { - expression: 'a and "string"', - markup: { - type: 'LogicalExpression', - relation: 'and', - left: { type: 'VariableLookup' }, - right: { type: 'String' }, - }, - }, - { - expression: 'a and "string" or a<1', - markup: { - type: 'LogicalExpression', - relation: 'and', - left: { type: 'VariableLookup' }, - right: { - type: 'LogicalExpression', - relation: 'or', - left: { type: 'String' }, - right: { - type: 'Comparison', - comparator: '<', - left: { type: 'VariableLookup' }, - right: { type: 'Number' }, - }, - }, - }, - }, - ].forEach(({ expression, markup }) => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{% ${tagName} ${expression} -%}{% end${tagName} %}`); - expectPath(ast, 'children.0.type').to.equal('LiquidTag'); - expectPath(ast, 'children.0.name').to.equal(tagName); - let cursor: any = markup; - let prefix = ''; - while (cursor) { - switch (cursor.type) { - case 'LogicalExpression': { - expectPath(ast, `children.0.markup${prefix}.type`).to.equal(cursor.type); - expectPath(ast, `children.0.markup${prefix}.relation`).to.equal( - cursor.relation, - ); - expectPath(ast, `children.0.markup${prefix}.left.type`).to.equal( - cursor.left.type, - ); - cursor = cursor.right; - prefix = prefix + '.right'; - break; - } - case 'Comparison': { - expectPath(ast, `children.0.markup${prefix}.type`).to.equal(cursor.type); - expectPath(ast, `children.0.markup${prefix}.comparator`).to.equal( - cursor.comparator, - ); - expectPath(ast, `children.0.markup${prefix}.left.type`).to.equal( - cursor.left.type, - ); - expectPath(ast, `children.0.markup${prefix}.right.type`).to.equal( - cursor.right.type, - ); - cursor = cursor.right; - prefix = prefix + '.right'; - break; - } - default: { - expectPath(ast, `children.0.markup${prefix}.type`).to.equal(cursor.type); - cursor = null; - break; - } - } - } - - expectPosition(ast, 'children.0'); - } - }); - }); - }); - - describe('Case: content_for', () => { - it('should parse content_for tags with no arguments', () => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{% content_for "blocks" %}`); - expectPath(ast, 'children.0.type').to.equal('LiquidTag'); - expectPath(ast, 'children.0.name').to.equal('content_for'); - expectPath(ast, 'children.0.markup.type').to.equal('ContentForMarkup'); - expectPath(ast, 'children.0.markup.contentForType.type').to.equal('String'); - expectPath(ast, 'children.0.markup.contentForType.value').to.equal('blocks'); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - } - }); - - it('should parse content_for named expression arguments', () => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{% content_for "snippet", s: 'string', n: 10, r: (1..2), v: variable %}`); - expectPath(ast, 'children.0.type').to.equal('LiquidTag'); - expectPath(ast, 'children.0.name').to.equal('content_for'); - expectPath(ast, 'children.0.markup.type').to.equal('ContentForMarkup'); - expectPath(ast, 'children.0.markup.contentForType.type').to.equal('String'); - expectPath(ast, 'children.0.markup.contentForType.value').to.equal('snippet'); - expectPath(ast, 'children.0.markup.args').to.have.lengthOf(4); - expectPath(ast, 'children.0.markup.args.0.type').to.equal('NamedArgument'); - expectPath(ast, 'children.0.markup.args.0.name').to.equal('s'); - expectPath(ast, 'children.0.markup.args.0.value.type').to.equal('String'); - expectPath(ast, 'children.0.markup.args.1.type').to.equal('NamedArgument'); - expectPath(ast, 'children.0.markup.args.1.name').to.equal('n'); - expectPath(ast, 'children.0.markup.args.1.value.type').to.equal('Number'); - expectPath(ast, 'children.0.markup.args.2.type').to.equal('NamedArgument'); - expectPath(ast, 'children.0.markup.args.2.name').to.equal('r'); - expectPath(ast, 'children.0.markup.args.2.value.type').to.equal('Range'); - expectPath(ast, 'children.0.markup.args.3.type').to.equal('NamedArgument'); - expectPath(ast, 'children.0.markup.args.3.name').to.equal('v'); - expectPath(ast, 'children.0.markup.args.3.value.type').to.equal('VariableLookup'); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - } - }); - - it('should parse content_for with dotted named argument keys', () => { - for (const { toAST, expectPath } of testCases) { - ast = toAST(`{% content_for "block", closest.collection: collection %}`); - expectPath(ast, 'children.0.type').to.equal('LiquidTag'); - expectPath(ast, 'children.0.name').to.equal('content_for'); - expectPath(ast, 'children.0.markup.args').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.args.0.type').to.equal('NamedArgument'); - expectPath(ast, 'children.0.markup.args.0.name').to.equal('closest.collection'); - expectPath(ast, 'children.0.markup.args.0.value.type').to.equal('VariableLookup'); - } - }); - }); - }); - - it(`should parse liquid inline comments`, () => { - for (const { toAST, expectPath } of testCases) { - ast = toAST(`{% #%}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('#'); - expectPath(ast, 'children.0.markup').to.eql(''); - - ast = toAST(`{% #hello world %}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('#'); - expectPath(ast, 'children.0.markup').to.eql('hello world'); - } - }); - - it(`should parse liquid case as branches`, () => { - for (const { toAST, expectPath } of testCases) { - ast = toAST(`{% case A %}{% when A %}A{% when "B" %}B{% else %}C{% endcase %}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('case'); - - // There's an empty child node between the case and first when. That's OK (?) - // What if there's whitespace? I think that's a printer problem. If - // there's freeform text we should somehow catch it. - expectPath(ast, 'children.0.children.0').to.exist; - expectPath(ast, 'children.0.children.0.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.children.0.name').to.eql(null); - - expectPath(ast, 'children.0.children.1').to.exist; - expectPath(ast, 'children.0.children.1.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.children.1.name').to.eql('when'); - expectPath(ast, 'children.0.children.1.markup').to.have.lengthOf(1); - expectPath(ast, 'children.0.children.1.markup.0.type').to.equal('VariableLookup'); - expectPath(ast, 'children.0.children.1.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.1.children.0.value').to.eql('A'); - - expectPath(ast, 'children.0.children.2.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.children.2.name').to.eql('when'); - expectPath(ast, 'children.0.children.2.markup.0.type').to.equal('String'); - expectPath(ast, 'children.0.children.2.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.2.children.0.value').to.eql('B'); - - expectPath(ast, 'children.0.children.3.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.children.3.name').to.eql('else'); - expectPath(ast, 'children.0.children.3.markup').to.eql(''); - expectPath(ast, 'children.0.children.3.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.3.children.0.value').to.eql('C'); - } - }); - - it(`should parse liquid ifs as branches`, () => { - for (const { toAST, expectPath } of testCases) { - const source = `{% if a %}A{% elsif b %}B{% else %}C{% endif %}`; - ast = toAST(source); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('if'); - expectPath(ast, 'children.0.children.0').to.exist; - expectPath(ast, 'children.0.children.0.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.children.0.markup').to.eql(''); - expectPath(ast, 'children.0.children.0.position.start').to.eql(source.indexOf('A')); - expectPath(ast, 'children.0.children.0.position.end').to.eql(source.indexOf('A') + 1); - expectPath(ast, 'children.0.children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.children.0.value').to.eql('A'); - - expectPath(ast, 'children.0.children.1.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.children.1.name').to.eql('elsif'); - expectPath(ast, 'children.0.children.1.markup.type').to.eql('VariableLookup'); - expectPath(ast, 'children.0.children.1.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.1.children.0.value').to.eql('B'); - - expectPath(ast, 'children.0.children.2.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.children.2.name').to.eql('else'); - expectPath(ast, 'children.0.children.2.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.2.children.0.value').to.eql('C'); - } - }); - - it('should parse ifchanged as a block tag with no markup and children', () => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST('{% ifchanged %}hello{% endifchanged %}'); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('ifchanged'); - expectPath(ast, 'children.0.markup').to.eql(null); - expectPath(ast, 'children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.value').to.eql('hello'); - expectPosition(ast, 'children.0'); - } - }); - - it('should parse partial as a block tag with string markup', () => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{% partial 'header' %}content{% endpartial %}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('partial'); - expectPath(ast, 'children.0.markup.type').to.eql('String'); - expectPath(ast, 'children.0.markup.value').to.eql('header'); - expectPath(ast, 'children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.value').to.eql('content'); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - } - }); - - it('should parse block as a block tag with name and children', () => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST(`{% block 'foo' %}content{% endblock %}`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('block'); - expectPath(ast, 'children.0.markup.type').to.eql('BlockMarkup'); - expectPath(ast, 'children.0.markup.name.type').to.eql('String'); - expectPath(ast, 'children.0.markup.name.value').to.eql('foo'); - expectPath(ast, 'children.0.markup.args').to.eql([]); - expectPath(ast, 'children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.value').to.eql('content'); - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.markup'); - } - }); - - it('should include trailing whitespace in block markup position (Bug 29 fix)', () => { - for (const { toAST, expectPosition } of testCases) { - ast = toAST(`{% block 'name' %}{% endblock %}`); - expectPosition(ast, 'children.0.markup').to.eql(`'name' `); - - ast = toAST(`{% block 'name', key: 'val' %}{% endblock %}`); - expectPosition(ast, 'children.0.markup').to.eql(`'name', key: 'val' `); - } - }); - - it('should parse block with kwargs', () => { - for (const { toAST, expectPath } of testCases) { - ast = toAST(`{% block 'foo', key: 'val' %}content{% endblock %}`); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('block'); - expectPath(ast, 'children.0.markup.name.value').to.eql('foo'); - expectPath(ast, 'children.0.markup.args.0.type').to.eql('NamedArgument'); - expectPath(ast, 'children.0.markup.args.0.name').to.eql('key'); - expectPath(ast, 'children.0.markup.args.0.value.value').to.eql('val'); - expectPath(ast, 'children.0.children.0.value').to.eql('content'); - } - }); - - it('should parse section self-closing (existing behavior preserved)', () => { - for (const { toAST, expectPath } of testCases) { - ast = toAST(`{% section 'foo' %}`); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('section'); - expectPath(ast, 'children.0.markup.type').to.eql('SectionMarkup'); - expectPath(ast, 'children.0.markup.name.type').to.eql('String'); - expectPath(ast, 'children.0.markup.name.value').to.eql('foo'); - expectPath(ast, 'children.0.markup.args').to.eql([]); - expectPath(ast, 'children.0.children').to.be.undefined; - } - }); - - it('should parse section self-closing with kwargs', () => { - for (const { toAST, expectPath } of testCases) { - ast = toAST(`{% section 'foo', key: 'val' %}`); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('section'); - expectPath(ast, 'children.0.markup.name.value').to.eql('foo'); - expectPath(ast, 'children.0.markup.args.0.type').to.eql('NamedArgument'); - expectPath(ast, 'children.0.markup.args.0.name').to.eql('key'); - expectPath(ast, 'children.0.markup.args.0.value.value').to.eql('val'); - } - }); - - it('should treat section as standalone and reject stray endsection tags', () => { - for (const { toAST } of testCases) { - expect(() => toAST(`{% section 'foo' %}content{% endsection %}`)).to.throw( - /without a matching 'section'/, - ); - expect(() => toAST(`{% endsection %}`)).to.throw(/without a matching 'section'/); - expect(() => toAST(`{% if a %}{% endsection %}{% endif %}`)).to.throw( - /before LiquidTag 'if' was closed/, - ); - } - }); - - it('should still throw on stray end tags for other registered tags', () => { - for (const { toAST } of testCases) { - expect(() => toAST(`{% endrender %}`)).to.throw(/without a matching 'render'/); - expect(() => toAST(`{% endsections %}`)).to.throw(/without a matching 'sections'/); - expect(() => toAST(`{% if a %}{% endrender %}{% endif %}`)).to.throw( - /before LiquidTag 'if' was closed/, - ); - } - }); - - it('should correctly report the position of branches', () => { - for (const { toAST, expectPath } of testCases) { - const branchA = '
A
'; - const branchB = ' B '; - const branchC = ' {% if C %}{% endif %} '; - const source = `{% if a %}${branchA}{% elsif b %}${branchB}{% else %}${branchC}{% endif %}`; - ast = toAST(source); - - expectPath(ast, 'children.0.children.0.type').to.equal('LiquidBranch'); - expectPath(ast, 'children.0.children.0.position.start').to.equal(source.indexOf(branchA)); - expectPath(ast, 'children.0.children.0.position.end').to.equal( - source.indexOf(branchA) + branchA.length, - ); - expectPath(ast, 'children.0.children.0.blockStartPosition.start').to.equal( - source.indexOf(branchA), - ); - expectPath(ast, 'children.0.children.0.blockStartPosition.end').to.equal( - source.indexOf(branchA), - ); - expectPath(ast, 'children.0.children.0.blockEndPosition.start').to.equal( - source.indexOf('{% elsif b %}'), - ); - expectPath(ast, 'children.0.children.0.blockEndPosition.end').to.equal( - source.indexOf('{% elsif b %}'), - ); - - expectPath(ast, 'children.0.children.1.type').to.equal('LiquidBranch'); - expectPath(ast, 'children.0.children.1.position.start').to.equal( - source.indexOf('{% elsif b %}' + branchB), - ); - expectPath(ast, 'children.0.children.1.position.end').to.equal( - source.indexOf(branchB) + branchB.length, - ); - expectPath(ast, 'children.0.children.1.blockStartPosition.start').to.equal( - source.indexOf('{% elsif b %}'), - ); - expectPath(ast, 'children.0.children.1.blockStartPosition.end').to.equal( - source.indexOf('{% elsif b %}') + '{% elsif b %}'.length, - ); - expectPath(ast, 'children.0.children.1.blockEndPosition.start').to.equal( - source.indexOf('{% else %}'), - ); - expectPath(ast, 'children.0.children.1.blockEndPosition.end').to.equal( - source.indexOf('{% else %}'), - ); - - expectPath(ast, 'children.0.children.2.type').to.equal('LiquidBranch'); - expectPath(ast, 'children.0.children.2.position.start').to.equal( - source.indexOf('{% else %}' + branchC), - ); - expectPath(ast, 'children.0.children.2.position.end').to.equal( - source.indexOf(branchC) + branchC.length, - ); - expectPath(ast, 'children.0.children.2.blockStartPosition.start').to.equal( - source.indexOf('{% else %}'), - ); - expectPath(ast, 'children.0.children.2.blockStartPosition.end').to.equal( - source.indexOf('{% else %}') + '{% else %}'.length, - ); - expectPath(ast, 'children.0.children.2.blockEndPosition.start').to.equal( - source.indexOf('{% endif %}', source.indexOf(branchC) + branchC.length), - ); - expectPath(ast, 'children.0.children.2.blockEndPosition.end').to.equal( - source.indexOf('{% endif %}', source.indexOf(branchC) + branchC.length), - ); - } - }); - - it('should parse raw-like tags', () => { - const tags = ['javascript', 'style']; - for (const { toAST, expectPath, expectPosition } of testCases) { - for (const tag of tags) { - const code = ` - {% ${tag} %} - {% liquid - assign x = 1 - assign x = 2 - %} - /* comment string */ - {% end${tag} %} - `; - ast = toAST(code); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.body.type').to.eql('RawMarkup'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.body.nodes.0.markup.0.name').to.eql('assign'); - expectPath(ast, 'children.0.body.nodes.0.markup.1.name').to.eql('assign'); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('TextNode'); - expectPosition(ast, 'children.0.body.nodes.0.markup.0').toEqual('assign x = 1'); - expectPosition(ast, 'children.0.body.nodes.0.markup.1').toEqual('assign x = 2'); - expectPosition(ast, 'children.0.body.nodes.1').toEqual('/* comment string */'); - } - } - }); - - it('should parse schema tag body as a TextNode (Bug 10 regression)', () => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST('{% schema %}{"name":"test"}{% endschema %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('schema'); - expectPath(ast, 'children.0.body.type').to.eql('RawMarkup'); - expectPath(ast, 'children.0.body.kind').to.eql('json'); - expectPath(ast, 'children.0.body.value').to.eql('{"name":"test"}'); - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(1); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.0.value').to.eql('{"name":"test"}'); - expectPosition(ast, 'children.0.body.nodes.0').toEqual('{"name":"test"}'); - } - }); - - it('should parse schema tag body with surrounding whitespace (Bug 10 regression)', () => { - for (const { toAST, expectPath } of testCases) { - ast = toAST('{% schema %}\n {"name":"test"}\n{% endschema %}'); - expectPath(ast, 'children.0.body.value').to.eql('\n {"name":"test"}\n'); - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(1); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.0.value').to.eql('{"name":"test"}'); - } - }); - - it('should parse empty schema tag body as empty nodes (Bug 10 regression)', () => { - for (const { toAST, expectPath } of testCases) { - ast = toAST('{% schema %}{% endschema %}'); - expectPath(ast, 'children.0.body.value').to.eql(''); - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(0); - } - }); - - it('should assign kind=css for {% style %} tags without Liquid (Bug 13+42 regression)', () => { - for (const { toAST, expectPath } of testCases) { - ast = toAST('{% style %}.foo { color: red }{% endstyle %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('style'); - expectPath(ast, 'children.0.body.kind').to.eql(RawMarkupKinds.css); - } - }); - - it('should assign kind=css for HTML '); - expect(deepGet('children.0.type'.split('.'), ast)).to.eql('HtmlRawNode'); - expect(deepGet('children.0.name'.split('.'), ast)).to.eql('style'); - expect(deepGet('children.0.body.kind'.split('.'), ast)).to.eql(RawMarkupKinds.css); - }); - - it('should assign kind=text for HTML '); - expect(deepGet('children.0.type'.split('.'), ast)).to.eql('HtmlRawNode'); - expect(deepGet('children.0.name'.split('.'), ast)).to.eql('style'); - expect(deepGet('children.0.body.kind'.split('.'), ast)).to.eql(RawMarkupKinds.text); - }); - - it('should assign kind=text for HTML '); - expect(deepGet('children.0.type'.split('.'), ast)).to.eql('HtmlRawNode'); - expect(deepGet('children.0.name'.split('.'), ast)).to.eql('style'); - expect(deepGet('children.0.body.kind'.split('.'), ast)).to.eql(RawMarkupKinds.text); - }); - - it('should assign kind=css for {% style %} without Liquid content (Bug 42 regression)', () => { - ast = toLiquidHtmlAST('{% style %}.foo { color: red }{% endstyle %}'); - expect(deepGet('children.0.type'.split('.'), ast)).to.eql('LiquidRawTag'); - expect(deepGet('children.0.name'.split('.'), ast)).to.eql('style'); - expect(deepGet('children.0.body.kind'.split('.'), ast)).to.eql(RawMarkupKinds.css); - }); - - it('should assign kind=text for {% style %} with Liquid content (Bug 42 regression)', () => { - ast = toLiquidHtmlAST('{% style %}.foo { color: {{ color }} }{% endstyle %}'); - expect(deepGet('children.0.type'.split('.'), ast)).to.eql('LiquidRawTag'); - expect(deepGet('children.0.name'.split('.'), ast)).to.eql('style'); - expect(deepGet('children.0.body.kind'.split('.'), ast)).to.eql(RawMarkupKinds.text); - }); - - it('should parse inside SVG raw body with Liquid blocks as TextNode, not HtmlElement (Bug 46)', () => { - const expectPath = makeExpectPath('toLiquidHtmlAST - SVG use Bug 46'); - ast = toLiquidHtmlAST('{% if true %}{% endif %}'); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.name').to.eql('svg'); - // The {% if %} block inside SVG raw body should have TextNode children, - // not HtmlElement — SVG raw bodies disable HTML parsing. - const ifTag = deepGet('children.0.body.nodes.0'.split('.'), ast); - expect(ifTag.type).to.eql('LiquidTag'); - expect(ifTag.name).to.eql('if'); - const branch = ifTag.children[0]; - expect(branch.type).to.eql('LiquidBranch'); - // The tag should be a TextNode, not an HtmlElement - const useChild = branch.children.find( - (c: any) => c.type !== 'TextNode' || c.value.includes(' { - const expectPath = makeExpectPath('toLiquidHtmlAST - nested raw close balancing'); - - // Nested elements: the outer must close at the LAST - // , keeping the inner … as raw body text rather than - // closing early at the first (which previously threw / mis-parsed). - ast = toLiquidHtmlAST('abc'); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.name').to.eql('svg'); - expectPath(ast, 'children.0.body.value').to.eql('abc'); - - // Same balancing for a non-svg raw tag (c'); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.name').to.eql('script'); - expectPath(ast, 'children.0.body.value').to.eql('ac'); - }); - - it(`should parse a basic text node into a TextNode`, () => { - for (const { toAST, expectPath, expectPosition } of testCases) { - ast = toAST('Hello world!'); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql('Hello world!'); - expectPosition(ast, 'children.0'); - } - }); - }); - - describe('Unit: toLiquidHtmlAST(text)', () => { - let ast: any; - let expectPath = makeExpectPath('toLiquidHtmlAST'); - let expectPosition = makeExpectPosition('toLiquidHtmlAST'); - - it('should parse HTML attributes', () => { - ast = toLiquidHtmlAST(``); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('HtmlVoidElement'); - expectPath(ast, 'children.0.name').to.eql('img'); - expectPath(ast, 'children.0.attributes.0.name.0.value').to.eql('src'); - expectPath(ast, 'children.0.attributes.0.value.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.attributes.0.value.0.value').to.eql('https://1234'); - expectPath(ast, 'children.0.attributes.1.name.0.value').to.eql('loading'); - expectPath(ast, 'children.0.attributes.1.value.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.attributes.1.value.0.value').to.eql('lazy'); - expectPath(ast, 'children.0.attributes.2.name.0.value').to.eql('disabled'); - expectPath(ast, 'children.0.attributes.3.name.0.value').to.eql('checked'); - expectPath(ast, 'children.0.attributes.3.value.0').to.be.undefined; - - expectPosition(ast, 'children.0'); - expectPosition(ast, 'children.0.attributes.0'); - expectPosition(ast, 'children.0.attributes.0.value.0'); - expectPosition(ast, 'children.0.attributes.1'); - expectPosition(ast, 'children.0.attributes.1.value.0'); - expectPosition(ast, 'children.0.attributes.2'); - }); - - it('should parse HTML attributes inside tags', () => { - ast = toLiquidHtmlAST( - ``, - ); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('HtmlVoidElement'); - expectPath(ast, 'children.0.name').to.eql('img'); - expectPath(ast, 'children.0.attributes.0.name').to.eql('if'); - expectPath(ast, 'children.0.attributes.0.children.0.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.attributes.0.children.0.children.0.type').to.eql( - 'AttrDoubleQuoted', - ); - expectPath(ast, 'children.0.attributes.0.children.0.children.1.type').to.eql( - 'AttrSingleQuoted', - ); - }); - - it('should preserve whitespace in LiquidBranch children inside attribute values', () => { - ast = toLiquidHtmlAST(`
`); - // The LiquidTag (if) is inside the attribute value - const ifTag = 'children.0.attributes.0.value.1'; - expectPath(ast, `${ifTag}.type`).to.eql('LiquidTag'); - expectPath(ast, `${ifTag}.name`).to.eql('if'); - // The branch child TextNode must preserve its leading space - const branchChild = `${ifTag}.children.0.children.0`; - expectPath(ast, `${branchChild}.type`).to.eql('TextNode'); - expectPath(ast, `${branchChild}.value`).to.eql(' extra-class'); - }); - - it('should parse HTML tags with Liquid Drop names', () => { - [ - `<{{ node_type }} src="https://1234" loading='lazy' disabled>`, - `<{{ node_type }} src="https://1234" loading='lazy' disabled>`, - `<{{ node_type }} src="https://1234" loading='lazy' disabled>`, - `<{{ node_type }} src="https://1234" loading='lazy' disabled>`, - `<{{ node_type -}} src="https://1234" loading='lazy' disabled>`, - `<{{ node_type -}} src="https://1234" loading='lazy' disabled>`, - `<{{- node_type -}} src="https://1234" loading='lazy' disabled>`, - ].forEach((testCase) => { - ast = toLiquidHtmlAST(testCase); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('HtmlElement'); - expectPath(ast, 'children.0.name.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.name.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.name.0.markup.rawSource').to.eql('node_type'); - expectPath(ast, 'children.0.attributes.0.name.0.value').to.eql('src'); - expectPath(ast, 'children.0.attributes.0.value.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.attributes.0.value.0.value').to.eql('https://1234'); - expectPath(ast, 'children.0.attributes.1.name.0.value').to.eql('loading'); - expectPath(ast, 'children.0.attributes.1.value.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.attributes.1.value.0.value').to.eql('lazy'); - expectPath(ast, 'children.0.attributes.2.name.0.value').to.eql('disabled'); - }); - }); - - it('should parse HTML tags with compound Liquid Drop names', () => { - ast = toLiquidHtmlAST(`<{{ node_type }}--header >`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('HtmlElement'); - expectPath(ast, 'children.0.name.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.name.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.name.0.markup.rawSource').to.eql('node_type'); - expectPath(ast, 'children.0.name.1.value').to.eql('--header'); - }); - - it('should parse HTML self-closing elements with compound Liquid Drop names', () => { - ast = toLiquidHtmlAST(`<{{ node_type }}--header />`); - expectPath(ast, 'children.0').to.exist; - expectPath(ast, 'children.0.type').to.eql('HtmlSelfClosingElement'); - expectPath(ast, 'children.0.name.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.name.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.name.0.markup.rawSource').to.eql('node_type'); - expectPath(ast, 'children.0.name.1.value').to.eql('--header'); - }); - - it('should allow unclosed nodes inside conditional and case branches', () => { - let testCases = [ - // one unclosed - '{% if cond %}
{% endif %}', - '{% if cond %}{% else %}
{% endif %}', - '{% if cond %}
{% else %}{% endif %}', - '{% if cond %}{% elsif cond %}
{% endif %}', - // two unclosed - '{% if cond %}
{% endif %}', - '{% if cond %}{% else %}
{% endif %}', - '{% if cond %}{% elsif cond %}
{% endif %}', - '{% case cond %}{% when %}
{% endcase %}', - // three unclosed - '{% if cond %}{% endif %}', - '{% if cond %}{% else %}{% endif %}', - '{% if cond %}{% elsif cond %}{% endif %}', - '{% case cond %}{% when %}{% endcase %}', - // 1 closed, last unclosed - '{% if cond %}hi{% endif %}', - // last unclosed with closed child - '{% if cond %}hi{% endif %}', - '{% if cond %}{% else %}hi{% endif %}', - '{% if cond %}{% else %}hi{% endif %}', - '{% if cond %}{% elsif cond %}hi{% endif %}', - '{% if cond %}{% elsif cond %}hi{% endif %}', - '{% case cond %}{% when %}hi{% endcase %}', - ]; - for (const testCase of testCases) { - expect(() => toLiquidHtmlAST(testCase), testCase).not.to.throw(); - } - }); - - describe('Case: unclosed HTML nodes', () => { - it('should let me write unclosed nodes inside if statements', () => { - const unclosedDetailsSummary = '
hello'; - const testCases = [ - { - testCase: `{% if cond %}${unclosedDetailsSummary}{% endif %}`, - detailsNodePath: 'children.0.children.0.children.0', - }, - { - testCase: `{% if cond %}${unclosedDetailsSummary}{% else %}{% endif %}`, - detailsNodePath: 'children.0.children.0.children.0', - }, - { - testCase: `{% if cond %}{% elsif other_cond %}${unclosedDetailsSummary}{% endif %}`, - detailsNodePath: 'children.0.children.1.children.0', - }, - { - testCase: `{% if cond %}{% elsif other_cond %}{% else %}${unclosedDetailsSummary}{% endif %}`, - detailsNodePath: 'children.0.children.2.children.0', - extraExpectations: (ast: DocumentNode) => { - expectPosition(ast, 'children.0.children.0').toEqual(''); - expectPosition(ast, 'children.0.children.1').toEqual('{% elsif other_cond %}'); - expectPosition(ast, 'children.0.children.2').toEqual( - `{% else %}${unclosedDetailsSummary}`, - ); - }, - }, - { - testCase: `{% unless cond %}${unclosedDetailsSummary}{% endunless %}`, - detailsNodePath: 'children.0.children.0.children.0', - }, - { - testCase: `{% case thing %}{% when cond %}${unclosedDetailsSummary}{% endcase %}`, - detailsNodePath: 'children.0.children.1.children.0', - }, - { - testCase: `{% case thing %}{% when cond %}${unclosedDetailsSummary}{% else %}{% endcase %}`, - detailsNodePath: 'children.0.children.1.children.0', - }, - { - testCase: `{% case thing %}{% else %}${unclosedDetailsSummary}{% endcase %}`, - detailsNodePath: 'children.0.children.1.children.0', - }, - ]; - for (const { testCase, detailsNodePath, extraExpectations } of testCases) { - const ast = toLiquidHtmlAST(testCase, { - allowUnclosedDocumentNode: false, - mode: 'tolerant', - }); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.children.0.type').to.eql('LiquidBranch'); - expectPosition(ast, `${detailsNodePath}`).toEqual('
hello'); - - // Does not have a close tag so the slice of the end tag is empty string - expectPosition(ast, `${detailsNodePath}`, 'blockEndPosition').toEqual(''); - - // making sure blockEndPosition isn't -1, -1 but adjusted to the child position - expect( - deepGet(`${detailsNodePath}.blockEndPosition.start`.split('.'), ast), - ).toBeGreaterThan( - deepGet(`${detailsNodePath}.blockStartPosition.end`.split('.'), ast), - ); - - expectPosition(ast, `${detailsNodePath}.children.0`).toEqual('hello'); - expectPosition(ast, `${detailsNodePath}.children.0`, 'blockEndPosition').toEqual( - '', - ); - if (extraExpectations) extraExpectations(ast); - } - }); - - it('should throw an error when writing an unclosed node inside any other tag', async () => { - const testCases = [ - '{% for x in y %}
{% endfor %}', - '{% tablerow x in y %}
{% endtablerow %}', - '{% form "cart", cart %}
{% endform %}', - ]; - for (const testCase of testCases) { - try { - toLiquidHtmlAST(testCase, { - allowUnclosedDocumentNode: false, - mode: 'tolerant', - }); - expect(true, `expected ${testCase} to throw LiquidHTMLASTParsingError`).to.be.false; - } catch (e: any) { - expect(e.name, testCase).to.eql('LiquidHTMLParsingError'); - expect(e.message, testCase).to.match( - /Attempting to close \w+ '[^']+' before \w+ '[^']+' was closed/, - ); - expect(e.message).not.to.match(/undefined/i); - expect(e.loc, `expected ${e} to have location information`).not.to.be.undefined; - } - } - }); - }); - - it('should throw when trying to close the wrong node', () => { - const testCases = [ - '
', - '{% for a in b %}
{% endfor %}', - '{% for a in b %}{% if condition %}{% endfor %}', - '{% for a in b %}{% if condition %}
{% endfor %}', - '<{{ node_type }}>
', - '<{{ node_type }}>', - ]; - for (const testCase of testCases) { - try { - toLiquidHtmlAST(testCase); - expect(true, `expected ${testCase} to throw LiquidHTMLParsingError`).to.be.false; - } catch (e: any) { - expect(e.name).to.eql('LiquidHTMLParsingError'); - expect(e.message, testCase).to.match( - /Attempting to (open|close) \w+ '[^']+' before \w+ '[^']+' was closed/, - ); - expect(e.message).not.to.match(/undefined/i); - expect(e.loc, `expected ${e} to have location information`).not.to.be.undefined; - } - } - }); - - it('should throw when doing weird shit', () => { - const testCases = ['{% if cond %}{% endif %}']; - for (const testCase of testCases) { - try { - toLiquidHtmlAST(testCase); - expect(true, `expected ${testCase} to throw LiquidHTMLParsingError`).to.be.false; - } catch (e: any) { - expect(e.name).to.eql('LiquidHTMLParsingError'); - expect(e.message).not.to.match(/undefined/i); - expect(e.loc, `expected ${e} to have location information`).not.to.be.undefined; - } - } - }); - - it('should throw when trying to open a new branch when a Liquid node was not closed', () => { - const testCases = [ - '{% if cond %}{% form "cart", cart %}{% else %}{% endif %}', - '{% if cond %}{% form "cart", cart %}{% elsif cond %}{% endif %}', - ]; - for (const testCase of testCases) { - try { - toLiquidHtmlAST(testCase); - expect(true, `expected ${testCase} to throw LiquidHTMLParsingError`).to.be.false; - } catch (e: any) { - expect(e.name).to.eql('LiquidHTMLParsingError'); - expect(e.message, testCase).to.match( - /Attempting to open \w+ '[^']+' before \w+ '[^']+' was closed/, - ); - expect(e.message).not.to.match(/undefined/i); - expect(e.loc, `expected ${e} to have location information`).not.to.be.undefined; - } - } - }); - - it('should throw when closing at the top level', () => { - const testCases = ['', '{% if %}']; - for (const testCase of testCases) { - try { - toLiquidHtmlAST(testCase); - expect(true, `expected ${testCase} to throw LiquidHTMLASTParsingError`).to.be.false; - } catch (e: any) { - expect(e.name).to.eql('LiquidHTMLParsingError'); - expect(e.message).to.match(/Attempting to end parsing before \w+ '[^']+' was closed/); - expect(e.message).not.to.match(/undefined/i); - expect(e.loc, `expected ${e} to have location information`).not.to.be.undefined; - } - } - }); - - it('should throw when forgetting to close', () => { - const testCases = ['', '{% endif %}']; - for (const testCase of testCases) { - try { - toLiquidHtmlAST(testCase); - expect(true, `expected ${testCase} to throw LiquidHTMLASTParsingError`).to.be.false; - } catch (e: any) { - expect(e.name).to.eql('LiquidHTMLParsingError'); - expect(e.message).to.match(/Attempting to close \w+ '[^']+' before it was opened/); - expect(e.message).not.to.match(/undefined/i); - expect(e.loc, `expected ${e} to have location information`).not.to.be.undefined; - } - } - }); - - it('should throw when trying to end doc with unclosed nodes', () => { - const testCases = ['

', '{% if condition %}', '`); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.name').to.eql('script'); - expectPath(ast, 'children.0.body.type').to.eql('RawMarkup'); - expectPath(ast, 'children.0.body.kind').to.eql('javascript'); - expectPath(ast, 'children.0.body.value').to.eql('\n const a = {{ product | json }};\n'); - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(3); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.body.nodes.2.type').to.eql('TextNode'); - expectPosition(ast, 'children.0.body.nodes.0').toEqual('const a ='); - expectPosition(ast, 'children.0.body.nodes.1').toEqual('{{ product | json }}'); - expectPosition(ast, 'children.0.body.nodes.2').toEqual(';'); - expectPosition(ast, 'children.0'); - }); - - it('should detect json kind for script type="application/ld+json"', () => { - ast = toLiquidHtmlAST(``); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.name').to.eql('script'); - expectPath(ast, 'children.0.body.kind').to.eql('json'); - }); - - it('should detect json kind for script type="application/json"', () => { - ast = toLiquidHtmlAST(``); - expectPath(ast, 'children.0.body.kind').to.eql('json'); - }); - - it('should detect json kind for script type="importmap"', () => { - ast = toLiquidHtmlAST(``); - expectPath(ast, 'children.0.body.kind').to.eql('json'); - }); - - it('should detect json kind for script type="speculationrules"', () => { - ast = toLiquidHtmlAST(``); - expectPath(ast, 'children.0.body.kind').to.eql('json'); - }); - - it('should detect javascript kind for script with no type attribute', () => { - ast = toLiquidHtmlAST(``); - expectPath(ast, 'children.0.body.kind').to.eql('javascript'); - }); - - it('should parse style tags as raw markup', () => { - ast = toLiquidHtmlAST(``); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.name').to.eql('style'); - expectPath(ast, 'children.0.body.type').to.eql('RawMarkup'); - expectPath(ast, 'children.0.body.kind').to.eql('text'); - expectPath(ast, 'children.0.body.value').to.eql('\n :root { --bg: {{ settings.bg }}}\n'); - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(3); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.body.nodes.2.type').to.eql('TextNode'); - expectPosition(ast, 'children.0.body.nodes.0').toEqual(':root { --bg:'); - expectPosition(ast, 'children.0.body.nodes.1').toEqual('{{ settings.bg }}'); - expectPosition(ast, 'children.0.body.nodes.2').toEqual('}'); - expectPosition(ast, 'children.0'); - }); - }); - - describe('Unit: toLiquidAST(text)', () => { - let ast: any; - let expectPath = makeExpectPath('toLiquidAST'); - - it('should parse nested unclosed tags', () => { - ast = toLiquidAST('{% for a in b %}
{% if true %}'); - - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('for'); - - expectPath(ast, 'children.0.children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.children.0.value').to.eql('
'); - - expectPath(ast, 'children.0.children.0.children.1.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.children.0.children.1.name').to.eql('if'); - }); - - it('should parse unclosed conditions with assignments', () => { - ast = toLiquidAST(` - {%- liquid - assign var1 = product - - if use_variant - assign var2 = var1 - assign var3 = var2 - -%} - `); - - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('liquid'); - - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('assign'); - expectPath(ast, 'children.0.markup.0.markup.name').to.eql('var1'); - - expectPath(ast, 'children.0.markup.1.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.markup.1.name').to.eql('if'); - - expectPath(ast, 'children.0.markup.1.children.0.children.0.name').to.eql('assign'); - expectPath(ast, 'children.0.markup.1.children.0.children.0.markup.name').to.eql('var2'); - - expectPath(ast, 'children.0.markup.1.children.0.children.1.name').to.eql('assign'); - expectPath(ast, 'children.0.markup.1.children.0.children.1.markup.name').to.eql('var3'); - }); - - it('should preserve string enum doc types in Liquid and HTML documents', () => { - const paramType = "'Heading' | 'a|b' | '}' | ''"; - const source = `{% doc %}\n @param {${paramType}} [variant] - Shared text style\n{% enddoc %}`; - for (const toAST of [toLiquidAST, toLiquidHtmlAST]) { - const expectPosition = makeExpectPosition(toAST.name); - ast = toAST(source); - expectPath(ast, 'children.0.body.nodes.0.paramType.type').toEqual('TextNode'); - expectPath(ast, 'children.0.body.nodes.0.paramType.value').toEqual(paramType); - expectPosition(ast, 'children.0.body.nodes.0.paramType').toEqual(paramType); - expectPosition(ast, 'children.0.body.nodes.0.paramName').toEqual('variant'); - expectPath(ast, 'children.0.body.nodes.0.required').toEqual(false); - expectPath(ast, 'children.0.body.nodes.0.paramDescription.value').toEqual( - 'Shared text style', - ); - } - }); - - it(`should parse doc tags`, () => { - ast = toLiquidAST(`{% doc %}{% enddoc %}`); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.markup').toEqual(''); - expectPath(ast, 'children.0.body.value').to.eql(''); - expectPath(ast, 'children.0.body.type').toEqual('RawMarkup'); - expectPath(ast, 'children.0.body.nodes').toEqual([]); - - ast = toLiquidAST(` - {% doc -%} - @param requiredParamWithNoType - @param {String} paramWithDescription - param with description and \`punctation\`. This is still a valid param description. - @param {String} paramWithNoDescription - @param {String} [optionalParameterWithTypeAndDescription] - optional parameter with type and description - @param [optionalParameterWithDescription] - optional parameter description - @param {String} [optionalParameterWithType] - @unsupported this node falls back to a text node - {%- enddoc %} - `); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.0.name').to.eql('param'); - expectPath(ast, 'children.0.body.nodes.0.required').to.eql(true); - expectPath(ast, 'children.0.body.nodes.0.paramName.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('requiredParamWithNoType'); - expectPath(ast, 'children.0.body.nodes.0.paramType').to.be.null; - expectPath(ast, 'children.0.body.nodes.0.paramDescription').to.be.null; - - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.1.name').to.eql('param'); - expectPath(ast, 'children.0.body.nodes.1.required').to.eql(true); - expectPath(ast, 'children.0.body.nodes.1.paramName.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('paramWithDescription'); - expectPath(ast, 'children.0.body.nodes.1.paramDescription.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql( - 'param with description and `punctation`. This is still a valid param description.', - ); - expectPath(ast, 'children.0.body.nodes.1.paramType.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.1.paramType.value').to.eql('String'); - - expectPath(ast, 'children.0.body.nodes.2.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.2.name').to.eql('param'); - expectPath(ast, 'children.0.body.nodes.2.paramName.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('paramWithNoDescription'); - expectPath(ast, 'children.0.body.nodes.2.paramDescription').to.be.null; - expectPath(ast, 'children.0.body.nodes.2.paramType.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.2.paramType.value').to.eql('String'); - - expectPath(ast, 'children.0.body.nodes.3.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.3.name').to.eql('param'); - expectPath(ast, 'children.0.body.nodes.3.required').to.eql(false); - expectPath(ast, 'children.0.body.nodes.3.paramName.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.3.paramName.value').to.eql( - 'optionalParameterWithTypeAndDescription', - ); - expectPath(ast, 'children.0.body.nodes.3.paramDescription.value').to.eql( - 'optional parameter with type and description', - ); - expectPath(ast, 'children.0.body.nodes.3.paramType.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.3.paramType.value').to.eql('String'); - - expectPath(ast, 'children.0.body.nodes.4.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.4.name').to.eql('param'); - expectPath(ast, 'children.0.body.nodes.4.required').to.eql(false); - expectPath(ast, 'children.0.body.nodes.4.paramName.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.4.paramName.value').to.eql( - 'optionalParameterWithDescription', - ); - expectPath(ast, 'children.0.body.nodes.4.paramDescription.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.4.paramDescription.value').to.eql( - 'optional parameter description', - ); - expectPath(ast, 'children.0.body.nodes.4.paramType').to.be.null; - - expectPath(ast, 'children.0.body.nodes.5.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.5.name').to.eql('param'); - expectPath(ast, 'children.0.body.nodes.5.required').to.eql(false); - expectPath(ast, 'children.0.body.nodes.5.paramName.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.5.paramName.value').to.eql( - 'optionalParameterWithType', - ); - expectPath(ast, 'children.0.body.nodes.5.paramDescription').to.be.null; - expectPath(ast, 'children.0.body.nodes.5.paramType.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.5.paramType.value').to.eql('String'); - - expectPath(ast, 'children.0.body.nodes.6.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.6.value').to.eql( - '@unsupported this node falls back to a text node', - ); - - ast = toLiquidAST(` - {% doc -%} - @example simple inline example - {%- enddoc %} - `); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.body.nodes.0.name').to.eql('example'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocExampleNode'); - expectPath(ast, 'children.0.body.nodes.0.content.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('simple inline example'); - - ast = toLiquidAST(` - {% doc -%} - @example including inline code - This is a valid example - It can have multiple lines - {% enddoc %} - `); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocExampleNode'); - expectPath(ast, 'children.0.body.nodes.0.name').to.eql('example'); - expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql( - 'including inline code\n This is a valid example\n It can have multiple lines\n', - ); - - ast = toLiquidAST(` - {% doc -%} - @example - First Example - @example - Second Example - {% enddoc %} - `); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocExampleNode'); - expectPath(ast, 'children.0.body.nodes.0.name').to.eql('example'); - expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('First Example\n'); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocExampleNode'); - expectPath(ast, 'children.0.body.nodes.1.name').to.eql('example'); - expectPath(ast, 'children.0.body.nodes.1.content.value').to.eql('Second Example\n'); - - ast = toLiquidAST(` - {% doc -%} - @example - This is a valid example - It can have multiple lines - @param {String} paramWithDescription - param with description - {% enddoc %} - `); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocExampleNode'); - expectPath(ast, 'children.0.body.nodes.0.name').to.eql('example'); - expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql( - 'This is a valid example\n It can have multiple lines\n', - ); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.1.name').to.eql('param'); - expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('paramWithDescription'); - expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql( - 'param with description', - ); - - ast = toLiquidAST(` - {% doc -%} - @description This is a description - @description This is another description - it can have multiple lines - {% enddoc %} - `); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocDescriptionNode'); - expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('This is a description'); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocDescriptionNode'); - expectPath(ast, 'children.0.body.nodes.1.content.value').to.eql( - 'This is another description\n it can have multiple lines\n', - ); - - ast = toLiquidAST(` - {% doc -%} - @description This is a description - @example This is an example - @param {String} paramWithDescription - param with description - {% enddoc %} - `); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocDescriptionNode'); - expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('This is a description'); - - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocExampleNode'); - expectPath(ast, 'children.0.body.nodes.1.name').to.eql('example'); - expectPath(ast, 'children.0.body.nodes.1.content.value').to.eql('This is an example'); - - expectPath(ast, 'children.0.body.nodes.2.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.2.name').to.eql('param'); - expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('paramWithDescription'); - expectPath(ast, 'children.0.body.nodes.2.paramDescription.value').to.eql( - 'param with description', - ); - - ast = toLiquidAST(` - {% doc -%} - this is an implicit description - in a header - - @description with a description annotation - {% enddoc %} - `); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocDescriptionNode'); - expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql( - 'this is an implicit description\n in a header\n\n', - ); - expectPath(ast, 'children.0.body.nodes.0.isImplicit').to.eql(true); - - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocDescriptionNode'); - expectPath(ast, 'children.0.body.nodes.1.content.value').to.eql( - 'with a description annotation', - ); - expectPath(ast, 'children.0.body.nodes.1.isImplicit').to.eql(false); - - ast = toLiquidAST(` -{% doc -%} - @prompt - This is a prompt - It can have multiple lines -{% enddoc %}`); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocPromptNode'); - expectPath(ast, 'children.0.body.nodes.0.name').to.eql('prompt'); - expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql( - '\n This is a prompt\n It can have multiple lines\n', - ); - - ast = toLiquidAST(` -{% doc -%} - this block was AI generated - - @prompt - First prompt - - @param {String} paramName - param description -{% enddoc %} - `); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocDescriptionNode'); - expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql( - 'this block was AI generated\n\n', - ); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocPromptNode'); - expectPath(ast, 'children.0.body.nodes.1.name').to.eql('prompt'); - expectPath(ast, 'children.0.body.nodes.1.content.value').to.eql('\n First prompt\n\n'); - expectPath(ast, 'children.0.body.nodes.2.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('paramName'); - }); - - it('should split doc body at mid-line @word patterns', () => { - // Regression: mid-line `@utility` in description text must split - // into description + unsupported annotation TextNode (Bug 26) - ast = toLiquidAST(` -{% doc %} - Description text with \`@utility\` mid-line token. - - @param {string} [name] - A param - @category content -{% enddoc %}`); - // Description stops before `@utility` - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocDescriptionNode'); - expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('Description text with `'); - // `@utility` becomes an unsupported annotation TextNode - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.1.value').to.eql('@utility` mid-line token.'); - // Param still parsed correctly - expectPath(ast, 'children.0.body.nodes.2.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('name'); - // @category becomes an unsupported annotation TextNode - expectPath(ast, 'children.0.body.nodes.3.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.3.value').to.eql('@category content'); - - // Mid-line @word in inline content after @param should NOT split - ast = toLiquidAST(` -{% doc %} - @param {string} id - Unique id. @required - @category content -{% enddoc %}`); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.0.paramDescription.value').to.eql( - 'Unique id. @required', - ); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.1.value').to.eql('@category content'); - - // Bug 43 Pattern 3: multiline @param descriptions spanning continuation lines - ast = toLiquidAST(`{% doc %} - @param {string} [alignment] - The horizontal alignment - ('left', 'center', or 'right'). Defaults to 'center'. - @param {boolean} [full_width] - Spans full width. -{% enddoc %}`); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('alignment'); - expectPath(ast, 'children.0.body.nodes.0.paramDescription.value').to.eql( - "The horizontal alignment\n ('left', 'center', or 'right'). Defaults to 'center'.", - ); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('full_width'); - - // Bug 43 Pattern 4: optional @param without description includes bracket in end position - ast = toLiquidAST(`{% doc %} - @param {number} index - @param {boolean} [current] - @param {boolean} [image_only] -{% enddoc %}`); - // Required param end = end of name word - const indexParam = ast.children[0].body.nodes[0]; - expect(indexParam.position.end).to.eql(indexParam.paramName.position.end); - // Optional param end = end of closing bracket (past paramName.position.end) - const currentParam = ast.children[0].body.nodes[1]; - expect(currentParam.position.end).to.be.greaterThan(currentParam.paramName.position.end); - const imageOnlyParam = ast.children[0].body.nodes[2]; - expect(imageOnlyParam.position.end).to.be.greaterThan(imageOnlyParam.paramName.position.end); - }); - - it('should parse unclosed tables with assignments', () => { - ast = toLiquidAST(` - {%- liquid - assign var1 = product - -%} - - {% tablerow var2 in collections.first.products %} - {% assign var3 = var2 %} - {{ var3.title }} - `); - - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('liquid'); - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('assign'); - expectPath(ast, 'children.0.markup.0.markup.name').to.eql('var1'); - - expectPath(ast, 'children.1.type').to.eql('TextNode'); - expectPath(ast, 'children.1.value').to.eql('
'); - - expectPath(ast, 'children.2.type').to.eql('LiquidTag'); - expectPath(ast, 'children.2.name').to.eql('tablerow'); - }); - - it('should parse script tags as a text node', () => { - ast = toLiquidAST(``); - - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql(''); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.name').to.eql('script'); - expectPath(ast, 'children.0.body.kind').to.eql('javascript'); - expectPath(ast, 'children.0.body.value').to.eql('var x = 1;'); - expectPosition(ast, 'children.0').to.eql(''); - }); - - it('should parse style tags as HtmlRawNode', () => { - const ast = toLiquidHtmlAST(''); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.name').to.eql('style'); - expectPath(ast, 'children.0.body.kind').to.eql('css'); - expectPath(ast, 'children.0.body.value').to.eql('.x { color: red; }'); - }); - - // The tokenizer splits `` (NBSP) into several Text tokens. As - // in browsers, that is body text, not a close tag. - it.each(['script', 'style'])( - 'should keep a %s close-tag candidate with a non-ASCII space in the body', - (tag) => { - const body = `x = "";`; - const ast = toLiquidHtmlAST(`<${tag}>${body}`); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.body.value').to.eql(body); - }, - ); - - it('should parse Liquid drops inside raw HTML body', () => { - const ast = toLiquidHtmlAST(''); - expectPath(ast, 'children.0.type').to.eql('HtmlRawNode'); - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(3); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.body.nodes.2.type').to.eql('TextNode'); - }); - - it('should parse HTML doctype', () => { - const ast = toLiquidHtmlAST(''); - expectPath(ast, 'children.0.type').to.eql('HtmlDoctype'); - expectPath(ast, 'children.0.legacyDoctypeString').to.eql(null); - expectPosition(ast, 'children.0').to.eql(''); - }); - - it('should parse HTML inside Liquid blocks', () => { - const ast = toLiquidHtmlAST('{% if x %}
hello
{% endif %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - 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('div'); - }); - - it('should parse Liquid inside HTML children', () => { - const ast = toLiquidHtmlAST('
{{ x }}
'); - expectPath(ast, 'children.0.type').to.eql('HtmlElement'); - expectPath(ast, 'children.0.children.0.type').to.eql('LiquidVariableOutput'); - }); - - it('should parse empty HTML element', () => { - const ast = toLiquidHtmlAST('
'); - expectPath(ast, 'children.0.type').to.eql('HtmlElement'); - expectPath(ast, 'children.0.children').to.have.lengthOf(0); - }); - - it('should parse adjacent HTML elements', () => { - const ast = toLiquidHtmlAST('

'); - expectPath(ast, 'children.0.type').to.eql('HtmlElement'); - expectPath(ast, 'children.0.name.0.value').to.eql('div'); - expectPath(ast, 'children.1.type').to.eql('HtmlElement'); - expectPath(ast, 'children.1.name.0.value').to.eql('p'); - }); - - it('should set blockStartPosition and blockEndPosition correctly', () => { - const source = '
content
'; - const ast = toLiquidHtmlAST(source); - const el = deepGet('children.0'.split('.'), ast) as any; - expect(source.slice(el.blockStartPosition.start, el.blockStartPosition.end)).to.eql( - '
', - ); - expect(source.slice(el.blockEndPosition.start, el.blockEndPosition.end)).to.eql('
'); - }); - - it('should set attributePosition correctly on quoted attributes', () => { - const source = '
bar
'; - const ast = toLiquidHtmlAST(source); - const attr = deepGet('children.0.attributes.0'.split('.'), ast) as any; - expect(source.slice(attr.attributePosition.start, attr.attributePosition.end)).to.eql('foo'); - }); - - it('should treat HTML as text in toLiquidAST mode', () => { - const ast = toLiquidAST('
hello
'); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - }); - - it('should parse void elements with self-closing syntax as HtmlSelfClosingElement', () => { - const ast = toLiquidHtmlAST('
'); - expectPath(ast, 'children.0.type').to.eql('HtmlSelfClosingElement'); - }); - - it('should parse text after liquid tag inside quoted attribute value as TextNode, not AttrEmpty (bug 45)', () => { - const ast = toLiquidHtmlAST( - '', - ); - const outerIf = deepGet('children.0.attributes.0'.split('.'), ast) as any; - expect(outerIf.type).to.eql('LiquidTag'); - expect(outerIf.name).to.eql('if'); - - const classAttr = outerIf.children[0].children[0]; - expect(classAttr.type).to.eql('AttrDoubleQuoted'); - expect(classAttr.name[0].value).to.eql('class'); - - const innerIf = classAttr.value[1]; - expect(innerIf.type).to.eql('LiquidTag'); - expect(innerIf.name).to.eql('if'); - - const branch = innerIf.children[0]; - expect(branch.type).to.eql('LiquidBranch'); - expect(branch.children.length).to.eql(1); - expect(branch.children[0].type).to.eql('TextNode'); - expect(branch.children[0].value).to.eql(' b'); - }); -}); diff --git a/packages/liquid-html-parser/src/document/liquid-blocks.test.ts b/packages/liquid-html-parser/src/document/liquid-blocks.test.ts index bd7c1d87c..cf8e3d7ca 100644 --- a/packages/liquid-html-parser/src/document/liquid-blocks.test.ts +++ b/packages/liquid-html-parser/src/document/liquid-blocks.test.ts @@ -1,375 +1,8 @@ import { describe, it, expect } from 'vitest'; -import { toLiquidHtmlAST, toLiquidAST } from '../ast'; -import { - expectPath, - expectPosition, - expectBlockStartPosition, - expectBlockEndPosition, - sourceAt, -} from './test-helpers'; +import { toLiquidHtmlAST } from '../ast'; describe('Unit: liquid-blocks', () => { - describe('branchless blocks (branches.length === 0)', () => { - it('should parse {% capture x %}hello{% endcapture %}', () => { - const ast = toLiquidHtmlAST('{% capture x %}hello{% endcapture %}'); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('capture'); - expectPath(ast, 'children.0.markup.type').to.eql('VariableLookup'); - expectPath(ast, 'children.0.markup.name').to.eql('x'); - expectPath(ast, 'children.0.children').to.have.lengthOf(1); - expectPath(ast, 'children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.value').to.eql('hello'); - }); - - it('should parse {% ifchanged %}content{% endifchanged %}', () => { - const ast = toLiquidHtmlAST('{% ifchanged %}content{% endifchanged %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('ifchanged'); - expectPath(ast, 'children.0.children').to.have.lengthOf(1); - expectPath(ast, 'children.0.children.0.value').to.eql('content'); - }); - - it('should parse empty branchless block', () => { - const ast = toLiquidHtmlAST('{% capture x %}{% endcapture %}'); - expectPath(ast, 'children.0.name').to.eql('capture'); - expectPath(ast, 'children.0.children').to.have.lengthOf(0); - }); - - it('should parse block tag with Liquid children', () => { - const ast = toLiquidHtmlAST('{% capture x %}{{ y }}{% endcapture %}'); - expectPath(ast, 'children.0.children').to.have.lengthOf(1); - expectPath(ast, 'children.0.children.0.type').to.eql('LiquidVariableOutput'); - }); - - it('should have correct positions for branchless block', () => { - const source = '{% capture x %}hello{% endcapture %}'; - const ast = toLiquidHtmlAST(source); - expectPosition(ast, 'children.0').to.eql(source); - expectPosition(ast, 'children.0.children.0').to.eql('hello'); - }); - - it('should set blockStartPosition for branchless block', () => { - const source = '{% capture x %}hello{% endcapture %}'; - const ast = toLiquidHtmlAST(source); - expectBlockStartPosition(ast, 'children.0').to.eql('{% capture x %}'); - }); - - it('should set blockEndPosition for branchless block', () => { - const source = '{% capture x %}hello{% endcapture %}'; - const ast = toLiquidHtmlAST(source); - expectBlockEndPosition(ast, 'children.0').to.eql('{% endcapture %}'); - }); - - it('should set delimiterWhitespace for end tag', () => { - const source = '{% capture x %}hello{%- endcapture -%}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.delimiterWhitespaceStart').to.eql('-'); - expectPath(ast, 'children.0.delimiterWhitespaceEnd').to.eql('-'); - }); - - it('should have undefined delimiterWhitespace when no stripping', () => { - const source = '{% capture x %}hello{% endcapture %}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.delimiterWhitespaceStart').to.eql(''); - expectPath(ast, 'children.0.delimiterWhitespaceEnd').to.eql(''); - }); - - it('should fall back to base case for invalid markup (tolerant)', () => { - const ast = toLiquidHtmlAST('{% capture x y z %}hello{% endcapture %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('capture'); - expectPath(ast, 'children.0.markup').to.eql('x y z'); - expectPath(ast, 'children.0.children').to.have.lengthOf(1); - }); - - it('should parse HTML elements inside block children', () => { - const source = '{% capture x %}
hello
{% endcapture %}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.children').to.have.lengthOf(1); - expectPath(ast, 'children.0.children.0.type').to.eql('HtmlElement'); - expectPath(ast, 'children.0.children.0.name.0.value').to.eql('div'); - }); - }); - - describe('branched blocks (branches.length > 0)', () => { - it('should wrap children in LiquidBranch for {% if %}', () => { - const ast = toLiquidHtmlAST('{% if true %}hello{% endif %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('if'); - expectPath(ast, 'children.0.children').to.have.lengthOf(1); - expectPath(ast, 'children.0.children.0.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.children.0.markup').to.eql(''); - expectPath(ast, 'children.0.children.0.children').to.have.lengthOf(1); - expectPath(ast, 'children.0.children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.children.0.value').to.eql('hello'); - }); - - it('should parse {% if %}...{% else %}...{% endif %}', () => { - const ast = toLiquidHtmlAST('{% if true %}a{% else %}b{% endif %}'); - expectPath(ast, 'children.0.children').to.have.lengthOf(2); - // First branch (unnamed) - expectPath(ast, 'children.0.children.0.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.children.0.children.0.value').to.eql('a'); - // Second branch (else) - expectPath(ast, 'children.0.children.1.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.children.1.name').to.eql('else'); - expectPath(ast, 'children.0.children.1.markup').to.eql(''); - expectPath(ast, 'children.0.children.1.children.0.value').to.eql('b'); - }); - - it('should parse {% if %}...{% elsif %}...{% else %}...{% endif %}', () => { - const ast = toLiquidHtmlAST('{% if a %}1{% elsif b %}2{% else %}3{% endif %}'); - expectPath(ast, 'children.0.children').to.have.lengthOf(3); - // Unnamed branch - expectPath(ast, 'children.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.children.0.children.0.value').to.eql('1'); - // elsif branch — parsed markup (conditionalExpression returns VariableLookup for simple var) - expectPath(ast, 'children.0.children.1.name').to.eql('elsif'); - expectPath(ast, 'children.0.children.1.markup.type').to.eql('VariableLookup'); - expectPath(ast, 'children.0.children.1.children.0.value').to.eql('2'); - // else branch - expectPath(ast, 'children.0.children.2.name').to.eql('else'); - expectPath(ast, 'children.0.children.2.markup').to.eql(''); - expectPath(ast, 'children.0.children.2.children.0.value').to.eql('3'); - }); - - it('should parse {% case %}...{% when %}...{% else %}...{% endcase %}', () => { - const ast = toLiquidHtmlAST( - '{% case x %}{% when 1 %}one{% when 2 %}two{% else %}other{% endcase %}', - ); - expectPath(ast, 'children.0.name').to.eql('case'); - expectPath(ast, 'children.0.children').to.have.lengthOf(4); - // Unnamed (empty) - expectPath(ast, 'children.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.children.0.children').to.have.lengthOf(0); - // when 1 - expectPath(ast, 'children.0.children.1.name').to.eql('when'); - expectPath(ast, 'children.0.children.1.children.0.value').to.eql('one'); - // when 2 - expectPath(ast, 'children.0.children.2.name').to.eql('when'); - expectPath(ast, 'children.0.children.2.children.0.value').to.eql('two'); - // else - expectPath(ast, 'children.0.children.3.name').to.eql('else'); - expectPath(ast, 'children.0.children.3.children.0.value').to.eql('other'); - }); - - it('should parse {% for %}...{% else %}...{% endfor %}', () => { - const ast = toLiquidHtmlAST('{% for item in items %}{{ item }}{% else %}empty{% endfor %}'); - expectPath(ast, 'children.0.name').to.eql('for'); - expectPath(ast, 'children.0.markup.type').to.eql('ForMarkup'); - expectPath(ast, 'children.0.children').to.have.lengthOf(2); - // Unnamed branch - expectPath(ast, 'children.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.children.0.children.0.type').to.eql('LiquidVariableOutput'); - // else branch - expectPath(ast, 'children.0.children.1.name').to.eql('else'); - expectPath(ast, 'children.0.children.1.children.0.value').to.eql('empty'); - }); - - it('should parse empty branched block', () => { - const ast = toLiquidHtmlAST('{% if true %}{% endif %}'); - expectPath(ast, 'children.0.children').to.have.lengthOf(1); - expectPath(ast, 'children.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.children.0.children').to.have.lengthOf(0); - }); - - it('should parse {% when %} with multiple values', () => { - const ast = toLiquidHtmlAST('{% case x %}{% when 1, 2 %}matched{% endcase %}'); - expectPath(ast, 'children.0.children.1.name').to.eql('when'); - // markup is an array of expressions - expectPath(ast, 'children.0.children.1.markup').to.have.lengthOf(2); - }); - - it('should expose branch markup positions', () => { - const source = '{% case x %}{% when 1, 2 %}matched{% else %}other{% endcase %}'; - const ast = toLiquidHtmlAST(source); - const block = ast.children[0] as any; - const unnamed = block.children[0]; - const when = block.children[1]; - const elseBranch = block.children[2]; - - expect(sourceAt(source, unnamed.markupPosition)).to.eql(''); - expect(sourceAt(source, when.markupPosition)).to.eql('1, 2 '); - expect(sourceAt(source, elseBranch.markupPosition)).to.eql(''); - }); - - it('should expose branch markup positions with whitespace-control delimiters', () => { - const source = '{% if a %}A{%- elsif b -%}B{% endif %}'; - const ast = toLiquidHtmlAST(source); - const block = ast.children[0] as any; - const branch = block.children[1]; - - expect(sourceAt(source, branch.markupPosition)).to.eql('b '); - }); - }); - - describe('nested blocks', () => { - it('should parse nested block tags', () => { - const ast = toLiquidHtmlAST( - '{% if a %}{% for item in items %}{{ item }}{% endfor %}{% endif %}', - ); - expectPath(ast, 'children.0.name').to.eql('if'); - expectPath(ast, 'children.0.children.0.children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.children.0.children.0.name').to.eql('for'); - expectPath(ast, 'children.0.children.0.children.0.children.0.children.0.type').to.eql( - 'LiquidVariableOutput', - ); - }); - - it('should parse nested branchless in branched', () => { - const ast = toLiquidHtmlAST('{% if true %}{% capture x %}hello{% endcapture %}{% endif %}'); - expectPath(ast, 'children.0.children.0.children.0.name').to.eql('capture'); - expectPath(ast, 'children.0.children.0.children.0.children.0.value').to.eql('hello'); - }); - }); - - describe('positions', () => { - it('should have correct position for branched block tag', () => { - const source = '{% if true %}hello{% endif %}'; - const ast = toLiquidHtmlAST(source); - expectPosition(ast, 'children.0').to.eql(source); - }); - - it('should set blockEndPosition for branched block', () => { - const source = '{% if true %}hello{% endif %}'; - const ast = toLiquidHtmlAST(source); - expectBlockEndPosition(ast, 'children.0').to.eql('{% endif %}'); - }); - - it('should set unnamed branch positions correctly', () => { - const source = '{% if true %}hello{% endif %}'; - const ast = toLiquidHtmlAST(source); - const branch = (ast.children[0] as any).children[0]; - // blockStartPosition is zero-width at end of open tag - expect(branch.blockStartPosition.start).to.eql(branch.blockStartPosition.end); - expect(branch.blockStartPosition.start).to.eql('{% if true %}'.length); - // blockEndPosition is zero-width at start of end tag - expect(branch.blockEndPosition.start).to.eql(branch.blockEndPosition.end); - expect(branch.blockEndPosition.start).to.eql('{% if true %}hello'.length); - }); - - it('should set named branch positions correctly', () => { - const source = '{% if true %}a{% else %}b{% endif %}'; - const ast = toLiquidHtmlAST(source); - // blockStartPosition spans the branch tag - expectBlockStartPosition(ast, 'children.0.children.1').to.eql('{% else %}'); - // blockEndPosition is zero-width at start of end tag - const elseBranch = (ast.children[0] as any).children[1]; - expect(elseBranch.blockEndPosition.start).to.eql(elseBranch.blockEndPosition.end); - }); - - it('should set position.end of branch to end of last child', () => { - const source = '{% if true %}hello{% endif %}'; - const ast = toLiquidHtmlAST(source); - const branch = (ast.children[0] as any).children[0]; - // position.end should be end of 'hello' text node - expect(branch.position.end).to.eql('{% if true %}hello'.length); - }); - - it('should set position of empty unnamed branch to bodyStart', () => { - const source = '{% if true %}{% endif %}'; - const ast = toLiquidHtmlAST(source); - const branch = (ast.children[0] as any).children[0]; - expect(branch.position.start).to.eql('{% if true %}'.length); - expect(branch.position.end).to.eql('{% if true %}'.length); - }); - }); - - describe('whitespace stripping', () => { - it('should handle whitespace stripping on open and end tags', () => { - const ast = toLiquidHtmlAST('{%- if true -%}hello{%- endif -%}'); - expectPath(ast, 'children.0.whitespaceStart').to.eql('-'); - expectPath(ast, 'children.0.whitespaceEnd').to.eql('-'); - expectPath(ast, 'children.0.delimiterWhitespaceStart').to.eql('-'); - expectPath(ast, 'children.0.delimiterWhitespaceEnd').to.eql('-'); - }); - - it('should handle whitespace stripping on branch tags', () => { - const ast = toLiquidHtmlAST('{% if true %}a{%- else -%}b{% endif %}'); - expectPath(ast, 'children.0.children.1.whitespaceStart').to.eql('-'); - expectPath(ast, 'children.0.children.1.whitespaceEnd').to.eql('-'); - }); - }); - - describe('error handling', () => { - it('should throw for unclosed block tag', () => { - expect(() => toLiquidHtmlAST('{% if true %}hello')).to.throw(/before.*if.*was closed/i); - }); - - it('should throw for orphaned end tag at top level', () => { - expect(() => toLiquidHtmlAST('{% endif %}')).to.throw( - /Attempting to close LiquidTag 'if' before it was opened/, - ); - }); - - it('should throw for orphaned endfor at top level', () => { - expect(() => toLiquidHtmlAST('{% endfor %}')).to.throw( - /Attempting to close LiquidTag 'for' before it was opened/, - ); - }); - }); - - describe('parity: toLiquidAST matches toLiquidHtmlAST', () => { - it('should produce same structure for branchless block', () => { - const source = '{% capture x %}hello{% endcapture %}'; - const htmlAst = toLiquidHtmlAST(source); - const liquidAst = toLiquidAST(source); - expectPath(htmlAst, 'children.0.name').to.eql('capture'); - expectPath(liquidAst, 'children.0.name').to.eql('capture'); - expectPath(htmlAst, 'children.0.children.0.value').to.eql('hello'); - expectPath(liquidAst, 'children.0.children.0.value').to.eql('hello'); - }); - - it('should produce same structure for branched block', () => { - const source = '{% if true %}a{% else %}b{% endif %}'; - const htmlAst = toLiquidHtmlAST(source); - const liquidAst = toLiquidAST(source); - expectPath(htmlAst, 'children.0.children').to.have.lengthOf(2); - expectPath(liquidAst, 'children.0.children').to.have.lengthOf(2); - expectPath(htmlAst, 'children.0.children.1.name').to.eql('else'); - expectPath(liquidAst, 'children.0.children.1.name').to.eql('else'); - }); - }); - - describe('branchless blocks in HTML attribute context', () => { - it('should produce AttrEmpty for bare text inside capture in attribute position', () => { - const source = ''; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.type').to.eql('HtmlVoidElement'); - expectPath(ast, 'children.0.attributes.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.attributes.0.name').to.eql('capture'); - expectPath(ast, 'children.0.attributes.0.children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.attributes.0.children.1.type').to.eql('AttrEmpty'); - expectPath(ast, 'children.0.attributes.0.children.1.name.0.value').to.eql('%'); - }); - }); - describe('malformed block bodies terminate instead of looping forever', () => { - // Regression: an orphan `{% end* %}` naming a branch keyword (e.g. `endwhen`) - // inside a block parsed in attribute context used to deadlock — the block - // loop wouldn't consume it and the attribute parser treated it as a - // terminator, so neither advanced the cursor. It must now throw cleanly. - it('should throw (not hang) on an orphan endwhen inside a case in attribute position', () => { - expect(() => - toLiquidHtmlAST(`
  • z
  • `), - ).to.throw(/Unexpected Liquid tag 'endwhen'/); - }); - - it('should throw (not hang) on an orphan end tag inside a branchless block', () => { - expect(() => - toLiquidHtmlAST(`
  • z
  • `), - ).to.throw(/Unexpected Liquid tag 'endwhen'/); - }); - - it('tolerant mode (toLiquidAST) still degrades without looping', () => { - expect(() => - toLiquidAST(`
  • z
  • `), - ).not.to.throw(); - }); - // Regression: the real tolerant path is HTML-mode (`toLiquidHtmlAST` with // allowUnclosedDocumentNode), used by liquid-render-tree's parse-html-fragment. // There the orphan `{% endwhen %}` is consumed as a degraded node so the loop reaches diff --git a/packages/liquid-html-parser/src/document/liquid-lines.test.ts b/packages/liquid-html-parser/src/document/liquid-lines.test.ts deleted file mode 100644 index e5c1729cb..000000000 --- a/packages/liquid-html-parser/src/document/liquid-lines.test.ts +++ /dev/null @@ -1,266 +0,0 @@ -import { describe, it } from 'vitest'; -import { toLiquidHtmlAST } from '../ast'; -import { expectPath, expectPosition, expectBlockEndPosition } from './test-helpers'; - -describe('Unit: liquid-lines', () => { - it('should parse an empty liquid tag', () => { - const ast = toLiquidHtmlAST('{% liquid %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('liquid'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(0); - }); - - it('should parse a single echo statement', () => { - const ast = toLiquidHtmlAST('{% liquid\necho "hi"\n%}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('liquid'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('echo'); - expectPath(ast, 'children.0.markup.0.markup.type').to.eql('LiquidVariable'); - }); - - it('should parse multiple statements', () => { - const ast = toLiquidHtmlAST('{% liquid\nassign x = 1\necho x\n%}'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(2); - expectPath(ast, 'children.0.markup.0.name').to.eql('assign'); - expectPath(ast, 'children.0.markup.1.name').to.eql('echo'); - }); - - it('should parse a block tag (if/endif)', () => { - const ast = toLiquidHtmlAST('{% liquid\nif true\necho "yes"\nendif\n%}'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('if'); - // if has branches; first (unnamed) branch has echo child - expectPath(ast, 'children.0.markup.0.children').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.children.0.type').to.eql('LiquidBranch'); - expectPath(ast, 'children.0.markup.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.markup.0.children.0.children').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.children.0.children.0.name').to.eql('echo'); - }); - - it('should parse branched block (if/elsif/else)', () => { - const ast = toLiquidHtmlAST( - '{% liquid\nif a\necho "1"\nelsif b\necho "2"\nelse\necho "3"\nendif\n%}', - ); - expectPath(ast, 'children.0.markup.0.name').to.eql('if'); - expectPath(ast, 'children.0.markup.0.children').to.have.lengthOf(3); - expectPath(ast, 'children.0.markup.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.markup.0.children.0.children.0.name').to.eql('echo'); - expectPath(ast, 'children.0.markup.0.children.1.name').to.eql('elsif'); - expectPath(ast, 'children.0.markup.0.children.1.children.0.name').to.eql('echo'); - expectPath(ast, 'children.0.markup.0.children.2.name').to.eql('else'); - expectPath(ast, 'children.0.markup.0.children.2.children.0.name').to.eql('echo'); - }); - - it('should parse case/when', () => { - const ast = toLiquidHtmlAST( - '{% liquid\ncase x\nwhen 1\necho "one"\nwhen 2\necho "two"\nendcase\n%}', - ); - expectPath(ast, 'children.0.markup.0.name').to.eql('case'); - expectPath(ast, 'children.0.markup.0.children').to.have.lengthOf(3); - // First branch is unnamed (before first when) - expectPath(ast, 'children.0.markup.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.markup.0.children.1.name').to.eql('when'); - expectPath(ast, 'children.0.markup.0.children.1.children.0.name').to.eql('echo'); - expectPath(ast, 'children.0.markup.0.children.2.name').to.eql('when'); - expectPath(ast, 'children.0.markup.0.children.2.children.0.name').to.eql('echo'); - }); - - it('should parse for/else', () => { - const ast = toLiquidHtmlAST( - '{% liquid\nfor item in items\necho item\nelse\necho "empty"\nendfor\n%}', - ); - expectPath(ast, 'children.0.markup.0.name').to.eql('for'); - expectPath(ast, 'children.0.markup.0.children').to.have.lengthOf(2); - expectPath(ast, 'children.0.markup.0.children.0.name').to.eql(null); - expectPath(ast, 'children.0.markup.0.children.0.children.0.name').to.eql('echo'); - expectPath(ast, 'children.0.markup.0.children.1.name').to.eql('else'); - expectPath(ast, 'children.0.markup.0.children.1.children.0.name').to.eql('echo'); - }); - - it('should parse raw tag (comment)', () => { - const ast = toLiquidHtmlAST('{% liquid\ncomment\nthis is ignored\nendcomment\n%}'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('comment'); - expectPath(ast, 'children.0.markup.0.body.type').to.eql('RawMarkup'); - expectPath(ast, 'children.0.markup.0.body.value').to.eql('this is ignored\n'); - }); - - it('should set RawMarkup start position to lineEnd, not lineEnd+1 (Bug 47 regression)', () => { - // Source: {% liquid\ncomment\nbody text\nendcomment\n%} - // Offsets: 0 9 10 17 18 27 28 37 38 40 - // ^{%liquid ^comment ^body text ^endcomment ^%} - // lineEnd for 'comment' line = 17 (the \n after 'comment') - // RawMarkup.position.start should be 17 (lineEnd), not 18 (lineEnd + 1) - const source = '{% liquid\ncomment\nbody text\nendcomment\n%}'; - const ast = toLiquidHtmlAST(source); - const body = 'children.0.markup.0.body'; - expectPath(ast, `${body}.type`).to.eql('RawMarkup'); - expectPath(ast, `${body}.position.start`).to.eql(17); - expectPath(ast, `${body}.position.end`).to.eql(28); - expectPath(ast, `${body}.value`).to.eql('body text\n'); - }); - - it('should parse inline comment (#)', () => { - const ast = toLiquidHtmlAST('{% liquid\n# a comment\necho "hi"\n%}'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(2); - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('#'); - expectPath(ast, 'children.0.markup.1.name').to.eql('echo'); - }); - - it('should preserve inner indentation after `#`, stripping only one separator space (Category J)', () => { - // `# fancy` must keep four spaces of the ASCII-art indent — only a - // single separator space after `#` is stripped (ohm `"#" space?`). - const ast = toLiquidHtmlAST('{% liquid\n# fancy\necho "hi"\n%}'); - expectPath(ast, 'children.0.markup.0.name').to.eql('#'); - expectPath(ast, 'children.0.markup.0.markup').to.eql(' fancy'); - }); - - it('should skip empty lines', () => { - const ast = toLiquidHtmlAST('{% liquid\n\necho "hi"\n\n%}'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.name').to.eql('echo'); - }); - - it('should parse unknown tag as base case', () => { - const ast = toLiquidHtmlAST('{% liquid\nunknown_tag foo\n%}'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('unknown_tag'); - expectPath(ast, 'children.0.markup.0.markup').to.eql('foo'); - }); - - it('should parse nested blocks', () => { - const ast = toLiquidHtmlAST( - '{% liquid\nfor item in items\nif item\necho item\nendif\nendfor\n%}', - ); - expectPath(ast, 'children.0.markup.0.name').to.eql('for'); - // for has unnamed branch - const forBranch = 'children.0.markup.0.children.0'; - expectPath(ast, `${forBranch}.children`).to.have.lengthOf(1); - expectPath(ast, `${forBranch}.children.0.name`).to.eql('if'); - // if has unnamed branch with echo - expectPath(ast, `${forBranch}.children.0.children.0.children.0.name`).to.eql('echo'); - }); - - it('should have document-relative positions', () => { - const source = '{% liquid\necho "hi"\n%}'; - const ast = toLiquidHtmlAST(source); - // The liquid tag itself spans the whole thing - expectPosition(ast, 'children.0').to.eql(source); - // The echo statement inside: "echo" starts at offset 10 (after "{% liquid\n") - expectPath(ast, 'children.0.markup.0.position.start').to.eql(10); - }); - - it('should fall back to base case for bad markup (tolerant)', () => { - const ast = toLiquidHtmlAST('{% liquid\nassign !bad\n%}'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('assign'); - // markup should be string (base case) not parsed AssignMarkup - expectPath(ast, 'children.0.markup.0.markup').to.be.a('string'); - }); - - it('should parse multiple blocks at same level', () => { - const ast = toLiquidHtmlAST('{% liquid\nif a\necho "1"\nendif\nif b\necho "2"\nendif\n%}'); - expectPath(ast, 'children.0.markup').to.have.lengthOf(2); - expectPath(ast, 'children.0.markup.0.name').to.eql('if'); - expectPath(ast, 'children.0.markup.1.name').to.eql('if'); - }); - - describe('blockEndPosition spans end keyword (Bug 12)', () => { - it('should have non-zero-width blockEndPosition for block tags (if/endif)', () => { - const source = '{% liquid\nif true\necho "yes"\nendif\n%}'; - const ast = toLiquidHtmlAST(source); - const ifTag = 'children.0.markup.0'; - expectPath(ast, `${ifTag}.name`).to.eql('if'); - expectBlockEndPosition(ast, ifTag).to.eql('endif'); - }); - - it('should have non-zero-width blockEndPosition for nested blocks', () => { - const source = '{% liquid\nfor item in items\nif item\necho item\nendif\nendfor\n%}'; - const ast = toLiquidHtmlAST(source); - const forTag = 'children.0.markup.0'; - expectPath(ast, `${forTag}.name`).to.eql('for'); - expectBlockEndPosition(ast, forTag).to.eql('endfor'); - const ifTag = `${forTag}.children.0.children.0`; - expectPath(ast, `${ifTag}.name`).to.eql('if'); - expectBlockEndPosition(ast, ifTag).to.eql('endif'); - }); - - it('should have non-zero-width blockEndPosition for branched blocks', () => { - const source = '{% liquid\nif a\necho "1"\nelsif b\necho "2"\nelse\necho "3"\nendif\n%}'; - const ast = toLiquidHtmlAST(source); - const ifTag = 'children.0.markup.0'; - expectPath(ast, `${ifTag}.name`).to.eql('if'); - expectBlockEndPosition(ast, ifTag).to.eql('endif'); - }); - - it('should have non-zero-width blockEndPosition for case/endcase', () => { - const source = '{% liquid\ncase x\nwhen 1\necho "one"\nendcase\n%}'; - const ast = toLiquidHtmlAST(source); - const caseTag = 'children.0.markup.0'; - expectPath(ast, `${caseTag}.name`).to.eql('case'); - expectBlockEndPosition(ast, caseTag).to.eql('endcase'); - }); - - it('should have non-zero-width blockEndPosition for raw tags (comment)', () => { - const source = '{% liquid\ncomment\nignored\nendcomment\n%}'; - const ast = toLiquidHtmlAST(source); - const commentTag = 'children.0.markup.0'; - expectPath(ast, `${commentTag}.name`).to.eql('comment'); - expectBlockEndPosition(ast, commentTag).to.eql('endcomment'); - }); - - it('should have correct position.end for tags inside liquid blocks', () => { - const source = '{% liquid\nif true\necho "yes"\nendif\n%}'; - const ast = toLiquidHtmlAST(source); - const ifTag = 'children.0.markup.0'; - // position.end should equal blockEndPosition.end (both span through endif keyword) - expectPosition(ast, ifTag).to.eql(source.slice(10, 10 + 'if true\necho "yes"\nendif'.length)); - }); - - it('should have zero-width blockEndPosition on branches (not tags)', () => { - const source = '{% liquid\nif true\necho "yes"\nendif\n%}'; - const ast = toLiquidHtmlAST(source); - const branch = 'children.0.markup.0.children.0'; - expectPath(ast, `${branch}.type`).to.eql('LiquidBranch'); - // Branch blockEndPosition should be zero-width (start === end) - expectPath(ast, `${branch}.blockEndPosition.start`).to.eql(source.indexOf('endif')); - expectPath(ast, `${branch}.blockEndPosition.end`).to.eql(source.indexOf('endif')); - }); - }); - - describe('nested comment/doc balancing (Category B)', () => { - it('should balance nested comments so the outer endcomment closes the block', () => { - // The inner `comment`/`endcomment` pair must not close the outer - // block early; the outer comment ends at the final `endcomment`. - const source = '{% liquid\ncomment\nouter\ncomment\ninner\nendcomment\nendcomment\n%}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.markup').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('comment'); - expectPath(ast, 'children.0.markup.0.body.value').to.eql( - 'outer\ncomment\ninner\nendcomment\n', - ); - }); - - it('should carve out a nested raw so a literal endcomment inside it does not close early', () => { - // The `endcomment` line sits inside a nested `raw`…`endraw` block, so it - // must be ignored by the depth scan; the outer comment closes at the - // real trailing `endcomment`. - const source = '{% liquid\ncomment\nbefore\nraw\nendcomment\nendraw\nafter\nendcomment\n%}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.markup').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.markup.0.name').to.eql('comment'); - expectPath(ast, 'children.0.markup.0.body.value').to.eql( - 'before\nraw\nendcomment\nendraw\nafter\n', - ); - }); - }); -}); diff --git a/packages/liquid-html-parser/src/document/liquid-raw.test.ts b/packages/liquid-html-parser/src/document/liquid-raw.test.ts deleted file mode 100644 index da2bfa5f3..000000000 --- a/packages/liquid-html-parser/src/document/liquid-raw.test.ts +++ /dev/null @@ -1,396 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { toLiquidHtmlAST, toLiquidAST, RawMarkupKinds } from '../ast'; -import { - expectPath, - expectPosition, - expectBlockStartPosition, - expectBlockEndPosition, - sourceAt, -} from './test-helpers'; - -describe('Unit: liquid-raw', () => { - it('should parse basic raw tag', () => { - const ast = toLiquidHtmlAST('{% raw %}hello world{% endraw %}'); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('raw'); - expectPath(ast, 'children.0.markup').to.eql(''); - expectPath(ast, 'children.0.body.type').to.eql('RawMarkup'); - expectPath(ast, 'children.0.body.value').to.eql('hello world'); - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(1); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.0.value').to.eql('hello world'); - expectPath(ast, 'children.0.body.kind').to.eql(RawMarkupKinds.text); - }); - - it('should parse comment tag', () => { - const ast = toLiquidHtmlAST('{% comment %}this is a comment{% endcomment %}'); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('comment'); - expectPath(ast, 'children.0.body.value').to.eql('this is a comment'); - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(1); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.0.value').to.eql('this is a comment'); - expectPath(ast, 'children.0.body.kind').to.eql(RawMarkupKinds.text); - }); - - it('should parse schema tag', () => { - const ast = toLiquidHtmlAST('{% schema %}{"key": "value"}{% endschema %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('schema'); - expectPath(ast, 'children.0.body.value').to.eql('{"key": "value"}'); - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(1); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.0.value').to.eql('{"key": "value"}'); - expectPath(ast, 'children.0.body.kind').to.eql(RawMarkupKinds.json); - }); - - it('should parse style tag with plain body', () => { - const ast = toLiquidHtmlAST('{% style %}.foo { color: red }{% endstyle %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('style'); - expectPath(ast, 'children.0.body.kind').to.eql(RawMarkupKinds.css); - expectPath(ast, 'children.0.body.value').to.eql('.foo { color: red }'); - }); - - it('should parse style tag with Liquid in body', () => { - const ast = toLiquidHtmlAST('{% style %}.foo { color: {{ color }} }{% endstyle %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('style'); - expectPath(ast, 'children.0.body.kind').to.eql(RawMarkupKinds.text); - expectPath(ast, 'children.0.body.value').to.eql('.foo { color: {{ color }} }'); - // nodes should contain TextNode and LiquidVariableOutput interleaved - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(3); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.0.value').to.eql('.foo { color:'); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.body.nodes.2.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.2.value').to.eql('}'); - }); - - it('should parse javascript tag with Liquid in body', () => { - const ast = toLiquidHtmlAST('{% javascript %}var x = {{ value }};{% endjavascript %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('javascript'); - expectPath(ast, 'children.0.body.kind').to.eql(RawMarkupKinds.javascript); - expectPath(ast, 'children.0.body.value').to.eql('var x = {{ value }};'); - expectPath(ast, 'children.0.body.nodes').to.have.lengthOf(3); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.0.value').to.eql('var x ='); - expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.body.nodes.2.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.2.value').to.eql(';'); - }); - - it('should parse empty doc tag', () => { - const ast = toLiquidAST('{% doc %}{% enddoc %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.body.value').to.eql(''); - expectPath(ast, 'children.0.body.nodes').to.eql([]); - expectPath(ast, 'children.0.body.kind').to.eql(RawMarkupKinds.text); - }); - - it('should parse doc tag with param content', () => { - const ast = toLiquidAST('{% doc %}@param {String} name - description{% enddoc %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode'); - expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('name'); - }); - - it('should handle whitespace stripping on open and close', () => { - const ast = toLiquidHtmlAST('{%- raw -%}hello{%- endraw -%}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.whitespaceStart').to.eql('-'); - expectPath(ast, 'children.0.whitespaceEnd').to.eql('-'); - expectPath(ast, 'children.0.delimiterWhitespaceStart').to.eql('-'); - expectPath(ast, 'children.0.delimiterWhitespaceEnd').to.eql('-'); - expectPath(ast, 'children.0.body.value').to.eql('hello'); - }); - - it('should track raw tag markup position', () => { - const source = '{% raw foo %}{% endraw %}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.markup').to.eql('foo'); - expect(sourceAt(source, (ast.children[0] as any).markupPosition)).to.eql('foo'); - }); - - it('should track whitespace-stripped raw tag markup position', () => { - const source = '{%- raw foo -%}{%- endraw -%}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.markup').to.eql('foo'); - expect(sourceAt(source, (ast.children[0] as any).markupPosition)).to.eql('foo'); - }); - - it('should track positions correctly with prefix', () => { - const source = 'XXXXX{% raw %}hello{% endraw %}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children').to.have.lengthOf(2); - // TextNode for "XXXXX" - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql('XXXXX'); - // LiquidRawTag - expectBlockStartPosition(ast, 'children.1').to.eql('{% raw %}'); - expectBlockEndPosition(ast, 'children.1').to.eql('{% endraw %}'); - expectPosition(ast, 'children.1.body').to.eql('hello'); - expectPosition(ast, 'children.1').to.eql('{% raw %}hello{% endraw %}'); - }); - - it('should map RawMarkupKinds correctly for each tag name', () => { - const cases: [string, RawMarkupKinds][] = [ - ['raw', RawMarkupKinds.text], - ['comment', RawMarkupKinds.text], - ['schema', RawMarkupKinds.json], - ['javascript', RawMarkupKinds.javascript], - ['style', RawMarkupKinds.css], - ['stylesheet', RawMarkupKinds.css], - ['doc', RawMarkupKinds.text], - ]; - for (const [tagName, expectedKind] of cases) { - const ast = toLiquidAST(`{% ${tagName} %}body{% end${tagName} %}`); - expectPath(ast, 'children.0.body.kind').to.eql(expectedKind); - } - }); - - it('should parse empty body', () => { - const ast = toLiquidHtmlAST('{% raw %}{% endraw %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.body.value').to.eql(''); - expectPath(ast, 'children.0.body.nodes').to.eql([]); - }); - - it('should preserve newlines in multiline body', () => { - const ast = toLiquidHtmlAST('{% comment %}\nline1\nline2\n{% endcomment %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.body.value').to.eql('\nline1\nline2\n'); - }); - - it('should throw for unclosed raw tag', () => { - expect(() => toLiquidHtmlAST('{% raw %}hello')).to.throw(); - }); - - it('should work with toLiquidAST', () => { - const ast = toLiquidAST('{% raw %}hello{% endraw %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('raw'); - expectPath(ast, 'children.0.body.value').to.eql('hello'); - }); - - it('should parse stylesheet tag', () => { - const ast = toLiquidHtmlAST('{% stylesheet %}.cls { }{% endstylesheet %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('stylesheet'); - expectPath(ast, 'children.0.body.kind').to.eql(RawMarkupKinds.css); - }); - - it('should parse doc tag whose body contains liquid-like syntax', () => { - // Regression: the doc body contains {% render ... } (note: `}` not `%}`) - // which caused the tokenizer to enter LiquidTag mode and swallow the - // {%- enddoc -%} close tag into a mangled token. - const source = [ - '{%- doc -%}', - ' @param {object} price - The price.', - ' @example', - " {% render 'unit-price', price: variant.unit_price }", - '{%- enddoc -%}', - 'after', - ].join('\n'); - const ast = toLiquidAST(source); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.whitespaceStart').to.eql('-'); - expectPath(ast, 'children.0.whitespaceEnd').to.eql('-'); - expectPath(ast, 'children.0.delimiterWhitespaceStart').to.eql('-'); - expectPath(ast, 'children.0.delimiterWhitespaceEnd').to.eql('-'); - // Body should contain the full text between doc and enddoc - expect(ast.children[0]).to.have.property('body'); - const body = (ast.children[0] as any).body; - expect(body.value).to.contain('@param'); - expect(body.value).to.contain("{% render 'unit-price'"); - // "after" text should be a sibling, not swallowed into the doc body - expectPath(ast, 'children.1.type').to.eql('TextNode'); - expectPath(ast, 'children.1.value').to.contain('after'); - }); - - it('should parse style tag with Liquid tag in body', () => { - const ast = toLiquidHtmlAST('{% style %}{% if true %}.active{}{% endif %}{% endstyle %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('style'); - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.body.nodes.0.name').to.eql('if'); - }); - - it('preserves output after endraw when an unclosed {{ in the body straddles the boundary', () => { - // Regression (#2895): the unclosed `{{ invalid` inside the raw body opens a - // LiquidVariableOutput token in the tokenizer that greedily runs past the - // {% endraw %} to the next `}}`, so the straddling token used to swallow - // the real trailing `{{ 1 }}`. After the fix the parser re-tokenizes from - // the end-tag boundary, so the trailing output survives as its own node. - const source = '{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children').to.have.lengthOf(2); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.body.value').to.eql(' Foobar {{ invalid '); - expectPath(ast, 'children.1.type').to.eql('LiquidVariableOutput'); - expectPosition(ast, 'children.1').to.eql('{{ 1 }}'); - }); - - it('preserves plain text after endraw when the raw body contains a stray {{', () => { - // Companion to #2895: a stray `{{` with no following `}}` after endraw must - // not consume the post-endraw text either. - const source = '{% raw %}{{ a {% endraw %}tail'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.body.value').to.eql('{{ a '); - expectPath(ast, 'children.1.type').to.eql('TextNode'); - expectPath(ast, 'children.1.value').to.eql('tail'); - }); - - it('re-tokenizes a straddling endraw inside a quoted attribute without losing quote context', () => { - // Regression: when a raw tag inside a quoted HTML attribute has a stray `{{` - // in its body, the `}}` close straddles the {% endraw %} boundary. The - // boundary reslice used to re-tokenize the suffix from document-start mode, - // so the attribute's closing `"` and `>` became Text instead of - // HtmlQuoteClose/HtmlTagClose and attribute parsing threw - // "Expected HtmlQuoteClose". The reslice now resumes in QuotedValue mode, so - // the attribute closes cleanly and the element body parses normally. - const source = '
    x
    '; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.type').to.eql('HtmlElement'); - expectPath(ast, 'children.0.name.0.value').to.eql('div'); - expectPath(ast, 'children.0.attributes.0.type').to.eql('AttrDoubleQuoted'); - expectPath(ast, 'children.0.attributes.0.name.0.value').to.eql('data'); - expectPath(ast, 'children.0.attributes.0.value.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.value').to.eql('x'); - }); - - it('handles a straddling endraw inside a single-quoted attribute', () => { - // Companion to the double-quoted case: the recovered quote char must match - // whatever opened the attribute, so single quotes close on `'` too. - const source = "
    y
    "; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.type').to.eql('HtmlElement'); - expectPath(ast, 'children.0.attributes.0.type').to.eql('AttrSingleQuoted'); - expectPath(ast, 'children.0.attributes.0.value.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.children.0.value').to.eql('y'); - }); - - it('recovers HtmlTag context for a straddling endraw in an unquoted attribute position', () => { - // Regression: a stray `{{` in the raw body opens LiquidVariableOutput mode - // that runs past {% endraw %} to the next `}}`, straddling the boundary. The - // suffix reslice used to re-tokenize from Default mode, where `>` stays Text - // (only HtmlTag mode emits HtmlTagClose), so the attribute list never broke - // on the tag close, consumed ``'s `>`, and parseHtmlElement threw at - // EOF with the element still open. The reslice now resumes in HtmlTag mode, - // so the element closes on its real `>` and its body parses normally. - const source = '
    ok
    '; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.type').to.eql('HtmlElement'); - expectPath(ast, 'children.0.name.0.value').to.eql('div'); - expectPath(ast, 'children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.value').to.eql('ok'); - }); - - it('does not duplicate a straddling {{ into both a Liquid-in-body raw body and a sibling', () => { - // Regression: for parseLiquidInBody raw tags (style/javascript/stylesheet) - // the body is parsed before the boundary reslice. A stray `{{` whose `}}` - // lands past {% endstyle %} used to be consumed into the body (a node - // spanning past the end tag) AND re-emitted as a post-tag sibling — the same - // source range in two overlapping nodes. The body parse now clamps a - // construct that does not close within the body to literal text, so - // `body.nodes` matches `body.value` and only the sibling carries `{{ 1 }}`. - const source = '{% style %}{{ invalid {% endstyle %}{{ 1 }}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children').to.have.lengthOf(2); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('style'); - expectPath(ast, 'children.0.body.value').to.eql('{{ invalid '); - // Body node must not extend past the end tag (no overlap with the sibling). - expectPath(ast, 'children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.1.type').to.eql('LiquidVariableOutput'); - expectPosition(ast, 'children.1').to.eql('{{ 1 }}'); - }); - - it('does not let a decoy endcommentXXXXX close the comment; only the real endcomment does', () => { - // Regression (#2855, CommentTagUnitTest#test_ignores_delimiter_with_extra_strings): - // a comment closes on the tag NAME `endcomment`, and `endcommentXXXXX` is a - // genuinely different tag name, so the decoy `{% endcommentXXXXX %}` must NOT - // terminate the raw comment body — only the real `{% endcomment %}` closes it, - // and the decoy open/close and its inner "wut" are swallowed as comment body - // text. (Note: Ruby's `FullToken` group 2 `(#|\w+)` matches the `endcomment` - // name regardless of trailing markup; the JS `scanForEndTag` `\s*%}` anchor is - // a JS-implementation detail that is stricter than Ruby, not the parity rule. - // It is irrelevant to the decoy case here, where the name itself differs.) - const source = [ - '{% if true %}', - ' {% comment %}', - ' {% commentXXXXX %}wut{% endcommentXXXXX %}', - ' {% endcomment %}', - '{% endif %}', - ].join('\n'); - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('if'); - // The comment raw tag lives in the if-branch and swallows the decoy as text. - expectPath(ast, 'children.0.children.0.children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.children.0.children.0.name').to.eql('comment'); - expectPath(ast, 'children.0.children.0.children.0.body.value').to.contain( - '{% commentXXXXX %}wut{% endcommentXXXXX %}', - ); - // The decoy is captured as a single TextNode, not parsed as a nested tag. - expectPath(ast, 'children.0.children.0.children.0.body.nodes').to.have.lengthOf(1); - expectPath(ast, 'children.0.children.0.children.0.body.nodes.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.children.0.body.nodes.0.value').to.eql( - '{% commentXXXXX %}wut{% endcommentXXXXX %}', - ); - // The real {% endcomment %} closes the comment, so the comment is the only - // child of the if-branch (the decoy did not leak siblings into the branch). - expectPath(ast, 'children.0.children.0.children').to.have.lengthOf(1); - }); - - it('parses a malformed {%comment}...{%endcomment} (no %}) into two flat base-case tags without throwing', () => { - // Parse-level documentation for #2854 (CommentTagUnitTest#test_delimiter_can_have_extra_strings). - // `{%comment}` has no closing `%}`, so the tokenizer scans to the first `%}` - // and reads `{%comment}{% assign a = 1 %}` as one base-case LiquidTag and the - // trailing `{%endcomment}{% endif %}` as another. JS splits the tag name on the - // first whitespace, so the two names are `comment}{%` and `endcomment}{%` — two - // flat sibling nodes, and the template parses without throwing. - // - // This only asserts the strict parse shape; it does NOT assert a render result. - // The spec is currently DEFERRED (see RENDER_TREE_SPEC_GAPS, #2854): the - // parity-faithful behaviour (Ruby extracts the tag name as the leading `(#|\w+)` - // run, yielding a `comment` block that swallows the rest and renders `''`) needs - // a strict-parser tag-name-extraction change in envelopeFromTokens that would - // alter the shared toLiquidHtmlAST AST consumed by theme-check (DD-7), so it is - // out of scope here. - const source = '{%comment}{% assign a = 1 %}{%endcomment}{% endif %}'; - expect(() => toLiquidHtmlAST(source)).not.to.throw(); - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children').to.have.lengthOf(2); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('comment}{%'); - expectPath(ast, 'children.1.type').to.eql('LiquidTag'); - expectPath(ast, 'children.1.name').to.eql('endcomment}{%'); - }); - - it('distinguishes a trimming endraw -%} from a non-trimming endraw %}', () => { - // Regression (#2898): the closing-delimiter trim must be recorded on - // delimiterWhitespaceEnd, independent of the opening raw -%} trim - // (whitespaceEnd). Two templates that differ only in the endraw marker - // must produce different delimiterWhitespaceEnd values. - const trimmed = toLiquidHtmlAST('> {%- raw -%} inner {%- endraw -%} <'); - expectPath(trimmed, 'children.1.type').to.eql('LiquidRawTag'); - expectPath(trimmed, 'children.1.whitespaceEnd').to.eql('-'); - expectPath(trimmed, 'children.1.delimiterWhitespaceEnd').to.eql('-'); - - const notTrimmed = toLiquidHtmlAST('> {%- raw -%} inner {%- endraw %} <'); - expectPath(notTrimmed, 'children.1.type').to.eql('LiquidRawTag'); - expectPath(notTrimmed, 'children.1.whitespaceEnd').to.eql('-'); - expectPath(notTrimmed, 'children.1.delimiterWhitespaceEnd').to.eql(''); - }); -}); diff --git a/packages/liquid-html-parser/src/document/liquid-tags.test.ts b/packages/liquid-html-parser/src/document/liquid-tags.test.ts deleted file mode 100644 index b085ee248..000000000 --- a/packages/liquid-html-parser/src/document/liquid-tags.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { toLiquidHtmlAST, toLiquidAST } from '../ast'; -import { expectPath, expectPosition, sourceAt } from './test-helpers'; - -describe('Unit: liquid-tags', () => { - it('should parse unknown tag as base case', () => { - const ast = toLiquidHtmlAST('{% unknown_tag %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('unknown_tag'); - expectPath(ast, 'children.0.markup').to.eql(''); - expectPath(ast, 'children.0.children').to.eql(undefined); - }); - - it('should parse unknown tag with markup', () => { - const ast = toLiquidHtmlAST('{% unknown_tag foo bar %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('unknown_tag'); - expectPath(ast, 'children.0.markup').to.eql('foo bar'); - }); - - it('should parse inline comment # (empty)', () => { - const ast = toLiquidHtmlAST('{% #%}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('#'); - expectPath(ast, 'children.0.markup').to.eql(''); - }); - - it('should parse inline comment # (with content)', () => { - const ast = toLiquidHtmlAST('{% #hello world %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('#'); - expectPath(ast, 'children.0.markup').to.eql('hello world'); - }); - - it.each([ - ['{% # hello %}', 'hello', ' hello '], - ['{% #%}', '', ''], - ['{% #hello world %}', 'hello world', 'hello world '], - ['{% #ok\nfoo %}', 'ok\nfoo', 'ok\nfoo '], - ])('should expose inline comment raw markup for %s', (source, markup, rawMarkup) => { - const ast = toLiquidHtmlAST(source); - const node = ast.children[0]; - - if (node.type !== 'LiquidTag') { - throw new Error('expected LiquidTag'); - } - - expect(node.name).to.eql('#'); - expect(node.markup).to.eql(markup); - expect(sourceAt(ast.source, node.markupPosition)).to.eql(rawMarkup); - }); - - it('should parse known standalone echo', () => { - const ast = toLiquidHtmlAST('{% echo "hi" %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('echo'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.expression.type').to.eql('String'); - expectPath(ast, 'children.0.children').to.eql(undefined); - }); - - it('should parse known standalone assign', () => { - const ast = toLiquidHtmlAST('{% assign x = "hello" %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('assign'); - expectPath(ast, 'children.0.markup.type').to.eql('AssignMarkup'); - }); - - it('should parse known standalone render', () => { - const ast = toLiquidHtmlAST('{% render "snippet" %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('render'); - expectPath(ast, 'children.0.markup.type').to.eql('RenderMarkup'); - }); - - it('should fall back to base case for invalid markup (tolerant)', () => { - const ast = toLiquidHtmlAST('{% assign !bad %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('assign'); - expectPath(ast, 'children.0.markup').to.eql('!bad'); - }); - - it('should handle whitespace stripping', () => { - const ast = toLiquidHtmlAST('{%- echo "hi" -%}'); - expectPath(ast, 'children.0.whitespaceStart').to.eql('-'); - expectPath(ast, 'children.0.whitespaceEnd').to.eql('-'); - }); - - it('should have correct position for tag node', () => { - const ast = toLiquidHtmlAST('before{% echo "x" %}after'); - expectPosition(ast, 'children.1').to.eql('{% echo "x" %}'); - }); - - it('should produce text before and after a tag', () => { - const ast = toLiquidHtmlAST('hello {% echo "x" %} world'); - expectPath(ast, 'children').to.have.lengthOf(3); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.1.type').to.eql('LiquidTag'); - expectPath(ast, 'children.2.type').to.eql('TextNode'); - }); - - it('should parse multiple tags', () => { - const ast = toLiquidHtmlAST('{% echo "a" %}{% assign x = "b" %}'); - expectPath(ast, 'children').to.have.lengthOf(2); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('echo'); - expectPath(ast, 'children.1.type').to.eql('LiquidTag'); - expectPath(ast, 'children.1.name').to.eql('assign'); - }); - - it('should work with toLiquidAST', () => { - const ast = toLiquidAST('{% echo "x" %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('echo'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - }); - - it('should have undefined delimiterWhitespace for standalone tags', () => { - const ast = toLiquidHtmlAST('{% echo "x" %}'); - expectPath(ast, 'children.0.delimiterWhitespaceStart').to.eql(undefined); - expectPath(ast, 'children.0.delimiterWhitespaceEnd').to.eql(undefined); - }); - - it('should have undefined blockEndPosition for standalone tags', () => { - const ast = toLiquidHtmlAST('{% echo "x" %}'); - expectPath(ast, 'children.0.blockEndPosition').to.eql(undefined); - }); -}); diff --git a/packages/liquid-html-parser/src/document/liquid-variable-output.test.ts b/packages/liquid-html-parser/src/document/liquid-variable-output.test.ts index 0e9f524ca..cee2551cc 100644 --- a/packages/liquid-html-parser/src/document/liquid-variable-output.test.ts +++ b/packages/liquid-html-parser/src/document/liquid-variable-output.test.ts @@ -3,43 +3,6 @@ import { toLiquidHtmlAST, toLiquidAST } from '../ast'; import { expectPath, expectPosition } from './test-helpers'; describe('Unit: liquid-variable-output', () => { - it('should fall back to string markup for unparseable content', () => { - const ast = toLiquidHtmlAST('{{ !-asd }}'); - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup').to.eql('!-asd'); - }); - - it('should parse a simple variable', () => { - const ast = toLiquidHtmlAST('{{ product }}'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.expression.type').to.eql('VariableLookup'); - expectPath(ast, 'children.0.markup.expression.name').to.eql('product'); - }); - - it('should parse a dotted lookup', () => { - const ast = toLiquidHtmlAST('{{ product.title }}'); - expectPath(ast, 'children.0.markup.expression.type').to.eql('VariableLookup'); - expectPath(ast, 'children.0.markup.expression.lookups.0.value').to.eql('title'); - }); - - it('should parse a string expression', () => { - const ast = toLiquidHtmlAST('{{ "hello" }}'); - expectPath(ast, 'children.0.markup.expression.type').to.eql('String'); - expectPath(ast, 'children.0.markup.expression.value').to.eql('hello'); - }); - - it('should parse a number expression', () => { - const ast = toLiquidHtmlAST('{{ 42 }}'); - expectPath(ast, 'children.0.markup.expression.type').to.eql('Number'); - expectPath(ast, 'children.0.markup.expression.value').to.eql('42'); - }); - - it('should parse a filter', () => { - const ast = toLiquidHtmlAST('{{ product | upcase }}'); - expectPath(ast, 'children.0.markup.filters').to.have.lengthOf(1); - expectPath(ast, 'children.0.markup.filters.0.name').to.eql('upcase'); - }); - it.each(["{{ 'key' | t:}}", "{{ 'key' | t: }}", "{{- 'key' | t:\n-}}"])( 'should parse an empty filter argument list in strict mode: %s', (source) => { @@ -55,19 +18,6 @@ describe('Unit: liquid-variable-output', () => { }, ); - it('should parse another filter after an empty argument list', () => { - const ast = toLiquidHtmlAST("{{ 'key' | t: | escape }}", { - mode: 'strict', - allowUnclosedDocumentNode: false, - }); - expectPath(ast, 'children.0.markup.filters').to.have.lengthOf(2); - expectPath(ast, 'children.0.markup.filters.0.name').to.eql('t'); - expectPath(ast, 'children.0.markup.filters.0.args').to.eql([]); - expectPosition(ast, 'children.0.markup.filters.0').to.eql(' | t:'); - expectPath(ast, 'children.0.markup.filters.1.name').to.eql('escape'); - expectPosition(ast, 'children.0.markup.filters.1').to.eql(' | escape'); - }); - it.each([ "'key' | t: title:", "'key' | t: title: | escape", @@ -83,63 +33,4 @@ describe('Unit: liquid-variable-output', () => { expectPath(ast, 'children.0.markup').to.eql(markup); } }); - - it('should parse multiple filters', () => { - const ast = toLiquidHtmlAST('{{ product | upcase | strip }}'); - expectPath(ast, 'children.0.markup.filters').to.have.lengthOf(2); - }); - - it('should parse a comparison as BooleanExpression', () => { - const ast = toLiquidHtmlAST('{{ 1 == 1 }}'); - expectPath(ast, 'children.0.markup.expression.type').to.eql('BooleanExpression'); - }); - - it('should parse logical expressions', () => { - const ast = toLiquidHtmlAST('{{ 1 == 1 and 2 == 2 }}'); - expectPath(ast, 'children.0.markup.expression.condition.type').to.eql('LogicalExpression'); - }); - - it('should handle whitespace stripping', () => { - const ast = toLiquidHtmlAST('{{- x -}}'); - expectPath(ast, 'children.0.whitespaceStart').to.eql('-'); - expectPath(ast, 'children.0.whitespaceEnd').to.eql('-'); - }); - - it('should have correct position for drop node', () => { - const ast = toLiquidHtmlAST('before{{ x }}after'); - expectPosition(ast, 'children.1').to.eql('{{ x }}'); - }); - - it('should produce text before and after a drop', () => { - const ast = toLiquidHtmlAST('hello {{ x }} world'); - expectPath(ast, 'children').to.have.lengthOf(3); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.1.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.2.type').to.eql('TextNode'); - }); - - it('should parse multiple drops', () => { - const ast = toLiquidHtmlAST('{{ a }}{{ b }}'); - expectPath(ast, 'children').to.have.lengthOf(2); - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.1.type').to.eql('LiquidVariableOutput'); - }); - - it('should work with toLiquidAST', () => { - const ast = toLiquidAST('{{ product }}'); - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup.type').to.eql('LiquidVariable'); - expectPath(ast, 'children.0.markup.expression.name').to.eql('product'); - }); - - it('should parse a range expression', () => { - const ast = toLiquidHtmlAST('{{ (1..5) }}'); - expectPath(ast, 'children.0.markup.expression.type').to.eql('Range'); - }); - - it('should fall back to string markup for empty drop', () => { - const ast = toLiquidHtmlAST('{{ }}'); - expectPath(ast, 'children.0.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.markup').to.eql(''); - }); }); diff --git a/packages/liquid-html-parser/src/document/parser.test.ts b/packages/liquid-html-parser/src/document/parser.test.ts deleted file mode 100644 index 89e322d3e..000000000 --- a/packages/liquid-html-parser/src/document/parser.test.ts +++ /dev/null @@ -1,226 +0,0 @@ -import { describe, it } from 'vitest'; -import { toLiquidHtmlAST, toLiquidAST } from '../ast'; -import { expectPath, expectPosition } from './test-helpers'; - -describe('Unit: document-parser', () => { - describe('empty document', () => { - it('should return DocumentNode with empty children', () => { - const ast = toLiquidHtmlAST(''); - expectPath(ast, 'type').to.eql('Document'); - expectPath(ast, 'name').to.eql('#document'); - expectPath(ast, 'children').to.have.lengthOf(0); - expectPath(ast, 'position.start').to.eql(0); - expectPath(ast, 'position.end').to.eql(0); - expectPath(ast, 'source').to.eql(''); - expectPath(ast, '_source').to.eql(''); - }); - - it('should work with toLiquidAST too', () => { - const ast = toLiquidAST(''); - expectPath(ast, 'type').to.eql('Document'); - expectPath(ast, 'children').to.have.lengthOf(0); - }); - }); - - describe('plain text', () => { - it('should parse plain text as a single TextNode', () => { - const source = 'hello world'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql('hello world'); - expectPosition(ast, 'children.0').to.eql('hello world'); - }); - - it('should preserve whitespace and newlines', () => { - const source = ' hello\n world\n'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - // Leading/trailing whitespace is trimmed from document-root text nodes - expectPath(ast, 'children.0.value').to.eql('hello\n world'); - }); - - it('should work with whitespace-only source', () => { - const source = ' \n\t '; - const ast = toLiquidHtmlAST(source); - // Whitespace-only text nodes are stripped from document root edges - expectPath(ast, 'children').to.have.lengthOf(0); - }); - }); - - describe('YAML frontmatter', () => { - it('should parse YAML frontmatter followed by text', () => { - const source = '---\nkey: value\n---\nrest'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children').to.have.lengthOf(2); - expectPath(ast, 'children.0.type').to.eql('YAMLFrontmatter'); - expectPath(ast, 'children.0.body').to.eql('key: value\n'); - expectPosition(ast, 'children.0').to.eql('---\nkey: value\n---\n'); - expectPath(ast, 'children.1.type').to.eql('TextNode'); - expectPath(ast, 'children.1.value').to.eql('rest'); - expectPosition(ast, 'children.1').to.eql('rest'); - }); - - it('should parse YAML frontmatter without trailing text', () => { - const source = '---\nkey: value\n---'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('YAMLFrontmatter'); - expectPath(ast, 'children.0.body').to.eql('key: value\n'); - }); - - it('should extract multi-line body correctly', () => { - const source = '---\ntitle: hello\nauthor: world\n---\n'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.body').to.eql('title: hello\nauthor: world\n'); - }); - }); - - describe('skeleton: non-HTML tokens become TextNode in Liquid mode', () => { - it('should collapse HTML tokens into TextNode in toLiquidAST mode', () => { - const source = '
    hello
    '; - const ast = toLiquidAST(source); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql('
    hello
    '); - }); - - it('should handle YAML frontmatter + Liquid drop', () => { - const source = '---\nk: v\n---\n{{ x }}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children').to.have.lengthOf(2); - expectPath(ast, 'children.0.type').to.eql('YAMLFrontmatter'); - expectPath(ast, 'children.1.type').to.eql('LiquidVariableOutput'); - }); - }); - - describe('DocumentNode invariants', () => { - const sources = ['', 'hello', '---\nk: v\n---\nrest', '{{ x }}', '
    text
    ']; - - for (const source of sources) { - it(`should have correct structure for: "${source.slice(0, 30)}"`, () => { - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'type').to.eql('Document'); - expectPath(ast, 'name').to.eql('#document'); - expectPath(ast, 'source').to.eql(source); - expectPath(ast, '_source').to.eql(source); - expectPath(ast, 'position.start').to.eql(0); - expectPath(ast, 'position.end').to.eql(source.length); - }); - } - }); - - describe('parseHtml: false mode', () => { - it('should collapse all HTML into text nodes', () => { - const ast = toLiquidAST('
    text
    '); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql('
    text
    '); - }); - - it('should parse Liquid drops between HTML text', () => { - const ast = toLiquidAST('
    {{ x }}
    '); - expectPath(ast, 'children').to.have.lengthOf(3); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql('
    '); - expectPath(ast, 'children.1.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.2.type').to.eql('TextNode'); - expectPath(ast, 'children.2.value').to.eql('
    '); - }); - - it('should collapse HTML comments into text', () => { - const ast = toLiquidAST(''); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql(''); - }); - - it('should collapse DOCTYPE into text', () => { - const ast = toLiquidAST(''); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql(''); - }); - - it('should collapse self-closing tags into text', () => { - const ast = toLiquidAST(''); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql(''); - }); - - it('should handle HTML inside Liquid block bodies', () => { - const ast = toLiquidAST('{% for item in items %}
  • {{ item }}
  • {% endfor %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('for'); - // branch children: TextNode(
  • ) + LiquidVariableOutput + TextNode(
  • ) - expectPath(ast, 'children.0.children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.children.0.value').to.eql('
  • '); - expectPath(ast, 'children.0.children.0.children.1.type').to.eql('LiquidVariableOutput'); - expectPath(ast, 'children.0.children.0.children.2.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.children.2.value').to.eql('
  • '); - }); - - it('should tolerate unclosed Liquid block tags at end-of-input', () => { - const ast = toLiquidAST('{% for a in b %}
    {% if true %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('for'); - // Inside for: TextNode + if tag - expectPath(ast, 'children.0.children.0.children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.children.0.children.1.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.children.0.children.1.name').to.eql('if'); - }); - - it('should tolerate unclosed Liquid tags in {% liquid %} blocks', () => { - const ast = toLiquidAST(`{%- liquid - assign var1 = product - if use_variant - assign var2 = var1 --%}`); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.name').to.eql('liquid'); - // Verify inner structure is properly parsed (not base case fallback) - expectPath(ast, 'children.0.markup.0.name').to.eql('assign'); - expectPath(ast, 'children.0.markup.1.name').to.eql('if'); - // It ends where its last line does, since there's no endif. - expectPosition(ast, 'children.0.markup.1').toEqual('if use_variant\n assign var2 = var1'); - expectPath(ast, 'children.0.markup.1.blockEndPosition').to.eql({ start: 74, end: 74 }); - }); - - it('should parse raw Liquid tags normally (doc, comment, raw)', () => { - const ast = toLiquidAST('{% doc %}@param x{% enddoc %}'); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - }); - - it('should handle script/style tags with Liquid as text nodes', () => { - const source = ''; - const ast = toLiquidAST(source); - // First child is text up to the Liquid drop (trailing space trimmed at non-text boundary) - expectPath(ast, 'children.0.type').to.eql('TextNode'); - expectPath(ast, 'children.0.value').to.eql(' - expectPath(ast, 'children.2.type').to.eql('TextNode'); - expectPath(ast, 'children.2.value').to.eql(';\n'); - }); - - it('should maintain position accuracy for merged text nodes', () => { - const source = '
    hello
    '; - const ast = toLiquidAST(source); - expectPath(ast, 'children').to.have.lengthOf(1); - expectPath(ast, 'children.0.type').to.eql('TextNode'); - // In non-HTML mode, leading/trailing whitespace is trimmed from text at document root - expectPosition(ast, 'children.0').to.eql('
    hello
    '); - }); - - it('should parse YAML frontmatter normally', () => { - const ast = toLiquidAST('---\ntitle: test\n---\n
    {{ x }}
    '); - expectPath(ast, 'children.0.type').to.eql('YAMLFrontmatter'); - expectPath(ast, 'children.1.type').to.eql('TextNode'); - expectPath(ast, 'children.2.type').to.eql('LiquidVariableOutput'); - }); - }); -}); diff --git a/packages/liquid-html-parser/src/document/test-helpers.ts b/packages/liquid-html-parser/src/document/test-helpers.ts index 4e81bdcae..943b05a2e 100644 --- a/packages/liquid-html-parser/src/document/test-helpers.ts +++ b/packages/liquid-html-parser/src/document/test-helpers.ts @@ -11,19 +11,3 @@ export function expectPosition(ast: LiquidHtmlNode, path: string) { const end = deepGet((path + '.position.end').split('.'), ast) as number; return expect(ast.source.slice(start, end)); } - -export function sourceAt(source: string, position: { start: number; end: number }): string { - return source.slice(position.start, position.end); -} - -export function expectBlockStartPosition(ast: LiquidHtmlNode, path: string) { - const start = deepGet((path + '.blockStartPosition.start').split('.'), ast) as number; - const end = deepGet((path + '.blockStartPosition.end').split('.'), ast) as number; - return expect(ast.source.slice(start, end)); -} - -export function expectBlockEndPosition(ast: LiquidHtmlNode, path: string) { - const start = deepGet((path + '.blockEndPosition.start').split('.'), ast) as number; - const end = deepGet((path + '.blockEndPosition.end').split('.'), ast) as number; - return expect(ast.source.slice(start, end)); -} diff --git a/packages/liquid-html-parser/src/document/unclosed.test.ts b/packages/liquid-html-parser/src/document/unclosed.test.ts deleted file mode 100644 index 5a62dd90f..000000000 --- a/packages/liquid-html-parser/src/document/unclosed.test.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { describe, it, expect } from 'vitest'; -import { toLiquidHtmlAST } from '../ast'; -import { deepGet } from '../utils'; -import { expectPath, sourceAt } from './test-helpers'; - -describe('Unit: unclosed', () => { - describe('conditional branches allow unclosed HTML (if/unless/case)', () => { - it('should allow unclosed
    inside if', () => { - const source = '{% if cond %}
    {% endif %}'; - const ast = toLiquidHtmlAST(source); - expectPath(ast, 'children.0.type').to.eql('LiquidTag'); - expectPath(ast, 'children.0.children.0.type').to.eql('LiquidBranch'); - const el = deepGet('children.0.children.0.children.0'.split('.'), ast) as any; - expect(el.type).to.eql('HtmlElement'); - // blockEndPosition slices to empty string (zero-width) - expect(sourceAt(source, el.blockEndPosition)).to.eql(''); - // blockEndPosition is not -1 (>= blockStartPosition.end) - expect(el.blockEndPosition.start).to.be.greaterThanOrEqual(el.blockStartPosition.end); - }); - - it('should allow unclosed
    with text child inside if/else', () => { - const source = '{% if cond %}
    text{% else %}more{% endif %}'; - const ast = toLiquidHtmlAST(source); - const el = deepGet('children.0.children.0.children.0'.split('.'), ast) as any; - expect(el.type).to.eql('HtmlElement'); - expect(el.children).to.have.lengthOf(1); - expect(el.children[0].value).to.eql('text'); - expect(sourceAt(source, el.blockEndPosition)).to.eql(''); - expect(el.blockEndPosition.start).to.be.greaterThan(el.blockStartPosition.end); - }); - - it('should allow unclosed
    inside unless', () => { - const source = '{% unless cond %}
    {% endunless %}'; - const ast = toLiquidHtmlAST(source); - const el = deepGet('children.0.children.0.children.0'.split('.'), ast) as any; - expect(el.type).to.eql('HtmlElement'); - expect(sourceAt(source, el.blockEndPosition)).to.eql(''); - expect(el.blockEndPosition.start).to.be.greaterThanOrEqual(el.blockStartPosition.end); - }); - - it('should allow unclosed
    inside case/when', () => { - const source = '{% case x %}{% when y %}
    {% endcase %}'; - const ast = toLiquidHtmlAST(source); - const el = deepGet('children.0.children.1.children.0'.split('.'), ast) as any; - expect(el.type).to.eql('HtmlElement'); - expect(sourceAt(source, el.blockEndPosition)).to.eql(''); - expect(el.blockEndPosition.start).to.be.greaterThanOrEqual(el.blockStartPosition.end); - }); - - it('should allow 3 nested unclosed elements', () => { - const source = '{% if cond %}{% endif %}'; - const ast = toLiquidHtmlAST(source); - const a = deepGet('children.0.children.0.children.0'.split('.'), ast) as any; - expect(a.type).to.eql('HtmlElement'); - expect(a.name[0].value).to.eql('a'); - const b = a.children[0]; - expect(b.type).to.eql('HtmlElement'); - expect(b.name[0].value).to.eql('b'); - const c = b.children[0]; - expect(c.type).to.eql('HtmlElement'); - expect(c.name[0].value).to.eql('c'); - }); - - it('should allow closed sibling before unclosed', () => { - const source = '{% if cond %}hi{% endif %}'; - const ast = toLiquidHtmlAST(source); - const branch = deepGet('children.0.children.0'.split('.'), ast) as any; - expect(branch.children).to.have.lengthOf(2); - expect(branch.children[0].type).to.eql('HtmlElement'); - expect(branch.children[0].name[0].value).to.eql('a'); - expect(branch.children[1].type).to.eql('HtmlElement'); - expect(branch.children[1].name[0].value).to.eql('b'); - }); - - it('should allow unclosed parent with closed child', () => { - const source = '{% if cond %}hi{% endif %}'; - const ast = toLiquidHtmlAST(source); - const b = deepGet('children.0.children.0.children.0'.split('.'), ast) as any; - expect(b.type).to.eql('HtmlElement'); - expect(b.name[0].value).to.eql('b'); - expect(b.children).to.have.lengthOf(1); - expect(b.children[0].type).to.eql('HtmlElement'); - expect(b.children[0].name[0].value).to.eql('a'); - }); - }); - - describe('non-conditional blocks throw on unclosed HTML', () => { - const errorCases = [ - '{% for x in y %}
    {% endfor %}', - '{% tablerow x in y %}
    {% endtablerow %}', - '{% form "cart", cart %}
    {% endform %}', - '{% capture x %}
    {% endcapture %}', - ]; - - for (const testCase of errorCases) { - it(`should throw for: ${testCase}`, () => { - expect(() => toLiquidHtmlAST(testCase)).to.throw(/Attempting to close/); - try { - toLiquidHtmlAST(testCase); - } catch (e: any) { - expect(e.name).to.eql('LiquidHTMLParsingError'); - } - }); - } - }); - - describe('HtmlDanglingMarkerClose (orphaned close tags in conditional branches)', () => { - it('should produce HtmlDanglingMarkerClose for in if', () => { - const source = '{% if cond %}{% endif %}'; - const ast = toLiquidHtmlAST(source); - const node = deepGet('children.0.children.0.children.0'.split('.'), ast) as any; - expect(node.type).to.eql('HtmlDanglingMarkerClose'); - expect(node.name[0].value).to.eql('section'); - }); - - it('should produce HtmlDanglingMarkerClose in both if/else branches', () => { - const source = '{% if cond %}
    {% else %}{% endif %}'; - const ast = toLiquidHtmlAST(source); - const node0 = deepGet('children.0.children.0.children.0'.split('.'), ast) as any; - expect(node0.type).to.eql('HtmlDanglingMarkerClose'); - expect(node0.name[0].value).to.eql('div'); - const node1 = deepGet('children.0.children.1.children.0'.split('.'), ast) as any; - expect(node1.type).to.eql('HtmlDanglingMarkerClose'); - expect(node1.name[0].value).to.eql('main'); - }); - }); - - describe('document-level unclosed', () => { - it('should throw for unclosed
    at document level', () => { - expect(() => toLiquidHtmlAST('
    ')).to.throw(); - }); - - it('should throw for unclosed

    at document level', () => { - expect(() => toLiquidHtmlAST('

    ')).to.throw(); - }); - }); - - describe('cross-node errors', () => { - it('should throw for mismatched close tag
    ', () => { - expect(() => toLiquidHtmlAST('
    ')).to.throw(); - }); - - it('should throw for Liquid closing over unclosed HTML', () => { - expect(() => toLiquidHtmlAST('{% for a in b %}
    {% endfor %}')).to.throw( - /Attempting to close/, - ); - }); - - it('should throw for unclosed form at branch boundary', () => { - expect(() => - toLiquidHtmlAST('{% if cond %}{% form "cart" %}{% else %}{% endif %}'), - ).to.throw(); - }); - }); - - describe('dangling open tags with attributes in conditional branches', () => { - it('should parse unclosed element with attributes', () => { - const source = '{% if cond %}
    {% endif %}'; - const ast = toLiquidHtmlAST(source); - const el = deepGet('children.0.children.0.children.0'.split('.'), ast) as any; - expect(el.type).to.eql('HtmlElement'); - expect(el.attributes).to.have.lengthOf(2); - expect(el.attributes[0].name[0].value).to.eql('class'); - expect(el.attributes[1].name[0].value).to.eql('id'); - expect(sourceAt(source, el.blockEndPosition)).to.eql(''); - }); - }); - - describe('void elements should NOT be unclosed', () => { - it('should parse
    inside if as HtmlVoidElement', () => { - const source = '{% if cond %}
    {% endif %}'; - const ast = toLiquidHtmlAST(source); - const el = deepGet('children.0.children.0.children.0'.split('.'), ast) as any; - expect(el.type).to.eql('HtmlVoidElement'); - expect(el.name).to.eql('br'); - }); - - it('should parse with attribute inside if as HtmlVoidElement', () => { - const source = '{% if cond %}{% endif %}'; - const ast = toLiquidHtmlAST(source); - const el = deepGet('children.0.children.0.children.0'.split('.'), ast) as any; - expect(el.type).to.eql('HtmlVoidElement'); - expect(el.name).to.eql('img'); - expect(el.attributes).to.have.lengthOf(1); - }); - }); -}); diff --git a/packages/liquid-html-parser/src/specs.test.ts b/packages/liquid-html-parser/src/specs.test.ts new file mode 100644 index 000000000..1b80b4ea3 --- /dev/null +++ b/packages/liquid-html-parser/src/specs.test.ts @@ -0,0 +1,151 @@ +import { readdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { afterAll, describe, expect, it } from 'vitest'; +import { Document, isScalar, parse, visit } from 'yaml'; +import { toLiquidAST, toLiquidHtmlAST } from './ast'; +import { LiquidHTMLASTParsingError } from './errors'; +import { nonTraversableProperties } from './types'; + +/** + * Runs the language-neutral parser specs in specs/parser at the repo root. + * The format is described in specs/parser/README.md. + * + * With UPDATE_SPECS=1, it writes each case's ast or error from this parser instead. + */ + +type Parse = 'html' | 'liquid'; +type Start = [line: number, character: number]; + +interface SpecPosition { + start: Start; + text: string; +} + +interface SpecError extends SpecPosition { + message: string; +} + +interface Case { + name: string; + source: string; + parse: Parse[]; + ast?: unknown; + error?: SpecError; +} + +const specsDir = resolve(__dirname, '../../../specs/parser'); +const caseKeys = ['name', 'source', 'parse', 'ast', 'error']; +const parsers = { html: toLiquidHtmlAST, liquid: toLiquidAST }; +const update = !!process.env.UPDATE_SPECS; + +for (const specFile of readdirSync(specsDir).filter((f) => f.endsWith('.yml'))) { + const specPath = resolve(specsDir, specFile); + const spec: { cases: Case[] } = parse(readFileSync(specPath, 'utf8')); + + describe(`specs/parser/${specFile}`, () => { + for (const c of spec.cases) { + it(c.name, () => { + // A misspelled key would silently check nothing. + for (const key of Object.keys(c)) expect(caseKeys, 'case keys').toContain(key); + expect(c.parse.length, 'parse').toBeGreaterThan(0); + for (const p of c.parse) expect(Object.keys(parsers), 'parse').toContain(p); + const results = c.parse.map((p) => run(p, c.source)); + if (update) { + for (const result of results) + expect(result, 'html and liquid differ').toEqual(results[0]); + delete c.ast; + delete c.error; + Object.assign(c, results[0]); + } else { + expect('ast' in c !== 'error' in c, 'one of ast and error').toBe(true); + c.parse.forEach((p, i) => expect(results[i], p).toEqual({ ast: c.ast, error: c.error })); + } + }); + } + + if (update) afterAll(() => writeFileSync(specPath, toYaml(spec))); + }); +} + +function run(p: Parse, source: string): Pick { + try { + return { ast: toSpecTree(source, parsers[p](source)) }; + } catch (e) { + if (!(e instanceof LiquidHTMLASTParsingError)) throw e; + return { error: toSpecError(source, e) }; + } +} + +/** + * The tree without the source and the links between nodes, and with positions as text. A node's + * nested nodes come after its other properties. + */ +function toSpecTree(source: string, tree: unknown): unknown { + const leaf = (value: unknown) => value === null || typeof value !== 'object' || isRange(value); + return JSON.parse( + JSON.stringify(tree, (key, value) => { + if (key === 'source' || key === '_source') { + // It's the template on every node, so the spec leaves it out. + expect(value, key).toBe(source); + return; + } + if (nonTraversableProperties.has(key)) return; + if (isRange(value)) return toSpecPosition(source, value.start, value.end); + if (!value || typeof value !== 'object' || Array.isArray(value)) return value; + return Object.fromEntries( + Object.entries(value).sort(([, a], [, b]) => +!leaf(a) - +!leaf(b)), + ); + }), + ); +} + +function isRange(value: any): value is { start: number; end: number } { + return ( + typeof value?.start === 'number' && + typeof value?.end === 'number' && + Object.keys(value).length === 2 + ); +} + +// Cut out at the range, so with start the text pins it even when it repeats. +function toSpecPosition(source: string, start: number, end: number): SpecPosition { + expect(0 <= start && start <= end && end <= source.length, 'range in source').toBe(true); + const before = source.slice(0, start); + const line = before.split('\n').length - 1; + return { + start: [line, start - (before.lastIndexOf('\n') + 1)], + text: source.slice(start, end), + }; +} + +function toSpecError(source: string, e: LiquidHTMLASTParsingError): SpecError { + // loc is 1-based. Its columns, like JS string indices, are in UTF-16 code units. + // The parser never sets it, so a spec error has no unclosed node. + expect(e.unclosed, 'unclosed').toBeNull(); + const lines = source.split('\n'); + const offset = ({ line, column }: { line: number; column: number }) => + lines.slice(0, line - 1).reduce((n, l) => n + l.length + 1, 0) + column - 1; + return { + message: e.message, + ...toSpecPosition(source, offset(e.loc!.start), offset(e.loc!.end)), + }; +} + +/** Positions and lists of scalars on one line, unless the text has several, spaced as prettier does. */ +function toYaml(spec: unknown) { + const doc = new Document(spec); + visit(doc, { + Map(_, map) { + const text = map.get('text'); + if (map.items.length === 2 && map.has('start') && !String(text).includes('\n')) { + map.flow = true; + } + }, + Seq(_, seq) { + if (seq.items.every(isScalar)) seq.flow = true; + }, + }); + return doc + .toString({ lineWidth: 0, flowCollectionPadding: false }) + .replace(/\{(start: .*)\}$/gm, '{ $1 }'); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7c45e32c5..8c82c71cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -145,6 +145,9 @@ importers: '@types/node': specifier: ^22.18.8 version: 22.18.8 + yaml: + specifier: ^2.8.3 + version: 2.8.3 packages/prettier-plugin-liquid: dependencies: diff --git a/specs/parser/README.md b/specs/parser/README.md new file mode 100644 index 000000000..660512684 --- /dev/null +++ b/specs/parser/README.md @@ -0,0 +1,92 @@ +# Parser specs + +Executable, language-neutral specs for the Liquid HTML parser: the tree it builds for a template, +or the error it throws. Every implementation runs them. In TypeScript that's +`packages/liquid-html-parser/src/specs.test.ts`, which runs with the rest of the vitest suite. To +change what the parser does, change its spec first. + +## Format + +```yaml +cases: + - name: parses a drop + source: "{{ x }}" # the template + parse: [html, liquid] # the parses that give this tree + ast: # the whole tree + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ x }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " x " } + position: { start: [0, 0], text: "{{ x }}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [0, 3], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: x } + lookups: [] + lookupModes: [] + filters: [] +``` + +`parse` lists the parses the case is for. Both use their default options: + +| `parse` | What it parses | TypeScript | +| -------- | ------------------------- | ----------------- | +| `html` | Liquid and HTML | `toLiquidHtmlAST` | +| `liquid` | Liquid, with HTML as text | `toLiquidAST` | + +When the two give different results for a template, they're two cases. + +`ast` is every node, with every property, as TypeScript's parser builds them. A case fails on a +missing, extra or different property. The order of properties doesn't matter, and the order of +lists does. Left out are: + +- `source` and `_source`, which are the template on every node. The TypeScript runner checks + that they are. +- The links between nodes: `parentNode`, `prev`, `next`, `firstChild` and `lastChild`. The parser + doesn't set them. +- Properties that are `undefined`. A `null` stays. + +A position is where its range starts and the text it covers. `start` is a line and character, +0-based, in UTF-16 code units. `text` is cut out of the template at the range, never searched +for, so `start` and `text` pin the range even when the text repeats. + +## Errors + +A case whose parse throws has `error` instead of `ast`: + +```yaml +cases: + - name: closes a tag before its element + source: "{% for a in b %}
    {% endfor %}" + parse: [html] + error: + message: Attempting to close LiquidTag 'endfor' before HtmlElement 'details' was closed + start: [0, 16] + text:
    +``` + +The parse throws a parse error, `LiquidHTMLASTParsingError` in TypeScript, with that `message`. +`start` and `text` are its location (`loc`), as for positions. + +A location never goes past the template's last character, so an error at its end covers one +character less than the tag: an unclosed `{% if a %}` covers `{% if a %`. + +## Writing a case + +Write its `name`, `source` and `parse`, and have TypeScript fill in the result: + +```sh +UPDATE_SPECS=1 npx vitest run packages/liquid-html-parser/src/specs.test.ts +``` + +It writes every case's `ast` or `error` from the TypeScript parser, and fails on a case whose +parses give different results, which needs splitting. Read the diff before committing it: it's +what every implementation will be held to. diff --git a/specs/parser/document.yml b/specs/parser/document.yml new file mode 100644 index 000000000..4c622b33f --- /dev/null +++ b/specs/parser/document.yml @@ -0,0 +1,628 @@ +cases: + - name: should parse a basic text node into a TextNode + source: Hello world! + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: Hello world! } + children: + - type: TextNode + value: Hello world! + position: { start: [0, 0], text: Hello world! } + - name: should parse script tags as a text node + source: |- + + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + + children: + - type: TextNode + value: |- + + position: + start: [1, 32] + text: |- + ; + + - name: should parse style tags as a text node + source: |- + + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + + children: + - type: TextNode + value: |- + + position: + start: [1, 33] + text: |- + } + + - name: should collapse DOCTYPE into text + source: + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: TextNode + value: + position: { start: [0, 0], text: } + - name: should parse Liquid drops between HTML text + source:
    {{ x }}
    + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "
    {{ x }}
    " } + children: + - type: TextNode + value:
    + position: { start: [0, 0], text:
    } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 7], text: " x " } + position: { start: [0, 5], text: "{{ x }}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [0, 8], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [0, 8], text: x } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value:
    + position: { start: [0, 12], text:
    } + - name: should return DocumentNode with empty children + source: "" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: [] + - name: should parse plain text as a single TextNode + source: hello world + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: hello world } + children: + - type: TextNode + value: hello world + position: { start: [0, 0], text: hello world } + - name: should preserve whitespace and newlines + source: |2 + hello + world + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |2 + hello + world + children: + - type: TextNode + value: |- + hello + world + position: + start: [0, 2] + text: |- + hello + world + - name: should work with whitespace-only source + source: " \ \n\t " + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " \ \n\t " + children: [] + - name: should parse YAML frontmatter followed by text + source: |- + --- + key: value + --- + rest + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + --- + key: value + --- + rest + children: + - type: YAMLFrontmatter + body: | + key: value + position: + start: [0, 0] + text: | + --- + key: value + --- + - type: TextNode + value: rest + position: { start: [3, 0], text: rest } + - name: should parse YAML frontmatter without trailing text + source: |- + --- + key: value + --- + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + --- + key: value + --- + children: + - type: YAMLFrontmatter + body: | + key: value + position: + start: [0, 0] + text: |- + --- + key: value + --- + - name: should extract multi-line body correctly + source: | + --- + title: hello + author: world + --- + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: | + --- + title: hello + author: world + --- + children: + - type: YAMLFrontmatter + body: | + title: hello + author: world + position: + start: [0, 0] + text: | + --- + title: hello + author: world + --- + - name: should handle YAML frontmatter + Liquid drop + source: |- + --- + k: v + --- + {{ x }} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + --- + k: v + --- + {{ x }} + children: + - type: YAMLFrontmatter + body: | + k: v + position: + start: [0, 0] + text: | + --- + k: v + --- + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [3, 2], text: " x " } + position: { start: [3, 0], text: "{{ x }}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [3, 3], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [3, 3], text: x } + lookups: [] + lookupModes: [] + filters: [] + - name: 'should have correct structure for: "hello"' + source: hello + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: hello } + children: + - type: TextNode + value: hello + position: { start: [0, 0], text: hello } + - name: 'should have correct structure for: "--- k: v --- rest"' + source: |- + --- + k: v + --- + rest + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + --- + k: v + --- + rest + children: + - type: YAMLFrontmatter + body: | + k: v + position: + start: [0, 0] + text: | + --- + k: v + --- + - type: TextNode + value: rest + position: { start: [3, 0], text: rest } + - name: 'should have correct structure for: "
    text
    "' + source:
    text
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    text
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text:
    } + blockEndPosition: { start: [0, 9], text:
    } + position: { start: [0, 0], text:
    text
    } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: [] + children: + - type: TextNode + value: text + position: { start: [0, 5], text: text } + - name: should collapse all HTML into text nodes + source:
    text
    + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    text
    } + children: + - type: TextNode + value:
    text
    + position: { start: [0, 0], text:
    text
    } + - name: should collapse HTML comments into text + source: + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: TextNode + value: + position: { start: [0, 0], text: } + - name: should collapse self-closing tags into text + source: + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: TextNode + value: + position: { start: [0, 0], text: } + - name: should handle HTML inside Liquid block bodies + source: "{% for item in items %}
  • {{ item }}
  • {% endfor %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% for item in items %}
  • {{ item }}
  • {% endfor %}" } + children: + - type: LiquidTag + name: for + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% for item in items %}" } + blockEndPosition: { start: [0, 42], text: "{% endfor %}" } + markupPosition: { start: [0, 7], text: "item in items " } + position: { start: [0, 0], text: "{% for item in items %}
  • {{ item }}
  • {% endfor %}" } + markup: + type: ForMarkup + variableName: item + reversed: false + position: { start: [0, 7], text: "item in items " } + collection: + type: VariableLookup + name: items + position: { start: [0, 15], text: items } + lookups: [] + lookupModes: [] + args: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 23], text: "" } + blockStartPosition: { start: [0, 23], text: "" } + blockEndPosition: { start: [0, 42], text: "" } + position: { start: [0, 23], text: "
  • {{ item }}
  • " } + children: + - type: TextNode + value:
  • + position: { start: [0, 23], text:
  • } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 29], text: " item " } + position: { start: [0, 27], text: "{{ item }}" } + markup: + type: LiquidVariable + rawSource: item + position: { start: [0, 30], text: "item " } + expression: + type: VariableLookup + name: item + position: { start: [0, 30], text: item } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value:
  • + position: { start: [0, 37], text: } + - name: should parse raw Liquid tags normally (doc, comment, raw) + source: "{% doc %}@param x{% enddoc %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% doc %}@param x{% enddoc %}" } + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% doc %}" } + blockEndPosition: { start: [0, 17], text: "{% enddoc %}" } + position: { start: [0, 0], text: "{% doc %}@param x{% enddoc %}" } + body: + type: RawMarkup + kind: text + value: "@param x" + position: { start: [0, 9], text: "@param x" } + nodes: + - type: LiquidDocParamNode + name: param + paramType: null + paramDescription: null + required: true + position: { start: [0, 9], text: "@param x" } + paramName: + type: TextNode + value: x + position: { start: [0, 16], text: x } + - name: should maintain position accuracy for merged text nodes + source: '
    hello
    ' + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '
    hello
    ' } + children: + - type: TextNode + value:
    hello
    + position: { start: [0, 2], text:
    hello
    } + - name: should parse YAML frontmatter normally + source: |- + --- + title: test + --- +
    {{ x }}
    + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + --- + title: test + --- +
    {{ x }}
    + children: + - type: YAMLFrontmatter + body: | + title: test + position: + start: [0, 0] + text: | + --- + title: test + --- + - type: TextNode + value:
    + position: { start: [3, 0], text:
    } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [3, 7], text: " x " } + position: { start: [3, 5], text: "{{ x }}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [3, 8], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [3, 8], text: x } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value:
    + position: { start: [3, 12], text:
    } + - name: should count positions in UTF-16 code units + source: 🙂 {{ x }} + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "🙂 {{ x }}" } + children: + - type: TextNode + value: 🙂 + position: { start: [0, 0], text: 🙂 } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 5], text: " x " } + position: { start: [0, 3], text: "{{ x }}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [0, 6], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [0, 6], text: x } + lookups: [] + lookupModes: [] + filters: [] + - name: should count error locations in UTF-16 code units + source: |- + 🙂 + 🙂 {% if a %} + parse: [html] + error: + message: Attempting to end parsing before LiquidTag 'if' was closed + start: [1, 3] + text: "{% if a %" diff --git a/specs/parser/html.yml b/specs/parser/html.yml new file mode 100644 index 000000000..8e5047ca9 --- /dev/null +++ b/specs/parser/html.yml @@ -0,0 +1,4930 @@ +cases: + - name: should parse HTML attributes + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 10], text: https://1234 } + position: { start: [0, 5], text: src="https://1234" } + name: + - type: TextNode + value: src + position: { start: [0, 5], text: src } + value: + - type: TextNode + value: https://1234 + position: { start: [0, 10], text: https://1234 } + - type: AttrSingleQuoted + attributePosition: { start: [0, 33], text: lazy } + position: { start: [0, 24], text: loading='lazy' } + name: + - type: TextNode + value: loading + position: { start: [0, 24], text: loading } + value: + - type: TextNode + value: lazy + position: { start: [0, 33], text: lazy } + - type: AttrEmpty + position: { start: [0, 39], text: disabled } + name: + - type: TextNode + value: disabled + position: { start: [0, 39], text: disabled } + - type: AttrDoubleQuoted + attributePosition: { start: [0, 57], text: "" } + position: { start: [0, 48], text: checked="" } + name: + - type: TextNode + value: checked + position: { start: [0, 48], text: checked } + value: [] + - name: should parse HTML attributes inside tags + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: "" } + position: { start: [0, 0], text: "" } + attributes: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 5], text: "{% if cond %}" } + blockEndPosition: { start: [0, 69], text: "{% endif %}" } + markupPosition: { start: [0, 11], text: "cond " } + position: { start: [0, 5], text: "{% if cond %}src=\"https://1234\" loading='lazy'{% else %}disabled{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 11], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 18], text: "" } + blockStartPosition: { start: [0, 18], text: "" } + blockEndPosition: { start: [0, 51], text: "" } + position: { start: [0, 18], text: src="https://1234" loading='lazy' } + children: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 23], text: https://1234 } + position: { start: [0, 18], text: src="https://1234" } + name: + - type: TextNode + value: src + position: { start: [0, 18], text: src } + value: + - type: TextNode + value: https://1234 + position: { start: [0, 23], text: https://1234 } + - type: AttrSingleQuoted + attributePosition: { start: [0, 46], text: lazy } + position: { start: [0, 37], text: loading='lazy' } + name: + - type: TextNode + value: loading + position: { start: [0, 37], text: loading } + value: + - type: TextNode + value: lazy + position: { start: [0, 46], text: lazy } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 59], text: "" } + blockStartPosition: { start: [0, 51], text: "{% else %}" } + blockEndPosition: { start: [0, 69], text: "" } + position: { start: [0, 51], text: "{% else %}disabled" } + children: + - type: AttrEmpty + position: { start: [0, 61], text: disabled } + name: + - type: TextNode + value: disabled + position: { start: [0, 61], text: disabled } + - name: should preserve whitespace in LiquidBranch children inside attribute values + source:
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '
    ' } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: '
    ' } + blockEndPosition: { start: [0, 51], text:
    } + position: { start: [0, 0], text: '
    ' } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 12], text: "base{% if x %} extra-class{% endif %}" } + position: { start: [0, 5], text: 'class="base{% if x %} extra-class{% endif %}"' } + name: + - type: TextNode + value: class + position: { start: [0, 5], text: class } + value: + - type: TextNode + value: base + position: { start: [0, 12], text: base } + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 16], text: "{% if x %}" } + blockEndPosition: { start: [0, 38], text: "{% endif %}" } + markupPosition: { start: [0, 22], text: "x " } + position: { start: [0, 16], text: "{% if x %} extra-class{% endif %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 22], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 26], text: "" } + blockStartPosition: { start: [0, 26], text: "" } + blockEndPosition: { start: [0, 38], text: "" } + position: { start: [0, 26], text: " extra-class" } + children: + - type: TextNode + value: " extra-class" + position: { start: [0, 26], text: " extra-class" } + children: [] + - name: should parse HTML tags with Liquid Drop names (1) + source: <{{ node_type }} src="https://1234" loading='lazy' disabled> + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + blockEndPosition: { start: [0, 60], text: "" } + position: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + name: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 3], text: " node_type " } + position: { start: [0, 1], text: "{{ node_type }}" } + markup: + type: LiquidVariable + rawSource: node_type + position: { start: [0, 4], text: "node_type " } + expression: + type: VariableLookup + name: node_type + position: { start: [0, 4], text: node_type } + lookups: [] + lookupModes: [] + filters: [] + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 22], text: https://1234 } + position: { start: [0, 17], text: src="https://1234" } + name: + - type: TextNode + value: src + position: { start: [0, 17], text: src } + value: + - type: TextNode + value: https://1234 + position: { start: [0, 22], text: https://1234 } + - type: AttrSingleQuoted + attributePosition: { start: [0, 45], text: lazy } + position: { start: [0, 36], text: loading='lazy' } + name: + - type: TextNode + value: loading + position: { start: [0, 36], text: loading } + value: + - type: TextNode + value: lazy + position: { start: [0, 45], text: lazy } + - type: AttrEmpty + position: { start: [0, 51], text: disabled } + name: + - type: TextNode + value: disabled + position: { start: [0, 51], text: disabled } + children: [] + - name: should parse HTML tags with Liquid Drop names (2) + source: <{{ node_type }} src="https://1234" loading='lazy' disabled> + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + blockEndPosition: { start: [0, 60], text: "" } + position: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + name: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 3], text: " node_type " } + position: { start: [0, 1], text: "{{ node_type }}" } + markup: + type: LiquidVariable + rawSource: node_type + position: { start: [0, 4], text: "node_type " } + expression: + type: VariableLookup + name: node_type + position: { start: [0, 4], text: node_type } + lookups: [] + lookupModes: [] + filters: [] + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 22], text: https://1234 } + position: { start: [0, 17], text: src="https://1234" } + name: + - type: TextNode + value: src + position: { start: [0, 17], text: src } + value: + - type: TextNode + value: https://1234 + position: { start: [0, 22], text: https://1234 } + - type: AttrSingleQuoted + attributePosition: { start: [0, 45], text: lazy } + position: { start: [0, 36], text: loading='lazy' } + name: + - type: TextNode + value: loading + position: { start: [0, 36], text: loading } + value: + - type: TextNode + value: lazy + position: { start: [0, 45], text: lazy } + - type: AttrEmpty + position: { start: [0, 51], text: disabled } + name: + - type: TextNode + value: disabled + position: { start: [0, 51], text: disabled } + children: [] + - name: should parse HTML tags with Liquid Drop names (3) + source: <{{ node_type }} src="https://1234" loading='lazy' disabled> + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + blockEndPosition: { start: [0, 60], text: "" } + position: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + name: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 3], text: " node_type " } + position: { start: [0, 1], text: "{{ node_type }}" } + markup: + type: LiquidVariable + rawSource: node_type + position: { start: [0, 4], text: "node_type " } + expression: + type: VariableLookup + name: node_type + position: { start: [0, 4], text: node_type } + lookups: [] + lookupModes: [] + filters: [] + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 22], text: https://1234 } + position: { start: [0, 17], text: src="https://1234" } + name: + - type: TextNode + value: src + position: { start: [0, 17], text: src } + value: + - type: TextNode + value: https://1234 + position: { start: [0, 22], text: https://1234 } + - type: AttrSingleQuoted + attributePosition: { start: [0, 45], text: lazy } + position: { start: [0, 36], text: loading='lazy' } + name: + - type: TextNode + value: loading + position: { start: [0, 36], text: loading } + value: + - type: TextNode + value: lazy + position: { start: [0, 45], text: lazy } + - type: AttrEmpty + position: { start: [0, 51], text: disabled } + name: + - type: TextNode + value: disabled + position: { start: [0, 51], text: disabled } + children: [] + - name: should parse HTML tags with Liquid Drop names (4) + source: <{{ node_type }} src="https://1234" loading='lazy' disabled> + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + blockEndPosition: { start: [0, 60], text: "" } + position: { start: [0, 0], text: "<{{ node_type }} src=\"https://1234\" loading='lazy' disabled>" } + name: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 3], text: " node_type " } + position: { start: [0, 1], text: "{{ node_type }}" } + markup: + type: LiquidVariable + rawSource: node_type + position: { start: [0, 4], text: "node_type " } + expression: + type: VariableLookup + name: node_type + position: { start: [0, 4], text: node_type } + lookups: [] + lookupModes: [] + filters: [] + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 22], text: https://1234 } + position: { start: [0, 17], text: src="https://1234" } + name: + - type: TextNode + value: src + position: { start: [0, 17], text: src } + value: + - type: TextNode + value: https://1234 + position: { start: [0, 22], text: https://1234 } + - type: AttrSingleQuoted + attributePosition: { start: [0, 45], text: lazy } + position: { start: [0, 36], text: loading='lazy' } + name: + - type: TextNode + value: loading + position: { start: [0, 36], text: loading } + value: + - type: TextNode + value: lazy + position: { start: [0, 45], text: lazy } + - type: AttrEmpty + position: { start: [0, 51], text: disabled } + name: + - type: TextNode + value: disabled + position: { start: [0, 51], text: disabled } + children: [] + - name: should parse HTML tags with Liquid Drop names (5) + source: <{{ node_type -}} src="https://1234" loading='lazy' disabled> + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{{ node_type -}} src=\"https://1234\" loading='lazy' disabled>" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{{ node_type -}} src=\"https://1234\" loading='lazy' disabled>" } + blockEndPosition: { start: [0, 61], text: "" } + position: { start: [0, 0], text: "<{{ node_type -}} src=\"https://1234\" loading='lazy' disabled>" } + name: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "-" + markupPosition: { start: [0, 3], text: " node_type " } + position: { start: [0, 1], text: "{{ node_type -}}" } + markup: + type: LiquidVariable + rawSource: node_type + position: { start: [0, 4], text: "node_type " } + expression: + type: VariableLookup + name: node_type + position: { start: [0, 4], text: node_type } + lookups: [] + lookupModes: [] + filters: [] + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 23], text: https://1234 } + position: { start: [0, 18], text: src="https://1234" } + name: + - type: TextNode + value: src + position: { start: [0, 18], text: src } + value: + - type: TextNode + value: https://1234 + position: { start: [0, 23], text: https://1234 } + - type: AttrSingleQuoted + attributePosition: { start: [0, 46], text: lazy } + position: { start: [0, 37], text: loading='lazy' } + name: + - type: TextNode + value: loading + position: { start: [0, 37], text: loading } + value: + - type: TextNode + value: lazy + position: { start: [0, 46], text: lazy } + - type: AttrEmpty + position: { start: [0, 52], text: disabled } + name: + - type: TextNode + value: disabled + position: { start: [0, 52], text: disabled } + children: [] + - name: should parse HTML tags with Liquid Drop names (6) + source: <{{- node_type -}} src="https://1234" loading='lazy' disabled> + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{{- node_type -}} src=\"https://1234\" loading='lazy' disabled>" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{{- node_type -}} src=\"https://1234\" loading='lazy' disabled>" } + blockEndPosition: { start: [0, 62], text: "" } + position: { start: [0, 0], text: "<{{- node_type -}} src=\"https://1234\" loading='lazy' disabled>" } + name: + - type: LiquidVariableOutput + whitespaceStart: "-" + whitespaceEnd: "-" + markupPosition: { start: [0, 4], text: " node_type " } + position: { start: [0, 1], text: "{{- node_type -}}" } + markup: + type: LiquidVariable + rawSource: node_type + position: { start: [0, 5], text: "node_type " } + expression: + type: VariableLookup + name: node_type + position: { start: [0, 5], text: node_type } + lookups: [] + lookupModes: [] + filters: [] + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 24], text: https://1234 } + position: { start: [0, 19], text: src="https://1234" } + name: + - type: TextNode + value: src + position: { start: [0, 19], text: src } + value: + - type: TextNode + value: https://1234 + position: { start: [0, 24], text: https://1234 } + - type: AttrSingleQuoted + attributePosition: { start: [0, 47], text: lazy } + position: { start: [0, 38], text: loading='lazy' } + name: + - type: TextNode + value: loading + position: { start: [0, 38], text: loading } + value: + - type: TextNode + value: lazy + position: { start: [0, 47], text: lazy } + - type: AttrEmpty + position: { start: [0, 53], text: disabled } + name: + - type: TextNode + value: disabled + position: { start: [0, 53], text: disabled } + children: [] + - name: should parse HTML tags with compound Liquid Drop names + source: <{{ node_type }}--header > + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{{ node_type }}--header >" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{{ node_type }}--header >" } + blockEndPosition: { start: [0, 26], text: "" } + position: { start: [0, 0], text: "<{{ node_type }}--header >" } + name: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 3], text: " node_type " } + position: { start: [0, 1], text: "{{ node_type }}" } + markup: + type: LiquidVariable + rawSource: node_type + position: { start: [0, 4], text: "node_type " } + expression: + type: VariableLookup + name: node_type + position: { start: [0, 4], text: node_type } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value: --header + position: { start: [0, 16], text: --header } + attributes: [] + children: [] + - name: should parse HTML self-closing elements with compound Liquid Drop names + source: <{{ node_type }}--header /> + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{{ node_type }}--header />" } + children: + - type: HtmlSelfClosingElement + blockStartPosition: { start: [0, 0], text: "<{{ node_type }}--header />" } + position: { start: [0, 0], text: "<{{ node_type }}--header />" } + name: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 3], text: " node_type " } + position: { start: [0, 1], text: "{{ node_type }}" } + markup: + type: LiquidVariable + rawSource: node_type + position: { start: [0, 4], text: "node_type " } + expression: + type: VariableLookup + name: node_type + position: { start: [0, 4], text: node_type } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value: --header + position: { start: [0, 16], text: --header } + attributes: [] + - name: should allow unclosed nodes inside conditional and case branches (1) + source: "{% if cond %}
    {% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}
    {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 18], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}
    {% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 18], text: "" } + position: { start: [0, 13], text:
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 13], text:
    } + blockEndPosition: { start: [0, 18], text: "" } + position: { start: [0, 13], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 14], text: div } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (2) + source: "{% if cond %}{% else %}
    {% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% else %}
    {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 28], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% else %}
    {% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 21], text: "" } + blockStartPosition: { start: [0, 13], text: "{% else %}" } + blockEndPosition: { start: [0, 28], text: "" } + position: { start: [0, 13], text: "{% else %}
    " } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 23], text:
    } + blockEndPosition: { start: [0, 28], text: "" } + position: { start: [0, 23], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 24], text: div } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (3) + source: "{% if cond %}
    {% else %}{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}
    {% else %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 28], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}
    {% else %}{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 18], text: "" } + position: { start: [0, 13], text:
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 13], text:
    } + blockEndPosition: { start: [0, 18], text: "" } + position: { start: [0, 13], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 14], text: div } + attributes: [] + children: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 26], text: "" } + blockStartPosition: { start: [0, 18], text: "{% else %}" } + blockEndPosition: { start: [0, 28], text: "" } + position: { start: [0, 18], text: "{% else %}" } + children: [] + - name: should allow unclosed nodes inside conditional and case branches (4) + source: "{% if cond %}{% elsif cond %}
    {% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% elsif cond %}
    {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 34], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% elsif cond %}
    {% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 22], text: "cond " } + blockStartPosition: { start: [0, 13], text: "{% elsif cond %}" } + blockEndPosition: { start: [0, 34], text: "" } + position: { start: [0, 13], text: "{% elsif cond %}
    " } + markup: + type: VariableLookup + name: cond + position: { start: [0, 22], text: cond } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 29], text:
    } + blockEndPosition: { start: [0, 34], text: "" } + position: { start: [0, 29], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 30], text: div } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (5) + source: "{% if cond %}
    {% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}
    {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 21], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}
    {% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 21], text: "" } + position: { start: [0, 13], text:
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 13], text:
    } + blockEndPosition: { start: [0, 21], text: "" } + position: { start: [0, 13], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 14], text: div } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 18], text: } + blockEndPosition: { start: [0, 21], text: "" } + position: { start: [0, 18], text: } + name: + - type: TextNode + value: a + position: { start: [0, 19], text: a } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (6) + source: "{% if cond %}{% else %}
    {% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% else %}
    {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 31], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% else %}
    {% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 21], text: "" } + blockStartPosition: { start: [0, 13], text: "{% else %}" } + blockEndPosition: { start: [0, 31], text: "" } + position: { start: [0, 13], text: "{% else %}
    " } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 23], text:
    } + blockEndPosition: { start: [0, 31], text: "" } + position: { start: [0, 23], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 24], text: div } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 28], text: } + blockEndPosition: { start: [0, 31], text: "" } + position: { start: [0, 28], text: } + name: + - type: TextNode + value: a + position: { start: [0, 29], text: a } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (7) + source: "{% if cond %}{% elsif cond %}
    {% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% elsif cond %}
    {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 37], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% elsif cond %}
    {% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 22], text: "cond " } + blockStartPosition: { start: [0, 13], text: "{% elsif cond %}" } + blockEndPosition: { start: [0, 37], text: "" } + position: { start: [0, 13], text: "{% elsif cond %}
    " } + markup: + type: VariableLookup + name: cond + position: { start: [0, 22], text: cond } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 29], text:
    } + blockEndPosition: { start: [0, 37], text: "" } + position: { start: [0, 29], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 30], text: div } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 34], text: } + blockEndPosition: { start: [0, 37], text: "" } + position: { start: [0, 34], text: } + name: + - type: TextNode + value: a + position: { start: [0, 35], text: a } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (8) + source: "{% case cond %}{% when %}
    {% endcase %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% case cond %}{% when %}
    {% endcase %}" } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case cond %}" } + blockEndPosition: { start: [0, 33], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "cond " } + position: { start: [0, 0], text: "{% case cond %}{% when %}
    {% endcase %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 8], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 15], text: "" } + blockStartPosition: { start: [0, 15], text: "" } + blockEndPosition: { start: [0, 15], text: "" } + position: { start: [0, 15], text: "" } + children: [] + - type: LiquidBranch + name: when + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 23], text: "" } + blockStartPosition: { start: [0, 15], text: "{% when %}" } + blockEndPosition: { start: [0, 33], text: "" } + position: { start: [0, 15], text: "{% when %}
    " } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 25], text:
    } + blockEndPosition: { start: [0, 33], text: "" } + position: { start: [0, 25], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 26], text: div } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 30], text: } + blockEndPosition: { start: [0, 33], text: "" } + position: { start: [0, 30], text: } + name: + - type: TextNode + value: a + position: { start: [0, 31], text: a } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (9) + source: "{% if cond %}{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 22], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 22], text: "" } + position: { start: [0, 13], text: } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 13], text: } + blockEndPosition: { start: [0, 22], text: "" } + position: { start: [0, 13], text: } + name: + - type: TextNode + value: a + position: { start: [0, 14], text: a } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 16], text: } + blockEndPosition: { start: [0, 22], text: "" } + position: { start: [0, 16], text: } + name: + - type: TextNode + value: b + position: { start: [0, 17], text: b } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 19], text: } + blockEndPosition: { start: [0, 22], text: "" } + position: { start: [0, 19], text: } + name: + - type: TextNode + value: c + position: { start: [0, 20], text: c } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (10) + source: "{% if cond %}{% else %}{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% else %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 32], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% else %}{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 21], text: "" } + blockStartPosition: { start: [0, 13], text: "{% else %}" } + blockEndPosition: { start: [0, 32], text: "" } + position: { start: [0, 13], text: "{% else %}" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 23], text: } + blockEndPosition: { start: [0, 32], text: "" } + position: { start: [0, 23], text: } + name: + - type: TextNode + value: a + position: { start: [0, 24], text: a } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 26], text: } + blockEndPosition: { start: [0, 32], text: "" } + position: { start: [0, 26], text: } + name: + - type: TextNode + value: b + position: { start: [0, 27], text: b } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 29], text: } + blockEndPosition: { start: [0, 32], text: "" } + position: { start: [0, 29], text: } + name: + - type: TextNode + value: c + position: { start: [0, 30], text: c } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (11) + source: "{% if cond %}{% elsif cond %}{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% elsif cond %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 38], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% elsif cond %}{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 22], text: "cond " } + blockStartPosition: { start: [0, 13], text: "{% elsif cond %}" } + blockEndPosition: { start: [0, 38], text: "" } + position: { start: [0, 13], text: "{% elsif cond %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 22], text: cond } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 29], text: } + blockEndPosition: { start: [0, 38], text: "" } + position: { start: [0, 29], text: } + name: + - type: TextNode + value: a + position: { start: [0, 30], text: a } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 32], text: } + blockEndPosition: { start: [0, 38], text: "" } + position: { start: [0, 32], text: } + name: + - type: TextNode + value: b + position: { start: [0, 33], text: b } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 35], text: } + blockEndPosition: { start: [0, 38], text: "" } + position: { start: [0, 35], text: } + name: + - type: TextNode + value: c + position: { start: [0, 36], text: c } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (12) + source: "{% case cond %}{% when %}{% endcase %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% case cond %}{% when %}{% endcase %}" } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case cond %}" } + blockEndPosition: { start: [0, 34], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "cond " } + position: { start: [0, 0], text: "{% case cond %}{% when %}{% endcase %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 8], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 15], text: "" } + blockStartPosition: { start: [0, 15], text: "" } + blockEndPosition: { start: [0, 15], text: "" } + position: { start: [0, 15], text: "" } + children: [] + - type: LiquidBranch + name: when + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 23], text: "" } + blockStartPosition: { start: [0, 15], text: "{% when %}" } + blockEndPosition: { start: [0, 34], text: "" } + position: { start: [0, 15], text: "{% when %}" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 25], text: } + blockEndPosition: { start: [0, 34], text: "" } + position: { start: [0, 25], text: } + name: + - type: TextNode + value: a + position: { start: [0, 26], text: a } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 28], text: } + blockEndPosition: { start: [0, 34], text: "" } + position: { start: [0, 28], text: } + name: + - type: TextNode + value: b + position: { start: [0, 29], text: b } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 31], text: } + blockEndPosition: { start: [0, 34], text: "" } + position: { start: [0, 31], text: } + name: + - type: TextNode + value: c + position: { start: [0, 32], text: c } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (13) + source: "{% if cond %}hi{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}hi{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 25], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}hi{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 25], text: "" } + position: { start: [0, 13], text: hi } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 13], text: } + blockEndPosition: { start: [0, 18], text: } + position: { start: [0, 13], text: hi } + name: + - type: TextNode + value: a + position: { start: [0, 14], text: a } + attributes: [] + children: + - type: TextNode + value: hi + position: { start: [0, 16], text: hi } + - type: HtmlElement + blockStartPosition: { start: [0, 22], text: } + blockEndPosition: { start: [0, 25], text: "" } + position: { start: [0, 22], text: } + name: + - type: TextNode + value: b + position: { start: [0, 23], text: b } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (14) + source: "{% if cond %}hi{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}hi{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 25], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}hi{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 25], text: "" } + position: { start: [0, 13], text: hi } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 13], text: } + blockEndPosition: { start: [0, 25], text: "" } + position: { start: [0, 13], text: hi } + name: + - type: TextNode + value: b + position: { start: [0, 14], text: b } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 16], text: } + blockEndPosition: { start: [0, 21], text: } + position: { start: [0, 16], text: hi } + name: + - type: TextNode + value: a + position: { start: [0, 17], text: a } + attributes: [] + children: + - type: TextNode + value: hi + position: { start: [0, 19], text: hi } + - name: should allow unclosed nodes inside conditional and case branches (15) + source: "{% if cond %}{% else %}hi{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% else %}hi{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 35], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% else %}hi{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 21], text: "" } + blockStartPosition: { start: [0, 13], text: "{% else %}" } + blockEndPosition: { start: [0, 35], text: "" } + position: { start: [0, 13], text: "{% else %}hi" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 23], text: } + blockEndPosition: { start: [0, 28], text: } + position: { start: [0, 23], text: hi } + name: + - type: TextNode + value: a + position: { start: [0, 24], text: a } + attributes: [] + children: + - type: TextNode + value: hi + position: { start: [0, 26], text: hi } + - type: HtmlElement + blockStartPosition: { start: [0, 32], text: } + blockEndPosition: { start: [0, 35], text: "" } + position: { start: [0, 32], text: } + name: + - type: TextNode + value: b + position: { start: [0, 33], text: b } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (16) + source: "{% if cond %}{% else %}hi{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% else %}hi{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 35], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% else %}hi{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 21], text: "" } + blockStartPosition: { start: [0, 13], text: "{% else %}" } + blockEndPosition: { start: [0, 35], text: "" } + position: { start: [0, 13], text: "{% else %}hi" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 23], text: } + blockEndPosition: { start: [0, 35], text: "" } + position: { start: [0, 23], text: hi } + name: + - type: TextNode + value: b + position: { start: [0, 24], text: b } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 26], text: } + blockEndPosition: { start: [0, 31], text: } + position: { start: [0, 26], text: hi } + name: + - type: TextNode + value: a + position: { start: [0, 27], text: a } + attributes: [] + children: + - type: TextNode + value: hi + position: { start: [0, 29], text: hi } + - name: should allow unclosed nodes inside conditional and case branches (17) + source: "{% if cond %}{% elsif cond %}hi{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% elsif cond %}hi{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 41], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% elsif cond %}hi{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 22], text: "cond " } + blockStartPosition: { start: [0, 13], text: "{% elsif cond %}" } + blockEndPosition: { start: [0, 41], text: "" } + position: { start: [0, 13], text: "{% elsif cond %}hi" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 22], text: cond } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 29], text: } + blockEndPosition: { start: [0, 34], text: } + position: { start: [0, 29], text: hi } + name: + - type: TextNode + value: a + position: { start: [0, 30], text: a } + attributes: [] + children: + - type: TextNode + value: hi + position: { start: [0, 32], text: hi } + - type: HtmlElement + blockStartPosition: { start: [0, 38], text: } + blockEndPosition: { start: [0, 41], text: "" } + position: { start: [0, 38], text: } + name: + - type: TextNode + value: b + position: { start: [0, 39], text: b } + attributes: [] + children: [] + - name: should allow unclosed nodes inside conditional and case branches (18) + source: "{% if cond %}{% elsif cond %}hi{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% elsif cond %}hi{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 41], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% elsif cond %}hi{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 22], text: "cond " } + blockStartPosition: { start: [0, 13], text: "{% elsif cond %}" } + blockEndPosition: { start: [0, 41], text: "" } + position: { start: [0, 13], text: "{% elsif cond %}hi" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 22], text: cond } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 29], text: } + blockEndPosition: { start: [0, 41], text: "" } + position: { start: [0, 29], text: hi } + name: + - type: TextNode + value: b + position: { start: [0, 30], text: b } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 32], text: } + blockEndPosition: { start: [0, 37], text: } + position: { start: [0, 32], text: hi } + name: + - type: TextNode + value: a + position: { start: [0, 33], text: a } + attributes: [] + children: + - type: TextNode + value: hi + position: { start: [0, 35], text: hi } + - name: should allow unclosed nodes inside conditional and case branches (19) + source: "{% case cond %}{% when %}hi{% endcase %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% case cond %}{% when %}hi{% endcase %}" } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case cond %}" } + blockEndPosition: { start: [0, 37], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "cond " } + position: { start: [0, 0], text: "{% case cond %}{% when %}hi{% endcase %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 8], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 15], text: "" } + blockStartPosition: { start: [0, 15], text: "" } + blockEndPosition: { start: [0, 15], text: "" } + position: { start: [0, 15], text: "" } + children: [] + - type: LiquidBranch + name: when + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 23], text: "" } + blockStartPosition: { start: [0, 15], text: "{% when %}" } + blockEndPosition: { start: [0, 37], text: "" } + position: { start: [0, 15], text: "{% when %}hi" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 25], text: } + blockEndPosition: { start: [0, 30], text: } + position: { start: [0, 25], text: hi } + name: + - type: TextNode + value: a + position: { start: [0, 26], text: a } + attributes: [] + children: + - type: TextNode + value: hi + position: { start: [0, 28], text: hi } + - type: HtmlElement + blockStartPosition: { start: [0, 34], text: } + blockEndPosition: { start: [0, 37], text: "" } + position: { start: [0, 34], text: } + name: + - type: TextNode + value: b + position: { start: [0, 35], text: b } + attributes: [] + children: [] + - name: should let me write unclosed nodes inside if statements (1) + source: "{% if cond %}
    hello{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}
    hello{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 46], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}
    hello{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 46], text: "" } + position: { start: [0, 13], text:
    hello } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 13], text:
    } + blockEndPosition: { start: [0, 46], text: "" } + position: { start: [0, 13], text:
    hello } + name: + - type: TextNode + value: details + position: { start: [0, 14], text: details } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 22], text: } + blockEndPosition: { start: [0, 36], text: } + position: { start: [0, 22], text: hello } + name: + - type: TextNode + value: summary + position: { start: [0, 23], text: summary } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 31], text: hello } + - name: should let me write unclosed nodes inside if statements (2) + source: "{% if cond %}
    hello{% else %}{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}
    hello{% else %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 56], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}
    hello{% else %}{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 46], text: "" } + position: { start: [0, 13], text:
    hello } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 13], text:
    } + blockEndPosition: { start: [0, 46], text: "" } + position: { start: [0, 13], text:
    hello } + name: + - type: TextNode + value: details + position: { start: [0, 14], text: details } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 22], text: } + blockEndPosition: { start: [0, 36], text: } + position: { start: [0, 22], text: hello } + name: + - type: TextNode + value: summary + position: { start: [0, 23], text: summary } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 31], text: hello } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 54], text: "" } + blockStartPosition: { start: [0, 46], text: "{% else %}" } + blockEndPosition: { start: [0, 56], text: "" } + position: { start: [0, 46], text: "{% else %}" } + children: [] + - name: should let me write unclosed nodes inside if statements (3) + source: "{% if cond %}{% elsif other_cond %}
    hello{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% elsif other_cond %}
    hello{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 68], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% elsif other_cond %}
    hello{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 22], text: "other_cond " } + blockStartPosition: { start: [0, 13], text: "{% elsif other_cond %}" } + blockEndPosition: { start: [0, 68], text: "" } + position: { start: [0, 13], text: "{% elsif other_cond %}
    hello" } + markup: + type: VariableLookup + name: other_cond + position: { start: [0, 22], text: other_cond } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 35], text:
    } + blockEndPosition: { start: [0, 68], text: "" } + position: { start: [0, 35], text:
    hello } + name: + - type: TextNode + value: details + position: { start: [0, 36], text: details } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 44], text: } + blockEndPosition: { start: [0, 58], text: } + position: { start: [0, 44], text: hello } + name: + - type: TextNode + value: summary + position: { start: [0, 45], text: summary } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 53], text: hello } + - name: should let me write unclosed nodes inside if statements (4) + source: "{% if cond %}{% elsif other_cond %}{% else %}
    hello{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% elsif other_cond %}{% else %}
    hello{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 78], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% elsif other_cond %}{% else %}
    hello{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 22], text: "other_cond " } + blockStartPosition: { start: [0, 13], text: "{% elsif other_cond %}" } + blockEndPosition: { start: [0, 35], text: "" } + position: { start: [0, 13], text: "{% elsif other_cond %}" } + markup: + type: VariableLookup + name: other_cond + position: { start: [0, 22], text: other_cond } + lookups: [] + lookupModes: [] + children: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 43], text: "" } + blockStartPosition: { start: [0, 35], text: "{% else %}" } + blockEndPosition: { start: [0, 78], text: "" } + position: { start: [0, 35], text: "{% else %}
    hello" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 45], text:
    } + blockEndPosition: { start: [0, 78], text: "" } + position: { start: [0, 45], text:
    hello } + name: + - type: TextNode + value: details + position: { start: [0, 46], text: details } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 54], text: } + blockEndPosition: { start: [0, 68], text: } + position: { start: [0, 54], text: hello } + name: + - type: TextNode + value: summary + position: { start: [0, 55], text: summary } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 63], text: hello } + - name: should let me write unclosed nodes inside if statements (5) + source: "{% unless cond %}
    hello{% endunless %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% unless cond %}
    hello{% endunless %}" } + children: + - type: LiquidTag + name: unless + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% unless cond %}" } + blockEndPosition: { start: [0, 50], text: "{% endunless %}" } + markupPosition: { start: [0, 10], text: "cond " } + position: { start: [0, 0], text: "{% unless cond %}
    hello{% endunless %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 10], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 17], text: "" } + blockStartPosition: { start: [0, 17], text: "" } + blockEndPosition: { start: [0, 50], text: "" } + position: { start: [0, 17], text:
    hello } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 17], text:
    } + blockEndPosition: { start: [0, 50], text: "" } + position: { start: [0, 17], text:
    hello } + name: + - type: TextNode + value: details + position: { start: [0, 18], text: details } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 26], text: } + blockEndPosition: { start: [0, 40], text: } + position: { start: [0, 26], text: hello } + name: + - type: TextNode + value: summary + position: { start: [0, 27], text: summary } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 35], text: hello } + - name: should let me write unclosed nodes inside if statements (6) + source: "{% case thing %}{% when cond %}
    hello{% endcase %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% case thing %}{% when cond %}
    hello{% endcase %}" } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case thing %}" } + blockEndPosition: { start: [0, 64], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "thing " } + position: { start: [0, 0], text: "{% case thing %}{% when cond %}
    hello{% endcase %}" } + markup: + type: VariableLookup + name: thing + position: { start: [0, 8], text: thing } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 16], text: "" } + blockStartPosition: { start: [0, 16], text: "" } + blockEndPosition: { start: [0, 16], text: "" } + position: { start: [0, 16], text: "" } + children: [] + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 24], text: "cond " } + blockStartPosition: { start: [0, 16], text: "{% when cond %}" } + blockEndPosition: { start: [0, 64], text: "" } + position: { start: [0, 16], text: "{% when cond %}
    hello" } + markup: + - type: VariableLookup + name: cond + position: { start: [0, 24], text: cond } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 31], text:
    } + blockEndPosition: { start: [0, 64], text: "" } + position: { start: [0, 31], text:
    hello } + name: + - type: TextNode + value: details + position: { start: [0, 32], text: details } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 40], text: } + blockEndPosition: { start: [0, 54], text: } + position: { start: [0, 40], text: hello } + name: + - type: TextNode + value: summary + position: { start: [0, 41], text: summary } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 49], text: hello } + - name: should let me write unclosed nodes inside if statements (7) + source: "{% case thing %}{% when cond %}
    hello{% else %}{% endcase %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% case thing %}{% when cond %}
    hello{% else %}{% endcase %}" } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case thing %}" } + blockEndPosition: { start: [0, 74], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "thing " } + position: { start: [0, 0], text: "{% case thing %}{% when cond %}
    hello{% else %}{% endcase %}" } + markup: + type: VariableLookup + name: thing + position: { start: [0, 8], text: thing } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 16], text: "" } + blockStartPosition: { start: [0, 16], text: "" } + blockEndPosition: { start: [0, 16], text: "" } + position: { start: [0, 16], text: "" } + children: [] + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 24], text: "cond " } + blockStartPosition: { start: [0, 16], text: "{% when cond %}" } + blockEndPosition: { start: [0, 64], text: "" } + position: { start: [0, 16], text: "{% when cond %}
    hello" } + markup: + - type: VariableLookup + name: cond + position: { start: [0, 24], text: cond } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 31], text:
    } + blockEndPosition: { start: [0, 64], text: "" } + position: { start: [0, 31], text:
    hello } + name: + - type: TextNode + value: details + position: { start: [0, 32], text: details } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 40], text: } + blockEndPosition: { start: [0, 54], text: } + position: { start: [0, 40], text: hello } + name: + - type: TextNode + value: summary + position: { start: [0, 41], text: summary } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 49], text: hello } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 72], text: "" } + blockStartPosition: { start: [0, 64], text: "{% else %}" } + blockEndPosition: { start: [0, 74], text: "" } + position: { start: [0, 64], text: "{% else %}" } + children: [] + - name: should let me write unclosed nodes inside if statements (8) + source: "{% case thing %}{% else %}
    hello{% endcase %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% case thing %}{% else %}
    hello{% endcase %}" } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case thing %}" } + blockEndPosition: { start: [0, 59], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "thing " } + position: { start: [0, 0], text: "{% case thing %}{% else %}
    hello{% endcase %}" } + markup: + type: VariableLookup + name: thing + position: { start: [0, 8], text: thing } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 16], text: "" } + blockStartPosition: { start: [0, 16], text: "" } + blockEndPosition: { start: [0, 16], text: "" } + position: { start: [0, 16], text: "" } + children: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 24], text: "" } + blockStartPosition: { start: [0, 16], text: "{% else %}" } + blockEndPosition: { start: [0, 59], text: "" } + position: { start: [0, 16], text: "{% else %}
    hello" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 26], text:
    } + blockEndPosition: { start: [0, 59], text: "" } + position: { start: [0, 26], text:
    hello } + name: + - type: TextNode + value: details + position: { start: [0, 27], text: details } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 35], text: } + blockEndPosition: { start: [0, 49], text: } + position: { start: [0, 35], text: hello } + name: + - type: TextNode + value: summary + position: { start: [0, 36], text: summary } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 44], text: hello } + - name: should throw an error when writing an unclosed node inside any other tag (1) + source: "{% for x in y %}
    {% endfor %}" + parse: [html] + error: + message: Attempting to close LiquidTag 'endfor' before HtmlElement 'details' was closed + start: [0, 16] + text:
    + - name: should throw an error when writing an unclosed node inside any other tag (2) + source: "{% tablerow x in y %}
    {% endtablerow %}" + parse: [html] + error: + message: Attempting to close LiquidTag 'endtablerow' before HtmlElement 'details' was closed + start: [0, 21] + text:
    + - name: should throw an error when writing an unclosed node inside any other tag (3) + source: '{% form "cart", cart %}
    {% endform %}' + parse: [html] + error: + message: Attempting to close LiquidTag 'endform' before HtmlElement 'details' was closed + start: [0, 23] + text:
    + - name: should throw when trying to close the wrong node (1) + source:
    + parse: [html] + error: + message: Attempting to close HtmlElement 'a' before HtmlElement 'div' was closed + start: [0, 8] + text: {% endfor %}" + parse: [html] + error: + message: Attempting to close LiquidTag 'endfor' before HtmlElement 'div' was closed + start: [0, 16] + text:
    + - name: should throw when trying to close the wrong node (3) + source: "{% for a in b %}{% if condition %}{% endfor %}" + parse: [html] + error: + message: Attempting to close LiquidTag 'for' before LiquidTag 'if' was closed + start: [0, 16] + text: "{% if condition %}" + - name: should throw when trying to close the wrong node (4) + source: "{% for a in b %}{% if condition %}
    {% endfor %}" + parse: [html] + error: + message: Attempting to close LiquidTag 'for' before LiquidTag 'if' was closed + start: [0, 16] + text: "{% if condition %}" + - name: should throw when trying to close the wrong node (5) + source: <{{ node_type }}>
    + parse: [html] + error: + message: Attempting to close HtmlElement 'compound' before HtmlElement 'div' was closed + start: [0, 22] + text: + parse: [html] + error: + message: Attempting to close HtmlElement 'compound' before HtmlElement 'compound' was closed + start: [0, 17] + text: {% endif %}' + parse: [html] + error: + message: Unexpected Liquid tag 'elsif' inside quoted attribute value + start: [0, 22] + text: "{%" + - name: should throw when trying to open a new branch when a Liquid node was not closed (1) + source: '{% if cond %}{% form "cart", cart %}{% else %}{% endif %}' + parse: [html] + error: + message: Attempting to open LiquidBranch 'else' before LiquidTag 'form' was closed + start: [0, 13] + text: '{% form "cart", cart %}' + - name: should throw when trying to open a new branch when a Liquid node was not closed (2) + source: '{% if cond %}{% form "cart", cart %}{% elsif cond %}{% endif %}' + parse: [html] + error: + message: Attempting to open LiquidBranch 'elsif' before LiquidTag 'form' was closed + start: [0, 13] + text: '{% form "cart", cart %}' + - name: should throw when closing at the top level (1) + source: + parse: [html] + error: + message: Attempting to end parsing before HtmlElement 'a' was closed + start: [0, 0] + text: + parse: [html] + error: + message: Attempting to close HtmlElement 'a' before it was opened + start: [0, 0] + text:
    + parse: [html] + error: + message: Attempting to end parsing before HtmlElement 'div' was closed + start: [0, 3] + text:
    + parse: [html] + error: + message: Attempting to end parsing before HtmlRawNode 'script' was closed + start: [0, 0] + text: + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + + children: + - type: HtmlRawNode + name: script + blockStartPosition: { start: [0, 0], text: } + position: + start: [0, 0] + text: |- + + attributes: [] + body: + type: RawMarkup + kind: javascript + value: |2 + + const a = {{ product | json }}; + position: + start: [0, 8] + text: |2 + + const a = {{ product | json }}; + nodes: + - type: TextNode + value: const a = + position: { start: [1, 2], text: const a = } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 14], text: " product | json " } + position: { start: [1, 12], text: "{{ product | json }}" } + markup: + type: LiquidVariable + rawSource: product | json + position: { start: [1, 15], text: "product | json " } + expression: + type: VariableLookup + name: product + position: { start: [1, 15], text: product } + lookups: [] + lookupModes: [] + filters: + - type: LiquidFilter + name: json + position: { start: [1, 22], text: " | json" } + args: [] + - type: TextNode + value: ; + position: { start: [1, 32], text: ; } + - name: should detect json kind for script type="application/ld+json" + source: '' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '' } + children: + - type: HtmlRawNode + name: script + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: '' } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 14], text: application/ld+json } + position: { start: [0, 8], text: type="application/ld+json" } + name: + - type: TextNode + value: type + position: { start: [0, 8], text: type } + value: + - type: TextNode + value: application/ld+json + position: { start: [0, 14], text: application/ld+json } + body: + type: RawMarkup + kind: json + value: '{"@type": "Product"}' + position: { start: [0, 35], text: '{"@type": "Product"}' } + nodes: + - type: TextNode + value: '{"@type": "Product"}' + position: { start: [0, 35], text: '{"@type": "Product"}' } + - name: should detect json kind for script type="application/json" + source: '' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '' } + children: + - type: HtmlRawNode + name: script + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: '' } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 14], text: application/json } + position: { start: [0, 8], text: type="application/json" } + name: + - type: TextNode + value: type + position: { start: [0, 8], text: type } + value: + - type: TextNode + value: application/json + position: { start: [0, 14], text: application/json } + body: + type: RawMarkup + kind: json + value: '{"key": "value"}' + position: { start: [0, 32], text: '{"key": "value"}' } + nodes: + - type: TextNode + value: '{"key": "value"}' + position: { start: [0, 32], text: '{"key": "value"}' } + - name: should detect json kind for script type="importmap" + source: '' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '' } + children: + - type: HtmlRawNode + name: script + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: '' } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 14], text: importmap } + position: { start: [0, 8], text: type="importmap" } + name: + - type: TextNode + value: type + position: { start: [0, 8], text: type } + value: + - type: TextNode + value: importmap + position: { start: [0, 14], text: importmap } + body: + type: RawMarkup + kind: json + value: '{"imports": {}}' + position: { start: [0, 25], text: '{"imports": {}}' } + nodes: + - type: TextNode + value: '{"imports": {}}' + position: { start: [0, 25], text: '{"imports": {}}' } + - name: should detect json kind for script type="speculationrules" + source: '' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '' } + children: + - type: HtmlRawNode + name: script + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: '' } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 14], text: speculationrules } + position: { start: [0, 8], text: type="speculationrules" } + name: + - type: TextNode + value: type + position: { start: [0, 8], text: type } + value: + - type: TextNode + value: speculationrules + position: { start: [0, 14], text: speculationrules } + body: + type: RawMarkup + kind: json + value: '{"prerender": []}' + position: { start: [0, 32], text: '{"prerender": []}' } + nodes: + - type: TextNode + value: '{"prerender": []}' + position: { start: [0, 32], text: '{"prerender": []}' } + - name: should detect javascript kind for script with no type attribute + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: HtmlRawNode + name: script + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: } + attributes: [] + body: + type: RawMarkup + kind: javascript + value: var x = 1; + position: { start: [0, 8], text: var x = 1; } + nodes: + - type: TextNode + value: var x = 1; + position: { start: [0, 8], text: var x = 1; } + - name: should parse style tags as raw markup + source: |- + + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + + children: + - type: HtmlRawNode + name: style + blockStartPosition: { start: [0, 0], text: } + position: + start: [0, 0] + text: |- + + attributes: [] + body: + type: RawMarkup + kind: text + value: |2 + + :root { --bg: {{ settings.bg }}} + position: + start: [0, 7] + text: |2 + + :root { --bg: {{ settings.bg }}} + nodes: + - type: TextNode + value: ":root { --bg:" + position: { start: [1, 2], text: ":root { --bg:" } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 18], text: " settings.bg " } + position: { start: [1, 16], text: "{{ settings.bg }}" } + markup: + type: LiquidVariable + rawSource: settings.bg + position: { start: [1, 19], text: "settings.bg " } + expression: + type: VariableLookup + name: settings + position: { start: [1, 19], text: settings.bg } + lookups: + - type: String + value: bg + position: { start: [1, 28], text: bg } + lookupModes: [bareword] + filters: [] + - type: TextNode + value: "}" + position: { start: [1, 33], text: "}" } + - name: should parse a simple HTML element + source:
    hello
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    hello
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text:
    } + blockEndPosition: { start: [0, 10], text:
    } + position: { start: [0, 0], text:
    hello
    } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 5], text: hello } + - name: should treat HTML as text in toLiquidAST mode + source:
    hello
    + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    hello
    } + children: + - type: TextNode + value:
    hello
    + position: { start: [0, 0], text:
    hello
    } + - name: should parse nested HTML elements + source:
    text
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    text
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text:
    } + blockEndPosition: { start: [0, 22], text:
    } + position: { start: [0, 0], text:
    text
    } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 5], text: } + blockEndPosition: { start: [0, 15], text: } + position: { start: [0, 5], text: text } + name: + - type: TextNode + value: span + position: { start: [0, 6], text: span } + attributes: [] + children: + - type: TextNode + value: text + position: { start: [0, 11], text: text } + - name: should parse void elements + source:
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    } + children: + - type: HtmlVoidElement + name: br + blockStartPosition: { start: [0, 0], text:
    } + position: { start: [0, 0], text:
    } + attributes: [] + - name: should parse void elements with attributes + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 10], text: photo.jpg } + position: { start: [0, 5], text: src="photo.jpg" } + name: + - type: TextNode + value: src + position: { start: [0, 5], text: src } + value: + - type: TextNode + value: photo.jpg + position: { start: [0, 10], text: photo.jpg } + - name: should parse self-closing elements + source:
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    } + children: + - type: HtmlSelfClosingElement + blockStartPosition: { start: [0, 0], text:
    } + position: { start: [0, 0], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: [] + - name: should parse all attribute types + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 10], text: x } + position: { start: [0, 5], text: src="x" } + name: + - type: TextNode + value: src + position: { start: [0, 5], text: src } + value: + - type: TextNode + value: x + position: { start: [0, 10], text: x } + - type: AttrSingleQuoted + attributePosition: { start: [0, 22], text: lazy } + position: { start: [0, 13], text: loading='lazy' } + name: + - type: TextNode + value: loading + position: { start: [0, 13], text: loading } + value: + - type: TextNode + value: lazy + position: { start: [0, 22], text: lazy } + - type: AttrEmpty + position: { start: [0, 28], text: disabled } + name: + - type: TextNode + value: disabled + position: { start: [0, 28], text: disabled } + - type: AttrDoubleQuoted + attributePosition: { start: [0, 46], text: "" } + position: { start: [0, 37], text: checked="" } + name: + - type: TextNode + value: checked + position: { start: [0, 37], text: checked } + value: [] + - name: should parse unquoted attribute values + source:
    bar
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    bar
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text:
    } + blockEndPosition: { start: [0, 18], text:
    } + position: { start: [0, 0], text:
    bar
    } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: + - type: AttrUnquoted + attributePosition: { start: [0, 11], text: foo } + position: { start: [0, 5], text: class=foo } + name: + - type: TextNode + value: class + position: { start: [0, 5], text: class } + value: + - type: TextNode + value: foo + position: { start: [0, 11], text: foo } + children: + - type: TextNode + value: bar + position: { start: [0, 15], text: bar } + - name: "should parse whitespace around `=` as one attribute: \"\"" + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: "" } + position: { start: [0, 0], text: "" } + attributes: + - type: AttrSingleQuoted + attributePosition: { start: [0, 19], text: "{{ ratio }}" } + position: { start: [0, 5], text: "data-ratio = '{{ ratio }}'" } + name: + - type: TextNode + value: data-ratio + position: { start: [0, 5], text: data-ratio } + value: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 21], text: " ratio " } + position: { start: [0, 19], text: "{{ ratio }}" } + markup: + type: LiquidVariable + rawSource: ratio + position: { start: [0, 22], text: "ratio " } + expression: + type: VariableLookup + name: ratio + position: { start: [0, 22], text: ratio } + lookups: [] + lookupModes: [] + filters: [] + - name: 'should parse whitespace around `=` as one attribute: ""' + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '' } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: '' } + position: { start: [0, 0], text: '' } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 14], text: "{{ a }}, {{ b }} 2x" } + position: { start: [0, 5], text: 'srcset= "{{ a }}, {{ b }} 2x"' } + name: + - type: TextNode + value: srcset + position: { start: [0, 5], text: srcset } + value: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 16], text: " a " } + position: { start: [0, 14], text: "{{ a }}" } + markup: + type: LiquidVariable + rawSource: a + position: { start: [0, 17], text: "a " } + expression: + type: VariableLookup + name: a + position: { start: [0, 17], text: a } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value: ", " + position: { start: [0, 21], text: ", " } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 25], text: " b " } + position: { start: [0, 23], text: "{{ b }}" } + markup: + type: LiquidVariable + rawSource: b + position: { start: [0, 26], text: "b " } + expression: + type: VariableLookup + name: b + position: { start: [0, 26], text: b } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value: " 2x" + position: { start: [0, 30], text: " 2x" } + - name: 'should parse whitespace around `=` as one attribute: "\"x\""' + source: x + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: x } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: x } + position: { start: [0, 0], text: x } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 11], text: x } + position: { start: [0, 5], text: alt ="x" } + name: + - type: TextNode + value: alt + position: { start: [0, 5], text: alt } + value: + - type: TextNode + value: x + position: { start: [0, 11], text: x } + - name: 'should parse whitespace around `=` as one attribute: "x"' + source: x + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: x } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: x } + position: { start: [0, 0], text: x } + attributes: + - type: AttrUnquoted + attributePosition: { start: [0, 11], text: x } + position: { start: [0, 5], text: alt = x } + name: + - type: TextNode + value: alt + position: { start: [0, 5], text: alt } + value: + - type: TextNode + value: x + position: { start: [0, 11], text: x } + - name: 'should parse whitespace around `=` as one attribute: ""' + source: |- + + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + + children: + - type: HtmlVoidElement + name: img + blockStartPosition: + start: [0, 0] + text: |- + + position: + start: [0, 0] + text: |- + + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [2, 3], text: x } + position: + start: [0, 5] + text: |- + alt + = + "x" + name: + - type: TextNode + value: alt + position: { start: [0, 5], text: alt } + value: + - type: TextNode + value: x + position: { start: [2, 3], text: x } + - name: should keep whitespace-separated attributes apart when no `=` follows + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: + - type: AttrEmpty + position: { start: [0, 5], text: hidden } + name: + - type: TextNode + value: hidden + position: { start: [0, 5], text: hidden } + - type: AttrDoubleQuoted + attributePosition: { start: [0, 20], text: x } + position: { start: [0, 13], text: class="x" } + name: + - type: TextNode + value: class + position: { start: [0, 13], text: class } + value: + - type: TextNode + value: x + position: { start: [0, 20], text: x } + children: + - type: TextNode + value: y + position: { start: [0, 23], text: y } + - name: should parse whitespace around `=` inside a Liquid branch in attribute position + source:
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '
    ' } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: '
    ' } + blockEndPosition: { start: [0, 45], text:
    } + position: { start: [0, 0], text: '
    ' } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 5], text: "{% if a %}" } + blockEndPosition: { start: [0, 33], text: "{% endif %}" } + markupPosition: { start: [0, 11], text: "a " } + position: { start: [0, 5], text: '{% if a %}data-x = "{{ v }}"{% endif %}' } + markup: + type: VariableLookup + name: a + position: { start: [0, 11], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 15], text: "" } + blockStartPosition: { start: [0, 15], text: "" } + blockEndPosition: { start: [0, 33], text: "" } + position: { start: [0, 15], text: 'data-x = "{{ v }}"' } + children: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 25], text: "{{ v }}" } + position: { start: [0, 15], text: 'data-x = "{{ v }}"' } + name: + - type: TextNode + value: data-x + position: { start: [0, 15], text: data-x } + value: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 27], text: " v " } + position: { start: [0, 25], text: "{{ v }}" } + markup: + type: LiquidVariable + rawSource: v + position: { start: [0, 28], text: "v " } + expression: + type: VariableLookup + name: v + position: { start: [0, 28], text: v } + lookups: [] + lookupModes: [] + filters: [] + children: [] + - name: should parse Liquid drops in attribute values + source:
    text
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '
    text
    ' } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: '
    ' } + blockEndPosition: { start: [0, 25], text:
    } + position: { start: [0, 0], text: '
    text
    ' } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 12], text: "{{ x }}" } + position: { start: [0, 5], text: 'class="{{ x }}"' } + name: + - type: TextNode + value: class + position: { start: [0, 5], text: class } + value: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 14], text: " x " } + position: { start: [0, 12], text: "{{ x }}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [0, 15], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [0, 15], text: x } + lookups: [] + lookupModes: [] + filters: [] + children: + - type: TextNode + value: text + position: { start: [0, 21], text: text } + - name: should parse Liquid tags between attributes + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '' } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: '' } + position: { start: [0, 0], text: '' } + attributes: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 5], text: "{% if cond %}" } + blockEndPosition: { start: [0, 25], text: "{% endif %}" } + markupPosition: { start: [0, 11], text: "cond " } + position: { start: [0, 5], text: '{% if cond %}src="x"{% endif %}' } + markup: + type: VariableLookup + name: cond + position: { start: [0, 11], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 18], text: "" } + blockStartPosition: { start: [0, 18], text: "" } + blockEndPosition: { start: [0, 25], text: "" } + position: { start: [0, 18], text: src="x" } + children: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 23], text: x } + position: { start: [0, 18], text: src="x" } + name: + - type: TextNode + value: src + position: { start: [0, 18], text: src } + value: + - type: TextNode + value: x + position: { start: [0, 23], text: x } + - name: should NOT swallow a Liquid tag glued to an attribute name into the name + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "" } + blockEndPosition: { start: [0, 93], text: } + position: { start: [0, 0], text: "" } + name: + - type: TextNode + value: s-scroll-dock + position: { start: [0, 1], text: s-scroll-dock } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 22], text: block } + position: { start: [0, 15], text: class="block" } + name: + - type: TextNode + value: class + position: { start: [0, 15], text: class } + value: + - type: TextNode + value: block + position: { start: [0, 22], text: block } + - type: AttrEmpty + position: { start: [0, 29], text: data-dock } + name: + - type: TextNode + value: data-dock + position: { start: [0, 29], text: data-dock } + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 38], text: "{% if template == 'index' %}" } + blockEndPosition: { start: [0, 81], text: "{% endif %}" } + markupPosition: { start: [0, 44], text: "template == 'index' " } + position: { start: [0, 38], text: "{% if template == 'index' %} data-immersive{% endif %}" } + markup: + type: Comparison + comparator: == + position: { start: [0, 44], text: template == 'index' } + left: + type: VariableLookup + name: template + position: { start: [0, 44], text: template } + lookups: [] + lookupModes: [] + right: + type: String + single: true + value: index + position: { start: [0, 56], text: "'index'" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 66], text: "" } + blockStartPosition: { start: [0, 66], text: "" } + blockEndPosition: { start: [0, 81], text: "" } + position: { start: [0, 66], text: " data-immersive" } + children: + - type: AttrEmpty + position: { start: [0, 67], text: data-immersive } + name: + - type: TextNode + value: data-immersive + position: { start: [0, 67], text: data-immersive } + children: [] + - name: should parse a Liquid tag glued to an attribute name on a void element + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: "" } + position: { start: [0, 0], text: "" } + attributes: + - type: AttrEmpty + position: { start: [0, 5], text: data-x } + name: + - type: TextNode + value: data-x + position: { start: [0, 5], text: data-x } + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 11], text: "{% if cond %}" } + blockEndPosition: { start: [0, 31], text: "{% endif %}" } + markupPosition: { start: [0, 17], text: "cond " } + position: { start: [0, 11], text: "{% if cond %} data-y{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 17], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 24], text: "" } + blockStartPosition: { start: [0, 24], text: "" } + blockEndPosition: { start: [0, 31], text: "" } + position: { start: [0, 24], text: " data-y" } + children: + - type: AttrEmpty + position: { start: [0, 25], text: data-y } + name: + - type: TextNode + value: data-y + position: { start: [0, 25], text: data-y } + - name: should parse compound tag names with Liquid drops + source: <{{ node_type }}>content + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{{ node_type }}>content" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{{ node_type }}>" } + blockEndPosition: { start: [0, 24], text: "" } + position: { start: [0, 0], text: "<{{ node_type }}>content" } + name: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 3], text: " node_type " } + position: { start: [0, 1], text: "{{ node_type }}" } + markup: + type: LiquidVariable + rawSource: node_type + position: { start: [0, 4], text: "node_type " } + expression: + type: VariableLookup + name: node_type + position: { start: [0, 4], text: node_type } + lookups: [] + lookupModes: [] + filters: [] + attributes: [] + children: + - type: TextNode + value: content + position: { start: [0, 17], text: content } + - name: should parse compound names with text and Liquid segments + source: <{{ type }}--header> + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{{ type }}--header>" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{{ type }}--header>" } + blockEndPosition: { start: [0, 20], text: "" } + position: { start: [0, 0], text: "<{{ type }}--header>" } + name: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 3], text: " type " } + position: { start: [0, 1], text: "{{ type }}" } + markup: + type: LiquidVariable + rawSource: type + position: { start: [0, 4], text: "type " } + expression: + type: VariableLookup + name: type + position: { start: [0, 4], text: type } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value: --header + position: { start: [0, 11], text: --header } + attributes: [] + children: [] + - name: should parse compound tag names with Liquid tags (conditionals) + source: <{% if true %}sticky-header{% else %}div{% endif %}>content + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{% if true %}sticky-header{% else %}div{% endif %}>content" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{% if true %}sticky-header{% else %}div{% endif %}>" } + blockEndPosition: { start: [0, 59], text: "" } + position: { start: [0, 0], text: "<{% if true %}sticky-header{% else %}div{% endif %}>content" } + name: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 1], text: "{% if true %}" } + blockEndPosition: { start: [0, 40], text: "{% endif %}" } + markupPosition: { start: [0, 7], text: "true " } + position: { start: [0, 1], text: "{% if true %}sticky-header{% else %}div{% endif %}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 7], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 14], text: "" } + blockStartPosition: { start: [0, 14], text: "" } + blockEndPosition: { start: [0, 27], text: "" } + position: { start: [0, 14], text: sticky-header } + children: + - type: TextNode + value: sticky-header + position: { start: [0, 14], text: sticky-header } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 35], text: "" } + blockStartPosition: { start: [0, 27], text: "{% else %}" } + blockEndPosition: { start: [0, 40], text: "" } + position: { start: [0, 27], text: "{% else %}div" } + children: + - type: TextNode + value: div + position: { start: [0, 37], text: div } + attributes: [] + children: + - type: TextNode + value: content + position: { start: [0, 52], text: content } + - name: should parse compound tag names mixing Liquid tags and text + source: <{% if true %}sticky{% endif %}-header>content + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "<{% if true %}sticky{% endif %}-header>content" } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "<{% if true %}sticky{% endif %}-header>" } + blockEndPosition: { start: [0, 46], text: "" } + position: { start: [0, 0], text: "<{% if true %}sticky{% endif %}-header>content" } + name: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 1], text: "{% if true %}" } + blockEndPosition: { start: [0, 20], text: "{% endif %}" } + markupPosition: { start: [0, 7], text: "true " } + position: { start: [0, 1], text: "{% if true %}sticky{% endif %}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 7], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 14], text: "" } + blockStartPosition: { start: [0, 14], text: "" } + blockEndPosition: { start: [0, 20], text: "" } + position: { start: [0, 14], text: sticky } + children: + - type: TextNode + value: sticky + position: { start: [0, 14], text: sticky } + - type: TextNode + value: -header + position: { start: [0, 31], text: -header } + attributes: [] + children: + - type: TextNode + value: content + position: { start: [0, 39], text: content } + - name: should treat a Liquid tag glued to a text tag name as an attribute, not part of the name + source:
    + parse: [html] + error: + message: Attempting to close HtmlElement 'div' before HtmlElement 'compound' was closed + start: [0, 36] + text:
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: HtmlComment + body: hello + position: { start: [0, 0], text: } + - name: should parse style tags as HtmlRawNode + source: "" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlRawNode + name: style + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: "" } + attributes: [] + body: + type: RawMarkup + kind: css + value: ".x { color: red; }" + position: { start: [0, 7], text: ".x { color: red; }" } + nodes: + - type: TextNode + value: ".x { color: red; }" + position: { start: [0, 7], text: ".x { color: red; }" } + - name: should keep a script close-tag candidate with a non-ASCII space in the body + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: HtmlRawNode + name: script + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: } + attributes: [] + body: + type: RawMarkup + kind: javascript + value: x = ""; + position: { start: [0, 8], text: x = ""; } + nodes: + - type: TextNode + value: x = ""; + position: { start: [0, 8], text: x = ""; } + - name: should keep a style close-tag candidate with a non-ASCII space in the body + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: HtmlRawNode + name: style + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: } + attributes: [] + body: + type: RawMarkup + kind: css + value: x = ""; + position: { start: [0, 7], text: x = ""; } + nodes: + - type: TextNode + value: x = ""; + position: { start: [0, 7], text: x = ""; } + - name: should parse Liquid drops inside raw HTML body + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlRawNode + name: script + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: "" } + attributes: [] + body: + type: RawMarkup + kind: javascript + value: const a = {{ product | json }}; + position: { start: [0, 8], text: "const a = {{ product | json }};" } + nodes: + - type: TextNode + value: const a = + position: { start: [0, 8], text: const a = } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 20], text: " product | json " } + position: { start: [0, 18], text: "{{ product | json }}" } + markup: + type: LiquidVariable + rawSource: product | json + position: { start: [0, 21], text: "product | json " } + expression: + type: VariableLookup + name: product + position: { start: [0, 21], text: product } + lookups: [] + lookupModes: [] + filters: + - type: LiquidFilter + name: json + position: { start: [0, 28], text: " | json" } + args: [] + - type: TextNode + value: ; + position: { start: [0, 38], text: ; } + - name: should parse HTML doctype + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: } + children: + - type: HtmlDoctype + legacyDoctypeString: null + position: { start: [0, 0], text: } + - name: should parse HTML inside Liquid blocks + source: "{% if x %}
    hello
    {% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if x %}
    hello
    {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if x %}" } + blockEndPosition: { start: [0, 26], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "x " } + position: { start: [0, 0], text: "{% if x %}
    hello
    {% endif %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 6], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 10], text: "" } + blockStartPosition: { start: [0, 10], text: "" } + blockEndPosition: { start: [0, 26], text: "" } + position: { start: [0, 10], text:
    hello
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 10], text:
    } + blockEndPosition: { start: [0, 20], text:
    } + position: { start: [0, 10], text:
    hello
    } + name: + - type: TextNode + value: div + position: { start: [0, 11], text: div } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 15], text: hello } + - name: should parse Liquid inside HTML children + source:
    {{ x }}
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "
    {{ x }}
    " } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text:
    } + blockEndPosition: { start: [0, 12], text:
    } + position: { start: [0, 0], text: "
    {{ x }}
    " } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: [] + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 7], text: " x " } + position: { start: [0, 5], text: "{{ x }}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [0, 8], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [0, 8], text: x } + lookups: [] + lookupModes: [] + filters: [] + - name: should parse empty HTML element + source:
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text:
    } + blockEndPosition: { start: [0, 5], text:
    } + position: { start: [0, 0], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: [] + children: [] + - name: should parse adjacent HTML elements + source:

    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:

    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text:
    } + blockEndPosition: { start: [0, 5], text:
    } + position: { start: [0, 0], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: [] + children: [] + - type: HtmlElement + blockStartPosition: { start: [0, 11], text:

    } + blockEndPosition: { start: [0, 14], text:

    } + position: { start: [0, 11], text:

    } + name: + - type: TextNode + value: p + position: { start: [0, 12], text: p } + attributes: [] + children: [] + - name: should set blockStartPosition and blockEndPosition correctly + source:
    content
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    content
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text:
    } + blockEndPosition: { start: [0, 22], text:
    } + position: { start: [0, 0], text:
    content
    } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 12], text: x } + position: { start: [0, 5], text: class="x" } + name: + - type: TextNode + value: class + position: { start: [0, 5], text: class } + value: + - type: TextNode + value: x + position: { start: [0, 12], text: x } + children: + - type: TextNode + value: content + position: { start: [0, 15], text: content } + - name: should set attributePosition correctly on quoted attributes + source:
    bar
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    bar
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text:
    } + blockEndPosition: { start: [0, 20], text:
    } + position: { start: [0, 0], text:
    bar
    } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 12], text: foo } + position: { start: [0, 5], text: class="foo" } + name: + - type: TextNode + value: class + position: { start: [0, 5], text: class } + value: + - type: TextNode + value: foo + position: { start: [0, 12], text: foo } + children: + - type: TextNode + value: bar + position: { start: [0, 17], text: bar } + - name: should parse void elements with self-closing syntax as HtmlSelfClosingElement + source:
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text:
    } + children: + - type: HtmlSelfClosingElement + blockStartPosition: { start: [0, 0], text:
    } + position: { start: [0, 0], text:
    } + name: + - type: TextNode + value: br + position: { start: [0, 1], text: br } + attributes: [] + - name: should parse text after liquid tag inside quoted attribute value as TextNode, not AttrEmpty (bug 45) + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '' } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: '' } + blockEndPosition: { start: [0, 60], text: } + position: { start: [0, 0], text: '' } + name: + - type: TextNode + value: span + position: { start: [0, 1], text: span } + attributes: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 6], text: "{% if x %}" } + blockEndPosition: { start: [0, 48], text: "{% endif %}" } + markupPosition: { start: [0, 12], text: "x " } + position: { start: [0, 6], text: '{% if x %}class="a{% if y %} b{% endif %}"{% endif %}' } + markup: + type: VariableLookup + name: x + position: { start: [0, 12], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 16], text: "" } + blockStartPosition: { start: [0, 16], text: "" } + blockEndPosition: { start: [0, 48], text: "" } + position: { start: [0, 16], text: 'class="a{% if y %} b{% endif %}"' } + children: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 23], text: "a{% if y %} b{% endif %}" } + position: { start: [0, 16], text: 'class="a{% if y %} b{% endif %}"' } + name: + - type: TextNode + value: class + position: { start: [0, 16], text: class } + value: + - type: TextNode + value: a + position: { start: [0, 23], text: a } + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 24], text: "{% if y %}" } + blockEndPosition: { start: [0, 36], text: "{% endif %}" } + markupPosition: { start: [0, 30], text: "y " } + position: { start: [0, 24], text: "{% if y %} b{% endif %}" } + markup: + type: VariableLookup + name: y + position: { start: [0, 30], text: y } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 34], text: "" } + blockStartPosition: { start: [0, 34], text: "" } + blockEndPosition: { start: [0, 36], text: "" } + position: { start: [0, 34], text: " b" } + children: + - type: TextNode + value: " b" + position: { start: [0, 34], text: " b" } + children: [] diff --git a/specs/parser/liquid-blocks.yml b/specs/parser/liquid-blocks.yml new file mode 100644 index 000000000..deb7773b2 --- /dev/null +++ b/specs/parser/liquid-blocks.yml @@ -0,0 +1,1096 @@ +cases: + - name: should parse {% capture x %}hello{% endcapture %} + source: "{% capture x %}hello{% endcapture %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% capture x %}hello{% endcapture %}" } + children: + - type: LiquidTag + name: capture + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% capture x %}" } + blockEndPosition: { start: [0, 20], text: "{% endcapture %}" } + markupPosition: { start: [0, 11], text: "x " } + position: { start: [0, 0], text: "{% capture x %}hello{% endcapture %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 11], text: x } + lookups: [] + lookupModes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 15], text: hello } + - name: should parse {% ifchanged %}content{% endifchanged %} + source: "{% ifchanged %}content{% endifchanged %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% ifchanged %}content{% endifchanged %}" } + children: + - type: LiquidTag + name: ifchanged + markup: null + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% ifchanged %}" } + blockEndPosition: { start: [0, 22], text: "{% endifchanged %}" } + markupPosition: { start: [0, 13], text: "" } + position: { start: [0, 0], text: "{% ifchanged %}content{% endifchanged %}" } + children: + - type: TextNode + value: content + position: { start: [0, 15], text: content } + - name: should parse empty branchless block + source: "{% capture x %}{% endcapture %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% capture x %}{% endcapture %}" } + children: + - type: LiquidTag + name: capture + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% capture x %}" } + blockEndPosition: { start: [0, 15], text: "{% endcapture %}" } + markupPosition: { start: [0, 11], text: "x " } + position: { start: [0, 0], text: "{% capture x %}{% endcapture %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 11], text: x } + lookups: [] + lookupModes: [] + children: [] + - name: should parse block tag with Liquid children + source: "{% capture x %}{{ y }}{% endcapture %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% capture x %}{{ y }}{% endcapture %}" } + children: + - type: LiquidTag + name: capture + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% capture x %}" } + blockEndPosition: { start: [0, 22], text: "{% endcapture %}" } + markupPosition: { start: [0, 11], text: "x " } + position: { start: [0, 0], text: "{% capture x %}{{ y }}{% endcapture %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 11], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 17], text: " y " } + position: { start: [0, 15], text: "{{ y }}" } + markup: + type: LiquidVariable + rawSource: y + position: { start: [0, 18], text: "y " } + expression: + type: VariableLookup + name: y + position: { start: [0, 18], text: y } + lookups: [] + lookupModes: [] + filters: [] + - name: should set delimiterWhitespace for end tag + source: "{% capture x %}hello{%- endcapture -%}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% capture x %}hello{%- endcapture -%}" } + children: + - type: LiquidTag + name: capture + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: "{% capture x %}" } + blockEndPosition: { start: [0, 20], text: "{%- endcapture -%}" } + markupPosition: { start: [0, 11], text: "x " } + position: { start: [0, 0], text: "{% capture x %}hello{%- endcapture -%}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 11], text: x } + lookups: [] + lookupModes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 15], text: hello } + - name: should fall back to base case for invalid markup (tolerant) + source: "{% capture x y z %}hello{% endcapture %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% capture x y z %}hello{% endcapture %}" } + children: + - type: LiquidTag + name: capture + markup: x y z + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% capture x y z %}" } + blockEndPosition: { start: [0, 24], text: "{% endcapture %}" } + markupPosition: { start: [0, 11], text: "x y z " } + position: { start: [0, 0], text: "{% capture x y z %}hello{% endcapture %}" } + reason: unexpected tokens after markup + children: + - type: TextNode + value: hello + position: { start: [0, 19], text: hello } + - name: should parse HTML elements inside block children + source: "{% capture x %}
    hello
    {% endcapture %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% capture x %}
    hello
    {% endcapture %}" } + children: + - type: LiquidTag + name: capture + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% capture x %}" } + blockEndPosition: { start: [0, 31], text: "{% endcapture %}" } + markupPosition: { start: [0, 11], text: "x " } + position: { start: [0, 0], text: "{% capture x %}
    hello
    {% endcapture %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 11], text: x } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 15], text:
    } + blockEndPosition: { start: [0, 25], text:
    } + position: { start: [0, 15], text:
    hello
    } + name: + - type: TextNode + value: div + position: { start: [0, 16], text: div } + attributes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 20], text: hello } + - name: should wrap children in LiquidBranch for {% if %} + source: "{% if true %}hello{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if true %}hello{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if true %}" } + blockEndPosition: { start: [0, 18], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "true " } + position: { start: [0, 0], text: "{% if true %}hello{% endif %}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 6], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 18], text: "" } + position: { start: [0, 13], text: hello } + children: + - type: TextNode + value: hello + position: { start: [0, 13], text: hello } + - name: should parse {% if %}...{% else %}...{% endif %} + source: "{% if true %}a{% else %}b{% endif %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if true %}a{% else %}b{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if true %}" } + blockEndPosition: { start: [0, 25], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "true " } + position: { start: [0, 0], text: "{% if true %}a{% else %}b{% endif %}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 6], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 14], text: "" } + position: { start: [0, 13], text: a } + children: + - type: TextNode + value: a + position: { start: [0, 13], text: a } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 22], text: "" } + blockStartPosition: { start: [0, 14], text: "{% else %}" } + blockEndPosition: { start: [0, 25], text: "" } + position: { start: [0, 14], text: "{% else %}b" } + children: + - type: TextNode + value: b + position: { start: [0, 24], text: b } + - name: should parse {% if %}...{% elsif %}...{% else %}...{% endif %} + source: "{% if a %}1{% elsif b %}2{% else %}3{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if a %}1{% elsif b %}2{% else %}3{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if a %}" } + blockEndPosition: { start: [0, 36], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "a " } + position: { start: [0, 0], text: "{% if a %}1{% elsif b %}2{% else %}3{% endif %}" } + markup: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 10], text: "" } + blockStartPosition: { start: [0, 10], text: "" } + blockEndPosition: { start: [0, 11], text: "" } + position: { start: [0, 10], text: "1" } + children: + - type: TextNode + value: "1" + position: { start: [0, 10], text: "1" } + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 20], text: "b " } + blockStartPosition: { start: [0, 11], text: "{% elsif b %}" } + blockEndPosition: { start: [0, 25], text: "" } + position: { start: [0, 11], text: "{% elsif b %}2" } + markup: + type: VariableLookup + name: b + position: { start: [0, 20], text: b } + lookups: [] + lookupModes: [] + children: + - type: TextNode + value: "2" + position: { start: [0, 24], text: "2" } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 33], text: "" } + blockStartPosition: { start: [0, 25], text: "{% else %}" } + blockEndPosition: { start: [0, 36], text: "" } + position: { start: [0, 25], text: "{% else %}3" } + children: + - type: TextNode + value: "3" + position: { start: [0, 35], text: "3" } + - name: should parse {% case %}...{% when %}...{% else %}...{% endcase %} + source: "{% case x %}{% when 1 %}one{% when 2 %}two{% else %}other{% endcase %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% case x %}{% when 1 %}one{% when 2 %}two{% else %}other{% endcase %}" } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case x %}" } + blockEndPosition: { start: [0, 57], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "x " } + position: { start: [0, 0], text: "{% case x %}{% when 1 %}one{% when 2 %}two{% else %}other{% endcase %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 8], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 12], text: "" } + blockStartPosition: { start: [0, 12], text: "" } + blockEndPosition: { start: [0, 12], text: "" } + position: { start: [0, 12], text: "" } + children: [] + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 20], text: "1 " } + blockStartPosition: { start: [0, 12], text: "{% when 1 %}" } + blockEndPosition: { start: [0, 27], text: "" } + position: { start: [0, 12], text: "{% when 1 %}one" } + markup: + - type: Number + value: "1" + position: { start: [0, 20], text: "1" } + children: + - type: TextNode + value: one + position: { start: [0, 24], text: one } + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 35], text: "2 " } + blockStartPosition: { start: [0, 27], text: "{% when 2 %}" } + blockEndPosition: { start: [0, 42], text: "" } + position: { start: [0, 27], text: "{% when 2 %}two" } + markup: + - type: Number + value: "2" + position: { start: [0, 35], text: "2" } + children: + - type: TextNode + value: two + position: { start: [0, 39], text: two } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 50], text: "" } + blockStartPosition: { start: [0, 42], text: "{% else %}" } + blockEndPosition: { start: [0, 57], text: "" } + position: { start: [0, 42], text: "{% else %}other" } + children: + - type: TextNode + value: other + position: { start: [0, 52], text: other } + - name: should parse {% for %}...{% else %}...{% endfor %} + source: "{% for item in items %}{{ item }}{% else %}empty{% endfor %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% for item in items %}{{ item }}{% else %}empty{% endfor %}" } + children: + - type: LiquidTag + name: for + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% for item in items %}" } + blockEndPosition: { start: [0, 48], text: "{% endfor %}" } + markupPosition: { start: [0, 7], text: "item in items " } + position: { start: [0, 0], text: "{% for item in items %}{{ item }}{% else %}empty{% endfor %}" } + markup: + type: ForMarkup + variableName: item + reversed: false + position: { start: [0, 7], text: "item in items " } + collection: + type: VariableLookup + name: items + position: { start: [0, 15], text: items } + lookups: [] + lookupModes: [] + args: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 23], text: "" } + blockStartPosition: { start: [0, 23], text: "" } + blockEndPosition: { start: [0, 33], text: "" } + position: { start: [0, 23], text: "{{ item }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 25], text: " item " } + position: { start: [0, 23], text: "{{ item }}" } + markup: + type: LiquidVariable + rawSource: item + position: { start: [0, 26], text: "item " } + expression: + type: VariableLookup + name: item + position: { start: [0, 26], text: item } + lookups: [] + lookupModes: [] + filters: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 41], text: "" } + blockStartPosition: { start: [0, 33], text: "{% else %}" } + blockEndPosition: { start: [0, 48], text: "" } + position: { start: [0, 33], text: "{% else %}empty" } + children: + - type: TextNode + value: empty + position: { start: [0, 43], text: empty } + - name: should parse empty branched block + source: "{% if true %}{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if true %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if true %}" } + blockEndPosition: { start: [0, 13], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "true " } + position: { start: [0, 0], text: "{% if true %}{% endif %}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 6], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 13], text: "" } + position: { start: [0, 13], text: "" } + children: [] + - name: should parse {% when %} with multiple values + source: "{% case x %}{% when 1, 2 %}matched{% endcase %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% case x %}{% when 1, 2 %}matched{% endcase %}" } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case x %}" } + blockEndPosition: { start: [0, 34], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "x " } + position: { start: [0, 0], text: "{% case x %}{% when 1, 2 %}matched{% endcase %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 8], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 12], text: "" } + blockStartPosition: { start: [0, 12], text: "" } + blockEndPosition: { start: [0, 12], text: "" } + position: { start: [0, 12], text: "" } + children: [] + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 20], text: "1, 2 " } + blockStartPosition: { start: [0, 12], text: "{% when 1, 2 %}" } + blockEndPosition: { start: [0, 34], text: "" } + position: { start: [0, 12], text: "{% when 1, 2 %}matched" } + markup: + - type: Number + value: "1" + position: { start: [0, 20], text: "1" } + - type: Number + value: "2" + position: { start: [0, 23], text: "2" } + children: + - type: TextNode + value: matched + position: { start: [0, 27], text: matched } + - name: should expose branch markup positions + source: "{% case x %}{% when 1, 2 %}matched{% else %}other{% endcase %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% case x %}{% when 1, 2 %}matched{% else %}other{% endcase %}" } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case x %}" } + blockEndPosition: { start: [0, 49], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "x " } + position: { start: [0, 0], text: "{% case x %}{% when 1, 2 %}matched{% else %}other{% endcase %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 8], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 12], text: "" } + blockStartPosition: { start: [0, 12], text: "" } + blockEndPosition: { start: [0, 12], text: "" } + position: { start: [0, 12], text: "" } + children: [] + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 20], text: "1, 2 " } + blockStartPosition: { start: [0, 12], text: "{% when 1, 2 %}" } + blockEndPosition: { start: [0, 34], text: "" } + position: { start: [0, 12], text: "{% when 1, 2 %}matched" } + markup: + - type: Number + value: "1" + position: { start: [0, 20], text: "1" } + - type: Number + value: "2" + position: { start: [0, 23], text: "2" } + children: + - type: TextNode + value: matched + position: { start: [0, 27], text: matched } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 42], text: "" } + blockStartPosition: { start: [0, 34], text: "{% else %}" } + blockEndPosition: { start: [0, 49], text: "" } + position: { start: [0, 34], text: "{% else %}other" } + children: + - type: TextNode + value: other + position: { start: [0, 44], text: other } + - name: should expose branch markup positions with whitespace-control delimiters + source: "{% if a %}A{%- elsif b -%}B{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if a %}A{%- elsif b -%}B{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if a %}" } + blockEndPosition: { start: [0, 29], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "a " } + position: { start: [0, 0], text: "{% if a %}A{%- elsif b -%}B{% endif %}" } + markup: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 10], text: "" } + blockStartPosition: { start: [0, 10], text: "" } + blockEndPosition: { start: [0, 11], text: "" } + position: { start: [0, 10], text: A } + children: + - type: TextNode + value: A + position: { start: [0, 10], text: A } + - type: LiquidBranch + name: elsif + whitespaceStart: "-" + whitespaceEnd: "-" + markupPosition: { start: [0, 23], text: "b " } + blockStartPosition: { start: [0, 11], text: "{%- elsif b -%}" } + blockEndPosition: { start: [0, 29], text: "" } + position: { start: [0, 11], text: "{%- elsif b -%}B" } + markup: + type: VariableLookup + name: b + position: { start: [0, 23], text: b } + lookups: [] + lookupModes: [] + children: + - type: TextNode + value: B + position: { start: [0, 28], text: B } + - name: should parse nested block tags + source: "{% if a %}{% for item in items %}{{ item }}{% endfor %}{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if a %}{% for item in items %}{{ item }}{% endfor %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if a %}" } + blockEndPosition: { start: [0, 55], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "a " } + position: { start: [0, 0], text: "{% if a %}{% for item in items %}{{ item }}{% endfor %}{% endif %}" } + markup: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 10], text: "" } + blockStartPosition: { start: [0, 10], text: "" } + blockEndPosition: { start: [0, 55], text: "" } + position: { start: [0, 10], text: "{% for item in items %}{{ item }}{% endfor %}" } + children: + - type: LiquidTag + name: for + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 10], text: "{% for item in items %}" } + blockEndPosition: { start: [0, 43], text: "{% endfor %}" } + markupPosition: { start: [0, 17], text: "item in items " } + position: { start: [0, 10], text: "{% for item in items %}{{ item }}{% endfor %}" } + markup: + type: ForMarkup + variableName: item + reversed: false + position: { start: [0, 17], text: "item in items " } + collection: + type: VariableLookup + name: items + position: { start: [0, 25], text: items } + lookups: [] + lookupModes: [] + args: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 33], text: "" } + blockStartPosition: { start: [0, 33], text: "" } + blockEndPosition: { start: [0, 43], text: "" } + position: { start: [0, 33], text: "{{ item }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 35], text: " item " } + position: { start: [0, 33], text: "{{ item }}" } + markup: + type: LiquidVariable + rawSource: item + position: { start: [0, 36], text: "item " } + expression: + type: VariableLookup + name: item + position: { start: [0, 36], text: item } + lookups: [] + lookupModes: [] + filters: [] + - name: should parse nested branchless in branched + source: "{% if true %}{% capture x %}hello{% endcapture %}{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if true %}{% capture x %}hello{% endcapture %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if true %}" } + blockEndPosition: { start: [0, 49], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "true " } + position: { start: [0, 0], text: "{% if true %}{% capture x %}hello{% endcapture %}{% endif %}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 6], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 49], text: "" } + position: { start: [0, 13], text: "{% capture x %}hello{% endcapture %}" } + children: + - type: LiquidTag + name: capture + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 13], text: "{% capture x %}" } + blockEndPosition: { start: [0, 33], text: "{% endcapture %}" } + markupPosition: { start: [0, 24], text: "x " } + position: { start: [0, 13], text: "{% capture x %}hello{% endcapture %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 24], text: x } + lookups: [] + lookupModes: [] + children: + - type: TextNode + value: hello + position: { start: [0, 28], text: hello } + - name: should handle whitespace stripping on open and end tags + source: "{%- if true -%}hello{%- endif -%}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{%- if true -%}hello{%- endif -%}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "-" + whitespaceEnd: "-" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: "{%- if true -%}" } + blockEndPosition: { start: [0, 20], text: "{%- endif -%}" } + markupPosition: { start: [0, 7], text: "true " } + position: { start: [0, 0], text: "{%- if true -%}hello{%- endif -%}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 7], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 15], text: "" } + blockStartPosition: { start: [0, 15], text: "" } + blockEndPosition: { start: [0, 20], text: "" } + position: { start: [0, 15], text: hello } + children: + - type: TextNode + value: hello + position: { start: [0, 15], text: hello } + - name: should handle whitespace stripping on branch tags + source: "{% if true %}a{%- else -%}b{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if true %}a{%- else -%}b{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if true %}" } + blockEndPosition: { start: [0, 27], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "true " } + position: { start: [0, 0], text: "{% if true %}a{%- else -%}b{% endif %}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 6], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 14], text: "" } + position: { start: [0, 13], text: a } + children: + - type: TextNode + value: a + position: { start: [0, 13], text: a } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "-" + whitespaceEnd: "-" + markupPosition: { start: [0, 23], text: "" } + blockStartPosition: { start: [0, 14], text: "{%- else -%}" } + blockEndPosition: { start: [0, 27], text: "" } + position: { start: [0, 14], text: "{%- else -%}b" } + children: + - type: TextNode + value: b + position: { start: [0, 26], text: b } + - name: should throw for unclosed block tag + source: "{% if true %}hello" + parse: [html] + error: + message: Attempting to end parsing before LiquidTag 'if' was closed + start: [0, 0] + text: "{% if true %}" + - name: should throw for orphaned endfor at top level + source: "{% endfor %}" + parse: [html] + error: + message: Attempting to close LiquidTag 'for' before it was opened without a matching 'for' + start: [0, 0] + text: "{% endfor %" + - name: should produce AttrEmpty for bare text inside capture in attribute position + source: + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 0], text: "" } + position: { start: [0, 0], text: "" } + attributes: + - type: LiquidTag + name: capture + whitespaceStart: "-" + whitespaceEnd: "" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "-" + blockStartPosition: { start: [0, 5], text: "{%- capture x %}" } + blockEndPosition: { start: [0, 29], text: "{%- endcapture -%}" } + markupPosition: { start: [0, 17], text: "x " } + position: { start: [0, 5], text: "{%- capture x %}{{ y }}%{%- endcapture -%}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 17], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 23], text: " y " } + position: { start: [0, 21], text: "{{ y }}" } + markup: + type: LiquidVariable + rawSource: y + position: { start: [0, 24], text: "y " } + expression: + type: VariableLookup + name: y + position: { start: [0, 24], text: y } + lookups: [] + lookupModes: [] + filters: [] + - type: AttrEmpty + position: { start: [0, 28], text: "%" } + name: + - type: TextNode + value: "%" + position: { start: [0, 28], text: "%" } + - name: should throw (not hang) on an orphan endwhen inside a case in attribute position + source:
  • z
  • + parse: [html] + error: + message: Unexpected Liquid tag 'endwhen' inside LiquidTag 'case' + start: [0, 37] + text: "{%" + - name: tolerant mode (toLiquidAST) still degrades without looping + source:
  • z
  • + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "
  • z
  • " } + children: + - type: TextNode + value:
  • z
  • " + position: { start: [0, 63], text: ">z" } + - name: should throw (not hang) on an orphan end tag inside a branchless block + source:
  • z
  • + parse: [html] + error: + message: Unexpected Liquid tag 'endwhen' inside LiquidTag 'capture' + start: [0, 19] + text: "{%" diff --git a/specs/parser/liquid-doc.yml b/specs/parser/liquid-doc.yml new file mode 100644 index 000000000..ede95e21c --- /dev/null +++ b/specs/parser/liquid-doc.yml @@ -0,0 +1,1653 @@ +cases: + - name: should preserve string enum doc types in Liquid and HTML documents + source: |- + {% doc %} + @param {'Heading' | 'a|b' | '}' | ''} [variant] - Shared text style + {% enddoc %} + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% doc %} + @param {'Heading' | 'a|b' | '}' | ''} [variant] - Shared text style + {% enddoc %} + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% doc %}" } + blockEndPosition: { start: [2, 0], text: "{% enddoc %}" } + position: + start: [0, 0] + text: |- + {% doc %} + @param {'Heading' | 'a|b' | '}' | ''} [variant] - Shared text style + {% enddoc %} + body: + type: RawMarkup + kind: text + value: |2 + + @param {'Heading' | 'a|b' | '}' | ''} [variant] - Shared text style + position: + start: [0, 9] + text: |2 + + @param {'Heading' | 'a|b' | '}' | ''} [variant] - Shared text style + nodes: + - type: LiquidDocParamNode + name: param + required: false + position: { start: [1, 2], text: "@param {'Heading' | 'a|b' | '}' | ''} [variant] - Shared text style" } + paramName: + type: TextNode + value: variant + position: { start: [1, 41], text: variant } + paramType: + type: TextNode + value: "'Heading' | 'a|b' | '}' | ''" + position: { start: [1, 10], text: "'Heading' | 'a|b' | '}' | ''" } + paramDescription: + type: TextNode + value: Shared text style + position: { start: [1, 52], text: Shared text style } + - name: should parse doc tags (1) + source: "{% doc %}{% enddoc %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% doc %}{% enddoc %}" } + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% doc %}" } + blockEndPosition: { start: [0, 9], text: "{% enddoc %}" } + position: { start: [0, 0], text: "{% doc %}{% enddoc %}" } + body: + type: RawMarkup + kind: text + value: "" + position: { start: [0, 9], text: "" } + nodes: [] + - name: should parse doc tags (2) + source: " + + \ {% doc -%} + + \ @param requiredParamWithNoType + + \ @param {String} paramWithDescription - param with description and `punctation`. This is still a valid param description. + + \ @param {String} paramWithNoDescription + + \ @param {String} [optionalParameterWithTypeAndDescription] - optional parameter with type and description + + \ @param [optionalParameterWithDescription] - optional parameter description + + \ @param {String} [optionalParameterWithType] + + \ @unsupported this node falls back to a text node + + \ {%- enddoc %} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% doc -%} + + \ @param requiredParamWithNoType + + \ @param {String} paramWithDescription - param with description and `punctation`. This is still a valid param description. + + \ @param {String} paramWithNoDescription + + \ @param {String} [optionalParameterWithTypeAndDescription] - optional parameter with type and description + + \ @param [optionalParameterWithDescription] - optional parameter description + + \ @param {String} [optionalParameterWithType] + + \ @unsupported this node falls back to a text node + + \ {%- enddoc %} + + \ " + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 15], text: "" } + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% doc -%}" } + blockEndPosition: { start: [9, 8], text: "{%- enddoc %}" } + position: + start: [1, 8] + text: |- + {% doc -%} + @param requiredParamWithNoType + @param {String} paramWithDescription - param with description and `punctation`. This is still a valid param description. + @param {String} paramWithNoDescription + @param {String} [optionalParameterWithTypeAndDescription] - optional parameter with type and description + @param [optionalParameterWithDescription] - optional parameter description + @param {String} [optionalParameterWithType] + @unsupported this node falls back to a text node + {%- enddoc %} + body: + type: RawMarkup + kind: text + value: " + + \ @param requiredParamWithNoType + + \ @param {String} paramWithDescription - param with description and `punctation`. This is still a valid param description. + + \ @param {String} paramWithNoDescription + + \ @param {String} [optionalParameterWithTypeAndDescription] - optional parameter with type and description + + \ @param [optionalParameterWithDescription] - optional parameter description + + \ @param {String} [optionalParameterWithType] + + \ @unsupported this node falls back to a text node + + \ " + position: + start: [1, 18] + text: " + + \ @param requiredParamWithNoType + + \ @param {String} paramWithDescription - param with description and `punctation`. This is still a valid param description. + + \ @param {String} paramWithNoDescription + + \ @param {String} [optionalParameterWithTypeAndDescription] - optional parameter with type and description + + \ @param [optionalParameterWithDescription] - optional parameter description + + \ @param {String} [optionalParameterWithType] + + \ @unsupported this node falls back to a text node + + \ " + nodes: + - type: LiquidDocParamNode + name: param + paramType: null + paramDescription: null + required: true + position: { start: [2, 8], text: "@param requiredParamWithNoType" } + paramName: + type: TextNode + value: requiredParamWithNoType + position: { start: [2, 15], text: requiredParamWithNoType } + - type: LiquidDocParamNode + name: param + required: true + position: { start: [3, 8], text: "@param {String} paramWithDescription - param with description and `punctation`. This is still a valid param description." } + paramName: + type: TextNode + value: paramWithDescription + position: { start: [3, 24], text: paramWithDescription } + paramType: + type: TextNode + value: String + position: { start: [3, 16], text: String } + paramDescription: + type: TextNode + value: param with description and `punctation`. This is still a valid param description. + position: { start: [3, 47], text: param with description and `punctation`. This is still a valid param description. } + - type: LiquidDocParamNode + name: param + paramDescription: null + required: true + position: { start: [4, 8], text: "@param {String} paramWithNoDescription" } + paramName: + type: TextNode + value: paramWithNoDescription + position: { start: [4, 24], text: paramWithNoDescription } + paramType: + type: TextNode + value: String + position: { start: [4, 16], text: String } + - type: LiquidDocParamNode + name: param + required: false + position: { start: [5, 8], text: "@param {String} [optionalParameterWithTypeAndDescription] - optional parameter with type and description" } + paramName: + type: TextNode + value: optionalParameterWithTypeAndDescription + position: { start: [5, 25], text: optionalParameterWithTypeAndDescription } + paramType: + type: TextNode + value: String + position: { start: [5, 16], text: String } + paramDescription: + type: TextNode + value: optional parameter with type and description + position: { start: [5, 68], text: optional parameter with type and description } + - type: LiquidDocParamNode + name: param + paramType: null + required: false + position: { start: [6, 8], text: "@param [optionalParameterWithDescription] - optional parameter description" } + paramName: + type: TextNode + value: optionalParameterWithDescription + position: { start: [6, 16], text: optionalParameterWithDescription } + paramDescription: + type: TextNode + value: optional parameter description + position: { start: [6, 52], text: optional parameter description } + - type: LiquidDocParamNode + name: param + paramDescription: null + required: false + position: { start: [7, 8], text: "@param {String} [optionalParameterWithType]" } + paramName: + type: TextNode + value: optionalParameterWithType + position: { start: [7, 25], text: optionalParameterWithType } + paramType: + type: TextNode + value: String + position: { start: [7, 16], text: String } + - type: TextNode + value: "@unsupported this node falls back to a text node" + position: { start: [8, 8], text: "@unsupported this node falls back to a text node" } + - name: should parse doc tags (3) + source: " + + \ {% doc -%} + + \ @example simple inline example + + \ {%- enddoc %} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% doc -%} + + \ @example simple inline example + + \ {%- enddoc %} + + \ " + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 15], text: "" } + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% doc -%}" } + blockEndPosition: { start: [3, 8], text: "{%- enddoc %}" } + position: + start: [1, 8] + text: |- + {% doc -%} + @example simple inline example + {%- enddoc %} + body: + type: RawMarkup + kind: text + value: " + + \ @example simple inline example + + \ " + position: + start: [1, 18] + text: " + + \ @example simple inline example + + \ " + nodes: + - type: LiquidDocExampleNode + name: example + isInline: true + position: { start: [2, 8], text: "@example simple inline example" } + content: + type: TextNode + value: simple inline example + position: { start: [2, 17], text: simple inline example } + - name: should parse doc tags (4) + source: " + + \ {% doc -%} + + \ @example including inline code + + \ This is a valid example + + \ It can have multiple lines + + \ {% enddoc %} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% doc -%} + + \ @example including inline code + + \ This is a valid example + + \ It can have multiple lines + + \ {% enddoc %} + + \ " + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 15], text: "" } + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% doc -%}" } + blockEndPosition: { start: [5, 8], text: "{% enddoc %}" } + position: + start: [1, 8] + text: |- + {% doc -%} + @example including inline code + This is a valid example + It can have multiple lines + {% enddoc %} + body: + type: RawMarkup + kind: text + value: " + + \ @example including inline code + + \ This is a valid example + + \ It can have multiple lines + + \ " + position: + start: [1, 18] + text: " + + \ @example including inline code + + \ This is a valid example + + \ It can have multiple lines + + \ " + nodes: + - type: LiquidDocExampleNode + name: example + isInline: true + position: + start: [2, 8] + text: "@example including inline code + + \ This is a valid example + + \ It can have multiple lines + + \ " + content: + type: TextNode + value: | + including inline code + This is a valid example + It can have multiple lines + position: + start: [2, 17] + text: "including inline code + + \ This is a valid example + + \ It can have multiple lines + + \ " + - name: should parse doc tags (5) + source: " + + \ {% doc -%} + + \ @example + + \ First Example + + \ @example + + \ Second Example + + \ {% enddoc %} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% doc -%} + + \ @example + + \ First Example + + \ @example + + \ Second Example + + \ {% enddoc %} + + \ " + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 15], text: "" } + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% doc -%}" } + blockEndPosition: { start: [6, 8], text: "{% enddoc %}" } + position: + start: [1, 8] + text: |- + {% doc -%} + @example + First Example + @example + Second Example + {% enddoc %} + body: + type: RawMarkup + kind: text + value: " + + \ @example + + \ First Example + + \ @example + + \ Second Example + + \ " + position: + start: [1, 18] + text: " + + \ @example + + \ First Example + + \ @example + + \ Second Example + + \ " + nodes: + - type: LiquidDocExampleNode + name: example + isInline: false + position: + start: [2, 8] + text: | + @example + First Example + content: + type: TextNode + value: | + First Example + position: + start: [3, 8] + text: | + First Example + - type: LiquidDocExampleNode + name: example + isInline: false + position: + start: [4, 8] + text: "@example + + \ Second Example + + \ " + content: + type: TextNode + value: | + Second Example + position: + start: [5, 8] + text: "Second Example\n " + - name: should parse doc tags (6) + source: " + + \ {% doc -%} + + \ @example + + \ This is a valid example + + \ It can have multiple lines + + \ @param {String} paramWithDescription - param with description + + \ {% enddoc %} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% doc -%} + + \ @example + + \ This is a valid example + + \ It can have multiple lines + + \ @param {String} paramWithDescription - param with description + + \ {% enddoc %} + + \ " + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 15], text: "" } + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% doc -%}" } + blockEndPosition: { start: [6, 8], text: "{% enddoc %}" } + position: + start: [1, 8] + text: |- + {% doc -%} + @example + This is a valid example + It can have multiple lines + @param {String} paramWithDescription - param with description + {% enddoc %} + body: + type: RawMarkup + kind: text + value: " + + \ @example + + \ This is a valid example + + \ It can have multiple lines + + \ @param {String} paramWithDescription - param with description + + \ " + position: + start: [1, 18] + text: " + + \ @example + + \ This is a valid example + + \ It can have multiple lines + + \ @param {String} paramWithDescription - param with description + + \ " + nodes: + - type: LiquidDocExampleNode + name: example + isInline: false + position: + start: [2, 8] + text: | + @example + This is a valid example + It can have multiple lines + content: + type: TextNode + value: | + This is a valid example + It can have multiple lines + position: + start: [3, 8] + text: | + This is a valid example + It can have multiple lines + - type: LiquidDocParamNode + name: param + required: true + position: { start: [5, 8], text: "@param {String} paramWithDescription - param with description" } + paramName: + type: TextNode + value: paramWithDescription + position: { start: [5, 24], text: paramWithDescription } + paramType: + type: TextNode + value: String + position: { start: [5, 16], text: String } + paramDescription: + type: TextNode + value: param with description + position: { start: [5, 47], text: param with description } + - name: should parse doc tags (7) + source: " + + \ {% doc -%} + + \ @description This is a description + + \ @description This is another description + + \ it can have multiple lines + + \ {% enddoc %} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% doc -%} + + \ @description This is a description + + \ @description This is another description + + \ it can have multiple lines + + \ {% enddoc %} + + \ " + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 15], text: "" } + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% doc -%}" } + blockEndPosition: { start: [5, 8], text: "{% enddoc %}" } + position: + start: [1, 8] + text: |- + {% doc -%} + @description This is a description + @description This is another description + it can have multiple lines + {% enddoc %} + body: + type: RawMarkup + kind: text + value: " + + \ @description This is a description + + \ @description This is another description + + \ it can have multiple lines + + \ " + position: + start: [1, 18] + text: " + + \ @description This is a description + + \ @description This is another description + + \ it can have multiple lines + + \ " + nodes: + - type: LiquidDocDescriptionNode + name: description + isImplicit: false + isInline: true + position: { start: [2, 8], text: "@description This is a description" } + content: + type: TextNode + value: This is a description + position: { start: [2, 21], text: This is a description } + - type: LiquidDocDescriptionNode + name: description + isImplicit: false + isInline: true + position: + start: [3, 8] + text: "@description This is another description + + \ it can have multiple lines + + \ " + content: + type: TextNode + value: | + This is another description + it can have multiple lines + position: + start: [3, 21] + text: "This is another description + + \ it can have multiple lines + + \ " + - name: should parse doc tags (8) + source: " + + \ {% doc -%} + + \ @description This is a description + + \ @example This is an example + + \ @param {String} paramWithDescription - param with description + + \ {% enddoc %} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% doc -%} + + \ @description This is a description + + \ @example This is an example + + \ @param {String} paramWithDescription - param with description + + \ {% enddoc %} + + \ " + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 15], text: "" } + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% doc -%}" } + blockEndPosition: { start: [5, 8], text: "{% enddoc %}" } + position: + start: [1, 8] + text: |- + {% doc -%} + @description This is a description + @example This is an example + @param {String} paramWithDescription - param with description + {% enddoc %} + body: + type: RawMarkup + kind: text + value: " + + \ @description This is a description + + \ @example This is an example + + \ @param {String} paramWithDescription - param with description + + \ " + position: + start: [1, 18] + text: " + + \ @description This is a description + + \ @example This is an example + + \ @param {String} paramWithDescription - param with description + + \ " + nodes: + - type: LiquidDocDescriptionNode + name: description + isImplicit: false + isInline: true + position: { start: [2, 8], text: "@description This is a description" } + content: + type: TextNode + value: This is a description + position: { start: [2, 21], text: This is a description } + - type: LiquidDocExampleNode + name: example + isInline: true + position: { start: [3, 8], text: "@example This is an example" } + content: + type: TextNode + value: This is an example + position: { start: [3, 17], text: This is an example } + - type: LiquidDocParamNode + name: param + required: true + position: { start: [4, 8], text: "@param {String} paramWithDescription - param with description" } + paramName: + type: TextNode + value: paramWithDescription + position: { start: [4, 24], text: paramWithDescription } + paramType: + type: TextNode + value: String + position: { start: [4, 16], text: String } + paramDescription: + type: TextNode + value: param with description + position: { start: [4, 47], text: param with description } + - name: should parse doc tags (9) + source: " + + \ {% doc -%} + + \ this is an implicit description + + \ in a header + + + \ @description with a description annotation + + \ {% enddoc %} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% doc -%} + + \ this is an implicit description + + \ in a header + + + \ @description with a description annotation + + \ {% enddoc %} + + \ " + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 15], text: "" } + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% doc -%}" } + blockEndPosition: { start: [6, 8], text: "{% enddoc %}" } + position: + start: [1, 8] + text: |- + {% doc -%} + this is an implicit description + in a header + + @description with a description annotation + {% enddoc %} + body: + type: RawMarkup + kind: text + value: " + + \ this is an implicit description + + \ in a header + + + \ @description with a description annotation + + \ " + position: + start: [1, 18] + text: " + + \ this is an implicit description + + \ in a header + + + \ @description with a description annotation + + \ " + nodes: + - type: LiquidDocDescriptionNode + name: description + isImplicit: true + isInline: true + position: + start: [2, 8] + text: |+ + this is an implicit description + in a header + + content: + type: TextNode + value: |+ + this is an implicit description + in a header + + position: + start: [2, 8] + text: |+ + this is an implicit description + in a header + + - type: LiquidDocDescriptionNode + name: description + isImplicit: false + isInline: true + position: { start: [5, 8], text: "@description with a description annotation" } + content: + type: TextNode + value: with a description annotation + position: { start: [5, 21], text: with a description annotation } + - name: should parse doc tags (10) + source: |- + + {% doc -%} + @prompt + This is a prompt + It can have multiple lines + {% enddoc %} + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + + {% doc -%} + @prompt + This is a prompt + It can have multiple lines + {% enddoc %} + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: "{% doc -%}" } + blockEndPosition: { start: [5, 0], text: "{% enddoc %}" } + position: + start: [1, 0] + text: |- + {% doc -%} + @prompt + This is a prompt + It can have multiple lines + {% enddoc %} + body: + type: RawMarkup + kind: text + value: |2 + + @prompt + This is a prompt + It can have multiple lines + position: + start: [1, 10] + text: |2 + + @prompt + This is a prompt + It can have multiple lines + nodes: + - type: LiquidDocPromptNode + name: prompt + position: + start: [2, 2] + text: | + @prompt + This is a prompt + It can have multiple lines + content: + type: TextNode + value: |2 + + This is a prompt + It can have multiple lines + position: + start: [2, 9] + text: |2 + + This is a prompt + It can have multiple lines + - name: should parse doc tags (11) + source: " + + {% doc -%} + + \ this block was AI generated + + + \ @prompt + + \ First prompt + + + \ @param {String} paramName - param description + + {% enddoc %} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + {% doc -%} + + \ this block was AI generated + + + \ @prompt + + \ First prompt + + + \ @param {String} paramName - param description + + {% enddoc %} + + \ " + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: "{% doc -%}" } + blockEndPosition: { start: [8, 0], text: "{% enddoc %}" } + position: + start: [1, 0] + text: |- + {% doc -%} + this block was AI generated + + @prompt + First prompt + + @param {String} paramName - param description + {% enddoc %} + body: + type: RawMarkup + kind: text + value: |2 + + this block was AI generated + + @prompt + First prompt + + @param {String} paramName - param description + position: + start: [1, 10] + text: |2 + + this block was AI generated + + @prompt + First prompt + + @param {String} paramName - param description + nodes: + - type: LiquidDocDescriptionNode + name: description + isImplicit: true + isInline: true + position: + start: [2, 2] + text: |+ + this block was AI generated + + content: + type: TextNode + value: |+ + this block was AI generated + + position: + start: [2, 2] + text: |+ + this block was AI generated + + - type: LiquidDocPromptNode + name: prompt + position: + start: [4, 2] + text: |+ + @prompt + First prompt + + content: + type: TextNode + value: |2+ + + First prompt + + position: + start: [4, 9] + text: |2+ + + First prompt + + - type: LiquidDocParamNode + name: param + required: true + position: { start: [7, 2], text: "@param {String} paramName - param description" } + paramName: + type: TextNode + value: paramName + position: { start: [7, 18], text: paramName } + paramType: + type: TextNode + value: String + position: { start: [7, 10], text: String } + paramDescription: + type: TextNode + value: param description + position: { start: [7, 30], text: param description } + - name: should split doc body at mid-line @word patterns (1) + source: |- + + {% doc %} + Description text with `@utility` mid-line token. + + @param {string} [name] - A param + @category content + {% enddoc %} + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + + {% doc %} + Description text with `@utility` mid-line token. + + @param {string} [name] - A param + @category content + {% enddoc %} + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: "{% doc %}" } + blockEndPosition: { start: [6, 0], text: "{% enddoc %}" } + position: + start: [1, 0] + text: |- + {% doc %} + Description text with `@utility` mid-line token. + + @param {string} [name] - A param + @category content + {% enddoc %} + body: + type: RawMarkup + kind: text + value: |2 + + Description text with `@utility` mid-line token. + + @param {string} [name] - A param + @category content + position: + start: [1, 9] + text: |2 + + Description text with `@utility` mid-line token. + + @param {string} [name] - A param + @category content + nodes: + - type: LiquidDocDescriptionNode + name: description + isImplicit: true + isInline: true + position: { start: [2, 2], text: Description text with ` } + content: + type: TextNode + value: Description text with ` + position: { start: [2, 2], text: Description text with ` } + - type: TextNode + value: "@utility` mid-line token." + position: { start: [2, 25], text: "@utility` mid-line token." } + - type: LiquidDocParamNode + name: param + required: false + position: { start: [4, 2], text: "@param {string} [name] - A param" } + paramName: + type: TextNode + value: name + position: { start: [4, 19], text: name } + paramType: + type: TextNode + value: string + position: { start: [4, 10], text: string } + paramDescription: + type: TextNode + value: A param + position: { start: [4, 27], text: A param } + - type: TextNode + value: "@category content" + position: { start: [5, 2], text: "@category content" } + - name: should split doc body at mid-line @word patterns (2) + source: |- + + {% doc %} + @param {string} id - Unique id. @required + @category content + {% enddoc %} + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + + {% doc %} + @param {string} id - Unique id. @required + @category content + {% enddoc %} + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [1, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: "{% doc %}" } + blockEndPosition: { start: [4, 0], text: "{% enddoc %}" } + position: + start: [1, 0] + text: |- + {% doc %} + @param {string} id - Unique id. @required + @category content + {% enddoc %} + body: + type: RawMarkup + kind: text + value: |2 + + @param {string} id - Unique id. @required + @category content + position: + start: [1, 9] + text: |2 + + @param {string} id - Unique id. @required + @category content + nodes: + - type: LiquidDocParamNode + name: param + required: true + position: { start: [2, 2], text: "@param {string} id - Unique id. @required" } + paramName: + type: TextNode + value: id + position: { start: [2, 18], text: id } + paramType: + type: TextNode + value: string + position: { start: [2, 10], text: string } + paramDescription: + type: TextNode + value: Unique id. @required + position: { start: [2, 23], text: Unique id. @required } + - type: TextNode + value: "@category content" + position: { start: [3, 2], text: "@category content" } + - name: should split doc body at mid-line @word patterns (3) + source: |- + {% doc %} + @param {string} [alignment] - The horizontal alignment + ('left', 'center', or 'right'). Defaults to 'center'. + @param {boolean} [full_width] - Spans full width. + {% enddoc %} + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% doc %} + @param {string} [alignment] - The horizontal alignment + ('left', 'center', or 'right'). Defaults to 'center'. + @param {boolean} [full_width] - Spans full width. + {% enddoc %} + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% doc %}" } + blockEndPosition: { start: [4, 0], text: "{% enddoc %}" } + position: + start: [0, 0] + text: |- + {% doc %} + @param {string} [alignment] - The horizontal alignment + ('left', 'center', or 'right'). Defaults to 'center'. + @param {boolean} [full_width] - Spans full width. + {% enddoc %} + body: + type: RawMarkup + kind: text + value: |2 + + @param {string} [alignment] - The horizontal alignment + ('left', 'center', or 'right'). Defaults to 'center'. + @param {boolean} [full_width] - Spans full width. + position: + start: [0, 9] + text: |2 + + @param {string} [alignment] - The horizontal alignment + ('left', 'center', or 'right'). Defaults to 'center'. + @param {boolean} [full_width] - Spans full width. + nodes: + - type: LiquidDocParamNode + name: param + required: false + position: + start: [1, 2] + text: |- + @param {string} [alignment] - The horizontal alignment + ('left', 'center', or 'right'). Defaults to 'center'. + paramName: + type: TextNode + value: alignment + position: { start: [1, 19], text: alignment } + paramType: + type: TextNode + value: string + position: { start: [1, 10], text: string } + paramDescription: + type: TextNode + value: |- + The horizontal alignment + ('left', 'center', or 'right'). Defaults to 'center'. + position: + start: [1, 32] + text: |- + The horizontal alignment + ('left', 'center', or 'right'). Defaults to 'center'. + - type: LiquidDocParamNode + name: param + required: false + position: { start: [3, 2], text: "@param {boolean} [full_width] - Spans full width." } + paramName: + type: TextNode + value: full_width + position: { start: [3, 20], text: full_width } + paramType: + type: TextNode + value: boolean + position: { start: [3, 10], text: boolean } + paramDescription: + type: TextNode + value: Spans full width. + position: { start: [3, 34], text: Spans full width. } + - name: should split doc body at mid-line @word patterns (4) + source: |- + {% doc %} + @param {number} index + @param {boolean} [current] + @param {boolean} [image_only] + {% enddoc %} + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% doc %} + @param {number} index + @param {boolean} [current] + @param {boolean} [image_only] + {% enddoc %} + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% doc %}" } + blockEndPosition: { start: [4, 0], text: "{% enddoc %}" } + position: + start: [0, 0] + text: |- + {% doc %} + @param {number} index + @param {boolean} [current] + @param {boolean} [image_only] + {% enddoc %} + body: + type: RawMarkup + kind: text + value: |2 + + @param {number} index + @param {boolean} [current] + @param {boolean} [image_only] + position: + start: [0, 9] + text: |2 + + @param {number} index + @param {boolean} [current] + @param {boolean} [image_only] + nodes: + - type: LiquidDocParamNode + name: param + paramDescription: null + required: true + position: { start: [1, 2], text: "@param {number} index" } + paramName: + type: TextNode + value: index + position: { start: [1, 18], text: index } + paramType: + type: TextNode + value: number + position: { start: [1, 10], text: number } + - type: LiquidDocParamNode + name: param + paramDescription: null + required: false + position: { start: [2, 2], text: "@param {boolean} [current]" } + paramName: + type: TextNode + value: current + position: { start: [2, 20], text: current } + paramType: + type: TextNode + value: boolean + position: { start: [2, 10], text: boolean } + - type: LiquidDocParamNode + name: param + paramDescription: null + required: false + position: { start: [3, 2], text: "@param {boolean} [image_only]" } + paramName: + type: TextNode + value: image_only + position: { start: [3, 20], text: image_only } + paramType: + type: TextNode + value: boolean + position: { start: [3, 10], text: boolean } + - name: should parse a description glued to @description + source: |- + {% doc %} + @descriptionRenders a product card + with its image. + {% enddoc %} + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% doc %} + @descriptionRenders a product card + with its image. + {% enddoc %} + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% doc %}" } + blockEndPosition: { start: [3, 0], text: "{% enddoc %}" } + position: + start: [0, 0] + text: |- + {% doc %} + @descriptionRenders a product card + with its image. + {% enddoc %} + body: + type: RawMarkup + kind: text + value: |2 + + @descriptionRenders a product card + with its image. + position: + start: [0, 9] + text: |2 + + @descriptionRenders a product card + with its image. + nodes: + - type: LiquidDocDescriptionNode + name: description + isImplicit: false + isInline: true + position: + start: [1, 2] + text: | + @descriptionRenders a product card + with its image. + content: + type: TextNode + value: | + Renders a product card + with its image. + position: + start: [1, 14] + text: | + Renders a product card + with its image. diff --git a/specs/parser/liquid-lines.yml b/specs/parser/liquid-lines.yml new file mode 100644 index 000000000..6abbf566a --- /dev/null +++ b/specs/parser/liquid-lines.yml @@ -0,0 +1,2055 @@ +cases: + - name: should not include a leading newline in comment body.value inside {% liquid %} + source: |- + {% liquid + comment + this is a comment + endcomment + %} + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + comment + this is a comment + endcomment + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + comment + this is a comment + endcomment + %} + markupPosition: + start: [0, 9] + text: | + + comment + this is a comment + endcomment + position: + start: [0, 0] + text: |- + {% liquid + comment + this is a comment + endcomment + %} + markup: + - type: LiquidRawTag + name: comment + markup: "" + markupPosition: { start: [1, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: comment } + blockEndPosition: { start: [3, 0], text: endcomment } + position: + start: [1, 0] + text: |- + comment + this is a comment + endcomment + body: + type: RawMarkup + kind: text + value: | + this is a comment + position: + start: [1, 7] + text: | + + this is a comment + nodes: + - type: TextNode + value: this is a comment + position: { start: [2, 0], text: this is a comment } + - name: should parse an empty liquid tag + source: "{% liquid %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% liquid %}" } + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% liquid %}" } + markupPosition: { start: [0, 10], text: "" } + position: { start: [0, 0], text: "{% liquid %}" } + markup: [] + - name: should parse a single echo statement + source: |- + {% liquid + echo "hi" + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + echo "hi" + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + echo "hi" + %} + markupPosition: + start: [0, 9] + text: | + + echo "hi" + position: + start: [0, 0] + text: |- + {% liquid + echo "hi" + %} + markup: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: echo "hi" } + markupPosition: { start: [1, 5], text: '"hi"' } + position: { start: [1, 0], text: echo "hi" } + markup: + type: LiquidVariable + rawSource: '"hi"' + position: { start: [1, 5], text: '"hi"' } + expression: + type: String + single: false + value: hi + position: { start: [1, 5], text: '"hi"' } + filters: [] + - name: should parse multiple statements + source: |- + {% liquid + assign x = 1 + echo x + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + assign x = 1 + echo x + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + assign x = 1 + echo x + %} + markupPosition: + start: [0, 9] + text: | + + assign x = 1 + echo x + position: + start: [0, 0] + text: |- + {% liquid + assign x = 1 + echo x + %} + markup: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: assign x = 1 } + markupPosition: { start: [1, 7], text: x = 1 } + position: { start: [1, 0], text: assign x = 1 } + markup: + type: AssignMarkup + name: x + position: { start: [1, 7], text: x = 1 } + value: + type: LiquidVariable + rawSource: "1" + position: { start: [1, 11], text: "1" } + expression: + type: Number + value: "1" + position: { start: [1, 11], text: "1" } + filters: [] + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 0], text: echo x } + markupPosition: { start: [2, 5], text: x } + position: { start: [2, 0], text: echo x } + markup: + type: LiquidVariable + rawSource: x + position: { start: [2, 5], text: x } + expression: + type: VariableLookup + name: x + position: { start: [2, 5], text: x } + lookups: [] + lookupModes: [] + filters: [] + - name: should parse a block tag (if/endif) + source: |- + {% liquid + if true + echo "yes" + endif + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + if true + echo "yes" + endif + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + if true + echo "yes" + endif + %} + markupPosition: + start: [0, 9] + text: | + + if true + echo "yes" + endif + position: + start: [0, 0] + text: |- + {% liquid + if true + echo "yes" + endif + %} + markup: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: if true } + blockEndPosition: { start: [3, 0], text: endif } + markupPosition: { start: [1, 3], text: "true" } + position: + start: [1, 0] + text: |- + if true + echo "yes" + endif + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [1, 3], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 7], text: "" } + blockStartPosition: { start: [1, 7], text: "" } + blockEndPosition: { start: [3, 0], text: "" } + position: + start: [1, 7] + text: | + + echo "yes" + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 0], text: echo "yes" } + markupPosition: { start: [2, 5], text: '"yes"' } + position: { start: [2, 0], text: echo "yes" } + markup: + type: LiquidVariable + rawSource: '"yes"' + position: { start: [2, 5], text: '"yes"' } + expression: + type: String + single: false + value: yes + position: { start: [2, 5], text: '"yes"' } + filters: [] + - name: should parse branched block (if/elsif/else) + source: |- + {% liquid + if a + echo "1" + elsif b + echo "2" + else + echo "3" + endif + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + if a + echo "1" + elsif b + echo "2" + else + echo "3" + endif + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + if a + echo "1" + elsif b + echo "2" + else + echo "3" + endif + %} + markupPosition: + start: [0, 9] + text: | + + if a + echo "1" + elsif b + echo "2" + else + echo "3" + endif + position: + start: [0, 0] + text: |- + {% liquid + if a + echo "1" + elsif b + echo "2" + else + echo "3" + endif + %} + markup: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: if a } + blockEndPosition: { start: [7, 0], text: endif } + markupPosition: { start: [1, 3], text: a } + position: + start: [1, 0] + text: |- + if a + echo "1" + elsif b + echo "2" + else + echo "3" + endif + markup: + type: VariableLookup + name: a + position: { start: [1, 3], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 4], text: "" } + blockStartPosition: { start: [1, 4], text: "" } + blockEndPosition: { start: [3, 0], text: "" } + position: + start: [1, 4] + text: | + + echo "1" + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 0], text: echo "1" } + markupPosition: { start: [2, 5], text: '"1"' } + position: { start: [2, 0], text: echo "1" } + markup: + type: LiquidVariable + rawSource: '"1"' + position: { start: [2, 5], text: '"1"' } + expression: + type: String + single: false + value: "1" + position: { start: [2, 5], text: '"1"' } + filters: [] + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [3, 6], text: b } + blockStartPosition: { start: [3, 0], text: elsif b } + blockEndPosition: { start: [5, 0], text: "" } + position: + start: [3, 0] + text: | + elsif b + echo "2" + markup: + type: VariableLookup + name: b + position: { start: [3, 6], text: b } + lookups: [] + lookupModes: [] + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [4, 0], text: echo "2" } + markupPosition: { start: [4, 5], text: '"2"' } + position: { start: [4, 0], text: echo "2" } + markup: + type: LiquidVariable + rawSource: '"2"' + position: { start: [4, 5], text: '"2"' } + expression: + type: String + single: false + value: "2" + position: { start: [4, 5], text: '"2"' } + filters: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [5, 4], text: "" } + blockStartPosition: { start: [5, 0], text: else } + blockEndPosition: { start: [7, 0], text: "" } + position: + start: [5, 0] + text: | + else + echo "3" + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [6, 0], text: echo "3" } + markupPosition: { start: [6, 5], text: '"3"' } + position: { start: [6, 0], text: echo "3" } + markup: + type: LiquidVariable + rawSource: '"3"' + position: { start: [6, 5], text: '"3"' } + expression: + type: String + single: false + value: "3" + position: { start: [6, 5], text: '"3"' } + filters: [] + - name: should parse case/when + source: |- + {% liquid + case x + when 1 + echo "one" + when 2 + echo "two" + endcase + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + case x + when 1 + echo "one" + when 2 + echo "two" + endcase + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + case x + when 1 + echo "one" + when 2 + echo "two" + endcase + %} + markupPosition: + start: [0, 9] + text: | + + case x + when 1 + echo "one" + when 2 + echo "two" + endcase + position: + start: [0, 0] + text: |- + {% liquid + case x + when 1 + echo "one" + when 2 + echo "two" + endcase + %} + markup: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: case x } + blockEndPosition: { start: [6, 0], text: endcase } + markupPosition: { start: [1, 5], text: x } + position: + start: [1, 0] + text: |- + case x + when 1 + echo "one" + when 2 + echo "two" + endcase + markup: + type: VariableLookup + name: x + position: { start: [1, 5], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 6], text: "" } + blockStartPosition: { start: [1, 6], text: "" } + blockEndPosition: { start: [2, 0], text: "" } + position: + start: [1, 6] + text: |+ + + children: [] + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [2, 5], text: "1" } + blockStartPosition: { start: [2, 0], text: when 1 } + blockEndPosition: { start: [4, 0], text: "" } + position: + start: [2, 0] + text: | + when 1 + echo "one" + markup: + - type: Number + value: "1" + position: { start: [2, 5], text: "1" } + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [3, 0], text: echo "one" } + markupPosition: { start: [3, 5], text: '"one"' } + position: { start: [3, 0], text: echo "one" } + markup: + type: LiquidVariable + rawSource: '"one"' + position: { start: [3, 5], text: '"one"' } + expression: + type: String + single: false + value: one + position: { start: [3, 5], text: '"one"' } + filters: [] + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [4, 5], text: "2" } + blockStartPosition: { start: [4, 0], text: when 2 } + blockEndPosition: { start: [6, 0], text: "" } + position: + start: [4, 0] + text: | + when 2 + echo "two" + markup: + - type: Number + value: "2" + position: { start: [4, 5], text: "2" } + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [5, 0], text: echo "two" } + markupPosition: { start: [5, 5], text: '"two"' } + position: { start: [5, 0], text: echo "two" } + markup: + type: LiquidVariable + rawSource: '"two"' + position: { start: [5, 5], text: '"two"' } + expression: + type: String + single: false + value: two + position: { start: [5, 5], text: '"two"' } + filters: [] + - name: should parse for/else + source: |- + {% liquid + for item in items + echo item + else + echo "empty" + endfor + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + for item in items + echo item + else + echo "empty" + endfor + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + for item in items + echo item + else + echo "empty" + endfor + %} + markupPosition: + start: [0, 9] + text: | + + for item in items + echo item + else + echo "empty" + endfor + position: + start: [0, 0] + text: |- + {% liquid + for item in items + echo item + else + echo "empty" + endfor + %} + markup: + - type: LiquidTag + name: for + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: for item in items } + blockEndPosition: { start: [5, 0], text: endfor } + markupPosition: { start: [1, 4], text: item in items } + position: + start: [1, 0] + text: |- + for item in items + echo item + else + echo "empty" + endfor + markup: + type: ForMarkup + variableName: item + reversed: false + position: { start: [1, 4], text: item in items } + collection: + type: VariableLookup + name: items + position: { start: [1, 12], text: items } + lookups: [] + lookupModes: [] + args: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 17], text: "" } + blockStartPosition: { start: [1, 17], text: "" } + blockEndPosition: { start: [3, 0], text: "" } + position: + start: [1, 17] + text: | + + echo item + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 0], text: echo item } + markupPosition: { start: [2, 5], text: item } + position: { start: [2, 0], text: echo item } + markup: + type: LiquidVariable + rawSource: item + position: { start: [2, 5], text: item } + expression: + type: VariableLookup + name: item + position: { start: [2, 5], text: item } + lookups: [] + lookupModes: [] + filters: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [3, 4], text: "" } + blockStartPosition: { start: [3, 0], text: else } + blockEndPosition: { start: [5, 0], text: "" } + position: + start: [3, 0] + text: | + else + echo "empty" + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [4, 0], text: echo "empty" } + markupPosition: { start: [4, 5], text: '"empty"' } + position: { start: [4, 0], text: echo "empty" } + markup: + type: LiquidVariable + rawSource: '"empty"' + position: { start: [4, 5], text: '"empty"' } + expression: + type: String + single: false + value: empty + position: { start: [4, 5], text: '"empty"' } + filters: [] + - name: should parse raw tag (comment) + source: |- + {% liquid + comment + this is ignored + endcomment + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + comment + this is ignored + endcomment + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + comment + this is ignored + endcomment + %} + markupPosition: + start: [0, 9] + text: | + + comment + this is ignored + endcomment + position: + start: [0, 0] + text: |- + {% liquid + comment + this is ignored + endcomment + %} + markup: + - type: LiquidRawTag + name: comment + markup: "" + markupPosition: { start: [1, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: comment } + blockEndPosition: { start: [3, 0], text: endcomment } + position: + start: [1, 0] + text: |- + comment + this is ignored + endcomment + body: + type: RawMarkup + kind: text + value: | + this is ignored + position: + start: [1, 7] + text: | + + this is ignored + nodes: + - type: TextNode + value: this is ignored + position: { start: [2, 0], text: this is ignored } + - name: should set RawMarkup start position to lineEnd, not lineEnd+1 (Bug 47 regression) + source: |- + {% liquid + comment + body text + endcomment + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + comment + body text + endcomment + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + comment + body text + endcomment + %} + markupPosition: + start: [0, 9] + text: | + + comment + body text + endcomment + position: + start: [0, 0] + text: |- + {% liquid + comment + body text + endcomment + %} + markup: + - type: LiquidRawTag + name: comment + markup: "" + markupPosition: { start: [1, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: comment } + blockEndPosition: { start: [3, 0], text: endcomment } + position: + start: [1, 0] + text: |- + comment + body text + endcomment + body: + type: RawMarkup + kind: text + value: | + body text + position: + start: [1, 7] + text: | + + body text + nodes: + - type: TextNode + value: body text + position: { start: [2, 0], text: body text } + - name: should parse inline comment (#) + source: |- + {% liquid + # a comment + echo "hi" + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + # a comment + echo "hi" + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + # a comment + echo "hi" + %} + markupPosition: + start: [0, 9] + text: | + + # a comment + echo "hi" + position: + start: [0, 0] + text: |- + {% liquid + # a comment + echo "hi" + %} + markup: + - type: LiquidTag + name: "#" + markup: a comment + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: "# a comment" } + markupPosition: { start: [1, 2], text: a comment } + position: { start: [1, 0], text: "# a comment" } + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 0], text: echo "hi" } + markupPosition: { start: [2, 5], text: '"hi"' } + position: { start: [2, 0], text: echo "hi" } + markup: + type: LiquidVariable + rawSource: '"hi"' + position: { start: [2, 5], text: '"hi"' } + expression: + type: String + single: false + value: hi + position: { start: [2, 5], text: '"hi"' } + filters: [] + - name: should preserve inner indentation after `#`, stripping only one separator space (Category J) + source: |- + {% liquid + # fancy + echo "hi" + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + # fancy + echo "hi" + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + # fancy + echo "hi" + %} + markupPosition: + start: [0, 9] + text: | + + # fancy + echo "hi" + position: + start: [0, 0] + text: |- + {% liquid + # fancy + echo "hi" + %} + markup: + - type: LiquidTag + name: "#" + markup: " fancy" + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: "# fancy" } + markupPosition: { start: [1, 2], text: " fancy" } + position: { start: [1, 0], text: "# fancy" } + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 0], text: echo "hi" } + markupPosition: { start: [2, 5], text: '"hi"' } + position: { start: [2, 0], text: echo "hi" } + markup: + type: LiquidVariable + rawSource: '"hi"' + position: { start: [2, 5], text: '"hi"' } + expression: + type: String + single: false + value: hi + position: { start: [2, 5], text: '"hi"' } + filters: [] + - name: should skip empty lines + source: |- + {% liquid + + echo "hi" + + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + + echo "hi" + + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + + echo "hi" + + %} + markupPosition: + start: [0, 9] + text: |+ + + + echo "hi" + + position: + start: [0, 0] + text: |- + {% liquid + + echo "hi" + + %} + markup: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 0], text: echo "hi" } + markupPosition: { start: [2, 5], text: '"hi"' } + position: { start: [2, 0], text: echo "hi" } + markup: + type: LiquidVariable + rawSource: '"hi"' + position: { start: [2, 5], text: '"hi"' } + expression: + type: String + single: false + value: hi + position: { start: [2, 5], text: '"hi"' } + filters: [] + - name: should parse unknown tag as base case + source: |- + {% liquid + unknown_tag foo + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + unknown_tag foo + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + unknown_tag foo + %} + markupPosition: + start: [0, 9] + text: | + + unknown_tag foo + position: + start: [0, 0] + text: |- + {% liquid + unknown_tag foo + %} + markup: + - type: LiquidTag + name: unknown_tag + markup: foo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: unknown_tag foo } + markupPosition: { start: [1, 12], text: foo } + position: { start: [1, 0], text: unknown_tag foo } + - name: should parse nested blocks + source: |- + {% liquid + for item in items + if item + echo item + endif + endfor + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + for item in items + if item + echo item + endif + endfor + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + for item in items + if item + echo item + endif + endfor + %} + markupPosition: + start: [0, 9] + text: | + + for item in items + if item + echo item + endif + endfor + position: + start: [0, 0] + text: |- + {% liquid + for item in items + if item + echo item + endif + endfor + %} + markup: + - type: LiquidTag + name: for + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: for item in items } + blockEndPosition: { start: [5, 0], text: endfor } + markupPosition: { start: [1, 4], text: item in items } + position: + start: [1, 0] + text: |- + for item in items + if item + echo item + endif + endfor + markup: + type: ForMarkup + variableName: item + reversed: false + position: { start: [1, 4], text: item in items } + collection: + type: VariableLookup + name: items + position: { start: [1, 12], text: items } + lookups: [] + lookupModes: [] + args: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 17], text: "" } + blockStartPosition: { start: [1, 17], text: "" } + blockEndPosition: { start: [5, 0], text: "" } + position: + start: [1, 17] + text: | + + if item + echo item + endif + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [2, 0], text: if item } + blockEndPosition: { start: [4, 0], text: endif } + markupPosition: { start: [2, 3], text: item } + position: + start: [2, 0] + text: |- + if item + echo item + endif + markup: + type: VariableLookup + name: item + position: { start: [2, 3], text: item } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [2, 7], text: "" } + blockStartPosition: { start: [2, 7], text: "" } + blockEndPosition: { start: [4, 0], text: "" } + position: + start: [2, 7] + text: | + + echo item + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [3, 0], text: echo item } + markupPosition: { start: [3, 5], text: item } + position: { start: [3, 0], text: echo item } + markup: + type: LiquidVariable + rawSource: item + position: { start: [3, 5], text: item } + expression: + type: VariableLookup + name: item + position: { start: [3, 5], text: item } + lookups: [] + lookupModes: [] + filters: [] + - name: should fall back to base case for bad markup (tolerant) + source: |- + {% liquid + assign !bad + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + assign !bad + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + assign !bad + %} + markupPosition: + start: [0, 9] + text: | + + assign !bad + position: + start: [0, 0] + text: |- + {% liquid + assign !bad + %} + markup: + - type: LiquidTag + name: assign + markup: "!bad" + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: assign !bad } + markupPosition: { start: [1, 7], text: "!bad" } + position: { start: [1, 0], text: assign !bad } + reason: Expected Equality but found EndOfString () + - name: should parse multiple blocks at same level + source: |- + {% liquid + if a + echo "1" + endif + if b + echo "2" + endif + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + if a + echo "1" + endif + if b + echo "2" + endif + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + if a + echo "1" + endif + if b + echo "2" + endif + %} + markupPosition: + start: [0, 9] + text: | + + if a + echo "1" + endif + if b + echo "2" + endif + position: + start: [0, 0] + text: |- + {% liquid + if a + echo "1" + endif + if b + echo "2" + endif + %} + markup: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: if a } + blockEndPosition: { start: [3, 0], text: endif } + markupPosition: { start: [1, 3], text: a } + position: + start: [1, 0] + text: |- + if a + echo "1" + endif + markup: + type: VariableLookup + name: a + position: { start: [1, 3], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 4], text: "" } + blockStartPosition: { start: [1, 4], text: "" } + blockEndPosition: { start: [3, 0], text: "" } + position: + start: [1, 4] + text: | + + echo "1" + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 0], text: echo "1" } + markupPosition: { start: [2, 5], text: '"1"' } + position: { start: [2, 0], text: echo "1" } + markup: + type: LiquidVariable + rawSource: '"1"' + position: { start: [2, 5], text: '"1"' } + expression: + type: String + single: false + value: "1" + position: { start: [2, 5], text: '"1"' } + filters: [] + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [4, 0], text: if b } + blockEndPosition: { start: [6, 0], text: endif } + markupPosition: { start: [4, 3], text: b } + position: + start: [4, 0] + text: |- + if b + echo "2" + endif + markup: + type: VariableLookup + name: b + position: { start: [4, 3], text: b } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [4, 4], text: "" } + blockStartPosition: { start: [4, 4], text: "" } + blockEndPosition: { start: [6, 0], text: "" } + position: + start: [4, 4] + text: | + + echo "2" + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [5, 0], text: echo "2" } + markupPosition: { start: [5, 5], text: '"2"' } + position: { start: [5, 0], text: echo "2" } + markup: + type: LiquidVariable + rawSource: '"2"' + position: { start: [5, 5], text: '"2"' } + expression: + type: String + single: false + value: "2" + position: { start: [5, 5], text: '"2"' } + filters: [] + - name: should have non-zero-width blockEndPosition for case/endcase + source: |- + {% liquid + case x + when 1 + echo "one" + endcase + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + case x + when 1 + echo "one" + endcase + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + case x + when 1 + echo "one" + endcase + %} + markupPosition: + start: [0, 9] + text: | + + case x + when 1 + echo "one" + endcase + position: + start: [0, 0] + text: |- + {% liquid + case x + when 1 + echo "one" + endcase + %} + markup: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: case x } + blockEndPosition: { start: [4, 0], text: endcase } + markupPosition: { start: [1, 5], text: x } + position: + start: [1, 0] + text: |- + case x + when 1 + echo "one" + endcase + markup: + type: VariableLookup + name: x + position: { start: [1, 5], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 6], text: "" } + blockStartPosition: { start: [1, 6], text: "" } + blockEndPosition: { start: [2, 0], text: "" } + position: + start: [1, 6] + text: |+ + + children: [] + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [2, 5], text: "1" } + blockStartPosition: { start: [2, 0], text: when 1 } + blockEndPosition: { start: [4, 0], text: "" } + position: + start: [2, 0] + text: | + when 1 + echo "one" + markup: + - type: Number + value: "1" + position: { start: [2, 5], text: "1" } + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [3, 0], text: echo "one" } + markupPosition: { start: [3, 5], text: '"one"' } + position: { start: [3, 0], text: echo "one" } + markup: + type: LiquidVariable + rawSource: '"one"' + position: { start: [3, 5], text: '"one"' } + expression: + type: String + single: false + value: one + position: { start: [3, 5], text: '"one"' } + filters: [] + - name: should have non-zero-width blockEndPosition for raw tags (comment) + source: |- + {% liquid + comment + ignored + endcomment + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + comment + ignored + endcomment + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + comment + ignored + endcomment + %} + markupPosition: + start: [0, 9] + text: | + + comment + ignored + endcomment + position: + start: [0, 0] + text: |- + {% liquid + comment + ignored + endcomment + %} + markup: + - type: LiquidRawTag + name: comment + markup: "" + markupPosition: { start: [1, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: comment } + blockEndPosition: { start: [3, 0], text: endcomment } + position: + start: [1, 0] + text: |- + comment + ignored + endcomment + body: + type: RawMarkup + kind: text + value: | + ignored + position: + start: [1, 7] + text: | + + ignored + nodes: + - type: TextNode + value: ignored + position: { start: [2, 0], text: ignored } + - name: should balance nested comments so the outer endcomment closes the block + source: |- + {% liquid + comment + outer + comment + inner + endcomment + endcomment + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + comment + outer + comment + inner + endcomment + endcomment + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + comment + outer + comment + inner + endcomment + endcomment + %} + markupPosition: + start: [0, 9] + text: | + + comment + outer + comment + inner + endcomment + endcomment + position: + start: [0, 0] + text: |- + {% liquid + comment + outer + comment + inner + endcomment + endcomment + %} + markup: + - type: LiquidRawTag + name: comment + markup: "" + markupPosition: { start: [1, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: comment } + blockEndPosition: { start: [6, 0], text: endcomment } + position: + start: [1, 0] + text: |- + comment + outer + comment + inner + endcomment + endcomment + body: + type: RawMarkup + kind: text + value: | + outer + comment + inner + endcomment + position: + start: [1, 7] + text: | + + outer + comment + inner + endcomment + nodes: + - type: TextNode + value: |- + outer + comment + inner + endcomment + position: + start: [2, 0] + text: |- + outer + comment + inner + endcomment + - name: should carve out a nested raw so a literal endcomment inside it does not close early + source: |- + {% liquid + comment + before + raw + endcomment + endraw + after + endcomment + %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + comment + before + raw + endcomment + endraw + after + endcomment + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + comment + before + raw + endcomment + endraw + after + endcomment + %} + markupPosition: + start: [0, 9] + text: | + + comment + before + raw + endcomment + endraw + after + endcomment + position: + start: [0, 0] + text: |- + {% liquid + comment + before + raw + endcomment + endraw + after + endcomment + %} + markup: + - type: LiquidRawTag + name: comment + markup: "" + markupPosition: { start: [1, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 0], text: comment } + blockEndPosition: { start: [7, 0], text: endcomment } + position: + start: [1, 0] + text: |- + comment + before + raw + endcomment + endraw + after + endcomment + body: + type: RawMarkup + kind: text + value: | + before + raw + endcomment + endraw + after + position: + start: [1, 7] + text: | + + before + raw + endcomment + endraw + after + nodes: + - type: TextNode + value: |- + before + raw + endcomment + endraw + after + position: + start: [2, 0] + text: |- + before + raw + endcomment + endraw + after diff --git a/specs/parser/liquid-raw.yml b/specs/parser/liquid-raw.yml new file mode 100644 index 000000000..31febb4d9 --- /dev/null +++ b/specs/parser/liquid-raw.yml @@ -0,0 +1,1954 @@ +cases: + - name: should parse raw-like tags (1) + source: " + + \ {% javascript %} + + \ {% liquid + + \ assign x = 1 + + \ assign x = 2 + + \ %} + + \ /* comment string */ + + \ {% endjavascript %} + + \ " + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% javascript %} + + \ {% liquid + + \ assign x = 1 + + \ assign x = 2 + + \ %} + + \ /* comment string */ + + \ {% endjavascript %} + + \ " + children: + - type: LiquidRawTag + name: javascript + markup: "" + markupPosition: { start: [1, 26], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 12], text: "{% javascript %}" } + blockEndPosition: { start: [7, 12], text: "{% endjavascript %}" } + position: + start: [1, 12] + text: |- + {% javascript %} + {% liquid + assign x = 1 + assign x = 2 + %} + /* comment string */ + {% endjavascript %} + body: + type: RawMarkup + kind: javascript + value: " + + \ {% liquid + + \ assign x = 1 + + \ assign x = 2 + + \ %} + + \ /* comment string */ + + \ " + position: + start: [1, 28] + text: " + + \ {% liquid + + \ assign x = 1 + + \ assign x = 2 + + \ %} + + \ /* comment string */ + + \ " + nodes: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [2, 14] + text: |- + {% liquid + assign x = 1 + assign x = 2 + %} + markupPosition: + start: [2, 23] + text: " + + \ assign x = 1 + + \ assign x = 2 + + \ " + position: + start: [2, 14] + text: |- + {% liquid + assign x = 1 + assign x = 2 + %} + markup: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [3, 16], text: assign x = 1 } + markupPosition: { start: [3, 23], text: x = 1 } + position: { start: [3, 16], text: assign x = 1 } + markup: + type: AssignMarkup + name: x + position: { start: [3, 23], text: x = 1 } + value: + type: LiquidVariable + rawSource: "1" + position: { start: [3, 27], text: "1" } + expression: + type: Number + value: "1" + position: { start: [3, 27], text: "1" } + filters: [] + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [4, 16], text: assign x = 2 } + markupPosition: { start: [4, 23], text: x = 2 } + position: { start: [4, 16], text: assign x = 2 } + markup: + type: AssignMarkup + name: x + position: { start: [4, 23], text: x = 2 } + value: + type: LiquidVariable + rawSource: "2" + position: { start: [4, 27], text: "2" } + expression: + type: Number + value: "2" + position: { start: [4, 27], text: "2" } + filters: [] + - type: TextNode + value: /* comment string */ + position: { start: [6, 14], text: /* comment string */ } + - name: should parse raw-like tags (2) + source: " + + \ {% style %} + + \ {% liquid + + \ assign x = 1 + + \ assign x = 2 + + \ %} + + \ /* comment string */ + + \ {% endstyle %} + + \ " + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% style %} + + \ {% liquid + + \ assign x = 1 + + \ assign x = 2 + + \ %} + + \ /* comment string */ + + \ {% endstyle %} + + \ " + children: + - type: LiquidRawTag + name: style + markup: "" + markupPosition: { start: [1, 21], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 12], text: "{% style %}" } + blockEndPosition: { start: [7, 12], text: "{% endstyle %}" } + position: + start: [1, 12] + text: |- + {% style %} + {% liquid + assign x = 1 + assign x = 2 + %} + /* comment string */ + {% endstyle %} + body: + type: RawMarkup + kind: text + value: " + + \ {% liquid + + \ assign x = 1 + + \ assign x = 2 + + \ %} + + \ /* comment string */ + + \ " + position: + start: [1, 23] + text: " + + \ {% liquid + + \ assign x = 1 + + \ assign x = 2 + + \ %} + + \ /* comment string */ + + \ " + nodes: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [2, 14] + text: |- + {% liquid + assign x = 1 + assign x = 2 + %} + markupPosition: + start: [2, 23] + text: " + + \ assign x = 1 + + \ assign x = 2 + + \ " + position: + start: [2, 14] + text: |- + {% liquid + assign x = 1 + assign x = 2 + %} + markup: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [3, 16], text: assign x = 1 } + markupPosition: { start: [3, 23], text: x = 1 } + position: { start: [3, 16], text: assign x = 1 } + markup: + type: AssignMarkup + name: x + position: { start: [3, 23], text: x = 1 } + value: + type: LiquidVariable + rawSource: "1" + position: { start: [3, 27], text: "1" } + expression: + type: Number + value: "1" + position: { start: [3, 27], text: "1" } + filters: [] + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [4, 16], text: assign x = 2 } + markupPosition: { start: [4, 23], text: x = 2 } + position: { start: [4, 16], text: assign x = 2 } + markup: + type: AssignMarkup + name: x + position: { start: [4, 23], text: x = 2 } + value: + type: LiquidVariable + rawSource: "2" + position: { start: [4, 27], text: "2" } + expression: + type: Number + value: "2" + position: { start: [4, 27], text: "2" } + filters: [] + - type: TextNode + value: /* comment string */ + position: { start: [6, 14], text: /* comment string */ } + - name: should parse schema tag body as a TextNode (Bug 10 regression) + source: '{% schema %}{"name":"test"}{% endschema %}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% schema %}{"name":"test"}{% endschema %}' } + children: + - type: LiquidRawTag + name: schema + markup: "" + markupPosition: { start: [0, 10], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% schema %}" } + blockEndPosition: { start: [0, 27], text: "{% endschema %}" } + position: { start: [0, 0], text: '{% schema %}{"name":"test"}{% endschema %}' } + body: + type: RawMarkup + kind: json + value: '{"name":"test"}' + position: { start: [0, 12], text: '{"name":"test"}' } + nodes: + - type: TextNode + value: '{"name":"test"}' + position: { start: [0, 12], text: '{"name":"test"}' } + - name: should parse schema tag body with surrounding whitespace (Bug 10 regression) + source: |- + {% schema %} + {"name":"test"} + {% endschema %} + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% schema %} + {"name":"test"} + {% endschema %} + children: + - type: LiquidRawTag + name: schema + markup: "" + markupPosition: { start: [0, 10], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% schema %}" } + blockEndPosition: { start: [2, 0], text: "{% endschema %}" } + position: + start: [0, 0] + text: |- + {% schema %} + {"name":"test"} + {% endschema %} + body: + type: RawMarkup + kind: json + value: |2 + + {"name":"test"} + position: + start: [0, 12] + text: |2 + + {"name":"test"} + nodes: + - type: TextNode + value: '{"name":"test"}' + position: { start: [1, 2], text: '{"name":"test"}' } + - name: should parse empty schema tag body as empty nodes (Bug 10 regression) + source: "{% schema %}{% endschema %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% schema %}{% endschema %}" } + children: + - type: LiquidRawTag + name: schema + markup: "" + markupPosition: { start: [0, 10], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% schema %}" } + blockEndPosition: { start: [0, 12], text: "{% endschema %}" } + position: { start: [0, 0], text: "{% schema %}{% endschema %}" } + body: + type: RawMarkup + kind: json + value: "" + position: { start: [0, 12], text: "" } + nodes: [] + - name: should assign kind=css for {% style %} tags without Liquid (Bug 13+42 regression) + source: "{% style %}.foo { color: red }{% endstyle %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% style %}.foo { color: red }{% endstyle %}" } + children: + - type: LiquidRawTag + name: style + markup: "" + markupPosition: { start: [0, 9], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% style %}" } + blockEndPosition: { start: [0, 30], text: "{% endstyle %}" } + position: { start: [0, 0], text: "{% style %}.foo { color: red }{% endstyle %}" } + body: + type: RawMarkup + kind: css + value: ".foo { color: red }" + position: { start: [0, 11], text: ".foo { color: red }" } + nodes: + - type: TextNode + value: ".foo { color: red }" + position: { start: [0, 11], text: ".foo { color: red }" } + - name: should assign kind=css for HTML " + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlRawNode + name: style + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: "" } + attributes: [] + body: + type: RawMarkup + kind: css + value: ".foo { color: red }" + position: { start: [0, 7], text: ".foo { color: red }" } + nodes: + - type: TextNode + value: ".foo { color: red }" + position: { start: [0, 7], text: ".foo { color: red }" } + - name: should assign kind=text for HTML " + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlRawNode + name: style + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: "" } + attributes: [] + body: + type: RawMarkup + kind: text + value: ".foo { color: {{ color }} }" + position: { start: [0, 7], text: ".foo { color: {{ color }} }" } + nodes: + - type: TextNode + value: ".foo { color:" + position: { start: [0, 7], text: ".foo { color:" } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 23], text: " color " } + position: { start: [0, 21], text: "{{ color }}" } + markup: + type: LiquidVariable + rawSource: color + position: { start: [0, 24], text: "color " } + expression: + type: VariableLookup + name: color + position: { start: [0, 24], text: color } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value: "}" + position: { start: [0, 33], text: "}" } + - name: should assign kind=text for HTML + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlRawNode + name: style + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: "" } + attributes: [] + body: + type: RawMarkup + kind: text + value: "{% if true %}.foo { }{% endif %}" + position: { start: [0, 7], text: "{% if true %}.foo { }{% endif %}" } + nodes: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 7], text: "{% if true %}" } + blockEndPosition: { start: [0, 28], text: "{% endif %}" } + markupPosition: { start: [0, 13], text: "true " } + position: { start: [0, 7], text: "{% if true %}.foo { }{% endif %}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 13], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 20], text: "" } + blockStartPosition: { start: [0, 20], text: "" } + blockEndPosition: { start: [0, 28], text: "" } + position: { start: [0, 20], text: ".foo { }" } + children: + - type: TextNode + value: .foo { } + position: { start: [0, 20], text: ".foo { }" } + - name: should assign kind=text for {% style %} with Liquid content (Bug 42 regression) + source: "{% style %}.foo { color: {{ color }} }{% endstyle %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% style %}.foo { color: {{ color }} }{% endstyle %}" } + children: + - type: LiquidRawTag + name: style + markup: "" + markupPosition: { start: [0, 9], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% style %}" } + blockEndPosition: { start: [0, 38], text: "{% endstyle %}" } + position: { start: [0, 0], text: "{% style %}.foo { color: {{ color }} }{% endstyle %}" } + body: + type: RawMarkup + kind: text + value: ".foo { color: {{ color }} }" + position: { start: [0, 11], text: ".foo { color: {{ color }} }" } + nodes: + - type: TextNode + value: ".foo { color:" + position: { start: [0, 11], text: ".foo { color:" } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 27], text: " color " } + position: { start: [0, 25], text: "{{ color }}" } + markup: + type: LiquidVariable + rawSource: color + position: { start: [0, 28], text: "color " } + expression: + type: VariableLookup + name: color + position: { start: [0, 28], text: color } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value: "}" + position: { start: [0, 37], text: "}" } + - name: should parse inside SVG raw body with Liquid blocks as TextNode, not HtmlElement (Bug 46) + source: {% if true %}{% endif %} + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% if true %}{% endif %}' } + children: + - type: HtmlRawNode + name: svg + blockStartPosition: { start: [0, 0], text: } + blockEndPosition: { start: [0, 49], text: } + position: { start: [0, 0], text: '{% if true %}{% endif %}' } + attributes: [] + body: + type: RawMarkup + kind: text + value: '{% if true %}{% endif %}' + position: { start: [0, 5], text: '{% if true %}{% endif %}' } + nodes: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 5], text: "{% if true %}" } + blockEndPosition: { start: [0, 38], text: "{% endif %}" } + markupPosition: { start: [0, 11], text: "true " } + position: { start: [0, 5], text: '{% if true %}{% endif %}' } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 11], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 18], text: "" } + blockStartPosition: { start: [0, 18], text: "" } + blockEndPosition: { start: [0, 38], text: "" } + position: { start: [0, 18], text: } + children: + - type: TextNode + value: + position: { start: [0, 18], text: } + - name: should depth-balance nested same-name raw tags and close at the outer tag (issue 156) (1) + source: abc + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: abc } + children: + - type: HtmlRawNode + name: svg + blockStartPosition: { start: [0, 0], text: } + blockEndPosition: { start: [0, 19], text: } + position: { start: [0, 0], text: abc } + attributes: [] + body: + type: RawMarkup + kind: text + value: abc + position: { start: [0, 5], text: abc } + nodes: + - type: TextNode + value: abc + position: { start: [0, 5], text: abc } + - name: should depth-balance nested same-name raw tags and close at the outer tag (issue 156) (2) + source: c + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: c } + children: + - type: HtmlRawNode + name: script + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: c } + attributes: [] + body: + type: RawMarkup + kind: javascript + value: ac + position: { start: [0, 8], text: ac } + nodes: + - type: TextNode + value: ac + position: { start: [0, 8], text: ac } + - name: should parse basic raw tag + source: "{% raw %}hello world{% endraw %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% raw %}hello world{% endraw %}" } + children: + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% raw %}" } + blockEndPosition: { start: [0, 20], text: "{% endraw %}" } + position: { start: [0, 0], text: "{% raw %}hello world{% endraw %}" } + body: + type: RawMarkup + kind: text + value: hello world + position: { start: [0, 9], text: hello world } + nodes: + - type: TextNode + value: hello world + position: { start: [0, 9], text: hello world } + - name: should parse comment tag + source: "{% comment %}this is a comment{% endcomment %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% comment %}this is a comment{% endcomment %}" } + children: + - type: LiquidRawTag + name: comment + markup: "" + markupPosition: { start: [0, 11], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% comment %}" } + blockEndPosition: { start: [0, 30], text: "{% endcomment %}" } + position: { start: [0, 0], text: "{% comment %}this is a comment{% endcomment %}" } + body: + type: RawMarkup + kind: text + value: this is a comment + position: { start: [0, 13], text: this is a comment } + nodes: + - type: TextNode + value: this is a comment + position: { start: [0, 13], text: this is a comment } + - name: should parse schema tag + source: '{% schema %}{"key": "value"}{% endschema %}' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% schema %}{"key": "value"}{% endschema %}' } + children: + - type: LiquidRawTag + name: schema + markup: "" + markupPosition: { start: [0, 10], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% schema %}" } + blockEndPosition: { start: [0, 28], text: "{% endschema %}" } + position: { start: [0, 0], text: '{% schema %}{"key": "value"}{% endschema %}' } + body: + type: RawMarkup + kind: json + value: '{"key": "value"}' + position: { start: [0, 12], text: '{"key": "value"}' } + nodes: + - type: TextNode + value: '{"key": "value"}' + position: { start: [0, 12], text: '{"key": "value"}' } + - name: should parse javascript tag with Liquid in body + source: "{% javascript %}var x = {{ value }};{% endjavascript %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% javascript %}var x = {{ value }};{% endjavascript %}" } + children: + - type: LiquidRawTag + name: javascript + markup: "" + markupPosition: { start: [0, 14], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% javascript %}" } + blockEndPosition: { start: [0, 36], text: "{% endjavascript %}" } + position: { start: [0, 0], text: "{% javascript %}var x = {{ value }};{% endjavascript %}" } + body: + type: RawMarkup + kind: javascript + value: var x = {{ value }}; + position: { start: [0, 16], text: "var x = {{ value }};" } + nodes: + - type: TextNode + value: var x = + position: { start: [0, 16], text: var x = } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 26], text: " value " } + position: { start: [0, 24], text: "{{ value }}" } + markup: + type: LiquidVariable + rawSource: value + position: { start: [0, 27], text: "value " } + expression: + type: VariableLookup + name: value + position: { start: [0, 27], text: value } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value: ; + position: { start: [0, 35], text: ; } + - name: should parse doc tag with param content + source: "{% doc %}@param {String} name - description{% enddoc %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% doc %}@param {String} name - description{% enddoc %}" } + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% doc %}" } + blockEndPosition: { start: [0, 43], text: "{% enddoc %}" } + position: { start: [0, 0], text: "{% doc %}@param {String} name - description{% enddoc %}" } + body: + type: RawMarkup + kind: text + value: "@param {String} name - description" + position: { start: [0, 9], text: "@param {String} name - description" } + nodes: + - type: LiquidDocParamNode + name: param + required: true + position: { start: [0, 9], text: "@param {String} name - description" } + paramName: + type: TextNode + value: name + position: { start: [0, 25], text: name } + paramType: + type: TextNode + value: String + position: { start: [0, 17], text: String } + paramDescription: + type: TextNode + value: description + position: { start: [0, 32], text: description } + - name: should handle whitespace stripping on open and close + source: "{%- raw -%}hello{%- endraw -%}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{%- raw -%}hello{%- endraw -%}" } + children: + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 8], text: "" } + whitespaceStart: "-" + whitespaceEnd: "-" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: "{%- raw -%}" } + blockEndPosition: { start: [0, 16], text: "{%- endraw -%}" } + position: { start: [0, 0], text: "{%- raw -%}hello{%- endraw -%}" } + body: + type: RawMarkup + kind: text + value: hello + position: { start: [0, 11], text: hello } + nodes: + - type: TextNode + value: hello + position: { start: [0, 11], text: hello } + - name: should track raw tag markup position + source: "{% raw foo %}{% endraw %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% raw foo %}{% endraw %}" } + children: + - type: LiquidRawTag + name: raw + markup: foo + markupPosition: { start: [0, 7], text: foo } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% raw foo %}" } + blockEndPosition: { start: [0, 13], text: "{% endraw %}" } + position: { start: [0, 0], text: "{% raw foo %}{% endraw %}" } + body: + type: RawMarkup + kind: text + value: "" + position: { start: [0, 13], text: "" } + nodes: [] + - name: should track whitespace-stripped raw tag markup position + source: "{%- raw foo -%}{%- endraw -%}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{%- raw foo -%}{%- endraw -%}" } + children: + - type: LiquidRawTag + name: raw + markup: foo + markupPosition: { start: [0, 8], text: foo } + whitespaceStart: "-" + whitespaceEnd: "-" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: "{%- raw foo -%}" } + blockEndPosition: { start: [0, 15], text: "{%- endraw -%}" } + position: { start: [0, 0], text: "{%- raw foo -%}{%- endraw -%}" } + body: + type: RawMarkup + kind: text + value: "" + position: { start: [0, 15], text: "" } + nodes: [] + - name: should track positions correctly with prefix + source: XXXXX{% raw %}hello{% endraw %} + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "XXXXX{% raw %}hello{% endraw %}" } + children: + - type: TextNode + value: XXXXX + position: { start: [0, 0], text: XXXXX } + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 12], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 5], text: "{% raw %}" } + blockEndPosition: { start: [0, 19], text: "{% endraw %}" } + position: { start: [0, 5], text: "{% raw %}hello{% endraw %}" } + body: + type: RawMarkup + kind: text + value: hello + position: { start: [0, 14], text: hello } + nodes: + - type: TextNode + value: hello + position: { start: [0, 14], text: hello } + - name: should map RawMarkupKinds correctly for each tag name (1) + source: "{% raw %}body{% endraw %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% raw %}body{% endraw %}" } + children: + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% raw %}" } + blockEndPosition: { start: [0, 13], text: "{% endraw %}" } + position: { start: [0, 0], text: "{% raw %}body{% endraw %}" } + body: + type: RawMarkup + kind: text + value: body + position: { start: [0, 9], text: body } + nodes: + - type: TextNode + value: body + position: { start: [0, 9], text: body } + - name: should map RawMarkupKinds correctly for each tag name (2) + source: "{% comment %}body{% endcomment %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% comment %}body{% endcomment %}" } + children: + - type: LiquidRawTag + name: comment + markup: "" + markupPosition: { start: [0, 11], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% comment %}" } + blockEndPosition: { start: [0, 17], text: "{% endcomment %}" } + position: { start: [0, 0], text: "{% comment %}body{% endcomment %}" } + body: + type: RawMarkup + kind: text + value: body + position: { start: [0, 13], text: body } + nodes: + - type: TextNode + value: body + position: { start: [0, 13], text: body } + - name: should map RawMarkupKinds correctly for each tag name (3) + source: "{% schema %}body{% endschema %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% schema %}body{% endschema %}" } + children: + - type: LiquidRawTag + name: schema + markup: "" + markupPosition: { start: [0, 10], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% schema %}" } + blockEndPosition: { start: [0, 16], text: "{% endschema %}" } + position: { start: [0, 0], text: "{% schema %}body{% endschema %}" } + body: + type: RawMarkup + kind: json + value: body + position: { start: [0, 12], text: body } + nodes: + - type: TextNode + value: body + position: { start: [0, 12], text: body } + - name: should map RawMarkupKinds correctly for each tag name (4) + source: "{% javascript %}body{% endjavascript %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% javascript %}body{% endjavascript %}" } + children: + - type: LiquidRawTag + name: javascript + markup: "" + markupPosition: { start: [0, 14], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% javascript %}" } + blockEndPosition: { start: [0, 20], text: "{% endjavascript %}" } + position: { start: [0, 0], text: "{% javascript %}body{% endjavascript %}" } + body: + type: RawMarkup + kind: javascript + value: body + position: { start: [0, 16], text: body } + nodes: + - type: TextNode + value: body + position: { start: [0, 16], text: body } + - name: should map RawMarkupKinds correctly for each tag name (5) + source: "{% style %}body{% endstyle %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% style %}body{% endstyle %}" } + children: + - type: LiquidRawTag + name: style + markup: "" + markupPosition: { start: [0, 9], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% style %}" } + blockEndPosition: { start: [0, 15], text: "{% endstyle %}" } + position: { start: [0, 0], text: "{% style %}body{% endstyle %}" } + body: + type: RawMarkup + kind: css + value: body + position: { start: [0, 11], text: body } + nodes: + - type: TextNode + value: body + position: { start: [0, 11], text: body } + - name: should map RawMarkupKinds correctly for each tag name (6) + source: "{% stylesheet %}body{% endstylesheet %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% stylesheet %}body{% endstylesheet %}" } + children: + - type: LiquidRawTag + name: stylesheet + markup: "" + markupPosition: { start: [0, 14], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% stylesheet %}" } + blockEndPosition: { start: [0, 20], text: "{% endstylesheet %}" } + position: { start: [0, 0], text: "{% stylesheet %}body{% endstylesheet %}" } + body: + type: RawMarkup + kind: css + value: body + position: { start: [0, 16], text: body } + nodes: + - type: TextNode + value: body + position: { start: [0, 16], text: body } + - name: should map RawMarkupKinds correctly for each tag name (7) + source: "{% doc %}body{% enddoc %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% doc %}body{% enddoc %}" } + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% doc %}" } + blockEndPosition: { start: [0, 13], text: "{% enddoc %}" } + position: { start: [0, 0], text: "{% doc %}body{% enddoc %}" } + body: + type: RawMarkup + kind: text + value: body + position: { start: [0, 9], text: body } + nodes: + - type: LiquidDocDescriptionNode + name: description + isImplicit: true + isInline: true + position: { start: [0, 9], text: body } + content: + type: TextNode + value: body + position: { start: [0, 9], text: body } + - name: should parse empty body + source: "{% raw %}{% endraw %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% raw %}{% endraw %}" } + children: + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% raw %}" } + blockEndPosition: { start: [0, 9], text: "{% endraw %}" } + position: { start: [0, 0], text: "{% raw %}{% endraw %}" } + body: + type: RawMarkup + kind: text + value: "" + position: { start: [0, 9], text: "" } + nodes: [] + - name: should preserve newlines in multiline body + source: |- + {% comment %} + line1 + line2 + {% endcomment %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% comment %} + line1 + line2 + {% endcomment %} + children: + - type: LiquidRawTag + name: comment + markup: "" + markupPosition: { start: [0, 11], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% comment %}" } + blockEndPosition: { start: [3, 0], text: "{% endcomment %}" } + position: + start: [0, 0] + text: |- + {% comment %} + line1 + line2 + {% endcomment %} + body: + type: RawMarkup + kind: text + value: | + + line1 + line2 + position: + start: [0, 13] + text: | + + line1 + line2 + nodes: + - type: TextNode + value: |- + line1 + line2 + position: + start: [1, 0] + text: |- + line1 + line2 + - name: should throw for unclosed raw tag + source: "{% raw %}hello" + parse: [html] + error: + message: Attempting to end parsing before LiquidRawTag 'raw' was closed + start: [0, 0] + text: "{% raw %}" + - name: should work with toLiquidAST + source: "{% raw %}hello{% endraw %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% raw %}hello{% endraw %}" } + children: + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% raw %}" } + blockEndPosition: { start: [0, 14], text: "{% endraw %}" } + position: { start: [0, 0], text: "{% raw %}hello{% endraw %}" } + body: + type: RawMarkup + kind: text + value: hello + position: { start: [0, 9], text: hello } + nodes: + - type: TextNode + value: hello + position: { start: [0, 9], text: hello } + - name: should parse stylesheet tag + source: "{% stylesheet %}.cls { }{% endstylesheet %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% stylesheet %}.cls { }{% endstylesheet %}" } + children: + - type: LiquidRawTag + name: stylesheet + markup: "" + markupPosition: { start: [0, 14], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% stylesheet %}" } + blockEndPosition: { start: [0, 24], text: "{% endstylesheet %}" } + position: { start: [0, 0], text: "{% stylesheet %}.cls { }{% endstylesheet %}" } + body: + type: RawMarkup + kind: css + value: .cls { } + position: { start: [0, 16], text: ".cls { }" } + nodes: + - type: TextNode + value: .cls { } + position: { start: [0, 16], text: ".cls { }" } + - name: should parse doc tag whose body contains liquid-like syntax + source: |- + {%- doc -%} + @param {object} price - The price. + @example + {% render 'unit-price', price: variant.unit_price } + {%- enddoc -%} + after + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {%- doc -%} + @param {object} price - The price. + @example + {% render 'unit-price', price: variant.unit_price } + {%- enddoc -%} + after + children: + - type: LiquidRawTag + name: doc + markup: "" + markupPosition: { start: [0, 8], text: "" } + whitespaceStart: "-" + whitespaceEnd: "-" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: "{%- doc -%}" } + blockEndPosition: { start: [4, 0], text: "{%- enddoc -%}" } + position: + start: [0, 0] + text: |- + {%- doc -%} + @param {object} price - The price. + @example + {% render 'unit-price', price: variant.unit_price } + {%- enddoc -%} + body: + type: RawMarkup + kind: text + value: |2 + + @param {object} price - The price. + @example + {% render 'unit-price', price: variant.unit_price } + position: + start: [0, 11] + text: |2 + + @param {object} price - The price. + @example + {% render 'unit-price', price: variant.unit_price } + nodes: + - type: LiquidDocParamNode + name: param + required: true + position: { start: [1, 2], text: "@param {object} price - The price." } + paramName: + type: TextNode + value: price + position: { start: [1, 18], text: price } + paramType: + type: TextNode + value: object + position: { start: [1, 10], text: object } + paramDescription: + type: TextNode + value: The price. + position: { start: [1, 26], text: The price. } + - type: LiquidDocExampleNode + name: example + isInline: false + position: + start: [2, 2] + text: | + @example + {% render 'unit-price', price: variant.unit_price } + content: + type: TextNode + value: | + {% render 'unit-price', price: variant.unit_price } + position: + start: [3, 2] + text: | + {% render 'unit-price', price: variant.unit_price } + - type: TextNode + value: after + position: { start: [5, 0], text: after } + - name: should parse style tag with Liquid tag in body + source: "{% style %}{% if true %}.active{}{% endif %}{% endstyle %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% style %}{% if true %}.active{}{% endif %}{% endstyle %}" } + children: + - type: LiquidRawTag + name: style + markup: "" + markupPosition: { start: [0, 9], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% style %}" } + blockEndPosition: { start: [0, 44], text: "{% endstyle %}" } + position: { start: [0, 0], text: "{% style %}{% if true %}.active{}{% endif %}{% endstyle %}" } + body: + type: RawMarkup + kind: text + value: "{% if true %}.active{}{% endif %}" + position: { start: [0, 11], text: "{% if true %}.active{}{% endif %}" } + nodes: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 11], text: "{% if true %}" } + blockEndPosition: { start: [0, 33], text: "{% endif %}" } + markupPosition: { start: [0, 17], text: "true " } + position: { start: [0, 11], text: "{% if true %}.active{}{% endif %}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 17], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 24], text: "" } + blockStartPosition: { start: [0, 24], text: "" } + blockEndPosition: { start: [0, 33], text: "" } + position: { start: [0, 24], text: ".active{}" } + children: + - type: TextNode + value: .active{} + position: { start: [0, 24], text: ".active{}" } + - name: preserves output after endraw when an unclosed {{ in the body straddles the boundary + source: "{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% raw %} Foobar {{ invalid {% endraw %}{{ 1 }}" } + children: + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% raw %}" } + blockEndPosition: { start: [0, 28], text: "{% endraw %}" } + position: { start: [0, 0], text: "{% raw %} Foobar {{ invalid {% endraw %}" } + body: + type: RawMarkup + kind: text + value: " Foobar {{ invalid " + position: { start: [0, 9], text: " Foobar {{ invalid " } + nodes: + - type: TextNode + value: Foobar {{ invalid + position: { start: [0, 10], text: "Foobar {{ invalid" } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 42], text: " 1 " } + position: { start: [0, 40], text: "{{ 1 }}" } + markup: + type: LiquidVariable + rawSource: "1" + position: { start: [0, 43], text: "1 " } + expression: + type: Number + value: "1" + position: { start: [0, 43], text: "1" } + filters: [] + - name: preserves plain text after endraw when the raw body contains a stray {{ + source: "{% raw %}{{ a {% endraw %}tail" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% raw %}{{ a {% endraw %}tail" } + children: + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 7], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% raw %}" } + blockEndPosition: { start: [0, 14], text: "{% endraw %}" } + position: { start: [0, 0], text: "{% raw %}{{ a {% endraw %}" } + body: + type: RawMarkup + kind: text + value: "{{ a " + position: { start: [0, 9], text: "{{ a " } + nodes: + - type: TextNode + value: "{{ a" + position: { start: [0, 9], text: "{{ a" } + - type: TextNode + value: tail + position: { start: [0, 26], text: tail } + - name: re-tokenizes a straddling endraw inside a quoted attribute without losing quote context + source:
    x
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '
    x
    ' } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: '
    ' } + blockEndPosition: { start: [0, 46], text:
    } + position: { start: [0, 0], text: '
    x
    ' } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 11], text: "{% raw %}{{ invalid {% endraw %}" } + position: { start: [0, 5], text: 'data="{% raw %}{{ invalid {% endraw %}"' } + name: + - type: TextNode + value: data + position: { start: [0, 5], text: data } + value: + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 18], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 11], text: "{% raw %}" } + blockEndPosition: { start: [0, 31], text: "{% endraw %}" } + position: { start: [0, 11], text: "{% raw %}{{ invalid {% endraw %}" } + body: + type: RawMarkup + kind: text + value: "{{ invalid " + position: { start: [0, 20], text: "{{ invalid " } + nodes: + - type: TextNode + value: "{{ invalid" + position: { start: [0, 20], text: "{{ invalid" } + children: + - type: TextNode + value: x + position: { start: [0, 45], text: x } + - name: handles a straddling endraw inside a single-quoted attribute + source:
    y
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "
    y
    " } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: "
    " } + blockEndPosition: { start: [0, 46], text:
    } + position: { start: [0, 0], text: "
    y
    " } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: + - type: AttrSingleQuoted + attributePosition: { start: [0, 11], text: "{% raw %}{{ invalid {% endraw %}" } + position: { start: [0, 5], text: "data='{% raw %}{{ invalid {% endraw %}'" } + name: + - type: TextNode + value: data + position: { start: [0, 5], text: data } + value: + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 18], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 11], text: "{% raw %}" } + blockEndPosition: { start: [0, 31], text: "{% endraw %}" } + position: { start: [0, 11], text: "{% raw %}{{ invalid {% endraw %}" } + body: + type: RawMarkup + kind: text + value: "{{ invalid " + position: { start: [0, 20], text: "{{ invalid " } + nodes: + - type: TextNode + value: "{{ invalid" + position: { start: [0, 20], text: "{{ invalid" } + children: + - type: TextNode + value: y + position: { start: [0, 45], text: y } + - name: recovers HtmlTag context for a straddling endraw in an unquoted attribute position + source:
    ok
    + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '
    ok
    ' } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 0], text: '
    ' } + blockEndPosition: { start: [0, 58], text:
    } + position: { start: [0, 0], text: '
    ok
    ' } + name: + - type: TextNode + value: div + position: { start: [0, 1], text: div } + attributes: + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 12], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 5], text: "{% raw %}" } + blockEndPosition: { start: [0, 25], text: "{% endraw %}" } + position: { start: [0, 5], text: "{% raw %}{{ invalid {% endraw %}" } + body: + type: RawMarkup + kind: text + value: "{{ invalid " + position: { start: [0, 14], text: "{{ invalid " } + nodes: + - type: TextNode + value: "{{ invalid" + position: { start: [0, 14], text: "{{ invalid" } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 39], text: " 1 " } + position: { start: [0, 37], text: "{{ 1 }}" } + markup: + type: LiquidVariable + rawSource: "1" + position: { start: [0, 40], text: "1 " } + expression: + type: Number + value: "1" + position: { start: [0, 40], text: "1" } + filters: [] + - type: AttrDoubleQuoted + attributePosition: { start: [0, 53], text: "1" } + position: { start: [0, 45], text: data-x="1" } + name: + - type: TextNode + value: data-x + position: { start: [0, 45], text: data-x } + value: + - type: TextNode + value: "1" + position: { start: [0, 53], text: "1" } + children: + - type: TextNode + value: ok + position: { start: [0, 56], text: ok } + - name: does not duplicate a straddling {{ into both a Liquid-in-body raw body and a sibling + source: "{% style %}{{ invalid {% endstyle %}{{ 1 }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% style %}{{ invalid {% endstyle %}{{ 1 }}" } + children: + - type: LiquidRawTag + name: style + markup: "" + markupPosition: { start: [0, 9], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% style %}" } + blockEndPosition: { start: [0, 22], text: "{% endstyle %}" } + position: { start: [0, 0], text: "{% style %}{{ invalid {% endstyle %}" } + body: + type: RawMarkup + kind: text + value: "{{ invalid " + position: { start: [0, 11], text: "{{ invalid " } + nodes: + - type: TextNode + value: "{{ invalid" + position: { start: [0, 11], text: "{{ invalid" } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 38], text: " 1 " } + position: { start: [0, 36], text: "{{ 1 }}" } + markup: + type: LiquidVariable + rawSource: "1" + position: { start: [0, 39], text: "1 " } + expression: + type: Number + value: "1" + position: { start: [0, 39], text: "1" } + filters: [] + - name: does not let a decoy endcommentXXXXX close the comment; only the real endcomment does + source: |- + {% if true %} + {% comment %} + {% commentXXXXX %}wut{% endcommentXXXXX %} + {% endcomment %} + {% endif %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% if true %} + {% comment %} + {% commentXXXXX %}wut{% endcommentXXXXX %} + {% endcomment %} + {% endif %} + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if true %}" } + blockEndPosition: { start: [4, 0], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "true " } + position: + start: [0, 0] + text: |- + {% if true %} + {% comment %} + {% commentXXXXX %}wut{% endcommentXXXXX %} + {% endcomment %} + {% endif %} + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 6], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [4, 0], text: "" } + position: + start: [0, 13] + text: |2 + + {% comment %} + {% commentXXXXX %}wut{% endcommentXXXXX %} + {% endcomment %} + children: + - type: LiquidRawTag + name: comment + markup: "" + markupPosition: { start: [1, 13], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 2], text: "{% comment %}" } + blockEndPosition: { start: [3, 2], text: "{% endcomment %}" } + position: + start: [1, 2] + text: |- + {% comment %} + {% commentXXXXX %}wut{% endcommentXXXXX %} + {% endcomment %} + body: + type: RawMarkup + kind: text + value: " + + \ {% commentXXXXX %}wut{% endcommentXXXXX %} + + \ " + position: + start: [1, 15] + text: " + + \ {% commentXXXXX %}wut{% endcommentXXXXX %} + + \ " + nodes: + - type: TextNode + value: "{% commentXXXXX %}wut{% endcommentXXXXX %}" + position: { start: [2, 4], text: "{% commentXXXXX %}wut{% endcommentXXXXX %}" } + - name: parses a malformed {%comment}...{%endcomment} (no %}) into two flat base-case tags without throwing + source: "{%comment}{% assign a = 1 %}{%endcomment}{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{%comment}{% assign a = 1 %}{%endcomment}{% endif %}" } + children: + - type: LiquidTag + name: comment}{% + markup: assign a = 1 + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{%comment}{% assign a = 1 %}" } + markupPosition: { start: [0, 13], text: "assign a = 1 " } + position: { start: [0, 0], text: "{%comment}{% assign a = 1 %}" } + - type: LiquidTag + name: endcomment}{% + markup: endif + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 28], text: "{%endcomment}{% endif %}" } + markupPosition: { start: [0, 44], text: "endif " } + position: { start: [0, 28], text: "{%endcomment}{% endif %}" } + - name: distinguishes a trimming endraw -%} from a non-trimming endraw %} (1) + source: "> {%- raw -%} inner {%- endraw -%} <" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "> {%- raw -%} inner {%- endraw -%} <" } + children: + - type: TextNode + value: ">" + position: { start: [0, 0], text: ">" } + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 10], text: "" } + whitespaceStart: "-" + whitespaceEnd: "-" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "-" + blockStartPosition: { start: [0, 2], text: "{%- raw -%}" } + blockEndPosition: { start: [0, 20], text: "{%- endraw -%}" } + position: { start: [0, 2], text: "{%- raw -%} inner {%- endraw -%}" } + body: + type: RawMarkup + kind: text + value: " inner " + position: { start: [0, 13], text: " inner " } + nodes: + - type: TextNode + value: inner + position: { start: [0, 14], text: inner } + - type: TextNode + value: < + position: { start: [0, 35], text: < } + - name: distinguishes a trimming endraw -%} from a non-trimming endraw %} (2) + source: "> {%- raw -%} inner {%- endraw %} <" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "> {%- raw -%} inner {%- endraw %} <" } + children: + - type: TextNode + value: ">" + position: { start: [0, 0], text: ">" } + - type: LiquidRawTag + name: raw + markup: "" + markupPosition: { start: [0, 10], text: "" } + whitespaceStart: "-" + whitespaceEnd: "-" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 2], text: "{%- raw -%}" } + blockEndPosition: { start: [0, 20], text: "{%- endraw %}" } + position: { start: [0, 2], text: "{%- raw -%} inner {%- endraw %}" } + body: + type: RawMarkup + kind: text + value: " inner " + position: { start: [0, 13], text: " inner " } + nodes: + - type: TextNode + value: inner + position: { start: [0, 14], text: inner } + - type: TextNode + value: < + position: { start: [0, 34], text: < } diff --git a/specs/parser/liquid-tags.yml b/specs/parser/liquid-tags.yml new file mode 100644 index 000000000..55885f1ae --- /dev/null +++ b/specs/parser/liquid-tags.yml @@ -0,0 +1,1913 @@ +cases: + - name: should transform a basic Liquid Tag into a LiquidTag + source: "{% name %}{% if -%}{%- endif %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% name %}{% if -%}{%- endif %}" } + children: + - type: LiquidTag + name: name + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% name %}" } + markupPosition: { start: [0, 8], text: "" } + position: { start: [0, 0], text: "{% name %}" } + - type: LiquidTag + name: if + markup: "" + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "-" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 10], text: "{% if -%}" } + blockEndPosition: { start: [0, 19], text: "{%- endif %}" } + markupPosition: { start: [0, 16], text: "" } + position: { start: [0, 10], text: "{% if -%}{%- endif %}" } + reason: Expected expression but found EndOfString () + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 19], text: "" } + blockStartPosition: { start: [0, 19], text: "" } + blockEndPosition: { start: [0, 19], text: "" } + position: { start: [0, 19], text: "" } + children: [] + - name: should parse echo tags (1) + source: '{% echo "hi" -%}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% echo "hi" -%}' } + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: '{% echo "hi" -%}' } + markupPosition: { start: [0, 8], text: '"hi" ' } + position: { start: [0, 0], text: '{% echo "hi" -%}' } + markup: + type: LiquidVariable + rawSource: '"hi"' + position: { start: [0, 8], text: '"hi" ' } + expression: + type: String + single: false + value: hi + position: { start: [0, 8], text: '"hi"' } + filters: [] + - name: should parse echo tags (2) + source: "{% echo x | f -%}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% echo x | f -%}" } + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: "{% echo x | f -%}" } + markupPosition: { start: [0, 8], text: "x | f " } + position: { start: [0, 0], text: "{% echo x | f -%}" } + markup: + type: LiquidVariable + rawSource: x | f + position: { start: [0, 8], text: "x | f " } + expression: + type: VariableLookup + name: x + position: { start: [0, 8], text: x } + lookups: [] + lookupModes: [] + filters: + - type: LiquidFilter + name: f + position: { start: [0, 9], text: " | f" } + args: [] + - name: should parse assign tags (1) + source: '{% assign x = "hi" -%}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% assign x = "hi" -%}' } + children: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: '{% assign x = "hi" -%}' } + markupPosition: { start: [0, 10], text: 'x = "hi" ' } + position: { start: [0, 0], text: '{% assign x = "hi" -%}' } + markup: + type: AssignMarkup + name: x + position: { start: [0, 10], text: 'x = "hi" ' } + value: + type: LiquidVariable + rawSource: '"hi"' + position: { start: [0, 14], text: '"hi" ' } + expression: + type: String + single: false + value: hi + position: { start: [0, 14], text: '"hi"' } + filters: [] + - name: should parse assign tags (2) + source: "{% assign z = y | f -%}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% assign z = y | f -%}" } + children: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: "{% assign z = y | f -%}" } + markupPosition: { start: [0, 10], text: "z = y | f " } + position: { start: [0, 0], text: "{% assign z = y | f -%}" } + markup: + type: AssignMarkup + name: z + position: { start: [0, 10], text: "z = y | f " } + value: + type: LiquidVariable + rawSource: y | f + position: { start: [0, 14], text: "y | f " } + expression: + type: VariableLookup + name: y + position: { start: [0, 14], text: y } + lookups: [] + lookupModes: [] + filters: + - type: LiquidFilter + name: f + position: { start: [0, 15], text: " | f" } + args: [] + - name: should parse render tags (1) + source: '{% render "snippet" -%}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% render "snippet" -%}' } + children: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: '{% render "snippet" -%}' } + markupPosition: { start: [0, 10], text: '"snippet" ' } + position: { start: [0, 0], text: '{% render "snippet" -%}' } + markup: + type: RenderMarkup + variable: null + alias: null + position: { start: [0, 10], text: '"snippet" ' } + snippet: + type: String + single: false + value: snippet + position: { start: [0, 10], text: '"snippet"' } + args: [] + - name: should parse render tags (2) + source: '{% render "snippet" as foo -%}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% render "snippet" as foo -%}' } + children: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: '{% render "snippet" as foo -%}' } + markupPosition: { start: [0, 10], text: '"snippet" as foo ' } + position: { start: [0, 0], text: '{% render "snippet" as foo -%}' } + markup: + type: RenderMarkup + variable: null + position: { start: [0, 10], text: '"snippet" as foo ' } + snippet: + type: String + single: false + value: snippet + position: { start: [0, 10], text: '"snippet"' } + alias: + type: RenderAliasExpression + value: foo + position: { start: [0, 20], text: as foo } + args: [] + - name: should parse render tags (3) + source: '{% render "snippet" with "string" as foo -%}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% render "snippet" with "string" as foo -%}' } + children: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: '{% render "snippet" with "string" as foo -%}' } + markupPosition: { start: [0, 10], text: '"snippet" with "string" as foo ' } + position: { start: [0, 0], text: '{% render "snippet" with "string" as foo -%}' } + markup: + type: RenderMarkup + position: { start: [0, 10], text: '"snippet" with "string" as foo ' } + snippet: + type: String + single: false + value: snippet + position: { start: [0, 10], text: '"snippet"' } + variable: + type: RenderVariableExpression + kind: with + position: { start: [0, 20], text: with "string" } + name: + type: String + single: false + value: string + position: { start: [0, 25], text: '"string"' } + alias: + type: RenderAliasExpression + value: foo + position: { start: [0, 34], text: as foo } + args: [] + - name: should parse render tags (4) + source: '{% render "snippet" for products as product -%}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% render "snippet" for products as product -%}' } + children: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: '{% render "snippet" for products as product -%}' } + markupPosition: { start: [0, 10], text: '"snippet" for products as product ' } + position: { start: [0, 0], text: '{% render "snippet" for products as product -%}' } + markup: + type: RenderMarkup + position: { start: [0, 10], text: '"snippet" for products as product ' } + snippet: + type: String + single: false + value: snippet + position: { start: [0, 10], text: '"snippet"' } + variable: + type: RenderVariableExpression + kind: for + position: { start: [0, 20], text: for products } + name: + type: VariableLookup + name: products + position: { start: [0, 24], text: products } + lookups: [] + lookupModes: [] + alias: + type: RenderAliasExpression + value: product + position: { start: [0, 33], text: as product } + args: [] + - name: should parse render tags (5) + source: '{% render variable with "string" as foo, key1: val1, key2: "hi" -%}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% render variable with "string" as foo, key1: val1, key2: "hi" -%}' } + children: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: '{% render variable with "string" as foo, key1: val1, key2: "hi" -%}' } + markupPosition: { start: [0, 10], text: 'variable with "string" as foo, key1: val1, key2: "hi" ' } + position: { start: [0, 0], text: '{% render variable with "string" as foo, key1: val1, key2: "hi" -%}' } + markup: + type: RenderMarkup + position: { start: [0, 10], text: 'variable with "string" as foo, key1: val1, key2: "hi" ' } + snippet: + type: VariableLookup + name: variable + position: { start: [0, 10], text: variable } + lookups: [] + lookupModes: [] + variable: + type: RenderVariableExpression + kind: with + position: { start: [0, 19], text: with "string" } + name: + type: String + single: false + value: string + position: { start: [0, 24], text: '"string"' } + alias: + type: RenderAliasExpression + value: foo + position: { start: [0, 33], text: as foo } + args: + - type: NamedArgument + name: key1 + position: { start: [0, 41], text: "key1: val1" } + value: + type: VariableLookup + name: val1 + position: { start: [0, 47], text: val1 } + lookups: [] + lookupModes: [] + - type: NamedArgument + name: key2 + position: { start: [0, 53], text: 'key2: "hi"' } + value: + type: String + single: false + value: hi + position: { start: [0, 59], text: '"hi"' } + - name: should parse render tags with named args and no comma (Bug 23 regression) (1) + source: "{% render 'snippet' section: section %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% render 'snippet' section: section %}" } + children: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% render 'snippet' section: section %}" } + markupPosition: { start: [0, 10], text: "'snippet' section: section " } + position: { start: [0, 0], text: "{% render 'snippet' section: section %}" } + markup: + type: RenderMarkup + variable: null + alias: null + position: { start: [0, 10], text: "'snippet' section: section " } + snippet: + type: String + single: true + value: snippet + position: { start: [0, 10], text: "'snippet'" } + args: + - type: NamedArgument + name: section + position: { start: [0, 20], text: "section: section" } + value: + type: VariableLookup + name: section + position: { start: [0, 29], text: section } + lookups: [] + lookupModes: [] + - name: should parse render tags with named args and no comma (Bug 23 regression) (2) + source: "{% render 'snippet', section: section %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% render 'snippet', section: section %}" } + children: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% render 'snippet', section: section %}" } + markupPosition: { start: [0, 10], text: "'snippet', section: section " } + position: { start: [0, 0], text: "{% render 'snippet', section: section %}" } + markup: + type: RenderMarkup + variable: null + alias: null + position: { start: [0, 10], text: "'snippet', section: section " } + snippet: + type: String + single: true + value: snippet + position: { start: [0, 10], text: "'snippet'" } + args: + - type: NamedArgument + name: section + position: { start: [0, 21], text: "section: section" } + value: + type: VariableLookup + name: section + position: { start: [0, 30], text: section } + lookups: [] + lookupModes: [] + - name: should parse render tags with named args and no comma (Bug 23 regression) (3) + source: "{% render 'snippet' key1: val1, key2: val2 %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% render 'snippet' key1: val1, key2: val2 %}" } + children: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% render 'snippet' key1: val1, key2: val2 %}" } + markupPosition: { start: [0, 10], text: "'snippet' key1: val1, key2: val2 " } + position: { start: [0, 0], text: "{% render 'snippet' key1: val1, key2: val2 %}" } + markup: + type: RenderMarkup + variable: null + alias: null + position: { start: [0, 10], text: "'snippet' key1: val1, key2: val2 " } + snippet: + type: String + single: true + value: snippet + position: { start: [0, 10], text: "'snippet'" } + args: + - type: NamedArgument + name: key1 + position: { start: [0, 20], text: "key1: val1" } + value: + type: VariableLookup + name: val1 + position: { start: [0, 26], text: val1 } + lookups: [] + lookupModes: [] + - type: NamedArgument + name: key2 + position: { start: [0, 32], text: "key2: val2" } + value: + type: VariableLookup + name: val2 + position: { start: [0, 38], text: val2 } + lookups: [] + lookupModes: [] + - name: should parse render tags with named args and no comma (Bug 23 regression) (4) + source: "{% render 'snippet' for products section: section %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% render 'snippet' for products section: section %}" } + children: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% render 'snippet' for products section: section %}" } + markupPosition: { start: [0, 10], text: "'snippet' for products section: section " } + position: { start: [0, 0], text: "{% render 'snippet' for products section: section %}" } + markup: + type: RenderMarkup + alias: null + position: { start: [0, 10], text: "'snippet' for products section: section " } + snippet: + type: String + single: true + value: snippet + position: { start: [0, 10], text: "'snippet'" } + variable: + type: RenderVariableExpression + kind: for + position: { start: [0, 20], text: for products } + name: + type: VariableLookup + name: products + position: { start: [0, 24], text: products } + lookups: [] + lookupModes: [] + args: + - type: NamedArgument + name: section + position: { start: [0, 33], text: "section: section" } + value: + type: VariableLookup + name: section + position: { start: [0, 42], text: section } + lookups: [] + lookupModes: [] + - name: should parse conditional tags into conditional expressions (1) + source: "{% if a -%}{% endif %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if a -%}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if a -%}" } + blockEndPosition: { start: [0, 11], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "a " } + position: { start: [0, 0], text: "{% if a -%}{% endif %}" } + markup: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 11], text: "" } + blockStartPosition: { start: [0, 11], text: "" } + blockEndPosition: { start: [0, 11], text: "" } + position: { start: [0, 11], text: "" } + children: [] + - name: should parse conditional tags into conditional expressions (2) + source: '{% if a and "string" -%}{% endif %}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% if a and "string" -%}{% endif %}' } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% if a and "string" -%}' } + blockEndPosition: { start: [0, 24], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: 'a and "string" ' } + position: { start: [0, 0], text: '{% if a and "string" -%}{% endif %}' } + markup: + type: LogicalExpression + relation: and + position: { start: [0, 6], text: 'a and "string" ' } + left: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + right: + type: String + single: false + value: string + position: { start: [0, 12], text: '"string"' } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 24], text: "" } + blockStartPosition: { start: [0, 24], text: "" } + blockEndPosition: { start: [0, 24], text: "" } + position: { start: [0, 24], text: "" } + children: [] + - name: should parse conditional tags into conditional expressions (3) + source: '{% if a and "string" or a<1 -%}{% endif %}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% if a and "string" or a<1 -%}{% endif %}' } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% if a and "string" or a<1 -%}' } + blockEndPosition: { start: [0, 31], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: 'a and "string" or a<1 ' } + position: { start: [0, 0], text: '{% if a and "string" or a<1 -%}{% endif %}' } + markup: + type: LogicalExpression + relation: and + position: { start: [0, 6], text: 'a and "string" or a<1 ' } + left: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + right: + type: LogicalExpression + relation: or + position: { start: [0, 8], text: 'and "string" or a<1 ' } + left: + type: String + single: false + value: string + position: { start: [0, 12], text: '"string"' } + right: + type: Comparison + comparator: < + position: { start: [0, 24], text: a<1 } + left: + type: VariableLookup + name: a + position: { start: [0, 24], text: a } + lookups: [] + lookupModes: [] + right: + type: Number + value: "1" + position: { start: [0, 26], text: "1" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 31], text: "" } + blockStartPosition: { start: [0, 31], text: "" } + blockEndPosition: { start: [0, 31], text: "" } + position: { start: [0, 31], text: "" } + children: [] + - name: should parse conditional tags into conditional expressions (4) + source: "{% unless a -%}{% endunless %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% unless a -%}{% endunless %}" } + children: + - type: LiquidTag + name: unless + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% unless a -%}" } + blockEndPosition: { start: [0, 15], text: "{% endunless %}" } + markupPosition: { start: [0, 10], text: "a " } + position: { start: [0, 0], text: "{% unless a -%}{% endunless %}" } + markup: + type: VariableLookup + name: a + position: { start: [0, 10], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 15], text: "" } + blockStartPosition: { start: [0, 15], text: "" } + blockEndPosition: { start: [0, 15], text: "" } + position: { start: [0, 15], text: "" } + children: [] + - name: should parse conditional tags into conditional expressions (5) + source: '{% unless a and "string" -%}{% endunless %}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% unless a and "string" -%}{% endunless %}' } + children: + - type: LiquidTag + name: unless + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% unless a and "string" -%}' } + blockEndPosition: { start: [0, 28], text: "{% endunless %}" } + markupPosition: { start: [0, 10], text: 'a and "string" ' } + position: { start: [0, 0], text: '{% unless a and "string" -%}{% endunless %}' } + markup: + type: LogicalExpression + relation: and + position: { start: [0, 10], text: 'a and "string" ' } + left: + type: VariableLookup + name: a + position: { start: [0, 10], text: a } + lookups: [] + lookupModes: [] + right: + type: String + single: false + value: string + position: { start: [0, 16], text: '"string"' } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 28], text: "" } + blockStartPosition: { start: [0, 28], text: "" } + blockEndPosition: { start: [0, 28], text: "" } + position: { start: [0, 28], text: "" } + children: [] + - name: should parse conditional tags into conditional expressions (6) + source: '{% unless a and "string" or a<1 -%}{% endunless %}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% unless a and "string" or a<1 -%}{% endunless %}' } + children: + - type: LiquidTag + name: unless + whitespaceStart: "" + whitespaceEnd: "-" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% unless a and "string" or a<1 -%}' } + blockEndPosition: { start: [0, 35], text: "{% endunless %}" } + markupPosition: { start: [0, 10], text: 'a and "string" or a<1 ' } + position: { start: [0, 0], text: '{% unless a and "string" or a<1 -%}{% endunless %}' } + markup: + type: LogicalExpression + relation: and + position: { start: [0, 10], text: 'a and "string" or a<1 ' } + left: + type: VariableLookup + name: a + position: { start: [0, 10], text: a } + lookups: [] + lookupModes: [] + right: + type: LogicalExpression + relation: or + position: { start: [0, 12], text: 'and "string" or a<1 ' } + left: + type: String + single: false + value: string + position: { start: [0, 16], text: '"string"' } + right: + type: Comparison + comparator: < + position: { start: [0, 28], text: a<1 } + left: + type: VariableLookup + name: a + position: { start: [0, 28], text: a } + lookups: [] + lookupModes: [] + right: + type: Number + value: "1" + position: { start: [0, 30], text: "1" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 35], text: "" } + blockStartPosition: { start: [0, 35], text: "" } + blockEndPosition: { start: [0, 35], text: "" } + position: { start: [0, 35], text: "" } + children: [] + - name: should parse content_for tags with no arguments + source: '{% content_for "blocks" %}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% content_for "blocks" %}' } + children: + - type: LiquidTag + name: content_for + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% content_for "blocks" %}' } + markupPosition: { start: [0, 15], text: '"blocks" ' } + position: { start: [0, 0], text: '{% content_for "blocks" %}' } + markup: + type: ContentForMarkup + position: { start: [0, 15], text: '"blocks" ' } + contentForType: + type: String + single: false + value: blocks + position: { start: [0, 15], text: '"blocks"' } + args: [] + - name: should parse content_for named expression arguments + source: "{% content_for \"snippet\", s: 'string', n: 10, r: (1..2), v: variable %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% content_for \"snippet\", s: 'string', n: 10, r: (1..2), v: variable %}" } + children: + - type: LiquidTag + name: content_for + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% content_for \"snippet\", s: 'string', n: 10, r: (1..2), v: variable %}" } + markupPosition: { start: [0, 15], text: "\"snippet\", s: 'string', n: 10, r: (1..2), v: variable " } + position: { start: [0, 0], text: "{% content_for \"snippet\", s: 'string', n: 10, r: (1..2), v: variable %}" } + markup: + type: ContentForMarkup + position: { start: [0, 15], text: "\"snippet\", s: 'string', n: 10, r: (1..2), v: variable " } + contentForType: + type: String + single: false + value: snippet + position: { start: [0, 15], text: '"snippet"' } + args: + - type: NamedArgument + name: s + position: { start: [0, 26], text: "s: 'string'" } + value: + type: String + single: true + value: string + position: { start: [0, 29], text: "'string'" } + - type: NamedArgument + name: n + position: { start: [0, 39], text: "n: 10" } + value: + type: Number + value: "10" + position: { start: [0, 42], text: "10" } + - type: NamedArgument + name: r + position: { start: [0, 46], text: "r: (1..2)" } + value: + type: Range + position: { start: [0, 49], text: (1..2) } + start: + type: Number + value: "1" + position: { start: [0, 50], text: "1" } + end: + type: Number + value: "2" + position: { start: [0, 53], text: "2" } + - type: NamedArgument + name: v + position: { start: [0, 57], text: "v: variable" } + value: + type: VariableLookup + name: variable + position: { start: [0, 60], text: variable } + lookups: [] + lookupModes: [] + - name: should parse content_for with dotted named argument keys + source: '{% content_for "block", closest.collection: collection %}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% content_for "block", closest.collection: collection %}' } + children: + - type: LiquidTag + name: content_for + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% content_for "block", closest.collection: collection %}' } + markupPosition: { start: [0, 15], text: '"block", closest.collection: collection ' } + position: { start: [0, 0], text: '{% content_for "block", closest.collection: collection %}' } + markup: + type: ContentForMarkup + position: { start: [0, 15], text: '"block", closest.collection: collection ' } + contentForType: + type: String + single: false + value: block + position: { start: [0, 15], text: '"block"' } + args: + - type: NamedArgument + name: closest.collection + position: { start: [0, 24], text: "closest.collection: collection" } + value: + type: VariableLookup + name: collection + position: { start: [0, 44], text: collection } + lookups: [] + lookupModes: [] + - name: should parse liquid inline comments (1) + source: "{% #%}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% #%}" } + children: + - type: LiquidTag + name: "#" + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% #%}" } + markupPosition: { start: [0, 4], text: "" } + position: { start: [0, 0], text: "{% #%}" } + - name: should parse liquid inline comments (2) + source: "{% #hello world %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% #hello world %}" } + children: + - type: LiquidTag + name: "#" + markup: hello world + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% #hello world %}" } + markupPosition: { start: [0, 4], text: "hello world " } + position: { start: [0, 0], text: "{% #hello world %}" } + - name: should parse liquid case as branches + source: '{% case A %}{% when A %}A{% when "B" %}B{% else %}C{% endcase %}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% case A %}{% when A %}A{% when "B" %}B{% else %}C{% endcase %}' } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case A %}" } + blockEndPosition: { start: [0, 54], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "A " } + position: { start: [0, 0], text: '{% case A %}{% when A %}A{% when "B" %}B{% else %}C{% endcase %}' } + markup: + type: VariableLookup + name: A + position: { start: [0, 8], text: A } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 12], text: "" } + blockStartPosition: { start: [0, 12], text: "" } + blockEndPosition: { start: [0, 12], text: "" } + position: { start: [0, 12], text: "" } + children: [] + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 20], text: "A " } + blockStartPosition: { start: [0, 12], text: "{% when A %}" } + blockEndPosition: { start: [0, 25], text: "" } + position: { start: [0, 12], text: "{% when A %}A" } + markup: + - type: VariableLookup + name: A + position: { start: [0, 20], text: A } + lookups: [] + lookupModes: [] + children: + - type: TextNode + value: A + position: { start: [0, 24], text: A } + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 33], text: '"B" ' } + blockStartPosition: { start: [0, 25], text: '{% when "B" %}' } + blockEndPosition: { start: [0, 40], text: "" } + position: { start: [0, 25], text: '{% when "B" %}B' } + markup: + - type: String + single: false + value: B + position: { start: [0, 33], text: '"B"' } + children: + - type: TextNode + value: B + position: { start: [0, 39], text: B } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 51], text: "" } + blockStartPosition: { start: [0, 40], text: "{% else %}" } + blockEndPosition: { start: [0, 54], text: "" } + position: { start: [0, 40], text: "{% else %}C" } + children: + - type: TextNode + value: C + position: { start: [0, 53], text: C } + - name: should parse liquid ifs as branches + source: "{% if a %}A{% elsif b %}B{% else %}C{% endif %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if a %}A{% elsif b %}B{% else %}C{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if a %}" } + blockEndPosition: { start: [0, 36], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "a " } + position: { start: [0, 0], text: "{% if a %}A{% elsif b %}B{% else %}C{% endif %}" } + markup: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 10], text: "" } + blockStartPosition: { start: [0, 10], text: "" } + blockEndPosition: { start: [0, 11], text: "" } + position: { start: [0, 10], text: A } + children: + - type: TextNode + value: A + position: { start: [0, 10], text: A } + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 20], text: "b " } + blockStartPosition: { start: [0, 11], text: "{% elsif b %}" } + blockEndPosition: { start: [0, 25], text: "" } + position: { start: [0, 11], text: "{% elsif b %}B" } + markup: + type: VariableLookup + name: b + position: { start: [0, 20], text: b } + lookups: [] + lookupModes: [] + children: + - type: TextNode + value: B + position: { start: [0, 24], text: B } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 33], text: "" } + blockStartPosition: { start: [0, 25], text: "{% else %}" } + blockEndPosition: { start: [0, 36], text: "" } + position: { start: [0, 25], text: "{% else %}C" } + children: + - type: TextNode + value: C + position: { start: [0, 35], text: C } + - name: should parse ifchanged as a block tag with no markup and children + source: "{% ifchanged %}hello{% endifchanged %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% ifchanged %}hello{% endifchanged %}" } + children: + - type: LiquidTag + name: ifchanged + markup: null + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% ifchanged %}" } + blockEndPosition: { start: [0, 20], text: "{% endifchanged %}" } + markupPosition: { start: [0, 13], text: "" } + position: { start: [0, 0], text: "{% ifchanged %}hello{% endifchanged %}" } + children: + - type: TextNode + value: hello + position: { start: [0, 15], text: hello } + - name: should parse partial as a block tag with string markup + source: "{% partial 'header' %}content{% endpartial %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% partial 'header' %}content{% endpartial %}" } + children: + - type: LiquidTag + name: partial + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% partial 'header' %}" } + blockEndPosition: { start: [0, 29], text: "{% endpartial %}" } + markupPosition: { start: [0, 11], text: "'header' " } + position: { start: [0, 0], text: "{% partial 'header' %}content{% endpartial %}" } + markup: + type: String + single: true + value: header + position: { start: [0, 11], text: "'header'" } + children: + - type: TextNode + value: content + position: { start: [0, 22], text: content } + - name: should parse block as a block tag with name and children + source: "{% block 'foo' %}content{% endblock %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% block 'foo' %}content{% endblock %}" } + children: + - type: LiquidTag + name: block + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% block 'foo' %}" } + blockEndPosition: { start: [0, 24], text: "{% endblock %}" } + markupPosition: { start: [0, 9], text: "'foo' " } + position: { start: [0, 0], text: "{% block 'foo' %}content{% endblock %}" } + markup: + type: BlockMarkup + position: { start: [0, 9], text: "'foo' " } + name: + type: String + single: true + value: foo + position: { start: [0, 9], text: "'foo'" } + args: [] + children: + - type: TextNode + value: content + position: { start: [0, 17], text: content } + - name: should include trailing whitespace in block markup position (Bug 29 fix) (1) + source: "{% block 'name' %}{% endblock %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% block 'name' %}{% endblock %}" } + children: + - type: LiquidTag + name: block + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% block 'name' %}" } + blockEndPosition: { start: [0, 18], text: "{% endblock %}" } + markupPosition: { start: [0, 9], text: "'name' " } + position: { start: [0, 0], text: "{% block 'name' %}{% endblock %}" } + markup: + type: BlockMarkup + position: { start: [0, 9], text: "'name' " } + name: + type: String + single: true + value: name + position: { start: [0, 9], text: "'name'" } + args: [] + children: [] + - name: should include trailing whitespace in block markup position (Bug 29 fix) (2) + source: "{% block 'name', key: 'val' %}{% endblock %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% block 'name', key: 'val' %}{% endblock %}" } + children: + - type: LiquidTag + name: block + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% block 'name', key: 'val' %}" } + blockEndPosition: { start: [0, 30], text: "{% endblock %}" } + markupPosition: { start: [0, 9], text: "'name', key: 'val' " } + position: { start: [0, 0], text: "{% block 'name', key: 'val' %}{% endblock %}" } + markup: + type: BlockMarkup + position: { start: [0, 9], text: "'name', key: 'val' " } + name: + type: String + single: true + value: name + position: { start: [0, 9], text: "'name'" } + args: + - type: NamedArgument + name: key + position: { start: [0, 17], text: "key: 'val'" } + value: + type: String + single: true + value: val + position: { start: [0, 22], text: "'val'" } + children: [] + - name: should parse block with kwargs + source: "{% block 'foo', key: 'val' %}content{% endblock %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% block 'foo', key: 'val' %}content{% endblock %}" } + children: + - type: LiquidTag + name: block + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% block 'foo', key: 'val' %}" } + blockEndPosition: { start: [0, 36], text: "{% endblock %}" } + markupPosition: { start: [0, 9], text: "'foo', key: 'val' " } + position: { start: [0, 0], text: "{% block 'foo', key: 'val' %}content{% endblock %}" } + markup: + type: BlockMarkup + position: { start: [0, 9], text: "'foo', key: 'val' " } + name: + type: String + single: true + value: foo + position: { start: [0, 9], text: "'foo'" } + args: + - type: NamedArgument + name: key + position: { start: [0, 16], text: "key: 'val'" } + value: + type: String + single: true + value: val + position: { start: [0, 21], text: "'val'" } + children: + - type: TextNode + value: content + position: { start: [0, 29], text: content } + - name: should parse section self-closing (existing behavior preserved) + source: "{% section 'foo' %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% section 'foo' %}" } + children: + - type: LiquidTag + name: section + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% section 'foo' %}" } + markupPosition: { start: [0, 11], text: "'foo' " } + position: { start: [0, 0], text: "{% section 'foo' %}" } + markup: + type: SectionMarkup + position: { start: [0, 11], text: "'foo' " } + name: + type: String + single: true + value: foo + position: { start: [0, 11], text: "'foo'" } + args: [] + - name: should parse section self-closing with kwargs + source: "{% section 'foo', key: 'val' %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% section 'foo', key: 'val' %}" } + children: + - type: LiquidTag + name: section + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% section 'foo', key: 'val' %}" } + markupPosition: { start: [0, 11], text: "'foo', key: 'val' " } + position: { start: [0, 0], text: "{% section 'foo', key: 'val' %}" } + markup: + type: SectionMarkup + position: { start: [0, 11], text: "'foo', key: 'val' " } + name: + type: String + single: true + value: foo + position: { start: [0, 11], text: "'foo'" } + args: + - type: NamedArgument + name: key + position: { start: [0, 18], text: "key: 'val'" } + value: + type: String + single: true + value: val + position: { start: [0, 23], text: "'val'" } + - name: should treat section as standalone and reject stray endsection tags (1) + source: "{% section 'foo' %}content{% endsection %}" + parse: [html, liquid] + error: + message: Attempting to close LiquidTag 'section' before it was opened without a matching 'section' + start: [0, 26] + text: "{% endsection %" + - name: should treat section as standalone and reject stray endsection tags (2) + source: "{% endsection %}" + parse: [html, liquid] + error: + message: Attempting to close LiquidTag 'section' before it was opened without a matching 'section' + start: [0, 0] + text: "{% endsection %" + - name: should treat section as standalone and reject stray endsection tags (3) + source: "{% if a %}{% endsection %}{% endif %}" + parse: [html, liquid] + error: + message: Attempting to close LiquidTag 'section' before LiquidTag 'if' was closed + start: [0, 0] + text: "{% if a %}" + - name: should still throw on stray end tags for other registered tags (1) + source: "{% endrender %}" + parse: [html, liquid] + error: + message: Attempting to close LiquidTag 'render' before it was opened without a matching 'render' + start: [0, 0] + text: "{% endrender %" + - name: should still throw on stray end tags for other registered tags (2) + source: "{% endsections %}" + parse: [html, liquid] + error: + message: Attempting to close LiquidTag 'sections' before it was opened without a matching 'sections' + start: [0, 0] + text: "{% endsections %" + - name: should still throw on stray end tags for other registered tags (3) + source: "{% if a %}{% endrender %}{% endif %}" + parse: [html, liquid] + error: + message: Attempting to close LiquidTag 'render' before LiquidTag 'if' was closed + start: [0, 0] + text: "{% if a %}" + - name: should correctly report the position of branches (1) + source: "{% if a %}
    A
    {% elsif b %} B {% else %} {% if C %}{% endif %} {% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if a %}
    A
    {% elsif b %} B {% else %} {% if C %}{% endif %} {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if a %}" } + blockEndPosition: { start: [0, 86], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "a " } + position: { start: [0, 0], text: "{% if a %}
    A
    {% elsif b %} B {% else %} {% if C %}{% endif %} {% endif %}" } + markup: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 10], text: "" } + blockStartPosition: { start: [0, 10], text: "" } + blockEndPosition: { start: [0, 24], text: "" } + position: { start: [0, 10], text: "
    A
    " } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 11], text:
    } + blockEndPosition: { start: [0, 17], text:
    } + position: { start: [0, 11], text:
    A
    } + name: + - type: TextNode + value: div + position: { start: [0, 12], text: div } + attributes: [] + children: + - type: TextNode + value: A + position: { start: [0, 16], text: A } + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 33], text: "b " } + blockStartPosition: { start: [0, 24], text: "{% elsif b %}" } + blockEndPosition: { start: [0, 53], text: "" } + position: { start: [0, 24], text: "{% elsif b %} B " } + markup: + type: VariableLookup + name: b + position: { start: [0, 33], text: b } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 38], text: } + blockEndPosition: { start: [0, 45], text: } + position: { start: [0, 38], text: B } + name: + - type: TextNode + value: span + position: { start: [0, 39], text: span } + attributes: [] + children: + - type: TextNode + value: B + position: { start: [0, 44], text: B } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 61], text: "" } + blockStartPosition: { start: [0, 53], text: "{% else %}" } + blockEndPosition: { start: [0, 86], text: "" } + position: { start: [0, 53], text: "{% else %} {% if C %}{% endif %} " } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 64], text: "{% if C %}" } + blockEndPosition: { start: [0, 74], text: "{% endif %}" } + markupPosition: { start: [0, 70], text: "C " } + position: { start: [0, 64], text: "{% if C %}{% endif %}" } + markup: + type: VariableLookup + name: C + position: { start: [0, 70], text: C } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 74], text: "" } + blockStartPosition: { start: [0, 74], text: "" } + blockEndPosition: { start: [0, 74], text: "" } + position: { start: [0, 74], text: "" } + children: [] + - name: should correctly report the position of branches (2) + source: "{% if a %}
    A
    {% elsif b %} B {% else %} {% if C %}{% endif %} {% endif %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if a %}
    A
    {% elsif b %} B {% else %} {% if C %}{% endif %} {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if a %}" } + blockEndPosition: { start: [0, 86], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "a " } + position: { start: [0, 0], text: "{% if a %}
    A
    {% elsif b %} B {% else %} {% if C %}{% endif %} {% endif %}" } + markup: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 10], text: "" } + blockStartPosition: { start: [0, 10], text: "" } + blockEndPosition: { start: [0, 24], text: "" } + position: { start: [0, 10], text: "
    A
    " } + children: + - type: TextNode + value:
    A
    + position: { start: [0, 11], text:
    A
    } + - type: LiquidBranch + name: elsif + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 33], text: "b " } + blockStartPosition: { start: [0, 24], text: "{% elsif b %}" } + blockEndPosition: { start: [0, 53], text: "" } + position: { start: [0, 24], text: "{% elsif b %} B " } + markup: + type: VariableLookup + name: b + position: { start: [0, 33], text: b } + lookups: [] + lookupModes: [] + children: + - type: TextNode + value: B + position: { start: [0, 38], text: B } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 61], text: "" } + blockStartPosition: { start: [0, 53], text: "{% else %}" } + blockEndPosition: { start: [0, 86], text: "" } + position: { start: [0, 53], text: "{% else %} {% if C %}{% endif %} " } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 64], text: "{% if C %}" } + blockEndPosition: { start: [0, 74], text: "{% endif %}" } + markupPosition: { start: [0, 70], text: "C " } + position: { start: [0, 64], text: "{% if C %}{% endif %}" } + markup: + type: VariableLookup + name: C + position: { start: [0, 70], text: C } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 74], text: "" } + blockStartPosition: { start: [0, 74], text: "" } + blockEndPosition: { start: [0, 74], text: "" } + position: { start: [0, 74], text: "" } + children: [] + - name: should parse unknown tag as base case + source: "{% unknown_tag %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% unknown_tag %}" } + children: + - type: LiquidTag + name: unknown_tag + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% unknown_tag %}" } + markupPosition: { start: [0, 15], text: "" } + position: { start: [0, 0], text: "{% unknown_tag %}" } + - name: should parse unknown tag with markup + source: "{% unknown_tag foo bar %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% unknown_tag foo bar %}" } + children: + - type: LiquidTag + name: unknown_tag + markup: foo bar + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% unknown_tag foo bar %}" } + markupPosition: { start: [0, 15], text: "foo bar " } + position: { start: [0, 0], text: "{% unknown_tag foo bar %}" } + - name: "should expose inline comment raw markup for {% # hello %}" + source: "{% # hello %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% # hello %}" } + children: + - type: LiquidTag + name: "#" + markup: hello + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% # hello %}" } + markupPosition: { start: [0, 4], text: " hello " } + position: { start: [0, 0], text: "{% # hello %}" } + - name: "should expose inline comment raw markup for {% #ok foo %}" + source: |- + {% #ok + foo %} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% #ok + foo %} + children: + - type: LiquidTag + name: "#" + markup: |- + ok + foo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% #ok + foo %} + markupPosition: + start: [0, 4] + text: |- + ok + foo + position: + start: [0, 0] + text: |- + {% #ok + foo %} + - name: should parse known standalone echo + source: '{% echo "hi" %}' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% echo "hi" %}' } + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% echo "hi" %}' } + markupPosition: { start: [0, 8], text: '"hi" ' } + position: { start: [0, 0], text: '{% echo "hi" %}' } + markup: + type: LiquidVariable + rawSource: '"hi"' + position: { start: [0, 8], text: '"hi" ' } + expression: + type: String + single: false + value: hi + position: { start: [0, 8], text: '"hi"' } + filters: [] + - name: should parse known standalone assign + source: '{% assign x = "hello" %}' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% assign x = "hello" %}' } + children: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% assign x = "hello" %}' } + markupPosition: { start: [0, 10], text: 'x = "hello" ' } + position: { start: [0, 0], text: '{% assign x = "hello" %}' } + markup: + type: AssignMarkup + name: x + position: { start: [0, 10], text: 'x = "hello" ' } + value: + type: LiquidVariable + rawSource: '"hello"' + position: { start: [0, 14], text: '"hello" ' } + expression: + type: String + single: false + value: hello + position: { start: [0, 14], text: '"hello"' } + filters: [] + - name: should parse known standalone render + source: '{% render "snippet" %}' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% render "snippet" %}' } + children: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% render "snippet" %}' } + markupPosition: { start: [0, 10], text: '"snippet" ' } + position: { start: [0, 0], text: '{% render "snippet" %}' } + markup: + type: RenderMarkup + variable: null + alias: null + position: { start: [0, 10], text: '"snippet" ' } + snippet: + type: String + single: false + value: snippet + position: { start: [0, 10], text: '"snippet"' } + args: [] + - name: should fall back to base case for invalid markup (tolerant) + source: "{% assign !bad %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% assign !bad %}" } + children: + - type: LiquidTag + name: assign + markup: "!bad" + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% assign !bad %}" } + markupPosition: { start: [0, 10], text: "!bad " } + position: { start: [0, 0], text: "{% assign !bad %}" } + reason: Expected Equality but found EndOfString () + - name: should handle whitespace stripping + source: '{%- echo "hi" -%}' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{%- echo "hi" -%}' } + children: + - type: LiquidTag + name: echo + whitespaceStart: "-" + whitespaceEnd: "-" + blockStartPosition: { start: [0, 0], text: '{%- echo "hi" -%}' } + markupPosition: { start: [0, 9], text: '"hi" ' } + position: { start: [0, 0], text: '{%- echo "hi" -%}' } + markup: + type: LiquidVariable + rawSource: '"hi"' + position: { start: [0, 9], text: '"hi" ' } + expression: + type: String + single: false + value: hi + position: { start: [0, 9], text: '"hi"' } + filters: [] + - name: should have correct position for tag node + source: before{% echo "x" %}after + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: 'before{% echo "x" %}after' } + children: + - type: TextNode + value: before + position: { start: [0, 0], text: before } + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 6], text: '{% echo "x" %}' } + markupPosition: { start: [0, 14], text: '"x" ' } + position: { start: [0, 6], text: '{% echo "x" %}' } + markup: + type: LiquidVariable + rawSource: '"x"' + position: { start: [0, 14], text: '"x" ' } + expression: + type: String + single: false + value: x + position: { start: [0, 14], text: '"x"' } + filters: [] + - type: TextNode + value: after + position: { start: [0, 20], text: after } + - name: should produce text before and after a tag + source: hello {% echo "x" %} world + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: 'hello {% echo "x" %} world' } + children: + - type: TextNode + value: hello + position: { start: [0, 0], text: hello } + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 6], text: '{% echo "x" %}' } + markupPosition: { start: [0, 14], text: '"x" ' } + position: { start: [0, 6], text: '{% echo "x" %}' } + markup: + type: LiquidVariable + rawSource: '"x"' + position: { start: [0, 14], text: '"x" ' } + expression: + type: String + single: false + value: x + position: { start: [0, 14], text: '"x"' } + filters: [] + - type: TextNode + value: world + position: { start: [0, 21], text: world } + - name: should parse multiple tags + source: '{% echo "a" %}{% assign x = "b" %}' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% echo "a" %}{% assign x = "b" %}' } + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% echo "a" %}' } + markupPosition: { start: [0, 8], text: '"a" ' } + position: { start: [0, 0], text: '{% echo "a" %}' } + markup: + type: LiquidVariable + rawSource: '"a"' + position: { start: [0, 8], text: '"a" ' } + expression: + type: String + single: false + value: a + position: { start: [0, 8], text: '"a"' } + filters: [] + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 14], text: '{% assign x = "b" %}' } + markupPosition: { start: [0, 24], text: 'x = "b" ' } + position: { start: [0, 14], text: '{% assign x = "b" %}' } + markup: + type: AssignMarkup + name: x + position: { start: [0, 24], text: 'x = "b" ' } + value: + type: LiquidVariable + rawSource: '"b"' + position: { start: [0, 28], text: '"b" ' } + expression: + type: String + single: false + value: b + position: { start: [0, 28], text: '"b"' } + filters: [] + - name: should work with toLiquidAST + source: '{% echo "x" %}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% echo "x" %}' } + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: '{% echo "x" %}' } + markupPosition: { start: [0, 8], text: '"x" ' } + position: { start: [0, 0], text: '{% echo "x" %}' } + markup: + type: LiquidVariable + rawSource: '"x"' + position: { start: [0, 8], text: '"x" ' } + expression: + type: String + single: false + value: x + position: { start: [0, 8], text: '"x"' } + filters: [] diff --git a/specs/parser/liquid-variable-output.yml b/specs/parser/liquid-variable-output.yml new file mode 100644 index 000000000..a0062d60a --- /dev/null +++ b/specs/parser/liquid-variable-output.yml @@ -0,0 +1,2838 @@ +cases: + - name: should transform a base case Liquid Drop into a LiquidVariableOutput + source: "{{ !-asd }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ !-asd }}" } + children: + - type: LiquidVariableOutput + markup: "!-asd" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " !-asd " } + position: { start: [0, 0], text: "{{ !-asd }}" } + - name: should set LiquidVariable end position to the markup boundary (Bug 7 regression) (1) + source: "{{ product.title }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ product.title }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " product.title " } + position: { start: [0, 0], text: "{{ product.title }}" } + markup: + type: LiquidVariable + rawSource: product.title + position: { start: [0, 3], text: "product.title " } + expression: + type: VariableLookup + name: product + position: { start: [0, 3], text: product.title } + lookups: + - type: String + value: title + position: { start: [0, 11], text: title } + lookupModes: [bareword] + filters: [] + - name: should set LiquidVariable end position to the markup boundary (Bug 7 regression) (2) + source: "{{ price | money }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ price | money }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " price | money " } + position: { start: [0, 0], text: "{{ price | money }}" } + markup: + type: LiquidVariable + rawSource: price | money + position: { start: [0, 3], text: "price | money " } + expression: + type: VariableLookup + name: price + position: { start: [0, 3], text: price } + lookups: [] + lookupModes: [] + filters: + - type: LiquidFilter + name: money + position: { start: [0, 8], text: " | money" } + args: [] + - name: should set rawSource for LiquidVariable inside {% liquid %} blocks (Bug 21 fix) (1) + source: |- + {% liquid + echo product.title + %} + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + echo product.title + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + echo product.title + %} + markupPosition: + start: [0, 9] + text: |2 + + echo product.title + position: + start: [0, 0] + text: |- + {% liquid + echo product.title + %} + markup: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 2], text: echo product.title } + markupPosition: { start: [1, 7], text: product.title } + position: { start: [1, 2], text: echo product.title } + markup: + type: LiquidVariable + rawSource: product.title + position: { start: [1, 7], text: product.title } + expression: + type: VariableLookup + name: product + position: { start: [1, 7], text: product.title } + lookups: + - type: String + value: title + position: { start: [1, 15], text: title } + lookupModes: [bareword] + filters: [] + - name: should set rawSource for LiquidVariable inside {% liquid %} blocks (Bug 21 fix) (2) + source: |- + {% liquid + echo product.title | upcase + %} + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + echo product.title | upcase + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + echo product.title | upcase + %} + markupPosition: + start: [0, 9] + text: |2 + + echo product.title | upcase + position: + start: [0, 0] + text: |- + {% liquid + echo product.title | upcase + %} + markup: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 2], text: echo product.title | upcase } + markupPosition: { start: [1, 7], text: product.title | upcase } + position: { start: [1, 2], text: echo product.title | upcase } + markup: + type: LiquidVariable + rawSource: product.title | upcase + position: { start: [1, 7], text: product.title | upcase } + expression: + type: VariableLookup + name: product + position: { start: [1, 7], text: product.title } + lookups: + - type: String + value: title + position: { start: [1, 15], text: title } + lookupModes: [bareword] + filters: + - type: LiquidFilter + name: upcase + position: { start: [1, 20], text: " | upcase" } + args: [] + - name: should set rawSource for LiquidVariable inside {% liquid %} blocks (Bug 21 fix) (3) + source: "{% echo product.title %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% echo product.title %}" } + children: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% echo product.title %}" } + markupPosition: { start: [0, 8], text: "product.title " } + position: { start: [0, 0], text: "{% echo product.title %}" } + markup: + type: LiquidVariable + rawSource: product.title + position: { start: [0, 8], text: "product.title " } + expression: + type: VariableLookup + name: product + position: { start: [0, 8], text: product.title } + lookups: + - type: String + value: title + position: { start: [0, 16], text: title } + lookupModes: [bareword] + filters: [] + - name: should populate rawSource for LiquidVariable inside raw tag bodies (Bug 28) (1) + source: "{% style %}{{ product.title }}{% endstyle %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% style %}{{ product.title }}{% endstyle %}" } + children: + - type: LiquidRawTag + name: style + markup: "" + markupPosition: { start: [0, 9], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% style %}" } + blockEndPosition: { start: [0, 30], text: "{% endstyle %}" } + position: { start: [0, 0], text: "{% style %}{{ product.title }}{% endstyle %}" } + body: + type: RawMarkup + kind: text + value: "{{ product.title }}" + position: { start: [0, 11], text: "{{ product.title }}" } + nodes: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: " product.title " } + position: { start: [0, 11], text: "{{ product.title }}" } + markup: + type: LiquidVariable + rawSource: product.title + position: { start: [0, 14], text: "product.title " } + expression: + type: VariableLookup + name: product + position: { start: [0, 14], text: product.title } + lookups: + - type: String + value: title + position: { start: [0, 22], text: title } + lookupModes: [bareword] + filters: [] + - name: should populate rawSource for LiquidVariable inside raw tag bodies (Bug 28) (2) + source: "{% javascript %}{{ settings.color }}{% endjavascript %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% javascript %}{{ settings.color }}{% endjavascript %}" } + children: + - type: LiquidRawTag + name: javascript + markup: "" + markupPosition: { start: [0, 14], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% javascript %}" } + blockEndPosition: { start: [0, 36], text: "{% endjavascript %}" } + position: { start: [0, 0], text: "{% javascript %}{{ settings.color }}{% endjavascript %}" } + body: + type: RawMarkup + kind: javascript + value: "{{ settings.color }}" + position: { start: [0, 16], text: "{{ settings.color }}" } + nodes: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 18], text: " settings.color " } + position: { start: [0, 16], text: "{{ settings.color }}" } + markup: + type: LiquidVariable + rawSource: settings.color + position: { start: [0, 19], text: "settings.color " } + expression: + type: VariableLookup + name: settings + position: { start: [0, 19], text: settings.color } + lookups: + - type: String + value: color + position: { start: [0, 28], text: color } + lookupModes: [bareword] + filters: [] + - name: should populate rawSource for LiquidVariable inside raw tag bodies (Bug 28) (3) + source: "{% style %}{% echo product.title %}{% endstyle %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% style %}{% echo product.title %}{% endstyle %}" } + children: + - type: LiquidRawTag + name: style + markup: "" + markupPosition: { start: [0, 9], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% style %}" } + blockEndPosition: { start: [0, 35], text: "{% endstyle %}" } + position: { start: [0, 0], text: "{% style %}{% echo product.title %}{% endstyle %}" } + body: + type: RawMarkup + kind: text + value: "{% echo product.title %}" + position: { start: [0, 11], text: "{% echo product.title %}" } + nodes: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 11], text: "{% echo product.title %}" } + markupPosition: { start: [0, 19], text: "product.title " } + position: { start: [0, 11], text: "{% echo product.title %}" } + markup: + type: LiquidVariable + rawSource: product.title + position: { start: [0, 19], text: "product.title " } + expression: + type: VariableLookup + name: product + position: { start: [0, 19], text: product.title } + lookups: + - type: String + value: title + position: { start: [0, 27], text: title } + lookupModes: [bareword] + filters: [] + - name: should populate rawSource for LiquidVariable inside raw tag bodies (Bug 28) (4) + source: "{% style %}{% assign x = product.title %}{% endstyle %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% style %}{% assign x = product.title %}{% endstyle %}" } + children: + - type: LiquidRawTag + name: style + markup: "" + markupPosition: { start: [0, 9], text: "" } + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% style %}" } + blockEndPosition: { start: [0, 41], text: "{% endstyle %}" } + position: { start: [0, 0], text: "{% style %}{% assign x = product.title %}{% endstyle %}" } + body: + type: RawMarkup + kind: text + value: "{% assign x = product.title %}" + position: { start: [0, 11], text: "{% assign x = product.title %}" } + nodes: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [0, 11], text: "{% assign x = product.title %}" } + markupPosition: { start: [0, 21], text: "x = product.title " } + position: { start: [0, 11], text: "{% assign x = product.title %}" } + markup: + type: AssignMarkup + name: x + position: { start: [0, 21], text: "x = product.title " } + value: + type: LiquidVariable + rawSource: product.title + position: { start: [0, 25], text: "product.title " } + expression: + type: VariableLookup + name: product + position: { start: [0, 25], text: product.title } + lookups: + - type: String + value: title + position: { start: [0, 33], text: title } + lookupModes: [bareword] + filters: [] + - name: should populate rawSource for {{ }} drops inside HTML + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlRawNode + name: style + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: "" } + attributes: [] + body: + type: RawMarkup + kind: text + value: "{{ section.id }}" + position: { start: [0, 7], text: "{{ section.id }}" } + nodes: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 9], text: " section.id " } + position: { start: [0, 7], text: "{{ section.id }}" } + markup: + type: LiquidVariable + rawSource: section.id + position: { start: [0, 10], text: "section.id " } + expression: + type: VariableLookup + name: section + position: { start: [0, 10], text: section.id } + lookups: + - type: String + value: id + position: { start: [0, 18], text: id } + lookupModes: [bareword] + filters: [] + - name: should populate rawSource for {{ }} drops inside HTML " + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "" } + children: + - type: HtmlRawNode + name: style + blockStartPosition: { start: [0, 0], text: } + position: { start: [0, 0], text: "" } + attributes: [] + body: + type: RawMarkup + kind: text + value: "{{ settings.type_body_font | font_face: font_display: 'swap' }}" + position: { start: [0, 7], text: "{{ settings.type_body_font | font_face: font_display: 'swap' }}" } + nodes: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 9], text: " settings.type_body_font | font_face: font_display: 'swap' " } + position: { start: [0, 7], text: "{{ settings.type_body_font | font_face: font_display: 'swap' }}" } + markup: + type: LiquidVariable + rawSource: "settings.type_body_font | font_face: font_display: 'swap'" + position: { start: [0, 10], text: "settings.type_body_font | font_face: font_display: 'swap' " } + expression: + type: VariableLookup + name: settings + position: { start: [0, 10], text: settings.type_body_font } + lookups: + - type: String + value: type_body_font + position: { start: [0, 19], text: type_body_font } + lookupModes: [bareword] + filters: + - type: LiquidFilter + name: font_face + position: { start: [0, 33], text: " | font_face: font_display: 'swap'" } + args: + - type: NamedArgument + name: font_display + position: { start: [0, 47], text: "font_display: 'swap'" } + value: + type: String + single: true + value: swap + position: { start: [0, 61], text: "'swap'" } + - name: should not truncate rawSource with filters inside {% liquid %} blocks (Bug 21 regression) (1) + source: |- + {% liquid + echo 'home.hero.title' | t + %} + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + echo 'home.hero.title' | t + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + echo 'home.hero.title' | t + %} + markupPosition: + start: [0, 9] + text: |2 + + echo 'home.hero.title' | t + position: + start: [0, 0] + text: |- + {% liquid + echo 'home.hero.title' | t + %} + markup: + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 2], text: echo 'home.hero.title' | t } + markupPosition: { start: [1, 7], text: "'home.hero.title' | t" } + position: { start: [1, 2], text: echo 'home.hero.title' | t } + markup: + type: LiquidVariable + rawSource: "'home.hero.title' | t" + position: { start: [1, 7], text: "'home.hero.title' | t" } + expression: + type: String + single: true + value: home.hero.title + position: { start: [1, 7], text: "'home.hero.title'" } + filters: + - type: LiquidFilter + name: t + position: { start: [1, 24], text: " | t" } + args: [] + - name: should not truncate rawSource with filters inside {% liquid %} blocks (Bug 21 regression) (2) + source: |- + {% liquid + assign x = 'foo' + echo x | upcase | append: '.bar' + %} + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + assign x = 'foo' + echo x | upcase | append: '.bar' + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + assign x = 'foo' + echo x | upcase | append: '.bar' + %} + markupPosition: + start: [0, 9] + text: |2 + + assign x = 'foo' + echo x | upcase | append: '.bar' + position: + start: [0, 0] + text: |- + {% liquid + assign x = 'foo' + echo x | upcase | append: '.bar' + %} + markup: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 2], text: assign x = 'foo' } + markupPosition: { start: [1, 9], text: x = 'foo' } + position: { start: [1, 2], text: assign x = 'foo' } + markup: + type: AssignMarkup + name: x + position: { start: [1, 9], text: x = 'foo' } + value: + type: LiquidVariable + rawSource: "'foo'" + position: { start: [1, 13], text: "'foo'" } + expression: + type: String + single: true + value: foo + position: { start: [1, 13], text: "'foo'" } + filters: [] + - type: LiquidTag + name: echo + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 2], text: "echo x | upcase | append: '.bar'" } + markupPosition: { start: [2, 7], text: "x | upcase | append: '.bar'" } + position: { start: [2, 2], text: "echo x | upcase | append: '.bar'" } + markup: + type: LiquidVariable + rawSource: "x | upcase | append: '.bar'" + position: { start: [2, 7], text: "x | upcase | append: '.bar'" } + expression: + type: VariableLookup + name: x + position: { start: [2, 7], text: x } + lookups: [] + lookupModes: [] + filters: + - type: LiquidFilter + name: upcase + position: { start: [2, 8], text: " | upcase" } + args: [] + - type: LiquidFilter + name: append + position: { start: [2, 17], text: " | append: '.bar'" } + args: + - type: String + single: true + value: .bar + position: { start: [2, 28], text: "'.bar'" } + - name: should parse multiline render inside {% liquid %} as a single statement (Bug 44) + source: |- + {% liquid + render 'foo', + bar: baz, + qux: quux + %} + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {% liquid + render 'foo', + bar: baz, + qux: quux + %} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [0, 0] + text: |- + {% liquid + render 'foo', + bar: baz, + qux: quux + %} + markupPosition: + start: [0, 9] + text: |2 + + render 'foo', + bar: baz, + qux: quux + position: + start: [0, 0] + text: |- + {% liquid + render 'foo', + bar: baz, + qux: quux + %} + markup: + - type: LiquidTag + name: render + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: + start: [1, 2] + text: |- + render 'foo', + bar: baz, + qux: quux + markupPosition: + start: [1, 9] + text: |- + 'foo', + bar: baz, + qux: quux + position: + start: [1, 2] + text: |- + render 'foo', + bar: baz, + qux: quux + markup: + type: RenderMarkup + variable: null + alias: null + position: + start: [1, 9] + text: |- + 'foo', + bar: baz, + qux: quux + snippet: + type: String + single: true + value: foo + position: { start: [1, 9], text: "'foo'" } + args: + - type: NamedArgument + name: bar + position: { start: [2, 4], text: "bar: baz" } + value: + type: VariableLookup + name: baz + position: { start: [2, 9], text: baz } + lookups: [] + lookupModes: [] + - type: NamedArgument + name: qux + position: { start: [3, 4], text: "qux: quux" } + value: + type: VariableLookup + name: quux + position: { start: [3, 9], text: quux } + lookups: [] + lookupModes: [] + - name: should set LogicalExpression position.end to the markup boundary (Bug 9 regression) (1) + source: "{% if a and b %}{% endif %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if a and b %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if a and b %}" } + blockEndPosition: { start: [0, 16], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "a and b " } + position: { start: [0, 0], text: "{% if a and b %}{% endif %}" } + markup: + type: LogicalExpression + relation: and + position: { start: [0, 6], text: "a and b " } + left: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + right: + type: VariableLookup + name: b + position: { start: [0, 12], text: b } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 16], text: "" } + blockStartPosition: { start: [0, 16], text: "" } + blockEndPosition: { start: [0, 16], text: "" } + position: { start: [0, 16], text: "" } + children: [] + - name: should set LogicalExpression position.end to the markup boundary (Bug 9 regression) (2) + source: "{% if x == 1 %}{% endif %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if x == 1 %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if x == 1 %}" } + blockEndPosition: { start: [0, 15], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "x == 1 " } + position: { start: [0, 0], text: "{% if x == 1 %}{% endif %}" } + markup: + type: Comparison + comparator: == + position: { start: [0, 6], text: x == 1 } + left: + type: VariableLookup + name: x + position: { start: [0, 6], text: x } + lookups: [] + lookupModes: [] + right: + type: Number + value: "1" + position: { start: [0, 11], text: "1" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 15], text: "" } + blockStartPosition: { start: [0, 15], text: "" } + blockEndPosition: { start: [0, 15], text: "" } + position: { start: [0, 15], text: "" } + children: [] + - name: should set nested LogicalExpression right.position.start to include operator keyword (Bug 19 regression) + source: "{% if a == 1 and b == 2 or c == 3 %}{% endif %}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if a == 1 and b == 2 or c == 3 %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if a == 1 and b == 2 or c == 3 %}" } + blockEndPosition: { start: [0, 36], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "a == 1 and b == 2 or c == 3 " } + position: { start: [0, 0], text: "{% if a == 1 and b == 2 or c == 3 %}{% endif %}" } + markup: + type: LogicalExpression + relation: and + position: { start: [0, 6], text: "a == 1 and b == 2 or c == 3 " } + left: + type: Comparison + comparator: == + position: { start: [0, 6], text: a == 1 } + left: + type: VariableLookup + name: a + position: { start: [0, 6], text: a } + lookups: [] + lookupModes: [] + right: + type: Number + value: "1" + position: { start: [0, 11], text: "1" } + right: + type: LogicalExpression + relation: or + position: { start: [0, 13], text: "and b == 2 or c == 3 " } + left: + type: Comparison + comparator: == + position: { start: [0, 17], text: b == 2 } + left: + type: VariableLookup + name: b + position: { start: [0, 17], text: b } + lookups: [] + lookupModes: [] + right: + type: Number + value: "2" + position: { start: [0, 22], text: "2" } + right: + type: Comparison + comparator: == + position: { start: [0, 27], text: c == 3 } + left: + type: VariableLookup + name: c + position: { start: [0, 27], text: c } + lookups: [] + lookupModes: [] + right: + type: Number + value: "3" + position: { start: [0, 32], text: "3" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 36], text: "" } + blockStartPosition: { start: [0, 36], text: "" } + blockEndPosition: { start: [0, 36], text: "" } + position: { start: [0, 36], text: "" } + children: [] + - name: BooleanExpression > Comparison (1) + source: "{{ 1 == 1 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 == 1 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 == 1 " } + position: { start: [0, 0], text: "{{ 1 == 1 }}" } + markup: + type: LiquidVariable + rawSource: 1 == 1 + position: { start: [0, 3], text: "1 == 1 " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: 1 == 1 } + condition: + type: Comparison + comparator: == + position: { start: [0, 3], text: 1 == 1 } + left: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + right: + type: Number + value: "1" + position: { start: [0, 8], text: "1" } + filters: [] + - name: BooleanExpression > Comparison (2) + source: "{{ 1 != 1 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 != 1 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 != 1 " } + position: { start: [0, 0], text: "{{ 1 != 1 }}" } + markup: + type: LiquidVariable + rawSource: 1 != 1 + position: { start: [0, 3], text: "1 != 1 " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: 1 != 1 } + condition: + type: Comparison + comparator: "!=" + position: { start: [0, 3], text: 1 != 1 } + left: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + right: + type: Number + value: "1" + position: { start: [0, 8], text: "1" } + filters: [] + - name: BooleanExpression > Comparison (3) + source: "{{ 1 > 1 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 > 1 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 > 1 " } + position: { start: [0, 0], text: "{{ 1 > 1 }}" } + markup: + type: LiquidVariable + rawSource: 1 > 1 + position: { start: [0, 3], text: "1 > 1 " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: 1 > 1 } + condition: + type: Comparison + comparator: ">" + position: { start: [0, 3], text: 1 > 1 } + left: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + right: + type: Number + value: "1" + position: { start: [0, 7], text: "1" } + filters: [] + - name: BooleanExpression > Comparison (4) + source: "{{ 1 < 1 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 < 1 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 < 1 " } + position: { start: [0, 0], text: "{{ 1 < 1 }}" } + markup: + type: LiquidVariable + rawSource: 1 < 1 + position: { start: [0, 3], text: "1 < 1 " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: 1 < 1 } + condition: + type: Comparison + comparator: < + position: { start: [0, 3], text: 1 < 1 } + left: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + right: + type: Number + value: "1" + position: { start: [0, 7], text: "1" } + filters: [] + - name: BooleanExpression > Comparison (5) + source: "{{ 1 >= 1 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 >= 1 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 >= 1 " } + position: { start: [0, 0], text: "{{ 1 >= 1 }}" } + markup: + type: LiquidVariable + rawSource: 1 >= 1 + position: { start: [0, 3], text: "1 >= 1 " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: 1 >= 1 } + condition: + type: Comparison + comparator: ">=" + position: { start: [0, 3], text: 1 >= 1 } + left: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + right: + type: Number + value: "1" + position: { start: [0, 8], text: "1" } + filters: [] + - name: BooleanExpression > Comparison (6) + source: "{{ 1 <= 1 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 <= 1 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 <= 1 " } + position: { start: [0, 0], text: "{{ 1 <= 1 }}" } + markup: + type: LiquidVariable + rawSource: 1 <= 1 + position: { start: [0, 3], text: "1 <= 1 " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: 1 <= 1 } + condition: + type: Comparison + comparator: <= + position: { start: [0, 3], text: 1 <= 1 } + left: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + right: + type: Number + value: "1" + position: { start: [0, 8], text: "1" } + filters: [] + - name: BooleanExpression > LogicalExpression (1) + source: "{{ 1 == 1 and 2 == 2 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 == 1 and 2 == 2 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 == 1 and 2 == 2 " } + position: { start: [0, 0], text: "{{ 1 == 1 and 2 == 2 }}" } + markup: + type: LiquidVariable + rawSource: 1 == 1 and 2 == 2 + position: { start: [0, 3], text: "1 == 1 and 2 == 2 " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: 1 == 1 and 2 == 2 } + condition: + type: LogicalExpression + relation: and + position: { start: [0, 3], text: 1 == 1 and 2 == 2 } + left: + type: Comparison + comparator: == + position: { start: [0, 3], text: 1 == 1 } + left: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + right: + type: Number + value: "1" + position: { start: [0, 8], text: "1" } + right: + type: Comparison + comparator: == + position: { start: [0, 14], text: 2 == 2 } + left: + type: Number + value: "2" + position: { start: [0, 14], text: "2" } + right: + type: Number + value: "2" + position: { start: [0, 19], text: "2" } + filters: [] + - name: BooleanExpression > LogicalExpression (2) + source: "{{ 1 == 1 or 2 == 2 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 == 1 or 2 == 2 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 == 1 or 2 == 2 " } + position: { start: [0, 0], text: "{{ 1 == 1 or 2 == 2 }}" } + markup: + type: LiquidVariable + rawSource: 1 == 1 or 2 == 2 + position: { start: [0, 3], text: "1 == 1 or 2 == 2 " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: 1 == 1 or 2 == 2 } + condition: + type: LogicalExpression + relation: or + position: { start: [0, 3], text: 1 == 1 or 2 == 2 } + left: + type: Comparison + comparator: == + position: { start: [0, 3], text: 1 == 1 } + left: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + right: + type: Number + value: "1" + position: { start: [0, 8], text: "1" } + right: + type: Comparison + comparator: == + position: { start: [0, 13], text: 2 == 2 } + left: + type: Number + value: "2" + position: { start: [0, 13], text: "2" } + right: + type: Number + value: "2" + position: { start: [0, 18], text: "2" } + filters: [] + - name: BooleanExpression > LogicalExpression (3) + source: "{{ 1 == 1 and 2 == 2 or 3 == 3 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 == 1 and 2 == 2 or 3 == 3 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 == 1 and 2 == 2 or 3 == 3 " } + position: { start: [0, 0], text: "{{ 1 == 1 and 2 == 2 or 3 == 3 }}" } + markup: + type: LiquidVariable + rawSource: 1 == 1 and 2 == 2 or 3 == 3 + position: { start: [0, 3], text: "1 == 1 and 2 == 2 or 3 == 3 " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: 1 == 1 and 2 == 2 or 3 == 3 } + condition: + type: LogicalExpression + relation: and + position: { start: [0, 3], text: 1 == 1 and 2 == 2 or 3 == 3 } + left: + type: Comparison + comparator: == + position: { start: [0, 3], text: 1 == 1 } + left: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + right: + type: Number + value: "1" + position: { start: [0, 8], text: "1" } + right: + type: LogicalExpression + relation: or + position: { start: [0, 10], text: and 2 == 2 or 3 == 3 } + left: + type: Comparison + comparator: == + position: { start: [0, 14], text: 2 == 2 } + left: + type: Number + value: "2" + position: { start: [0, 14], text: "2" } + right: + type: Number + value: "2" + position: { start: [0, 19], text: "2" } + right: + type: Comparison + comparator: == + position: { start: [0, 24], text: 3 == 3 } + left: + type: Number + value: "3" + position: { start: [0, 24], text: "3" } + right: + type: Number + value: "3" + position: { start: [0, 29], text: "3" } + filters: [] + - name: BooleanExpression > LogicalExpression (4) + source: "{{ 1 == 1 and some_variable or 3 == 3 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 == 1 and some_variable or 3 == 3 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 == 1 and some_variable or 3 == 3 " } + position: { start: [0, 0], text: "{{ 1 == 1 and some_variable or 3 == 3 }}" } + markup: + type: LiquidVariable + rawSource: 1 == 1 and some_variable or 3 == 3 + position: { start: [0, 3], text: "1 == 1 and some_variable or 3 == 3 " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: 1 == 1 and some_variable or 3 == 3 } + condition: + type: LogicalExpression + relation: and + position: { start: [0, 3], text: 1 == 1 and some_variable or 3 == 3 } + left: + type: Comparison + comparator: == + position: { start: [0, 3], text: 1 == 1 } + left: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + right: + type: Number + value: "1" + position: { start: [0, 8], text: "1" } + right: + type: LogicalExpression + relation: or + position: { start: [0, 10], text: and some_variable or 3 == 3 } + left: + type: VariableLookup + name: some_variable + position: { start: [0, 14], text: some_variable } + lookups: [] + lookupModes: [] + right: + type: Comparison + comparator: == + position: { start: [0, 31], text: 3 == 3 } + left: + type: Number + value: "3" + position: { start: [0, 31], text: "3" } + right: + type: Number + value: "3" + position: { start: [0, 36], text: "3" } + filters: [] + - name: BooleanExpression > LogicalExpression (5) + source: "{{ some_var and 'raw string' }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ some_var and 'raw string' }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " some_var and 'raw string' " } + position: { start: [0, 0], text: "{{ some_var and 'raw string' }}" } + markup: + type: LiquidVariable + rawSource: some_var and 'raw string' + position: { start: [0, 3], text: "some_var and 'raw string' " } + expression: + type: BooleanExpression + position: { start: [0, 3], text: some_var and 'raw string' } + condition: + type: LogicalExpression + relation: and + position: { start: [0, 3], text: some_var and 'raw string' } + left: + type: VariableLookup + name: some_var + position: { start: [0, 3], text: some_var } + lookups: [] + lookupModes: [] + right: + type: String + single: true + value: raw string + position: { start: [0, 16], text: "'raw string'" } + filters: [] + - name: should parse strings as LiquidVariable > String (1) + source: "{{ \"string o' string\" }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ \"string o' string\" }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " \"string o' string\" " } + position: { start: [0, 0], text: "{{ \"string o' string\" }}" } + markup: + type: LiquidVariable + rawSource: "\"string o' string\"" + position: { start: [0, 3], text: "\"string o' string\" " } + expression: + type: String + single: false + value: string o' string + position: { start: [0, 3], text: "\"string o' string\"" } + filters: [] + - name: should parse strings as LiquidVariable > String (2) + source: "{{ 'He said: \"hi!\"' }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'He said: \"hi!\"' }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'He said: \"hi!\"' " } + position: { start: [0, 0], text: "{{ 'He said: \"hi!\"' }}" } + markup: + type: LiquidVariable + rawSource: "'He said: \"hi!\"'" + position: { start: [0, 3], text: "'He said: \"hi!\"' " } + expression: + type: String + single: true + value: 'He said: "hi!"' + position: { start: [0, 3], text: "'He said: \"hi!\"'" } + filters: [] + - name: should parse numbers as LiquidVariable > Number (1) + source: "{{ 1 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1 " } + position: { start: [0, 0], text: "{{ 1 }}" } + markup: + type: LiquidVariable + rawSource: "1" + position: { start: [0, 3], text: "1 " } + expression: + type: Number + value: "1" + position: { start: [0, 3], text: "1" } + filters: [] + - name: should parse numbers as LiquidVariable > Number (2) + source: "{{ 1.02 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 1.02 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 1.02 " } + position: { start: [0, 0], text: "{{ 1.02 }}" } + markup: + type: LiquidVariable + rawSource: "1.02" + position: { start: [0, 3], text: "1.02 " } + expression: + type: Number + value: "1.02" + position: { start: [0, 3], text: "1.02" } + filters: [] + - name: should parse numbers as LiquidVariable > Number (3) + source: "{{ 0 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 0 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 0 " } + position: { start: [0, 0], text: "{{ 0 }}" } + markup: + type: LiquidVariable + rawSource: "0" + position: { start: [0, 3], text: "0 " } + expression: + type: Number + value: "0" + position: { start: [0, 3], text: "0" } + filters: [] + - name: should parse numbers as LiquidVariable > Number (4) + source: "{{ -0 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ -0 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " -0 " } + position: { start: [0, 0], text: "{{ -0 }}" } + markup: + type: LiquidVariable + rawSource: "-0" + position: { start: [0, 3], text: "-0 " } + expression: + type: Number + value: "-0" + position: { start: [0, 3], text: "-0" } + filters: [] + - name: should parse numbers as LiquidVariable > Number (5) + source: "{{ -0.0 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ -0.0 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " -0.0 " } + position: { start: [0, 0], text: "{{ -0.0 }}" } + markup: + type: LiquidVariable + rawSource: "-0.0" + position: { start: [0, 3], text: "-0.0 " } + expression: + type: Number + value: "-0.0" + position: { start: [0, 3], text: "-0.0" } + filters: [] + - name: should parse numbers as LiquidVariable > LiquidLiteral (1) + source: "{{ nil }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ nil }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " nil " } + position: { start: [0, 0], text: "{{ nil }}" } + markup: + type: LiquidVariable + rawSource: nil + position: { start: [0, 3], text: "nil " } + expression: + type: LiquidLiteral + keyword: nil + value: null + position: { start: [0, 3], text: nil } + filters: [] + - name: should parse numbers as LiquidVariable > LiquidLiteral (2) + source: "{{ null }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ null }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " null " } + position: { start: [0, 0], text: "{{ null }}" } + markup: + type: LiquidVariable + rawSource: "null" + position: { start: [0, 3], text: "null " } + expression: + type: LiquidLiteral + keyword: "null" + value: null + position: { start: [0, 3], text: "null" } + filters: [] + - name: should parse numbers as LiquidVariable > LiquidLiteral (3) + source: "{{ true }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ true }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " true " } + position: { start: [0, 0], text: "{{ true }}" } + markup: + type: LiquidVariable + rawSource: "true" + position: { start: [0, 3], text: "true " } + expression: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 3], text: "true" } + filters: [] + - name: should parse numbers as LiquidVariable > LiquidLiteral (4) + source: "{{ blank }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ blank }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " blank " } + position: { start: [0, 0], text: "{{ blank }}" } + markup: + type: LiquidVariable + rawSource: blank + position: { start: [0, 3], text: "blank " } + expression: + type: LiquidLiteral + keyword: blank + value: "" + position: { start: [0, 3], text: blank } + filters: [] + - name: should parse numbers as LiquidVariable > LiquidLiteral (5) + source: "{{ empty }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ empty }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " empty " } + position: { start: [0, 0], text: "{{ empty }}" } + markup: + type: LiquidVariable + rawSource: empty + position: { start: [0, 3], text: "empty " } + expression: + type: LiquidLiteral + keyword: empty + value: "" + position: { start: [0, 3], text: empty } + filters: [] + - name: should parse ranges as LiquidVariable > Range (1) + source: "{{ (0..5) }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ (0..5) }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " (0..5) " } + position: { start: [0, 0], text: "{{ (0..5) }}" } + markup: + type: LiquidVariable + rawSource: (0..5) + position: { start: [0, 3], text: "(0..5) " } + expression: + type: Range + position: { start: [0, 3], text: (0..5) } + start: + type: Number + value: "0" + position: { start: [0, 4], text: "0" } + end: + type: Number + value: "5" + position: { start: [0, 7], text: "5" } + filters: [] + - name: should parse ranges as LiquidVariable > Range (2) + source: "{{ ( 0 .. 5 ) }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ ( 0 .. 5 ) }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " ( 0 .. 5 ) " } + position: { start: [0, 0], text: "{{ ( 0 .. 5 ) }}" } + markup: + type: LiquidVariable + rawSource: ( 0 .. 5 ) + position: { start: [0, 3], text: "( 0 .. 5 ) " } + expression: + type: Range + position: { start: [0, 3], text: ( 0 .. 5 ) } + start: + type: Number + value: "0" + position: { start: [0, 5], text: "0" } + end: + type: Number + value: "5" + position: { start: [0, 10], text: "5" } + filters: [] + - name: should parse ranges as LiquidVariable > Range (3) + source: "{{ (true..false) }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ (true..false) }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " (true..false) " } + position: { start: [0, 0], text: "{{ (true..false) }}" } + markup: + type: LiquidVariable + rawSource: (true..false) + position: { start: [0, 3], text: "(true..false) " } + expression: + type: Range + position: { start: [0, 3], text: (true..false) } + start: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 4], text: "true" } + end: + type: LiquidLiteral + keyword: "false" + value: false + position: { start: [0, 10], text: "false" } + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (1) + source: "{{ x }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ x }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " x " } + position: { start: [0, 0], text: "{{ x }}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [0, 3], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: x } + lookups: [] + lookupModes: [] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (2) + source: "{{ x.y }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ x.y }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " x.y " } + position: { start: [0, 0], text: "{{ x.y }}" } + markup: + type: LiquidVariable + rawSource: x.y + position: { start: [0, 3], text: "x.y " } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: x.y } + lookups: + - type: String + value: y + position: { start: [0, 5], text: y } + lookupModes: [bareword] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (3) + source: '{{ x["y"] }}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{{ x["y"] }}' } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: ' x["y"] ' } + position: { start: [0, 0], text: '{{ x["y"] }}' } + markup: + type: LiquidVariable + rawSource: x["y"] + position: { start: [0, 3], text: 'x["y"] ' } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: 'x["y"]' } + lookups: + - type: String + single: false + value: y + position: { start: [0, 5], text: '"y"' } + lookupModes: [subscript] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (4) + source: "{{ x['y'] }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ x['y'] }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " x['y'] " } + position: { start: [0, 0], text: "{{ x['y'] }}" } + markup: + type: LiquidVariable + rawSource: x['y'] + position: { start: [0, 3], text: "x['y'] " } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: "x['y']" } + lookups: + - type: String + single: true + value: y + position: { start: [0, 5], text: "'y'" } + lookupModes: [subscript] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (5) + source: "{{ x[1] }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ x[1] }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " x[1] " } + position: { start: [0, 0], text: "{{ x[1] }}" } + markup: + type: LiquidVariable + rawSource: x[1] + position: { start: [0, 3], text: "x[1] " } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: "x[1]" } + lookups: + - type: Number + value: "1" + position: { start: [0, 5], text: "1" } + lookupModes: [subscript] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (6) + source: "{{ x.y.z }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ x.y.z }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " x.y.z " } + position: { start: [0, 0], text: "{{ x.y.z }}" } + markup: + type: LiquidVariable + rawSource: x.y.z + position: { start: [0, 3], text: "x.y.z " } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: x.y.z } + lookups: + - type: String + value: y + position: { start: [0, 5], text: y } + - type: String + value: z + position: { start: [0, 7], text: z } + lookupModes: [bareword, bareword] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (7) + source: '{{ x["y"]["z"] }}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{{ x["y"]["z"] }}' } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: ' x["y"]["z"] ' } + position: { start: [0, 0], text: '{{ x["y"]["z"] }}' } + markup: + type: LiquidVariable + rawSource: x["y"]["z"] + position: { start: [0, 3], text: 'x["y"]["z"] ' } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: 'x["y"]["z"]' } + lookups: + - type: String + single: false + value: y + position: { start: [0, 5], text: '"y"' } + - type: String + single: false + value: z + position: { start: [0, 10], text: '"z"' } + lookupModes: [subscript, subscript] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (8) + source: '{{ x["y"].z }}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{{ x["y"].z }}' } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: ' x["y"].z ' } + position: { start: [0, 0], text: '{{ x["y"].z }}' } + markup: + type: LiquidVariable + rawSource: x["y"].z + position: { start: [0, 3], text: 'x["y"].z ' } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: 'x["y"].z' } + lookups: + - type: String + single: false + value: y + position: { start: [0, 5], text: '"y"' } + - type: String + value: z + position: { start: [0, 10], text: z } + lookupModes: [subscript, bareword] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (9) + source: '{{ ["product"] }}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{{ ["product"] }}' } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: ' ["product"] ' } + position: { start: [0, 0], text: '{{ ["product"] }}' } + markup: + type: LiquidVariable + rawSource: '["product"]' + position: { start: [0, 3], text: '["product"] ' } + expression: + type: VariableLookup + name: null + position: { start: [0, 3], text: '["product"]' } + lookups: + - type: String + single: false + value: product + position: { start: [0, 4], text: '"product"' } + lookupModes: [subscript] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (10) + source: "{{ page.about-us }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ page.about-us }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " page.about-us " } + position: { start: [0, 0], text: "{{ page.about-us }}" } + markup: + type: LiquidVariable + rawSource: page.about-us + position: { start: [0, 3], text: "page.about-us " } + expression: + type: VariableLookup + name: page + position: { start: [0, 3], text: page.about-us } + lookups: + - type: String + value: about-us + position: { start: [0, 8], text: about-us } + lookupModes: [bareword] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (11) + source: '{{ ["x"].y }}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{{ ["x"].y }}' } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: ' ["x"].y ' } + position: { start: [0, 0], text: '{{ ["x"].y }}' } + markup: + type: LiquidVariable + rawSource: '["x"].y' + position: { start: [0, 3], text: '["x"].y ' } + expression: + type: VariableLookup + name: null + position: { start: [0, 3], text: '["x"].y' } + lookups: + - type: String + single: false + value: x + position: { start: [0, 4], text: '"x"' } + - type: String + value: y + position: { start: [0, 9], text: y } + lookupModes: [subscript, bareword] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (12) + source: '{{ ["x"]["y"] }}' + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{{ ["x"]["y"] }}' } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: ' ["x"]["y"] ' } + position: { start: [0, 0], text: '{{ ["x"]["y"] }}' } + markup: + type: LiquidVariable + rawSource: '["x"]["y"]' + position: { start: [0, 3], text: '["x"]["y"] ' } + expression: + type: VariableLookup + name: null + position: { start: [0, 3], text: '["x"]["y"]' } + lookups: + - type: String + single: false + value: x + position: { start: [0, 4], text: '"x"' } + - type: String + single: false + value: y + position: { start: [0, 9], text: '"y"' } + lookupModes: [subscript, subscript] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (13) + source: "{{ x[y] }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ x[y] }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " x[y] " } + position: { start: [0, 0], text: "{{ x[y] }}" } + markup: + type: LiquidVariable + rawSource: x[y] + position: { start: [0, 3], text: "x[y] " } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: "x[y]" } + lookups: + - type: VariableLookup + name: y + position: { start: [0, 5], text: y } + lookups: [] + lookupModes: [] + lookupModes: [subscript] + filters: [] + - name: should parse variable lookups as LiquidVariable > VariableLookup (14) + source: "{{ x[y.z] }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ x[y.z] }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " x[y.z] " } + position: { start: [0, 0], text: "{{ x[y.z] }}" } + markup: + type: LiquidVariable + rawSource: x[y.z] + position: { start: [0, 3], text: "x[y.z] " } + expression: + type: VariableLookup + name: x + position: { start: [0, 3], text: "x[y.z]" } + lookups: + - type: VariableLookup + name: y + position: { start: [0, 5], text: y.z } + lookups: + - type: String + value: z + position: { start: [0, 7], text: z } + lookupModes: [bareword] + lookupModes: [subscript] + filters: [] + - name: should parse filters (1) + source: "{{ 'hello' | filter1 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'hello' | filter1 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'hello' | filter1 " } + position: { start: [0, 0], text: "{{ 'hello' | filter1 }}" } + markup: + type: LiquidVariable + rawSource: "'hello' | filter1" + position: { start: [0, 3], text: "'hello' | filter1 " } + expression: + type: String + single: true + value: hello + position: { start: [0, 3], text: "'hello'" } + filters: + - type: LiquidFilter + name: filter1 + position: { start: [0, 10], text: " | filter1" } + args: [] + - name: should parse filters (2) + source: "{{ 'hello' | filter1 | filter2 }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'hello' | filter1 | filter2 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'hello' | filter1 | filter2 " } + position: { start: [0, 0], text: "{{ 'hello' | filter1 | filter2 }}" } + markup: + type: LiquidVariable + rawSource: "'hello' | filter1 | filter2" + position: { start: [0, 3], text: "'hello' | filter1 | filter2 " } + expression: + type: String + single: true + value: hello + position: { start: [0, 3], text: "'hello'" } + filters: + - type: LiquidFilter + name: filter1 + position: { start: [0, 10], text: " | filter1" } + args: [] + - type: LiquidFilter + name: filter2 + position: { start: [0, 20], text: " | filter2" } + args: [] + - name: should parse filters (3) + source: "{{ 'hello' | filter1: 'hi', 'there' }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'hello' | filter1: 'hi', 'there' }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'hello' | filter1: 'hi', 'there' " } + position: { start: [0, 0], text: "{{ 'hello' | filter1: 'hi', 'there' }}" } + markup: + type: LiquidVariable + rawSource: "'hello' | filter1: 'hi', 'there'" + position: { start: [0, 3], text: "'hello' | filter1: 'hi', 'there' " } + expression: + type: String + single: true + value: hello + position: { start: [0, 3], text: "'hello'" } + filters: + - type: LiquidFilter + name: filter1 + position: { start: [0, 10], text: " | filter1: 'hi', 'there'" } + args: + - type: String + single: true + value: hi + position: { start: [0, 22], text: "'hi'" } + - type: String + single: true + value: there + position: { start: [0, 28], text: "'there'" } + - name: should parse filters (4) + source: "{{ 'hello' | filter1: key: value, kind: 'string' }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'hello' | filter1: key: value, kind: 'string' }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'hello' | filter1: key: value, kind: 'string' " } + position: { start: [0, 0], text: "{{ 'hello' | filter1: key: value, kind: 'string' }}" } + markup: + type: LiquidVariable + rawSource: "'hello' | filter1: key: value, kind: 'string'" + position: { start: [0, 3], text: "'hello' | filter1: key: value, kind: 'string' " } + expression: + type: String + single: true + value: hello + position: { start: [0, 3], text: "'hello'" } + filters: + - type: LiquidFilter + name: filter1 + position: { start: [0, 10], text: " | filter1: key: value, kind: 'string'" } + args: + - type: NamedArgument + name: key + position: { start: [0, 22], text: "key: value" } + value: + type: VariableLookup + name: value + position: { start: [0, 27], text: value } + lookups: [] + lookupModes: [] + - type: NamedArgument + name: kind + position: { start: [0, 34], text: "kind: 'string'" } + value: + type: String + single: true + value: string + position: { start: [0, 40], text: "'string'" } + - name: should parse filters (5) + source: "{{ 'hello' | f1: 'hi', key: (0..1) | f2: key: value, kind: 'string' }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'hello' | f1: 'hi', key: (0..1) | f2: key: value, kind: 'string' }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'hello' | f1: 'hi', key: (0..1) | f2: key: value, kind: 'string' " } + position: { start: [0, 0], text: "{{ 'hello' | f1: 'hi', key: (0..1) | f2: key: value, kind: 'string' }}" } + markup: + type: LiquidVariable + rawSource: "'hello' | f1: 'hi', key: (0..1) | f2: key: value, kind: 'string'" + position: { start: [0, 3], text: "'hello' | f1: 'hi', key: (0..1) | f2: key: value, kind: 'string' " } + expression: + type: String + single: true + value: hello + position: { start: [0, 3], text: "'hello'" } + filters: + - type: LiquidFilter + name: f1 + position: { start: [0, 10], text: " | f1: 'hi', key: (0..1)" } + args: + - type: String + single: true + value: hi + position: { start: [0, 17], text: "'hi'" } + - type: NamedArgument + name: key + position: { start: [0, 23], text: "key: (0..1)" } + value: + type: Range + position: { start: [0, 28], text: (0..1) } + start: + type: Number + value: "0" + position: { start: [0, 29], text: "0" } + end: + type: Number + value: "1" + position: { start: [0, 32], text: "1" } + - type: LiquidFilter + name: f2 + position: { start: [0, 34], text: " | f2: key: value, kind: 'string'" } + args: + - type: NamedArgument + name: key + position: { start: [0, 41], text: "key: value" } + value: + type: VariableLookup + name: value + position: { start: [0, 46], text: value } + lookups: [] + lookupModes: [] + - type: NamedArgument + name: kind + position: { start: [0, 53], text: "kind: 'string'" } + value: + type: String + single: true + value: string + position: { start: [0, 59], text: "'string'" } + - name: should parse a simple variable + source: "{{ product }}" + parse: [html, liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ product }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " product " } + position: { start: [0, 0], text: "{{ product }}" } + markup: + type: LiquidVariable + rawSource: product + position: { start: [0, 3], text: "product " } + expression: + type: VariableLookup + name: product + position: { start: [0, 3], text: product } + lookups: [] + lookupModes: [] + filters: [] + - name: should parse a string expression + source: '{{ "hello" }}' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{{ "hello" }}' } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: ' "hello" ' } + position: { start: [0, 0], text: '{{ "hello" }}' } + markup: + type: LiquidVariable + rawSource: '"hello"' + position: { start: [0, 3], text: '"hello" ' } + expression: + type: String + single: false + value: hello + position: { start: [0, 3], text: '"hello"' } + filters: [] + - name: should parse a number expression + source: "{{ 42 }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 42 }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 42 " } + position: { start: [0, 0], text: "{{ 42 }}" } + markup: + type: LiquidVariable + rawSource: "42" + position: { start: [0, 3], text: "42 " } + expression: + type: Number + value: "42" + position: { start: [0, 3], text: "42" } + filters: [] + - name: should parse a filter + source: "{{ product | upcase }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ product | upcase }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " product | upcase " } + position: { start: [0, 0], text: "{{ product | upcase }}" } + markup: + type: LiquidVariable + rawSource: product | upcase + position: { start: [0, 3], text: "product | upcase " } + expression: + type: VariableLookup + name: product + position: { start: [0, 3], text: product } + lookups: [] + lookupModes: [] + filters: + - type: LiquidFilter + name: upcase + position: { start: [0, 10], text: " | upcase" } + args: [] + - name: "should parse an empty filter argument list in strict mode: {{ 'key' | t:}}" + source: "{{ 'key' | t:}}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'key' | t:}}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'key' | t:" } + position: { start: [0, 0], text: "{{ 'key' | t:}}" } + markup: + type: LiquidVariable + rawSource: "'key' | t:" + position: { start: [0, 3], text: "'key' | t:" } + expression: + type: String + single: true + value: key + position: { start: [0, 3], text: "'key'" } + filters: + - type: LiquidFilter + name: t + position: { start: [0, 8], text: " | t:" } + args: [] + - name: "should parse an empty filter argument list in strict mode: {{ 'key' | t: }}" + source: "{{ 'key' | t: }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'key' | t: }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'key' | t: " } + position: { start: [0, 0], text: "{{ 'key' | t: }}" } + markup: + type: LiquidVariable + rawSource: "'key' | t:" + position: { start: [0, 3], text: "'key' | t: " } + expression: + type: String + single: true + value: key + position: { start: [0, 3], text: "'key'" } + filters: + - type: LiquidFilter + name: t + position: { start: [0, 8], text: " | t:" } + args: [] + - name: "should parse an empty filter argument list in strict mode: {{- 'key' | t: -}}" + source: |- + {{- 'key' | t: + -}} + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {{- 'key' | t: + -}} + children: + - type: LiquidVariableOutput + whitespaceStart: "-" + whitespaceEnd: "-" + markupPosition: + start: [0, 3] + text: |2 + 'key' | t: + position: + start: [0, 0] + text: |- + {{- 'key' | t: + -}} + markup: + type: LiquidVariable + rawSource: "'key' | t:" + position: + start: [0, 4] + text: | + 'key' | t: + expression: + type: String + single: true + value: key + position: { start: [0, 4], text: "'key'" } + filters: + - type: LiquidFilter + name: t + position: { start: [0, 9], text: " | t:" } + args: [] + - name: should parse another filter after an empty argument list + source: "{{ 'key' | t: | escape }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'key' | t: | escape }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'key' | t: | escape " } + position: { start: [0, 0], text: "{{ 'key' | t: | escape }}" } + markup: + type: LiquidVariable + rawSource: "'key' | t: | escape" + position: { start: [0, 3], text: "'key' | t: | escape " } + expression: + type: String + single: true + value: key + position: { start: [0, 3], text: "'key'" } + filters: + - type: LiquidFilter + name: t + position: { start: [0, 8], text: " | t:" } + args: [] + - type: LiquidFilter + name: escape + position: { start: [0, 13], text: " | escape" } + args: [] + - name: "should keep malformed filter arguments as string markup: 'key' | t: title:" + source: "{{ 'key' | t: title: }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'key' | t: title: }}" } + children: + - type: LiquidVariableOutput + markup: "'key' | t: title:" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'key' | t: title: " } + position: { start: [0, 0], text: "{{ 'key' | t: title: }}" } + - name: "should keep malformed filter arguments as string markup: 'key' | t: title: | escape" + source: "{{ 'key' | t: title: | escape }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'key' | t: title: | escape }}" } + children: + - type: LiquidVariableOutput + markup: "'key' | t: title: | escape" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'key' | t: title: | escape " } + position: { start: [0, 0], text: "{{ 'key' | t: title: | escape }}" } + - name: "should keep malformed filter arguments as string markup: 'key' | t: ," + source: "{{ 'key' | t: , }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'key' | t: , }}" } + children: + - type: LiquidVariableOutput + markup: "'key' | t: ," + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'key' | t: , " } + position: { start: [0, 0], text: "{{ 'key' | t: , }}" } + - name: "should keep malformed filter arguments as string markup: 'key' | t: title: product.title," + source: "{{ 'key' | t: title: product.title, }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'key' | t: title: product.title, }}" } + children: + - type: LiquidVariableOutput + markup: "'key' | t: title: product.title," + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'key' | t: title: product.title, " } + position: { start: [0, 0], text: "{{ 'key' | t: title: product.title, }}" } + - name: "should keep malformed filter arguments as string markup: 'key' | t: @" + source: "{{ 'key' | t: @ }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ 'key' | t: @ }}" } + children: + - type: LiquidVariableOutput + markup: "'key' | t: @" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " 'key' | t: @ " } + position: { start: [0, 0], text: "{{ 'key' | t: @ }}" } + - name: should parse multiple filters + source: "{{ product | upcase | strip }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ product | upcase | strip }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " product | upcase | strip " } + position: { start: [0, 0], text: "{{ product | upcase | strip }}" } + markup: + type: LiquidVariable + rawSource: product | upcase | strip + position: { start: [0, 3], text: "product | upcase | strip " } + expression: + type: VariableLookup + name: product + position: { start: [0, 3], text: product } + lookups: [] + lookupModes: [] + filters: + - type: LiquidFilter + name: upcase + position: { start: [0, 10], text: " | upcase" } + args: [] + - type: LiquidFilter + name: strip + position: { start: [0, 19], text: " | strip" } + args: [] + - name: should handle whitespace stripping + source: "{{- x -}}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{- x -}}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "-" + whitespaceEnd: "-" + markupPosition: { start: [0, 3], text: " x " } + position: { start: [0, 0], text: "{{- x -}}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [0, 4], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [0, 4], text: x } + lookups: [] + lookupModes: [] + filters: [] + - name: should have correct position for drop node + source: before{{ x }}after + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "before{{ x }}after" } + children: + - type: TextNode + value: before + position: { start: [0, 0], text: before } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 8], text: " x " } + position: { start: [0, 6], text: "{{ x }}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [0, 9], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [0, 9], text: x } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value: after + position: { start: [0, 13], text: after } + - name: should produce text before and after a drop + source: hello {{ x }} world + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "hello {{ x }} world" } + children: + - type: TextNode + value: hello + position: { start: [0, 0], text: hello } + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 8], text: " x " } + position: { start: [0, 6], text: "{{ x }}" } + markup: + type: LiquidVariable + rawSource: x + position: { start: [0, 9], text: "x " } + expression: + type: VariableLookup + name: x + position: { start: [0, 9], text: x } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value: world + position: { start: [0, 14], text: world } + - name: should parse multiple drops + source: "{{ a }}{{ b }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ a }}{{ b }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " a " } + position: { start: [0, 0], text: "{{ a }}" } + markup: + type: LiquidVariable + rawSource: a + position: { start: [0, 3], text: "a " } + expression: + type: VariableLookup + name: a + position: { start: [0, 3], text: a } + lookups: [] + lookupModes: [] + filters: [] + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 9], text: " b " } + position: { start: [0, 7], text: "{{ b }}" } + markup: + type: LiquidVariable + rawSource: b + position: { start: [0, 10], text: "b " } + expression: + type: VariableLookup + name: b + position: { start: [0, 10], text: b } + lookups: [] + lookupModes: [] + filters: [] + - name: should parse a range expression + source: "{{ (1..5) }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ (1..5) }}" } + children: + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " (1..5) " } + position: { start: [0, 0], text: "{{ (1..5) }}" } + markup: + type: LiquidVariable + rawSource: (1..5) + position: { start: [0, 3], text: "(1..5) " } + expression: + type: Range + position: { start: [0, 3], text: (1..5) } + start: + type: Number + value: "1" + position: { start: [0, 4], text: "1" } + end: + type: Number + value: "5" + position: { start: [0, 7], text: "5" } + filters: [] + - name: should fall back to string markup for empty drop + source: "{{ }}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{{ }}" } + children: + - type: LiquidVariableOutput + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 2], text: " " } + position: { start: [0, 0], text: "{{ }}" } diff --git a/specs/parser/unclosed.yml b/specs/parser/unclosed.yml new file mode 100644 index 000000000..f3c58b4eb --- /dev/null +++ b/specs/parser/unclosed.yml @@ -0,0 +1,1826 @@ +cases: + - name: should parse nested unclosed tags + source: "{% for a in b %}
    {% if true %}" + parse: [liquid] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% for a in b %}
    {% if true %}" } + children: + - type: LiquidTag + name: for + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% for a in b %}" } + blockEndPosition: { start: [0, 36], text: "" } + markupPosition: { start: [0, 7], text: "a in b " } + position: { start: [0, 0], text: "{% for a in b %}
    {% if true %}" } + markup: + type: ForMarkup + variableName: a + reversed: false + position: { start: [0, 7], text: "a in b " } + collection: + type: VariableLookup + name: b + position: { start: [0, 12], text: b } + lookups: [] + lookupModes: [] + args: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 16], text: "" } + blockStartPosition: { start: [0, 16], text: "" } + blockEndPosition: { start: [0, 36], text: "" } + position: { start: [0, 16], text: "
    {% if true %}" } + children: + - type: TextNode + value:
    + position: { start: [0, 17], text:
    } + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 23], text: "{% if true %}" } + blockEndPosition: { start: [0, 36], text: "" } + markupPosition: { start: [0, 29], text: "true " } + position: { start: [0, 23], text: "{% if true %}" } + markup: + type: LiquidLiteral + keyword: "true" + value: true + position: { start: [0, 29], text: "true" } + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 36], text: "" } + blockStartPosition: { start: [0, 36], text: "" } + blockEndPosition: { start: [0, 36], text: "" } + position: { start: [0, 36], text: "" } + children: [] + - name: should parse unclosed conditions with assignments + source: " + + \ {%- liquid + + \ assign var1 = product + + + \ if use_variant + + \ assign var2 = var1 + + \ assign var3 = var2 + + \ -%} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {%- liquid + + \ assign var1 = product + + + \ if use_variant + + \ assign var2 = var1 + + \ assign var3 = var2 + + \ -%} + + \ " + children: + - type: LiquidTag + name: liquid + whitespaceStart: "-" + whitespaceEnd: "-" + blockStartPosition: + start: [1, 8] + text: |- + {%- liquid + assign var1 = product + + if use_variant + assign var2 = var1 + assign var3 = var2 + -%} + markupPosition: + start: [1, 18] + text: " + + \ assign var1 = product + + + \ if use_variant + + \ assign var2 = var1 + + \ assign var3 = var2 + + \ " + position: + start: [1, 8] + text: |- + {%- liquid + assign var1 = product + + if use_variant + assign var2 = var1 + assign var3 = var2 + -%} + markup: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 10], text: assign var1 = product } + markupPosition: { start: [2, 17], text: var1 = product } + position: { start: [2, 10], text: assign var1 = product } + markup: + type: AssignMarkup + name: var1 + position: { start: [2, 17], text: var1 = product } + value: + type: LiquidVariable + rawSource: product + position: { start: [2, 24], text: product } + expression: + type: VariableLookup + name: product + position: { start: [2, 24], text: product } + lookups: [] + lookupModes: [] + filters: [] + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [4, 10], text: if use_variant } + blockEndPosition: { start: [6, 30], text: "" } + markupPosition: { start: [4, 13], text: use_variant } + position: + start: [4, 10] + text: |- + if use_variant + assign var2 = var1 + assign var3 = var2 + markup: + type: VariableLookup + name: use_variant + position: { start: [4, 13], text: use_variant } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [4, 24], text: "" } + blockStartPosition: { start: [4, 24], text: "" } + blockEndPosition: { start: [6, 30], text: "" } + position: + start: [4, 24] + text: |2- + + assign var2 = var1 + assign var3 = var2 + children: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [5, 12], text: assign var2 = var1 } + markupPosition: { start: [5, 19], text: var2 = var1 } + position: { start: [5, 12], text: assign var2 = var1 } + markup: + type: AssignMarkup + name: var2 + position: { start: [5, 19], text: var2 = var1 } + value: + type: LiquidVariable + rawSource: var1 + position: { start: [5, 26], text: var1 } + expression: + type: VariableLookup + name: var1 + position: { start: [5, 26], text: var1 } + lookups: [] + lookupModes: [] + filters: [] + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [6, 12], text: assign var3 = var2 } + markupPosition: { start: [6, 19], text: var3 = var2 } + position: { start: [6, 12], text: assign var3 = var2 } + markup: + type: AssignMarkup + name: var3 + position: { start: [6, 19], text: var3 = var2 } + value: + type: LiquidVariable + rawSource: var2 + position: { start: [6, 26], text: var2 } + expression: + type: VariableLookup + name: var2 + position: { start: [6, 26], text: var2 } + lookups: [] + lookupModes: [] + filters: [] + - name: should tolerate unclosed Liquid tags in {% liquid %} blocks + source: |- + {%- liquid + assign var1 = product + if use_variant + assign var2 = var1 + -%} + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: |- + {%- liquid + assign var1 = product + if use_variant + assign var2 = var1 + -%} + children: + - type: LiquidTag + name: liquid + whitespaceStart: "-" + whitespaceEnd: "-" + blockStartPosition: + start: [0, 0] + text: |- + {%- liquid + assign var1 = product + if use_variant + assign var2 = var1 + -%} + markupPosition: + start: [0, 10] + text: |2 + + assign var1 = product + if use_variant + assign var2 = var1 + position: + start: [0, 0] + text: |- + {%- liquid + assign var1 = product + if use_variant + assign var2 = var1 + -%} + markup: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [1, 2], text: assign var1 = product } + markupPosition: { start: [1, 9], text: var1 = product } + position: { start: [1, 2], text: assign var1 = product } + markup: + type: AssignMarkup + name: var1 + position: { start: [1, 9], text: var1 = product } + value: + type: LiquidVariable + rawSource: product + position: { start: [1, 16], text: product } + expression: + type: VariableLookup + name: product + position: { start: [1, 16], text: product } + lookups: [] + lookupModes: [] + filters: [] + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [2, 2], text: if use_variant } + blockEndPosition: { start: [3, 22], text: "" } + markupPosition: { start: [2, 5], text: use_variant } + position: + start: [2, 2] + text: |- + if use_variant + assign var2 = var1 + markup: + type: VariableLookup + name: use_variant + position: { start: [2, 5], text: use_variant } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [2, 16], text: "" } + blockStartPosition: { start: [2, 16], text: "" } + blockEndPosition: { start: [3, 22], text: "" } + position: + start: [2, 16] + text: |2- + + assign var2 = var1 + children: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [3, 4], text: assign var2 = var1 } + markupPosition: { start: [3, 11], text: var2 = var1 } + position: { start: [3, 4], text: assign var2 = var1 } + markup: + type: AssignMarkup + name: var2 + position: { start: [3, 11], text: var2 = var1 } + value: + type: LiquidVariable + rawSource: var1 + position: { start: [3, 18], text: var1 } + expression: + type: VariableLookup + name: var1 + position: { start: [3, 18], text: var1 } + lookups: [] + lookupModes: [] + filters: [] + - name: should parse unclosed tables with assignments + source: " + + \ {%- liquid + + \ assign var1 = product + + \ -%} + + \
    + + \ {% tablerow var2 in collections.first.products %} + + \ {% assign var3 = var2 %} + + \ {{ var3.title }} + + \ " + parse: [liquid] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {%- liquid + + \ assign var1 = product + + \ -%} + + \
    + + \ {% tablerow var2 in collections.first.products %} + + \ {% assign var3 = var2 %} + + \ {{ var3.title }} + + \ " + children: + - type: LiquidTag + name: liquid + whitespaceStart: "-" + whitespaceEnd: "-" + blockStartPosition: + start: [1, 8] + text: |- + {%- liquid + assign var1 = product + -%} + markupPosition: + start: [1, 18] + text: " + + \ assign var1 = product + + \ " + position: + start: [1, 8] + text: |- + {%- liquid + assign var1 = product + -%} + markup: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [2, 10], text: assign var1 = product } + markupPosition: { start: [2, 17], text: var1 = product } + position: { start: [2, 10], text: assign var1 = product } + markup: + type: AssignMarkup + name: var1 + position: { start: [2, 17], text: var1 = product } + value: + type: LiquidVariable + rawSource: product + position: { start: [2, 24], text: product } + expression: + type: VariableLookup + name: product + position: { start: [2, 24], text: product } + lookups: [] + lookupModes: [] + filters: [] + - type: TextNode + value:
    + position: { start: [4, 8], text:
    } + - type: LiquidTag + name: tablerow + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [5, 10], text: "{% tablerow var2 in collections.first.products %}" } + blockEndPosition: { start: [8, 6], text: "" } + markupPosition: { start: [5, 22], text: "var2 in collections.first.products " } + position: + start: [5, 10] + text: "{% tablerow var2 in collections.first.products %} + + \ {% assign var3 = var2 %} + + \ {{ var3.title }} + + \ " + markup: + type: ForMarkup + variableName: var2 + reversed: false + position: { start: [5, 22], text: "var2 in collections.first.products " } + collection: + type: VariableLookup + name: collections + position: { start: [5, 30], text: collections.first.products } + lookups: + - type: String + value: first + position: { start: [5, 42], text: first } + - type: String + value: products + position: { start: [5, 48], text: products } + lookupModes: [bareword, bareword] + args: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [5, 59], text: "" } + blockStartPosition: { start: [5, 59], text: "" } + blockEndPosition: { start: [8, 6], text: "" } + position: + start: [5, 59] + text: " + + \ {% assign var3 = var2 %} + + \ {{ var3.title }} + + \ " + children: + - type: LiquidTag + name: assign + whitespaceStart: "" + whitespaceEnd: "" + blockStartPosition: { start: [6, 12], text: "{% assign var3 = var2 %}" } + markupPosition: { start: [6, 22], text: "var3 = var2 " } + position: { start: [6, 12], text: "{% assign var3 = var2 %}" } + markup: + type: AssignMarkup + name: var3 + position: { start: [6, 22], text: "var3 = var2 " } + value: + type: LiquidVariable + rawSource: var2 + position: { start: [6, 29], text: "var2 " } + expression: + type: VariableLookup + name: var2 + position: { start: [6, 29], text: var2 } + lookups: [] + lookupModes: [] + filters: [] + - type: LiquidVariableOutput + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [7, 14], text: " var3.title " } + position: { start: [7, 12], text: "{{ var3.title }}" } + markup: + type: LiquidVariable + rawSource: var3.title + position: { start: [7, 15], text: "var3.title " } + expression: + type: VariableLookup + name: var3 + position: { start: [7, 15], text: var3.title } + lookups: + - type: String + value: title + position: { start: [7, 20], text: title } + lookupModes: [bareword] + filters: [] + - name: should allow for dangling html open tags inside branches when the conditions are right (1) + source: " + + \ {% if condition %} + + \
    + + \ {% endif %} + + \ " + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% if condition %} + + \
    + + \ {% endif %} + + \ " + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% if condition %}" } + blockEndPosition: { start: [3, 8], text: "{% endif %}" } + markupPosition: { start: [1, 14], text: "condition " } + position: + start: [1, 8] + text: |- + {% if condition %} +
    + {% endif %} + markup: + type: VariableLookup + name: condition + position: { start: [1, 14], text: condition } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 26], text: "" } + blockStartPosition: { start: [1, 26], text: "" } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [1, 26] + text: " + + \
    + + \ " + children: + - type: HtmlElement + blockStartPosition: { start: [2, 10], text:
    } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [2, 10] + text: "
    + + \ " + name: + - type: TextNode + value: section + position: { start: [2, 11], text: section } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [2, 26], text: unclosed } + position: { start: [2, 19], text: class="unclosed" } + name: + - type: TextNode + value: class + position: { start: [2, 19], text: class } + value: + - type: TextNode + value: unclosed + position: { start: [2, 26], text: unclosed } + children: [] + - name: should allow for dangling html open tags inside branches when the conditions are right (2) + source: " + + \ {% if condition %} + + \
    + + \ {% else %} + + \
    + + \ {% endif %} + + \ " + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% if condition %} + + \
    + + \ {% else %} + + \
    + + \ {% endif %} + + \ " + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% if condition %}" } + blockEndPosition: { start: [5, 8], text: "{% endif %}" } + markupPosition: { start: [1, 14], text: "condition " } + position: + start: [1, 8] + text: |- + {% if condition %} +
    + {% else %} +
    + {% endif %} + markup: + type: VariableLookup + name: condition + position: { start: [1, 14], text: condition } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 26], text: "" } + blockStartPosition: { start: [1, 26], text: "" } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [1, 26] + text: " + + \
    + + \ " + children: + - type: HtmlElement + blockStartPosition: { start: [2, 10], text:
    } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [2, 10] + text: "
    + + \ " + name: + - type: TextNode + value: section + position: { start: [2, 11], text: section } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [2, 26], text: unclosed } + position: { start: [2, 19], text: class="unclosed" } + name: + - type: TextNode + value: class + position: { start: [2, 19], text: class } + value: + - type: TextNode + value: unclosed + position: { start: [2, 26], text: unclosed } + children: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [3, 16], text: "" } + blockStartPosition: { start: [3, 8], text: "{% else %}" } + blockEndPosition: { start: [5, 8], text: "" } + position: + start: [3, 8] + text: "{% else %} + + \
    + + \ " + children: + - type: HtmlElement + blockStartPosition: { start: [4, 10], text:
    } + blockEndPosition: { start: [5, 8], text: "" } + position: + start: [4, 10] + text: "
    + + \ " + name: + - type: TextNode + value: section + position: { start: [4, 11], text: section } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [4, 26], text: unclosed } + position: { start: [4, 19], text: class="unclosed" } + name: + - type: TextNode + value: class + position: { start: [4, 19], text: class } + value: + - type: TextNode + value: unclosed + position: { start: [4, 26], text: unclosed } + children: [] + - name: should allow for dangling html open tags inside branches when the conditions are right (3) + source: " + + \ {% unless condition %} + + \
    + + \ {% endunless %} + + \ " + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% unless condition %} + + \
    + + \ {% endunless %} + + \ " + children: + - type: LiquidTag + name: unless + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% unless condition %}" } + blockEndPosition: { start: [3, 8], text: "{% endunless %}" } + markupPosition: { start: [1, 18], text: "condition " } + position: + start: [1, 8] + text: |- + {% unless condition %} +
    + {% endunless %} + markup: + type: VariableLookup + name: condition + position: { start: [1, 18], text: condition } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 30], text: "" } + blockStartPosition: { start: [1, 30], text: "" } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [1, 30] + text: " + + \
    + + \ " + children: + - type: HtmlElement + blockStartPosition: { start: [2, 10], text:
    } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [2, 10] + text: "
    + + \ " + name: + - type: TextNode + value: section + position: { start: [2, 11], text: section } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [2, 26], text: unclosed } + position: { start: [2, 19], text: class="unclosed" } + name: + - type: TextNode + value: class + position: { start: [2, 19], text: class } + value: + - type: TextNode + value: unclosed + position: { start: [2, 26], text: unclosed } + children: [] + - name: should allow for dangling html open tags inside branches when the conditions are right (4) + source: " + + \ {% unless condition %} + + \
    + + \ {% else %} + + \
    + + \ {% endunless %} + + \ " + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% unless condition %} + + \
    + + \ {% else %} + + \
    + + \ {% endunless %} + + \ " + children: + - type: LiquidTag + name: unless + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% unless condition %}" } + blockEndPosition: { start: [5, 8], text: "{% endunless %}" } + markupPosition: { start: [1, 18], text: "condition " } + position: + start: [1, 8] + text: |- + {% unless condition %} +
    + {% else %} +
    + {% endunless %} + markup: + type: VariableLookup + name: condition + position: { start: [1, 18], text: condition } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 30], text: "" } + blockStartPosition: { start: [1, 30], text: "" } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [1, 30] + text: " + + \
    + + \ " + children: + - type: HtmlElement + blockStartPosition: { start: [2, 10], text:
    } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [2, 10] + text: "
    + + \ " + name: + - type: TextNode + value: section + position: { start: [2, 11], text: section } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [2, 26], text: unclosed } + position: { start: [2, 19], text: class="unclosed" } + name: + - type: TextNode + value: class + position: { start: [2, 19], text: class } + value: + - type: TextNode + value: unclosed + position: { start: [2, 26], text: unclosed } + children: [] + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [3, 16], text: "" } + blockStartPosition: { start: [3, 8], text: "{% else %}" } + blockEndPosition: { start: [5, 8], text: "" } + position: + start: [3, 8] + text: "{% else %} + + \
    + + \ " + children: + - type: HtmlElement + blockStartPosition: { start: [4, 10], text:
    } + blockEndPosition: { start: [5, 8], text: "" } + position: + start: [4, 10] + text: "
    + + \ " + name: + - type: TextNode + value: section + position: { start: [4, 11], text: section } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [4, 26], text: unclosed } + position: { start: [4, 19], text: class="unclosed" } + name: + - type: TextNode + value: class + position: { start: [4, 19], text: class } + value: + - type: TextNode + value: unclosed + position: { start: [4, 26], text: unclosed } + children: [] + - name: should allow for dangling html close tags inside branches when the conditions are right (1) + source: " + + \ {% if condition %} + + \
    + + \ {% endif %} + + \ " + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% if condition %} + + \
    + + \ {% endif %} + + \ " + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% if condition %}" } + blockEndPosition: { start: [3, 8], text: "{% endif %}" } + markupPosition: { start: [1, 14], text: "condition " } + position: + start: [1, 8] + text: |- + {% if condition %} +
    + {% endif %} + markup: + type: VariableLookup + name: condition + position: { start: [1, 14], text: condition } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 26], text: "" } + blockStartPosition: { start: [1, 26], text: "" } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [1, 26] + text: "\n
    \n " + children: + - type: HtmlDanglingMarkerClose + blockStartPosition: { start: [2, 10], text:
    } + position: { start: [2, 10], text:
    } + name: + - type: TextNode + value: section + position: { start: [2, 12], text: section } + - name: should allow for dangling html close tags inside branches when the conditions are right (2) + source: " + + \ {% if condition %} + + \
    + + \ {% else %} + + \ + + \ {% endif %} + + \ " + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% if condition %} + + \
    + + \ {% else %} + + \ + + \ {% endif %} + + \ " + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% if condition %}" } + blockEndPosition: { start: [5, 8], text: "{% endif %}" } + markupPosition: { start: [1, 14], text: "condition " } + position: + start: [1, 8] + text: |- + {% if condition %} +
    + {% else %} + + {% endif %} + markup: + type: VariableLookup + name: condition + position: { start: [1, 14], text: condition } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 26], text: "" } + blockStartPosition: { start: [1, 26], text: "" } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [1, 26] + text: "\n
    \n " + children: + - type: HtmlDanglingMarkerClose + blockStartPosition: { start: [2, 10], text:
    } + position: { start: [2, 10], text:
    } + name: + - type: TextNode + value: section + position: { start: [2, 12], text: section } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [3, 16], text: "" } + blockStartPosition: { start: [3, 8], text: "{% else %}" } + blockEndPosition: { start: [5, 8], text: "" } + position: + start: [3, 8] + text: "{% else %} + + \ + + \ " + children: + - type: HtmlDanglingMarkerClose + blockStartPosition: { start: [4, 10], text: } + position: { start: [4, 10], text: } + name: + - type: TextNode + value: main + position: { start: [4, 12], text: main } + - name: should allow for dangling html close tags inside branches when the conditions are right (3) + source: " + + \ {% unless condition %} + + \
    + + \ {% endunless %} + + \ " + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% unless condition %} + + \
    + + \ {% endunless %} + + \ " + children: + - type: LiquidTag + name: unless + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% unless condition %}" } + blockEndPosition: { start: [3, 8], text: "{% endunless %}" } + markupPosition: { start: [1, 18], text: "condition " } + position: + start: [1, 8] + text: |- + {% unless condition %} +
    + {% endunless %} + markup: + type: VariableLookup + name: condition + position: { start: [1, 18], text: condition } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 30], text: "" } + blockStartPosition: { start: [1, 30], text: "" } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [1, 30] + text: "\n
    \n " + children: + - type: HtmlDanglingMarkerClose + blockStartPosition: { start: [2, 10], text:
    } + position: { start: [2, 10], text:
    } + name: + - type: TextNode + value: section + position: { start: [2, 12], text: section } + - name: should allow for dangling html close tags inside branches when the conditions are right (4) + source: " + + \ {% unless condition %} + + \
    + + \ {% else %} + + \ + + \ {% endunless %} + + \ " + parse: [html] + ast: + type: Document + name: "#document" + position: + start: [0, 0] + text: " + + \ {% unless condition %} + + \
    + + \ {% else %} + + \ + + \ {% endunless %} + + \ " + children: + - type: LiquidTag + name: unless + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [1, 8], text: "{% unless condition %}" } + blockEndPosition: { start: [5, 8], text: "{% endunless %}" } + markupPosition: { start: [1, 18], text: "condition " } + position: + start: [1, 8] + text: |- + {% unless condition %} +
    + {% else %} + + {% endunless %} + markup: + type: VariableLookup + name: condition + position: { start: [1, 18], text: condition } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [1, 30], text: "" } + blockStartPosition: { start: [1, 30], text: "" } + blockEndPosition: { start: [3, 8], text: "" } + position: + start: [1, 30] + text: "\n
    \n " + children: + - type: HtmlDanglingMarkerClose + blockStartPosition: { start: [2, 10], text:
    } + position: { start: [2, 10], text:
    } + name: + - type: TextNode + value: section + position: { start: [2, 12], text: section } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [3, 16], text: "" } + blockStartPosition: { start: [3, 8], text: "{% else %}" } + blockEndPosition: { start: [5, 8], text: "" } + position: + start: [3, 8] + text: "{% else %} + + \ + + \ " + children: + - type: HtmlDanglingMarkerClose + blockStartPosition: { start: [4, 10], text: } + position: { start: [4, 10], text: } + name: + - type: TextNode + value: main + position: { start: [4, 12], text: main } + - name: should allow unclosed
    with text child inside if/else + source: "{% if cond %}
    text{% else %}more{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}
    text{% else %}more{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 36], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}
    text{% else %}more{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 22], text: "" } + position: { start: [0, 13], text:
    text } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 13], text:
    } + blockEndPosition: { start: [0, 22], text: "" } + position: { start: [0, 13], text:
    text } + name: + - type: TextNode + value: div + position: { start: [0, 14], text: div } + attributes: [] + children: + - type: TextNode + value: text + position: { start: [0, 18], text: text } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 30], text: "" } + blockStartPosition: { start: [0, 22], text: "{% else %}" } + blockEndPosition: { start: [0, 36], text: "" } + position: { start: [0, 22], text: "{% else %}more" } + children: + - type: TextNode + value: more + position: { start: [0, 32], text: more } + - name: should allow unclosed
    inside unless + source: "{% unless cond %}
    {% endunless %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% unless cond %}
    {% endunless %}" } + children: + - type: LiquidTag + name: unless + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% unless cond %}" } + blockEndPosition: { start: [0, 22], text: "{% endunless %}" } + markupPosition: { start: [0, 10], text: "cond " } + position: { start: [0, 0], text: "{% unless cond %}
    {% endunless %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 10], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 17], text: "" } + blockStartPosition: { start: [0, 17], text: "" } + blockEndPosition: { start: [0, 22], text: "" } + position: { start: [0, 17], text:
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 17], text:
    } + blockEndPosition: { start: [0, 22], text: "" } + position: { start: [0, 17], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 18], text: div } + attributes: [] + children: [] + - name: should allow unclosed
    inside case/when + source: "{% case x %}{% when y %}
    {% endcase %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% case x %}{% when y %}
    {% endcase %}" } + children: + - type: LiquidTag + name: case + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% case x %}" } + blockEndPosition: { start: [0, 29], text: "{% endcase %}" } + markupPosition: { start: [0, 8], text: "x " } + position: { start: [0, 0], text: "{% case x %}{% when y %}
    {% endcase %}" } + markup: + type: VariableLookup + name: x + position: { start: [0, 8], text: x } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 12], text: "" } + blockStartPosition: { start: [0, 12], text: "" } + blockEndPosition: { start: [0, 12], text: "" } + position: { start: [0, 12], text: "" } + children: [] + - type: LiquidBranch + name: when + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 20], text: "y " } + blockStartPosition: { start: [0, 12], text: "{% when y %}" } + blockEndPosition: { start: [0, 29], text: "" } + position: { start: [0, 12], text: "{% when y %}
    " } + markup: + - type: VariableLookup + name: y + position: { start: [0, 20], text: y } + lookups: [] + lookupModes: [] + children: + - type: HtmlElement + blockStartPosition: { start: [0, 24], text:
    } + blockEndPosition: { start: [0, 29], text: "" } + position: { start: [0, 24], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 25], text: div } + attributes: [] + children: [] + - name: "should throw for: {% for x in y %}
    {% endfor %}" + source: "{% for x in y %}
    {% endfor %}" + parse: [html] + error: + message: Attempting to close LiquidTag 'endfor' before HtmlElement 'div' was closed + start: [0, 16] + text:
    + - name: "should throw for: {% tablerow x in y %}
    {% endtablerow %}" + source: "{% tablerow x in y %}
    {% endtablerow %}" + parse: [html] + error: + message: Attempting to close LiquidTag 'endtablerow' before HtmlElement 'div' was closed + start: [0, 21] + text:
    + - name: 'should throw for: {% form "cart", cart %}
    {% endform %}' + source: '{% form "cart", cart %}
    {% endform %}' + parse: [html] + error: + message: Attempting to close LiquidTag 'endform' before HtmlElement 'div' was closed + start: [0, 23] + text:
    + - name: "should throw for: {% capture x %}
    {% endcapture %}" + source: "{% capture x %}
    {% endcapture %}" + parse: [html] + error: + message: Attempting to close LiquidTag 'endcapture' before HtmlElement 'div' was closed + start: [0, 15] + text:
    + - name: should produce HtmlDanglingMarkerClose for
    in if + source: "{% if cond %}
    {% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}
    {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 23], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}
    {% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 23], text: "" } + position: { start: [0, 13], text:
    } + children: + - type: HtmlDanglingMarkerClose + blockStartPosition: { start: [0, 13], text:
    } + position: { start: [0, 13], text:
    } + name: + - type: TextNode + value: section + position: { start: [0, 15], text: section } + - name: should produce HtmlDanglingMarkerClose in both if/else branches + source: "{% if cond %}{% else %}{% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}{% else %}{% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 36], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}{% else %}{% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 19], text: "" } + position: { start: [0, 13], text: } + children: + - type: HtmlDanglingMarkerClose + blockStartPosition: { start: [0, 13], text: } + position: { start: [0, 13], text: } + name: + - type: TextNode + value: div + position: { start: [0, 15], text: div } + - type: LiquidBranch + name: else + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 27], text: "" } + blockStartPosition: { start: [0, 19], text: "{% else %}" } + blockEndPosition: { start: [0, 36], text: "" } + position: { start: [0, 19], text: "{% else %}" } + children: + - type: HtmlDanglingMarkerClose + blockStartPosition: { start: [0, 29], text: } + position: { start: [0, 29], text: } + name: + - type: TextNode + value: main + position: { start: [0, 31], text: main } + - name: should throw for unclosed
    at document level + source:
    + parse: [html] + error: + message: Attempting to end parsing before HtmlElement 'div' was closed + start: [0, 0] + text:
    {% endif %}' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% if cond %}
    {% endif %}' } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 39], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: '{% if cond %}
    {% endif %}' } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 39], text: "" } + position: { start: [0, 13], text:
    } + children: + - type: HtmlElement + blockStartPosition: { start: [0, 13], text:
    } + blockEndPosition: { start: [0, 39], text: "" } + position: { start: [0, 13], text:
    } + name: + - type: TextNode + value: div + position: { start: [0, 14], text: div } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 25], text: foo } + position: { start: [0, 18], text: class="foo" } + name: + - type: TextNode + value: class + position: { start: [0, 18], text: class } + value: + - type: TextNode + value: foo + position: { start: [0, 25], text: foo } + - type: AttrDoubleQuoted + attributePosition: { start: [0, 34], text: bar } + position: { start: [0, 30], text: id="bar" } + name: + - type: TextNode + value: id + position: { start: [0, 30], text: id } + value: + - type: TextNode + value: bar + position: { start: [0, 34], text: bar } + children: [] + - name: should parse
    inside if as HtmlVoidElement + source: "{% if cond %}
    {% endif %}" + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: "{% if cond %}
    {% endif %}" } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 17], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: "{% if cond %}
    {% endif %}" } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 17], text: "" } + position: { start: [0, 13], text:
    } + children: + - type: HtmlVoidElement + name: br + blockStartPosition: { start: [0, 13], text:
    } + position: { start: [0, 13], text:
    } + attributes: [] + - name: should parse with attribute inside if as HtmlVoidElement + source: '{% if cond %}{% endif %}' + parse: [html] + ast: + type: Document + name: "#document" + position: { start: [0, 0], text: '{% if cond %}{% endif %}' } + children: + - type: LiquidTag + name: if + whitespaceStart: "" + whitespaceEnd: "" + delimiterWhitespaceStart: "" + delimiterWhitespaceEnd: "" + blockStartPosition: { start: [0, 0], text: "{% if cond %}" } + blockEndPosition: { start: [0, 26], text: "{% endif %}" } + markupPosition: { start: [0, 6], text: "cond " } + position: { start: [0, 0], text: '{% if cond %}{% endif %}' } + markup: + type: VariableLookup + name: cond + position: { start: [0, 6], text: cond } + lookups: [] + lookupModes: [] + children: + - type: LiquidBranch + name: null + markup: "" + whitespaceStart: "" + whitespaceEnd: "" + markupPosition: { start: [0, 13], text: "" } + blockStartPosition: { start: [0, 13], text: "" } + blockEndPosition: { start: [0, 26], text: "" } + position: { start: [0, 13], text: } + children: + - type: HtmlVoidElement + name: img + blockStartPosition: { start: [0, 13], text: } + position: { start: [0, 13], text: } + attributes: + - type: AttrDoubleQuoted + attributePosition: { start: [0, 23], text: x } + position: { start: [0, 18], text: src="x" } + name: + - type: TextNode + value: src + position: { start: [0, 18], text: src } + value: + - type: TextNode + value: x + position: { start: [0, 23], text: x }