Skip to content

Commit 9f916a3

Browse files
committed
Fix ERB text flow formatting to keep adjacent inline elements together
Fixes: #903
1 parent 96329ce commit 9f916a3

File tree

4 files changed

+154
-8
lines changed

4 files changed

+154
-8
lines changed

javascript/packages/formatter/src/format-printer.ts

Lines changed: 78 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,8 +1742,71 @@ export class FormatPrinter extends Printer {
17421742
const adjacentInlineCount = countAdjacentInlineElements(children)
17431743

17441744
if (adjacentInlineCount >= 2) {
1745-
const { processedIndices } = this.renderAdjacentInlineElements(children, adjacentInlineCount)
1746-
this.visitRemainingChildren(children, processedIndices)
1745+
let processedCount = 0
1746+
const adjacentInlineContent = this.capture(() => {
1747+
const oldInlineMode = this.inlineMode
1748+
this.inlineMode = true
1749+
1750+
for (
1751+
let i = 0;
1752+
i < children.length && processedCount < adjacentInlineCount;
1753+
i++
1754+
) {
1755+
const child = children[i]
1756+
1757+
if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
1758+
continue
1759+
}
1760+
1761+
const isInlineOrERB =
1762+
(isNode(child, HTMLElementNode) &&
1763+
isInlineElement(getTagName(child))) ||
1764+
isNode(child, ERBContentNode)
1765+
1766+
if (isInlineOrERB) {
1767+
if (isNode(child, HTMLElementNode)) {
1768+
this.push(this.renderInlineElementAsString(child))
1769+
} else if (isNode(child, ERBContentNode)) {
1770+
this.push(this.renderERBAsString(child))
1771+
}
1772+
processedCount++
1773+
} else {
1774+
break
1775+
}
1776+
}
1777+
1778+
this.inlineMode = oldInlineMode
1779+
}).join("")
1780+
1781+
processedCount = 0
1782+
const remainingChildren = children.filter((child) => {
1783+
if (processedCount >= adjacentInlineCount) return true
1784+
1785+
if (isPureWhitespaceNode(child) || isNode(child, WhitespaceNode)) {
1786+
return true
1787+
}
1788+
1789+
const isInlineOrERB =
1790+
(isNode(child, HTMLElementNode) &&
1791+
isInlineElement(getTagName(child))) ||
1792+
isNode(child, ERBContentNode)
1793+
1794+
if (isInlineOrERB) {
1795+
processedCount++
1796+
return false
1797+
}
1798+
1799+
return true
1800+
})
1801+
1802+
if (adjacentInlineContent) {
1803+
this.buildAndWrapTextFlowWithPrefix(
1804+
adjacentInlineContent,
1805+
remainingChildren,
1806+
)
1807+
} else if (remainingChildren.length > 0) {
1808+
this.buildAndWrapTextFlow(remainingChildren)
1809+
}
17471810

17481811
return
17491812
}
@@ -1983,12 +2046,16 @@ export class FormatPrinter extends Printer {
19832046
}
19842047

