Skip to content

Commit 0a52108

Browse files
committed
[LiquidDoc] Parsing support for optional parameters and default values
---- - Added a parameter `required` to the LiquidDocParamNode - By default, parameters are required unless they have `[]` around the name - Parameters with incomplete delimiters `e.g. ([missingTail)` will map to a `TextNode` - Default values are supported for optional parameters - `[param=default]` - Default values will match anything between `=` and `]`. Leading and Trailing spaces are trimmed. I originally tried accessing everything from the top level in the param node. This is still the goal, but I think this should be done at stage 2 so that we can leverage the recursive nature of how to-AST works. My original approach attempted to Write everything to the top level in stage 1, but we should be doing this in stage 2.
1 parent 8bceaa3 commit 0a52108

File tree

7 files changed

+470
-66
lines changed

7 files changed

+470
-66
lines changed

.changeset/chatty-scissors-relax.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@shopify/liquid-html-parser': minor
3+
'theme-check-vscode': minor
4+
---
5+
6+
Add support for parsing optional parameters in LiquidDoc. Optional parameters are designated by wrapping the name in square brackets, and can accept a default value (e.g. `@param [parameter_name=default]`).

.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

+17-3
Original file line numberDiff line numberDiff line change
@@ -398,14 +398,28 @@ LiquidDoc <: Helpers {
398398
strictSpace = " " | "\t"
399399
// We use this as an escape hatch to stop matching TextNode and try again when one of these characters is encountered
400400
openControl:= "@" | end
401+
// We use this to map text values into a textNodes via mappings in stage-1-cst.ts
402+
textValue = identifierCharacter+
401403

402404
fallbackNode = "@" anyExceptStar<endOfParam>
403-
paramNode = "@param" strictSpace* paramType? strictSpace* paramName (strictSpace* "-")? strictSpace* paramDescription
405+
paramNode = "@param" strictSpace* paramType? strictSpace* (paramName | optionalParamName) (strictSpace* "-")? strictSpace* paramDescription
406+
404407
paramType = "{" strictSpace* paramTypeContent strictSpace* "}"
405408
paramTypeContent = anyExceptStar<("}"| strictSpace)>
406-
paramName = identifierCharacter+
407-
paramDescription = anyExceptStar<endOfParam>
409+
410+
paramDescription = (~"]" anyExceptStar<endOfParam>)
408411
endOfParam = strictSpace* (newline | end)
412+
413+
// Required parameter name - e.g. "paramName"
414+
paramName = textValue
415+
// Optional parameter name - e.g. "[paramName]"
416+
optionalParamName = "[" strictSpace* optionalParamNameContent strictSpace* "]"
417+
// Content of an optional parameter name - e.g. "paramName=defaultValue"
418+
optionalParamNameContent = strictSpace* textValue strictSpace* optionalParamNameDefaultValue?
419+
// Default value of an optional parameter name - e.g. "=defaultValue"
420+
optionalParamNameDefaultValue = "=" strictSpace* optionalParamNameDefaultContent
421+
optionalParamNameDefaultContent = anyExceptPlus<endOfOptionalParamNameDefaultValue>
422+
endOfOptionalParamNameDefaultValue = strictSpace* "]"
409423
}
410424

411425
LiquidHTML <: Liquid {

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

+343-15
Large diffs are not rendered by default.

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

+31-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+
required: boolean;
122+
defaultValue: ConcreteTextNode | null;
123+
paramNameContent: ConcreteTextNode;
124+
}
125+
118126
export interface ConcreteHtmlNodeBase<T> extends ConcreteBasicNode<T> {
119127
attrList?: ConcreteAttributeNode[];
120128
}
@@ -1344,7 +1352,28 @@ 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+
defaultValue: null,
1363+
},
1364+
optionalParamName: 2,
1365+
optionalParamNameContent: {
1366+
type: ConcreteNodeTypes.LiquidDocParamNameNode,
1367+
paramNameContent: 1,
1368+
required: false,
1369+
defaultValue: 3,
1370+
locStart,
1371+
locEnd,
1372+
source,
1373+
},
1374+
optionalParamNameDefaultValue: 2,
1375+
optionalParamNameDefaultContent: textNode,
1376+
textValue: textNode,
13481377
paramDescription: textNode,
13491378
fallbackNode: textNode,
13501379
};

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

