diff --git a/javascript/packages/formatter/rollup.config.mjs b/javascript/packages/formatter/rollup.config.mjs
index b22d7bcba..40d9c1751 100644
--- a/javascript/packages/formatter/rollup.config.mjs
+++ b/javascript/packages/formatter/rollup.config.mjs
@@ -8,6 +8,8 @@ const external = [
"url",
"fs",
"module",
+ "@herb-tools/node-wasm",
+ "@herb-tools/printer",
"@herb-tools/rewriter"
]
diff --git a/javascript/packages/formatter/test/cli/__snapshots__/rewriters.test.ts.snap b/javascript/packages/formatter/test/cli/__snapshots__/rewriters.test.ts.snap
index ccf53a111..5880bd205 100644
--- a/javascript/packages/formatter/test/cli/__snapshots__/rewriters.test.ts.snap
+++ b/javascript/packages/formatter/test/cli/__snapshots__/rewriters.test.ts.snap
@@ -5,7 +5,7 @@ exports[`CLI > Rewriters > should actually apply Tailwind class sorting 1`] = `
⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md
[32m
-Using 1 pre-format rewriter:[0m
+Using 1 post-format rewriter:[0m
[36m • tailwind-class-sorter[0m[2m (built-in)[0m
"
@@ -16,7 +16,7 @@ exports[`CLI > Rewriters > should format from stdin with rewriters configured 1`
⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md
[32m
-Using 1 pre-format rewriter:[0m
+Using 1 post-format rewriter:[0m
[36m • tailwind-class-sorter[0m[2m (built-in)[0m
"
@@ -32,7 +32,7 @@ exports[`CLI > Rewriters > should handle both pre and post rewriters 1`] = `
⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md
[32m
-Using 1 pre-format rewriter:[0m
+Using 1 post-format rewriter:[0m
[36m • tailwind-class-sorter[0m[2m (built-in)[0m
"
@@ -50,18 +50,18 @@ exports[`CLI > Rewriters > should show multiple rewriters on stderr 1`] = `
⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md
[32m
-Using 1 pre-format rewriter:[0m
+Using 1 post-format rewriter:[0m
[36m • tailwind-class-sorter[0m[2m (built-in)[0m
"
`;
-exports[`CLI > Rewriters > should show rewriter info on stderr when pre-format rewriters are configured 1`] = `
+exports[`CLI > Rewriters > should show rewriter info on stderr when post-format rewriters are configured 1`] = `
"✓ Using Herb config file at test-rewriters/.herb.yml
⚠️ Experimental Preview: The formatter is in early development. Please report any unexpected behavior or bugs to https://github.com/marcoroth/herb/issues/new?template=formatting-issue.md
[32m
-Using 1 pre-format rewriter:[0m
+Using 1 post-format rewriter:[0m
[36m • tailwind-class-sorter[0m[2m (built-in)[0m
"
diff --git a/javascript/packages/formatter/test/cli/rewriters.test.ts b/javascript/packages/formatter/test/cli/rewriters.test.ts
index 1ec0c9d97..c63f3178e 100644
--- a/javascript/packages/formatter/test/cli/rewriters.test.ts
+++ b/javascript/packages/formatter/test/cli/rewriters.test.ts
@@ -27,12 +27,12 @@ describe("CLI", () => {
await rm(testDir, { recursive: true }).catch(() => {})
})
- it("should show rewriter info on stderr when pre-format rewriters are configured", async () => {
+ it("should show rewriter info on stderr when post-format rewriters are configured", async () => {
const config = dedent`
formatter:
enabled: true
rewriter:
- pre:
+ post:
- tailwind-class-sorter
`
@@ -52,7 +52,7 @@ describe("CLI", () => {
formatter:
enabled: true
rewriter:
- pre:
+ post:
- tailwind-class-sorter
`
@@ -72,7 +72,7 @@ describe("CLI", () => {
formatter:
enabled: true
rewriter:
- pre:
+ post:
- tailwind-class-sorter
`
@@ -97,7 +97,7 @@ describe("CLI", () => {
formatter:
enabled: true
rewriter:
- pre:
+ post:
- tailwind-class-sorter
`
@@ -153,7 +153,7 @@ describe("CLI", () => {
formatter:
enabled: true
rewriter:
- pre:
+ post:
- tailwind-class-sorter
`
diff --git a/javascript/packages/formatter/test/rewriters/custom-rewriters.test.ts b/javascript/packages/formatter/test/rewriters/custom-rewriters.test.ts
index e81ba5382..caa8357cc 100644
--- a/javascript/packages/formatter/test/rewriters/custom-rewriters.test.ts
+++ b/javascript/packages/formatter/test/rewriters/custom-rewriters.test.ts
@@ -39,13 +39,15 @@ describe("Formatter with Rewriters Integration", () => {
})
test("combines custom rewriter with built-in Tailwind sorter", async () => {
- const { preRewriters, postRewriters, preCount, warnings } = await loadRewritersHelper({
+ const { preRewriters, postRewriters, preCount, postCount, warnings } = await loadRewritersHelper({
baseDir: process.cwd(),
patterns: ["test/rewriters/fixtures/**/*.js"],
- pre: ["tailwind-class-sorter", "uppercase-tags"]
+ pre: ["uppercase-tags"],
+ post: ["tailwind-class-sorter"]
})
- expect(preCount).toBe(2)
+ expect(preCount).toBe(1)
+ expect(postCount).toBe(1)
expect(warnings).toEqual([])
const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters })
diff --git a/javascript/packages/formatter/test/rewriters/formatter-integration.test.ts b/javascript/packages/formatter/test/rewriters/formatter-integration.test.ts
index ebaa6faed..ea33b4b0b 100644
--- a/javascript/packages/formatter/test/rewriters/formatter-integration.test.ts
+++ b/javascript/packages/formatter/test/rewriters/formatter-integration.test.ts
@@ -36,22 +36,22 @@ describe("Formatter with Rewriters Integration", () => {
expect(info.warnings).toEqual([])
})
- test("loadRewriters with Tailwind class sorter", async () => {
+ test("loadRewriters with Tailwind class sorter as post-rewriter", async () => {
const info = await loadRewritersHelper({
baseDir: process.cwd(),
- pre: ["tailwind-class-sorter"],
- post: [],
+ pre: [],
+ post: ["tailwind-class-sorter"],
loadCustomRewriters: false
})
- expect(info.preCount).toBe(1)
- expect(info.postCount).toBe(0)
+ expect(info.preCount).toBe(0)
+ expect(info.postCount).toBe(1)
})
test("formats with Tailwind class sorter enabled", async () => {
const { preRewriters, postRewriters } = await loadRewritersHelper({
baseDir: process.cwd(),
- pre: ["tailwind-class-sorter"],
+ post: ["tailwind-class-sorter"],
loadCustomRewriters: false
})
@@ -82,32 +82,28 @@ describe("Formatter with Rewriters Integration", () => {
test("loadRewriters is idempotent", async () => {
const info1 = await loadRewritersHelper({
baseDir: process.cwd(),
- pre: ["tailwind-class-sorter"],
+ post: ["tailwind-class-sorter"],
loadCustomRewriters: false
})
const info2 = await loadRewritersHelper({
baseDir: process.cwd(),
- pre: ["tailwind-class-sorter"],
+ post: ["tailwind-class-sorter"],
loadCustomRewriters: false
})
- expect(info1.preCount).toBe(info2.preCount)
+ expect(info1.postCount).toBe(info2.postCount)
})
test("format works with file path parameter", async () => {
const { preRewriters, postRewriters } = await loadRewritersHelper({
baseDir: process.cwd(),
- pre: ["tailwind-class-sorter"],
+ post: ["tailwind-class-sorter"],
loadCustomRewriters: false
})
const formatter = new Formatter(Herb, { indentWidth: 2, preRewriters, postRewriters })
-
- const source = dedent`
-
- `
-
+ const source = ``
const result = formatter.format(source, {}, "path/to/file.html.erb")
expect(result).toBeDefined()
@@ -116,7 +112,7 @@ describe("Formatter with Rewriters Integration", () => {
test("continues formatting even if rewriter fails", async () => {
const { preRewriters, postRewriters } = await loadRewritersHelper({
baseDir: process.cwd(),
- pre: ["tailwind-class-sorter"],
+ post: ["tailwind-class-sorter"],
loadCustomRewriters: false
})
@@ -145,7 +141,7 @@ describe("Formatter with Rewriters Integration", () => {
test("formats complex ERB with rewriters", async () => {
const { preRewriters, postRewriters } = await loadRewritersHelper({
baseDir: process.cwd(),
- pre: ["tailwind-class-sorter"],
+ post: ["tailwind-class-sorter"],
loadCustomRewriters: false
})
@@ -173,4 +169,93 @@ describe("Formatter with Rewriters Integration", () => {
`)
})
+
+ describe("Action View Tag Helper class sorting", () => {
+ test("sorts classes in tag.div with block", async () => {
+ const { preRewriters, postRewriters } = await loadRewritersHelper({
+ baseDir: process.cwd(),
+ post: ["tailwind-class-sorter"],
+ loadCustomRewriters: false
+ })
+
+ const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters })
+ const source = `<%= tag.div class: "px-4 bg-blue-500 text-white" do %><% end %>`
+ const result = formatter.format(source)
+
+ expect(result).toBe(`<%= tag.div class: "bg-blue-500 px-4 text-white" do %>\n<% end %>`)
+
+ })
+
+ test("sorts classes in tag.div with single quotes", async () => {
+ const { preRewriters, postRewriters } = await loadRewritersHelper({
+ baseDir: process.cwd(),
+ post: ["tailwind-class-sorter"],
+ loadCustomRewriters: false
+ })
+
+ const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters })
+ const source = `<%= tag.div class: 'px-4 bg-blue-500 text-white' do %><% end %>`
+ const result = formatter.format(source)
+
+ expect(result).toBe(`<%= tag.div class: 'bg-blue-500 px-4 text-white' do %>\n<% end %>`)
+
+ })
+
+ test("sorts classes in content_tag with block", async () => {
+ const { preRewriters, postRewriters } = await loadRewritersHelper({
+ baseDir: process.cwd(),
+ post: ["tailwind-class-sorter"],
+ loadCustomRewriters: false
+ })
+
+ const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters })
+ const source = `<%= content_tag :div, class: "px-4 bg-blue-500 text-white" do %><% end %>`
+ const result = formatter.format(source)
+
+ expect(result).toBe(`<%= content_tag :div, class: "bg-blue-500 px-4 text-white" do %>\n<% end %>`)
+
+ })
+
+ test("sorts both HTML and Action View Tag Helper classes together", async () => {
+ const { preRewriters, postRewriters } = await loadRewritersHelper({
+ baseDir: process.cwd(),
+ post: ["tailwind-class-sorter"],
+ loadCustomRewriters: false
+ })
+
+ const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters })
+
+ const source = dedent`
+
+ <%= tag.div class: "text-white rounded px-2" do %>
+ Content
+ <% end %>
+
+ `
+
+ const result = formatter.format(source)
+
+ expect(result).toBe(dedent`
+
+ <%= tag.div class: "rounded px-2 text-white" do %>
+ Content
+ <% end %>
+
+ `)
+ })
+
+ test("does not sort dynamic Action View Tag Helper class values", async () => {
+ const { preRewriters, postRewriters } = await loadRewritersHelper({
+ baseDir: process.cwd(),
+ post: ["tailwind-class-sorter"],
+ loadCustomRewriters: false
+ })
+
+ const formatter = new Formatter(Herb, { indentWidth: 2, maxLineLength: 80, preRewriters, postRewriters })
+ const source = `<%= tag.div class: dynamic_classes do %><% end %>`
+ const result = formatter.format(source)
+
+ expect(result).toBe(`<%= tag.div class: dynamic_classes do %>\n<% end %>`)
+ })
+ })
})
diff --git a/javascript/packages/rewriter/rollup.config.mjs b/javascript/packages/rewriter/rollup.config.mjs
index cf1aea451..8ac5908c9 100644
--- a/javascript/packages/rewriter/rollup.config.mjs
+++ b/javascript/packages/rewriter/rollup.config.mjs
@@ -8,6 +8,8 @@ const external = [
"url",
"fs",
"module",
+ "@herb-tools/node-wasm",
+ "@herb-tools/printer",
"@herb-tools/tailwind-class-sorter",
"tinyglobby"
]
diff --git a/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts b/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts
index 1f2946917..dd57a1da3 100644
--- a/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts
+++ b/javascript/packages/rewriter/src/built-ins/tailwind-class-sorter.ts
@@ -1,29 +1,37 @@
-import { getStaticAttributeName, isLiteralNode, isPureWhitespaceNode, splitLiteralsAtWhitespace, groupNodesByClass } from "@herb-tools/core"
+import { getStaticAttributeName, isLiteralNode, isPureWhitespaceNode, splitLiteralsAtWhitespace, groupNodesByClass, isERBOpenTagNode, isHTMLAttributeNode } from "@herb-tools/core"
import { LiteralNode, Location, Visitor } from "@herb-tools/core"
+import { Herb } from "@herb-tools/node-wasm"
+import { IdentityPrinter } from "@herb-tools/printer"
+
import { TailwindClassSorter } from "@herb-tools/tailwind-class-sorter"
-import { ASTRewriter } from "../ast-rewriter.js"
+import { StringRewriter } from "../string-rewriter.js"
import { asMutable } from "../mutable.js"
import type { RewriteContext } from "../context.js"
+
+type ClassSplice = { from: string, to: string }
+
import type {
- HTMLAttributeNode,
- HTMLAttributeValueNode,
- Node,
- ERBIfNode,
- ERBUnlessNode,
- ERBElseNode,
+ ERBBeginNode,
ERBBlockNode,
- ERBForNode,
- ERBCaseNode,
- ERBWhenNode,
ERBCaseMatchNode,
+ ERBCaseNode,
+ ERBElseNode,
+ ERBEnsureNode,
+ ERBForNode,
+ ERBIfNode,
ERBInNode,
- ERBWhileNode,
- ERBUntilNode,
- ERBBeginNode,
+ ERBOpenTagNode,
ERBRescueNode,
- ERBEnsureNode
+ ERBUnlessNode,
+ ERBUntilNode,
+ ERBWhenNode,
+ ERBWhileNode,
+ HTMLAttributeNode,
+ HTMLAttributeValueNode,
+ HTMLElementNode,
+ Node,
} from "@herb-tools/core"
/**
@@ -322,9 +330,92 @@ class ClassAttributeSorter extends Visitor {
}
/**
- * Built-in rewriter that sorts Tailwind CSS classes in class and className attributes
+ * Visitor that extracts class attribute splice operations from Action View Tag Helper elements.
+ * Finds static class attributes and records old/new ERB tag text for string-level replacement.
+ */
+class ActionViewClassSorterVisitor extends Visitor {
+ private sorter: TailwindClassSorter
+ private splices: ClassSplice[]
+
+ constructor(sorter: TailwindClassSorter, splices: ClassSplice[]) {
+ super()
+
+ this.sorter = sorter
+ this.splices = splices
+ }
+
+ visitHTMLElementNode(node: HTMLElementNode): void {
+ if (node.element_source && isERBOpenTagNode(node.open_tag)) {
+ this.extractSplice(node)
+ }
+
+ this.visitChildNodes(node)
+ }
+
+ private extractSplice(node: HTMLElementNode): void {
+ const openTag = node.open_tag as ERBOpenTagNode
+
+ if (!openTag.content) return
+ if (!openTag.tag_opening) return
+ if (!openTag.tag_closing) return
+ if (!openTag.children) return
+
+ for (const child of openTag.children) {
+ if (!isHTMLAttributeNode(child)) continue
+ if (!child.name || !child.value) continue
+
+ const attributeName = getStaticAttributeName(child.name)
+ if (attributeName !== "class") continue
+
+ const valueChildren = child.value.children
+ if (!valueChildren || valueChildren.length === 0) continue
+
+ if (!valueChildren.every(isLiteralNode)) continue
+
+ const classValue = (valueChildren as LiteralNode[]).map(n => n.content).join("")
+ if (!classValue.trim()) continue
+
+ let sortedValue: string
+
+ try {
+ sortedValue = this.sorter.sortClasses(classValue)
+ } catch {
+ continue
+ }
+
+ if (sortedValue === classValue) continue
+
+ const oldContent = openTag.content.value
+ const doubleQuoted = `"${classValue}"`
+ const singleQuoted = `'${classValue}'`
+
+ let newContent: string
+
+ if (oldContent.includes(doubleQuoted)) {
+ newContent = oldContent.replace(doubleQuoted, `"${sortedValue}"`)
+ } else if (oldContent.includes(singleQuoted)) {
+ newContent = oldContent.replace(singleQuoted, `'${sortedValue}'`)
+ } else {
+ continue
+ }
+
+ const oldTag = openTag.tag_opening.value + oldContent + openTag.tag_closing.value
+ const newTag = openTag.tag_opening.value + newContent + openTag.tag_closing.value
+
+ this.splices.push({ from: oldTag, to: newTag })
+ }
+ }
+}
+
+/**
+ * Built-in rewriter that sorts Tailwind CSS classes in class and className attributes.
+ *
+ * Operates as a string rewriter with two phases:
+ * 1. Parse the template normally and sort HTML element class attributes via AST manipulation.
+ * 2. Parse again with action_view_helpers enabled to locate and sort class attributes in
+ * Action View Tag Helper expressions (tag.div, content_tag, etc.) via string splicing.
*/
-export class TailwindClassSorterRewriter extends ASTRewriter {
+export class TailwindClassSorterRewriter extends StringRewriter {
private sorter?: TailwindClassSorter
get name(): string {
@@ -357,15 +448,54 @@ export class TailwindClassSorterRewriter extends ASTRewriter {
}
}
- rewrite(node: T, _context: RewriteContext): T {
- if (!this.sorter) {
- return node
+ rewrite(formatted: string, _context: RewriteContext): string {
+ if (!this.sorter) return formatted
+
+ let htmlSorted: string
+
+ try {
+ const parseResult = Herb.parse(formatted, { track_whitespace: true })
+
+ if (parseResult.failed) return formatted
+
+ const visitor = new TailwindClassSorterVisitor(this.sorter)
+ visitor.visit(parseResult.value)
+
+ htmlSorted = IdentityPrinter.print(parseResult.value)
+ } catch {
+ return formatted
}
- const visitor = new TailwindClassSorterVisitor(this.sorter)
+ return this.sortActionViewHelperClasses(htmlSorted)
+ }
- visitor.visit(node)
+ private sortActionViewHelperClasses(source: string): string {
+ let parseResult
- return node
+ try {
+ parseResult = Herb.parse(source, {
+ track_whitespace: true,
+ action_view_helpers: true
+ })
+ } catch {
+ return source
+ }
+
+ if (parseResult.failed) return source
+
+ const splices: ClassSplice[] = []
+ const visitor = new ActionViewClassSorterVisitor(this.sorter!, splices)
+
+ visitor.visit(parseResult.value)
+
+ if (splices.length === 0) return source
+
+ let result = source
+
+ for (const { from, to } of splices) {
+ result = result.replaceAll(from, to)
+ }
+
+ return result
}
}
diff --git a/javascript/packages/rewriter/test/built-ins/tailwind-class-sorter.test.ts b/javascript/packages/rewriter/test/built-ins/tailwind-class-sorter.test.ts
index 6368d2990..534606ee0 100644
--- a/javascript/packages/rewriter/test/built-ins/tailwind-class-sorter.test.ts
+++ b/javascript/packages/rewriter/test/built-ins/tailwind-class-sorter.test.ts
@@ -2,7 +2,6 @@ import dedent from "dedent"
import { describe, test, expect, beforeAll } from "vitest"
import { Herb } from "@herb-tools/node-wasm"
-import { IdentityPrinter } from "@herb-tools/printer"
import { TailwindClassSorterRewriter } from "../../src/built-ins/tailwind-class-sorter.js"
import { createRewriterTest } from "../helpers/rewriter-test-helper.js"
@@ -29,29 +28,24 @@ describe("tailwind-class-sorter", () => {
await expect(rewriter.initialize({ baseDir: process.cwd() })).resolves.not.toThrow()
})
- test("returns original AST when sorter not initialized", async () => {
+ test("returns original string when sorter not initialized", async () => {
const input = dedent`
`
const rewriter = new TailwindClassSorterRewriter()
+ const output = rewriter.rewrite(input, { baseDir: process.cwd() })
- const parseResult = Herb.parse(input, { track_whitespace: true })
- const document = rewriter.rewrite(parseResult.value, { baseDir: process.cwd() })
-
- expect(document).toBe(parseResult.value)
-
- const output = IdentityPrinter.print(document)
expect(output).toBe(input)
})
- test("returns rewritten AST for inspection", async () => {
- const document = await expectTransform(
+ test("returns rewritten string", async () => {
+ const result = await expectTransform(
``,
``
)
- expect(document.type).toBe("AST_DOCUMENT_NODE")
+ expect(result).toBeDefined()
})
})
@@ -589,4 +583,76 @@ describe("tailwind-class-sorter", () => {
await expectNoTransform(``)
})
})
+
+ describe("Action View Tag Helper class attributes", () => {
+ describe("tag.div", () => {
+ test("sorts classes in tag.div with block (double quotes)", async () => {
+ await expectTransform(
+ `<%= tag.div class: "px-4 bg-blue-500 text-white" do %><% end %>`,
+ `<%= tag.div class: "bg-blue-500 px-4 text-white" do %><% end %>`
+ )
+ })
+
+ test("sorts classes in tag.div with block (single quotes)", async () => {
+ await expectTransform(
+ `<%= tag.div class: 'px-4 bg-blue-500 text-white' do %><% end %>`,
+ `<%= tag.div class: 'bg-blue-500 px-4 text-white' do %><% end %>`
+ )
+ })
+
+ test("sorts classes in inline tag.div (no block)", async () => {
+ await expectTransform(
+ `<%= tag.div class: "px-4 bg-blue-500 text-white" %>`,
+ `<%= tag.div class: "bg-blue-500 px-4 text-white" %>`
+ )
+ })
+
+ test("does not change already sorted classes", async () => {
+ await expectNoTransform(
+ `<%= tag.div class: "bg-blue-500 px-4 text-white" do %><% end %>`
+ )
+ })
+
+ test("does not sort dynamic class values (RubyLiteralNode)", async () => {
+ await expectNoTransform(
+ `<%= tag.div class: dynamic_classes do %><% end %>`
+ )
+ })
+
+ test("does not sort class_names() calls", async () => {
+ await expectNoTransform(
+ `<%= tag.div class: class_names("px-4 bg-blue-500") do %><% end %>`
+ )
+ })
+
+ test("sorts multiple attributes, only touches class", async () => {
+ await expectTransform(
+ `<%= tag.div id: "main", class: "px-4 bg-blue-500" do %><% end %>`,
+ `<%= tag.div id: "main", class: "bg-blue-500 px-4" do %><% end %>`
+ )
+ })
+ })
+
+ describe("content_tag", () => {
+ test("sorts classes in content_tag with block", async () => {
+ await expectTransform(
+ `<%= content_tag :div, class: "px-4 bg-blue-500 text-white" do %><% end %>`,
+ `<%= content_tag :div, class: "bg-blue-500 px-4 text-white" do %><% end %>`
+ )
+ })
+
+ test("sorts classes in inline content_tag (no content argument)", async () => {
+ await expectTransform(
+ `<%= content_tag :div, class: "px-4 bg-blue-500" %>`,
+ `<%= content_tag :div, class: "bg-blue-500 px-4" %>`
+ )
+ })
+
+ test("does not sort dynamic class values", async () => {
+ await expectNoTransform(
+ `<%= content_tag :div, class: some_classes do %><% end %>`
+ )
+ })
+ })
+ })
})
diff --git a/javascript/packages/rewriter/test/helpers/rewriter-test-helper.ts b/javascript/packages/rewriter/test/helpers/rewriter-test-helper.ts
index d03dce7a7..f58c60a3a 100644
--- a/javascript/packages/rewriter/test/helpers/rewriter-test-helper.ts
+++ b/javascript/packages/rewriter/test/helpers/rewriter-test-helper.ts
@@ -3,28 +3,36 @@ import { beforeAll, expect } from "vitest"
import { Herb } from "@herb-tools/node-wasm"
import { IdentityPrinter } from "@herb-tools/printer"
-import type { ASTRewriter, RewriteContext } from "../../src/index.js"
+import { ASTRewriter } from "../../src/ast-rewriter.js"
+import { StringRewriter } from "../../src/string-rewriter.js"
+
+import type { RewriteContext } from "../../src/index.js"
import type { Node } from "@herb-tools/core"
+import type { ParseOptions } from "@herb-tools/core"
interface RewriterTestOptions {
context?: RewriteContext
+ parseOptions?: Partial
}
interface RewriterTestHelpers {
- expectTransform: (input: string, expected: string, options?: RewriterTestOptions) => Promise
- expectNoTransform: (input: string, options?: RewriterTestOptions) => Promise
+ expectTransform: (input: string, expected: string, options?: RewriterTestOptions) => Promise
+ expectNoTransform: (input: string, options?: RewriterTestOptions) => Promise
}
/**
* Creates a test helper for rewriters that reduces boilerplate in tests.
*
+ * Supports both ASTRewriter (parses input and rewrites the AST) and
+ * StringRewriter (passes the input string directly to the rewriter).
+ *
* @param RewriterClass - The rewriter class to test
* @returns Object with helper functions for testing
*/
export function createRewriterTest(
- RewriterClass: new () => ASTRewriter
+ RewriterClass: new () => ASTRewriter | StringRewriter
): RewriterTestHelpers {
- let rewriter: ASTRewriter
+ let rewriter: ASTRewriter | StringRewriter
beforeAll(async () => {
await Herb.load()
@@ -36,9 +44,18 @@ export function createRewriterTest(
input: string,
expected: string,
options?: RewriterTestOptions
- ): Promise => {
+ ): Promise => {
const context = options?.context ?? { baseDir: process.cwd() }
- const parseResult = Herb.parse(input, { track_whitespace: true })
+
+ if (rewriter instanceof StringRewriter) {
+ const output = rewriter.rewrite(input, context)
+
+ expect(output).toBe(expected)
+
+ return output
+ }
+
+ const parseResult = Herb.parse(input, { track_whitespace: true, ...options?.parseOptions })
if (parseResult.failed) {
throw new Error(
@@ -48,7 +65,7 @@ export function createRewriterTest(
)
}
- const node = rewriter.rewrite(parseResult.value, context)
+ const node = (rewriter as ASTRewriter).rewrite(parseResult.value, context)
const output = IdentityPrinter.print(node)
expect(output).toBe(expected)
@@ -59,7 +76,7 @@ export function createRewriterTest(
const expectNoTransform = async (
input: string,
options?: RewriterTestOptions
- ): Promise => {
+ ): Promise => {
return await expectTransform(input, input, options)
}