Skip to content

Commit

Permalink
Parsing support for optional param delimiters []
Browse files Browse the repository at this point in the history
----
- 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`
  • Loading branch information
jamesmengo committed Jan 28, 2025
1 parent fced038 commit 0387935
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 17 deletions.
5 changes: 5 additions & 0 deletions .changeset/smart-onions-pump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/liquid-html-parser': minor
---

Add parser support for optional liquiddoc parameters
11 changes: 8 additions & 3 deletions packages/liquid-html-parser/grammar/liquid-html.ohm
Original file line number Diff line number Diff line change
Expand Up @@ -400,11 +400,16 @@ LiquidDoc <: Helpers {
openControl:= "@" | end

fallbackNode = "@" anyExceptStar<endOfParam>
paramNode = "@param" strictSpace* paramType? strictSpace* paramName (strictSpace* "-")? strictSpace* paramDescription
paramNode = "@param" strictSpace* paramType? strictSpace* (optionalParamName | paramName) (strictSpace* "-")? strictSpace* paramDescription
paramType = "{" strictSpace* paramTypeContent strictSpace* "}"
paramTypeContent = anyExceptStar<("}"| strictSpace)>
paramName = identifierCharacter+
paramDescription = anyExceptStar<endOfParam>

paramName = (~endOfParamName identifierCharacter)+
optionalParamName = "[" strictSpace* optionalParamNameContent strictSpace* "]"
optionalParamNameContent = anyExceptStar<endOfParamName>
endOfParamName = strictSpace* ("]" | "@")

paramDescription = (~"]" anyExceptStar<endOfParam>)
endOfParam = strictSpace* (newline | end)
}

Expand Down
102 changes: 101 additions & 1 deletion packages/liquid-html-parser/src/stage-1-cst.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1011,11 +1011,12 @@ describe('Unit: Stage 1 (CST)', () => {
expectPath(cst, '0.children.0.value').to.equal('@param');
});

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

expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
expectPath(cst, '0.children.0.required').to.equal(true);
expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode');
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithNoDescription');
expectPath(cst, '0.children.0.paramName.locStart').to.equal(
Expand All @@ -1028,6 +1029,105 @@ describe('Unit: Stage 1 (CST)', () => {
expectPath(cst, '0.children.0.paramDescription.value').to.equal('');
});

it('should parse an optional @param', () => {
const testStr = `{% doc %}
@param [paramWithNoDescription]
@param [ paramWithWhitespace ]
@param {String} [optionalParam] - The optional param
{% enddoc %}`;
cst = toCST(testStr);

expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode');
expectPath(cst, '0.children.0.required').to.equal(false);
expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode');
expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithNoDescription');
expectPath(cst, '0.children.0.paramName.locStart').to.equal(
testStr.indexOf('paramWithNoDescription'),
);
expectPath(cst, '0.children.0.paramName.locEnd').to.equal(
testStr.indexOf('paramWithNoDescription') + 'paramWithNoDescription'.length,
);
expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode');
expectPath(cst, '0.children.0.paramDescription.value').to.equal('');

expectPath(cst, '0.children.1.type').to.equal('LiquidDocParamNode');
expectPath(cst, '0.children.1.required').to.equal(false);
expectPath(cst, '0.children.1.paramName.type').to.equal('TextNode');
expectPath(cst, '0.children.1.paramName.value').to.equal('paramWithWhitespace');
expectPath(cst, '0.children.1.paramName.locStart').to.equal(
testStr.indexOf('paramWithWhitespace'),
);
expectPath(cst, '0.children.1.paramName.locEnd').to.equal(
testStr.indexOf('paramWithWhitespace') + 'paramWithWhitespace'.length,
);

expectPath(cst, '0.children.2.type').to.equal('LiquidDocParamNode');
expectPath(cst, '0.children.2.required').to.equal(false);
expectPath(cst, '0.children.2.paramType.value').to.equal('String');
});

it('should parse @param with missing optional fallback Text Nodes', () => {
const testStr = `{% doc %}
@param paramWithMissingHeadDelim]
@param [paramWithMissingTailDelim
@param missingHeadWithDescription] - description value
@param [missingTailWithDescription - description value
@param [too many words] description
{% enddoc %}`;
cst = toCST(testStr);

expectPath(cst, '0.children.0.type').to.equal('TextNode');
expectPath(cst, '0.children.0.value').to.equal('@param paramWithMissingHeadDelim]');
expectPath(cst, '0.children.0.locStart').to.equal(
testStr.indexOf('@param paramWithMissingHeadDelim]'),
);
expectPath(cst, '0.children.0.locEnd').to.equal(
testStr.indexOf('@param paramWithMissingHeadDelim]') +
'@param paramWithMissingHeadDelim]'.length,
);

expectPath(cst, '0.children.1.type').to.equal('TextNode');
expectPath(cst, '0.children.1.value').to.equal('@param [paramWithMissingTailDelim');
expectPath(cst, '0.children.1.locStart').to.equal(
testStr.indexOf('@param [paramWithMissingTailDelim'),
);
expectPath(cst, '0.children.1.locEnd').to.equal(
testStr.indexOf('@param [paramWithMissingTailDelim') +
'@param [paramWithMissingTailDelim'.length,
);

expectPath(cst, '0.children.2.type').to.equal('TextNode');
expectPath(cst, '0.children.2.value').to.equal(
'@param missingHeadWithDescription] - description value',
);
expectPath(cst, '0.children.2.locStart').to.equal(
testStr.indexOf('@param missingHeadWithDescription] - description value'),
);
expectPath(cst, '0.children.2.locEnd').to.equal(
testStr.indexOf('@param missingHeadWithDescription] - description value') +
'@param missingHeadWithDescription] - description value'.length,
);

expectPath(cst, '0.children.3.type').to.equal('TextNode');
expectPath(cst, '0.children.3.value').to.equal(
'@param [missingTailWithDescription - description value',
);
expectPath(cst, '0.children.3.locStart').to.equal(
testStr.indexOf('@param [missingTailWithDescription - description value'),
);
expectPath(cst, '0.children.3.locEnd').to.equal(
testStr.indexOf('@param [missingTailWithDescription - description value') +
'@param [missingTailWithDescription - description value'.length,
);

expectPath(cst, '0.children.4.type').to.equal('LiquidDocParamNode');
expectPath(cst, '0.children.4.required').to.equal(false);
expectPath(cst, '0.children.4.paramName.type').to.equal('TextNode');
expectPath(cst, '0.children.4.paramName.value').to.equal('too many words');
expectPath(cst, '0.children.4.paramDescription.type').to.equal('TextNode');
expectPath(cst, '0.children.4.paramDescription.value').to.equal('description');
});

it('should parse @param with name and description', () => {
const testStr = `{% doc %} @param paramWithDescription param with description {% enddoc %}`;
cst = toCST(testStr);
Expand Down
9 changes: 9 additions & 0 deletions packages/liquid-html-parser/src/stage-1-cst.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export interface ConcreteLiquidDocParamNode
paramName: ConcreteTextNode;
paramDescription: ConcreteTextNode | null;
paramType: ConcreteTextNode | null;
required: boolean;
}

export interface ConcreteHtmlNodeBase<T> extends ConcreteBasicNode<T> {
Expand Down Expand Up @@ -1341,10 +1342,18 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number)
paramType: 2,
paramName: 4,
paramDescription: 8,
required: function (nodes: Node[]) {
const nameSourceString = nodes[4].sourceString;
const regex = /^\[(.*)\]$/;
return !regex.test(nameSourceString);
},
},
paramType: 2,
paramTypeContent: textNode,
paramName: textNode,
optionalParamName: 2,
optionalParamNameContent: textNode,
paramNameContent: textNode,
paramDescription: textNode,
fallbackNode: textNode,
};
Expand Down
34 changes: 22 additions & 12 deletions packages/liquid-html-parser/src/stage-2-ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1231,7 +1231,8 @@ describe('Unit: Stage 2 (AST)', () => {

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

expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocParamNode');
expectPath(ast, 'children.0.body.nodes.0.name').to.eql('param');
expectPath(ast, 'children.0.body.nodes.0.required').to.eql(true);
expectPath(ast, 'children.0.body.nodes.0.paramName.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('paramWithNoType');
expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('requiredParamWithNoType');
expectPath(ast, 'children.0.body.nodes.0.paramType').to.be.null;
expectPath(ast, 'children.0.body.nodes.0.paramDescription').to.be.null;

expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocParamNode');
expectPath(ast, 'children.0.body.nodes.1.name').to.eql('param');
expectPath(ast, 'children.0.body.nodes.1.required').to.eql(false);
expectPath(ast, 'children.0.body.nodes.1.paramName.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('paramWithDescription');
expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('optionalParameter');
expectPath(ast, 'children.0.body.nodes.1.paramDescription.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql(
'param with description and `punctation`. This is still a valid param description.',
'optional parameter description',
);
expectPath(ast, 'children.0.body.nodes.1.paramType.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.1.paramType.value').to.eql('String');
expectPath(ast, 'children.0.body.nodes.1.paramType').to.be.null;
expectPath(ast, 'children.0.body.nodes.1.paramType').to.be.null;

expectPath(ast, 'children.0.body.nodes.2.type').to.eql('LiquidDocParamNode');
expectPath(ast, 'children.0.body.nodes.2.name').to.eql('param');
expectPath(ast, 'children.0.body.nodes.2.required').to.eql(true);
expectPath(ast, 'children.0.body.nodes.2.paramName.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('paramWithNoDescription');
expectPath(ast, 'children.0.body.nodes.2.paramDescription').to.be.null;
expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('paramWithDescription');
expectPath(ast, 'children.0.body.nodes.2.paramDescription.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.2.paramDescription.value').to.eql(
'param with description and `punctation`. This is still a valid param description.',
);
expectPath(ast, 'children.0.body.nodes.2.paramType.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.2.paramType.value').to.eql('String');

expectPath(ast, 'children.0.body.nodes.3.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.3.value').to.eql(
'@unsupported this node falls back to a text node',
);
expectPath(ast, 'children.0.body.nodes.3.type').to.eql('LiquidDocParamNode');
expectPath(ast, 'children.0.body.nodes.3.name').to.eql('param');
expectPath(ast, 'children.0.body.nodes.2.paramName.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.3.paramName.value').to.eql('paramWithNoDescription');
expectPath(ast, 'children.0.body.nodes.3.paramDescription').to.be.null;
expectPath(ast, 'children.0.body.nodes.3.paramType.type').to.eql('TextNode');
expectPath(ast, 'children.0.body.nodes.3.paramType.value').to.eql('String');
});

it('should parse unclosed tables with assignments', () => {
Expand Down
5 changes: 4 additions & 1 deletion packages/liquid-html-parser/src/stage-2-ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ export interface TextNode extends ASTNode<NodeTypes.TextNode> {
value: string;
}

/** Represents a `@param` node in a LiquidDoc comment - `@param paramName {paramType} - paramDescription` */
/** Represents a `@param` node in a LiquidDoc comment - `@param {paramType} [paramName] - paramDescription` */
export interface LiquidDocParamNode extends ASTNode<NodeTypes.LiquidDocParamNode> {
name: 'param';
/** The name of the parameter (e.g. "product") */
Expand All @@ -764,6 +764,8 @@ export interface LiquidDocParamNode extends ASTNode<NodeTypes.LiquidDocParamNode
paramDescription: TextNode | null;
/** Optional type annotation for the parameter (e.g. "{string}", "{number}") */
paramType: TextNode | null;
/** Whether this parameter must be passed when using the snippet */
required: boolean;
}
export interface ASTNode<T> {
/**
Expand Down Expand Up @@ -1293,6 +1295,7 @@ function buildAst(
},
paramDescription: toNullableTextNode(node.paramDescription),
paramType: toNullableTextNode(node.paramType),
required: node.required,
});
break;
}
Expand Down

0 comments on commit 0387935

Please sign in to comment.