From d265a2f57c89c097e080ca883e56de49db659a5f Mon Sep 17 00:00:00 2001
From: Josh Faigan <josh.faigan@shopify.com>
Date: Mon, 20 Jan 2025 16:35:53 -0500
Subject: [PATCH 1/6] Implement Liquid grammar for examplenode

---
 .../liquid-html-parser/grammar/liquid-html.ohm   | 16 +++++++++++-----
 1 file changed, 11 insertions(+), 5 deletions(-)

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<contentForNamedArgument<delimTag>, argumentSeparatorOptionalComma>
   contentForNamedArgument<delim> = (variableSegment ("." variableSegment)*) space* ":" space* (liquidExpression<delim>)
 
@@ -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<endOfParam>
   paramNode = "@param" strictSpace* paramType? strictSpace* paramName (strictSpace* "-")? strictSpace* paramDescription
   paramType = "{" strictSpace* paramTypeContent strictSpace* "}"
   paramTypeContent = anyExceptStar<("}"| strictSpace)>
   paramName = identifierCharacter+
   paramDescription = anyExceptStar<endOfParam>
   endOfParam = strictSpace* (newline | end)
+
+  exampleNode = "@example" strictSpace* exampleContent
+  exampleContent = anyExceptStar<endOfExample>
+  endOfExample =  strictSpace* ("@" | end)
+
+  fallbackNode = "@" anyExceptStar<endOfParam>
 }
 
 LiquidHTML <: Liquid {

From c72cb9905ceb2ec0771a9de69aead2abbf351b80 Mon Sep 17 00:00:00 2001
From: Josh Faigan <josh.faigan@shopify.com>
Date: Mon, 20 Jan 2025 16:36:45 -0500
Subject: [PATCH 2/6] add examplenode in stage1 and test coverage

---
 .../src/stage-1-cst.spec.ts                   | 81 ++++++++++++++++++-
 .../liquid-html-parser/src/stage-1-cst.ts     | 18 ++++-
 2 files changed, 96 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 08303b8e0..ced4bc547 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,83 @@ 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').toEqual(
+            expect.stringContaining('This is an example'),
+          );
+          expectPath(cst, '0.children.0.exampleContent.value').toEqual(
+            expect.stringContaining('It supports multiple lines'),
+          );
+        });
+
+        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').toEqual(
+            expect.stringContaining('This is an example'),
+          );
+          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').toEqual(
+            expect.stringContaining('hello      there        my    friend'),
+          );
+          expectPath(cst, '0.children.0.exampleContent.value').toEqual(
+            expect.stringContaining('This is an example'),
+          );
+          expectPath(cst, '0.children.0.exampleContent.value').toEqual(
+            expect.stringContaining('It supports multiple lines'),
+          );
+        });
       }
     });
   });
@@ -1304,8 +1381,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<ConcreteNodeTypes.LiquidDocExampleNode> {
+  name: 'example';
+  exampleContent: ConcreteTextNode;
+}
+
 export interface ConcreteHtmlNodeBase<T> extends ConcreteBasicNode<T> {
   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,
   };
 

From e44887cd45b9d96048acc48900c94f1990ee3e3b Mon Sep 17 00:00:00 2001
From: Josh Faigan <josh.faigan@shopify.com>
Date: Mon, 20 Jan 2025 16:37:28 -0500
Subject: [PATCH 3/6] add examplenode to stage 2 and test coverage

---
 .../src/stage-2-ast.spec.ts                   | 49 +++++++++++++++++++
 .../liquid-html-parser/src/stage-2-ast.ts     | 31 +++++++++++-
 packages/liquid-html-parser/src/types.ts      |  1 +
 3 files changed, 80 insertions(+), 1 deletion(-)

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..11b4a7fc6 100644
--- a/packages/liquid-html-parser/src/stage-2-ast.spec.ts
+++ b/packages/liquid-html-parser/src/stage-2-ast.spec.ts
@@ -1258,6 +1258,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',
+      );
+
+      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\nThis is a valid example\nIt can have multiple lines',
+      );
+
+      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(
+        'This is a valid example\nIt can have multiple lines',
+      );
+      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..89d1de98d 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<NodeTypes.Document> {
@@ -765,6 +766,14 @@ export interface LiquidDocParamNode extends ASTNode<NodeTypes.LiquidDocParamNode
   /** Optional type annotation for the parameter (e.g. "{string}", "{number}") */
   paramType: TextNode | null;
 }
