Skip to content

Commit 0387935

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 fced038 commit 0387935

File tree

6 files changed

+149
-17
lines changed

6 files changed

+149
-17
lines changed

.changeset/smart-onions-pump.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/liquid-html-parser': minor
3+
---
4+
5+
Add parser support for optional liquiddoc parameters

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
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 = (~endOfParamName identifierCharacter)+
408+
optionalParamName = "[" strictSpace* optionalParamNameContent strictSpace* "]"
409+
optionalParamNameContent = anyExceptStar<endOfParamName>
410+
endOfParamName = strictSpace* ("]" | "@")
411+
412+
paramDescription = (~"]" anyExceptStar<endOfParam>)
408413
endOfParam = strictSpace* (newline | end)
409414
}
410415

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

+101-1
Original file line numberDiff line numberDiff line change
@@ -1011,11 +1011,12 @@ 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.required').to.equal(true);
10191020
expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode');
10201021
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithNoDescription');
10211022
expectPath(cst, '0.children.0.paramName.locStart').to.equal(
@@ -1028,6 +1029,105 @@ describe('Unit: Stage 1 (CST)', () => {
10281029
expectPath(cst, '0.children.0.paramDescription.value').to.equal('');
10291030
});
10301031

1032+
it('should parse an optional @param', () => {
1033+
const testStr = `{% doc %}
1034+
@param [paramWithNoDescription]
1035+
@param [ paramWithWhitespace ]
1036+
@param {String} [optionalParam] - The optional param
1037+
{% enddoc %}`;
1038+
cst = toCST(testStr);
1039+
1040+
expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
1041+
expectPath(cst, '0.children.0.required').to.equal(false);
1042+
expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode');
1043+
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithNoDescription');
1044+
expectPath(cst, '0.children.0.paramName.locStart').to.equal(
1045+
testStr.indexOf('paramWithNoDescription'),
1046+
);
1047+
expectPath(cst, '0.children.0.paramName.locEnd').to.equal(
1048+
testStr.indexOf('paramWithNoDescription') + 'paramWithNoDescription'.length,
1049+
);
1050+
expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode');
1051+
expectPath(cst, '0.children.0.paramDescription.value').to.equal('');
1052+
1053+
expectPath(cst, '0.children.1.type').to.equal('LiquidDocParamNode');
1054+
expectPath(cst, '0.children.1.required').to.equal(false);
1055+
expectPath(cst, '0.children.1.paramName.type').to.equal('TextNode');
1056+
expectPath(cst, '0.children.1.paramName.value').to.equal('paramWithWhitespace');
1057+
expectPath(cst, '0.children.1.paramName.locStart').to.equal(
1058+
testStr.indexOf('paramWithWhitespace'),
1059+
);
1060+
expectPath(cst, '0.children.1.paramName.locEnd').to.equal(
1061+
testStr.indexOf('paramWithWhitespace') + 'paramWithWhitespace'.length,
1062+
);
1063+
1064+
expectPath(cst, '0.children.2.type').to.equal('LiquidDocParamNode');
1065+
expectPath(cst, '0.children.2.required').to.equal(false);
1066+
expectPath(cst, '0.children.2.paramType.value').to.equal('String');
1067+
});
1068+
1069+
it('should parse @param with missing optional fallback Text Nodes', () => {
1070+
const testStr = `{% doc %}
1071+
@param paramWithMissingHeadDelim]
1072+
@param [paramWithMissingTailDelim
1073+
@param missingHeadWithDescription] - description value
1074+
@param [missingTailWithDescription - description value
1075+
@param [too many words] description
1076+
{% enddoc %}`;
1077+
cst = toCST(testStr);
1078+
1079+
expectPath(cst, '0.children.0.type').to.equal('TextNode');
1080+
expectPath(cst, '0.children.0.value').to.equal('@param paramWithMissingHeadDelim]');
1081+
expectPath(cst, '0.children.0.locStart').to.equal(
1082+
testStr.indexOf('@param paramWithMissingHeadDelim]'),
1083+
);
1084+
expectPath(cst, '0.children.0.locEnd').to.equal(
1085+
testStr.indexOf('@param paramWithMissingHeadDelim]') +
1086+
'@param paramWithMissingHeadDelim]'.length,
1087+
);
1088+
1089+
expectPath(cst, '0.children.1.type').to.equal('TextNode');
1090+
expectPath(cst, '0.children.1.value').to.equal('@param [paramWithMissingTailDelim');
1091+
expectPath(cst, '0.children.1.locStart').to.equal(
1092+
testStr.indexOf('@param [paramWithMissingTailDelim'),
1093+
);
1094+
expectPath(cst, '0.children.1.locEnd').to.equal(
1095+
testStr.indexOf('@param [paramWithMissingTailDelim') +
1096+
'@param [paramWithMissingTailDelim'.length,
1097+
);
1098+
1099+
expectPath(cst, '0.children.2.type').to.equal('TextNode');
1100+
expectPath(cst, '0.children.2.value').to.equal(
1101+
'@param missingHeadWithDescription] - description value',
1102+
);
1103+
expectPath(cst, '0.children.2.locStart').to.equal(
1104+
testStr.indexOf('@param missingHeadWithDescription] - description value'),
1105+
);
1106+
expectPath(cst, '0.children.2.locEnd').to.equal(
1107+
testStr.indexOf('@param missingHeadWithDescription] - description value') +
1108+
'@param missingHeadWithDescription] - description value'.length,
1109+
);
1110+
1111+
expectPath(cst, '0.children.3.type').to.equal('TextNode');
1112+
expectPath(cst, '0.children.3.value').to.equal(
1113+
'@param [missingTailWithDescription - description value',
1114+
);
1115+
expectPath(cst, '0.children.3.locStart').to.equal(
1116+
testStr.indexOf('@param [missingTailWithDescription - description value'),
1117+
);
1118+
expectPath(cst, '0.children.3.locEnd').to.equal(
1119+
testStr.indexOf('@param [missingTailWithDescription - description value') +
1120+
'@param [missingTailWithDescription - description value'.length,
1121+
);
1122+
1123+
expectPath(cst, '0.children.4.type').to.equal('LiquidDocParamNode');
1124+
expectPath(cst, '0.children.4.required').to.equal(false);
1125+
expectPath(cst, '0.children.4.paramName.type').to.equal('TextNode');
1126+
expectPath(cst, '0.children.4.paramName.value').to.equal('too many words');
1127+
expectPath(cst, '0.children.4.paramDescription.type').to.equal('TextNode');
1128+
expectPath(cst, '0.children.4.paramDescription.value').to.equal('description');
1129+
});
1130+
10311131
it('should parse @param with name and description', () => {
10321132
const testStr = `{% doc %} @param paramWithDescription param with description {% enddoc %}`;
10331133
cst = toCST(testStr);

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

+9
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ export interface ConcreteLiquidDocParamNode
113113
paramName: ConcreteTextNode;
114114
paramDescription: ConcreteTextNode | null;
115115
paramType: ConcreteTextNode | null;
116+
required: boolean;
116117
}
117118

118119
export interface ConcreteHtmlNodeBase<T> extends ConcreteBasicNode<T> {
@@ -1341,10 +1342,18 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number)
13411342
paramType: 2,
13421343
paramName: 4,
13431344
paramDescription: 8,
1345+
required: function (nodes: Node[]) {
1346+
const nameSourceString = nodes[4].sourceString;
1347+
const regex = /^\[(.*)\]$/;
1348+
return !regex.test(nameSourceString);
1349+
},
13441350
},
13451351
paramType: 2,
13461352
paramTypeContent: textNode,
13471353
paramName: textNode,
1354+
optionalParamName: 2,
1355+
optionalParamNameContent: textNode,
1356+
paramNameContent: textNode,
13481357
paramDescription: textNode,
13491358
fallbackNode: textNode,
13501359
};

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

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

