Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
24 changes: 23 additions & 1 deletion javascript/packages/printer/src/identity-printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,35 @@ 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
// (when whitespace tracking is on, the gap is already covered by an
// explicit WhitespaceNode, so no extra space is added in that case).
let previousEnd = node.tag_name?.location.end ?? node.tag_opening?.location.end

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

this.visit(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