+
+/** Represents a `@example` node in a LiquidDoc comment - `@example @exampleContent` */
+export interface LiquidDocExampleNode extends ASTNode<NodeTypes.LiquidDocExampleNode> {
+  name: 'example';
+  /** The contents of the example (e.g. "{{ product }}"). Can be multiline. */
+  exampleContent: TextNode;
+}
+
 export interface ASTNode<T> {
   /**
    * The type of the node, as a string.
@@ -1297,6 +1306,26 @@ 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
+              .split('\n')
+              .map((line) => line.trim())
+              .filter(Boolean)
+              .join('\n'),
+            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

From fcc693ef032267ef3ee576730cbe61d9ce33102b Mon Sep 17 00:00:00 2001
From: Josh Faigan <josh.faigan@shopify.com>
Date: Mon, 20 Jan 2025 16:38:04 -0500
Subject: [PATCH 4/6] add prettier support and test coverage for example in
 liquid doc

---
 .../preprocess/augment-with-css-properties.ts |  2 ++
 .../src/printer/print/liquid.ts               | 25 +++++++++++++++++++
 .../src/printer/printer-liquid-html.ts        |  6 +++++
 .../src/test/liquid-doc/fixed.liquid          |  6 +++++
 .../src/test/liquid-doc/index.liquid          |  5 ++++
 .../params/LiquidCompletionParams.ts          |  3 +++
 6 files changed, 47 insertions(+)

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<WithSiblings>, 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<LiquidDocExampleNode>,
+  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<LiquidDocParamNode>, options, print, args);
     }
 
+    case NodeTypes.LiquidDocExampleNode: {
+      return printLiquidDocExample(path as AstPath<LiquidDocExampleNode>, 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);

From 14f5b3acded7f7ee03e124ea244f96783aa3a8c9 Mon Sep 17 00:00:00 2001
From: Josh Faigan <josh.faigan@shopify.com>
Date: Tue, 28 Jan 2025 12:22:55 -0500
Subject: [PATCH 5/6] removed whitespace changes from stage 2 and updated tests

---
 .../src/stage-1-cst.spec.ts                   | 21 +++++--------------
 .../src/stage-2-ast.spec.ts                   |  7 ++++---
 .../liquid-html-parser/src/stage-2-ast.ts     |  6 +-----
 3 files changed, 10 insertions(+), 24 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 ced4bc547..6b22f55a2 100644
--- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts
+++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts
@@ -1140,11 +1140,8 @@ describe('Unit: Stage 1 (CST)', () => {
           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').toEqual(
-            expect.stringContaining('This is an example'),
-          );
-          expectPath(cst, '0.children.0.exampleContent.value').toEqual(
-            expect.stringContaining('It supports multiple lines'),
+          expectPath(cst, '0.children.0.exampleContent.value').to.equal(
+            '\n          This is an example\n          It supports multiple lines\n',
           );
         });
 
@@ -1157,9 +1154,7 @@ describe('Unit: Stage 1 (CST)', () => {
           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').toEqual(
-            expect.stringContaining('This is an 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');
         });
@@ -1175,14 +1170,8 @@ describe('Unit: Stage 1 (CST)', () => {
           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').toEqual(
-            expect.stringContaining('hello      there        my    friend'),
-          );
-          expectPath(cst, '0.children.0.exampleContent.value').toEqual(
-            expect.stringContaining('This is an example'),
-          );
-          expectPath(cst, '0.children.0.exampleContent.value').toEqual(
-            expect.stringContaining('It supports multiple lines'),
+          expectPath(cst, '0.children.0.exampleContent.value').to.equal(
+            'hello      there        my    friend\n          This is an example\n          It supports multiple lines\n',
           );
         });
       }
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 11b4a7fc6..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');
 
@@ -1270,7 +1271,7 @@ describe('Unit: Stage 2 (AST)', () => {
       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',
+        'simple inline example\n',
       );
 
       ast = toLiquidAST(`
@@ -1283,7 +1284,7 @@ describe('Unit: Stage 2 (AST)', () => {
       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\nThis is a valid example\nIt can have multiple lines',
+        'including inline code\n        This is a valid example\n        It can have multiple lines\n',
       );
 
       ast = toLiquidAST(`
@@ -1299,7 +1300,7 @@ describe('Unit: Stage 2 (AST)', () => {
       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(
-        'This is a valid example\nIt can have multiple lines',
+        '\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');
diff --git a/packages/liquid-html-parser/src/stage-2-ast.ts b/packages/liquid-html-parser/src/stage-2-ast.ts
index 89d1de98d..2d8b48d85 100644
--- a/packages/liquid-html-parser/src/stage-2-ast.ts
+++ b/packages/liquid-html-parser/src/stage-2-ast.ts
@@ -1314,11 +1314,7 @@ function buildAst(
           source: node.source,
           exampleContent: {
             type: NodeTypes.TextNode,
-            value: node.exampleContent.value
-              .split('\n')
-              .map((line) => line.trim())
-              .filter(Boolean)
-              .join('\n'),
+            value: node.exampleContent.value,
             position: position(node.exampleContent),
             source: node.exampleContent.source,
           },

From c24909d78445b0a8bfe57c0b44c3e16b18b9c061 Mon Sep 17 00:00:00 2001
From: Josh Faigan <josh.faigan@shopify.com>
Date: Fri, 31 Jan 2025 15:39:10 -0500
Subject: [PATCH 6/6] changed prettier rules and added more tests

---
 .../src/stage-1-cst.spec.ts                   |  4 +-
 .../src/stage-2-ast.spec.ts                   |  1 -
 .../src/printer/print/liquid.ts               | 44 ++++++++++++++++---
 .../src/test/liquid-doc/fixed.liquid          | 37 ++++++++++++++++
 .../src/test/liquid-doc/index.liquid          | 38 ++++++++++++++++
 5 files changed, 116 insertions(+), 8 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 6b22f55a2..2c0565471 100644
--- a/packages/liquid-html-parser/src/stage-1-cst.spec.ts
+++ b/packages/liquid-html-parser/src/stage-1-cst.spec.ts
@@ -1154,7 +1154,9 @@ describe('Unit: Stage 1 (CST)', () => {
           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.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');
         });
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 66afc3df4..fc7c595cb 100644
--- a/packages/liquid-html-parser/src/stage-2-ast.spec.ts
+++ b/packages/liquid-html-parser/src/stage-2-ast.spec.ts
@@ -1210,7 +1210,6 @@ 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');
 
diff --git a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts
index 6efcca730..ce86ee9f5 100644
--- a/packages/prettier-plugin-liquid/src/printer/print/liquid.ts
+++ b/packages/prettier-plugin-liquid/src/printer/print/liquid.ts
@@ -547,14 +547,46 @@ export function printLiquidDocExample(
   const parts: Doc[] = ['@example'];
 
   if (node.exampleContent?.value) {
-    const content = node.exampleContent.value.trim();
+    const content = node.exampleContent.value;
     if (content) {
+      // Count leading newlines before content (\n\nmy content)
+      const leadingNewlines = content.match(/^\n*/)?.[0]?.length ?? 0;
+      const trimmedContent = content.trim();
+
+      // Push inline content to new line
       parts.push(hardline);
