Skip to content

Commit 8c9f5bc

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 8c9f5bc

File tree

6 files changed

+226
-29
lines changed

6 files changed

+226
-29
lines changed

.changeset/smart-onions-pump.md

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

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

+7-3
Original file line numberDiff line numberDiff line change
@@ -400,11 +400,15 @@ 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+
paramNode = "@param" strictSpace* paramType? strictSpace* (optionalParamName | paramName) (strictSpace* "-")? strictSpace* paramDescription
404404
paramType = "{" strictSpace* paramTypeContent strictSpace* "}"
405405
paramTypeContent = anyExceptStar<("}"| strictSpace)>
406-
paramName = identifierCharacter+
407-
paramDescription = anyExceptStar<endOfParam>
406+
407+
paramName = textValue
408+
optionalParamName = "[" strictSpace* textValue strictSpace* "]"
409+
textValue = identifierCharacter+
410+
411+
paramDescription = (~"]" anyExceptStar<endOfParam>)
408412
endOfParam = strictSpace* (newline | end)
409413

410414
exampleNode = "@example" strictSpace* exampleContent

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

+134-12
Original file line numberDiff line numberDiff line change
@@ -1011,31 +1011,150 @@ 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.content.value').to.equal(
1022+
'paramWithNoDescription',
1023+
);
1024+
expectPath(cst, '0.children.0.paramName.content.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.content.value').to.equal(
1048+
'paramWithNoDescription',
1049+
);
1050+
expectPath(cst, '0.children.0.paramName.content.locStart').to.equal(
1051+
testStr.indexOf('paramWithNoDescription'),
1052+
);
1053+
expectPath(cst, '0.children.0.paramName.content.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.content.value').to.equal('paramWithWhitespace');
1063+
expectPath(cst, '0.children.1.paramName.content.locStart').to.equal(
1064+
testStr.indexOf('paramWithWhitespace'),
1065+
);
1066+
expectPath(cst, '0.children.1.paramName.content.locEnd').to.equal(
1067+
testStr.indexOf('paramWithWhitespace') + 'paramWithWhitespace'.length,
1068+
);
1069+
1070+
expectPath(cst, '0.children.2.type').to.equal('LiquidDocParamNode');
1071+
expectPath(cst, '0.children.2.paramName.type').to.equal('LiquidDocParamNameNode');
1072+
expectPath(cst, '0.children.2.paramName.required').to.equal(false);
1073+
expectPath(cst, '0.children.2.paramType.type').to.equal('TextNode');
1074+
expectPath(cst, '0.children.2.paramType.value').to.equal('String');
1075+
expectPath(cst, '0.children.2.paramDescription.type').to.equal('TextNode');
1076+
expectPath(cst, '0.children.2.paramDescription.value').to.equal('The optional param');
1077+
1078+
expectPath(cst, '0.children.3.type').to.equal('LiquidDocParamNode');
1079+
expectPath(cst, '0.children.3.paramName.type').to.equal('LiquidDocParamNameNode');
1080+
expectPath(cst, '0.children.3.paramName.required').to.equal(false);
1081+
expectPath(cst, '0.children.3.paramType.value').to.equal('String');
1082+
expectPath(cst, '0.children.3.paramDescription.value').to.equal('');
1083+
});
1084+
1085+
it('should parse @param with malformed optional delimiters as Text Nodes', () => {
1086+
const testStr = `{% doc %}
1087+
@param paramWithMissingHeadDelim]
1088+
@param [paramWithMissingTailDelim
1089+
@param missingHeadWithDescription] - description value
1090+
@param [missingTailWithDescription - description value
1091+
@param [too many words] description
1092+
{% enddoc %}`;
1093+
cst = toCST(testStr);
1094+
1095+
expectPath(cst, '0.children.0.type').to.equal('TextNode');
1096+
expectPath(cst, '0.children.0.value').to.equal('@param paramWithMissingHeadDelim]');
1097+
expectPath(cst, '0.children.0.locStart').to.equal(
1098+
testStr.indexOf('@param paramWithMissingHeadDelim]'),
1099+
);
1100+
expectPath(cst, '0.children.0.locEnd').to.equal(
1101+
testStr.indexOf('@param paramWithMissingHeadDelim]') +
1102+
'@param paramWithMissingHeadDelim]'.length,
1103+
);
1104+
1105+
expectPath(cst, '0.children.1.type').to.equal('TextNode');
1106+
expectPath(cst, '0.children.1.value').to.equal('@param [paramWithMissingTailDelim');
1107+
expectPath(cst, '0.children.1.locStart').to.equal(
1108+
testStr.indexOf('@param [paramWithMissingTailDelim'),
1109+
);
1110+
expectPath(cst, '0.children.1.locEnd').to.equal(
1111+
testStr.indexOf('@param [paramWithMissingTailDelim') +
1112+
'@param [paramWithMissingTailDelim'.length,
1113+
);
1114+
1115+
expectPath(cst, '0.children.2.type').to.equal('TextNode');
1116+
expectPath(cst, '0.children.2.value').to.equal(
1117+
'@param missingHeadWithDescription] - description value',
1118+
);
1119+
expectPath(cst, '0.children.2.locStart').to.equal(
1120+
testStr.indexOf('@param missingHeadWithDescription] - description value'),
1121+
);
1122+
expectPath(cst, '0.children.2.locEnd').to.equal(
1123+
testStr.indexOf('@param missingHeadWithDescription] - description value') +
1124+
'@param missingHeadWithDescription] - description value'.length,
1125+
);
1126+
1127+
expectPath(cst, '0.children.3.type').to.equal('TextNode');
1128+
expectPath(cst, '0.children.3.value').to.equal(
1129+
'@param [missingTailWithDescription - description value',
1130+
);
1131+
expectPath(cst, '0.children.3.locStart').to.equal(
1132+
testStr.indexOf('@param [missingTailWithDescription - description value'),
1133+
);
1134+
expectPath(cst, '0.children.3.locEnd').to.equal(
1135+
testStr.indexOf('@param [missingTailWithDescription - description value') +
1136+
'@param [missingTailWithDescription - description value'.length,
1137+
);
1138+
1139+
expectPath(cst, '0.children.4.type').to.equal('TextNode');
1140+
expectPath(cst, '0.children.4.value').to.equal('@param [too many words] description');
10291141
});
10301142