19852048
/**
1986-
* Build words array from text/inline/ERB and wrap them
2049+
* Build words array from text/inline/ERB with an optional prefix and wrap them
19872050
*/
1988-
private buildAndWrapTextFlow(children: Node[]): void {
2051+
private buildAndWrapTextFlowWithPrefix(prefix: string, children: Node[]): void {
19892052
const unitsWithNodes: ContentUnitWithNode[] = this.buildContentUnitsWithNodes(children)
19902053
const words: Array<{ word: string, isHerbDisable: boolean }> = []
19912054

2055+
if (prefix) {
2056+
words.push({ word: prefix, isHerbDisable: false })
2057+
}
2058+
19922059
for (const { unit, node } of unitsWithNodes) {
19932060
if (unit.breaksFlow) {
19942061
this.flushWords(words)
@@ -2036,6 +2103,13 @@ export class FormatPrinter extends Printer {
20362103
this.flushWords(words)
20372104
}
20382105

2106+
/**
2107+
* Build words array from text/inline/ERB and wrap them
2108+
*/
2109+
private buildAndWrapTextFlow(children: Node[]): void {
2110+
this.buildAndWrapTextFlowWithPrefix('', children)
2111+
}
2112+
20392113
/**
20402114
* Try to merge text that follows an atomic unit (ERB/inline) with no whitespace
20412115
* Returns true if merge was performed

javascript/packages/formatter/test/erb/erb.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1379,7 +1379,22 @@ describe("@herb-tools/formatter", () => {
13791379
`
13801380

13811381
const result = formatter.format(input)
1382-
expect(result).toBe(input)
1382+
expect(result).toBe(dedent`
1383+
<% if cover.present? %>
1384+
<figure class="figure">
1385+
<%= image_tag attachment_url(cover), size: "460x249", class: "img-fluid figure-img" %>
1386+
1387+
<figcaption class="figure-caption text-center">
1388+
<span>
1389+
<strong>Cover Image</strong><br> Dimensions
1390+
<strong><%= "#{cover.metadata['width']}x#{cover.metadata['height']}" %></strong>
1391+
&mdash;
1392+
<%= link_to "View original", rails_blob_path(cover), target: "_blank", rel: "noopener" %>
1393+
</span>
1394+
</figcaption>
1395+
</figure>
1396+
<% end %>
1397+
`)
13831398
})
13841399

13851400
test("adjecent ERB text within elements", () => {

javascript/packages/formatter/test/erb/whitespace-formatting.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,63 @@ describe("ERB whitespace formatting", () => {
504504
})
505505
})
506506

507+
describe("line breaking elements and text flow", () => {
508+
test("does not add line breaks after ERB tags following <br>", () => {
509+
const source = dedent`
510+
<p><strong>Ut enim ad minima veniam</strong><br>
511+
<%= foo %> sed quia consequuntur magni <%= bar %>. Lorem ipsum dolor sit amet...</p>
512+
`
513+
const result = formatter.format(source)
514+
515+
expect(result).not.toContain(`<%= foo %>\n`)
516+
expect(result).not.toContain(`<%= bar %>\n .`)
517+
})
518+
519+
test("keeps period attached to ERB tag in text flow", () => {
520+
const source = `<p><strong>Summary:</strong><br> Lorem ipsum <%= foo %> dolor <%= bar %>. Sit amet...</p>`
521+
const result = formatter.format(source)
522+
523+
expect(result).not.toContain(`<%= bar %>\n.`)
524+
})
525+
526+
test("does not separate ERB tags from surrounding text", () => {
527+
const source = `Failed to <%= model.ignored? ? "unignore" : "ignore" %> model`
528+
const result = formatter.format(source)
529+
530+
expect(result).toBe(
531+
`Failed to <%= model.ignored? ? "unignore" : "ignore" %> model`,
532+
)
533+
})
534+
535+
test("preserves apostrophe with ERB tag inline", () => {
536+
const source = `<p><%= user.name %>'s profile</p>`
537+
const result = formatter.format(source)
538+
539+
expect(result).toBe(`<p><%= user.name %>'s profile</p>`)
540+
})
541+
542+
test("preserves possessive apostrophe after ERB tag", () => {
543+
const source = `Waiting for <%= contractor.name.first_or_business %>'s signature.`
544+
const result = formatter.format(source)
545+
546+
expect(result).toBe(
547+
`Waiting for <%= contractor.name.first_or_business %>'s signature.`,
548+
)
549+
})
550+
551+
test("handles long text with ERB tags after <br>", () => {
552+
const source = dedent`
553+
<p><strong>Ut enim ad minima veniam</strong><br>
554+
<%= foo %> sed quia consequuntur magni <%= bar %>. Lorem ipsum dolor sit amet consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.</p>
555+
`
556+
const result = formatter.format(source)
557+
558+
expect(result).not.toContain(`<%= foo %>\n sed`)
559+
expect(result).not.toContain(`<%= bar %>\n .`)
560+
expect(result).toContain(`<%= bar %>. Lorem`)
561+
})
562+
})
563+
507564
describe("shared utility validation", () => {
508565
test("demonstrates consistent ERB content formatting where it applies", () => {
509566
const erbContentCases = [

javascript/packages/formatter/test/html/text-content.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,9 @@ describe("@herb-tools/formatter", () => {
269269
<body>
270270
<div class="main">
271271
<p>
272-
<strong>Bold Heading:</strong><br>
273-
<%= Date.current %>: Lorem ipsum dolor sit amet, consectetur adipiscing
274-
elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
272+
<strong>Bold Heading:</strong><br> <%= Date.current %>: Lorem ipsum
273+
dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor
274+
incididunt ut labore et dolore magna aliqua.
275275
</p>
276276
</div>
277277
</body>

0 commit comments

Comments
 (0)