diff --git a/javascript/packages/formatter/src/format-helpers.ts b/javascript/packages/formatter/src/format-helpers.ts index 5a47489ae..9b6763b3b 100644 --- a/javascript/packages/formatter/src/format-helpers.ts +++ b/javascript/packages/formatter/src/format-helpers.ts @@ -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 --- @@ -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) */ diff --git a/javascript/packages/formatter/src/spacing-analyzer.ts b/javascript/packages/formatter/src/spacing-analyzer.ts index 6e9b5c7c4..5b3c53133 100644 --- a/javascript/packages/formatter/src/spacing-analyzer.ts +++ b/javascript/packages/formatter/src/spacing-analyzer.ts @@ -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" @@ -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) diff --git a/javascript/packages/formatter/test/erb/erb.test.ts b/javascript/packages/formatter/test/erb/erb.test.ts index 6181b363c..652cd623e 100644 --- a/javascript/packages/formatter/test/erb/erb.test.ts +++ b/javascript/packages/formatter/test/erb/erb.test.ts @@ -843,7 +843,6 @@ describe("@herb-tools/formatter", () => { data-column="<%= column[:name] %>" <%= 'checked' if column[:default_visible] %> > - <%= column[:label] %> diff --git a/javascript/packages/formatter/test/spacing.test.ts b/javascript/packages/formatter/test/spacing.test.ts index 330f4b0d0..8990d00f7 100644 --- a/javascript/packages/formatter/test/spacing.test.ts +++ b/javascript/packages/formatter/test/spacing.test.ts @@ -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` +
+ <%= user.name %> + <% if user.admin? %> + (admin) + <% end %> +
+ ` + const result = formatter.format(source) + expect(result).toEqual(dedent` ++ <%= user.name %> + <% if user.admin? %> + (admin) + <% end %> +
+ `) + }) + + test("does not space an input from its text inside a label", () => { + const source = dedent` + + ` + const result = formatter.format(source) + expect(result).toEqual(dedent` + + `) + }) + + test("still spaces block-level siblings inside a container", () => { + const source = dedent` +one
+ <% if channels.any? %> +two
+ <% end %> +one
+ + <% if channels.any? %> +two
+ <% end %> +