Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions javascript/packages/formatter/src/format-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
33 changes: 25 additions & 8 deletions javascript/packages/formatter/src/format-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
isBlockLevelNode,
isClosingPunctuation,
isContentPreserving,
isFrontmatter,
isHerbDisableComment,
isInlineElement,
isNonWhitespaceNode,
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
---

<div class="page">
<h1><%= @title || "Default Title" %></h1>
Expand Down
229 changes: 229 additions & 0 deletions javascript/packages/formatter/test/frontmatter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
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
---

<div class="container">
<h1><%= @title %></h1>
</div>
`
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? %>
<p>Debug mode</p>
<% 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
---

<div>Content</div>
`
const result = formatter.format(source)
expect(result).toEqual(source)
})

test("normalizes whitespace after frontmatter", () => {
const source = dedent`
---
title: Test
---




<div>Content</div>
`
const result = formatter.format(source)
expect(result).toEqual(dedent`
---
title: Test
---

<div>Content</div>
`)
})

test("handles frontmatter with arrays and objects", () => {
const source = dedent`
---
tags:
- ruby
- rails
- erb
metadata:
author: John Doe
date: 2024-01-01
---

<article>
<h1>Title</h1>
</article>
`
const result = formatter.format(source)
expect(result).toEqual(source)
})

test("formats HTML but preserves frontmatter when HTML is messy", () => {
const source = dedent`
---
title: Test
---

<div class="container" >
<h1>Title</h1>
<p>Text</p>
</div>
`
const result = formatter.format(source)
expect(result).toEqual(dedent`
---
title: Test
---

<div class="container">
<h1>Title</h1>
<p>Text</p>
</div>
`)
})

test("does not treat --- in the middle of document as frontmatter", () => {
const source = dedent`
<div>
<p>Some content</p>
---
<p>More content</p>
</div>
`
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
<div>Content</div>
`
const result = formatter.format(source)

expect(result).toEqual(dedent`
--- title: Test --- not closing

<div>Content</div>
`)
})

test("empty frontmatter block", () => {
const source = dedent`
---
---

<div>Content</div>
`
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
---

<div>Content</div>
`
const result = formatter.format(source)
expect(result).toEqual(source)
})

test("frontmatter adds newline", () => {
const source = dedent`
---
title: My Page
---
<div>Content</div>
`
const result = formatter.format(source)
expect(result).toEqual(dedent`
---
title: My Page
---

<div>Content</div>
`)
})

test("frontmatter with no newline after ---", () => {
const source = dedent`
---
title: My Page
---<div>Content</div>
`
const result = formatter.format(source)
expect(result).toEqual(dedent`
---
title: My Page
---

<div>Content</div>
`)
})

// 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
`)
})
})
Loading