Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions javascript/packages/formatter/src/formatter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FormatPrinter } from "./format-printer.js"

import { isScaffoldTemplate } from "./scaffold-template-detector.js"
import { resolveFormatOptions } from "./options.js"

import type { Config } from "@herb-tools/config"
Expand Down Expand Up @@ -57,7 +58,9 @@ export class Formatter {
*/
format(source: string, options: FormatOptions = {}, filePath?: string): string {
let result = this.parse(source)

if (result.failed) return source
if (isScaffoldTemplate(result)) return source

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

Expand Down
33 changes: 33 additions & 0 deletions javascript/packages/formatter/src/scaffold-template-detector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Visitor } from "@herb-tools/core"
import type { ERBContentNode, ParseResult } from "@herb-tools/core"

export const isScaffoldTemplate = (result: ParseResult): boolean => {
const detector = new ScaffoldTemplateDetector()

detector.visit(result.value)

return detector.hasEscapedERB
}

/**
* Visitor that detects if the AST represents a Rails scaffold template.
* Scaffold templates contain escaped ERB tags (<%%= or <%%)
* and should not be formatted to preserve their exact structure.
*/
export class ScaffoldTemplateDetector extends Visitor {
public hasEscapedERB = false

visitERBContentNode(node: ERBContentNode): void {
const opening = node.tag_opening?.value

if (opening && opening.startsWith("<%%")) {
this.hasEscapedERB = true

return
}

if (this.hasEscapedERB) return

this.visitChildNodes(node)
}
}
74 changes: 74 additions & 0 deletions javascript/packages/formatter/test/erb/scaffold-templates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, test, expect, beforeAll } from "vitest"
import { Herb } from "@herb-tools/node-wasm"
import { Formatter } from "../../src"

import dedent from "dedent"

let formatter: Formatter

describe("ERB scaffold templates", () => {
beforeAll(async () => {
await Herb.load()

formatter = new Formatter(Herb, {
indentWidth: 2,
maxLineLength: 80
})
})

test("preserves entire document with escaped ERB output tags", () => {
const source = '<%%=content%%>'
const result = formatter.format(source)

expect(result).toEqual(source)
})

test("preserves entire document with escaped ERB logic tags", () => {
const source = '<%%if condition%%>'
const result = formatter.format(source)

expect(result).toEqual(source)
})

test("preserves entire document with escaped ERB tags and spaces", () => {
const source = '<%%= content %%>'
const result = formatter.format(source)

expect(result).toEqual(source)
})

test("preserves mixed escaped and regular ERB tags", () => {
const source = dedent`
<div>
<%%= spaced_escaped %%>
<%=normal%>
</div>
`
const result = formatter.format(source)

expect(result).toEqual(source)
})

test("preserves scaffold template from issue #673 exactly as-is", () => {
const source = dedent`
<%# frozen_string_literal: true %>
<%%= simple_form_for(@<%= singular_table_name %>) do |f| %>
<%%= f.error_notification %>
<%%= f.error_notification message: f.object.errors[:base].to_sentence if f.object.errors[:base].present? %>

<div class="form-inputs">
<%- attributes.each do |attribute| -%>
<%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %>
<%- end -%>
</div>

<div class="form-actions">
<%%= f.button :submit %>
</div>
<%% end %>
`
const result = formatter.format(source)

expect(result).toBe(source)
})
})
Loading