Skip to content

Commit 606304f

Browse files
authored
Merge pull request #621 from Shopify/jm/doc_tag
Add basic {% doc %} tag parsing support
2 parents 7e1df75 + 59c43dc commit 606304f

File tree

6 files changed

+129
-1
lines changed

6 files changed

+129
-1
lines changed

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

+14
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Liquid <: Helpers {
3737
endOfIdentifier = endOfTagName | endOfVarName
3838

3939
liquidNode =
40+
| liquidDoc
4041
| liquidBlockComment
4142
| liquidRawTag
4243
| liquidDrop
@@ -216,6 +217,15 @@ Liquid <: Helpers {
216217
commentBlockStart = "{%" "-"? space* ("comment" endOfIdentifier) space* tagMarkup "-"? "%}"
217218
commentBlockEnd = "{%" "-"? space* ("endcomment" endOfIdentifier) space* tagMarkup "-"? "%}"
218219

220+
liquidDoc =
221+
liquidDocStart
222+
liquidDocBody
223+
liquidDocEnd
224+
225+
liquidDocStart = "{%" "-"? space* ("doc" endOfIdentifier) space* tagMarkup "-"? "%}"
226+
liquidDocEnd = "{%" "-"? space* ("enddoc" endOfIdentifier) space* tagMarkup "-"? "%}"
227+
liquidDocBody = anyExceptStar<(liquidDocStart | liquidDocEnd)>
228+
219229
// In order for the grammar to "fallback" to the base case, this
220230
// rule must pass if and only if we support what we parse. This
221231
// implies that—since we don't support filters yet—we have a
@@ -378,6 +388,10 @@ LiquidStatement <: Liquid {
378388
delimTag := liquidStatementEnd
379389
}
380390

391+
LiquidDoc <: Helpers {
392+
Node := (TextNode)*
393+
}
394+
381395
LiquidHTML <: Liquid {
382396
Node := yamlFrontmatter? (HtmlNode | liquidNode | TextNode)*
383397
openControl += "<"

packages/liquid-html-parser/src/grammar.ts

+2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import ohm from 'ohm-js';
33
export const liquidHtmlGrammars = ohm.grammars(require('../grammar/liquid-html.ohm.js'));
44

55
export const TextNodeGrammar = liquidHtmlGrammars['Helpers'];
6+
export const LiquidDocGrammar = liquidHtmlGrammars['LiquidDoc'];
67

78
export interface LiquidGrammars {
89
Liquid: ohm.Grammar;
@@ -52,4 +53,5 @@ export const TAGS_WITHOUT_MARKUP = [
5253
'continue',
5354
'comment',
5455
'raw',
56+
'doc',
5557
];

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

+28
Original file line numberDiff line numberDiff line change
@@ -982,6 +982,34 @@ describe('Unit: Stage 1 (CST)', () => {
982982
}
983983
});
984984

985+
it('should parse doc tags', () => {
986+
for (const { toCST, expectPath } of testCases) {
987+
const testStr = `{% doc -%} Renders loading-spinner. {%- enddoc %}`;
988+
989+
cst = toCST(testStr);
990+
expectPath(cst, '0.type').to.equal('LiquidRawTag');
991+
expectPath(cst, '0.name').to.equal('doc');
992+
expectPath(cst, '0.body').to.include('Renders loading-spinner');
993+
expectPath(cst, '0.whitespaceStart').to.equal('');
994+
expectPath(cst, '0.whitespaceEnd').to.equal('-');
995+
expectPath(cst, '0.delimiterWhitespaceStart').to.equal('-');
996+
expectPath(cst, '0.delimiterWhitespaceEnd').to.equal('');
997+
expectPath(cst, '0.blockStartLocStart').to.equal(0);
998+
expectPath(cst, '0.blockStartLocEnd').to.equal(0 + '{% doc -%}'.length);
999+
expectPath(cst, '0.blockEndLocStart').to.equal(testStr.length - '{%- enddoc %}'.length);
1000+
expectPath(cst, '0.blockEndLocEnd').to.equal(testStr.length);
1001+
expectPath(cst, '0.children').to.deep.equal([
1002+
{
1003+
locEnd: 35,
1004+
locStart: 11,
1005+
source: '{% doc -%} Renders loading-spinner. {%- enddoc %}',
1006+
type: 'TextNode',
1007+
value: 'Renders loading-spinner.',
1008+
},
1009+
]);
1010+
}
1011+
});
1012+
9851013
it('should parse tag open / close', () => {
9861014
BLOCKS.forEach((block: string) => {
9871015
for (const { toCST, expectPath } of testCases) {

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

+57
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { Parser } from 'prettier';
3434
import ohm, { Node } from 'ohm-js';
3535
import { toAST } from 'ohm-js/extras';
3636
import {
37+
LiquidDocGrammar,
3738
LiquidGrammars,
3839
TextNodeGrammar,
3940
placeholderGrammars,
@@ -626,6 +627,30 @@ function toCST<T>(
626627
blockEndLocStart: (tokens: Node[]) => tokens[2].source.startIdx,
627628
blockEndLocEnd: (tokens: Node[]) => tokens[2].source.endIdx,
628629
},
630+
liquidDoc: {
631+
type: ConcreteNodeTypes.LiquidRawTag,
632+
name: 'doc',
633+
body: (tokens: Node[]) => tokens[1].sourceString,
634+
children: (tokens: Node[]) => {
635+
const contentNode = tokens[1];
636+
return toLiquidDocAST(
637+
source,
638+
contentNode.sourceString,
639+
offset + contentNode.source.startIdx,
640+
);
641+
},
642+
whitespaceStart: (tokens: Node[]) => tokens[0].children[1].sourceString,
643+
whitespaceEnd: (tokens: Node[]) => tokens[0].children[7].sourceString,
644+
delimiterWhitespaceStart: (tokens: Node[]) => tokens[2].children[1].sourceString,
645+
delimiterWhitespaceEnd: (tokens: Node[]) => tokens[2].children[7].sourceString,
646+
locStart,
647+
locEnd,
648+
source,
649+
blockStartLocStart: (tokens: Node[]) => tokens[0].source.startIdx,
650+
blockStartLocEnd: (tokens: Node[]) => tokens[0].source.endIdx,
651+
blockEndLocStart: (tokens: Node[]) => tokens[2].source.startIdx,
652+
blockEndLocEnd: (tokens: Node[]) => tokens[2].source.endIdx,
653+
},
629654
liquidInlineComment: {
630655
type: ConcreteNodeTypes.LiquidTag,
631656
name: 3,
@@ -1262,3 +1287,35 @@ function toCST<T>(
12621287

12631288
return toAST(res, selectedMappings) as T;
12641289
}
1290+
1291+
/**
1292+
* Builds an AST for LiquidDoc content.
1293+
*
1294+
* `toCST` includes mappings and logic that are not needed for LiquidDoc so we're separating this logic
1295+
*/
1296+
function toLiquidDocAST(source: string, matchingSource: string, offset: number) {
1297+
// When we switch parser, our locStart and locEnd functions must account
1298+
// for the offset of the {% doc %} markup
1299+
const locStart = (tokens: Node[]) => offset + tokens[0].source.startIdx;
1300+
const locEnd = (tokens: Node[]) => offset + tokens[tokens.length - 1].source.endIdx;
1301+
1302+
const res = LiquidDocGrammar.match(matchingSource, 'Node');
1303+
if (res.failed()) {
1304+
throw new LiquidHTMLCSTParsingError(res);
1305+
}
1306+
1307+
const LiquidDocMappings: Mapping = {
1308+
Node: 0,
1309+
TextNode: {
1310+
type: ConcreteNodeTypes.TextNode,
1311+
value: function () {
1312+
return (this as any).sourceString;
1313+
},
1314+
locStart,
1315+
locEnd,
1316+
source,
1317+
},
1318+
};
1319+
1320+
return toAST(res, LiquidDocMappings);
1321+
}

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

+27
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,33 @@ describe('Unit: Stage 2 (AST)', () => {
12201220
expectPath(ast, 'children.0.markup.1.children.0.children.1.markup.name').to.eql('var3');
12211221
});
12221222

1223+
it(`should parse doc tags`, () => {
1224+
ast = toLiquidAST(`{% doc %}{% enddoc %}`);
1225+
expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
1226+
expectPath(ast, 'children.0.name').to.eql('doc');
1227+
expectPath(ast, 'children.0.markup').toEqual('');
1228+
expectPath(ast, 'children.0.body.value').to.eql('');
1229+
expectPath(ast, 'children.0.body.type').toEqual('RawMarkup');
1230+
expectPath(ast, 'children.0.body.nodes').toEqual([]);
1231+
1232+
ast = toLiquidAST(`{% doc -%} single line doc {%- enddoc %}`);
1233+
expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
1234+
expectPath(ast, 'children.0.name').to.eql('doc');
1235+
expectPath(ast, 'children.0.body.value').to.eql(' single line doc ');
1236+
expectPath(ast, 'children.0.body.nodes.0.type').toEqual('TextNode');
1237+
1238+
ast = toLiquidAST(`{% doc -%}
1239+
multi line doc
1240+
multi line doc
1241+
{%- enddoc %}`);
1242+
expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
1243+
expectPath(ast, 'children.0.name').to.eql('doc');
1244+
expectPath(ast, 'children.0.body.nodes.0.value').to.eql(
1245+
`multi line doc\n multi line doc`,
1246+
);
1247+
expectPath(ast, 'children.0.body.nodes.0.type').toEqual('TextNode');
1248+
});
1249+
12231250
it('should parse unclosed tables with assignments', () => {
12241251
ast = toLiquidAST(`
12251252
{%- liquid

packages/theme-check-common/src/checks/liquid-html-syntax-error/index.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ describe('Module: LiquidHTMLSyntaxError', () => {
8686
const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
8787
expect(offenses).to.have.length(1);
8888
expect(offenses[0].message).to.equal(
89-
`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"`,
89+
`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"`,
9090
);
9191
});
9292

0 commit comments

Comments
 (0)