@prostojs/rewrite 0.1.1 → 0.2.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 CHANGED
@@ -1,123 +1,193 @@
1
1
  # @prostojs/rewrite
2
2
 
3
- Easy and light templates renderer with HTML (XML) support based on [@prostojs/parser](https://www.npmjs.com/package/@prostojs/parser).
3
+ A lightweight template engine for **code generation and scaffolding**. Embed conditional blocks, loops, and expressions directly in real source files — templates stay syntactically valid and readable. Supports both source code (text mode with comment-based directives) and HTML/XML (Vue-like `v-if`, `v-for`, `:attr` bindings).
4
4
 
5
- - Write syntactically valid templates (js, yaml, ...)
6
- - Use vue-like syntax for html-templates
7
- - Mix __html__ and __text__ modes at single template
5
+ - Templates compile to cached JS functions — repeated renders are near-instant
6
+ - Under 27 KB bundled, zero runtime dependencies beyond `@prostojs/parser`
7
+ - Auto-detects text vs HTML mode by file extension; switch modes mid-file with directives
8
+ - Point at a directory to scaffold entire projects with a single call
8
9
 
9
10
  ## Install
10
11
 
11
- npm:
12
+ ```bash
13
+ npm install @prostojs/rewrite
14
+ # or
15
+ pnpm add @prostojs/rewrite
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```js
21
+ import { ProstoRewrite } from '@prostojs/rewrite'
22
+
23
+ const rw = new ProstoRewrite()
24
+
25
+ // Text mode — rewrite a source code template
26
+ const result = rw.textRewriter.rewrite(
27
+ `Hello {{ name }}! You have {{ count }} items.`,
28
+ { name: 'World', count: 3 }
29
+ )
30
+ // => "Hello World! You have 3 items."
31
+
32
+ // HTML mode — rewrite markup
33
+ const html = rw.htmlRewriter.rewrite(
34
+ `<ul><li v-for="item of items">{{ item }}</li></ul>`,
35
+ { items: ['apple', 'banana', 'cherry'] }
36
+ )
37
+ // => "<ul><li>apple</li>\n<li>banana</li>\n<li>cherry</li></ul>"
38
+ ```
39
+
40
+ ## Text Mode
41
+
42
+ Text mode is designed for source code, config files, Dockerfiles, and any non-HTML content. Directives are embedded in line comments, keeping your templates syntactically valid.
43
+
44
+ ### Expressions
45
+
46
+ Use `{{ }}` delimiters (customizable) to interpolate any JavaScript expression:
47
+
48
+ ```
49
+ const greeting = '{{ salutation }} {{ name }}!'
50
+ const sum = {{ a + b }}
51
+ ```
12
52
 
13
- `npm install @prostojs/rewrite`
53
+ Context: `{ salutation: 'Hello', name: 'World', a: 2, b: 3 }`
14
54
 
15
- Via CDN:
55
+ Output:
16
56
  ```
17
- <script src="https://unpkg.com/@prostojs/tree"></script>
18
- <script src="https://unpkg.com/@prostojs/parser"></script>
19
- <script src="https://unpkg.com/@prostojs/rewrite"></script>
57
+ const greeting = 'Hello World!'
58
+ const sum = 5
20
59
  ```
21
60
 
22
- ## Usage
61
+ ### Conditional Blocks (IF / ELSE / ELSEIF)
23
62
 
24
- It's possible to point the `ProstoRewrite` to a file or a directory. It's also possible to rewrite any string using [rewriters](#rewriters) directly.
63
+ ```js
64
+ //=IF (env === 'production')
65
+ const API = 'https://api.prod.com'
66
+ //=ELSEIF (env === 'staging')
67
+ const API = 'https://api.staging.com'
68
+ //=ELSE
69
+ const API = 'http://localhost:3000'
70
+ //=ENDIF
71
+ ```
25
72
 
73
+ Context: `{ env: 'staging' }` produces:
26
74
  ```js
27
- const { ProstoRewrite } = require('@prostojs/rewrite')
28
- // or import module
29
- // import { ProstoRewrite } from '@prostojs/rewrite'
75
+ const API = 'https://api.staging.com'
76
+ ```
30
77
 
31
- const rw = new ProstoRewrite()
32
- main()
33
- async function main() {
34
- const context = { a: 1 } // context object for templates interpolation
35
-
36
- // rewrite a single file
37
- const renderedContent = await rw.rewriteFile({
38
- // required:
39
- input: 'path/to/file/filename.js',
40
- // optional:
41
- output: 'path/to/rewrite/filename.js',
42
- mode: 'auto', // text | html | auto
43
- }, context)
44
-
45
- // rewrite files in directory
46
- await rw.rewriteDir({
47
- // required:
48
- baseDir: 'path/to/files',
49
- // optional:
50
- include: ['*.{js,html}'], // glob pattern to include
51
- exclude: ['*.svg'], // glob pattern to exclude
52
- output: 'output/path',
53
- mode: 'auto', // text | html | auto
54
- onFile: (path, output) => {
55
- console.log('Result for file ' + path + '\n' + output)
56
- }
57
- }, context)
58
- }
78
+ The comment prefix (`//`, `#`, `--`, etc.) is automatically detected from the line. Use whatever comment style fits your language:
79
+
80
+ ```yaml
81
+ #=IF (includeRedis)
82
+ redis:
83
+ image: redis:alpine
84
+ #=ENDIF
85
+ ```
86
+
87
+ **Operation blocks reference:**
88
+
89
+ | Key | Example | Description |
90
+ |---|---|---|
91
+ | `IF` | `//=IF(condition)` | Includes lines below only if `condition` is truthy. |
92
+ | `ELSEIF` | `//=ELSEIF(condition)` | Alternative branch. Must follow `IF` or `ELSEIF`. |
93
+ | `ELSE` | `//=ELSE` | Fallback branch. Must follow `IF` or `ELSEIF`. |
94
+ | `ENDIF` | `//=ENDIF` | Closes the `IF` block chain. |
95
+ | `FOR` | `//=FOR(a of b)` | Iterates with any valid JS loop expression. |
96
+ | `ENDFOR` | `//=ENDFOR` | Closes the `FOR` block. |
97
+
98
+ > Operation blocks can have spaces between words: `ELSE IF`, `END FOR`, etc.
99
+ > All blocks work with `#` and `//` comment prefixes.
100
+
101
+ ### Loops (FOR / ENDFOR)
102
+
103
+ ```js
104
+ //=FOR (const route of routes)
105
+ app.get('{{ route.path }}', {{ route.handler }})
106
+ //=ENDFOR
59
107
  ```
60
108
 
61
- ## Templates
109
+ Context: `{ routes: [{ path: '/api', handler: 'apiHandler' }, { path: '/', handler: 'indexHandler' }] }`
62
110
 
63
- ### TEXT
111
+ Output:
112
+ ```js
113
+ app.get('/api', apiHandler)
114
+ app.get('/', indexHandler)
115
+ ```
116
+
117
+ ### Nesting
118
+
119
+ Blocks nest freely:
120
+
121
+ ```js
122
+ //=IF (features.auth)
123
+ //=FOR (const provider of authProviders)
124
+ //=IF (provider !== 'local')
125
+ import {{ provider }}Strategy from './strategies/{{ provider }}'
126
+ //=ENDIF
127
+ //=ENDFOR
128
+ //=ENDIF
129
+ ```
64
130
 
65
- The text template is good for any text source (js, ts, yaml, ...).
131
+ ### Reveal Lines
66
132
 
67
- To keep the source files syntactically valid all the control flow blocks
68
- must be written in comment `//` or `#`.
133
+ Lines prefixed with `//:` (comment + reveal marker) are **hidden in the template but appear in the output**. This is the inverse of normal lines — useful for generating code that shouldn't be visible in the template source:
69
134
 
70
- Text template consists of:
71
- - operation blocks (`//=IF(...)`),
72
- - directives (`//!@ignore-next-line`),
73
- - reveal comments (`//: const a = 1`),
74
- - string expressions (`conat a = {{ scopedValue }}`).
135
+ ```js
136
+ //=IF (useTypescript)
137
+ //: import type { Config } from './types'
138
+ //=ENDIF
139
+ const config = {}
140
+ ```
75
141
 
76
- **Operation blocks*** **:**
77
- Key | Example | Description
78
- ---|---|---
79
- `IF`|`//=IF(condition)` or `#=IF(condition)`|WIll add the lines below only if the `condition` returns `true`. The `condition` must be a valid javascript and can use `context` vars.
80
- `ELSEIF`**|`//=ELSEIF(condition)` or `#=ELSEIF(condition)`|WIll add the lines below only if the `condition` returns `true`. The `condition` must be a valid javascript and can use `context` vars. Must be used after `IF` or `ELSEIF` operation block.
81
- `ELSE`|`//=ELSE` or `#=ELSE`|WIll add the lines below only if the previous conditions didn't match. Must be used after `IF` or `ELSEIF` operation block.
82
- `ENDIF`**|`//=ENDIF` or `#=ENDIF`|Ends the `IF` blocks series.
83
- `FOR`|`//=FOR(a of b)` or `#=FOR(a of b)`|Will iterate through the loop. The `a of b` part must be a valid javascript loop expression and can use `context` vars.
84
- `ENDFOR`**|`//=ENDFOR` or `#=ENDFOR`|Ends the `FOR` block.
142
+ Context: `{ useTypescript: true }`
85
143
 
86
- *The operation block can have spaces before it and after it, but it must take only one line. All the operation blocks can start with `#` as well as with `//`
144
+ Output:
145
+ ```js
146
+ import type { Config } from './types'
147
+ const config = {}
148
+ ```
87
149
 
88
- **The operation block may have spaces between words e.g. `ELSE IF`, `END FOR`...
150
+ Reveal lines can contain expressions too:
89
151
 
90
- **Directives:**
152
+ ```js
153
+ //: const {{ varName }} = {{ JSON.stringify(defaultValue) }}
154
+ ```
91
155
 
92
- Key|Example|Description
93
- ---|---|---
94
- `ignore-next-line` |`//!@ignore-next-line` or `#!@ignore-next-line`|Instruct the processor to ignore (not interpolate) the next line of the source. The next line will be copied without any change.
95
- `html-mode-on`|`//!@html-mode-on` or `#!@html-mode-on`|Instructs the processor (only if it is mixed rewriter) to start using HTML mode for the futher lines of the source.
96
- `html-mode-off`|`//!@html-mode-off` or `#!@html-mode-off`|Instructs the processor (only if it is mixed rewriter) to stop using HTML mode for the futher lines of the source and return to the text mode.
156
+ ### Directives
97
157
 
98
- **Reveal comments:** the line prefixed with `reveal comment` prefix will be rendered as **uncommented** line. Use this when needed to keep the source file syntatically valid.
158
+ | Directive | Example | Description |
159
+ |---|---|---|
160
+ | `ignore-next-line` | `//!@ignore-next-line` | The next line passes through as-is, without interpolation. |
161
+ | `html-mode-on` | `//!@html-mode-on` | Switch to HTML mode (mixed rewriter only). |
162
+ | `html-mode-off` | `//!@html-mode-off` | Switch back to text mode. |
99
163
 
100
- **String expressions:** vue-like string expressions `{{ scopedValue }}` accepts valid javascript code and must interpolate into a `string` or anything that can be casted to `string`. The expression can access the context vars.
164
+ ```js
165
+ //!@ ignore-next-line
166
+ const template = '{{ this is not interpolated }}'
167
+ const value = '{{ this IS interpolated }}'
168
+ ```
101
169
 
102
- **Example:**
170
+ ### Full Text Mode Example
103
171
 
104
172
  Source:
105
173
  ```js
106
174
  let myVar = 1
107
175
  //=IF (a === b)
108
- //=FOR (const i of items)
176
+ //=FOR (const i of items)
109
177
  //: const item{{ i }} = '{{ i }}' // reveal comment
110
- //=IF (c === d)
178
+ //=IF (c === d)
111
179
  myVar += 2
112
- //=ELSE
180
+ //=ELSE
113
181
  myVar -= 4
114
- //=ENDIF
115
- //=END FOR
182
+ //=ENDIF
183
+ //=END FOR
116
184
  //=END IF
117
185
  const myVar2 = 2
118
186
  ```
119
187
 
120
- Will be rewritten with context = `{ a: 1, b: 1, c: 2, d: 2, items: [1, 2] }`:
188
+ Context: `{ a: 1, b: 1, c: 2, d: 2, items: [1, 2] }`
189
+
190
+ Output:
121
191
  ```js
122
192
  let myVar = 1
123
193
  const item1 = '1' // reveal comment
@@ -127,46 +197,96 @@ myVar += 2
127
197
  const myVar2 = 2
128
198
  ```
129
199
 
130
- ### HTML
200
+ ## HTML Mode
201
+
202
+ HTML mode parses markup structure and supports Vue-inspired directives for conditionals, loops, and dynamic attributes.
203
+
204
+ ### Expressions
205
+
206
+ Interpolation works the same as text mode:
207
+
208
+ ```html
209
+ <h1>{{ title }}</h1>
210
+ <p>Welcome, {{ user.name }}!</p>
211
+ ```
212
+
213
+ ### Conditional Rendering (v-if / v-else-if / v-else)
214
+
215
+ ```html
216
+ <div v-if="user.isAdmin">
217
+ <h2>Admin Panel</h2>
218
+ </div>
219
+ <div v-else-if="user.isEditor">
220
+ <h2>Editor Dashboard</h2>
221
+ </div>
222
+ <div v-else>
223
+ <h2>Welcome, {{ user.name }}</h2>
224
+ </div>
225
+ ```
226
+
227
+ Only the matching block is rendered. The `v-if` / `v-else-if` / `v-else` chain must be on sibling elements.
228
+
229
+ ### Loops (v-for)
230
+
231
+ ```html
232
+ <ul>
233
+ <li v-for="item of items">{{ item }}</li>
234
+ </ul>
235
+ ```
236
+
237
+ Any valid JavaScript loop expression works:
131
238
 
132
- The html template is good for XML-like sources (html, xml, svg, ...).
239
+ ```html
240
+ <tr v-for="let i = 0; i < rows.length; i++">
241
+ <td>{{ rows[i].name }}</td>
242
+ </tr>
243
+ ```
133
244
 
134
- By default html template uses vue-like syntax.
245
+ `v-for` and `v-if` can be combined on the same element:
135
246
 
136
- **It supports:**
137
- - `v-for="..."`: attribute for loop the node. Expression must be a valid javascript loop expression.
138
- - `v-if="..."`: attribute for conditional rendering of the node. Expression must be a valid javascript condition expression.
139
- - `v-else-if="..."`: attribute for conditional rendering of the node. Expression must be a valid javascript condition expression.
140
- - `v-else`: attribute for conditional rendering of the node.
141
- - `:<attr_name>="..."`: expression as attribute value. Expression must be a valid javascript code that returns string/boolean or anything that casts to a string/boolean. If the expression result is type of boolean, the attribute will have no value and will be hidden if the result equals `false`.
142
- - `{{ ... }}`: vue-like string expressions `{{ scopedValue }}` accepts valid javascript code and must interpolate into a `string` or anything that can be casted to `string`. The expression can access the context vars.
247
+ ```html
248
+ <li v-for="item of items" v-if="item.visible">{{ item.name }}</li>
249
+ ```
143
250
 
144
- ### TEXT & HTML
251
+ ### Dynamic Attributes
145
252
 
146
- By default `ProstoRewrite` supports both: text and html modes at the same time. The default mode is controled by the [options](#options).
253
+ Prefix any attribute with `:` to evaluate it as a JavaScript expression:
147
254
 
148
- If you want to switch to html mode in the middle of the text source you can use the following instruction:
255
+ ```html
256
+ <img :src="baseUrl + '/images/' + image.file" :alt="image.title">
257
+ <a :href="link.url" :target="link.external ? '_blank' : undefined">{{ link.text }}</a>
258
+ ```
149
259
 
150
- `//!@html-mode-on` or `#!@html-mode-on`
260
+ **Smart boolean handling:** if the expression evaluates to `true`, the attribute is rendered without a value (`<input disabled>`). If `false`, the attribute is omitted entirely:
151
261
 
152
- And to switch back use:
262
+ ```html
263
+ <input type="text" :disabled="isLocked" :required="isRequired">
264
+ ```
153
265
 
154
- `//!@html-mode-off` or `#!@html-mode-off`
266
+ Context: `{ isLocked: true, isRequired: false }`
155
267
 
156
- If you want to switch to text mode in the middle of the html source you can use the following instruction:
268
+ Output:
269
+ ```html
270
+ <input type="text" disabled>
271
+ ```
157
272
 
158
- `<!--!@text-mode-on-->`
273
+ ### Switch to Text Mode
159
274
 
160
- And to switch back use:
275
+ Inside an HTML file, switch to text-mode parsing with HTML comment directives:
161
276
 
162
- `<!--!@text-mode-off-->`
277
+ ```html
278
+ <script>
279
+ <!--!@ text-mode-on -->
280
+ //=FOR (const key of Object.keys(config))
281
+ window.{{ key }} = {{ JSON.stringify(config[key]) }}
282
+ //=ENDFOR
283
+ <!--!@ text-mode-off -->
284
+ </script>
285
+ ```
163
286
 
164
- **Examples:**
287
+ And the reverse — switch to HTML inside a text file:
165
288
 
166
- Text template with embedded html processor:
167
289
  ```js
168
- const a = 'test'
169
- //=IF (condition)
170
290
  const html = `
171
291
  //!@ html-mode-on
172
292
  <div v-for='item of items'>
@@ -174,176 +294,174 @@ const html = `
174
294
  </div>
175
295
  //!@ html-mode-off
176
296
  `
177
- //=END IF
178
297
  ```
179
298
 
180
- Html template with embedded text processor:
181
- ```html
182
- <div v-if="condition">
183
- <!--!@ text-mode-on -->
184
- const a = 'b'
185
- #=IF (condition)
186
- console.log(a)
187
- #=ENDIF
188
- <!--!@ text-mode-off -->
189
- </div>
190
- ```
299
+ ## File and Directory Rewriting
191
300
 
192
- ### Options
301
+ The main use case: scaffold entire project templates.
193
302
 
194
- Options object is totally optional. The example below demonstrates the default values.
303
+ ### Rewrite a Single File
195
304
 
196
305
  ```js
197
- const rw = new ProstoRewrite({
198
- defaultMode: 'auto', // text | html | auto
199
- debug: false,
200
- htmlPattern: ['*.{html,xhtml,xml,svg}'],
201
- textPattern: [
202
- '*.{js,jsx,ts,tsx,txt,json,yml,yaml,md,ini}',
203
- 'Dockerfile',
204
- '*config',
205
- '.gitignore',
206
- ],
207
- html: {
208
- exprDelimeters: ['{{', '}}'],
209
- attrExpression: ':',
210
- blockOperation: 'v-',
211
- directive: '!@',
212
- voidTags: [
213
- 'area',
214
- 'base',
215
- 'br',
216
- 'col',
217
- 'command',
218
- 'embed',
219
- 'hr',
220
- 'img',
221
- 'input',
222
- 'keygen',
223
- 'link',
224
- 'meta',
225
- 'param',
226
- 'source',
227
- 'track',
228
- 'wbr',
229
- ],
230
- textTags: ['script', 'style'],
231
- },
232
- text: {
233
- exprDelimeters: ['{{', '}}'],
234
- blockOperation: '=',
235
- revealLine: ':',
236
- directive: '!@',
237
- },
238
- })
306
+ import { ProstoRewrite } from '@prostojs/rewrite'
307
+
308
+ const rw = new ProstoRewrite()
309
+
310
+ // Returns rendered content; optionally writes to output path
311
+ const content = await rw.rewriteFile(
312
+ {
313
+ input: 'path/to/template.js',
314
+ output: 'path/to/output.js', // optional
315
+ mode: 'auto', // optional: 'text' | 'html' | 'auto'
316
+ },
317
+ { name: 'my-app', version: '1.0.0' }
318
+ )
319
+ ```
320
+
321
+ ### Rewrite an Entire Directory
322
+
323
+ ```js
324
+ await rw.rewriteDir(
325
+ {
326
+ baseDir: './template',
327
+ output: './my-new-project',
328
+ include: ['**/*'], // optional glob patterns
329
+ exclude: ['node_modules/**'], // optional glob patterns
330
+ renameFile: (name) => name.replace('__name__', 'my-app'), // optional
331
+ onFile: (path, output) => console.log(`Wrote: ${path}`), // optional callback
332
+ mode: 'auto', // optional
333
+ },
334
+ {
335
+ name: 'my-app',
336
+ description: 'My awesome app',
337
+ useTypescript: true,
338
+ features: ['auth', 'api', 'database'],
339
+ }
340
+ )
239
341
  ```
240
342
 
241
- Option | Type | Description
242
- ---|---|---
243
- defaultMode | `'text'` \| `'html'` \| `'auto'` | Determines the type of template processor used for templates. When `auto` it will use `htmlPattern` and `textPattern` options to decide on which processor to use.
244
- debug | `boolean` | Pushes debug messages to console when `true`.
245
- htmlPattern | `string[]` | Glob pattern that defines files to be processed by `html` processor for `defaultMode` = `'auto'` mode.
246
- textPattern | `string[]` | Glob pattern that defines files to be processed by `text` processor for `defaultMode` = `'auto'` mode.
247
- text | `object` | Options for `text` processor.
248
- text.exprDelimeters | `[string, string]` | Defines the delimeters for string expressions e.g. `{{ n.toLowerCase() }}`. Default: `['{{', '}}']`.
249
- text.blockOperation | `string` | Defines the prefix for block operations (e.g. `for`, `if`, ...). Default: `'='`.
250
- text.revealLine | `string` | Defines the prefix for reveal commented line. Default: `':'`.
251
- text.directive | `string` | Defines the prefix for directives (e.g. `ignore-next-line`, `html-mode-on`, ...). Default: `'!@'`.
252
- html | `object` | Options for `html` processor.
253
- html.exprDelimeters | `[string, string]` | Defines the delimeters for string expressions e.g. `{{ n.toLowerCase() }}`. Default: `['{{', '}}']`.
254
- html.attrExpression | `string` | Defines the prefix for attributes that have to be interpolated. Default: `':'`.
255
- html.blockOperation | `string` | Defines the prefix for block-operations attributes (e.g. `for`, `if`, ...). Default: `'v-'`.
256
- html.directive | `string` | Defines the prefix for directives (e.g. `text-mode-on`, ...). Default: `'!@'`.
257
- html.voidTags | `string[]` | List of void tags (without closing tags and without innerText)
258
- html.textTags | `string[]` | List of text tags (that contain text and no other tags)
343
+ **Mode auto-detection** picks the right parser based on file extension:
344
+ - **HTML mode:** `*.html`, `*.xhtml`, `*.xml`, `*.svg`
345
+ - **Text mode:** `*.js`, `*.ts`, `*.jsx`, `*.tsx`, `*.json`, `*.yml`, `*.yaml`, `*.md`, `*.txt`, `*.ini`, `Dockerfile`, `*config`, `.gitignore`
346
+
347
+ Files that don't match any pattern are copied as-is.
259
348
 
260
349
  ## Rewriters
261
350
 
262
- Instance of `ProstoRewrite` ships 3 versions of rewriters:
263
- - text
264
- - html
265
- - mixed
351
+ `ProstoRewrite` provides three rewriter flavors:
266
352
 
267
353
  ```js
268
354
  const rw = new ProstoRewrite()
269
- // 3 flavors:
270
- const trw = rw.textRewriter // rewriter that can parse only text
271
- const hrw = rw.htmlRewriter // rewriter that can parse only html
272
- const mrw = rw.mixedRewriter // rewriter that can parse both
273
- // source for text rewriter:
274
- const source = '//=IF (a === 1)\nconst a = 1'
275
- const context = { a: 1 }
276
- // each rewriter has the same interface:
277
- trw.genRewriteCode(source) // returns rendered function source code
278
- trw.genRewriteFunction(source) // returns rewrite-function
279
- trw.printAsTree(source) // prints parsed source as a tree
280
- trw.rewrite(source, context) // renders the source with context
281
- ```
282
-
283
- If you're going to use one template file for multiple renders it makes sense to cache its rewrite-function:
284
355
 
285
- ```js
286
- // An abstract example of using cached rewrite-function
287
- const rw = new ProstoRewrite()
288
- const trw = rw.textRewriter
289
- const rewriteFunc = trw.genRewriteFunction(source)
290
- const result1 = rewriteFunc(context1)
291
- const result2 = rewriteFunc(context2)
292
- const result3 = rewriteFunc(context3)
356
+ const trw = rw.textRewriter // text only
357
+ const hrw = rw.htmlRewriter // html only
358
+ const mrw = rw.mixedRewriter // both (supports mode-switching directives)
359
+ // mrw.text — text rewriter with html-mode-on support
360
+ // mrw.html — html rewriter with text-mode-on support
293
361
  ```
294
362
 
295
- Besides those there is one more **String Expression Rewriter** comes as a _bonus_.
363
+ Each rewriter has the same interface:
296
364
 
297
- It is can parse only string expressions and preserves the expression type if its source consists only from an expression. When the source has some surrounding string(s) it will cast the expression to string type.
298
-
299
- This kind of rewriter may be usefull when working with some configuration files that can have expressions as properties values.
300
-
301
- **Usage example**
302
365
  ```js
303
- import { getStringExpressionRewriter } from '@prostojs/rewrite'
366
+ // Generate the compiled JavaScript source (for inspection/debugging)
367
+ trw.genRewriteCode(source)
304
368
 
305
- const srw = getStringExpressionRewriter()
369
+ // Compile once, execute many times — ideal for repeated renders
370
+ const render = trw.genRewriteFunction(source)
371
+ const out1 = render({ name: 'Alice' })
372
+ const out2 = render({ name: 'Bob' })
306
373
 
307
- // the source has surrounding strings and the expression
308
- // will be casted to string type as well:
309
- console.log(srw.rewrite('before {{ a }} after', { a: 5 }))
310
- // output:
311
- // 'before 5 after'
374
+ // One-shot: parse + compile + execute
375
+ trw.rewrite(source, context)
312
376
 
313
- // the source consists only of a single expression
314
- // therefore its type will be preserved:
315
- console.log(srw.rewrite('{{ a }}', { a: 5 }) === 5 )
316
- // output:
317
- // true
377
+ // Debug: print the parse tree to console
378
+ trw.printAsTree(source)
318
379
  ```
319
380
 
320
- **Configuraton example**
381
+ ### String Expression Rewriter
321
382
 
322
- Let's assume that we want a config that must store some path generation logic. We can not store function as we're supposed to store the config to DB. We can use a string expression like so:
383
+ For simple interpolation without block operations (config values, file paths):
323
384
 
324
385
  ```js
325
386
  import { getStringExpressionRewriter } from '@prostojs/rewrite'
326
387
 
327
388
  const srw = getStringExpressionRewriter()
328
389
 
329
- const config = {
330
- path: 'some/path/{{ key.toLowerCase() }}.{{ type === \'javascript\' ? \'js\' : \'json\' }}'
331
- }
390
+ // Mixed string: always returns string
391
+ srw.rewrite('Hello {{ name }}, age {{ age }}', { name: 'World', age: 25 })
392
+ // => "Hello World, age 25"
332
393
 
333
- const context = {
334
- key: 'TEST',
335
- type: 'javascript',
394
+ // Single expression: preserves the original type
395
+ srw.rewrite('{{ count }}', { count: 42 })
396
+ // => 42 (number, not string)
397
+ ```
398
+
399
+ This is useful for configuration files where property values can contain expressions:
400
+
401
+ ```js
402
+ const config = {
403
+ path: "some/path/{{ key.toLowerCase() }}.{{ type === 'javascript' ? 'js' : 'json' }}",
336
404
  }
337
405
 
338
- // caching the rewrite function for path
339
- // to avoid parsing the path string every time
340
406
  const pathFunc = srw.genRewriteFunction(config.path)
407
+ pathFunc({ key: 'TEST', type: 'javascript' })
408
+ // => "some/path/test.js"
409
+ ```
341
410
 
342
- // rewriting the config path
343
- const thePath = pathFunc(context)
411
+ ## Options
344
412
 
345
- console.log(thePath)
346
- // output:
347
- // 'some/path/test.js'
413
+ All options are optional. The example below shows the default values:
414
+
415
+ ```js
416
+ const rw = new ProstoRewrite({
417
+ defaultMode: 'auto', // 'text' | 'html' | 'auto'
418
+ debug: false,
419
+
420
+ htmlPattern: ['*.{html,xhtml,xml,svg}'],
421
+ textPattern: [
422
+ '*.{js,jsx,ts,tsx,txt,json,yml,yaml,md,ini}',
423
+ 'Dockerfile',
424
+ '*config',
425
+ '.gitignore',
426
+ ],
427
+
428
+ text: {
429
+ exprDelimiters: ['{{', '}}'],
430
+ blockOperation: '=',
431
+ revealLine: ':',
432
+ directive: '!@',
433
+ },
434
+
435
+ html: {
436
+ exprDelimiters: ['{{', '}}'],
437
+ blockOperation: 'v-',
438
+ attrExpression: ':',
439
+ directive: '!@',
440
+ voidTags: ['area', 'base', 'br', 'col', 'command', 'embed', 'hr',
441
+ 'img', 'input', 'keygen', 'link', 'meta', 'param',
442
+ 'source', 'track', 'wbr'],
443
+ textTags: ['script', 'style'],
444
+ },
445
+ })
446
+ ```
348
447
 
349
- ```
448
+ | Option | Type | Description |
449
+ |---|---|---|
450
+ | `defaultMode` | `'text'` \| `'html'` \| `'auto'` | Template processor selection. `auto` uses file patterns to decide. |
451
+ | `debug` | `boolean` | Print debug parse trees to console. |
452
+ | `htmlPattern` | `string[]` | Glob patterns for files processed by HTML parser. |
453
+ | `textPattern` | `string[]` | Glob patterns for files processed by text parser. |
454
+ | `text.exprDelimiters` | `[string, string]` | Expression delimiters. Default: `['{{', '}}']` |
455
+ | `text.blockOperation` | `string` | Prefix for block operations (`IF`, `FOR`, ...). Default: `'='` |
456
+ | `text.revealLine` | `string` | Prefix for reveal lines. Default: `':'` |
457
+ | `text.directive` | `string` | Prefix for directives (`ignore-next-line`, ...). Default: `'!@'` |
458
+ | `html.exprDelimiters` | `[string, string]` | Expression delimiters. Default: `['{{', '}}']` |
459
+ | `html.blockOperation` | `string` | Prefix for block attributes (`if`, `for`, ...). Default: `'v-'` |
460
+ | `html.attrExpression` | `string` | Prefix for expression attributes. Default: `':'` |
461
+ | `html.directive` | `string` | Prefix for directives (`text-mode-on`, ...). Default: `'!@'` |
462
+ | `html.voidTags` | `string[]` | Self-closing HTML tags. |
463
+ | `html.textTags` | `string[]` | Tags with text-only content (no child tag parsing). |
464
+
465
+ ## License
466
+
467
+ MIT