From 0dae61d15dcc25aad839225fd95d82d2d8e9d3ab Mon Sep 17 00:00:00 2001 From: Marco Roth Date: Mon, 3 Nov 2025 04:19:58 +0100 Subject: [PATCH] Formatter: Skip formatting Scaffold Templates --- .../packages/formatter/src/formatter.ts | 3 + .../src/scaffold-template-detector.ts | 33 +++++++++ .../test/erb/scaffold-templates.test.ts | 74 +++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 javascript/packages/formatter/src/scaffold-template-detector.ts create mode 100644 javascript/packages/formatter/test/erb/scaffold-templates.test.ts diff --git a/javascript/packages/formatter/src/formatter.ts b/javascript/packages/formatter/src/formatter.ts index e505bfe6d..68062cd65 100644 --- a/javascript/packages/formatter/src/formatter.ts +++ b/javascript/packages/formatter/src/formatter.ts @@ -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" @@ -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 }) diff --git a/javascript/packages/formatter/src/scaffold-template-detector.ts b/javascript/packages/formatter/src/scaffold-template-detector.ts new file mode 100644 index 000000000..1976ffad4 --- /dev/null +++ b/javascript/packages/formatter/src/scaffold-template-detector.ts @@ -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) + } +} diff --git a/javascript/packages/formatter/test/erb/scaffold-templates.test.ts b/javascript/packages/formatter/test/erb/scaffold-templates.test.ts new file mode 100644 index 000000000..87b2e336f --- /dev/null +++ b/javascript/packages/formatter/test/erb/scaffold-templates.test.ts @@ -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` +
+ <%%= spaced_escaped %%> + <%=normal%> +
+ ` + 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? %> + +
+ <%- attributes.each do |attribute| -%> + <%%= f.<%= attribute.reference? ? :association : :input %> :<%= attribute.name %> %> + <%- end -%> +
+ +
+ <%%= f.button :submit %> +
+ <%% end %> + ` + const result = formatter.format(source) + + expect(result).toBe(source) + }) +})