@herb-tools/rewriter 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,342 @@
1
+ # Herb Rewriter
2
+
3
+ **Package:** [`@herb-tools/rewriter`](https://www.npmjs.com/package/@herb-tools/rewriter)
4
+
5
+ ---
6
+
7
+ Rewriter system for transforming HTML+ERB AST nodes and formatted strings. Provides base classes and utilities for creating custom rewriters that can modify templates.
8
+
9
+ ## Installation
10
+
11
+ :::code-group
12
+
13
+ ```shell [npm]
14
+ npm add @herb-tools/rewriter
15
+ ```
16
+
17
+ ```shell [pnpm]
18
+ pnpm add @herb-tools/rewriter
19
+ ```
20
+
21
+ ```shell [yarn]
22
+ yarn add @herb-tools/rewriter
23
+ ```
24
+
25
+ ```shell [bun]
26
+ bun add @herb-tools/rewriter
27
+ ```
28
+
29
+ :::
30
+
31
+
32
+ ## Overview
33
+
34
+ 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.
35
+
36
+
37
+ ### Rewriter Types
38
+
39
+ - **`ASTRewriter`**: Transform the parsed AST (e.g., sorting Tailwind classes, restructuring HTML)
40
+ - **`StringRewriter`**: Transform formatted strings (e.g., adding trailing newlines, normalizing whitespace)
41
+
42
+ ## Usage
43
+
44
+ ### Quick Start
45
+
46
+ The rewriter package exposes two main functions for applying rewriters to templates:
47
+
48
+ #### `rewrite()` - Transform AST Nodes
49
+
50
+ Use `rewrite()` when you already have a parsed AST node:
51
+
52
+ ```typescript
53
+ import { Herb } from "@herb-tools/node-wasm"
54
+ import { rewrite } from "@herb-tools/rewriter"
55
+ import { tailwindClassSorter } from "@herb-tools/rewriter/loader"
56
+
57
+ await Herb.load()
58
+
59
+ const template = `<div class="text-red-500 p-4 mt-2"></div>`
60
+ const parseResult = Herb.parse(template, { track_whitespace: true })
61
+
62
+ const sorter = await tailwindClassSorter()
63
+ const { output, node } = rewrite(parseResult.value, [sorter])
64
+ // output: "<div class="mt-2 p-4 text-red-500"></div>"
65
+ // node: The rewritten AST node
66
+ ```
67
+
68
+ #### `rewriteString()` - Transform Template Strings
69
+
70
+ Use `rewriteString()` as a convenience wrapper when working with template strings:
71
+
72
+ ```typescript
73
+ import { Herb } from "@herb-tools/node-wasm"
74
+ import { rewriteString } from "@herb-tools/rewriter"
75
+ import { tailwindClassSorter } from "@herb-tools/rewriter/loader"
76
+
77
+ await Herb.load()
78
+
79
+ const template = `<div class="text-red-500 p-4 mt-2"></div>`
80
+
81
+ const sorter = await tailwindClassSorter()
82
+ const output = rewriteString(Herb, template, [sorter])
83
+ // output: "<div class="mt-2 p-4 text-red-500"></div>"
84
+ ```
85
+
86
+ **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.
87
+
88
+ ## Built-in Rewriters
89
+
90
+ ### Tailwind Class Sorter
91
+
92
+ Automatically sorts Tailwind CSS classes in `class` attributes according to Tailwind's recommended order.
93
+
94
+ **Usage:**
95
+ ```typescript
96
+ import { Herb } from "@herb-tools/node-wasm"
97
+ import { rewriteString } from "@herb-tools/rewriter"
98
+ import { tailwindClassSorter } from "@herb-tools/rewriter/loader"
99
+
100
+ await Herb.load()
101
+
102
+ const template = `<div class="px-4 bg-blue-500 text-white rounded py-2"></div>`
103
+ const sorter = await tailwindClassSorter()
104
+ const output = rewriteString(Herb, template, [sorter])
105
+ // output: "<div class="rounded bg-blue-500 px-4 py-2 text-white"></div>"
106
+ ```
107
+
108
+ **Features:**
109
+ - Sorts classes in `class` attributes
110
+ - Auto-discovers Tailwind configuration from your project
111
+ - Supports both Tailwind v3 and v4
112
+ - Works with ERB expressions inside class attributes
113
+
114
+ **Example transformation:**
115
+
116
+ ```erb
117
+ <!-- Before -->
118
+ <div class="px-4 bg-blue-500 text-white rounded py-2">
119
+ <span class="font-bold text-lg">Hello</span>
120
+ </div>
121
+
122
+ <!-- After -->
123
+ <div class="rounded bg-blue-500 px-4 py-2 text-white">
124
+ <span class="text-lg font-bold">Hello</span>
125
+ </div>
126
+ ```
127
+
128
+ ## Custom Rewriters
129
+
130
+ You can create custom rewriters to transform templates in any way you need.
131
+
132
+ ### Creating an ASTRewriter
133
+
134
+ ASTRewriters receive and modify AST nodes:
135
+
136
+ ```javascript [.herb/rewriters/my-rewriter.mjs]
137
+ import { ASTRewriter } from "@herb-tools/rewriter"
138
+ import { Visitor } from "@herb-tools/core"
139
+
140
+ export default class MyASTRewriter extends ASTRewriter {
141
+ get name() {
142
+ return "my-ast-rewriter"
143
+ }
144
+
145
+ get description() {
146
+ return "Transforms the AST"
147
+ }
148
+
149
+ // Optional: Load configuration or setup
150
+ async initialize(context) {
151
+ // context.baseDir - project root directory
152
+ // context.filePath - current file being processed (optional)
153
+ }
154
+
155
+ // Transform the AST node
156
+ rewrite(node, context) {
157
+ // Use the Visitor pattern to traverse and modify the AST
158
+ const visitor = new MyVisitor()
159
+ visitor.visit(node)
160
+
161
+ // Return the modified node
162
+ return node
163
+ }
164
+ }
165
+
166
+ class MyVisitor extends Visitor {
167
+ visitHTMLElementNode(node) {
168
+ // Modify nodes as needed
169
+ // node.someProperty = "new value"
170
+
171
+ this.visitChildNodes(node)
172
+ }
173
+ }
174
+ ```
175
+
176
+ ### Creating a StringRewriter
177
+
178
+ StringRewriters receive and modify strings:
179
+
180
+ ```javascript [.herb/rewriters/add-newline.mjs]
181
+ import { StringRewriter } from "@herb-tools/rewriter"
182
+
183
+ export default class AddTrailingNewline extends StringRewriter {
184
+ get name() {
185
+ return "add-trailing-newline"
186
+ }
187
+
188
+ get description() {
189
+ return "Ensures file ends with a newline"
190
+ }
191
+
192
+ async initialize(context) {
193
+ // Optional setup
194
+ }
195
+
196
+ rewrite(content, context) {
197
+ return content.endsWith("\n") ? content : content + "\n"
198
+ }
199
+ }
200
+ ```
201
+
202
+ ### Using Custom Rewriters
203
+
204
+ By default, rewriters are auto-discovered from: `.herb/rewriters/**/*.mjs`
205
+
206
+ ::: info File Extension
207
+ Custom rewriters must use the `.mjs` extension to avoid Node.js module type warnings. The `.mjs` extension explicitly marks files as ES modules.
208
+ :::
209
+
210
+ #### Configuring in `.herb.yml`
211
+
212
+ Reference custom rewriters by their name in your configuration:
213
+
214
+ ```yaml [.herb.yml]
215
+ formatter:
216
+ enabled: true
217
+ rewriter:
218
+ pre:
219
+ - tailwind-class-sorter # Built-in rewriter
220
+ - my-ast-rewriter # Custom rewriter
221
+ post:
222
+ - add-trailing-newline # Custom rewriter
223
+ ```
224
+
225
+ When custom rewriters are loaded, the formatter will display them:
226
+
227
+ ```
228
+ Loaded 2 custom rewriters:
229
+ • my-ast-rewriter (.herb/rewriters/my-ast-rewriter.mjs)
230
+ • add-trailing-newline (.herb/rewriters/add-newline.mjs)
231
+
232
+ Using 2 pre-format rewriters:
233
+ • tailwind-class-sorter (built-in)
234
+ • my-ast-rewriter (.herb/rewriters/my-ast-rewriter.mjs)
235
+
236
+ Using 1 post-format rewriter:
237
+ • add-trailing-newline (.herb/rewriters/add-newline.mjs)
238
+ ```
239
+
240
+ ::: warning Rewriter Name Clashes
241
+ 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.
242
+ :::
243
+
244
+ ::: tip Hot Reload
245
+ Custom rewriters are automatically reloaded when changed in editors with the Herb Language Server. No need to restart your editor!
246
+ :::
247
+
248
+ ## API Reference
249
+
250
+ ### Functions
251
+
252
+ #### `rewrite()`
253
+
254
+ Transform an AST node using the provided rewriters.
255
+
256
+ ```typescript
257
+ function rewrite<T extends Node>(
258
+ node: T,
259
+ rewriters: Rewriter[],
260
+ options?: RewriteOptions
261
+ ): RewriteResult
262
+ ```
263
+
264
+ **Parameters:**
265
+ - `node`: The AST node to transform
266
+ - `rewriters`: Array of rewriter instances to apply (must be already initialized)
267
+ - `options`: Optional configuration
268
+ - `baseDir`: Base directory for resolving config files (defaults to `process.cwd()`)
269
+ - `filePath`: Optional file path for context
270
+
271
+ **Returns:** Object with:
272
+ - `output`: The rewritten template string
273
+ - `node`: The transformed AST node (preserves input type)
274
+
275
+ #### `rewriteString()`
276
+
277
+ Convenience wrapper around `rewrite()` that parses the template string first and returns just the output string.
278
+
279
+ ```typescript
280
+ function rewriteString(
281
+ herb: HerbBackend,
282
+ template: string,
283
+ rewriters: Rewriter[],
284
+ options?: RewriteOptions
285
+ ): string
286
+ ```
287
+
288
+ **Parameters:**
289
+ - `herb`: The Herb backend instance for parsing
290
+ - `template`: The HTML+ERB template string to rewrite
291
+ - `rewriters`: Array of rewriter instances to apply (must be already initialized)
292
+ - `options`: Optional configuration (same as `rewrite()`)
293
+
294
+ **Returns:** The rewritten template string
295
+
296
+ ### Base Classes
297
+
298
+ #### `ASTRewriter`
299
+
300
+ Base class for rewriters that transform AST nodes:
301
+
302
+ ```typescript
303
+ import { ASTRewriter } from "@herb-tools/rewriter"
304
+ import type { Node, RewriteContext } from "@herb-tools/rewriter"
305
+
306
+ class MyRewriter extends ASTRewriter {
307
+ abstract get name(): string
308
+ abstract get description(): string
309
+
310
+ async initialize(context: RewriteContext): Promise<void> {
311
+ // Optional initialization
312
+ }
313
+
314
+ abstract rewrite<T extends Node>(node: T, context: RewriteContext): T
315
+ }
316
+ ```
317
+
318
+ #### `StringRewriter`
319
+
320
+ Base class for rewriters that transform strings:
321
+
322
+ ```typescript
323
+ import { StringRewriter } from "@herb-tools/rewriter"
324
+ import type { RewriteContext } from "@herb-tools/rewriter"
325
+
326
+ class MyRewriter extends StringRewriter {
327
+ abstract get name(): string
328
+ abstract get description(): string
329
+
330
+ async initialize(context: RewriteContext): Promise<void> {
331
+ // Optional initialization
332
+ }
333
+
334
+ abstract rewrite(content: string, context: RewriteContext): string
335
+ }
336
+ ```
337
+
338
+ ## See Also
339
+
340
+ - [Formatter Documentation](/projects/formatter) - Using rewriters with the formatter
341
+ - [Core Documentation](/projects/core) - AST node types and visitor pattern
342
+ - [Config Documentation](/projects/config) - Configuring rewriters in `.herb.yml`