10311143
it('should parse @param with name and description', () => {
10321144
const testStr = `{% doc %} @param paramWithDescription param with description {% enddoc %}`;
10331145
cst = toCST(testStr);
10341146

10351147
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');
1148+
expectPath(cst, '0.children.0.paramName.type').to.equal('LiquidDocParamNameNode');
1149+
expectPath(cst, '0.children.0.paramName.required').to.equal(true);
1150+
expectPath(cst, '0.children.0.paramName.content.type').to.equal('TextNode');
1151+
expectPath(cst, '0.children.0.paramName.content.value').to.equal('paramWithDescription');
1152+
expectPath(cst, '0.children.0.paramName.content.locStart').to.equal(
1153+
testStr.indexOf('paramWithDescription'),
1154+
);
1155+
expectPath(cst, '0.children.0.paramName.content.locEnd').to.equal(
1156+
testStr.indexOf('paramWithDescription') + 'paramWithDescription'.length,
1157+
);
10391158
expectPath(cst, '0.children.0.paramDescription.value').to.equal('param with description');
10401159
});
10411160

@@ -1044,7 +1163,10 @@ describe('Unit: Stage 1 (CST)', () => {
10441163
cst = toCST(testStr);
10451164

10461165
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1047-
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithType');
1166+
expectPath(cst, '0.children.0.paramName.type').to.equal('LiquidDocParamNameNode');
1167+
expectPath(cst, '0.children.0.paramName.required').to.equal(true);
1168+
expectPath(cst, '0.children.0.paramName.content.type').to.equal('TextNode');
1169+
expectPath(cst, '0.children.0.paramName.content.value').to.equal('paramWithType');
10481170

10491171
expectPath(cst, '0.children.0.paramType.type').to.equal('TextNode');
10501172
expectPath(cst, '0.children.0.paramType.value').to.equal('String');
@@ -1059,7 +1181,7 @@ describe('Unit: Stage 1 (CST)', () => {
10591181
cst = toCST(testStr);
10601182

10611183
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1062-
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithType');
1184+
expectPath(cst, '0.children.0.paramName.content.value').to.equal('paramWithType');
10631185

10641186
expectPath(cst, '0.children.0.paramType.type').to.equal('TextNode');
10651187
expectPath(cst, '0.children.0.paramType.value').to.equal('String');
@@ -1098,11 +1220,11 @@ describe('Unit: Stage 1 (CST)', () => {
10981220
cst = toCST(testStr);
10991221

11001222
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1101-
expectPath(cst, '0.children.0.paramName.value').to.equal('param1');
1223+
expectPath(cst, '0.children.0.paramName.content.value').to.equal('param1');
11021224
expectPath(cst, '0.children.0.paramDescription.value').to.equal('first parameter');
11031225

11041226
expectPath(cst, '0.children.1.type').to.equal('LiquidDocParamNode');
1105-
expectPath(cst, '0.children.1.paramName.value').to.equal('param2');
1227+
expectPath(cst, '0.children.1.paramName.content.value').to.equal('param2');
11061228
expectPath(cst, '0.children.1.paramDescription.value').to.equal('second parameter');
11071229

11081230
expectPath(cst, '0.children.2.type').to.equal('TextNode');
@@ -1158,7 +1280,7 @@ describe('Unit: Stage 1 (CST)', () => {
11581280
'\n This is an example\n',
11591281
);
11601282
expectPath(cst, '0.children.1.type').to.equal('LiquidDocParamNode');
1161-
expectPath(cst, '0.children.1.paramName.value').to.equal('param1');
1283+
expectPath(cst, '0.children.1.paramName.content.value').to.equal('param1');
11621284
});
11631285

11641286
it('should parse example node with whitespace and new lines', () => {

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

+26-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+
content: ConcreteTextNode;
124+
required: boolean;
125+
}
126+
119127
export interface ConcreteLiquidDocExampleNode
120128
extends ConcreteBasicNode<ConcreteNodeTypes.LiquidDocExampleNode> {
121129
name: 'example';
@@ -1351,7 +1359,22 @@ 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+
content: 0,
1365+
locStart,
1366+
locEnd,
1367+
source,
1368+
required: true,
1369+
},
1370+
optionalParamName: {
1371+
type: ConcreteNodeTypes.LiquidDocParamNameNode,
1372+
content: 2,
1373+
locStart,
1374+
locEnd,
1375+
source,
1376+
required: false,
1377+
},
13551378
paramDescription: textNode,
13561379
exampleNode: {
13571380
type: ConcreteNodeTypes.LiquidDocExampleNode,
@@ -1362,6 +1385,7 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number)
13621385
exampleContent: 2,
13631386
},
13641387
exampleContent: textNode,
1388+
textValue: textNode,
13651389
fallbackNode: textNode,
13661390
};
13671391

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)