diff --git a/packages/liquid-html-parser/grammar/liquid-html.ohm b/packages/liquid-html-parser/grammar/liquid-html.ohm
index 4d95edbc5..42a2b8d4b 100644
--- a/packages/liquid-html-parser/grammar/liquid-html.ohm
+++ b/packages/liquid-html-parser/grammar/liquid-html.ohm
@@ -128,10 +128,10 @@ Liquid <: Helpers {
liquidTagLiquidMarkup = tagMarkup
liquidTagContentFor = liquidTagRule<"content_for", liquidTagContentForMarkup>
-
+
liquidTagContentForMarkup =
contentForType (argumentSeparatorOptionalComma contentForTagArgument) (space* ",")? space*
-
+
contentForTagArgument = listOf, argumentSeparatorOptionalComma>
contentForNamedArgument = (variableSegment ("." variableSegment)*) space* ":" space* (liquidExpression)
@@ -217,7 +217,7 @@ Liquid <: Helpers {
commentBlockStart = "{%" "-"? space* ("comment" endOfIdentifier) space* tagMarkup "-"? "%}"
commentBlockEnd = "{%" "-"? space* ("endcomment" endOfIdentifier) space* tagMarkup "-"? "%}"
- liquidDoc =
+ liquidDoc =
liquidDocStart
liquidDocBody
liquidDocEnd
@@ -392,20 +392,26 @@ LiquidDoc <: Helpers {
Node := (LiquidDocNode | TextNode)*
LiquidDocNode =
| paramNode
+ | exampleNode
| fallbackNode
// 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
+ // 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
endOfParam = strictSpace* (newline | end)
+
+ exampleNode = "@example" strictSpace* exampleContent
+ exampleContent = anyExceptStar
+ endOfExample = strictSpace* ("@" | end)
+
+ fallbackNode = "@" anyExceptStar
}
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 08303b8e0..6b22f55a2 100644
--- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts
+++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts
@@ -1108,6 +1108,72 @@ describe('Unit: Stage 1 (CST)', () => {
expectPath(cst, '0.children.2.type').to.equal('TextNode');
expectPath(cst, '0.children.2.value').to.equal('@unsupported');
});
+
+ it('should parse a basic example tag', () => {
+ const testStr = `{% doc -%} @example {%- enddoc %}`;
+ cst = toCST(testStr);
+ expectPath(cst, '0.type').to.equal('LiquidRawTag');
+ expectPath(cst, '0.name').to.equal('doc');
+ expectPath(cst, '0.children.0.type').to.equal('LiquidDocExampleNode');
+ expectPath(cst, '0.children.0.exampleContent.value').to.equal('');
+ });
+
+ it('should parse example tag with content that has leading whitespace', () => {
+ const testStr = `{% doc %} @example hello there {%- enddoc %}`;
+ cst = toCST(testStr);
+ expectPath(cst, '0.type').to.equal('LiquidRawTag');
+ expectPath(cst, '0.name').to.equal('doc');
+ expectPath(cst, '0.children.0.type').to.equal('LiquidDocExampleNode');
+ expectPath(cst, '0.children.0.name').to.equal('example');
+ expectPath(cst, '0.children.0.exampleContent.value').to.equal('hello there');
+ });
+
+ it('should parse an example tag with a value', () => {
+ const testStr = `{% doc %}
+ @example
+ This is an example
+ It supports multiple lines
+ {% enddoc %}`;
+
+ cst = toCST(testStr);
+ expectPath(cst, '0.type').to.equal('LiquidRawTag');
+ expectPath(cst, '0.name').to.equal('doc');
+ expectPath(cst, '0.children.0.type').to.equal('LiquidDocExampleNode');
+ expectPath(cst, '0.children.0.name').to.equal('example');
+ expectPath(cst, '0.children.0.exampleContent.value').to.equal(
+ '\n This is an example\n It supports multiple lines\n',
+ );
+ });
+
+ it('should parse example node and stop at the next @', () => {
+ const testStr = `{% doc %}
+ @example
+ This is an example
+ @param param1
+ {% enddoc %}`;
+ cst = toCST(testStr);
+ expectPath(cst, '0.children.0.type').to.equal('LiquidDocExampleNode');
+ expectPath(cst, '0.children.0.name').to.equal('example');
+ expectPath(cst, '0.children.0.exampleContent.value').to.equal('\n This is an example\n');
+ expectPath(cst, '0.children.1.type').to.equal('LiquidDocParamNode');
+ expectPath(cst, '0.children.1.paramName.value').to.equal('param1');
+ });
+
+ it('should parse example node with whitespace and new lines', () => {
+ const testStr = `{% doc %}
+ @example hello there my friend
+ This is an example
+ It supports multiple lines
+ {% enddoc %}`;
+ cst = toCST(testStr);
+ expectPath(cst, '0.type').to.equal('LiquidRawTag');
+ expectPath(cst, '0.name').to.equal('doc');
+ expectPath(cst, '0.children.0.type').to.equal('LiquidDocExampleNode');
+ expectPath(cst, '0.children.0.name').to.equal('example');
+ expectPath(cst, '0.children.0.exampleContent.value').to.equal(
+ 'hello there my friend\n This is an example\n It supports multiple lines\n',
+ );
+ });
}
});
});
@@ -1304,8 +1370,8 @@ describe('Unit: Stage 1 (CST)', () => {
{ type: 'AttrSingleQuoted', name: 'single', quote: '‘' },
{ type: 'AttrSingleQuoted', name: 'single', quote: '’' },
{ type: 'AttrDoubleQuoted', name: 'double', quote: '"' },
- { type: 'AttrDoubleQuoted', name: 'double', quote: '“' },
- { type: 'AttrDoubleQuoted', name: 'double', quote: '”' },
+ { type: 'AttrDoubleQuoted', name: 'double', quote: '"' },
+ { type: 'AttrDoubleQuoted', name: 'double', quote: '"' },
{ type: 'AttrUnquoted', name: 'unquoted', quote: '' },
].forEach((testConfig) => {
[
diff --git a/packages/liquid-html-parser/src/stage-1-cst.ts b/packages/liquid-html-parser/src/stage-1-cst.ts
index 905c52b3e..9cdbe9068 100644
--- a/packages/liquid-html-parser/src/stage-1-cst.ts
+++ b/packages/liquid-html-parser/src/stage-1-cst.ts
@@ -85,6 +85,7 @@ export enum ConcreteNodeTypes {
ContentForNamedArgument = 'ContentForNamedArgument',
LiquidDocParamNode = 'LiquidDocParamNode',
+ LiquidDocExampleNode = 'LiquidDocExampleNode',
}
export const LiquidLiteralValues = {
@@ -115,6 +116,12 @@ export interface ConcreteLiquidDocParamNode
paramType: ConcreteTextNode | null;
}
+export interface ConcreteLiquidDocExampleNode
+ extends ConcreteBasicNode {
+ name: 'example';
+ exampleContent: ConcreteTextNode;
+}
+
export interface ConcreteHtmlNodeBase extends ConcreteBasicNode {
attrList?: ConcreteAttributeNode[];
}
@@ -454,7 +461,7 @@ export type LiquidHtmlCST = LiquidHtmlConcreteNode[];
export type LiquidCST = LiquidConcreteNode[];
-export type LiquidDocConcreteNode = ConcreteLiquidDocParamNode;
+export type LiquidDocConcreteNode = ConcreteLiquidDocParamNode | ConcreteLiquidDocExampleNode;
interface Mapping {
[k: string]: number | TemplateMapping | TopLevelFunctionMapping;
@@ -1346,6 +1353,15 @@ function toLiquidDocAST(source: string, matchingSource: string, offset: number)
paramTypeContent: textNode,
paramName: textNode,
paramDescription: textNode,
+ exampleNode: {
+ type: ConcreteNodeTypes.LiquidDocExampleNode,
+ name: 'example',
+ locStart,
+ locEnd,
+ source,
+ exampleContent: 2,
+ },
+ exampleContent: textNode,
fallbackNode: textNode,
};
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 0e382fe7f..66afc3df4 100644
--- a/packages/liquid-html-parser/src/stage-2-ast.spec.ts
+++ b/packages/liquid-html-parser/src/stage-2-ast.spec.ts
@@ -1210,6 +1210,7 @@ describe('Unit: Stage 2 (AST)', () => {
expectPath(ast, 'children.0.markup.0.name').to.eql('assign');
expectPath(ast, 'children.0.markup.0.markup.name').to.eql('var1');
+
expectPath(ast, 'children.0.markup.1.type').to.eql('LiquidTag');
expectPath(ast, 'children.0.markup.1.name').to.eql('if');
@@ -1258,6 +1259,55 @@ describe('Unit: Stage 2 (AST)', () => {
expectPath(ast, 'children.0.body.nodes.2.value').to.eql(
'@unsupported this node falls back to a text node',
);
+
+ ast = toLiquidAST(`
+ {% doc -%}
+ @example simple inline example
+ {%- enddoc %}
+ `);
+ expectPath(ast, 'children.0.type').to.eql('LiquidRawTag');
+ expectPath(ast, 'children.0.name').to.eql('doc');
+ expectPath(ast, 'children.0.body.nodes.0.name').to.eql('example');
+ expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocExampleNode');
+ expectPath(ast, 'children.0.body.nodes.0.exampleContent.type').to.eql('TextNode');
+ expectPath(ast, 'children.0.body.nodes.0.exampleContent.value').to.eql(
+ 'simple inline example\n',
+ );
+
+ ast = toLiquidAST(`
+ {% doc -%}
+ @example including inline code
+ This is a valid example
+ It can have multiple lines
+ {% enddoc %}
+ `);
+ expectPath(ast, 'children.0.body.nodes.0.type').to.eql('LiquidDocExampleNode');
+ expectPath(ast, 'children.0.body.nodes.0.name').to.eql('example');
+ expectPath(ast, 'children.0.body.nodes.0.exampleContent.value').to.eql(
+ 'including inline code\n This is a valid example\n It can have multiple lines\n',
+ );
+
+ ast = toLiquidAST(`
+ {% doc -%}
+ @example
+ This is a valid example
+ It can have multiple lines
+ @param {String} paramWithDescription - param with description
+ {% enddoc %}
+ `);
+ 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('LiquidDocExampleNode');
+ expectPath(ast, 'children.0.body.nodes.0.name').to.eql('example');
+ expectPath(ast, 'children.0.body.nodes.0.exampleContent.value').to.eql(
+ '\n This is a valid example\n It can have multiple lines\n',
+ );
+ 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.value').to.eql('paramWithDescription');
+ expectPath(ast, 'children.0.body.nodes.1.paramDescription.value').to.eql(
+ 'param with description',
+ );
});
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 7dac9780d..2d8b48d85 100644
--- a/packages/liquid-html-parser/src/stage-2-ast.ts
+++ b/packages/liquid-html-parser/src/stage-2-ast.ts
@@ -108,7 +108,8 @@ export type LiquidHtmlNode =
| LiquidLogicalExpression
| LiquidComparison
| TextNode
- | LiquidDocParamNode;
+ | LiquidDocParamNode
+ | LiquidDocExampleNode;
/** The root node of all LiquidHTML ASTs. */
export interface DocumentNode extends ASTNode {
@@ -765,6 +766,14 @@ export interface LiquidDocParamNode extends ASTNode {
+ name: 'example';
+ /** The contents of the example (e.g. "{{ product }}"). Can be multiline. */
+ exampleContent: TextNode;
+}
+
export interface ASTNode {
/**
* The type of the node, as a string.
@@ -1297,6 +1306,22 @@ function buildAst(
break;
}
+ case ConcreteNodeTypes.LiquidDocExampleNode: {
+ builder.push({
+ type: NodeTypes.LiquidDocExampleNode,
+ name: node.name,
+ position: position(node),
+ source: node.source,
+ exampleContent: {
+ type: NodeTypes.TextNode,
+ value: node.exampleContent.value,
+ position: position(node.exampleContent),
+ source: node.exampleContent.source,
+ },
+ });
+ break;
+ }
+
default: {
assertNever(node);
}
diff --git a/packages/liquid-html-parser/src/types.ts b/packages/liquid-html-parser/src/types.ts
index 49efa1bf3..c31d097e2 100644
--- a/packages/liquid-html-parser/src/types.ts
+++ b/packages/liquid-html-parser/src/types.ts
@@ -45,6 +45,7 @@ export enum NodeTypes {
RenderMarkup = 'RenderMarkup',
RenderVariableExpression = 'RenderVariableExpression',
LiquidDocParamNode = 'LiquidDocParamNode',
+ LiquidDocExampleNode = 'LiquidDocExampleNode',
}
// 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 4ad565c6d..f3cb31691 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
@@ -129,6 +129,7 @@ function getCssDisplay(node: AugmentedNode, options: LiquidParserO
case NodeTypes.LogicalExpression:
case NodeTypes.Comparison:
case NodeTypes.LiquidDocParamNode:
+ case NodeTypes.LiquidDocExampleNode:
return 'should not be relevant';
default:
@@ -235,6 +236,7 @@ function getNodeCssStyleWhiteSpace(
case NodeTypes.LogicalExpression:
case NodeTypes.Comparison:
case NodeTypes.LiquidDocParamNode:
+ case NodeTypes.LiquidDocExampleNode:
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 47242ae0c..6efcca730 100644
--- a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts
+++ b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts
@@ -4,6 +4,7 @@ import {
isBranchedTag,
RawMarkup,
LiquidDocParamNode,
+ LiquidDocExampleNode,
} from '@shopify/liquid-html-parser';
import { Doc, doc } from 'prettier';
@@ -536,6 +537,30 @@ export function printLiquidDocParam(
return parts;
}
+export function printLiquidDocExample(
+ path: AstPath,
+ options: LiquidParserOptions,
+ _print: LiquidPrinter,
+ _args: LiquidPrinterArgs,
+): Doc {
+ const node = path.getValue();
+ const parts: Doc[] = ['@example'];
+
+ if (node.exampleContent?.value) {
+ const content = node.exampleContent.value.trim();
+ if (content) {
+ parts.push(hardline);
+ const lines = content
+ .split('\n')
+ .map((line) => line.trim())
+ .filter(Boolean);
+ parts.push(join(hardline, lines));
+ }
+ }
+
+ 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 781db0453..ab1b97138 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,
+ LiquidDocExampleNode,
LiquidDocParamNode,
NodeTypes,
Position,
@@ -47,6 +48,7 @@ import {
printLiquidTag,
printLiquidVariableOutput,
printLiquidDocParam,
+ printLiquidDocExample,
} from './print/liquid';
import { printClosingTagSuffix, printOpeningTagPrefix } from './print/tag';
import { bodyLines, hasLineBreakInRange, isEmpty, isTextLikeNode, reindent } from './utils';
@@ -559,6 +561,10 @@ function printNode(
return printLiquidDocParam(path as AstPath, options, print, args);
}
+ case NodeTypes.LiquidDocExampleNode: {
+ return printLiquidDocExample(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
index 3ec30f8c3..58c7c41f8 100644
--- a/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid
+++ b/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid
@@ -27,3 +27,9 @@ It should normalize the param description
{% doc %}
@param paramName - param with description
{% enddoc %}
+
+It should push example content to the next line
+{% doc %}
+ @example
+ This is a valid example
+{% 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 e57fd339d..f3d86be5e 100644
--- a/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid
+++ b/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid
@@ -27,3 +27,8 @@ It should normalize the param description
{% doc %}
@param paramName - param with description
{% enddoc %}
+
+It should push example content to the next line
+{% doc %}
+ @example This is a valid example
+{% enddoc %}
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 9ed9bcaf0..96e80e53f 100644
--- a/packages/theme-language-server-common/src/completions/params/LiquidCompletionParams.ts
+++ b/packages/theme-language-server-common/src/completions/params/LiquidCompletionParams.ts
@@ -406,6 +406,9 @@ function findCurrentNode(
case NodeTypes.LiquidDocParamNode: {
break;
}
+ case NodeTypes.LiquidDocExampleNode: {
+ break;
+ }
default: {
return assertNever(current);