Skip to content

Commit 6433a26

Browse files
authored
Formatter: Preserve user newlines in block elements with mixed content (#1279)
Resolves #1017 Resolves #1024
1 parent c70b962 commit 6433a26

File tree

4 files changed

+107
-52
lines changed

4 files changed

+107
-52
lines changed

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1244,6 +1244,7 @@ export class FormatPrinter extends Printer implements TextFlowDelegate, Attribut
12441244
const tagName = getTagName(node)
12451245
const children = filterSignificantChildren(node.body)
12461246
const openTagInline = this.shouldRenderOpenTagInline(node)
1247+
const openTagClosing = getOpenTagClosing(node)
12471248

12481249
if (!openTagInline) return false
12491250
if (children.length === 0) return true
@@ -1278,6 +1279,17 @@ export class FormatPrinter extends Printer implements TextFlowDelegate, Attribut
12781279
if (allChildrenAreERB) return false
12791280
}
12801281

1282+
if (!isInlineElement(tagName) && hasMixedTextAndInlineContent(children) && openTagClosing) {
1283+
const first = children[0]
1284+
const startsOnNewLine = first.location.start.line > openTagClosing.location.end.line
1285+
const hasLeadingNewline = isNode(first, HTMLTextNode) && /^\s*\n/.test(first.content)
1286+
const contentStartsOnNewLine = startsOnNewLine || hasLeadingNewline
1287+
1288+
if (contentStartsOnNewLine) {
1289+
return false
1290+
}
1291+
}
1292+
12811293
const allNestedAreInline = areAllNestedElementsInline(children)
12821294
const hasMultilineText = hasMultilineTextContent(children)
12831295
const hasMixedContent = hasMixedTextAndInlineContent(children)

javascript/packages/formatter/test/html/inline-elements.test.ts

Lines changed: 74 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import { describe, test, expect, beforeAll } from "vitest"
22
import { Herb } from "@herb-tools/node-wasm"
33
import { Formatter } from "../../src"
4+
import { createExpectFormattedToMatch } from "../helpers"
45

56
import dedent from "dedent"
67

78
let formatter: Formatter
9+
let expectFormattedToMatch: ReturnType<typeof createExpectFormattedToMatch>
810

