Skip to content

Commit a963b38

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` 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 fced038 commit a963b38

File tree

6 files changed

+234
-27
lines changed

6 files changed

+234
-27
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
openControl:= "@" | end
401401

402402
fallbackNode = "@" anyExceptStar<endOfParam>
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

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
}
8990

9091
export const LiquidLiteralValues = {
@@ -110,11 +111,18 @@ export interface ConcreteBasicNode<T> {
110111
export interface ConcreteLiquidDocParamNode
111112
extends ConcreteBasicNode<ConcreteNodeTypes.LiquidDocParamNode> {
112113
name: 'param';
113-
paramName: ConcreteTextNode;
114+
paramName: ConcreteLiquidDocParamNameNode;
114115
paramDescription: ConcreteTextNode | null;
115116
paramType: ConcreteTextNode | null;
116117
}
117118

119+
export interface ConcreteLiquidDocParamNameNode
120+
extends ConcreteBasicNode<ConcreteNodeTypes.LiquidDocParamNameNode> {
121+
name: 'paramName';
122+
paramNameContent: ConcreteTextNode;
123+
required: boolean;
124+
}
125+
118126
export interface ConcreteHtmlNodeBase<T> extends ConcreteBasicNode<T> {
119127
attrList?: ConcreteAttributeNode[];
120128
}
@@ -1344,8 +1352,26 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number)
13441352
},
13451353
paramType: 2,
13461354
paramTypeContent: textNode,
1347-
paramName: textNode,
1355+
paramName: {
1356+
type: ConcreteNodeTypes.LiquidDocParamNameNode,
1357+
paramNameContent: 0,
1358+
locStart,
1359+
locEnd,
1360+
source,
1361+
required: true,
1362+
},
1363+
optionalParamName: {
1364+
type: ConcreteNodeTypes.LiquidDocParamNameNode,
1365+
paramNameContent: 2,
1366+
locStart,
1367+
locEnd,
1368+
source,
1369+
required: false,
1370+
},
1371+
optionalParamNameContent: textNode,
1372+
paramNameContent: textNode,
13481373
paramDescription: textNode,
1374+
textValue: textNode,
13491375
fallbackNode: textNode,
13501376
};
13511377

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
});

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

+5-7
Original file line numberDiff line numberDiff line change
@@ -755,7 +755,7 @@ export interface TextNode extends ASTNode<NodeTypes.TextNode> {
755755
value: string;
756756
}
757757

758-
/** Represents a `@param` node in a LiquidDoc comment - `@param paramName {paramType} - paramDescription` */
758+
/** Represents a `@param` node in a LiquidDoc comment - `@param {paramType} [paramName] - paramDescription` */
759759
export interface LiquidDocParamNode extends ASTNode<NodeTypes.LiquidDocParamNode> {
760760
name: 'param';
761761
/** The name of the parameter (e.g. "product") */
@@ -764,6 +764,8 @@ export interface LiquidDocParamNode extends ASTNode<NodeTypes.LiquidDocParamNode
764764
paramDescription: TextNode | null;
765765
/** Optional type annotation for the parameter (e.g. "{string}", "{number}") */
766766
paramType: TextNode | null;
767+
/** Whether this parameter must be passed when using the snippet */
768+
required: boolean;
767769
}
768770
export interface ASTNode<T> {
769771
/**
@@ -1285,14 +1287,10 @@ function buildAst(
12851287
name: node.name,
12861288
position: position(node),
12871289
source: node.source,
1288-
paramName: {
1289-
type: NodeTypes.TextNode,
1290-
value: node.paramName.value,
1291-
position: position(node.paramName),
1292-
source: node.paramName.source,
1293-
},
1290+
paramName: toTextNode(node.paramName.paramNameContent),
12941291
paramDescription: toNullableTextNode(node.paramDescription),
12951292
paramType: toNullableTextNode(node.paramType),
1293+
required: node.paramName.required,
12961294
});
12971295
break;
12981296
}

0 commit comments

Comments
 (0)