Skip to content

Commit 26abb91

Browse files
committed
Parsing support for optional param delimiters []
---- - Added a parameter `required` to the LiquidDocParamNode - Parameters with `[]` around the name will return `required: true` - Parameters with incomplete delimiters `e.g. ([missingTail)` will map to a `TextNode`
1 parent 5db0986 commit 26abb91

File tree

6 files changed

+234
-27
lines changed

6 files changed

+234
-27
lines changed

.changeset/smart-onions-pump.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@shopify/liquid-html-parser': minor
3+
---
4+
5+
Add parsing support for optional param delimiters `[]`. Parameters with `[]` around a valid param name will be considered optional.
6+
Param names can be any alphanumeric character, `-`, or `_`.

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

+8-3
Original file line numberDiff line numberDiff line change
@@ -400,11 +400,16 @@ LiquidDoc <: Helpers {
400400
// We use this as an escape hatch to stop matching TextNode and try again when one of these characters is encountered
401401
openControl:= "@" | end
402402

403-
paramNode = "@param" strictSpace* paramType? strictSpace* paramName (strictSpace* "-")? strictSpace* paramDescription
403+
fallbackNode = "@" anyExceptStar<endOfParam>
404+
paramNode = "@param" strictSpace* paramType? strictSpace* (optionalParamName | paramName) (strictSpace* "-")? strictSpace* paramDescription
404405
paramType = "{" strictSpace* paramTypeContent strictSpace* "}"
405406
paramTypeContent = anyExceptStar<("}"| strictSpace)>
406-
paramName = identifierCharacter+
407-
paramDescription = anyExceptStar<endOfParam>
407+
408+
paramName = textValue
409+
optionalParamName = "[" strictSpace* textValue strictSpace* "]"
410+
textValue = identifierCharacter+
411+
412+
paramDescription = (~"]" anyExceptStar<endOfParam>)
408413
endOfParam = strictSpace* (newline | end)
409414

410415
exampleNode = "@example" strictSpace* exampleContent

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

+141-11
Original file line numberDiff line numberDiff line change
@@ -1011,31 +1011,154 @@ describe('Unit: Stage 1 (CST)', () => {
10111011
expectPath(cst, '0.children.0.value').to.equal('@param');
10121012
});
10131013

1014-
it('should parse @param with name', () => {
1014+
it('should parse required @param with name', () => {
10151015
const testStr = `{% doc %} @param paramWithNoDescription {% enddoc %}`;
10161016
cst = toCST(testStr);
10171017

10181018
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1019-
expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode');
1020-
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithNoDescription');
1021-
expectPath(cst, '0.children.0.paramName.locStart').to.equal(
1019+
expectPath(cst, '0.children.0.paramName.type').to.equal('LiquidDocParamNameNode');
1020+
expectPath(cst, '0.children.0.paramName.required').to.equal(true);
1021+
expectPath(cst, '0.children.0.paramName.paramNameContent.value').to.equal(
1022+
'paramWithNoDescription',
1023+
);
1024+
expectPath(cst, '0.children.0.paramName.paramNameContent.locStart').to.equal(
10221025
testStr.indexOf('paramWithNoDescription'),
10231026
);
10241027
expectPath(cst, '0.children.0.paramName.locEnd').to.equal(
10251028
testStr.indexOf('paramWithNoDescription') + 'paramWithNoDescription'.length,
10261029
);
1030+
1031+
expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode');
1032+
expectPath(cst, '0.children.0.paramDescription.value').to.equal('');
1033+
});
1034+
1035+
it('should parse an optional @param', () => {
1036+
const testStr = `{% doc %}
1037+
@param [paramWithNoDescription]
1038+
@param [ paramWithWhitespace ]
1039+
@param {String} [optionalParam] - The optional param
1040+
@param {String} [paramWithType]
1041+
{% enddoc %}`;
1042+
cst = toCST(testStr);
1043+
1044+
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1045+
expectPath(cst, '0.children.0.paramName.type').to.equal('LiquidDocParamNameNode');
1046+
expectPath(cst, '0.children.0.paramName.required').to.equal(false);
1047+
expectPath(cst, '0.children.0.paramName.paramNameContent.value').to.equal(
1048+
'paramWithNoDescription',
1049+
);
1050+
expectPath(cst, '0.children.0.paramName.paramNameContent.locStart').to.equal(
1051+
testStr.indexOf('paramWithNoDescription'),
1052+
);
1053+
expectPath(cst, '0.children.0.paramName.paramNameContent.locEnd').to.equal(
1054+
testStr.indexOf('paramWithNoDescription') + 'paramWithNoDescription'.length,
1055+
);
10271056
expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode');
10281057
expectPath(cst, '0.children.0.paramDescription.value').to.equal('');
1058+
1059+
expectPath(cst, '0.children.1.type').to.equal('LiquidDocParamNode');
1060+
expectPath(cst, '0.children.1.paramName.type').to.equal('LiquidDocParamNameNode');
1061+
expectPath(cst, '0.children.1.paramName.required').to.equal(false);
1062+
expectPath(cst, '0.children.1.paramName.paramNameContent.value').to.equal(
1063+
'paramWithWhitespace',
1064+
);
1065+
expectPath(cst, '0.children.1.paramName.paramNameContent.locStart').to.equal(
1066+
testStr.indexOf('paramWithWhitespace'),
1067+
);
1068+
expectPath(cst, '0.children.1.paramName.paramNameContent.locEnd').to.equal(
1069+
testStr.indexOf('paramWithWhitespace') + 'paramWithWhitespace'.length,
1070+
);
1071+
1072+
expectPath(cst, '0.children.2.type').to.equal('LiquidDocParamNode');
1073+
expectPath(cst, '0.children.2.paramName.type').to.equal('LiquidDocParamNameNode');
1074+
expectPath(cst, '0.children.2.paramName.required').to.equal(false);
1075+
expectPath(cst, '0.children.2.paramType.type').to.equal('TextNode');
1076+
expectPath(cst, '0.children.2.paramType.value').to.equal('String');
1077+
expectPath(cst, '0.children.2.paramDescription.type').to.equal('TextNode');
1078+
expectPath(cst, '0.children.2.paramDescription.value').to.equal('The optional param');
1079+
1080+
expectPath(cst, '0.children.3.type').to.equal('LiquidDocParamNode');
1081+
expectPath(cst, '0.children.3.paramName.type').to.equal('LiquidDocParamNameNode');
1082+
expectPath(cst, '0.children.3.paramName.required').to.equal(false);
1083+
expectPath(cst, '0.children.3.paramType.value').to.equal('String');
1084+
expectPath(cst, '0.children.3.paramDescription.value').to.equal('');
1085+
});
1086+
1087+
it('should parse @param with malformed optional delimiters as Text Nodes', () => {
1088+
const testStr = `{% doc %}
1089+
@param paramWithMissingHeadDelim]
1090+
@param [paramWithMissingTailDelim
1091+
@param missingHeadWithDescription] - description value
1092+
@param [missingTailWithDescription - description value
1093+
@param [too many words] description
1094+
{% enddoc %}`;
1095+
cst = toCST(testStr);
1096+
1097+
expectPath(cst, '0.children.0.type').to.equal('TextNode');
1098+
expectPath(cst, '0.children.0.value').to.equal('@param paramWithMissingHeadDelim]');
1099+
expectPath(cst, '0.children.0.locStart').to.equal(
1100+
testStr.indexOf('@param paramWithMissingHeadDelim]'),
1101+
);
1102+
expectPath(cst, '0.children.0.locEnd').to.equal(
1103+
testStr.indexOf('@param paramWithMissingHeadDelim]') +
1104+
'@param paramWithMissingHeadDelim]'.length,
1105+
);
1106+
1107+
expectPath(cst, '0.children.1.type').to.equal('TextNode');
1108+
expectPath(cst, '0.children.1.value').to.equal('@param [paramWithMissingTailDelim');
1109+
expectPath(cst, '0.children.1.locStart').to.equal(
1110+
testStr.indexOf('@param [paramWithMissingTailDelim'),
1111+
);
1112+
expectPath(cst, '0.children.1.locEnd').to.equal(
1113+
testStr.indexOf('@param [paramWithMissingTailDelim') +
1114+
'@param [paramWithMissingTailDelim'.length,
1115+
);
1116+
1117+
expectPath(cst, '0.children.2.type').to.equal('TextNode');
1118+
expectPath(cst, '0.children.2.value').to.equal(
1119+
'@param missingHeadWithDescription] - description value',
1120+
);
1121+
expectPath(cst, '0.children.2.locStart').to.equal(
1122+
testStr.indexOf('@param missingHeadWithDescription] - description value'),
1123+
);
1124+
expectPath(cst, '0.children.2.locEnd').to.equal(
1125+
testStr.indexOf('@param missingHeadWithDescription] - description value') +
1126+
'@param missingHeadWithDescription] - description value'.length,
1127+
);
1128+
1129+
expectPath(cst, '0.children.3.type').to.equal('TextNode');
1130+
expectPath(cst, '0.children.3.value').to.equal(
1131+
'@param [missingTailWithDescription - description value',
1132+
);
1133+
expectPath(cst, '0.children.3.locStart').to.equal(
1134+
testStr.indexOf('@param [missingTailWithDescription - description value'),
1135+
);
1136+
expectPath(cst, '0.children.3.locEnd').to.equal(
1137+
testStr.indexOf('@param [missingTailWithDescription - description value') +
1138+
'@param [missingTailWithDescription - description value'.length,
1139+
);
1140+
1141+
expectPath(cst, '0.children.4.type').to.equal('TextNode');
1142+
expectPath(cst, '0.children.4.value').to.equal('@param [too many words] description');
10291143
});
10301144

10311145
it('should parse @param with name and description', () => {
10321146
const testStr = `{% doc %} @param paramWithDescription param with description {% enddoc %}`;
10331147
cst = toCST(testStr);
10341148

10351149
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1036-
expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode');
1037-
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithDescription');
1038-
expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode');
1150+
expectPath(cst, '0.children.0.paramName.type').to.equal('LiquidDocParamNameNode');
1151+
expectPath(cst, '0.children.0.paramName.required').to.equal(true);
1152+
expectPath(cst, '0.children.0.paramName.paramNameContent.type').to.equal('TextNode');
1153+
expectPath(cst, '0.children.0.paramName.paramNameContent.value').to.equal(
1154+
'paramWithDescription',
1155+
);
1156+
expectPath(cst, '0.children.0.paramName.paramNameContent.locStart').to.equal(
1157+
testStr.indexOf('paramWithDescription'),
1158+
);
1159+
expectPath(cst, '0.children.0.paramName.paramNameContent.locEnd').to.equal(
1160+
testStr.indexOf('paramWithDescription') + 'paramWithDescription'.length,
1161+
);
10391162
expectPath(cst, '0.children.0.paramDescription.value').to.equal('param with description');
10401163
});
10411164

@@ -1044,7 +1167,12 @@ describe('Unit: Stage 1 (CST)', () => {
10441167
cst = toCST(testStr);
10451168

10461169
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1047-
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithType');
1170+
expectPath(cst, '0.children.0.paramName.type').to.equal('LiquidDocParamNameNode');
1171+
expectPath(cst, '0.children.0.paramName.required').to.equal(true);
1172+
expectPath(cst, '0.children.0.paramName.paramNameContent.type').to.equal('TextNode');
1173+
expectPath(cst, '0.children.0.paramName.paramNameContent.value').to.equal(
1174+
'paramWithType',
1175+
);
10481176

10491177
expectPath(cst, '0.children.0.paramType.type').to.equal('TextNode');
10501178
expectPath(cst, '0.children.0.paramType.value').to.equal('String');
@@ -1059,7 +1187,9 @@ describe('Unit: Stage 1 (CST)', () => {
10591187
cst = toCST(testStr);
10601188

10611189
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1062-
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithType');
1190+
expectPath(cst, '0.children.0.paramName.paramNameContent.value').to.equal(
1191+
'paramWithType',
1192+
);
10631193

10641194
expectPath(cst, '0.children.0.paramType.type').to.equal('TextNode');
10651195
expectPath(cst, '0.children.0.paramType.value').to.equal('String');
@@ -1098,11 +1228,11 @@ describe('Unit: Stage 1 (CST)', () => {
10981228
cst = toCST(testStr);
10991229

11001230
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1101-
expectPath(cst, '0.children.0.paramName.value').to.equal('param1');
1231+
expectPath(cst, '0.children.0.paramName.paramNameContent.value').to.equal('param1');
11021232
expectPath(cst, '0.children.0.paramDescription.value').to.equal('first parameter');
11031233

11041234
expectPath(cst, '0.children.1.type').to.equal('LiquidDocParamNode');
1105-
expectPath(cst, '0.children.1.paramName.value').to.equal('param2');
1235+
expectPath(cst, '0.children.1.paramName.paramNameContent.value').to.equal('param2');
11061236
expectPath(cst, '0.children.1.paramDescription.value').to.equal('second parameter');
11071237

11081238
expectPath(cst, '0.children.2.type').to.equal('TextNode');

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

+28-2
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ export enum ConcreteNodeTypes {
8585
ContentForNamedArgument = 'ContentForNamedArgument',
8686

8787
LiquidDocParamNode = 'LiquidDocParamNode',
88+
LiquidDocParamNameNode = 'LiquidDocParamNameNode',
8889
LiquidDocExampleNode = 'LiquidDocExampleNode',
8990
}
9091

@@ -111,11 +112,18 @@ export interface ConcreteBasicNode<T> {
111112
export interface ConcreteLiquidDocParamNode
112113
extends ConcreteBasicNode<ConcreteNodeTypes.LiquidDocParamNode> {
113114
name: 'param';
114-
paramName: ConcreteTextNode;
115+
paramName: ConcreteLiquidDocParamNameNode;
115116
paramDescription: ConcreteTextNode | null;
116117
paramType: ConcreteTextNode | null;
117118
}
118119

120+
export interface ConcreteLiquidDocParamNameNode
121+
extends ConcreteBasicNode<ConcreteNodeTypes.LiquidDocParamNameNode> {
122+
name: 'paramName';
123+
paramNameContent: ConcreteTextNode;
124+
required: boolean;
125+
}
126+
119127
export interface ConcreteLiquidDocExampleNode
120128
extends ConcreteBasicNode<ConcreteNodeTypes.LiquidDocExampleNode> {
121129
name: 'example';
@@ -1351,7 +1359,24 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number)
13511359
},
13521360
paramType: 2,
13531361
paramTypeContent: textNode,
1354-
paramName: textNode,
1362+
paramName: {
1363+
type: ConcreteNodeTypes.LiquidDocParamNameNode,
1364+
paramNameContent: 0,
1365+
locStart,
1366+
locEnd,
1367+
source,
1368+
required: true,
1369+
},
1370+
optionalParamName: {
1371+
type: ConcreteNodeTypes.LiquidDocParamNameNode,
1372+
paramNameContent: 2,
1373+
locStart,
1374+
locEnd,
1375+
source,
1376+
required: false,
1377+
},
1378+
optionalParamNameContent: textNode,
1379+
paramNameContent: textNode,
13551380
paramDescription: textNode,
13561381
exampleNode: {
13571382
type: ConcreteNodeTypes.LiquidDocExampleNode,
@@ -1362,6 +1387,7 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number)
13621387
exampleContent: 2,
13631388
},
13641389
exampleContent: textNode,
1390+
textValue: textNode,
13651391
fallbackNode: textNode,
13661392
};
13671393

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

+46-4
Original file line numberDiff line numberDiff line change
@@ -1231,9 +1231,12 @@ describe('Unit: Stage 2 (AST)', () => {
12311231

12321232
ast = toLiquidAST(`
12331233
{% doc -%}
1234-
@param paramWithNoType
1234+
@param requiredParamWithNoType
12351235
@param {String} paramWithDescription - param with description and \`punctation\`. This is still a valid param description.
12361236
@param {String} paramWithNoDescription
1237+
@param {String} [optionalParameterWithTypeAndDescription] - optional parameter with type and description
1238+
@param [optionalParameterWithDescription] - optional parameter description
1239+
@param {String} [optionalParameterWithType]
12371240
@unsupported this node falls back to a text node
12381241
{%- enddoc %}
12391242
`);
@@ -1242,13 +1245,15 @@ describe('Unit: Stage 2 (AST)', () => {
12421245

12431246
expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode');
12441247
expectPath(ast, 'children.0.body.nodes.0.name').to.eql('param');
1248+
expectPath(ast, 'children.0.body.nodes.0.required').to.eql(true);
12451249
expectPath(ast, 'children.0.body.nodes.0.paramName.type').to.eql('TextNode');
1246-
expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('paramWithNoType');
1250+
expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('requiredParamWithNoType');
12471251
expectPath(ast, 'children.0.body.nodes.0.paramType').to.be.null;
12481252
expectPath(ast, 'children.0.body.nodes.0.paramDescription').to.be.null;
12491253

12501254
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocParamNode');
12511255
expectPath(ast, 'children.0.body.nodes.1.name').to.eql('param');
1256+
expectPath(ast, 'children.0.body.nodes.1.required').to.eql(true);
12521257
expectPath(ast, 'children.0.body.nodes.1.paramName.type').to.eql('TextNode');
12531258
expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('paramWithDescription');
12541259
expectPath(ast, 'children.0.body.nodes.1.paramDescription.type').to.eql('TextNode');
@@ -1266,8 +1271,45 @@ describe('Unit: Stage 2 (AST)', () => {
12661271
expectPath(ast, 'children.0.body.nodes.2.paramType.type').to.eql('TextNode');
12671272
expectPath(ast, 'children.0.body.nodes.2.paramType.value').to.eql('String');
12681273

1269-
expectPath(ast, 'children.0.body.nodes.3.type').to.eql('TextNode');
1270-
expectPath(ast, 'children.0.body.nodes.3.value').to.eql(
1274+
expectPath(ast, 'children.0.body.nodes.3.type').to.eql('LiquidDocParamNode');
1275+
expectPath(ast, 'children.0.body.nodes.3.name').to.eql('param');
1276+
expectPath(ast, 'children.0.body.nodes.3.required').to.eql(false);
1277+
expectPath(ast, 'children.0.body.nodes.3.paramName.type').to.eql('TextNode');
1278+
expectPath(ast, 'children.0.body.nodes.3.paramName.value').to.eql(
1279+
'optionalParameterWithTypeAndDescription',
1280+
);
1281+
expectPath(ast, 'children.0.body.nodes.3.paramDescription.value').to.eql(
1282+
'optional parameter with type and description',
1283+
);
1284+
expectPath(ast, 'children.0.body.nodes.3.paramType.type').to.eql('TextNode');
1285+
expectPath(ast, 'children.0.body.nodes.3.paramType.value').to.eql('String');
1286+
1287+
expectPath(ast, 'children.0.body.nodes.4.type').to.eql('LiquidDocParamNode');
1288+
expectPath(ast, 'children.0.body.nodes.4.name').to.eql('param');
1289+
expectPath(ast, 'children.0.body.nodes.4.required').to.eql(false);
1290+
expectPath(ast, 'children.0.body.nodes.4.paramName.type').to.eql('TextNode');
1291+
expectPath(ast, 'children.0.body.nodes.4.paramName.value').to.eql(
1292+
'optionalParameterWithDescription',
1293+
);
1294+
expectPath(ast, 'children.0.body.nodes.4.paramDescription.type').to.eql('TextNode');
1295+
expectPath(ast, 'children.0.body.nodes.4.paramDescription.value').to.eql(
1296+
'optional parameter description',
1297+
);
1298+
expectPath(ast, 'children.0.body.nodes.4.paramType').to.be.null;
1299+
1300+
expectPath(ast, 'children.0.body.nodes.5.type').to.eql('LiquidDocParamNode');
1301+
expectPath(ast, 'children.0.body.nodes.5.name').to.eql('param');
1302+
expectPath(ast, 'children.0.body.nodes.5.required').to.eql(false);
1303+
expectPath(ast, 'children.0.body.nodes.5.paramName.type').to.eql('TextNode');
1304+
expectPath(ast, 'children.0.body.nodes.5.paramName.value').to.eql(
1305+
'optionalParameterWithType',
1306+
);
1307+
expectPath(ast, 'children.0.body.nodes.5.paramDescription').to.be.null;
1308+
expectPath(ast, 'children.0.body.nodes.5.paramType.type').to.eql('TextNode');
1309+
expectPath(ast, 'children.0.body.nodes.5.paramType.value').to.eql('String');
1310+
1311+
expectPath(ast, 'children.0.body.nodes.6.type').to.eql('TextNode');
1312+
expectPath(ast, 'children.0.body.nodes.6.value').to.eql(
12711313
'@unsupported this node falls back to a text node',
12721314
);
12731315

0 commit comments

Comments
 (0)