From fb03993150ad902ea064a9672e42c0645d6167c5 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Fri, 7 Nov 2025 05:35:21 +0100 Subject: [PATCH 1/2] Formatter: Respect Frontmatter when formatting documents --- .../packages/formatter/src/format-helpers.ts | 11 + .../packages/formatter/src/format-printer.ts | 33 ++- .../erb-formatter-fixtures.test.ts | 5 +- .../formatter/test/frontmatter.test.ts | 215 ++++++++++++++++++ 4 files changed, 255 insertions(+), 9 deletions(-) create mode 100644 javascript/packages/formatter/test/frontmatter.test.ts diff --git a/javascript/packages/formatter/src/format-helpers.ts b/javascript/packages/formatter/src/format-helpers.ts index 8fc9f1974..5a75f0853 100644 --- a/javascript/packages/formatter/src/format-helpers.ts +++ b/javascript/packages/formatter/src/format-helpers.ts @@ -521,3 +521,14 @@ export function isHerbDisableComment(node: Node): boolean { return trimmed.startsWith("herb:disable") } + +/** + * Check if a text node is YAML frontmatter (starts and ends with ---) + */ +export function isFrontmatter(node: Node): node is HTMLTextNode { + if (!isNode(node, HTMLTextNode)) return false + + const content = node.content.trim() + + return content.startsWith("---") && /---\s*$/.test(content) +} diff --git a/javascript/packages/formatter/src/format-printer.ts b/javascript/packages/formatter/src/format-printer.ts index ceca30000..7c20838bc 100644 --- a/javascript/packages/formatter/src/format-printer.ts +++ b/javascript/packages/formatter/src/format-printer.ts @@ -33,6 +33,7 @@ import { isBlockLevelNode, isClosingPunctuation, isContentPreserving, + isFrontmatter, isHerbDisableComment, isInlineElement, isNonWhitespaceNode, @@ -641,15 +642,16 @@ export class FormatPrinter extends Printer { // --- Visitor methods --- visitDocumentNode(node: DocumentNode) { - const hasTextFlow = this.isInTextFlowContext(null, node.children) + const children = this.formatFrontmatter(node) + const hasTextFlow = this.isInTextFlowContext(null, children) if (hasTextFlow) { - const children = filterSignificantChildren(node.children) + const filteredChildren = filterSignificantChildren(children) const wasInlineMode = this.inlineMode this.inlineMode = true - this.visitTextFlowChildren(children) + this.visitTextFlowChildren(filteredChildren) this.inlineMode = wasInlineMode @@ -659,10 +661,10 @@ export class FormatPrinter extends Printer { let lastWasMeaningful = false let hasHandledSpacing = false - for (let i = 0; i < node.children.length; i++) { - const child = node.children[i] + for (let i = 0; i < children.length; i++) { + const child = children[i] - if (shouldPreserveUserSpacing(child, node.children, i)) { + if (shouldPreserveUserSpacing(child, children, i)) { this.push("") hasHandledSpacing = true continue @@ -672,8 +674,8 @@ export class FormatPrinter extends Printer { continue } - if (shouldAppendToLastLine(child, node.children, i)) { - this.appendChildToLastLine(child, node.children, i) + if (shouldAppendToLastLine(child, children, i)) { + this.appendChildToLastLine(child, children, i) lastWasMeaningful = true hasHandledSpacing = false continue @@ -1473,6 +1475,21 @@ export class FormatPrinter extends Printer { // --- Utility methods --- + private formatFrontmatter(node: DocumentNode): Node[] { + const firstChild = node.children[0] + const hasFrontmatter = firstChild && isFrontmatter(firstChild) + + if (!hasFrontmatter) return node.children + + this.push(firstChild.content.trimEnd()) + + const remaining = node.children.slice(1) + + if (remaining.length > 0) this.push("") + + return remaining + } + /** * Append a child node to the last output line */ diff --git a/javascript/packages/formatter/test/erb-formatter/erb-formatter-fixtures.test.ts b/javascript/packages/formatter/test/erb-formatter/erb-formatter-fixtures.test.ts index ad83354c0..0c4ab6a8a 100644 --- a/javascript/packages/formatter/test/erb-formatter/erb-formatter-fixtures.test.ts +++ b/javascript/packages/formatter/test/erb-formatter/erb-formatter-fixtures.test.ts @@ -590,7 +590,10 @@ describe("ERB Formatter Fixture Tests", () => { const result = formatter.format(source) expect(result).toBe(dedent` - --- title: "My Page" layout: "application" --- + --- + title: "My Page" + layout: "application" + ---

<%= @title || "Default Title" %>

diff --git a/javascript/packages/formatter/test/frontmatter.test.ts b/javascript/packages/formatter/test/frontmatter.test.ts new file mode 100644 index 000000000..8705efb03 --- /dev/null +++ b/javascript/packages/formatter/test/frontmatter.test.ts @@ -0,0 +1,215 @@ +import { describe, test, expect, beforeAll } from "vitest" +import { Herb } from "@herb-tools/node-wasm" +import { Formatter } from "../src" + +import dedent from "dedent" + +let formatter: Formatter + +describe("@herb-tools/formatter", () => { + beforeAll(async () => { + await Herb.load() + + formatter = new Formatter(Herb, { + indentWidth: 2, + maxLineLength: 80 + }) + }) + + test("preserves YAML frontmatter with no formatting", () => { + const source = dedent` + --- + title: My Page + layout: application + published: true + --- + +
+

<%= @title %>

+
+ ` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves frontmatter with ERB content after it", () => { + const source = dedent` + --- + title: Test + --- + + <% if Rails.env.development? %> +

Debug mode

+ <% end %> + ` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("preserves frontmatter indentation as-is", () => { + const source = dedent` + --- + nested: + key: value + another: + deep: true + --- + +
Content
+ ` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("normalizes whitespace after frontmatter", () => { + const source = dedent` + --- + title: Test + --- + + + + +
Content
+ ` + const result = formatter.format(source) + expect(result).toEqual(dedent` + --- + title: Test + --- + +
Content
+ `) + }) + + test("handles frontmatter with arrays and objects", () => { + const source = dedent` + --- + tags: + - ruby + - rails + - erb + metadata: + author: John Doe + date: 2024-01-01 + --- + +
+

Title

+
+ ` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("formats HTML but preserves frontmatter when HTML is messy", () => { + const source = dedent` + --- + title: Test + --- + +
+

Title

+

Text

+
+ ` + const result = formatter.format(source) + expect(result).toEqual(dedent` + --- + title: Test + --- + +
+

Title

+

Text

+
+ `) + }) + + test("does not treat --- in the middle of document as frontmatter", () => { + const source = dedent` +
+

Some content

+ --- +

More content

+
+ ` + const result = formatter.format(source) + + expect(result).toEqual(source) + }) + + test("frontmatter must end with --- on its own line", () => { + const source = dedent` + --- + title: Test --- not closing +
Content
+ ` + const result = formatter.format(source) + + expect(result).toEqual(dedent` + --- title: Test --- not closing + +
Content
+ `) + }) + + test("empty frontmatter block", () => { + const source = dedent` + --- + --- + +
Content
+ ` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("frontmatter with comments", () => { + const source = dedent` + --- + # This is a YAML comment + title: My Page + # Another comment + layout: application + --- + +
Content
+ ` + const result = formatter.format(source) + expect(result).toEqual(source) + }) + + test("frontmatter adds newline", () => { + const source = dedent` + --- + title: My Page + --- +
Content
+ ` + const result = formatter.format(source) + expect(result).toEqual(dedent` + --- + title: My Page + --- + +
Content
+ `) + }) + + test("frontmatter with no newline after ---", () => { + const source = dedent` + --- + title: My Page + ---
Content
+ ` + const result = formatter.format(source) + expect(result).toEqual(dedent` + --- + title: My Page + --- + +
Content
+ `) + }) +}) From da6399f94bd4547368cc2fa7cf33ea09668b7b85 Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Fri, 7 Nov 2025 05:51:25 +0100 Subject: [PATCH 2/2] Add text only test --- .../packages/formatter/test/frontmatter.test.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/javascript/packages/formatter/test/frontmatter.test.ts b/javascript/packages/formatter/test/frontmatter.test.ts index 8705efb03..3ff8c0f27 100644 --- a/javascript/packages/formatter/test/frontmatter.test.ts +++ b/javascript/packages/formatter/test/frontmatter.test.ts @@ -212,4 +212,18 @@ describe("@herb-tools/formatter", () => {
Content
`) }) + + // TODO: maybe we can improve this in the future + test("frontmatter with text after ---", () => { + const source = dedent` + --- + title: My Page + --- + Content + ` + const result = formatter.format(source) + expect(result).toEqual(dedent` + --- title: My Page --- Content + `) + }) })