From a31f0334afa34793c8e493f61ccbe0a881039c10 Mon Sep 17 00:00:00 2001 From: James Meng Date: Tue, 7 Jan 2025 14:52:12 -0800 Subject: [PATCH 01/16] Parser + Prettier support for {% doc %} tag and @param node in Liquid Doc --- .../grammar/liquid-html.ohm | 11 +- .../src/stage-1-cst.spec.ts | 142 +++++++++++++----- .../liquid-html-parser/src/stage-1-cst.ts | 74 ++++++++- .../src/stage-2-ast.spec.ts | 41 +++-- .../liquid-html-parser/src/stage-2-ast.ts | 46 +++++- packages/liquid-html-parser/src/types.ts | 1 + .../preprocess/augment-with-css-properties.ts | 2 + .../src/printer/print/liquid.ts | 47 +++++- .../src/printer/printer-liquid-html.ts | 12 ++ .../src/test/liquid-doc/fixed.liquid | 29 ++++ .../src/test/liquid-doc/index.liquid | 30 ++++ .../src/test/liquid-doc/index.spec.ts | 7 + .../params/LiquidCompletionParams.ts | 3 +- 13 files changed, 391 insertions(+), 54 deletions(-) create mode 100644 packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid create mode 100644 packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid create mode 100644 packages/prettier-plugin-liquid/src/test/liquid-doc/index.spec.ts diff --git a/packages/liquid-html-parser/grammar/liquid-html.ohm b/packages/liquid-html-parser/grammar/liquid-html.ohm index 5d6481c26..56326f85e 100644 --- a/packages/liquid-html-parser/grammar/liquid-html.ohm +++ b/packages/liquid-html-parser/grammar/liquid-html.ohm @@ -389,7 +389,16 @@ LiquidStatement <: Liquid { } LiquidDoc <: Helpers { - Node := (TextNode)* + Node := (LiquidDocNode | TextNode)* + LiquidDocNode = + | paramNode + | fallbackNode + + fallbackNode = "@" anyExceptStar + paramNode = "@param" space* (paramType)? space* (paramName)? space* "-"? paramDescription? + paramType = "{" anyExceptStar<"}"> "}" + paramName = identifierCharacter+ + paramDescription = (~newline identifierCharacter | space)+ } LiquidHTML <: Liquid { diff --git a/packages/liquid-html-parser/src/stage-1-cst.spec.ts b/packages/liquid-html-parser/src/stage-1-cst.spec.ts index 7f02c68a8..8f5625814 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts @@ -981,53 +981,125 @@ describe('Unit: Stage 1 (CST)', () => { expectPath(cst, '0.blockEndLocEnd').to.equal(testStr.length); } }); + }); - it('should parse doc tags', () => { - for (const { toCST, expectPath } of testCases) { - const testStr = `{% doc -%} Renders loading-spinner. {%- enddoc %}`; - + describe('Case: LiquidDoc', () => { + for (const { toCST, expectPath } of testCases) { + it('should parse basic doc tag structure', () => { + const testStr = `{% doc -%} content {%- enddoc %}`; cst = toCST(testStr); + expectPath(cst, '0.type').to.equal('LiquidRawTag'); expectPath(cst, '0.name').to.equal('doc'); - expectPath(cst, '0.body').to.include('Renders loading-spinner'); expectPath(cst, '0.whitespaceStart').to.equal(''); expectPath(cst, '0.whitespaceEnd').to.equal('-'); expectPath(cst, '0.delimiterWhitespaceStart').to.equal('-'); expectPath(cst, '0.delimiterWhitespaceEnd').to.equal(''); - expectPath(cst, '0.blockStartLocStart').to.equal(0); - expectPath(cst, '0.blockStartLocEnd').to.equal(0 + '{% doc -%}'.length); + expectPath(cst, '0.blockStartLocStart').to.equal(testStr.indexOf('{% doc -%}')); + expectPath(cst, '0.blockStartLocEnd').to.equal( + testStr.indexOf('{% doc -%}') + '{% doc -%}'.length, + ); expectPath(cst, '0.blockEndLocStart').to.equal(testStr.length - '{%- enddoc %}'.length); expectPath(cst, '0.blockEndLocEnd').to.equal(testStr.length); - expectPath(cst, '0.children').to.deep.equal([ - { - locEnd: 35, - locStart: 11, - source: '{% doc -%} Renders loading-spinner. {%- enddoc %}', - type: 'TextNode', - value: 'Renders loading-spinner.', - }, - ]); - } - }); + }); - it('should parse tag open / close', () => { - BLOCKS.forEach((block: string) => { - for (const { toCST, expectPath } of testCases) { - cst = toCST(`{% ${block} args -%}{%- end${block} %}`); - expectPath(cst, '0.type').to.equal('LiquidTagOpen', block); - expectPath(cst, '0.name').to.equal(block); - expectPath(cst, '0.whitespaceStart').to.equal(null); - expectPath(cst, '0.whitespaceEnd').to.equal('-'); - if (!NamedTags.hasOwnProperty(block)) { - expectPath(cst, '0.markup').to.equal('args'); - } - expectPath(cst, '1.type').to.equal('LiquidTagClose'); - expectPath(cst, '1.name').to.equal(block); - expectPath(cst, '1.whitespaceStart').to.equal('-'); - expectPath(cst, '1.whitespaceEnd').to.equal(null); - } + it('should parse @param with no name or description', () => { + const testStr = `{% doc %} @param {% enddoc %}`; + cst = toCST(testStr); + + expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode'); + expectPath(cst, '0.children.0.value').to.equal('@param'); + expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode'); + expectPath(cst, '0.children.0.paramName.value').to.equal(''); + expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode'); + expectPath(cst, '0.children.0.paramDescription.value').to.equal(''); }); - }); + + it('should parse @param with name but no description', () => { + const testStr = `{% doc %} @param paramWithNoDescription {% enddoc %}`; + cst = toCST(testStr); + + expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode'); + 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(''); + }); + + it('should parse @param with name and description', () => { + const testStr = `{% doc %} @param paramWithDescription param with description {% enddoc %}`; + cst = toCST(testStr); + + expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode'); + expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode'); + expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithDescription'); + expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode'); + expectPath(cst, '0.children.0.paramDescription.value').to.equal('param with description'); + }); + + it('should parse @param with type', () => { + const testStr = `{% doc %} @param {String} paramWithType {% enddoc %}`; + cst = toCST(testStr); + + expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode'); + expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithType'); + + expectPath(cst, '0.children.0.paramType.type').to.equal('TextNode'); + expectPath(cst, '0.children.0.paramType.value').to.equal('String'); + expectPath(cst, '0.children.0.paramType.locStart').to.equal(testStr.indexOf('{String}')); + expectPath(cst, '0.children.0.paramType.locEnd').to.equal( + testStr.indexOf('{String}') + '{String}'.length, + ); + }); + + it('should parse @param with description seperated by a dash', () => { + const testStr = `{% doc %} + @param dashWithSpace - param with description + @param dashWithNoSpace -param with description + @param notDashSeparated param with description + {% enddoc %}`; + cst = toCST(testStr); + + expectPath(cst, '0.children.0.paramDescription.dashSeparated').to.equal(true); + expectPath(cst, '0.children.1.paramDescription.dashSeparated').to.equal(true); + expectPath(cst, '0.children.2.paramDescription.dashSeparated').to.equal(false); + }); + + it('should parse unsupported doc tags as text nodes', () => { + const testStr = `{% doc %} @unsupported this tag is not supported {% enddoc %}`; + cst = toCST(testStr); + + expectPath(cst, '0.children.0.type').to.equal('TextNode'); + expectPath(cst, '0.children.0.value').to.equal('@unsupported this tag is not supported'); + }); + + it('should parse multiple doc tags in sequence', () => { + const testStr = `{% doc %} + @param param1 first parameter + @param param2 second parameter + @unsupported + {% enddoc %}`; + + cst = toCST(testStr); + + expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode'); + expectPath(cst, '0.children.0.paramName.value').to.equal('param1'); + expectPath(cst, '0.children.0.paramDescription.value').to.equal('first parameter'); + + expectPath(cst, '0.children.1.type').to.equal('LiquidDocParamNode'); + expectPath(cst, '0.children.1.paramName.value').to.equal('param2'); + expectPath(cst, '0.children.1.paramDescription.value').to.equal('second parameter'); + + expectPath(cst, '0.children.2.type').to.equal('TextNode'); + expectPath(cst, '0.children.2.value').to.equal('@unsupported'); + }); + } }); }); diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index 4fdeffb8c..3c843a68b 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -83,6 +83,8 @@ export enum ConcreteNodeTypes { PaginateMarkup = 'PaginateMarkup', RenderVariableExpression = 'RenderVariableExpression', ContentForNamedArgument = 'ContentForNamedArgument', + + LiquidDocParamNode = 'LiquidDocParamNode', } export const LiquidLiteralValues = { @@ -105,6 +107,22 @@ export interface ConcreteBasicNode { locEnd: number; } +// todo: change param and description to concrete nodes +export interface ConcreteLiquidDocParamNode + extends ConcreteBasicNode { + name: string; + value: string; + paramName: ConcreteTextNode; + paramDescription: ConcreteLiquidDocParamDescription; + paramType: ConcreteTextNode; +} + +export interface ConcreteLiquidDocParamDescription + extends ConcreteBasicNode { + dashSeparated: boolean; + value: string; +} + export interface ConcreteHtmlNodeBase extends ConcreteBasicNode { attrList?: ConcreteAttributeNode[]; } @@ -440,10 +458,13 @@ export type LiquidConcreteNode = | ConcreteTextNode | ConcreteYamlFrontmatterNode; -export type LiquidHtmlCST = LiquidHtmlConcreteNode[]; +export type LiquidHtmlCST = LiquidHtmlConcreteNode[] | LiquidDocCST; export type LiquidCST = LiquidConcreteNode[]; +type LiquidDocCST = LiquidDocConcreteNode[]; +export type LiquidDocConcreteNode = ConcreteLiquidDocParamNode; + interface Mapping { [k: string]: number | TemplateMapping | TopLevelFunctionMapping; } @@ -1306,7 +1327,7 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) const LiquidDocMappings: Mapping = { Node: 0, - TextNode: { + textNode: { type: ConcreteNodeTypes.TextNode, value: function () { return (this as any).sourceString; @@ -1315,6 +1336,55 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) locEnd, source, }, + paramNode: { + type: ConcreteNodeTypes.LiquidDocParamNode, + name: 0, + value: 0, + locStart, + locEnd, + source, + paramType: function (nodes: Node[]) { + const typeNode = nodes[2]; + return { + type: ConcreteNodeTypes.TextNode, + value: typeNode.sourceString.slice(1, -1).trim(), + source, + locStart: offset + typeNode.source.startIdx, + locEnd: offset + typeNode.source.endIdx, + }; + }, + paramName: function (nodes: Node[]) { + const nameNode = nodes[4]; + return { + type: ConcreteNodeTypes.TextNode, + value: nameNode.sourceString.trim(), + source, + locStart: offset + nameNode.source.startIdx, + locEnd: offset + nameNode.source.endIdx, + }; + }, + paramDescription: function (nodes: Node[]) { + const dashNode = nodes[6]; + const descriptionNode = nodes[7]; + return { + type: ConcreteNodeTypes.TextNode, + value: descriptionNode.sourceString.trim(), + source, + locStart: offset + descriptionNode.source.startIdx, + locEnd: offset + descriptionNode.source.endIdx, + dashSeparated: dashNode.sourceString.trim() === '-', + }; + }, + }, + fallbackNode: { + type: ConcreteNodeTypes.TextNode, + value: function () { + return (this as any).sourceString.trim(); + }, + locStart, + locEnd, + source, + }, }; return toAST(res, LiquidDocMappings); diff --git a/packages/liquid-html-parser/src/stage-2-ast.spec.ts b/packages/liquid-html-parser/src/stage-2-ast.spec.ts index 84848ccb2..04d565f34 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.spec.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.spec.ts @@ -1229,22 +1229,37 @@ describe('Unit: Stage 2 (AST)', () => { expectPath(ast, 'children.0.body.type').toEqual('RawMarkup'); expectPath(ast, 'children.0.body.nodes').toEqual([]); - ast = toLiquidAST(`{% doc -%} single line doc {%- enddoc %}`); - expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); - expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.body.value').to.eql(' single line doc '); - expectPath(ast, 'children.0.body.nodes.0.type').toEqual('TextNode'); - - ast = toLiquidAST(`{% doc -%} - multi line doc - multi line doc - {%- enddoc %}`); + ast = toLiquidAST(` + {% doc -%} + @param asdf + @param {String} paramWithDescription - param with description + @unsupported this node falls back to a text node + {%- enddoc %} + `); expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); expectPath(ast, 'children.0.name').to.eql('doc'); - expectPath(ast, 'children.0.body.nodes.0.value').to.eql( - `multi line doc\n multi line doc`, + 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.paramName.type').to.eql('TextNode'); + expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('asdf'); + expectPath(ast, 'children.0.body.nodes.0.paramDescription.type').to.eql('TextNode'); + expectPath(ast, 'children.0.body.nodes.0.paramDescription.value').to.eql(''); + expectPath(ast, 'children.0.body.nodes.0.paramDescription.dashSeparated').to.eql(false); + 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.paramName.type').to.eql('TextNode'); + expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('paramWithDescription'); + expectPath(ast, 'children.0.body.nodes.1.paramDescription.type').to.eql('TextNode'); + expectPath(ast, 'children.0.body.nodes.1.paramDescription.dashSeparated').to.eql(true); + expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql( + 'param with 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.2.type').to.eql('TextNode'); + expectPath(ast, 'children.0.body.nodes.2.value').to.eql( + '@unsupported this node falls back to a text node', ); - expectPath(ast, 'children.0.body.nodes.0.type').toEqual('TextNode'); }); it('should parse unclosed tables with assignments', () => { diff --git a/packages/liquid-html-parser/src/stage-2-ast.ts b/packages/liquid-html-parser/src/stage-2-ast.ts index 15c7e61cb..28098e515 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.ts @@ -107,7 +107,8 @@ export type LiquidHtmlNode = | RenderVariableExpression | LiquidLogicalExpression | LiquidComparison - | TextNode; + | TextNode + | LiquidDocParamNode; /** The root node of all LiquidHTML ASTs. */ export interface DocumentNode extends ASTNode { @@ -754,6 +755,19 @@ export interface TextNode extends ASTNode { value: string; } +export interface LiquidDocParamNode extends ASTNode { + name: string; + value: string; + paramDescription: LiquidDocParamDescription; + paramName: TextNode; + paramType: TextNode; +} + +export interface LiquidDocParamDescription extends ASTNode { + dashSeparated: boolean; + value: string; +} + export interface ASTNode { /** * The type of the node, as a string. @@ -1268,6 +1282,36 @@ function buildAst( break; } + case ConcreteNodeTypes.LiquidDocParamNode: { + builder.push({ + type: NodeTypes.LiquidDocParamNode, + name: node.name, + position: position(node), + source: node.source, + value: node.value, + paramName: { + type: NodeTypes.TextNode, + value: node.paramName.value, + position: position(node.paramName), + source: node.paramName.source, + }, + paramDescription: { + type: NodeTypes.TextNode, + value: node.paramDescription.value, + position: position(node.paramDescription), + source: node.paramDescription.source, + dashSeparated: node.paramDescription.dashSeparated, + }, + paramType: { + type: NodeTypes.TextNode, + value: node.paramType.value, + position: position(node.paramType), + source: node.paramType.source, + }, + }); + break; + } + default: { assertNever(node); } diff --git a/packages/liquid-html-parser/src/types.ts b/packages/liquid-html-parser/src/types.ts index 29648aebb..49efa1bf3 100644 --- a/packages/liquid-html-parser/src/types.ts +++ b/packages/liquid-html-parser/src/types.ts @@ -44,6 +44,7 @@ export enum NodeTypes { RawMarkup = 'RawMarkup', RenderMarkup = 'RenderMarkup', RenderVariableExpression = 'RenderVariableExpression', + LiquidDocParamNode = 'LiquidDocParamNode', } // These are officially supported with special node types diff --git a/packages/prettier-plugin-liquid/src/printer/preprocess/augment-with-css-properties.ts b/packages/prettier-plugin-liquid/src/printer/preprocess/augment-with-css-properties.ts index cc16285e5..4ad565c6d 100644 --- a/packages/prettier-plugin-liquid/src/printer/preprocess/augment-with-css-properties.ts +++ b/packages/prettier-plugin-liquid/src/printer/preprocess/augment-with-css-properties.ts @@ -128,6 +128,7 @@ function getCssDisplay(node: AugmentedNode, options: LiquidParserO case NodeTypes.RenderVariableExpression: case NodeTypes.LogicalExpression: case NodeTypes.Comparison: + case NodeTypes.LiquidDocParamNode: return 'should not be relevant'; default: @@ -233,6 +234,7 @@ function getNodeCssStyleWhiteSpace( case NodeTypes.RenderVariableExpression: case NodeTypes.LogicalExpression: case NodeTypes.Comparison: + case NodeTypes.LiquidDocParamNode: return 'should not be relevant'; default: diff --git a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts index 778c7262e..bde225b08 100644 --- a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts +++ b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts @@ -1,4 +1,10 @@ -import { NodeTypes, NamedTags, isBranchedTag } from '@shopify/liquid-html-parser'; +import { + NodeTypes, + NamedTags, + isBranchedTag, + RawMarkup, + LiquidDocParamNode, +} from '@shopify/liquid-html-parser'; import { Doc, doc } from 'prettier'; import { @@ -490,6 +496,45 @@ export function printLiquidRawTag( return [blockStart, ...body, blockEnd]; } +export function printLiquidDoc( + path: AstPath, + _options: LiquidParserOptions, + print: LiquidPrinter, + _args: LiquidPrinterArgs, +) { + const body = path.map((p: any) => print(p), 'nodes'); + return [indent([hardline, join(hardline, body)]), hardline]; +} + +export function printLiquidDocParam( + path: AstPath, + _options: LiquidParserOptions, + _print: LiquidPrinter, + _args: LiquidPrinterArgs, +): Doc { + const node = path.getValue(); + const parts: Doc[] = ['@param']; + + if (node.paramType.value) { + parts.push(' ', `{${node.paramType.value}}`); + } + + if (node.paramName.value) { + parts.push(' ', node.paramName.value); + } + + if (node.paramDescription.value) { + const normalizedDescription = node.paramDescription.value.replace(/\s+/g, ' ').trim(); + if (node.paramDescription.dashSeparated) { + parts.push(' - ', normalizedDescription); + } else { + parts.push(' ', normalizedDescription); + } + } + + return parts; +} + function innerLeadingWhitespace(node: LiquidTag | LiquidBranch) { if (!node.firstChild) { if (node.isDanglingWhitespaceSensitive && node.hasDanglingWhitespace) { diff --git a/packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts b/packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts index 3905c8008..781db0453 100644 --- a/packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts +++ b/packages/prettier-plugin-liquid/src/printer/printer-liquid-html.ts @@ -1,5 +1,6 @@ import { getConditionalComment, + LiquidDocParamNode, NodeTypes, Position, RawMarkupKinds, @@ -30,6 +31,7 @@ import { LiquidTag, LiquidVariableOutput, nonTraversableProperties, + RawMarkup, TextNode, } from '../types'; import { assertNever } from '../utils'; @@ -40,9 +42,11 @@ import { printChildren } from './print/children'; import { printElement } from './print/element'; import { printLiquidBranch, + printLiquidDoc, printLiquidRawTag, printLiquidTag, printLiquidVariableOutput, + printLiquidDocParam, } from './print/liquid'; import { printClosingTagSuffix, printOpeningTagPrefix } from './print/tag'; import { bodyLines, hasLineBreakInRange, isEmpty, isTextLikeNode, reindent } from './utils'; @@ -210,6 +214,10 @@ function printNode( } case NodeTypes.RawMarkup: { + if (node.parentNode?.name === 'doc') { + return printLiquidDoc(path as AstPath, options, print, args); + } + const isRawMarkupIdentationSensitive = () => { switch (node.kind) { case RawMarkupKinds.typescript: @@ -547,6 +555,10 @@ function printNode( return [...doc, ...lookups]; } + case NodeTypes.LiquidDocParamNode: { + return printLiquidDocParam(path as AstPath, options, print, args); + } + default: { return assertNever(node); } diff --git a/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid b/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid new file mode 100644 index 000000000..7e94a2774 --- /dev/null +++ b/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid @@ -0,0 +1,29 @@ +It should indent the body +{% doc %} + @param paramName param with description +{% enddoc %} + +It should not dedent to root +{% doc %} + @param paramName param with description +{% enddoc %} + +It should trim whitespace between nodes +{% doc %} + @param paramName param with description +{% enddoc %} + +It should format the param type +{% doc %} + @param {string} paramName param with description +{% enddoc %} + +It should format the param description with a dash separator +{% doc %} + @param paramName - param with description +{% enddoc %} + +It should normalize the param description +{% doc %} + @param paramName param with description +{% enddoc %} diff --git a/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid b/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid new file mode 100644 index 000000000..2d087a985 --- /dev/null +++ b/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid @@ -0,0 +1,30 @@ +It should indent the body +{% doc %} +@param paramName param with description +{% enddoc %} + +It should not dedent to root +{% doc %} +@param paramName param with description +{% enddoc %} + +It should trim whitespace between nodes +{% doc %} +@param paramName param with description +{% enddoc %} + +It should format the param type +{% doc %} +@param { string } paramName param with description +{% enddoc %} + +It should format the param description with a dash separator +{% doc %} +@param paramName - param with description +{% enddoc %} + +It should normalize the param description +{% doc %} +@param paramName param with description +{% enddoc %} + diff --git a/packages/prettier-plugin-liquid/src/test/liquid-doc/index.spec.ts b/packages/prettier-plugin-liquid/src/test/liquid-doc/index.spec.ts new file mode 100644 index 000000000..bf0e16d16 --- /dev/null +++ b/packages/prettier-plugin-liquid/src/test/liquid-doc/index.spec.ts @@ -0,0 +1,7 @@ +import { test } from 'vitest'; +import { assertFormattedEqualsFixed } from '../test-helpers'; +import * as path from 'path'; + +test('Unit: liquid-doc', async () => { + await assertFormattedEqualsFixed(__dirname); +}); diff --git a/packages/theme-language-server-common/src/completions/params/LiquidCompletionParams.ts b/packages/theme-language-server-common/src/completions/params/LiquidCompletionParams.ts index 75d6e1e75..9ed9bcaf0 100644 --- a/packages/theme-language-server-common/src/completions/params/LiquidCompletionParams.ts +++ b/packages/theme-language-server-common/src/completions/params/LiquidCompletionParams.ts @@ -402,7 +402,8 @@ function findCurrentNode( case NodeTypes.TextNode: case NodeTypes.LiquidLiteral: case NodeTypes.String: - case NodeTypes.Number: { + case NodeTypes.Number: + case NodeTypes.LiquidDocParamNode: { break; } From d02e8c265b2c71deffd8a44d313ada73d4316839 Mon Sep 17 00:00:00 2001 From: James Meng Date: Tue, 7 Jan 2025 15:41:31 -0800 Subject: [PATCH 02/16] Make paramName required --- packages/liquid-html-parser/grammar/liquid-html.ohm | 2 +- packages/liquid-html-parser/src/stage-1-cst.spec.ts | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/liquid-html-parser/grammar/liquid-html.ohm b/packages/liquid-html-parser/grammar/liquid-html.ohm index 56326f85e..7bf9d64d8 100644 --- a/packages/liquid-html-parser/grammar/liquid-html.ohm +++ b/packages/liquid-html-parser/grammar/liquid-html.ohm @@ -395,7 +395,7 @@ LiquidDoc <: Helpers { | fallbackNode fallbackNode = "@" anyExceptStar - paramNode = "@param" space* (paramType)? space* (paramName)? space* "-"? paramDescription? + paramNode = "@param" space* (paramType)? space* paramName space* "-"? paramDescription? paramType = "{" anyExceptStar<"}"> "}" paramName = identifierCharacter+ paramDescription = (~newline identifierCharacter | space)+ diff --git a/packages/liquid-html-parser/src/stage-1-cst.spec.ts b/packages/liquid-html-parser/src/stage-1-cst.spec.ts index 8f5625814..287b782c1 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts @@ -1003,19 +1003,15 @@ describe('Unit: Stage 1 (CST)', () => { expectPath(cst, '0.blockEndLocEnd').to.equal(testStr.length); }); - it('should parse @param with no name or description', () => { + it('should not parse @param without a name', () => { const testStr = `{% doc %} @param {% enddoc %}`; cst = toCST(testStr); - expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode'); + expectPath(cst, '0.children.0.type').to.equal('TextNode'); expectPath(cst, '0.children.0.value').to.equal('@param'); - expectPath(cst, '0.children.0.paramName.type').to.equal('TextNode'); - expectPath(cst, '0.children.0.paramName.value').to.equal(''); - expectPath(cst, '0.children.0.paramDescription.type').to.equal('TextNode'); - expectPath(cst, '0.children.0.paramDescription.value').to.equal(''); }); - it('should parse @param with name but no description', () => { + it('should parse @param with name', () => { const testStr = `{% doc %} @param paramWithNoDescription {% enddoc %}`; cst = toCST(testStr); From 3f3df3d7fb4a582622f4956b0971947f930ccd95 Mon Sep 17 00:00:00 2001 From: James Meng Date: Tue, 7 Jan 2025 14:37:57 -0800 Subject: [PATCH 03/16] Make LiquidDoc param descriptions more flexible --- packages/liquid-html-parser/grammar/liquid-html.ohm | 4 ++-- packages/liquid-html-parser/src/stage-1-cst.spec.ts | 9 +++++++++ packages/liquid-html-parser/src/stage-2-ast.spec.ts | 4 ++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/liquid-html-parser/grammar/liquid-html.ohm b/packages/liquid-html-parser/grammar/liquid-html.ohm index 7bf9d64d8..b7397cec2 100644 --- a/packages/liquid-html-parser/grammar/liquid-html.ohm +++ b/packages/liquid-html-parser/grammar/liquid-html.ohm @@ -395,10 +395,10 @@ LiquidDoc <: Helpers { | fallbackNode fallbackNode = "@" anyExceptStar - paramNode = "@param" space* (paramType)? space* paramName space* "-"? paramDescription? + paramNode = "@param" space* paramType? space* paramName (space* "-")? paramDescription paramType = "{" anyExceptStar<"}"> "}" paramName = identifierCharacter+ - paramDescription = (~newline identifierCharacter | space)+ + paramDescription = anyExceptStar<(newline | end)> } LiquidHTML <: Liquid { diff --git a/packages/liquid-html-parser/src/stage-1-cst.spec.ts b/packages/liquid-html-parser/src/stage-1-cst.spec.ts index 287b782c1..4881e156e 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts @@ -1067,6 +1067,15 @@ describe('Unit: Stage 1 (CST)', () => { expectPath(cst, '0.children.2.paramDescription.dashSeparated').to.equal(false); }); + it('should accept punctation inside the param description body', () => { + const testStr = `{% doc %} @param paramName paramDescription - asdf . \`should\` work {% enddoc %}`; + cst = toCST(testStr); + + expectPath(cst, '0.children.0.paramDescription.value').to.equal( + 'paramDescription - asdf . `should` work', + ); + }); + it('should parse unsupported doc tags as text nodes', () => { const testStr = `{% doc %} @unsupported this tag is not supported {% enddoc %}`; cst = toCST(testStr); diff --git a/packages/liquid-html-parser/src/stage-2-ast.spec.ts b/packages/liquid-html-parser/src/stage-2-ast.spec.ts index 04d565f34..d3cfb4f6f 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.spec.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.spec.ts @@ -1232,7 +1232,7 @@ describe('Unit: Stage 2 (AST)', () => { ast = toLiquidAST(` {% doc -%} @param asdf - @param {String} paramWithDescription - param with description + @param {String} paramWithDescription - param with description and \`punctation\`. This is still a valid param description. @unsupported this node falls back to a text node {%- enddoc %} `); @@ -1252,7 +1252,7 @@ describe('Unit: Stage 2 (AST)', () => { expectPath(ast, 'children.0.body.nodes.1.paramDescription.type').to.eql('TextNode'); expectPath(ast, 'children.0.body.nodes.1.paramDescription.dashSeparated').to.eql(true); expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql( - 'param with description', + 'param with description and `punctation`. This is still a valid param 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'); From 3d6a40f9aeb27f7fd5d31013bed040e71eeccdd1 Mon Sep 17 00:00:00 2001 From: James Meng Date: Fri, 10 Jan 2025 10:20:31 -0800 Subject: [PATCH 04/16] Remove artifact comment --- packages/liquid-html-parser/src/stage-1-cst.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index 3c843a68b..2acaf2ddc 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -107,7 +107,6 @@ export interface ConcreteBasicNode { locEnd: number; } -// todo: change param and description to concrete nodes export interface ConcreteLiquidDocParamNode extends ConcreteBasicNode { name: string; From 6d62a212d63ebce362c50b77034bd6981c3a8000 Mon Sep 17 00:00:00 2001 From: James Meng Date: Fri, 10 Jan 2025 10:23:23 -0800 Subject: [PATCH 05/16] Improve clarity of fallback node in stage-1 CST test --- packages/liquid-html-parser/src/stage-1-cst.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/liquid-html-parser/src/stage-1-cst.spec.ts b/packages/liquid-html-parser/src/stage-1-cst.spec.ts index 4881e156e..a426a960c 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts @@ -1076,12 +1076,14 @@ describe('Unit: Stage 1 (CST)', () => { ); }); - it('should parse unsupported doc tags as text nodes', () => { - const testStr = `{% doc %} @unsupported this tag is not supported {% enddoc %}`; + it('should parse fallback nodes as text nodes', () => { + const testStr = `{% doc %} @unsupported this should get matched as a fallback node and translated into a text node {% enddoc %}`; cst = toCST(testStr); expectPath(cst, '0.children.0.type').to.equal('TextNode'); - expectPath(cst, '0.children.0.value').to.equal('@unsupported this tag is not supported'); + expectPath(cst, '0.children.0.value').to.equal( + '@unsupported this should get matched as a fallback node and translated into a text node', + ); }); it('should parse multiple doc tags in sequence', () => { From b2697da432b323ba0f5b84e0d9ac10742a11c788 Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 13 Jan 2025 11:41:22 -0800 Subject: [PATCH 06/16] Remove unused value property for LiquidDocParamNode --- packages/liquid-html-parser/src/stage-1-cst.ts | 2 -- packages/liquid-html-parser/src/stage-2-ast.ts | 2 -- 2 files changed, 4 deletions(-) diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index 2acaf2ddc..a64226076 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -110,7 +110,6 @@ export interface ConcreteBasicNode { export interface ConcreteLiquidDocParamNode extends ConcreteBasicNode { name: string; - value: string; paramName: ConcreteTextNode; paramDescription: ConcreteLiquidDocParamDescription; paramType: ConcreteTextNode; @@ -1338,7 +1337,6 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) paramNode: { type: ConcreteNodeTypes.LiquidDocParamNode, name: 0, - value: 0, locStart, locEnd, source, diff --git a/packages/liquid-html-parser/src/stage-2-ast.ts b/packages/liquid-html-parser/src/stage-2-ast.ts index 28098e515..01a3896e8 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.ts @@ -757,7 +757,6 @@ export interface TextNode extends ASTNode { export interface LiquidDocParamNode extends ASTNode { name: string; - value: string; paramDescription: LiquidDocParamDescription; paramName: TextNode; paramType: TextNode; @@ -1288,7 +1287,6 @@ function buildAst( name: node.name, position: position(node), source: node.source, - value: node.value, paramName: { type: NodeTypes.TextNode, value: node.paramName.value, From f545c19fa947f5b3cd57038546c7cb530094b47f Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 13 Jan 2025 11:43:01 -0800 Subject: [PATCH 07/16] Refactor LiquidDocParamNode to enforce 'param' as a constant name --- packages/liquid-html-parser/src/stage-1-cst.ts | 3 +-- packages/liquid-html-parser/src/stage-2-ast.ts | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index a64226076..f8ecb4837 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -109,7 +109,7 @@ export interface ConcreteBasicNode { export interface ConcreteLiquidDocParamNode extends ConcreteBasicNode { - name: string; + name: 'param'; paramName: ConcreteTextNode; paramDescription: ConcreteLiquidDocParamDescription; paramType: ConcreteTextNode; @@ -1336,7 +1336,6 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) }, paramNode: { type: ConcreteNodeTypes.LiquidDocParamNode, - name: 0, locStart, locEnd, source, diff --git a/packages/liquid-html-parser/src/stage-2-ast.ts b/packages/liquid-html-parser/src/stage-2-ast.ts index 01a3896e8..967f435c5 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.ts @@ -756,7 +756,7 @@ export interface TextNode extends ASTNode { } export interface LiquidDocParamNode extends ASTNode { - name: string; + name: 'param'; paramDescription: LiquidDocParamDescription; paramName: TextNode; paramType: TextNode; From 24d9d746e96076972573ed72b6d43b7000933994 Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 13 Jan 2025 12:01:25 -0800 Subject: [PATCH 08/16] Improve type clarity by making LiquidDocParamNode description and type optional --- .../liquid-html-parser/src/stage-1-cst.ts | 4 +- .../liquid-html-parser/src/stage-2-ast.ts | 50 +++++++++++++------ .../src/printer/print/liquid.ts | 4 +- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index f8ecb4837..fa52ac691 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -111,8 +111,8 @@ export interface ConcreteLiquidDocParamNode extends ConcreteBasicNode { name: 'param'; paramName: ConcreteTextNode; - paramDescription: ConcreteLiquidDocParamDescription; - paramType: ConcreteTextNode; + paramDescription: ConcreteLiquidDocParamDescription | null; + paramType: ConcreteTextNode | null; } export interface ConcreteLiquidDocParamDescription diff --git a/packages/liquid-html-parser/src/stage-2-ast.ts b/packages/liquid-html-parser/src/stage-2-ast.ts index 967f435c5..384a0a2dc 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.ts @@ -73,6 +73,7 @@ import { LiquidHtmlConcreteNode, ConcreteLiquidTagBaseCase, ConcreteLiquidTagContentForMarkup, + ConcreteLiquidDocParamDescription, } from './stage-1-cst'; import { Comparators, NamedTags, NodeTypes, nonTraversableProperties, Position } from './types'; import { assertNever, deepGet, dropLast } from './utils'; @@ -755,15 +756,22 @@ export interface TextNode extends ASTNode { value: string; } +/** Represents a `@param` node in a LiquidDoc comment - `@param paramName {paramType} - paramDescription` */ export interface LiquidDocParamNode extends ASTNode { name: 'param'; - paramDescription: LiquidDocParamDescription; + /** The name of the parameter (e.g. "product") */ paramName: TextNode; - paramType: TextNode; + /** Optional description of the parameter in a Liquid doc comment (e.g. "The product title") */ + paramDescription: LiquidDocParamDescription | null; + /** Optional type annotation for the parameter (e.g. "{string}", "{number}") */ + paramType: TextNode | null; } export interface LiquidDocParamDescription extends ASTNode { + /** Whether the parameter description is dash-separated (e.g. "paramName - paramDescription") */ dashSeparated: boolean; + + /** The body of the description */ value: string; } @@ -1293,19 +1301,8 @@ function buildAst( position: position(node.paramName), source: node.paramName.source, }, - paramDescription: { - type: NodeTypes.TextNode, - value: node.paramDescription.value, - position: position(node.paramDescription), - source: node.paramDescription.source, - dashSeparated: node.paramDescription.dashSeparated, - }, - paramType: { - type: NodeTypes.TextNode, - value: node.paramType.value, - position: position(node.paramType), - source: node.paramType.source, - }, + paramDescription: toParamDescription(node.paramDescription), + paramType: toParamType(node.paramType), }); break; } @@ -2090,3 +2087,26 @@ function getUnclosed(node?: ParentNode, parentNode?: ParentNode): UnclosedNode | blockStartPosition: 'blockStartPosition' in node ? node.blockStartPosition : node.position, }; } + +function toParamDescription( + node: ConcreteLiquidDocParamDescription | null, +): LiquidDocParamDescription | null { + if (!node) return null; + return { + type: NodeTypes.TextNode, + value: node.value, + position: position(node), + source: node.source, + dashSeparated: node.dashSeparated, + }; +} + +function toParamType(node: ConcreteTextNode | null): TextNode | null { + if (!node) return null; + return { + type: NodeTypes.TextNode, + value: node.value, + position: position(node), + source: node.source, + }; +} diff --git a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts index bde225b08..043427005 100644 --- a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts +++ b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts @@ -515,7 +515,7 @@ export function printLiquidDocParam( const node = path.getValue(); const parts: Doc[] = ['@param']; - if (node.paramType.value) { + if (node.paramType?.value) { parts.push(' ', `{${node.paramType.value}}`); } @@ -523,7 +523,7 @@ export function printLiquidDocParam( parts.push(' ', node.paramName.value); } - if (node.paramDescription.value) { + if (node.paramDescription?.value) { const normalizedDescription = node.paramDescription.value.replace(/\s+/g, ' ').trim(); if (node.paramDescription.dashSeparated) { parts.push(' - ', normalizedDescription); From 6a18b6f18ea772ba8173bd52d177ff83f7ba20a3 Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 13 Jan 2025 12:37:17 -0800 Subject: [PATCH 09/16] Make LiquidDoc @param description dash seperator a configurable prettier option and simplify types Now that this is a configurable option, we don't need to capture this information in the type. This means we can replace the description node with a simple text node. Simplify types: Remove Replace LiquidDocParamDescription with TextNode Now that the param description is configurable, this isn't needed --- .../liquid-html-parser/src/stage-1-cst.ts | 8 +--- .../src/stage-2-ast.spec.ts | 2 - .../liquid-html-parser/src/stage-2-ast.ts | 44 ++++--------------- packages/prettier-plugin-liquid/src/plugin.ts | 7 +++ .../src/printer/print/liquid.ts | 5 ++- .../src/test/liquid-doc/fixed.liquid | 20 ++++----- .../src/test/liquid-doc/index.liquid | 21 +++++---- packages/prettier-plugin-liquid/src/types.ts | 1 + 8 files changed, 40 insertions(+), 68 deletions(-) diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index fa52ac691..e4c4ea6e7 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -111,16 +111,10 @@ export interface ConcreteLiquidDocParamNode extends ConcreteBasicNode { name: 'param'; paramName: ConcreteTextNode; - paramDescription: ConcreteLiquidDocParamDescription | null; + paramDescription: ConcreteTextNode | null; paramType: ConcreteTextNode | null; } -export interface ConcreteLiquidDocParamDescription - extends ConcreteBasicNode { - dashSeparated: boolean; - value: string; -} - export interface ConcreteHtmlNodeBase extends ConcreteBasicNode { attrList?: ConcreteAttributeNode[]; } diff --git a/packages/liquid-html-parser/src/stage-2-ast.spec.ts b/packages/liquid-html-parser/src/stage-2-ast.spec.ts index d3cfb4f6f..51ddd2313 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.spec.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.spec.ts @@ -1244,13 +1244,11 @@ describe('Unit: Stage 2 (AST)', () => { expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('asdf'); expectPath(ast, 'children.0.body.nodes.0.paramDescription.type').to.eql('TextNode'); expectPath(ast, 'children.0.body.nodes.0.paramDescription.value').to.eql(''); - expectPath(ast, 'children.0.body.nodes.0.paramDescription.dashSeparated').to.eql(false); 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.paramName.type').to.eql('TextNode'); expectPath(ast, 'children.0.body.nodes.1.paramName.value').to.eql('paramWithDescription'); expectPath(ast, 'children.0.body.nodes.1.paramDescription.type').to.eql('TextNode'); - expectPath(ast, 'children.0.body.nodes.1.paramDescription.dashSeparated').to.eql(true); expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql( 'param with description and `punctation`. This is still a valid param description.', ); diff --git a/packages/liquid-html-parser/src/stage-2-ast.ts b/packages/liquid-html-parser/src/stage-2-ast.ts index 384a0a2dc..7dac9780d 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.ts @@ -73,7 +73,6 @@ import { LiquidHtmlConcreteNode, ConcreteLiquidTagBaseCase, ConcreteLiquidTagContentForMarkup, - ConcreteLiquidDocParamDescription, } from './stage-1-cst'; import { Comparators, NamedTags, NodeTypes, nonTraversableProperties, Position } from './types'; import { assertNever, deepGet, dropLast } from './utils'; @@ -762,19 +761,10 @@ export interface LiquidDocParamNode extends ASTNode { - /** Whether the parameter description is dash-separated (e.g. "paramName - paramDescription") */ - dashSeparated: boolean; - - /** The body of the description */ - value: string; -} - export interface ASTNode { /** * The type of the node, as a string. @@ -1301,8 +1291,8 @@ function buildAst( position: position(node.paramName), source: node.paramName.source, }, - paramDescription: toParamDescription(node.paramDescription), - paramType: toParamType(node.paramType), + paramDescription: toNullableTextNode(node.paramDescription), + paramType: toNullableTextNode(node.paramType), }); break; } @@ -1983,6 +1973,11 @@ function toHtmlSelfClosingElement( }; } +function toNullableTextNode(node: ConcreteTextNode | null): TextNode | null { + if (!node) return null; + return toTextNode(node); +} + function toTextNode(node: ConcreteTextNode): TextNode { return { type: NodeTypes.TextNode, @@ -2087,26 +2082,3 @@ function getUnclosed(node?: ParentNode, parentNode?: ParentNode): UnclosedNode | blockStartPosition: 'blockStartPosition' in node ? node.blockStartPosition : node.position, }; } - -function toParamDescription( - node: ConcreteLiquidDocParamDescription | null, -): LiquidDocParamDescription | null { - if (!node) return null; - return { - type: NodeTypes.TextNode, - value: node.value, - position: position(node), - source: node.source, - dashSeparated: node.dashSeparated, - }; -} - -function toParamType(node: ConcreteTextNode | null): TextNode | null { - if (!node) return null; - return { - type: NodeTypes.TextNode, - value: node.value, - position: position(node), - source: node.source, - }; -} diff --git a/packages/prettier-plugin-liquid/src/plugin.ts b/packages/prettier-plugin-liquid/src/plugin.ts index 37cacf514..f15694bde 100644 --- a/packages/prettier-plugin-liquid/src/plugin.ts +++ b/packages/prettier-plugin-liquid/src/plugin.ts @@ -61,6 +61,13 @@ const options: SupportOptions = { description: 'Indent the contents of the {% schema %} tag', since: '0.1.0', }, + liquidDocParamDash: { + type: 'boolean', + category: 'LIQUID', + default: true, + description: 'Append a dash (-) to separate descriptions in {% doc %} @param annotations', + since: '1.6.4', + }, }; const defaultOptions = { diff --git a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts index 043427005..47242ae0c 100644 --- a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts +++ b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts @@ -508,7 +508,7 @@ export function printLiquidDoc( export function printLiquidDocParam( path: AstPath, - _options: LiquidParserOptions, + options: LiquidParserOptions, _print: LiquidPrinter, _args: LiquidPrinterArgs, ): Doc { @@ -525,7 +525,8 @@ export function printLiquidDocParam( if (node.paramDescription?.value) { const normalizedDescription = node.paramDescription.value.replace(/\s+/g, ' ').trim(); - if (node.paramDescription.dashSeparated) { + + if (options.liquidDocParamDash) { parts.push(' - ', normalizedDescription); } else { parts.push(' ', normalizedDescription); diff --git a/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid b/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid index 7e94a2774..3ec30f8c3 100644 --- a/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid +++ b/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid @@ -1,21 +1,16 @@ It should indent the body {% doc %} - @param paramName param with description -{% enddoc %} - -It should not dedent to root -{% doc %} - @param paramName param with description + @param paramName - param with description {% enddoc %} It should trim whitespace between nodes {% doc %} - @param paramName param with description + @param paramName - param with description {% enddoc %} -It should format the param type +It should normalize the param type {% doc %} - @param {string} paramName param with description + @param {string} paramName - param with description {% enddoc %} It should format the param description with a dash separator @@ -23,7 +18,12 @@ It should format the param description with a dash separator @param paramName - param with description {% enddoc %} -It should normalize the param description +It should respect the liquidDocParamDash option liquidDocParamDash: false {% doc %} @param paramName param with description {% enddoc %} + +It should normalize the param description +{% doc %} + @param paramName - param with description +{% enddoc %} diff --git a/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid b/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid index 2d087a985..e57fd339d 100644 --- a/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid +++ b/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid @@ -1,30 +1,29 @@ It should indent the body {% doc %} -@param paramName param with description + @param paramName - param with description {% enddoc %} -It should not dedent to root +It should trim whitespace between nodes {% doc %} -@param paramName param with description + @param paramName - param with description {% enddoc %} -It should trim whitespace between nodes +It should normalize the param type {% doc %} -@param paramName param with description + @param { string } paramName - param with description {% enddoc %} -It should format the param type +It should format the param description with a dash separator {% doc %} -@param { string } paramName param with description + @param paramName - param with description {% enddoc %} -It should format the param description with a dash separator +It should respect the liquidDocParamDash option liquidDocParamDash: false {% doc %} -@param paramName - param with description + @param paramName param with description {% enddoc %} It should normalize the param description {% doc %} -@param paramName param with description + @param paramName - param with description {% enddoc %} - diff --git a/packages/prettier-plugin-liquid/src/types.ts b/packages/prettier-plugin-liquid/src/types.ts index 8269f2e48..f4e61a216 100644 --- a/packages/prettier-plugin-liquid/src/types.ts +++ b/packages/prettier-plugin-liquid/src/types.ts @@ -24,6 +24,7 @@ export type LiquidParserOptions = ParserOptions & { embeddedSingleQuote: boolean; indentSchema: boolean; captureWhitespaceSensitivity: 'strict' | 'ignore'; + liquidDocParamDash: boolean; }; export type LiquidPrinterArgs = { leadingSpaceGroupId?: symbol[] | symbol; From 61be9cceb3d1fee9c7811cf0ab93a34f2dea828f Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 13 Jan 2025 13:01:00 -0800 Subject: [PATCH 10/16] Add LiquidDocConcreteNode as a type of LiquidHtmlConcreteNode --- packages/liquid-html-parser/src/stage-1-cst.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index e4c4ea6e7..8233d206b 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -443,18 +443,18 @@ export type LiquidHtmlConcreteNode = | ConcreteHtmlNode | ConcreteLiquidNode | ConcreteTextNode - | ConcreteYamlFrontmatterNode; + | ConcreteYamlFrontmatterNode + | LiquidDocConcreteNode; export type LiquidConcreteNode = | ConcreteLiquidNode | ConcreteTextNode | ConcreteYamlFrontmatterNode; -export type LiquidHtmlCST = LiquidHtmlConcreteNode[] | LiquidDocCST; +export type LiquidHtmlCST = LiquidHtmlConcreteNode[]; export type LiquidCST = LiquidConcreteNode[]; -type LiquidDocCST = LiquidDocConcreteNode[]; export type LiquidDocConcreteNode = ConcreteLiquidDocParamNode; interface Mapping { From 9320665ef537839ccc1ce86f5256ed87757bd569 Mon Sep 17 00:00:00 2001 From: James Meng Date: Mon, 13 Jan 2025 18:52:01 -0800 Subject: [PATCH 11/16] Fix - pass 'param' as node name to stage 2 --- packages/liquid-html-parser/src/stage-1-cst.ts | 1 + packages/liquid-html-parser/src/stage-2-ast.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index 8233d206b..d34526c1c 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -1330,6 +1330,7 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) }, paramNode: { type: ConcreteNodeTypes.LiquidDocParamNode, + name: 'param', locStart, locEnd, source, diff --git a/packages/liquid-html-parser/src/stage-2-ast.spec.ts b/packages/liquid-html-parser/src/stage-2-ast.spec.ts index 51ddd2313..0e382fe7f 100644 --- a/packages/liquid-html-parser/src/stage-2-ast.spec.ts +++ b/packages/liquid-html-parser/src/stage-2-ast.spec.ts @@ -1239,13 +1239,13 @@ describe('Unit: Stage 2 (AST)', () => { expectPath(ast, 'children.0.type').to.eql('LiquidRawTag'); expectPath(ast, 'children.0.name').to.eql('doc'); 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.name').to.eql('param'); expectPath(ast, 'children.0.body.nodes.0.paramName.type').to.eql('TextNode'); expectPath(ast, 'children.0.body.nodes.0.paramName.value').to.eql('asdf'); expectPath(ast, 'children.0.body.nodes.0.paramDescription.type').to.eql('TextNode'); expectPath(ast, 'children.0.body.nodes.0.paramDescription.value').to.eql(''); 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.name').to.eql('param'); 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.paramDescription.type').to.eql('TextNode'); From 28cbadb6116fe5b65393b6814bef37be5ff91f54 Mon Sep 17 00:00:00 2001 From: James Meng Date: Tue, 14 Jan 2025 10:12:35 -0800 Subject: [PATCH 12/16] AST-1: Remove unused DashSeperator field --- packages/liquid-html-parser/src/stage-1-cst.spec.ts | 13 ------------- packages/liquid-html-parser/src/stage-1-cst.ts | 2 -- 2 files changed, 15 deletions(-) diff --git a/packages/liquid-html-parser/src/stage-1-cst.spec.ts b/packages/liquid-html-parser/src/stage-1-cst.spec.ts index a426a960c..855fcc54c 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts @@ -1054,19 +1054,6 @@ describe('Unit: Stage 1 (CST)', () => { ); }); - it('should parse @param with description seperated by a dash', () => { - const testStr = `{% doc %} - @param dashWithSpace - param with description - @param dashWithNoSpace -param with description - @param notDashSeparated param with description - {% enddoc %}`; - cst = toCST(testStr); - - expectPath(cst, '0.children.0.paramDescription.dashSeparated').to.equal(true); - expectPath(cst, '0.children.1.paramDescription.dashSeparated').to.equal(true); - expectPath(cst, '0.children.2.paramDescription.dashSeparated').to.equal(false); - }); - it('should accept punctation inside the param description body', () => { const testStr = `{% doc %} @param paramName paramDescription - asdf . \`should\` work {% enddoc %}`; cst = toCST(testStr); diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index d34526c1c..fc744114a 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -1355,7 +1355,6 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) }; }, paramDescription: function (nodes: Node[]) { - const dashNode = nodes[6]; const descriptionNode = nodes[7]; return { type: ConcreteNodeTypes.TextNode, @@ -1363,7 +1362,6 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) source, locStart: offset + descriptionNode.source.startIdx, locEnd: offset + descriptionNode.source.endIdx, - dashSeparated: dashNode.sourceString.trim() === '-', }; }, }, From 53fa27ee546ebd376616d8310f8a66ac32240568 Mon Sep 17 00:00:00 2001 From: James Meng Date: Tue, 14 Jan 2025 10:14:08 -0800 Subject: [PATCH 13/16] Refactor toLiquidDocAst: Assign param attributes via mappings rather than directly --- .../liquid-html-parser/src/stage-1-cst.ts | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index fc744114a..2fb6d7d1c 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -1319,7 +1319,7 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) const LiquidDocMappings: Mapping = { Node: 0, - textNode: { + TextNode: { type: ConcreteNodeTypes.TextNode, value: function () { return (this as any).sourceString; @@ -1334,36 +1334,36 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) locStart, locEnd, source, - paramType: function (nodes: Node[]) { - const typeNode = nodes[2]; - return { - type: ConcreteNodeTypes.TextNode, - value: typeNode.sourceString.slice(1, -1).trim(), - source, - locStart: offset + typeNode.source.startIdx, - locEnd: offset + typeNode.source.endIdx, - }; + paramType: 2, + paramName: 4, + paramDescription: 7, + }, + paramType: { + type: ConcreteNodeTypes.TextNode, + value: function (nodes: Node[]) { + return nodes[1].sourceString.trim(); }, - paramName: function (nodes: Node[]) { - const nameNode = nodes[4]; - return { - type: ConcreteNodeTypes.TextNode, - value: nameNode.sourceString.trim(), - source, - locStart: offset + nameNode.source.startIdx, - locEnd: offset + nameNode.source.endIdx, - }; + source, + locStart, + locEnd, + }, + paramName: { + type: ConcreteNodeTypes.TextNode, + value: function (nodes: Node[]) { + return nodes[0].sourceString.trim(); }, - paramDescription: function (nodes: Node[]) { - const descriptionNode = nodes[7]; - return { - type: ConcreteNodeTypes.TextNode, - value: descriptionNode.sourceString.trim(), - source, - locStart: offset + descriptionNode.source.startIdx, - locEnd: offset + descriptionNode.source.endIdx, - }; + source, + locStart, + locEnd, + }, + paramDescription: { + type: ConcreteNodeTypes.TextNode, + value: function (nodes: Node[]) { + return nodes[0].sourceString.trim(); }, + source, + locStart, + locEnd, }, fallbackNode: { type: ConcreteNodeTypes.TextNode, From bbe657fa2dc6cd9c9be92d34fd02c3e355c3402e Mon Sep 17 00:00:00 2001 From: James Meng Date: Tue, 14 Jan 2025 10:44:57 -0800 Subject: [PATCH 14/16] Add test for parsing @param with type containing spaces in LiquidDoc --- .../liquid-html-parser/src/stage-1-cst.spec.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/packages/liquid-html-parser/src/stage-1-cst.spec.ts b/packages/liquid-html-parser/src/stage-1-cst.spec.ts index 855fcc54c..b1c14d27d 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts @@ -1054,6 +1054,23 @@ describe('Unit: Stage 1 (CST)', () => { ); }); + it('should parse @param with type with space inside', () => { + const testStr = `{% doc %} @param { String } paramWithType {% enddoc %}`; + cst = toCST(testStr); + + expectPath(cst, '0.children.0.type').to.equal('LiquidDocParamNode'); + expectPath(cst, '0.children.0.paramName.value').to.equal('paramWithType'); + + expectPath(cst, '0.children.0.paramType.type').to.equal('TextNode'); + expectPath(cst, '0.children.0.paramType.value').to.equal('String'); + expectPath(cst, '0.children.0.paramType.locStart').to.equal( + testStr.indexOf('{ String }'), + ); + expectPath(cst, '0.children.0.paramType.locEnd').to.equal( + testStr.indexOf('{ String }') + '{ String }'.length, + ); + }); + it('should accept punctation inside the param description body', () => { const testStr = `{% doc %} @param paramName paramDescription - asdf . \`should\` work {% enddoc %}`; cst = toCST(testStr); From dcd4fd20dcb495dd362b81e60edfbfc04f14177a Mon Sep 17 00:00:00 2001 From: James Meng Date: Tue, 14 Jan 2025 15:05:20 -0800 Subject: [PATCH 15/16] Update grammar to strip whitespace using Ohm matcher --- .../grammar/liquid-html.ohm | 15 +++- .../src/stage-1-cst.spec.ts | 12 ++- .../liquid-html-parser/src/stage-1-cst.ts | 73 ++++++------------- 3 files changed, 39 insertions(+), 61 deletions(-) diff --git a/packages/liquid-html-parser/grammar/liquid-html.ohm b/packages/liquid-html-parser/grammar/liquid-html.ohm index b7397cec2..4d95edbc5 100644 --- a/packages/liquid-html-parser/grammar/liquid-html.ohm +++ b/packages/liquid-html-parser/grammar/liquid-html.ohm @@ -394,11 +394,18 @@ LiquidDoc <: Helpers { | paramNode | fallbackNode - fallbackNode = "@" anyExceptStar - paramNode = "@param" space* paramType? space* paramName (space* "-")? paramDescription - paramType = "{" anyExceptStar<"}"> "}" + // By default, space matches new lines as well. We override it here to make writing rules easier. + strictSpace = " " | "\t" + // We use this as an escape hatch to stop matching TextNode and try again when one of these characters is encountered + openControl:= "@" | end + + fallbackNode = "@" anyExceptStar + paramNode = "@param" strictSpace* paramType? strictSpace* paramName (strictSpace* "-")? strictSpace* paramDescription + paramType = "{" strictSpace* paramTypeContent strictSpace* "}" + paramTypeContent = anyExceptStar<("}"| strictSpace)> paramName = identifierCharacter+ - paramDescription = anyExceptStar<(newline | end)> + paramDescription = anyExceptStar + endOfParam = strictSpace* (newline | end) } LiquidHTML <: Liquid { diff --git a/packages/liquid-html-parser/src/stage-1-cst.spec.ts b/packages/liquid-html-parser/src/stage-1-cst.spec.ts index b1c14d27d..08303b8e0 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts @@ -1048,13 +1048,13 @@ describe('Unit: Stage 1 (CST)', () => { expectPath(cst, '0.children.0.paramType.type').to.equal('TextNode'); expectPath(cst, '0.children.0.paramType.value').to.equal('String'); - expectPath(cst, '0.children.0.paramType.locStart').to.equal(testStr.indexOf('{String}')); + expectPath(cst, '0.children.0.paramType.locStart').to.equal(testStr.indexOf('String')); expectPath(cst, '0.children.0.paramType.locEnd').to.equal( - testStr.indexOf('{String}') + '{String}'.length, + testStr.indexOf('String') + 'String'.length, ); }); - it('should parse @param with type with space inside', () => { + it('should strip whitespace around param type for @param annotation', () => { const testStr = `{% doc %} @param { String } paramWithType {% enddoc %}`; cst = toCST(testStr); @@ -1063,11 +1063,9 @@ describe('Unit: Stage 1 (CST)', () => { expectPath(cst, '0.children.0.paramType.type').to.equal('TextNode'); expectPath(cst, '0.children.0.paramType.value').to.equal('String'); - expectPath(cst, '0.children.0.paramType.locStart').to.equal( - testStr.indexOf('{ String }'), - ); + expectPath(cst, '0.children.0.paramType.locStart').to.equal(testStr.indexOf('String')); expectPath(cst, '0.children.0.paramType.locEnd').to.equal( - testStr.indexOf('{ String }') + '{ String }'.length, + testStr.indexOf('String') + 'String'.length, ); }); diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts index 2fb6d7d1c..905c52b3e 100644 --- a/packages/liquid-html-parser/src/stage-1-cst.ts +++ b/packages/liquid-html-parser/src/stage-1-cst.ts @@ -441,15 +441,14 @@ export interface ConcreteYamlFrontmatterNode export type LiquidHtmlConcreteNode = | ConcreteHtmlNode - | ConcreteLiquidNode - | ConcreteTextNode | ConcreteYamlFrontmatterNode - | LiquidDocConcreteNode; + | LiquidConcreteNode; export type LiquidConcreteNode = | ConcreteLiquidNode | ConcreteTextNode - | ConcreteYamlFrontmatterNode; + | ConcreteYamlFrontmatterNode + | LiquidDocConcreteNode; export type LiquidHtmlCST = LiquidHtmlConcreteNode[]; @@ -1317,17 +1316,22 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) throw new LiquidHTMLCSTParsingError(res); } + /** + * Reusable text node type + */ + const textNode = { + type: ConcreteNodeTypes.TextNode, + value: function () { + return (this as any).sourceString; + }, + locStart, + locEnd, + source, + }; + const LiquidDocMappings: Mapping = { Node: 0, - TextNode: { - type: ConcreteNodeTypes.TextNode, - value: function () { - return (this as any).sourceString; - }, - locStart, - locEnd, - source, - }, + TextNode: textNode, paramNode: { type: ConcreteNodeTypes.LiquidDocParamNode, name: 'param', @@ -1336,44 +1340,13 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number) source, paramType: 2, paramName: 4, - paramDescription: 7, - }, - paramType: { - type: ConcreteNodeTypes.TextNode, - value: function (nodes: Node[]) { - return nodes[1].sourceString.trim(); - }, - source, - locStart, - locEnd, - }, - paramName: { - type: ConcreteNodeTypes.TextNode, - value: function (nodes: Node[]) { - return nodes[0].sourceString.trim(); - }, - source, - locStart, - locEnd, - }, - paramDescription: { - type: ConcreteNodeTypes.TextNode, - value: function (nodes: Node[]) { - return nodes[0].sourceString.trim(); - }, - source, - locStart, - locEnd, - }, - fallbackNode: { - type: ConcreteNodeTypes.TextNode, - value: function () { - return (this as any).sourceString.trim(); - }, - locStart, - locEnd, - source, + paramDescription: 8, }, + paramType: 2, + paramTypeContent: textNode, + paramName: textNode, + paramDescription: textNode, + fallbackNode: textNode, }; return toAST(res, LiquidDocMappings); From ac55577aadd4ff38aa05c67bf70e2fa1267791a9 Mon Sep 17 00:00:00 2001 From: James Meng Date: Thu, 16 Jan 2025 14:17:13 -0800 Subject: [PATCH 16/16] Add changeset --- .changeset/slow-years-float.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/slow-years-float.md diff --git a/.changeset/slow-years-float.md b/.changeset/slow-years-float.md new file mode 100644 index 000000000..8b242780a --- /dev/null +++ b/.changeset/slow-years-float.md @@ -0,0 +1,7 @@ +--- +'@shopify/theme-language-server-common': minor +'@shopify/prettier-plugin-liquid': minor +'@shopify/liquid-html-parser': minor +--- + +Add prettier support for LiquidDoc {% doc %} tag and @param annotation