Skip to content

Commit c4bbf3b

Browse files
committedFeb 10, 2025·
Add LiquidDoc parsing support for @description annotation
----- We can parse multiple descriptions for a given LiquidDoc, though we will only use the first one if multiple are present. This logic will come in future PRs. We are NOT adding support for implicit descriptions, which are descriptions that are provided before any other annotations are provided - reference: https://jsdoc.app/tags-description

File tree

10 files changed

+162
-7
lines changed

10 files changed

+162
-7
lines changed
 

‎.changeset/four-snakes-compare.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@shopify/liquid-html-parser': minor
3+
---
4+
5+
[LiquidDoc]: Add parser support for @description annotations. These can be placed anywhere within the header, and can span numerous lines.

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

+6-1
Original file line numberDiff line numberDiff line change
@@ -393,13 +393,18 @@ LiquidDoc <: Helpers {
393393
LiquidDocNode =
394394
| paramNode
395395
| exampleNode
396+
| descriptionNode
396397
| fallbackNode
397-
398+
398399
// By default, space matches new lines as well. We override it here to make writing rules easier.
399400
strictSpace = " " | "\t"
400401
// We use this as an escape hatch to stop matching TextNode and try again when one of these characters is encountered
401402
openControl:= "@" | end
402403

404+
descriptionNode = "@description" strictSpace* descriptionContent
405+
descriptionContent = anyExceptStar<endOfDescription>
406+
endOfDescription = strictSpace* openControl
407+
403408
paramNode = "@param" strictSpace* paramType? strictSpace* (optionalParamName | paramName) (strictSpace* "-")? strictSpace* paramDescription
404409
paramType = "{" strictSpace* paramTypeContent strictSpace* "}"
405410
paramTypeContent = anyExceptStar<("}"| strictSpace)>

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

+64
Original file line numberDiff line numberDiff line change
@@ -1316,6 +1316,70 @@ describe('Unit: Stage 1 (CST)', () => {
13161316
expectPath(cst, '0.children.1.type').to.equal('LiquidDocExampleNode');
13171317
expectPath(cst, '0.children.1.exampleContent.value').to.equal('second example\n');
13181318
});
1319+
1320+
it('should parse @description node', () => {
1321+
const testStr = `{% doc %} @description {%- enddoc %}`;
1322+
cst = toCST(testStr);
1323+
expectPath(cst, '0.type').to.equal('LiquidRawTag');
1324+
expectPath(cst, '0.name').to.equal('doc');
1325+
expectPath(cst, '0.children.0.type').to.equal('LiquidDocDescriptionNode');
1326+
expectPath(cst, '0.children.0.content.value').to.equal('');
1327+
});
1328+
1329+
it('should parse @description node', () => {
1330+
const testStr = `{% doc %}
1331+
@description This is a description
1332+
@description This is a second description
1333+
{% enddoc %}`;
1334+
cst = toCST(testStr);
1335+
expectPath(cst, '0.children.0.type').to.equal('LiquidDocDescriptionNode');
1336+
expectPath(cst, '0.children.0.content.value').to.equal('This is a description\n');
1337+
1338+
expectPath(cst, '0.children.1.type').to.equal('LiquidDocDescriptionNode');
1339+
expectPath(cst, '0.children.1.content.value').to.equal('This is a second description\n');
1340+
});
1341+
1342+
it('should parse and strip whitespace from description tag with content that has leading whitespace', () => {
1343+
const testStr = `{% doc %} @description hello there {%- enddoc %}`;
1344+
cst = toCST(testStr);
1345+
expectPath(cst, '0.type').to.equal('LiquidRawTag');
1346+
expectPath(cst, '0.name').to.equal('doc');
1347+
expectPath(cst, '0.children.0.type').to.equal('LiquidDocDescriptionNode');
1348+
expectPath(cst, '0.children.0.name').to.equal('description');
1349+
expectPath(cst, '0.children.0.content.value').to.equal('hello there');
1350+
expectPath(cst, '0.children.0.content.locStart').to.equal(testStr.indexOf('hello there'));
1351+
expectPath(cst, '0.children.0.content.locEnd').to.equal(
1352+
testStr.indexOf('hello there') + 'hello there'.length,
1353+
);
1354+
});
1355+
1356+
it('should parse description node with whitespace and new lines', () => {
1357+
const testStr = `{% doc %}
1358+
@description hello there my friend
1359+
This is a description
1360+
It supports multiple lines
1361+
{% enddoc %}`;
1362+
cst = toCST(testStr);
1363+
expectPath(cst, '0.type').to.equal('LiquidRawTag');
1364+
expectPath(cst, '0.name').to.equal('doc');
1365+
expectPath(cst, '0.children.0.type').to.equal('LiquidDocDescriptionNode');
1366+
expectPath(cst, '0.children.0.name').to.equal('description');
1367+
expectPath(cst, '0.children.0.content.value').to.equal(
1368+
'hello there my friend\n This is a description\n It supports multiple lines\n',
1369+
);
1370+
});
1371+
1372+
it('should parse multiple description nodes', () => {
1373+
const testStr = `{% doc %}
1374+
@description hello there
1375+
@description second description
1376+
{% enddoc %}`;
1377+
cst = toCST(testStr);
1378+
expectPath(cst, '0.children.0.type').to.equal('LiquidDocDescriptionNode');
1379+
expectPath(cst, '0.children.0.content.value').to.equal('hello there\n');
1380+
expectPath(cst, '0.children.1.type').to.equal('LiquidDocDescriptionNode');
1381+
expectPath(cst, '0.children.1.content.value').to.equal('second description\n');
1382+
});
13191383
}
13201384
});
13211385
});

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

