Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,18 @@ describe("@herb-tools/formatter - content preserving tags", () => {
expect(formatter.format(result)).toEqual(result)
})

test("preserves attribute spacing on an element nested inside an ERB block (#2142)", () => {
const source = `<pre><% if condition %><span class="x">x</span><% end %></pre>`
const result = formatter.format(source)
expect(result).toEqual(`<pre><% if condition %><span class="x">x</span><% end %></pre>`)
})

test("preserves spacing between multiple attributes on an element nested inside an ERB block", () => {
const source = `<pre><% if condition %><span class="x" id="y">x</span><% end %></pre>`
const result = formatter.format(source)
expect(result).toEqual(`<pre><% if condition %><span class="x" id="y">x</span><% end %></pre>`)
})

test("preserves textarea with ERB control flow", () => {
const source = dedent`
<textarea>
Expand Down
31 changes: 29 additions & 2 deletions javascript/packages/printer/src/identity-printer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Printer } from "./printer.js"
import { getNodesBeforePosition, getNodesAfterPosition } from "@herb-tools/core"
import { getNodesBeforePosition, getNodesAfterPosition, isWhitespaceNode } from "@herb-tools/core"

import type * as Nodes from "@herb-tools/core"

Expand Down Expand Up @@ -44,13 +44,40 @@ export class IdentityPrinter extends Printer {
this.write(node.tag_name.value)
}

this.visitChildNodes(node)
// Without `track_whitespace: true` the parser doesn't emit a node for the
// whitespace that separates the tag name from the first attribute, or the
// whitespace between attributes, so reconstructing children back-to-back
// would merge them together (e.g. `<span class="x">` becoming
// `<spanclass="x">`). Restore a single separating space wherever the
// previous node's end position doesn't line up with the next node's start.
//
// WhitespaceNode children already print their own whitespace (including
// synthetic ones inserted by autofixers, whose location doesn't line up
// with the surrounding nodes), so they're skipped here to avoid writing a
// duplicate separating space in addition to the whitespace they print.
let previousEnd = node.tag_name?.location.end ?? node.tag_opening?.location.end

node.children.forEach(child => {
if (previousEnd && !isWhitespaceNode(child) && !this.samePosition(previousEnd, child.location.start)) {
this.write(" ")
}

this.visit(child)

if (!isWhitespaceNode(child)) {
previousEnd = child.location.end
}
})

if (node.tag_closing) {
this.write(node.tag_closing.value)
}
}

private samePosition(a: Nodes.Position, b: Nodes.Position): boolean {
return a.line === b.line && a.column === b.column
}

visitHTMLCloseTagNode(node: Nodes.HTMLCloseTagNode): void {
if (node.tag_opening) {
this.write(node.tag_opening.value)
Expand Down
Loading