Skip to content

Commit 4bf7625

Browse files
authored
Merge pull request #665 from Shopify/jm/liquid_doc_param_description
Add parsing + prettier support for param name and description in doc tags
2 parents e7ed99d + dd1f0a1 commit 4bf7625

File tree

9 files changed

+171
-48
lines changed

9 files changed

+171
-48
lines changed

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

+3-2
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,9 @@ LiquidDoc <: Helpers {
395395
| fallbackNode
396396

397397
fallbackNode = "@" anyExceptStar<newline>
398-
paramNode = "@param" space* paramNodeName
399-
paramNodeName = anyExceptStar<newline>
398+
paramNode = "@param" space* (paramName)? (space+ (paramDescription))?
399+
paramName = identifierCharacter+
400+
paramDescription = (~newline identifierCharacter | space)+
400401
}
401402

402403
LiquidHTML <: Liquid {

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

+74-35
Original file line numberDiff line numberDiff line change
@@ -981,58 +981,97 @@ describe('Unit: Stage 1 (CST)', () => {
981981
expectPath(cst, '0.blockEndLocEnd').to.equal(testStr.length);
982982
}
983983
});
984+
});
984985

985-
it('should parse doc tags', () => {
986-
for (const { toCST, expectPath } of testCases) {
987-
const testStr = `{% doc -%}
988-
@param asdf
989-
@unsupported
990-
{%- enddoc %}`;
991-
986+
describe('Case: LiquidDoc', () => {
987+
for (const { toCST, expectPath } of testCases) {
988+
it('should parse basic doc tag structure', () => {
989+
const testStr = `{% doc -%} content {%- enddoc %}`;
992990
cst = toCST(testStr);
993991

994992
expectPath(cst, '0.type').to.equal('LiquidRawTag');
995993
expectPath(cst, '0.name').to.equal('doc');
996-
expectPath(cst, '0.body').to.include('@param asdf');
997994
expectPath(cst, '0.whitespaceStart').to.equal('');
998995
expectPath(cst, '0.whitespaceEnd').to.equal('-');
999996
expectPath(cst, '0.delimiterWhitespaceStart').to.equal('-');
1000997
expectPath(cst, '0.delimiterWhitespaceEnd').to.equal('');
1001-
expectPath(cst, '0.blockStartLocStart').to.equal(0);
1002-
expectPath(cst, '0.blockStartLocEnd').to.equal(0 + '{% doc -%}'.length);
998+
expectPath(cst, '0.blockStartLocStart').to.equal(testStr.indexOf('{% doc -%}'));
999+
expectPath(cst, '0.blockStartLocEnd').to.equal(
1000+
testStr.indexOf('{% doc -%}') + '{% doc -%}'.length,
1001+
);
10031002
expectPath(cst, '0.blockEndLocStart').to.equal(testStr.length - '{%- enddoc %}'.length);
10041003
expectPath(cst, '0.blockEndLocEnd').to.equal(testStr.length);
1004+
});
1005+
1006+
it('should parse @param with no name or description', () => {
1007+
const testStr = `{% doc %} @param {% enddoc %}`;
1008+
cst = toCST(testStr);
10051009

10061010
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1007-
expectPath(cst, '0.children.0.locStart').to.equal(testStr.indexOf('@param'));
1008-
expectPath(cst, '0.children.0.locEnd').to.equal(testStr.indexOf('asdf') + 'asdf'.length);
1011+
expectPath(cst, '0.children.0.value').to.equal('@param');
1012+
expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode');
1013+
expectPath(cst, '0.children.0.paramName.value').to.equal('');
1014+
expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode');
1015+
expectPath(cst, '0.children.0.paramDescription.value').to.equal('');
1016+
});
10091017

1010-
expectPath(cst, '0.children.1.type').to.equal('TextNode');
1011-
expectPath(cst, '0.children.1.locStart').to.equal(testStr.indexOf('@unsupported'));
1012-
expectPath(cst, '0.children.1.locEnd').to.equal(
1013-
testStr.indexOf('@unsupported') + '@unsupported'.length,
1018+
it('should parse @param with name but no description', () => {
1019+
const testStr = `{% doc %} @param paramWithNoDescription {% enddoc %}`;
1020+
cst = toCST(testStr);
1021+
1022+
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1023+
expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode');
1024+
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithNoDescription');
1025+
expectPath(cst, '0.children.0.paramName.locStart').to.equal(
1026+
testStr.indexOf('paramWithNoDescription'),
10141027
);
1015-
}
1016-
});
1028+
expectPath(cst, '0.children.0.paramName.locEnd').to.equal(
1029+
testStr.indexOf('paramWithNoDescription') + 'paramWithNoDescription'.length,
1030+
);
1031+
expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode');
1032+
expectPath(cst, '0.children.0.paramDescription.value').to.equal('');
1033+
});
10171034

1018-
it('should parse tag open / close', () => {
1019-
BLOCKS.forEach((block: string) => {
1020-
for (const { toCST, expectPath } of testCases) {
1021-
cst = toCST(`{% ${block} args -%}{%- end${block} %}`);
1022-
expectPath(cst, '0.type').to.equal('LiquidTagOpen', block);
1023-
expectPath(cst, '0.name').to.equal(block);
1024-
expectPath(cst, '0.whitespaceStart').to.equal(null);
1025-
expectPath(cst, '0.whitespaceEnd').to.equal('-');
1026-
if (!NamedTags.hasOwnProperty(block)) {
1027-
expectPath(cst, '0.markup').to.equal('args');
1028-
}
1029-
expectPath(cst, '1.type').to.equal('LiquidTagClose');
1030-
expectPath(cst, '1.name').to.equal(block);
1031-
expectPath(cst, '1.whitespaceStart').to.equal('-');
1032-
expectPath(cst, '1.whitespaceEnd').to.equal(null);
1033-
}
1035+
it('should parse @param with name and description', () => {
1036+
const testStr = `{% doc %} @param paramWithDescription param with description {% enddoc %}`;
1037+
cst = toCST(testStr);
1038+
1039+
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1040+
expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode');
1041+
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithDescription');
1042+
expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode');
1043+
expectPath(cst, '0.children.0.paramDescription.value').to.equal('param with description');
10341044
});
1035-
});
1045+
1046+
it('should parse unsupported doc tags as text nodes', () => {
1047+
const testStr = `{% doc %} @unsupported this tag is not supported {% enddoc %}`;
1048+
cst = toCST(testStr);
1049+
1050+
expectPath(cst, '0.children.0.type').to.equal('TextNode');
1051+
expectPath(cst, '0.children.0.value').to.equal('@unsupported this tag is not supported');
1052+
});
1053+
1054+
it('should parse multiple doc tags in sequence', () => {
1055+
const testStr = `{% doc %}
1056+
@param param1 first parameter
1057+
@param param2 second parameter
1058+
@unsupported
1059+
{% enddoc %}`;
1060+
1061+
cst = toCST(testStr);
1062+
1063+
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1064+
expectPath(cst, '0.children.0.paramName.value').to.equal('param1');
1065+
expectPath(cst, '0.children.0.paramDescription.value').to.equal('first parameter');
1066+
1067+
expectPath(cst, '0.children.1.type').to.equal('LiquidDocParamNode');
1068+
expectPath(cst, '0.children.1.paramName.value').to.equal('param2');
1069+
expectPath(cst, '0.children.1.paramDescription.value').to.equal('second parameter');
1070+
1071+
expectPath(cst, '0.children.2.type').to.equal('TextNode');
1072+
expectPath(cst, '0.children.2.value').to.equal('@unsupported');
1073+
});
1074+
}
10361075
});
10371076
});
10381077

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

+25-2
Original file line numberDiff line numberDiff line change
@@ -107,10 +107,13 @@ export interface ConcreteBasicNode<T> {
107107
locEnd: number;
108108
}
109109

110+
// todo: change param and description to concrete nodes
110111
export interface ConcreteLiquidDocParamNode
111112
extends ConcreteBasicNode<ConcreteNodeTypes.LiquidDocParamNode> {
112113
name: string;
113114
value: string;
115+
paramName: ConcreteTextNode;
116+
paramDescription: ConcreteTextNode;
114117
}
115118

116119
export interface ConcreteHtmlNodeBase<T> extends ConcreteBasicNode<T> {
@@ -1329,15 +1332,35 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number)
13291332
paramNode: {
13301333
type: ConcreteNodeTypes.LiquidDocParamNode,
13311334
name: 0,
1332-
value: 2,
1335+
value: 0,
13331336
locStart,
13341337
locEnd,
13351338
source,
1339+
paramName: function (nodes: Node[]) {
1340+
const nameNode = nodes[2];
1341+
return {
1342+
type: ConcreteNodeTypes.TextNode,
1343+
value: nameNode.sourceString.trim(),
1344+
source,
1345+
locStart: offset + nameNode.source.startIdx,
1346+
locEnd: offset + nameNode.source.endIdx,
1347+
};
1348+
},
1349+
paramDescription: function (nodes: Node[]) {
1350+
const descriptionNode = nodes[4];
1351+
return {
1352+
type: ConcreteNodeTypes.TextNode,
1353+
value: descriptionNode.sourceString.trim(),
1354+
source,
1355+
locStart: offset + descriptionNode.source.startIdx,
1356+
locEnd: offset + descriptionNode.source.endIdx,
1357+
};
1358+
},
13361359
},
13371360
fallbackNode: {
13381361
type: ConcreteNodeTypes.TextNode,
13391362
value: function () {
1340-
return (this as any).sourceString;
1363+
return (this as any).sourceString.trim();
13411364
},
13421365
locStart,
13431366
locEnd,

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

+16-2
Original file line numberDiff line numberDiff line change
@@ -1232,16 +1232,30 @@ describe('Unit: Stage 2 (AST)', () => {
12321232
ast = toLiquidAST(`
12331233
{% doc -%}
12341234
@param asdf
1235+
@param paramWithDescription param with description
12351236
@unsupported this node falls back to a text node
12361237
{%- enddoc %}
12371238
`);
12381239
expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
12391240
expectPath(ast, 'children.0.name').to.eql('doc');
12401241
expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode');
12411242
expectPath(ast, 'children.0.body.nodes.0.name').to.eql('@param');
1243+
expectPath(ast, 'children.0.body.nodes.0.paramName.type').to.eql('TextNode');
1244+
expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('asdf');
1245+
expectPath(ast, 'children.0.body.nodes.0.paramDescription.type').to.eql('TextNode');
1246+
expectPath(ast, 'children.0.body.nodes.0.paramDescription.value').to.eql('');
1247+
1248+
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocParamNode');
1249+
expectPath(ast, 'children.0.body.nodes.1.name').to.eql('@param');
1250+
expectPath(ast, 'children.0.body.nodes.1.paramName.type').to.eql('TextNode');
1251+
expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('paramWithDescription');
1252+
expectPath(ast, 'children.0.body.nodes.1.paramDescription.type').to.eql('TextNode');
1253+
expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql(
1254+
'param with description',
1255+
);
12421256

1243-
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('TextNode');
1244-
expectPath(ast, 'children.0.body.nodes.1.value').to.eql(
1257+
expectPath(ast, 'children.0.body.nodes.2.type').to.eql('TextNode');
1258+
expectPath(ast, 'children.0.body.nodes.2.value').to.eql(
12451259
'@unsupported this node falls back to a text node',
12461260
);
12471261
});

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

+14
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,8 @@ export interface TextNode extends ASTNode<NodeTypes.TextNode> {
758758
export interface LiquidDocParamNode extends ASTNode<NodeTypes.LiquidDocParamNode> {
759759
name: string;
760760
value: string;
761+
paramDescription: TextNode;
762+
paramName: TextNode;
761763
}
762764

763765
export interface ASTNode<T> {
@@ -1281,6 +1283,18 @@ function buildAst(
12811283
position: position(node),
12821284
source: node.source,
12831285
value: node.value,
1286+
paramName: {
1287+
type: NodeTypes.TextNode,
1288+
value: node.paramName.value,
1289+
position: position(node.paramName),
1290+
source: node.paramName.source,
1291+
},
1292+
paramDescription: {
1293+
type: NodeTypes.TextNode,
1294+
value: node.paramDescription.value,
1295+
position: position(node.paramDescription),
1296+
source: node.paramDescription.source,
1297+
},
12841298
});
12851299
break;
12861300
}

packages/prettier-plugin-liquid/src/printer/print/liquid.ts

+23-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
import { NodeTypes, NamedTags, isBranchedTag, RawMarkup } from '@shopify/liquid-html-parser';
1+
import {
2+
NodeTypes,
3+
NamedTags,
4+
isBranchedTag,
5+
RawMarkup,
6+
LiquidDocParamNode,
7+
} from '@shopify/liquid-html-parser';
28
import { Doc, doc } from 'prettier';
39

410
import {
@@ -497,7 +503,22 @@ export function printLiquidDoc(
497503
_args: LiquidPrinterArgs,
498504
) {
499505
const body = path.map((p: any) => print(p), 'nodes');
500-
return [indent([hardline, body]), hardline];
506+
return [indent([hardline, join(hardline, body)]), hardline];
507+
}
508+
509+
export function printLiquidDocParam(
510+
path: AstPath<LiquidDocParamNode>,
511+
_options: LiquidParserOptions,
512+
_print: LiquidPrinter,
513+
_args: LiquidPrinterArgs,
514+
): Doc {
515+
const node = path.getValue();
516+
return [
517+
node.name,
518+
' ',
519+
node.paramName.value,
520+
node.paramDescription.value ? ' ' + node.paramDescription.value : '',
521+
];
501522
}
502523

503524
function innerLeadingWhitespace(node: LiquidTag | LiquidBranch) {

packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
printLiquidRawTag,
4747
printLiquidTag,
4848
printLiquidVariableOutput,
49+
printLiquidDocParam,
4950
} from './print/liquid';
5051
import { printClosingTagSuffix, printOpeningTagPrefix } from './print/tag';
5152
import { bodyLines, hasLineBreakInRange, isEmpty, isTextLikeNode, reindent } from './utils';
@@ -555,7 +556,7 @@ function printNode(
555556
}
556557

557558
case NodeTypes.LiquidDocParamNode: {
558-
return [node.name, ' ', node.value];
559+
return printLiquidDocParam(path as AstPath<LiquidDocParamNode>, options, print, args);
559560
}
560561

561562
default: {
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
It should indent the body
22
{% doc %}
3-
@param body
3+
@param paramName param with description
44
{% enddoc %}
55

66
It should not dedent to root
77
{% doc %}
8-
@param body
8+
@param paramName param with description
9+
{% enddoc %}
10+
11+
It should trim whitespace between nodes
12+
{% doc %}
13+
@param paramName param with description
914
{% enddoc %}
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
It should indent the body
22
{% doc %}
3-
@param body
3+
@param paramName param with description
44
{% enddoc %}
55

66
It should not dedent to root
77
{% doc %}
8-
@param body
8+
@param paramName param with description
9+
{% enddoc %}
10+
11+
It should trim whitespace between nodes
12+
{% doc %}
13+
@param paramName param with description
914
{% enddoc %}

0 commit comments

Comments
 (0)