diff --git a/packages/liquid-html-parser/grammar/liquid-html.ohm b/packages/liquid-html-parser/grammar/liquid-html.ohm
index a431a97be..6b1c4045a 100644
--- a/packages/liquid-html-parser/grammar/liquid-html.ohm
+++ b/packages/liquid-html-parser/grammar/liquid-html.ohm
@@ -37,6 +37,7 @@ Liquid <: Helpers {
endOfIdentifier = endOfTagName | endOfVarName
liquidNode =
+ | liquidDoc
| liquidBlockComment
| liquidRawTag
| liquidDrop
@@ -211,6 +212,15 @@ Liquid <: Helpers {
commentBlockStart = "{%" "-"? space* ("comment" endOfIdentifier) space* tagMarkup "-"? "%}"
commentBlockEnd = "{%" "-"? space* ("endcomment" endOfIdentifier) space* tagMarkup "-"? "%}"
+ liquidDoc =
+ liquidDocStart
+ liquidDocBody
+ liquidDocEnd
+
+ liquidDocStart = "{%" "-"? space* ("doc" endOfIdentifier) space* tagMarkup "-"? "%}"
+ liquidDocEnd = "{%" "-"? space* ("enddoc" endOfIdentifier) space* tagMarkup "-"? "%}"
+ liquidDocBody = anyExceptStar<(liquidDocStart | liquidDocEnd)>
+
// In order for the grammar to "fallback" to the base case, this
// rule must pass if and only if we support what we parse. This
// implies that—since we don't support filters yet—we have a
@@ -368,6 +378,10 @@ LiquidStatement <: Liquid {
delimTag := liquidStatementEnd
}
+LiquidDoc <: Helpers {
+ Node := (TextNode)*
+}
+
LiquidHTML <: Liquid {
Node := yamlFrontmatter? (HtmlNode | liquidNode | TextNode)*
openControl += "<"
diff --git a/packages/liquid-html-parser/src/grammar.ts b/packages/liquid-html-parser/src/grammar.ts
index ab6735060..18167b8e9 100644
--- a/packages/liquid-html-parser/src/grammar.ts
+++ b/packages/liquid-html-parser/src/grammar.ts
@@ -3,6 +3,7 @@ import ohm from 'ohm-js';
export const liquidHtmlGrammars = ohm.grammars(require('../grammar/liquid-html.ohm.js'));
export const TextNodeGrammar = liquidHtmlGrammars['Helpers'];
+export const LiquidDocGrammar = liquidHtmlGrammars['LiquidDoc'];
export interface LiquidGrammars {
Liquid: ohm.Grammar;
@@ -52,4 +53,5 @@ export const TAGS_WITHOUT_MARKUP = [
'continue',
'comment',
'raw',
+ 'doc',
];
diff --git a/packages/liquid-html-parser/src/stage-1-cst.spec.ts b/packages/liquid-html-parser/src/stage-1-cst.spec.ts
index 147916b23..71ae05a67 100644
--- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts
+++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts
@@ -978,6 +978,34 @@ describe('Unit: Stage 1 (CST)', () => {
}
});
+ it('should parse doc tags', () => {
+ for (const { toCST, expectPath } of testCases) {
+ const testStr = `{% doc -%} Renders loading-spinner. {%- enddoc %}`;
+
+ cst = toCST(testStr);
+ expectPath(cst, '0.type').to.equal('LiquidRawTag');
+ expectPath(cst, '0.name').to.equal('doc');
+ expectPath(cst, '0.body').to.include('Renders loading-spinner');
+ expectPath(cst, '0.whitespaceStart').to.equal('');
+ expectPath(cst, '0.whitespaceEnd').to.equal('-');
+ expectPath(cst, '0.delimiterWhitespaceStart').to.equal('-');
+ expectPath(cst, '0.delimiterWhitespaceEnd').to.equal('');
+ expectPath(cst, '0.blockStartLocStart').to.equal(0);
+ expectPath(cst, '0.blockStartLocEnd').to.equal(0 + '{% doc -%}'.length);
+ expectPath(cst, '0.blockEndLocStart').to.equal(testStr.length - '{%- enddoc %}'.length);
+ expectPath(cst, '0.blockEndLocEnd').to.equal(testStr.length);
+ expectPath(cst, '0.children').to.deep.equal([
+ {
+ locEnd: 25,
+ locStart: 1,
+ source: '{% doc -%} Renders loading-spinner. {%- enddoc %}',
+ type: 'TextNode',
+ value: 'Renders loading-spinner.',
+ },
+ ]);
+ }
+ });
+
it('should parse tag open / close', () => {
BLOCKS.forEach((block: string) => {
for (const { toCST, expectPath } of testCases) {
diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts
index 36c2ab853..2bb9bfa8f 100644
--- a/packages/liquid-html-parser/src/stage-1-cst.ts
+++ b/packages/liquid-html-parser/src/stage-1-cst.ts
@@ -34,6 +34,7 @@ import { Parser } from 'prettier';
import ohm, { Node } from 'ohm-js';
import { toAST } from 'ohm-js/extras';
import {
+ LiquidDocGrammar,
LiquidGrammars,
TextNodeGrammar,
placeholderGrammars,
@@ -625,6 +626,30 @@ function toCST(
blockEndLocStart: (tokens: Node[]) => tokens[2].source.startIdx,
blockEndLocEnd: (tokens: Node[]) => tokens[2].source.endIdx,
},
+ liquidDoc: {
+ type: ConcreteNodeTypes.LiquidRawTag,
+ name: 'doc',
+ body: (tokens: Node[]) => tokens[1].sourceString,
+ children: (tokens: Node[]) => {
+ const contentNode = tokens[1];
+ return toLiquidDocAST(
+ source,
+ contentNode.sourceString,
+ offset + contentNode.source.startIdx,
+ );
+ },
+ whitespaceStart: (tokens: Node[]) => tokens[0].children[1].sourceString,
+ whitespaceEnd: (tokens: Node[]) => tokens[0].children[7].sourceString,
+ delimiterWhitespaceStart: (tokens: Node[]) => tokens[2].children[1].sourceString,
+ delimiterWhitespaceEnd: (tokens: Node[]) => tokens[2].children[7].sourceString,
+ locStart,
+ locEnd,
+ source,
+ blockStartLocStart: (tokens: Node[]) => tokens[0].source.startIdx,
+ blockStartLocEnd: (tokens: Node[]) => tokens[0].source.endIdx,
+ blockEndLocStart: (tokens: Node[]) => tokens[2].source.startIdx,
+ blockEndLocEnd: (tokens: Node[]) => tokens[2].source.endIdx,
+ },
liquidInlineComment: {
type: ConcreteNodeTypes.LiquidTag,
name: 3,
@@ -1073,6 +1098,25 @@ function toCST(
},
};
+ const LiquidDocMappings: Mapping = {
+ Node: 0,
+ TextNode: textNode,
+ };
+
+ function toLiquidDocAST(source: string, matchingSource: string, offset: number) {
+ const res = LiquidDocGrammar.match(matchingSource, 'Node');
+ if (res.failed()) {
+ throw new LiquidHTMLCSTParsingError(res);
+ }
+
+ const LiquidDocMappings: Mapping = {
+ Node: 0,
+ TextNode: textNode,
+ };
+
+ return toAST(res, LiquidDocMappings);
+ }
+
const LiquidHTMLMappings: Mapping = {
Node(frontmatter: Node, nodes: Node) {
const self = this as any;
diff --git a/packages/liquid-html-parser/src/stage-2-ast.spec.ts b/packages/liquid-html-parser/src/stage-2-ast.spec.ts
index de6de2631..84848ccb2 100644
--- a/packages/liquid-html-parser/src/stage-2-ast.spec.ts
+++ b/packages/liquid-html-parser/src/stage-2-ast.spec.ts
@@ -1220,6 +1220,33 @@ describe('Unit: Stage 2 (AST)', () => {
expectPath(ast, 'children.0.markup.1.children.0.children.1.markup.name').to.eql('var3');
});
+ 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 -%} single line 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(' single line doc ');
+ expectPath(ast, 'children.0.body.nodes.0.type').toEqual('TextNode');
+
+ ast = toLiquidAST(`{% doc -%}
+ multi line doc
+ multi line doc
+ {%- enddoc %}`);
+ expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
+ expectPath(ast, 'children.0.name').to.eql('doc');
+ expectPath(ast, 'children.0.body.nodes.0.value').to.eql(
+ `multi line doc\n multi line doc`,
+ );
+ expectPath(ast, 'children.0.body.nodes.0.type').toEqual('TextNode');
+ });
+
it('should parse unclosed tables with assignments', () => {
ast = toLiquidAST(`
{%- liquid
diff --git a/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts b/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts
index d73d8ab01..bf1abbf74 100644
--- a/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts
+++ b/packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts
@@ -86,7 +86,7 @@ describe('Module: LiquidHTMLSyntaxError', () => {
const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
expect(offenses).to.have.length(1);
expect(offenses[0].message).to.equal(
- `SyntaxError: expected "#", a letter, "when", "sections", "section", "render", "liquid", "layout", "increment", "include", "elsif", "else", "echo", "decrement", "content_for", "cycle", "continue", "break", "assign", "tablerow", "unless", "if", "ifchanged", "for", "case", "capture", "paginate", "form", "end", "style", "stylesheet", "schema", "javascript", "raw", or "comment"`,
+ `SyntaxError: expected "#", a letter, "when", "sections", "section", "render", "liquid", "layout", "increment", "include", "elsif", "else", "echo", "decrement", "content_for", "cycle", "continue", "break", "assign", "tablerow", "unless", "if", "ifchanged", "for", "case", "capture", "paginate", "form", "end", "style", "stylesheet", "schema", "javascript", "raw", "comment", or "doc"`,
);
});