-      const lines = content
-        .split('\n')
-        .map((line) => line.trim())
-        .filter(Boolean);
-      parts.push(join(hardline, lines));
+
+      // If there were two or more leading newlines, push another new line
+      if (leadingNewlines > 1) {
+        parts.push(hardline);
+      }
+
+      // If content doesn't have newlines in it, make sure it's on a new line (not inline)
+      if (!trimmedContent.includes('\n')) {
+        parts.push(trimmedContent);
+        return parts;
+      }
+
+      // For multi-line content
+      const lines = trimmedContent.split('\n');
+      const processedLines: string[] = [];
+      let emptyLineCount = 0;
+
+      for (let i = 0; i < lines.length; i++) {
+        const line = lines[i].trim();
+
+        if (line === '') {
+          emptyLineCount++;
+          if (emptyLineCount <= 2) {
+            processedLines.push('');
+          }
+        } else {
+          emptyLineCount = 0;
+          processedLines.push(line);
+        }
+      }
+
+      parts.push(join(hardline, processedLines));
     }
   }
 
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 58c7c41f8..cfd0afcd3 100644
--- a/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid
+++ b/packages/prettier-plugin-liquid/src/test/liquid-doc/fixed.liquid
@@ -33,3 +33,40 @@ It should push example content to the next line
   @example
   This is a valid example
 {% enddoc %}
+
+It should allow single empty lines between content
+{% doc %}
+  @example
+
+  This is a valid example
+
+  So is this without a newline
+  See?
+{% enddoc %}
+
+It should allow multiple empty lines between content
+{% doc %}
+  @example
+
+  This is a valid example
+
+
+  Here is another example
+
+  with a single empty line
+{% enddoc %}
+
+It should remove empty lines at the end of the content
+{% doc %}
+  @example
+
+  Here is my content and a newline
+{% enddoc %}
+
+It should respect example content with param and description
+{% doc %}
+  @param paramName - param with description
+  @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 f3d86be5e..33c83f71c 100644
--- a/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid
+++ b/packages/prettier-plugin-liquid/src/test/liquid-doc/index.liquid
@@ -32,3 +32,41 @@ It should push example content to the next line
 {% doc %}
   @example This is a valid example
 {% enddoc %}
+
+It should allow single empty lines between content
+{% doc %}
+  @example
+
+  This is a valid example
+
+  So is this without a newline
+  See?
+{% enddoc %}
+
+It should allow multiple empty lines between content
+{% doc %}
+  @example
+
+  This is a valid example
+
+
+  Here is another example
+
+  with a single empty line
+{% enddoc %}
+
+It should remove empty lines at the end of the content
+{% doc %}
+  @example
+
+  Here is my content and a newline
+
+{% enddoc %}
+
+It should respect example content with param and description
+{% doc %}
+@param paramName - param with description
+@example
+
+This is a valid example
+{% enddoc %}