Skip to content

Commit 47f7acc

Browse files
authored
Formatter: Respect maxLineLength for inline elements with ERB (#889)
Previously, inline elements containing ERB control flow could exceed the configured maxLineLength due to: 1. A hardcoded 120-character limit that bypassed `maxLineLength` setting 2. No detection of whether child elements could be rendered inline This pull request fixes these issues and adds recursive inline detection. **Input** ```html+erb <div> <span> <% [10, 20, 30, 50, 100].each do |per| %> <%= link_to_unless per_page == per, per.to_s, params.merge({ action:, page: 1, per:, remote: }) %> <% end %> </span> </div> ``` **Before** ```html+erb <div><span><% [10, 20, 30, 50, 100].each do |per| %><%= link_to_unless per_page == per, per.to_s, params.merge({ action:, page: 1, per:, remote: }) %><% end %></span></div> ``` **After** ```html+erb <div> <span> <% [10, 20, 30, 50, 100].each do |per| %> <%= link_to_unless per_page == per, per.to_s, params.merge({ action:, page: 1, per:, remote: }) %> <% end %> </span> </div> ``` Resolves #882
1 parent 2e293c2 commit 47f7acc

File tree

5 files changed

+416
-93
lines changed

5 files changed

+416
-93
lines changed

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1517,6 +1517,16 @@ export class FormatPrinter extends Printer {
15171517
if (!openTagInline) return false
15181518
if (children.length === 0) return true
15191519

1520+
const hasNonInlineChildElements = children.some(child => {
1521+
if (isNode(child, HTMLElementNode)) {
1522+
return !this.shouldRenderElementContentInline(child)
1523+
}
1524+
1525+
return false
1526+
})
1527+
1528+
if (hasNonInlineChildElements) return false
1529+
15201530
let hasLeadingHerbDisable = false
15211531

15221532
for (const child of node.body) {
@@ -1538,7 +1548,7 @@ export class FormatPrinter extends Printer {
15381548

15391549
if (fullInlineResult) {
15401550
const totalLength = this.indent.length + fullInlineResult.length
1541-
return totalLength <= this.maxLineLength || totalLength <= 120
1551+
return totalLength <= this.maxLineLength
15421552
}
15431553

15441554
return false
@@ -1573,8 +1583,9 @@ export class FormatPrinter extends Printer {
15731583

15741584
const childrenContent = this.renderChildrenInline(children)
15751585
const fullLine = openTagResult + childrenContent + `</${tagName}>`
1586+
const totalLength = this.indent.length + fullLine.length
15761587

1577-
if ((this.indent.length + fullLine.length) <= this.maxLineLength) {
1588+
if (totalLength <= this.maxLineLength) {
15781589
return true
15791590
}
15801591
}

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

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,4 +174,99 @@ describe("@herb-tools/formatter", () => {
174174
const output = formatter.format(input)
175175
expect(output).toEqual(input)
176176
})
177+
178+
test("case/when with long outputs inside inline element - breaks at ERB boundaries", () => {
179+
const input = dedent`
180+
<div>
181+
<span>
182+
<% case status %>
183+
<% when 'pending' %>
184+
<%= render partial: 'status_badge', locals: { text: 'Pending Review', color: 'yellow' } %>
185+
<% when 'approved' %>
186+
<%= render partial: 'status_badge', locals: { text: 'Approved', color: 'green' } %>
187+
<% when 'rejected' %>
188+
<%= render partial: 'status_badge', locals: { text: 'Rejected', color: 'red' } %>
189+
<% end %>
190+
</span>
191+
</div>
192+
`
193+
const output = formatter.format(input)
194+
expect(output).toEqual(input)
195+
})
196+
197+
test("case/when with method calls and long paths - breaks at ERB boundaries", () => {
198+
const input = dedent`
199+
<div>
200+
<span>
201+
<% case user.subscription_tier %>
202+
<% when :premium %>
203+
<%= link_to "Premium Dashboard", premium_dashboard_path(user_id: user.id, features: :all) %>
204+
<% when :basic %>
205+
<%= link_to "Basic Dashboard", basic_dashboard_path(user_id: user.id, features: :limited) %>
206+
<% else %>
207+
<%= link_to "Free Dashboard", free_dashboard_path(user_id: user.id, features: :minimal) %>
208+
<% end %>
209+
</span>
210+
</div>
211+
`
212+
const output = formatter.format(input)
213+
expect(output).toEqual(input)
214+
})
215+
216+
test("case/in with complex patterns and long outputs - breaks at ERB boundaries", () => {
217+
const input = dedent`
218+
<div>
219+
<span>
220+
<% case user_data %>
221+
<% in { role: 'admin', permissions: { level: level } } if level > 5 %>
222+
<%= link_to "Super Admin Panel", super_admin_path(user: user_data, access: :full) %>
223+
<% in { role: 'moderator', permissions: } %>
224+
<%= link_to "Moderator Tools", moderator_path(user: user_data, access: :limited) %>
225+
<% else %>
226+
<%= link_to "User Profile", profile_path(user: user_data, access: :basic) %>
227+
<% end %>
228+
</span>
229+
</div>
230+
`
231+
const output = formatter.format(input)
232+
expect(output).toEqual(input)
233+
})
234+
235+
describe("no whitespace introduction on single-line case statements", () => {
236+
test("short case/when stays on one line without whitespace", () => {
237+
const input = `<span><% case x %><% when 1 %>One<% when 2 %>Two<% else %>Other<% end %></span>`
238+
const output = formatter.format(input)
239+
expect(output).toEqual(input)
240+
})
241+
242+
test("short case/when with ERB output stays on one line - no whitespace", () => {
243+
const input = `<span><% case type %><% when :ok %><%= msg %><% else %>Error<% end %></span>`
244+
const output = formatter.format(input)
245+
expect(output).toEqual(input)
246+
})
247+
248+
test("short case/when with only text stays on one line - no whitespace", () => {
249+
const input = `<span><% case status %><% when 'ok' %>OK<% else %>Fail<% end %></span>`
250+
const output = formatter.format(input)
251+
expect(output).toEqual(input)
252+
})
253+
254+
test("short case/in stays on one line - no whitespace", () => {
255+
const input = `<span><% case val %><% in 1 %>A<% in 2 %>B<% else %>C<% end %></span>`
256+
const output = formatter.format(input)
257+
expect(output).toEqual(input)
258+
})
259+
260+
test("short case with symbols stays on one line - no whitespace", () => {
261+
const input = `<span><% case role %><% when :admin %>Admin<% when :user %>User<% end %></span>`
262+
const output = formatter.format(input)
263+
expect(output).toEqual(input)
264+
})
265+
266+
test("short case with ERB output stays on one line - no whitespace", () => {
267+
const input = `<span><% case a %><% when 1 %><%= x %><% when 2 %><%= y %><% end %></span>`
268+
const output = formatter.format(input)
269+
expect(output).toEqual(input)
270+
})
271+
})
177272
})

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

Lines changed: 93 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -795,100 +795,102 @@ describe("@herb-tools/formatter", () => {
795795
</script>
796796
`
797797

798-
const expected = dedent`
799-
<div class="relative">
800-
<button id="column-toggle-btn" class="d-btn d-btn-sm d-btn-secondary">
801-
<span>Show/Hide Columns</span>
802-
</button>
803-
804-
<div
805-
id="column-dropdown"
806-
class="
807-
absolute right-0 mt-1 bg-white border rounded-md shadow-lg z-10 p-2
808-
hidden
809-
"
810-
>
811-
<div class="flex flex-col space-y-1 min-w-[200px]">
812-
<% columns.each do |column| %>
813-
<div class="form-control">
814-
<label class="cursor-pointer label justify-start gap-2">
815-
<input
816-
type="checkbox"
817-
class="column-toggle checkbox checkbox-sm"
818-
data-column="<%= column[:name] %>"
819-
<%= 'checked' if column[:default_visible] %>
820-
>
821-
822-
<span class="label-text text-muted-foreground"><%= column[:label] %></span>
823-
</label>
824-
</div>
825-
<% end %>
826-
</div>
827-
</div>
828-
</div>
798+
const expected = dedent`
799+
<div class="relative">
800+
<button id="column-toggle-btn" class="d-btn d-btn-sm d-btn-secondary">
801+
<span>Show/Hide Columns</span>
802+
</button>
803+
804+
<div
805+
id="column-dropdown"
806+
class="
807+
absolute right-0 mt-1 bg-white border rounded-md shadow-lg z-10 p-2
808+
hidden
809+
"
810+
>
811+
<div class="flex flex-col space-y-1 min-w-[200px]">
812+
<% columns.each do |column| %>
813+
<div class="form-control">
814+
<label class="cursor-pointer label justify-start gap-2">
815+
<input
816+
type="checkbox"
817+
class="column-toggle checkbox checkbox-sm"
818+
data-column="<%= column[:name] %>"
819+
<%= 'checked' if column[:default_visible] %>
820+
>
829821
830-
<script>
831-
document.addEventListener('DOMContentLoaded', function() {
832-
// Column toggle functionality
833-
const columnToggleBtn = document.getElementById('column-toggle-btn');
834-
const columnDropdown = document.getElementById('column-dropdown');
835-
836-
// Toggle dropdown visibility
837-
columnToggleBtn.addEventListener('click', function() {
838-
columnDropdown.classList.toggle('hidden');
839-
});
822+
<span class="label-text text-muted-foreground">
823+
<%= column[:label] %>
824+
</span>
825+
</label>
826+
</div>
827+
<% end %>
828+
</div>
829+
</div>
830+
</div>
840831
841-
// Close dropdown when clicking outside
842-
document.addEventListener('click', function(event) {
843-
if (!columnToggleBtn.contains(event.target) && !columnDropdown.contains(event.target)) {
844-
columnDropdown.classList.add('hidden');
845-
}
846-
});
832+
<script>
833+
document.addEventListener('DOMContentLoaded', function() {
834+
// Column toggle functionality
835+
const columnToggleBtn = document.getElementById('column-toggle-btn');
836+
const columnDropdown = document.getElementById('column-dropdown');
847837
848-
// Handle column visibility toggles
849-
const columnToggles = document.querySelectorAll('.column-toggle');
850-
columnToggles.forEach(toggle => {
851-
toggle.addEventListener('change', function() {
852-
const columnName = this.dataset.column;
853-
const columnCells = document.querySelectorAll(\`.column-\${columnName}\`);
854-
855-
columnCells.forEach(cell => {
856-
if (this.checked) {
857-
cell.style.display = '';
858-
} else {
859-
cell.style.display = 'none';
860-
}
861-
});
838+
// Toggle dropdown visibility
839+
columnToggleBtn.addEventListener('click', function() {
840+
columnDropdown.classList.toggle('hidden');
841+
});
862842
863-
// Save column visibility preferences to localStorage
864-
localStorage.setItem(\`column_\${columnName}\`, this.checked ? 'visible' : 'hidden');
865-
});
843+
// Close dropdown when clicking outside
844+
document.addEventListener('click', function(event) {
845+
if (!columnToggleBtn.contains(event.target) && !columnDropdown.contains(event.target)) {
846+
columnDropdown.classList.add('hidden');
847+
}
848+
});
866849
867-
// Apply saved preferences on load
868-
const columnName = toggle.dataset.column;
869-
const savedPreference = localStorage.getItem(\`column_\${columnName}\`);
870-
871-
if (savedPreference === 'hidden') {
872-
toggle.checked = false;
873-
const columnCells = document.querySelectorAll(\`.column-\${columnName}\`);
874-
columnCells.forEach(cell => {
875-
cell.style.display = 'none';
876-
});
877-
}
878-
});
850+
// Handle column visibility toggles
851+
const columnToggles = document.querySelectorAll('.column-toggle');
852+
columnToggles.forEach(toggle => {
853+
toggle.addEventListener('change', function() {
854+
const columnName = this.dataset.column;
855+
const columnCells = document.querySelectorAll(\`.column-\${columnName}\`);
879856
880-
columnToggles.forEach(toggle => {
881-
if (!toggle.checked) {
882-
const columnName = toggle.dataset.column;
883-
const columnCells = document.querySelectorAll(\`.column-\${columnName}\`);
884-
columnCells.forEach(cell => {
885-
cell.style.display = 'none';
886-
});
887-
}
888-
});
889-
});
890-
</script>
891-
`
857+
columnCells.forEach(cell => {
858+
if (this.checked) {
859+
cell.style.display = '';
860+
} else {
861+
cell.style.display = 'none';
862+
}
863+
});
864+
865+
// Save column visibility preferences to localStorage
866+
localStorage.setItem(\`column_\${columnName}\`, this.checked ? 'visible' : 'hidden');
867+
});
868+
869+
// Apply saved preferences on load
870+
const columnName = toggle.dataset.column;
871+
const savedPreference = localStorage.getItem(\`column_\${columnName}\`);
872+
873+
if (savedPreference === 'hidden') {
874+
toggle.checked = false;
875+
const columnCells = document.querySelectorAll(\`.column-\${columnName}\`);
876+
columnCells.forEach(cell => {
877+
cell.style.display = 'none';
878+
});
879+
}
880+
});
881+
882+
columnToggles.forEach(toggle => {
883+
if (!toggle.checked) {
884+
const columnName = toggle.dataset.column;
885+
const columnCells = document.querySelectorAll(\`.column-\${columnName}\`);
886+
columnCells.forEach(cell => {
887+
cell.style.display = 'none';
888+
});
889+
}
890+
});
891+
});
892+
</script>
893+
`
892894

893895
const result = formatter.format(input)
894896
expect(result).toBe(expected)
@@ -1365,7 +1367,9 @@ describe("@herb-tools/formatter", () => {
13651367
<span>
13661368
<strong>Cover Image</strong><br>
13671369
Dimensions
1368-
<strong><%= "#{cover.metadata['width']}x#{cover.metadata['height']}" %></strong>
1370+
<strong>
1371+
<%= "#{cover.metadata['width']}x#{cover.metadata['height']}" %>
1372+
</strong>
13691373
&mdash;
13701374
<%= link_to "View original", rails_blob_path(cover), target: "_blank", rel: "noopener" %>
13711375
</span>

0 commit comments

Comments
 (0)