Skip to content
Closed
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
19 changes: 19 additions & 0 deletions javascript/packages/formatter/src/format-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ export const SPACEABLE_CONTAINERS = new Set([
'figure', 'details', 'summary', 'dialog', 'fieldset'
])

/**
* Elements whose content is a run of phrasing content rather than a list of
* blocks. Their children render as one continuous piece of text, so a blank
* line between them would split a single sentence in the source.
*/
export const TEXT_LEVEL_CONTAINERS = new Set([
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p'
])


// --- Node Utility Functions ---

Expand Down Expand Up @@ -227,6 +236,16 @@ export function isInlineElement(tagName: string): boolean {
return INLINE_ELEMENTS.has(tagName.toLowerCase())
}

/**
* Check if an element holds phrasing content, so its children render as one
* continuous run of text and must not be split apart by blank lines.
*/
export function isTextLevelContainer(tagName: string): boolean {
const normalized = tagName.toLowerCase()

return TEXT_LEVEL_CONTAINERS.has(normalized) || INLINE_ELEMENTS.has(normalized)
}

/**
* Check if the current inline element is adjacent to a previous inline element (no whitespace between)
*/
Expand Down
4 changes: 3 additions & 1 deletion javascript/packages/formatter/src/spacing-analyzer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Node, HTMLTextNode, HTMLElementNode, HTMLDoctypeNode, ERBContentNode, WhitespaceNode, XMLDeclarationNode } from "@herb-tools/core"
import { isNode, getTagName, isERBNode, isERBOutputNode, isERBCommentNode, isCommentNode, isERBControlFlowNode } from "@herb-tools/core"
import { findPreviousMeaningfulSibling, findNextMeaningfulSibling, isBlockLevelNode, isContentPreserving, isNonWhitespaceNode } from "./format-helpers.js"
import { findPreviousMeaningfulSibling, findNextMeaningfulSibling, isBlockLevelNode, isContentPreserving, isNonWhitespaceNode, isTextLevelContainer } from "./format-helpers.js"

import { INLINE_ELEMENTS, SPACEABLE_CONTAINERS } from "./format-helpers.js"

Expand Down Expand Up @@ -52,6 +52,8 @@ export class SpacingAnalyzer {

if (hasMixedContent) return false

if (parentElement && isTextLevelContainer(getTagName(parentElement))) return false

const isCurrentComment = isCommentNode(currentNode)
const isPreviousComment = previousNode ? isCommentNode(previousNode) : false
const isCurrentMultiline = this.isMultilineElement(currentNode)
Expand Down
1 change: 0 additions & 1 deletion javascript/packages/formatter/test/erb/erb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,6 @@ describe("@herb-tools/formatter", () => {
data-column="<%= column[:name] %>"
<%= 'checked' if column[:default_visible] %>
>

<span class="label-text text-muted-foreground">
<%= column[:label] %>
</span>
Expand Down
127 changes: 126 additions & 1 deletion javascript/packages/formatter/test/spacing.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,132 @@ describe("Spacing", () => {
})
})

describe("Text-level container Tests", () => {
test("does not space ERB output from a following ERB block inside a heading", () => {
const source = dedent`
<h3>
<%= pluralize(events.count, "event") %>
<% if channels.any? %>
- <%= channels.join(", ") %>
<% end %>
</h3>
`
const result = formatter.format(source)
expect(result).toEqual(dedent`
<h3>
<%= pluralize(events.count, "event") %>
<% if channels.any? %>
- <%= channels.join(", ") %>
<% end %>
</h3>
`)
})

test("does not space children of a paragraph", () => {
const source = dedent`
<p>
<%= user.name %>
<% if user.admin? %>
(admin)
<% end %>
</p>
`
const result = formatter.format(source)
expect(result).toEqual(dedent`
<p>
<%= user.name %>
<% if user.admin? %>
(admin)
<% end %>
</p>
`)
})

test("does not space an input from its text inside a label", () => {
const source = dedent`
<label>
<input type="checkbox" name="agree">
<span class="label-text">
<%= label %>
</span>
</label>
`
const result = formatter.format(source)
expect(result).toEqual(dedent`
<label>
<input type="checkbox" name="agree">
<span class="label-text">
<%= label %>
</span>
</label>
`)
})

test("still spaces block-level siblings inside a container", () => {
const source = dedent`
<div>
<p>one</p>
<% if channels.any? %>
<p>two</p>
<% end %>
</div>
`
const result = formatter.format(source)
expect(result).toEqual(dedent`
<div>
<p>one</p>

<% if channels.any? %>
<p>two</p>
<% end %>
</div>
`)
})

test("still separates ERB code from ERB output inside a container", () => {
const source = dedent`
<div>
<% user = current_user %>
<% time = Time.now %>
<%= user.name %>
<%= time.strftime("%Y") %>
</div>
`
const result = formatter.format(source)
expect(result).toEqual(dedent`
<div>
<% user = current_user %>
<% time = Time.now %>

<%= user.name %>
<%= time.strftime("%Y") %>
</div>
`)
})

test("keeps a blank line the author put inside a heading", () => {
const source = dedent`
<h3>
<%= pluralize(events.count, "event") %>

<% if channels.any? %>
- <%= channels.join(", ") %>
<% end %>
</h3>
`
const result = formatter.format(source)
expect(result).toEqual(dedent`
<h3>
<%= pluralize(events.count, "event") %>

<% if channels.any? %>
- <%= channels.join(", ") %>
<% end %>
</h3>
`)
})
})

describe("Inline Element Tests", () => {
test("inline elements (span, em, strong) don't get spaced", () => {
const source = dedent`
Expand All @@ -406,7 +532,6 @@ describe("Spacing", () => {
<span>Span 1</span>
<span>Span 2</span>
<span>Span 3</span>

<em>Emphasis</em>
<strong>Strong</strong>
</p>
Expand Down
Loading