+60-44
Original file line numberDiff line numberDiff line change
@@ -1220,56 +1220,72 @@ describe('Unit: Stage 2 (AST)', () => {
12201220
expectPath(ast, 'children.0.markup.1.children.0.children.1.markup.name').to.eql('var3');
12211221
});
12221222

1223-
it(`should parse doc tags`, () => {
1224-
ast = toLiquidAST(`{% doc %}{% enddoc %}`);
1225-
expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
1226-
expectPath(ast, 'children.0.name').to.eql('doc');
1227-
expectPath(ast, 'children.0.markup').toEqual('');
1228-
expectPath(ast, 'children.0.body.value').to.eql('');
1229-
expectPath(ast, 'children.0.body.type').toEqual('RawMarkup');
1230-
expectPath(ast, 'children.0.body.nodes').toEqual([]);
1231-
1232-
ast = toLiquidAST(`
1223+
describe('LiquidDoc', () => {
1224+
it(`should parse liquid doc tags`, () => {
1225+
ast = toLiquidAST(`{% doc %}{% enddoc %}`);
1226+
expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
1227+
expectPath(ast, 'children.0.name').to.eql('doc');
1228+
expectPath(ast, 'children.0.markup').toEqual('');
1229+
expectPath(ast, 'children.0.body.value').to.eql('');
1230+
expectPath(ast, 'children.0.body.type').toEqual('RawMarkup');
1231+
expectPath(ast, 'children.0.body.nodes').toEqual([]);
1232+
1233+
ast = toLiquidAST(`
12331234
{% doc -%}
1234-
@param paramWithNoType
1235+
@param requiredParamWithNoType
1236+
@param [optionalParameter] - optional parameter description
12351237
@param {String} paramWithDescription - param with description and \`punctation\`. This is still a valid param description.
12361238
@param {String} paramWithNoDescription
12371239
@unsupported this node falls back to a text node
1240+
@param [optionalParameterWithDefault=defaultValue] - optional parameter description with default
1241+
@param {String} [optionalParameterWithDefaultAndType=defaultValue]
12381242
{%- enddoc %}
12391243
`);
1240-
expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
1241-
expectPath(ast, 'children.0.name').to.eql('doc');
1242-
1243-
expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode');
1244-
expectPath(ast, 'children.0.body.nodes.0.name').to.eql('param');
1245-
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');
1247-
expectPath(ast, 'children.0.body.nodes.0.paramType').to.be.null;
1248-
expectPath(ast, 'children.0.body.nodes.0.paramDescription').to.be.null;
1249-
1250-
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocParamNode');
1251-
expectPath(ast, 'children.0.body.nodes.1.name').to.eql('param');
1252-
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');
1254-
expectPath(ast, 'children.0.body.nodes.1.paramDescription.type').to.eql('TextNode');
1255-
expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql(
1256-
'param with description and `punctation`. This is still a valid param description.',
1257-
);
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');
1260-
1261-
expectPath(ast, 'children.0.body.nodes.2.type').to.eql('LiquidDocParamNode');
1262-
expectPath(ast, 'children.0.body.nodes.2.name').to.eql('param');
1263-
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;
1266-
expectPath(ast, 'children.0.body.nodes.2.paramType.type').to.eql('TextNode');
1267-
expectPath(ast, 'children.0.body.nodes.2.paramType.value').to.eql('String');
1268-
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-
);
1244+
expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
1245+
expectPath(ast, 'children.0.name').to.eql('doc');
1246+
1247+
expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode');
1248+
expectPath(ast, 'children.0.body.nodes.0.name').to.eql('param');
1249+
expectPath(ast, 'children.0.body.nodes.0.required').to.eql(true);
1250+
expectPath(ast, 'children.0.body.nodes.0.paramName.type').to.eql('TextNode');
1251+
expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql(
1252+
'requiredParamWithNoType',
1253+
);
1254+
expectPath(ast, 'children.0.body.nodes.0.paramType').to.be.null;
1255+
expectPath(ast, 'children.0.body.nodes.0.paramDescription').to.be.null;
1256+
1257+
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocParamNode');
1258+
expectPath(ast, 'children.0.body.nodes.1.name').to.eql('param');
1259+
expectPath(ast, 'children.0.body.nodes.1.required').to.eql(false);
1260+
expectPath(ast, 'children.0.body.nodes.1.paramName.type').to.eql('TextNode');
1261+
expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('optionalParameter');
1262+
expectPath(ast, 'children.0.body.nodes.1.paramDescription.type').to.eql('TextNode');
1263+
expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql(
1264+
'optional parameter description',
1265+
);
1266+
expectPath(ast, 'children.0.body.nodes.1.paramType').to.be.null;
1267+
expectPath(ast, 'children.0.body.nodes.1.paramType').to.be.null;
1268+
1269+
expectPath(ast, 'children.0.body.nodes.2.type').to.eql('LiquidDocParamNode');
1270+
expectPath(ast, 'children.0.body.nodes.2.name').to.eql('param');
1271+
expectPath(ast, 'children.0.body.nodes.2.required').to.eql(true);
1272+
expectPath(ast, 'children.0.body.nodes.2.paramName.type').to.eql('TextNode');
1273+
expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('paramWithDescription');
1274+
expectPath(ast, 'children.0.body.nodes.2.paramDescription.type').to.eql('TextNode');
1275+
expectPath(ast, 'children.0.body.nodes.2.paramDescription.value').to.eql(
1276+
'param with description and `punctation`. This is still a valid param description.',
1277+
);
1278+
expectPath(ast, 'children.0.body.nodes.2.paramType.type').to.eql('TextNode');
1279+
expectPath(ast, 'children.0.body.nodes.2.paramType.value').to.eql('String');
1280+
1281+
expectPath(ast, 'children.0.body.nodes.3.type').to.eql('LiquidDocParamNode');
1282+
expectPath(ast, 'children.0.body.nodes.3.name').to.eql('param');
1283+
expectPath(ast, 'children.0.body.nodes.2.paramName.type').to.eql('TextNode');
1284+
expectPath(ast, 'children.0.body.nodes.3.paramName.value').to.eql('paramWithNoDescription');
1285+
expectPath(ast, 'children.0.body.nodes.3.paramDescription').to.be.null;
1286+
expectPath(ast, 'children.0.body.nodes.3.paramType.type').to.eql('TextNode');
1287+
expectPath(ast, 'children.0.body.nodes.3.paramType.value').to.eql('String');
1288+
});
12731289
});
12741290

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

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

+8-2
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=defaultValue] - paramDescription` */
759759
export interface LiquidDocParamNode extends ASTNode<NodeTypes.LiquidDocParamNode> {
760760
name: 'param';
761761
/** The name of the parameter (e.g. "product") */
@@ -764,6 +764,10 @@ 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;
769+
/** The default value of the parameter (e.g. "10") - null if the parameter is required or has no default value */
770+
defaultValue: TextNode | null;
767771
}
768772
export interface ASTNode<T> {
769773
/**
@@ -1287,12 +1291,14 @@ function buildAst(
12871291
source: node.source,
12881292
paramName: {
12891293
type: NodeTypes.TextNode,
1290-
value: node.paramName.value,
1294+
value: node.paramName.paramNameContent.value,
12911295
position: position(node.paramName),
12921296
source: node.paramName.source,
12931297
},
12941298
paramDescription: toNullableTextNode(node.paramDescription),
12951299
paramType: toNullableTextNode(node.paramType),
1300+
required: node.paramName.required,
1301+
defaultValue: toNullableTextNode(node.paramName.defaultValue),
12961302
});
12971303
break;
12981304
}

0 commit comments

Comments
 (0)