Skip to content

Commit b19834d

Browse files
authored
Formatter: Skip formatting Scaffold Templates (#771)
This pull request adds a visitor to detect if a given ParseResult contains escaped `<%%` or `<%%=` tags, which are typically used in Rails Scaffold Templates. In the case of a scaffold template we abort the formatting and just return the original source, since we cannot reliably tell how to format a scaffold template properly, yet. Resolves #673
1 parent 280ca4a commit b19834d

File tree

3 files changed

+110
-0
lines changed

3 files changed

+110
-0
lines changed

javascript/packages/formatter/src/formatter.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { FormatPrinter } from "./format-printer.js"
22

3+
import { isScaffoldTemplate } from "./scaffold-template-detector.js"
34
import { resolveFormatOptions } from "./options.js"
45

56
import type { Config } from "@herb-tools/config"
@@ -57,7 +58,9 @@ export class Formatter {
5758
*/
5859
format(source: string, options: FormatOptions = {}, filePath?: string): string {
5960
let result = this.parse(source)
61+
6062
if (result.failed) return source
63+
if (isScaffoldTemplate(result)) return source
6164

6265
const resolvedOptions = resolveFormatOptions({ ...this.options, ...options })
6366

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Visitor } from "@herb-tools/core"
2+
import type { ERBContentNode, ParseResult } from "@herb-tools/core"
3+
4+
export const isScaffoldTemplate = (result: ParseResult): boolean => {
5+
const detector = new ScaffoldTemplateDetector()
6+
7+
detector.visit(result.value)
8+
9+
return detector.hasEscapedERB
10+
}
11+
12+
/**
13+
* Visitor that detects if the AST represents a Rails scaffold template.
14+
* Scaffold templates contain escaped ERB tags (<%%= or <%%)
15+
* and should not be formatted to preserve their exact structure.
16+
*/
17+
export class ScaffoldTemplateDetector extends Visitor {
18+
public hasEscapedERB = false
19+
20+
visitERBContentNode(node: ERBContentNode): void {
21+
const opening = node.tag_opening?.value
22+
23+
if (opening && opening.startsWith("<%%")) {
24+
this.hasEscapedERB = true
25+
26+
return
27+
}
28+
29+
if (this.hasEscapedERB) return
30+
31+
this.visitChildNodes(node)
32+
}
33+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { describe, test, expect, beforeAll } from "vitest"
2+
import { Herb } from "@herb-tools/node-wasm"
3+
import { Formatter } from "../../src"
4+
5+
import dedent from "dedent"
6+
7+
let formatter: Formatter
8+
9+
describe("ERB scaffold templates", () => {
10+
beforeAll(async () => {
11+
await Herb.load()
12+
13+
formatter = new Formatter(Herb, {
14+
indentWidth: 2,
15+
maxLineLength: 80
16+
})
17+
})
18+
19+
test("preserves entire document with escaped ERB output tags", () => {
20+
const source = '<%%=content%%>'
21+
const result = formatter.format(source)
22+
23+
expect(result).toEqual(source)
24+
})
25+
26+
test("preserves entire document with escaped ERB logic tags", () => {
27+
const source = '<%%if condition%%>'
28+
const result = formatter.format(source)
29+
30+
expect(result).toEqual(source)
31+
})
32+
33+
test("preserves entire document with escaped ERB tags and spaces", () => {
34+
const source = '<%%= content %%>'
35+
const result = formatter.format(source)
36+
37+
expect(result).toEqual(source)
38+
})
39+
40+
test("preserves mixed escaped and regular ERB tags", () => {
41+
const source = dedent`
42+
<div>
43+
<%%= spaced_escaped %%>
44+
<%=normal%>
45+
</div>
46+
`
47+
const result = formatter.format(source)
48+
49+
expect(result).toEqual(source)
50+
})
51+
52+
test("preserves scaffold template from issue #673 exactly as-is", () => {
53+
const source = dedent`
54+
<%# frozen_string_literal: true %>
55+
<%%= simple_form_for(@<%= singular_table_name %>) do |f| %>
56+
<%%= f.error_notification %>
57+
<%%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>
58+
59+
<div class="form-inputs">
60+
<%- attributes.each do |attribute| -%>
61+
<%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %>
62+
<%- end -%>
63+
</div>
64+
65+
<div class="form-actions">
66+
<%%= f.button :submit %>
67+
</div>
68+
<%% end %>
69+
`
70+
const result = formatter.format(source)
71+
72+
expect(result).toBe(source)
73+
})
74+
})

0 commit comments

Comments
 (0)