+20-1
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export enum ConcreteNodeTypes {
8686

8787
LiquidDocParamNode = 'LiquidDocParamNode',
8888
LiquidDocParamNameNode = 'LiquidDocParamNameNode',
89+
LiquidDocDescriptionNode = 'LiquidDocDescriptionNode',
8990
LiquidDocExampleNode = 'LiquidDocExampleNode',
9091
}
9192

@@ -124,6 +125,12 @@ export interface ConcreteLiquidDocParamNameNode
124125
required: boolean;
125126
}
126127

128+
export interface ConcreteLiquidDocDescriptionNode
129+
extends ConcreteBasicNode<ConcreteNodeTypes.LiquidDocDescriptionNode> {
130+
name: 'description';
131+
content: ConcreteTextNode;
132+
}
133+
127134
export interface ConcreteLiquidDocExampleNode
128135
extends ConcreteBasicNode<ConcreteNodeTypes.LiquidDocExampleNode> {
129136
name: 'example';
@@ -469,7 +476,10 @@ export type LiquidHtmlCST = LiquidHtmlConcreteNode[];
469476

470477
export type LiquidCST = LiquidConcreteNode[];
471478

472-
export type LiquidDocConcreteNode = ConcreteLiquidDocParamNode | ConcreteLiquidDocExampleNode;
479+
export type LiquidDocConcreteNode =
480+
| ConcreteLiquidDocParamNode
481+
| ConcreteLiquidDocExampleNode
482+
| ConcreteLiquidDocDescriptionNode;
473483

474484
interface Mapping {
475485
[k: string]: number | TemplateMapping | TopLevelFunctionMapping;
@@ -1357,6 +1367,15 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number)
13571367
paramName: 4,
13581368
paramDescription: 8,
13591369
},
1370+
descriptionNode: {
1371+
type: ConcreteNodeTypes.LiquidDocDescriptionNode,
1372+
name: 'description',
1373+
locStart,
1374+
locEnd,
1375+
source,
1376+
content: 2,
1377+
},
1378+
descriptionContent: textNode,
13601379
paramType: 2,
13611380
paramTypeContent: textNode,
13621381
paramName: {

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

+37
Original file line numberDiff line numberDiff line change
@@ -1382,6 +1382,43 @@ describe('Unit: Stage 2 (AST)', () => {
13821382
expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql(
13831383
'param with description',
13841384
);
1385+
1386+
ast = toLiquidAST(`
1387+
{% doc -%}
1388+
@description This is a description
1389+
@description This is another description
1390+
it can have multiple lines
1391+
{% enddoc %}
1392+
`);
1393+
expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocDescriptionNode');
1394+
expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('This is a description\n');
1395+
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocDescriptionNode');
1396+
expectPath(ast, 'children.0.body.nodes.1.content.value').to.eql(
1397+
'This is another description\n it can have multiple lines\n',
1398+
);
1399+
1400+
ast = toLiquidAST(`
1401+
{% doc -%}
1402+
@description This is a description
1403+
@example This is an example
1404+
@param {String} paramWithDescription - param with description
1405+
{% enddoc %}
1406+
`);
1407+
expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocDescriptionNode');
1408+
expectPath(ast, 'children.0.body.nodes.0.content.value').to.eql('This is a description\n');
1409+
1410+
expectPath(ast, 'children.0.body.nodes.1.type').to.eql('LiquidDocExampleNode');
1411+
expectPath(ast, 'children.0.body.nodes.1.name').to.eql('example');
1412+
expectPath(ast, 'children.0.body.nodes.1.exampleContent.value').to.eql(
1413+
'This is an example\n',
1414+
);
1415+
1416+
expectPath(ast, 'children.0.body.nodes.2.type').to.eql('LiquidDocParamNode');
1417+
expectPath(ast, 'children.0.body.nodes.2.name').to.eql('param');
1418+
expectPath(ast, 'children.0.body.nodes.2.paramName.value').to.eql('paramWithDescription');
1419+
expectPath(ast, 'children.0.body.nodes.2.paramDescription.value').to.eql(
1420+
'param with description',
1421+
);
13851422
});
13861423

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

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

+20-1
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,8 @@ export type LiquidHtmlNode =
109109
| LiquidComparison
110110
| TextNode
111111
| LiquidDocParamNode
112-
| LiquidDocExampleNode;
112+
| LiquidDocExampleNode
113+
| LiquidDocDescriptionNode;
113114

114115
/** The root node of all LiquidHTML ASTs. */
115116
export interface DocumentNode extends ASTNode<NodeTypes.Document> {
@@ -769,6 +770,13 @@ export interface LiquidDocParamNode extends ASTNode<NodeTypes.LiquidDocParamNode
769770
required: boolean;
770771
}
771772

773+
/** Represents a `@description` node in a LiquidDoc comment - `@description descriptionContent` */
774+
export interface LiquidDocDescriptionNode extends ASTNode<NodeTypes.LiquidDocDescriptionNode> {
775+
name: 'description';
776+
/** The contents of the description (e.g. "This is a description"). Can be multiline. */
777+
content: TextNode;
778+
}
779+
772780
/** Represents a `@example` node in a LiquidDoc comment - `@example exampleContent` */
773781
export interface LiquidDocExampleNode extends ASTNode<NodeTypes.LiquidDocExampleNode> {
774782
name: 'example';
@@ -1304,6 +1312,17 @@ function buildAst(
13041312
break;
13051313
}
13061314

1315+
case ConcreteNodeTypes.LiquidDocDescriptionNode: {
1316+
builder.push({
1317+
type: NodeTypes.LiquidDocDescriptionNode,
1318+
name: node.name,
1319+
position: position(node),
1320+
source: node.source,
1321+
content: toTextNode(node.content),
1322+
});
1323+
break;
1324+
}
1325+
13071326
case ConcreteNodeTypes.LiquidDocExampleNode: {
13081327
builder.push({
13091328
type: NodeTypes.LiquidDocExampleNode,

‎packages/liquid-html-parser/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export enum NodeTypes {
4444
RawMarkup = 'RawMarkup',
4545
RenderMarkup = 'RenderMarkup',
4646
RenderVariableExpression = 'RenderVariableExpression',
47+
LiquidDocDescriptionNode = 'LiquidDocDescriptionNode',
4748
LiquidDocParamNode = 'LiquidDocParamNode',
4849
LiquidDocExampleNode = 'LiquidDocExampleNode',
4950
}

‎packages/prettier-plugin-liquid/src/printer/preprocess/augment-with-css-properties.ts

+2
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ function getCssDisplay(node: AugmentedNode<WithSiblings>, options: LiquidParserO
130130
case NodeTypes.Comparison:
131131
case NodeTypes.LiquidDocParamNode:
132132
case NodeTypes.LiquidDocExampleNode:
133+
case NodeTypes.LiquidDocDescriptionNode:
133134
return 'should not be relevant';
134135

135136
default:
@@ -237,6 +238,7 @@ function getNodeCssStyleWhiteSpace(
237238
case NodeTypes.Comparison:
238239
case NodeTypes.LiquidDocParamNode:
239240
case NodeTypes.LiquidDocExampleNode:
241+
case NodeTypes.LiquidDocDescriptionNode:
240242
return 'should not be relevant';
241243

242244
default:

‎packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts

+4
Original file line numberDiff line numberDiff line change
@@ -565,6 +565,10 @@ function printNode(
565565
return printLiquidDocExample(path as AstPath<LiquidDocExampleNode>, options, print, args);
566566
}
567567

568+
case NodeTypes.LiquidDocDescriptionNode: {
569+
return '';
570+
}
571+
568572
default: {
569573
return assertNever(node);
570574
}

‎packages/theme-language-server-common/src/completions/params/LiquidCompletionParams.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -403,10 +403,9 @@ function findCurrentNode(
403403
case NodeTypes.LiquidLiteral:
404404
case NodeTypes.String:
405405
case NodeTypes.Number:
406-
case NodeTypes.LiquidDocParamNode: {
407-
break;
408-
}
409-
case NodeTypes.LiquidDocExampleNode: {
406+
case NodeTypes.LiquidDocParamNode:
407+
case NodeTypes.LiquidDocExampleNode:
408+
case NodeTypes.LiquidDocDescriptionNode: {
410409
break;
411410
}
412411

0 commit comments

Comments
 (0)
Please sign in to comment.