911
describe("@herb-tools/formatter - inline elements", () => {
1012
beforeAll(async () => {
@@ -14,74 +16,48 @@ describe("@herb-tools/formatter - inline elements", () => {
1416
indentWidth: 2,
1517
maxLineLength: 80
1618
})
19+
20+
expectFormattedToMatch = createExpectFormattedToMatch(formatter)
1721
})
1822

1923
test("preserves inline elements in text flow (issue #251)", () => {
20-
const source = dedent`
21-
<p>Em<em>pha</em>sis</p>
22-
`
23-
const result = formatter.format(source)
24-
expect(result).toEqual(dedent`
24+
expectFormattedToMatch(dedent`
2525
<p>Em<em>pha</em>sis</p>
2626
`)
2727
})
2828

2929
test("preserves strong elements inline", () => {
30-
const source = dedent`
31-
<p>This is <strong>important</strong> text.</p>
32-
`
33-
const result = formatter.format(source)
34-
expect(result).toEqual(dedent`
30+
expectFormattedToMatch(dedent`
3531
<p>This is <strong>important</strong> text.</p>
3632
`)
3733
})
3834

3935
test("preserves span elements inline", () => {
40-
const source = dedent`
41-
<div>Some <span>inline</span> content</div>
42-
`
43-
const result = formatter.format(source)
44-
expect(result).toEqual(dedent`
36+
expectFormattedToMatch(dedent`
4537
<div>Some <span>inline</span> content</div>
4638
`)
4739
})
4840

4941
test("preserves links with attributes inline", () => {
50-
const source = dedent`
51-
<p>A <a href="/link">link</a> in text</p>
52-
`
53-
const result = formatter.format(source)
54-
expect(result).toEqual(dedent`
42+
expectFormattedToMatch(dedent`
5543
<p>A <a href="/link">link</a> in text</p>
5644
`)
5745
})
5846

5947
test("preserves multiple inline elements", () => {
60-
const source = dedent`
61-
<p>This has <em>emphasis</em> and <strong>strong</strong> text.</p>
62-
`
63-
const result = formatter.format(source)
64-
expect(result).toEqual(dedent`
48+
expectFormattedToMatch(dedent`
6549
<p>This has <em>emphasis</em> and <strong>strong</strong> text.</p>
6650
`)
6751
})
6852

6953
test("preserves nested inline elements", () => {
70-
const source = dedent`
71-
<p>This is <strong>very <em>important</em></strong> text.</p>
72-
`
73-
const result = formatter.format(source)
74-
expect(result).toEqual(dedent`
54+
expectFormattedToMatch(dedent`
7555
<p>This is <strong>very <em>important</em></strong> text.</p>
7656
`)
7757
})
7858

7959
test("preserves code elements inline", () => {
80-
const source = dedent`
81-
<p>Use the <code>foo()</code> function.</p>
82-
`
83-
const result = formatter.format(source)
84-
expect(result).toEqual(dedent`
60+
expectFormattedToMatch(dedent`
8561
<p>Use the <code>foo()</code> function.</p>
8662
`)
8763
})
@@ -99,11 +75,7 @@ describe("@herb-tools/formatter - inline elements", () => {
9975
})
10076

10177
test("preserves del and ins elements inline", () => {
102-
const source = dedent`
103-
<p>The price is <del>$50</del> <ins>$40</ins>.</p>
104-
`
105-
const result = formatter.format(source)
106-
expect(result).toEqual(dedent`
78+
expectFormattedToMatch(dedent`
10779
<p>The price is <del>$50</del> <ins>$40</ins>.</p>
10880
`)
10981
})
@@ -123,23 +95,25 @@ describe("@herb-tools/formatter - inline elements", () => {
12395
})
12496

12597
test("preserves inline elements with multiple attributes", () => {
126-
const source = dedent`
127-
<p>Visit <a href="/page" class="link" target="_blank">our page</a> today.</p>
128-
`
129-
const result = formatter.format(source)
130-
expect(result).toEqual(dedent`
98+
expectFormattedToMatch(dedent`
13199
<p>Visit <a href="/page" class="link" target="_blank">our page</a> today.</p>
132100
`)
133101
})
134102

103+
test("preserves inline elements when content starts on same line", () => {
104+
expectFormattedToMatch(`<p>Em<em>pha</em>sis</p>`)
105+
})
106+
135107
test("normalizes malformed closing tag placement", () => {
136108
const source = dedent`
137109
<p>
138110
Em<em>pha</em>sis</p>
139111
`
140112
const result = formatter.format(source)
141113
expect(result).toEqual(dedent`
142-
<p>Em<em>pha</em>sis</p>
114+
<p>
115+
Em<em>pha</em>sis
116+
</p>
143117
`)
144118
})
145119

@@ -151,7 +125,9 @@ describe("@herb-tools/formatter - inline elements", () => {
151125
`
152126
const result = formatter.format(source)
153127
expect(result).toEqual(dedent`
154-
<p>Em<em>pha</em>sis</p>
128+
<p>
129+
Em<em>pha</em>sis
130+
</p>
155131
`)
156132
})
157133

@@ -198,12 +174,61 @@ describe("@herb-tools/formatter - inline elements", () => {
198174
})
199175

200176
test("Element with ERB interpolation in text content", () => {
201-
const source = dedent`
177+
expectFormattedToMatch(dedent`
202178
<h2 class="title">Posts (<%= @posts.count %>)</h2>
179+
`)
180+
})
181+
182+
test("block element with ERB children on separate lines preserves format", () => {
183+
expectFormattedToMatch(dedent`
184+
<div class="form-inputs">
185+
<%= f.input :password, hint: false %>
186+
<%= f.input :password_confirmation %>
187+
</div>
188+
`)
189+
})
190+
191+
test("adjacent ERB tags on same line as block element expand to multiline", () => {
192+
const source = dedent`
193+
<div class="form-inputs"><%= f.input :password, hint: false %><%= f.input :password_confirmation %></div>
203194
`
204195
const result = formatter.format(source)
205196
expect(result).toEqual(dedent`
206-
<h2 class="title">Posts (<%= @posts.count %>)</h2>
197+
<div class="form-inputs">
198+
<%= f.input :password, hint: false %><%= f.input :password_confirmation %>
199+
</div>
200+
`)
201+
})
202+
203+
test("spaced ERB tags on same line as block element expand to multiline", () => {
204+
const source = dedent`
205+
<div class="form-inputs"><%= f.input :password, hint: false %> <%= f.input :password_confirmation %></div>
206+
`
207+
const result = formatter.format(source)
208+
expect(result).toEqual(dedent`
209+
<div class="form-inputs">
210+
<%= f.input :password, hint: false %> <%= f.input :password_confirmation %>
211+
</div>
212+
`)
213+
})
214+
215+
test("spaced ERB tags on separate line from block element preserves format", () => {
216+
const source = dedent`
217+
<div class="form-inputs">
218+
<%= f.input :password, hint: false %> <%= f.input :password_confirmation %>
219+
</div>
220+
`
221+
const result = formatter.format(source)
222+
expect(result).toEqual(dedent`
223+
<div class="form-inputs">
224+
<%= f.input :password, hint: false %> <%= f.input :password_confirmation %>
225+
</div>
226+
`)
227+
})
228+
229+
test("preserves ERB interpolation in attributes", () => {
230+
expectFormattedToMatch(dedent`
231+
<div class="<%= some_var %> style="<%= some_other_var" %>"></div>
207232
`)
208233
})
209234
})

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,9 @@ describe("@herb-tools/formatter", () => {
290290
`)
291291

292292
expect(result).toEqual(dedent`
293-
<div>Check <em>this</em> : it works!</div>
293+
<div>
294+
Check <em>this</em>: it works!
295+
</div>
294296
`)
295297
})
296298

@@ -392,6 +394,18 @@ describe("@herb-tools/formatter", () => {
392394
`)
393395
})
394396

397+
test("block element with mixed content and user newlines preserves line breaks", () => {
398+
expectFormattedToMatch(dedent`
399+
<div class="foo">
400+
This is some text and some <strong>bold</strong> text.
401+
</div>
402+
`)
403+
404+
expectFormattedToMatch(dedent`
405+
<div class="foo">This is some text and some <strong>bold</strong> text.</div>
406+
`)
407+
})
408+
395409
test("multiline span with text collapses to inline with spaces", () => {
396410
const source = dedent`
397411
<span>
@@ -462,7 +476,9 @@ describe("@herb-tools/formatter", () => {
462476
expect(result).toEqual(dedent`
463477
<p><b><%= a_thing %> <%= another_thing %></b></p>
464478
465-
<p><b><%= a_thing %> <%= another_thing %></b>a</p>
479+
<p>
480+
<b><%= a_thing %> <%= another_thing %></b>a
481+
</p>
466482
467483
<p>
468484
<b><%= a_thing %> <%= another_thing %></b>

javascript/packages/formatter/test/html/unicode-characters.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ describe("Unicode character handling", () => {
125125
const result = formatter.format(source)
126126

127127
expect(result).toBe(dedent`
128-
<p><%= "Text with — dash" %> and 'quotes'</p>
128+
<p>
129+
<%= "Text with — dash" %> and 'quotes'
130+
</p>
129131
130132
<%# ERB comment %>
131133
`)

0 commit comments

Comments
 (0)