`).to.be
+ .true;
+ expectMatchSucceeded('
').to.be.true;
+ expectMatchSucceeded('
').to.be.true;
+ expectMatchSucceeded('<6h>').to.be.false;
+
+ function expectMatchSucceeded(text: string) {
+ const match = grammar.LiquidHTML.match(text, 'Node');
+ return expect(match.succeeded(), text);
+ }
+ });
- it('should parse or not parse {% liquid %} lines', () => {
- expectMatchSucceeded(`
- layout none
+ it('should parse or not parse {% liquid %} lines', () => {
+ expectMatchSucceeded(`
+ layout none
- paginate search.results by 28
- for item in search.results
- if item.object_type != 'product'
- continue
- endif
+ paginate search.results by 28
+ for item in search.results
+ if item.object_type != 'product'
+ continue
+ endif
- render 'product-item', product: item
- endfor
- endpaginate
- `).to.be.true;
+ render 'product-item', product: item
+ endfor
+ endpaginate
+ `).to.be.true;
- function expectMatchSucceeded(text: string) {
- const match = liquidStatementsGrammar.match(text.trimStart(), 'Node');
- return expect(match.succeeded());
- }
+ function expectMatchSucceeded(text: string) {
+ const match = grammar.LiquidStatement.match(text.trimStart(), 'Node');
+ return expect(match.succeeded(), text);
+ }
+ });
+ });
});
});
diff --git a/src/parser/grammar.ts b/src/parser/grammar.ts
index 4027678..c61ddf4 100644
--- a/src/parser/grammar.ts
+++ b/src/parser/grammar.ts
@@ -4,18 +4,26 @@ export const liquidHtmlGrammars = ohm.grammars(
require('../../grammar/liquid-html.ohm.js'),
);
-export const liquidGrammar = liquidHtmlGrammars['Liquid'];
-export const liquidHtmlGrammar = liquidHtmlGrammars['LiquidHTML'];
-export const liquidStatementsGrammar = liquidHtmlGrammars['LiquidStatement'];
+export const strictGrammars = {
+ Liquid: liquidHtmlGrammars['StrictLiquid'],
+ LiquidHTML: liquidHtmlGrammars['StrictLiquidHTML'],
+ LiquidStatement: liquidHtmlGrammars['StrictLiquidStatement'],
+};
+
+export const tolerantGrammars = {
+ Liquid: liquidHtmlGrammars['Liquid'],
+ LiquidHTML: liquidHtmlGrammars['LiquidHTML'],
+ LiquidStatement: liquidHtmlGrammars['LiquidStatement'],
+};
// see ../../grammar/liquid-html.ohm for full list
export const BLOCKS = (
- liquidHtmlGrammar.rules as any
+ strictGrammars.LiquidHTML.rules as any
).blockName.body.factors[0].terms.map((x: any) => x.obj) as string[];
// see ../../grammar/liquid-html.ohm for full list
export const VOID_ELEMENTS = (
- liquidHtmlGrammar.rules as any
+ strictGrammars.LiquidHTML.rules as any
).voidElementName.body.factors[0].terms.map(
(x: any) => x.args[0].obj,
) as string[];
diff --git a/src/parser/stage-1-cst.ts b/src/parser/stage-1-cst.ts
index 1f9e56d..0303dce 100644
--- a/src/parser/stage-1-cst.ts
+++ b/src/parser/stage-1-cst.ts
@@ -33,11 +33,7 @@
import { Parser } from 'prettier';
import ohm, { Node } from 'ohm-js';
import { toAST } from 'ohm-js/extras';
-import {
- liquidGrammar,
- liquidHtmlGrammar,
- liquidHtmlGrammars,
-} from '~/parser/grammar';
+import { strictGrammars, tolerantGrammars } from '~/parser/grammar';
import { LiquidHTMLCSTParsingError } from '~/parser/errors';
import { Comparators, NamedTags } from '~/types';
@@ -505,21 +501,45 @@ const markup = (i: number) => (tokens: Node[]) => tokens[i].sourceString.trim();
const markupTrimEnd = (i: number) => (tokens: Node[]) =>
tokens[i].sourceString.trimEnd();
-export function toLiquidHtmlCST(source: string): LiquidHtmlCST {
- return toCST(source, liquidHtmlGrammar, [
+export interface CSTBuildOptions {
+ /**
+ * 'strict' will disable the Liquid parsing base cases. Which means that we will
+ * throw an error if we can't parse the node `markup` properly.
+ *
+ * 'tolerant' is the default case so that prettier can pretty print nodes
+ * that it doesn't understand.
+ */
+ mode: 'strict' | 'tolerant';
+}
+
+export function toLiquidHtmlCST(
+ source: string,
+ options: CSTBuildOptions = { mode: 'tolerant' },
+): LiquidHtmlCST {
+ const grammars =
+ options.mode === 'tolerant' ? tolerantGrammars : strictGrammars;
+ const grammar = grammars.LiquidHTML;
+ return toCST(source, grammars, grammar, [
'HelperMappings',
'LiquidMappings',
'LiquidHTMLMappings',
]);
}
-export function toLiquidCST(source: string): LiquidCST {
- return toCST(source, liquidGrammar, ['HelperMappings', 'LiquidMappings']);
+export function toLiquidCST(
+ source: string,
+ options: CSTBuildOptions = { mode: 'tolerant' },
+): LiquidCST {
+ const grammars =
+ options.mode === 'tolerant' ? tolerantGrammars : strictGrammars;
+ const grammar = grammars.Liquid;
+ return toCST(source, grammars, grammar, ['HelperMappings', 'LiquidMappings']);
}
function toCST
(
source: string,
- cstGrammar: ohm.Grammar,
+ grammars: typeof strictGrammars,
+ grammar: ohm.Grammar,
cstMappings: ('HelperMappings' | 'LiquidMappings' | 'LiquidHTMLMappings')[],
): T {
// When we switch parser, our locStart and locEnd functions must account
@@ -542,7 +562,7 @@ function toCST(
source,
};
- const res = cstGrammar.match(source, 'Node');
+ const res = grammar.match(source, 'Node');
if (res.failed()) {
throw new LiquidHTMLCSTParsingError(res);
}
@@ -625,6 +645,7 @@ function toCST(
},
liquidTagOpen: 0,
+ liquidTagOpenStrict: 0,
liquidTagOpenBaseCase: 0,
liquidTagOpenRule: {
type: ConcreteNodeTypes.LiquidTagOpen,
@@ -658,6 +679,8 @@ function toCST(
locEnd,
source,
},
+ liquidTagBreak: 0,
+ liquidTagContinue: 0,
liquidTagOpenTablerow: 0,
liquidTagOpenPaginate: 0,
liquidTagOpenPaginateMarkup: {
@@ -676,6 +699,7 @@ function toCST(
liquidTagOpenIf: 0,
liquidTagOpenUnless: 0,
liquidTagElsif: 0,
+ liquidTagElse: 0,
liquidTagOpenConditionalMarkup: 0,
condition: {
type: ConcreteNodeTypes.Condition,
@@ -706,6 +730,7 @@ function toCST(
},
liquidTag: 0,
+ liquidTagStrict: 0,
liquidTagBaseCase: 0,
liquidTagAssign: 0,
liquidTagEcho: 0,
@@ -737,7 +762,7 @@ function toCST(
liquidTagLiquid: 0,
liquidTagLiquidMarkup(tagMarkup: Node) {
- const res = liquidHtmlGrammars['LiquidStatement'].match(
+ const res = grammars['LiquidStatement'].match(
tagMarkup.sourceString,
'Node',
);
diff --git a/src/parser/stage-2-ast.spec.ts b/src/parser/stage-2-ast.spec.ts
index 9336e50..cfded1b 100644
--- a/src/parser/stage-2-ast.spec.ts
+++ b/src/parser/stage-2-ast.spec.ts
@@ -571,7 +571,7 @@ describe('Unit: Stage 2 (AST)', () => {
});
it(`${title} - should parse liquid case as branches`, () => {
- ast = toAST(`{% case A %}{% when A %}A{% when "B" %}B{% else %}C{% endcase %}`);
+ 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');
@@ -599,6 +599,7 @@ describe('Unit: Stage 2 (AST)', () => {
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');
});
diff --git a/src/parser/stage-2-ast.ts b/src/parser/stage-2-ast.ts
index 3582271..127c2b8 100644
--- a/src/parser/stage-2-ast.ts
+++ b/src/parser/stage-2-ast.ts
@@ -532,7 +532,19 @@ export interface ASTNode {
source: string;
}
-interface AstBuildOptions {
+interface ASTBuildOptions {
+ /**
+ * Whether the parser should throw if the document node isn't closed
+ */
+ allowUnclosedDocumentNode: boolean;
+
+ /**
+ * 'strict' will disable the Liquid parsing base cases. Which means that we will
+ * throw an error if we can't parse the node `markup` properly.
+ *
+ * 'tolerant' is the default case so that prettier can pretty print nodes
+ * that it doesn't understand.
+ */
mode: 'strict' | 'tolerant';
}
@@ -562,12 +574,18 @@ function isConcreteLiquidBranchDisguisedAsTag(
);
}
-export function toLiquidAST(source: string) {
- const cst = toLiquidCST(source);
+export function toLiquidAST(
+ source: string,
+ options: ASTBuildOptions = {
+ allowUnclosedDocumentNode: true,
+ mode: 'tolerant',
+ },
+) {
+ const cst = toLiquidCST(source, { mode: options.mode });
const root: DocumentNode = {
type: NodeTypes.Document,
source: source,
- children: cstToAst(cst, { mode: 'tolerant' }),
+ children: cstToAst(cst, options),
name: '#document',
position: {
start: 0,
@@ -577,12 +595,18 @@ export function toLiquidAST(source: string) {
return root;
}
-export function toLiquidHtmlAST(source: string): DocumentNode {
+export function toLiquidHtmlAST(
+ source: string,
+ options: ASTBuildOptions = {
+ allowUnclosedDocumentNode: false,
+ mode: 'tolerant',
+ },
+): DocumentNode {
const cst = toLiquidHtmlCST(source);
const root: DocumentNode = {
type: NodeTypes.Document,
source: source,
- children: cstToAst(cst, { mode: 'strict' }),
+ children: cstToAst(cst, options),
name: '#document',
position: {
start: 0,
@@ -752,14 +776,13 @@ function getName(
export function cstToAst(
cst: LiquidHtmlCST | LiquidCST | ConcreteAttributeNode[],
- options: AstBuildOptions,
+ options: ASTBuildOptions,
): LiquidHtmlNode[] {
if (cst.length === 0) return [];
const builder = buildAst(cst, options);
- const isStrictParser = options.mode === 'strict';
- if (isStrictParser && builder.cursor.length !== 0) {
+ if (!options.allowUnclosedDocumentNode && builder.cursor.length !== 0) {
throw new LiquidHTMLASTParsingError(
`Attempting to end parsing before ${builder.parent?.type} '${getName(
builder.parent,
@@ -775,7 +798,7 @@ export function cstToAst(
function buildAst(
cst: LiquidHtmlCST | LiquidCST | ConcreteAttributeNode[],
- options: AstBuildOptions,
+ options: ASTBuildOptions,
) {
const builder = new ASTBuilder(cst[0].source);
@@ -985,14 +1008,14 @@ function toAttributePosition(
function toAttributeValue(
value: (ConcreteLiquidNode | ConcreteTextNode)[],
- options: AstBuildOptions,
+ options: ASTBuildOptions,
): (LiquidNode | TextNode)[] {
return cstToAst(value, options) as (LiquidNode | TextNode)[];
}
function toAttributes(
attrList: ConcreteAttributeNode[],
- options: AstBuildOptions,
+ options: ASTBuildOptions,
): AttributeNode[] {
return cstToAst(attrList, options) as AttributeNode[];
}
@@ -1026,7 +1049,7 @@ function liquidBranchBaseAttributes(
function toLiquidTag(
node: ConcreteLiquidTag | ConcreteLiquidTagOpen,
- options: AstBuildOptions & { isBlockTag: boolean },
+ options: ASTBuildOptions & { isBlockTag: boolean },
): LiquidTag | LiquidBranch {
if (typeof node.markup !== 'string') {
return toNamedLiquidTag(node as ConcreteLiquidTagNamed, options);
@@ -1047,7 +1070,7 @@ function toLiquidTag(
function toNamedLiquidTag(
node: ConcreteLiquidTagNamed | ConcreteLiquidTagOpenNamed,
- options: AstBuildOptions,
+ options: ASTBuildOptions,
): LiquidTagNamed | LiquidBranchNamed {
switch (node.name) {
case NamedTags.echo: {
@@ -1559,7 +1582,7 @@ function toNamedArgument(
function toHtmlElement(
node: ConcreteHtmlTagOpen,
- options: AstBuildOptions,
+ options: ASTBuildOptions,
): HtmlElement {
return {
type: NodeTypes.HtmlElement,
@@ -1575,7 +1598,7 @@ function toHtmlElement(
function toHtmlDanglingMarkerOpen(
node: ConcreteHtmlTagOpen,
- options: AstBuildOptions,
+ options: ASTBuildOptions,
): HtmlDanglingMarkerOpen {
return {
type: NodeTypes.HtmlDanglingMarkerOpen,
@@ -1589,7 +1612,7 @@ function toHtmlDanglingMarkerOpen(
function toHtmlDanglingMarkerClose(
node: ConcreteHtmlTagClose,
- options: AstBuildOptions,
+ options: ASTBuildOptions,
): HtmlDanglingMarkerClose {
return {
type: NodeTypes.HtmlDanglingMarkerClose,
@@ -1602,7 +1625,7 @@ function toHtmlDanglingMarkerClose(
function toHtmlVoidElement(
node: ConcreteHtmlVoidElement,
- options: AstBuildOptions,
+ options: ASTBuildOptions,
): HtmlVoidElement {
return {
type: NodeTypes.HtmlVoidElement,
@@ -1616,7 +1639,7 @@ function toHtmlVoidElement(
function toHtmlSelfClosingElement(
node: ConcreteHtmlSelfClosingElement,
- options: AstBuildOptions,
+ options: ASTBuildOptions,
): HtmlSelfClosingElement {
return {
type: NodeTypes.HtmlSelfClosingElement,
diff --git a/test/liquid-tag-break/fixed.liquid b/test/liquid-tag-break/fixed.liquid
new file mode 100644
index 0000000..4a941e3
--- /dev/null
+++ b/test/liquid-tag-break/fixed.liquid
@@ -0,0 +1,17 @@
+It should strip extraneous whitespace
+printWidth: 1
+{% for i in col %}
+ {% break %}
+{% endfor %}
+
+It should support missing whitespace
+printWidth: 1
+{% for i in col %}
+ {%- break -%}
+{% endfor %}
+
+It should strip extraneous args
+printWidth: 1
+{% for i in col %}
+ {% break %}
+{% endfor %}
diff --git a/test/liquid-tag-break/index.liquid b/test/liquid-tag-break/index.liquid
new file mode 100644
index 0000000..1359a72
--- /dev/null
+++ b/test/liquid-tag-break/index.liquid
@@ -0,0 +1,11 @@
+It should strip extraneous whitespace
+printWidth: 1
+{% for i in col %} {% break %} {% endfor %}
+
+It should support missing whitespace
+printWidth: 1
+{% for i in col %} {%-break-%} {% endfor %}
+
+It should strip extraneous args
+printWidth: 1
+{% for i in col %} {% break huh?? %} {% endfor %}
diff --git a/test/liquid-tag-break/index.spec.ts b/test/liquid-tag-break/index.spec.ts
new file mode 100644
index 0000000..4587999
--- /dev/null
+++ b/test/liquid-tag-break/index.spec.ts
@@ -0,0 +1,6 @@
+import { assertFormattedEqualsFixed } from '../test-helpers';
+import * as path from 'path';
+
+describe(`Unit: ${path.basename(__dirname)}`, () => {
+ assertFormattedEqualsFixed(__dirname);
+});
diff --git a/test/liquid-tag-continue/fixed.liquid b/test/liquid-tag-continue/fixed.liquid
new file mode 100644
index 0000000..7898077
--- /dev/null
+++ b/test/liquid-tag-continue/fixed.liquid
@@ -0,0 +1,17 @@
+It should strip extraneous whitespace
+printWidth: 1
+{% for i in col %}
+ {% continue %}
+{% endfor %}
+
+It should support missing whitespace
+printWidth: 1
+{% for i in col %}
+ {%- continue -%}
+{% endfor %}
+
+It should strip extraneous args
+printWidth: 1
+{% for i in col %}
+ {% continue %}
+{% endfor %}
diff --git a/test/liquid-tag-continue/index.liquid b/test/liquid-tag-continue/index.liquid
new file mode 100644
index 0000000..46fc2f8
--- /dev/null
+++ b/test/liquid-tag-continue/index.liquid
@@ -0,0 +1,11 @@
+It should strip extraneous whitespace
+printWidth: 1
+{% for i in col %} {% continue %} {% endfor %}
+
+It should support missing whitespace
+printWidth: 1
+{% for i in col %} {%-continue-%} {% endfor %}
+
+It should strip extraneous args
+printWidth: 1
+{% for i in col %} {% continue huh?? %} {% endfor %}
diff --git a/test/liquid-tag-continue/index.spec.ts b/test/liquid-tag-continue/index.spec.ts
new file mode 100644
index 0000000..4587999
--- /dev/null
+++ b/test/liquid-tag-continue/index.spec.ts
@@ -0,0 +1,6 @@
+import { assertFormattedEqualsFixed } from '../test-helpers';
+import * as path from 'path';
+
+describe(`Unit: ${path.basename(__dirname)}`, () => {
+ assertFormattedEqualsFixed(__dirname);
+});
diff --git a/test/liquid-tag-else/fixed.liquid b/test/liquid-tag-else/fixed.liquid
new file mode 100644
index 0000000..d873094
--- /dev/null
+++ b/test/liquid-tag-else/fixed.liquid
@@ -0,0 +1,20 @@
+It should strip extraneous whitespace
+printWidth: 1
+{% if false %}
+{% else %}
+ hello
+{% endif %}
+
+It should support missing whitespace
+printWidth: 1
+{% if false %}
+{%- else -%}
+ hello
+{% endif %}
+
+It should strip extraneous args
+printWidth: 1
+{% if false %}
+{% else %}
+ hello
+{% endif %}
diff --git a/test/liquid-tag-else/index.liquid b/test/liquid-tag-else/index.liquid
new file mode 100644
index 0000000..eb2e6cf
--- /dev/null
+++ b/test/liquid-tag-else/index.liquid
@@ -0,0 +1,11 @@
+It should strip extraneous whitespace
+printWidth: 1
+{% if false %} {% else %} hello {% endif %}
+
+It should support missing whitespace
+printWidth: 1
+{% if false %} {%-else-%} hello {% endif %}
+
+It should strip extraneous args
+printWidth: 1
+{% if false %} {% else huh?? %} hello {% endif %}
diff --git a/test/liquid-tag-else/index.spec.ts b/test/liquid-tag-else/index.spec.ts
new file mode 100644
index 0000000..4587999
--- /dev/null
+++ b/test/liquid-tag-else/index.spec.ts
@@ -0,0 +1,6 @@
+import { assertFormattedEqualsFixed } from '../test-helpers';
+import * as path from 'path';
+
+describe(`Unit: ${path.basename(__dirname)}`, () => {
+ assertFormattedEqualsFixed(__dirname);
+});