Skip to content

Herb Rewriter

Package: @herb-tools/rewriter


Rewriter system for transforming HTML+ERB AST nodes and formatted strings. Provides base classes and utilities for creating custom rewriters that can modify templates.

Installation

shell
npm add @herb-tools/rewriter
shell
pnpm add @herb-tools/rewriter
shell
yarn add @herb-tools/rewriter
shell
bun add @herb-tools/rewriter

Overview

The rewriter package provides a plugin architecture for transforming HTML+ERB templates. Rewriters can be used to transform templates before formatting, implement linter autofixes, or perform any custom AST or string transformations.

Rewriter Types

  • ASTRewriter: Transform the parsed AST (e.g., sorting Tailwind classes, restructuring HTML)
  • StringRewriter: Transform formatted strings (e.g., adding trailing newlines, normalizing whitespace)

Usage

Quick Start

The rewriter package exposes two main functions for applying rewriters to templates:

rewrite() - Transform AST Nodes

Use rewrite() when you already have a parsed AST node:

typescript
import { Herb } from "@herb-tools/node-wasm"
import { rewrite } from "@herb-tools/rewriter"
import { tailwindClassSorter } from "@herb-tools/rewriter/loader"

await Herb.load()

const template = `<div class="text-red-500 p-4 mt-2"></div>`
const parseResult = Herb.parse(template, { track_whitespace: true })

const sorter = await tailwindClassSorter()
const { output, node } = rewrite(parseResult.value, [sorter])
// output: "<div class="mt-2 p-4 text-red-500"></div>"
// node: The rewritten AST node

rewriteString() - Transform Template Strings

Use rewriteString() as a convenience wrapper when working with template strings:

typescript
import { Herb } from "@herb-tools/node-wasm"
import { rewriteString } from "@herb-tools/rewriter"
import { tailwindClassSorter } from "@herb-tools/rewriter/loader"

await Herb.load()

const template = `<div class="text-red-500 p-4 mt-2"></div>`

const sorter = await tailwindClassSorter()
const output = rewriteString(Herb, template, [sorter])
// output: "<div class="mt-2 p-4 text-red-500"></div>"

Note: rewrite() returns both the rewritten string (output) and the transformed AST (node), which allows for partial rewrites and further processing. rewriteString() is a convenience wrapper that returns just the string.

Built-in Rewriters

Tailwind Class Sorter

Automatically sorts Tailwind CSS classes in class attributes according to Tailwind's recommended order.

Usage:

typescript
import { Herb } from "@herb-tools/node-wasm"
import { rewriteString } from "@herb-tools/rewriter"
import { tailwindClassSorter } from "@herb-tools/rewriter/loader"

await Herb.load()

const template = `<div class="px-4 bg-blue-500 text-white rounded py-2"></div>`
const sorter = await tailwindClassSorter()
const output = rewriteString(Herb, template, [sorter])
// output: "<div class="rounded bg-blue-500 px-4 py-2 text-white"></div>"

Features:

  • Sorts classes in class attributes
  • Auto-discovers Tailwind configuration from your project
  • Supports both Tailwind v3 and v4
  • Works with ERB expressions inside class attributes

Example transformation:

erb
<!-- Before -->
<div class="px-4 bg-blue-500 text-white rounded py-2">
  <span class="font-bold text-lg">Hello</span>
</div>

<!-- After -->
<div class="rounded bg-blue-500 px-4 py-2 text-white">
  <span class="text-lg font-bold">Hello</span>
</div>

Custom Rewriters

You can create custom rewriters to transform templates in any way you need.

Creating an ASTRewriter

ASTRewriters receive and modify AST nodes:

.herb/rewriters/my-rewriter.mjs
javascript
import { ASTRewriter } from "@herb-tools/rewriter"
import { Visitor } from "@herb-tools/core"

export default class MyASTRewriter extends ASTRewriter {
  get name() {
    return "my-ast-rewriter"
  }

  get description() {
    return "Transforms the AST"
  }

  // Optional: Load configuration or setup
  async initialize(context) {
    // context.baseDir - project root directory
    // context.filePath - current file being processed (optional)
  }

  // Transform the AST node
  rewrite(node, context) {
    // Use the Visitor pattern to traverse and modify the AST
    const visitor = new MyVisitor()
    visitor.visit(node)

    // Return the modified node
    return node
  }
}