12321232
ast = toLiquidAST(`
12331233
{% doc -%}
1234-
@param paramWithNoType
1234+
@param requiredParamWithNoType
1235+
@param [optionalParameter] - optional parameter description
12351236
@param {String} paramWithDescription - param with description and \`punctation\`. This is still a valid param description.
12361237
@param {String} paramWithNoDescription
12371238
@unsupported this node falls back to a text node
@@ -1242,34 +1243,43 @@ describe('Unit: Stage 2 (AST)', () => {
12421243

12431244
expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode');
12441245
expectPath(ast, 'children.0.body.nodes.0.name').to.eql('param');
1246+
expectPath(ast, 'children.0.body.nodes.0.required').to.eql(true);
12451247
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');
1248+
expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('requiredParamWithNoType');
12471249
expectPath(ast, 'children.0.body.nodes.0.paramType').to.be.null;
12481250
expectPath(ast, 'children.0.body.nodes.0.paramDescription').to.be.null;
12491251

12501252
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocParamNode');
12511253
expectPath(ast, 'children.0.body.nodes.1.name').to.eql('param');
1254+
expectPath(ast, 'children.0.body.nodes.1.required').to.eql(false);
12521255
expectPath(ast, 'children.0.body.nodes.1.paramName.type').to.eql('TextNode');
1253-
expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('paramWithDescription');
1256+
expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('optionalParameter');
12541257
expectPath(ast, 'children.0.body.nodes.1.paramDescription.type').to.eql('TextNode');
12551258
expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql(
1256-
'param with description and `punctation`. This is still a valid param description.',
1259+
'optional parameter description',
12571260
);
1258-
expectPath(ast, 'children.0.body.nodes.1.paramType.type').to.eql('TextNode');
1259-
expectPath(ast, 'children.0.body.nodes.1.paramType.value').to.eql('String');
1261+
expectPath(ast, 'children.0.body.nodes.1.paramType').to.be.null;
1262+
expectPath(ast, 'children.0.body.nodes.1.paramType').to.be.null;
12601263

12611264
expectPath(ast, 'children.0.body.nodes.2.type').to.eql('LiquidDocParamNode');
12621265
expectPath(ast, 'children.0.body.nodes.2.name').to.eql('param');
1266+
expectPath(ast, 'children.0.body.nodes.2.required').to.eql(true);
12631267
expectPath(ast, 'children.0.body.nodes.2.paramName.type').to.eql('TextNode');
1264-
expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('paramWithNoDescription');
1265-
expectPath(ast, 'children.0.body.nodes.2.paramDescription').to.be.null;
1268+
expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('paramWithDescription');
1269+
expectPath(ast, 'children.0.body.nodes.2.paramDescription.type').to.eql('TextNode');
1270+
expectPath(ast, 'children.0.body.nodes.2.paramDescription.value').to.eql(
1271+
'param with description and `punctation`. This is still a valid param description.',
1272+
);
12661273
expectPath(ast, 'children.0.body.nodes.2.paramType.type').to.eql('TextNode');
12671274
expectPath(ast, 'children.0.body.nodes.2.paramType.value').to.eql('String');
12681275

1269-
expectPath(ast, 'children.0.body.nodes.3.type').to.eql('TextNode');
1270-
expectPath(ast, 'children.0.body.nodes.3.value').to.eql(
1271-
'@unsupported this node falls back to a text node',
1272-
);
1276+
expectPath(ast, 'children.0.body.nodes.3.type').to.eql('LiquidDocParamNode');
1277+
expectPath(ast, 'children.0.body.nodes.3.name').to.eql('param');
1278+
expectPath(ast, 'children.0.body.nodes.2.paramName.type').to.eql('TextNode');
1279+
expectPath(ast, 'children.0.body.nodes.3.paramName.value').to.eql('paramWithNoDescription');
1280+
expectPath(ast, 'children.0.body.nodes.3.paramDescription').to.be.null;
1281+
expectPath(ast, 'children.0.body.nodes.3.paramType.type').to.eql('TextNode');
1282+
expectPath(ast, 'children.0.body.nodes.3.paramType.value').to.eql('String');
12731283
});
12741284

12751285
it('should parse unclosed tables with assignments', () => {

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

+4-1
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
/**
@@ -1293,6 +1295,7 @@ function buildAst(
12931295
},
12941296
paramDescription: toNullableTextNode(node.paramDescription),
12951297
paramType: toNullableTextNode(node.paramType),
1298+
required: node.required,
12961299
});
12971300
break;
12981301
}

0 commit comments

Comments
 (0)