class MyVisitor extends Visitor {
  visitHTMLElementNode(node) {
    // Modify nodes as needed
    // node.someProperty = "new value"

    this.visitChildNodes(node)
  }
}

Creating a StringRewriter

StringRewriters receive and modify strings:

.herb/rewriters/add-newline.mjs
javascript
import { StringRewriter } from "@herb-tools/rewriter"

export default class AddTrailingNewline extends StringRewriter {
  get name() {
    return "add-trailing-newline"
  }

  get description() {
    return "Ensures file ends with a newline"
  }

  async initialize(context) {
    // Optional setup
  }

  rewrite(content, context) {
    return content.endsWith("\n") ? content : content + "\n"
  }
}

Using Custom Rewriters

By default, rewriters are auto-discovered from: .herb/rewriters/**/*.mjs

File Extension

Custom rewriters must use the .mjs extension to avoid Node.js module type warnings. The .mjs extension explicitly marks files as ES modules.

Configuring in .herb.yml

Reference custom rewriters by their name in your configuration:

.herb.yml
yaml
formatter:
  enabled: true
  rewriter:
    pre:
      - tailwind-class-sorter  # Built-in rewriter
      - my-ast-rewriter        # Custom rewriter
    post:
      - add-trailing-newline   # Custom rewriter

When custom rewriters are loaded, the formatter will display them:

Loaded 2 custom rewriters:
  • my-ast-rewriter (.herb/rewriters/my-ast-rewriter.mjs)
  • add-trailing-newline (.herb/rewriters/add-newline.mjs)

Using 2 pre-format rewriters:
  • tailwind-class-sorter (built-in)
  • my-ast-rewriter (.herb/rewriters/my-ast-rewriter.mjs)

Using 1 post-format rewriter:
  • add-trailing-newline (.herb/rewriters/add-newline.mjs)

Rewriter Name Clashes

If a custom rewriter has the same name as a built-in rewriter or another custom rewriter, you'll see a warning. The custom rewriter will override the built-in one.

Hot Reload

Custom rewriters are automatically reloaded when changed in editors with the Herb Language Server. No need to restart your editor!

API Reference

Functions

rewrite()

Transform an AST node using the provided rewriters.

typescript
function rewrite<T extends Node>(
  node: T,
  rewriters: Rewriter[],
  options?: RewriteOptions
): RewriteResult

Parameters:

  • node: The AST node to transform
  • rewriters: Array of rewriter instances to apply (must be already initialized)
  • options: Optional configuration
    • baseDir: Base directory for resolving config files (defaults to process.cwd())
    • filePath: Optional file path for context

Returns: Object with:

  • output: The rewritten template string
  • node: The transformed AST node (preserves input type)

rewriteString()

Convenience wrapper around rewrite() that parses the template string first and returns just the output string.

typescript
function rewriteString(
  herb: HerbBackend,
  template: string,
  rewriters: Rewriter[],
  options?: RewriteOptions
): string

Parameters:

  • herb: The Herb backend instance for parsing
  • template: The HTML+ERB template string to rewrite
  • rewriters: Array of rewriter instances to apply (must be already initialized)
  • options: Optional configuration (same as rewrite())

Returns: The rewritten template string

Base Classes

ASTRewriter

Base class for rewriters that transform AST nodes:

typescript
import { ASTRewriter } from "@herb-tools/rewriter"
import type { Node, RewriteContext } from "@herb-tools/rewriter"

class MyRewriter extends ASTRewriter {
  abstract get name(): string
  abstract get description(): string

  async initialize(context: RewriteContext): Promise<void> {
    // Optional initialization
  }

  abstract rewrite<T extends Node>(node: T, context: RewriteContext): T
}

StringRewriter

Base class for rewriters that transform strings:

typescript
import { StringRewriter } from "@herb-tools/rewriter"
import type { RewriteContext } from "@herb-tools/rewriter"

class MyRewriter extends StringRewriter {
  abstract get name(): string
  abstract get description(): string

  async initialize(context: RewriteContext): Promise<void> {
    // Optional initialization
  }

  abstract rewrite(content: string, context: RewriteContext): string
}

See Also

Released under the MIT License.