miki-template 1.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/.github/workflows/ci.yml +54 -0
- package/AGENT.md +71 -0
- package/API_REFERENCE.md +314 -0
- package/CHANGELOG.md +97 -0
- package/CODE_OF_CONDUCT.md +14 -0
- package/CONTRIBUTING.md +27 -0
- package/README.md +304 -0
- package/ROADMAP.md +40 -0
- package/benchmarks/report.json +17 -0
- package/benchmarks/run.js +49 -0
- package/benchmarks/templates/large.dtpl +7 -0
- package/benchmarks/templates/medium.dtpl +3 -0
- package/benchmarks/templates/small.dtpl +7 -0
- package/context/component.md +109 -0
- package/context/prd.md +131 -0
- package/context/project-structure.md +33 -0
- package/docs/README.md +18 -0
- package/docs/advanced_usage.md +71 -0
- package/docs/api.md +102 -0
- package/docs/filters.md +540 -0
- package/docs/installation.md +106 -0
- package/docs/overview.md +57 -0
- package/docs/partialdef.md +41 -0
- package/docs/security.md +27 -0
- package/docs/tags.md +610 -0
- package/docs/usage.md +599 -0
- package/eslint.config.mjs +34 -0
- package/miki-template-1.2.0.vsix +0 -0
- package/miki-template-extension/LICENSE +21 -0
- package/miki-template-extension/README.md +82 -0
- package/miki-template-extension/icon.png +0 -0
- package/miki-template-extension/icon.svg +10 -0
- package/miki-template-extension/package.json +46 -0
- package/miki-template-extension/snippets/miki-template.json +177 -0
- package/miki-template-extension/syntaxes/language-configuration.json +26 -0
- package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +146 -0
- package/package.json +31 -0
- package/snippets/miki-template.json +177 -0
- package/src/asyncRender.js +21 -0
- package/src/cache.js +41 -0
- package/src/context.js +122 -0
- package/src/context_processors.js +41 -0
- package/src/esm.mjs +72 -0
- package/src/filters.js +527 -0
- package/src/i18n.js +171 -0
- package/src/index.js +454 -0
- package/src/lexer.js +92 -0
- package/src/libraries.js +240 -0
- package/src/parser.js +250 -0
- package/src/security.js +51 -0
- package/src/tags/control.js +591 -0
- package/src/tags/helpers.js +27 -0
- package/src/tags/i18n.js +230 -0
- package/src/tags/inheritance.js +216 -0
- package/src/tags/registry.js +18 -0
- package/src/tags/util.js +322 -0
- package/src/types.d.ts +107 -0
- package/syntaxes/language-configuration.json +26 -0
- package/syntaxes/miki-template.tmLanguage.json +146 -0
- package/tests/asyncRender.test.js +17 -0
- package/tests/base.html +6 -0
- package/tests/child.html +3 -0
- package/tests/context_processors.test.js +13 -0
- package/tests/esm.test.mjs +26 -0
- package/tests/filters.test.js +99 -0
- package/tests/include_security.test.js +9 -0
- package/tests/lexer.test.js +45 -0
- package/tests/parser.test.js +55 -0
- package/tests/partial.html +1 -0
- package/tests/partialdef.test.js +40 -0
- package/tests/production_checks.js +57 -0
- package/tests/security.test.js +28 -0
- package/tests/tags.test.js +203 -0
package/docs/filters.md
ADDED
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
# Filters Reference
|
|
2
|
+
|
|
3
|
+
`miki-template` includes a comprehensive set of built-in filters matching Django's filter library. Filters are applied to variables using the pipe `|` character, and can be chained: `{{ value|filter1|filter2:"arg" }}`.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Filter Chaining
|
|
8
|
+
|
|
9
|
+
Filters can be chained — the output of each filter becomes the input of the next:
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
{{ name|lower|capfirst }}
|
|
13
|
+
<!-- "miki" → "miki" → "Miki" -->
|
|
14
|
+
|
|
15
|
+
{{ user.bio|striptags|truncatewords:20 }}
|
|
16
|
+
<!-- Strip HTML tags, then truncate to 20 words -->
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Text Filters
|
|
22
|
+
|
|
23
|
+
### `upper`
|
|
24
|
+
Converts a string to UPPERCASE.
|
|
25
|
+
```html
|
|
26
|
+
{{ "hello"|upper }} → "HELLO"
|
|
27
|
+
{{ name|upper }} → "JOHN"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### `lower`
|
|
31
|
+
Converts a string to lowercase.
|
|
32
|
+
```html
|
|
33
|
+
{{ "HELLO"|lower }} → "hello"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### `title`
|
|
37
|
+
Converts to Title Case (first letter of each word capitalized).
|
|
38
|
+
```html
|
|
39
|
+
{{ "hello world"|title }} → "Hello World"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### `capfirst`
|
|
43
|
+
Capitalizes only the first character.
|
|
44
|
+
```html
|
|
45
|
+
{{ "hello"|capfirst }} → "Hello"
|
|
46
|
+
{{ "h"|capfirst }} → "H"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `slugify`
|
|
50
|
+
Converts to a URL-safe slug by lowercasing, removing accents, and replacing spaces with hyphens.
|
|
51
|
+
```html
|
|
52
|
+
{{ "Hello World!"|slugify }} → "hello-world"
|
|
53
|
+
{{ "Café con Leche"|slugify }} → "cafe-con-leche"
|
|
54
|
+
{{ " Multiple Spaces "|slugify }} → "multiple-spaces"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `wordcount`
|
|
58
|
+
Returns the number of words (whitespace-separated tokens).
|
|
59
|
+
```html
|
|
60
|
+
{{ "one two three"|wordcount }} → 3
|
|
61
|
+
{{ " "|wordcount }} → 0
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### `striptags`
|
|
65
|
+
Removes all HTML/XML tags from the string.
|
|
66
|
+
```html
|
|
67
|
+
{{ "<p>Hello <b>World</b></p>"|striptags }} → "Hello World"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `truncatewords:N`
|
|
71
|
+
Truncates the string to approximately N words, appending `...`.
|
|
72
|
+
```html
|
|
73
|
+
{{ "one two three four five"|truncatewords:3 }} → "one two three ..."
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `truncatechars:N`
|
|
77
|
+
Truncates to N characters (including the `...` suffix if truncation occurs).
|
|
78
|
+
```html
|
|
79
|
+
{{ "Hello World"|truncatechars:8 }} → "Hello..."
|
|
80
|
+
{{ "Hi"|truncatechars:5 }} → "Hi" (no truncation needed)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `linebreaks`
|
|
84
|
+
Converts newlines into paragraphs (`<p>`) and standalone line breaks into `<br>`.
|
|
85
|
+
```html
|
|
86
|
+
{{ "Line one\n\nLine two\nLine three"|linebreaks }}
|
|
87
|
+
<!-- Output: <p>Line one</p><p>Line two<br>Line three</p> -->
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### `linebreaksbr`
|
|
91
|
+
Converts all newlines to `<br>` tags. Does **not** wrap in `<p>` tags.
|
|
92
|
+
```html
|
|
93
|
+
{{ "Line one\nLine two"|linebreaksbr }}
|
|
94
|
+
<!-- Output: Line one<br>Line two -->
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### `cut:value`
|
|
98
|
+
Removes all occurrences of the specified value from the string.
|
|
99
|
+
```html
|
|
100
|
+
{{ "Hello World"|cut:" " }} → "HelloWorld"
|
|
101
|
+
{{ "a|b|c"|cut:"|" }} → "abc"
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### `addslashes`
|
|
105
|
+
Adds backslashes before single quotes, double quotes, and backslashes (for use in JavaScript strings).
|
|
106
|
+
```html
|
|
107
|
+
{{ 'He said "Hello"|addslashes }} → 'He said \"Hello\"'
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### `removetags:tag1,tag2,...`
|
|
111
|
+
Removes the named HTML tags (and their contents).
|
|
112
|
+
```html
|
|
113
|
+
{{ "<p>Hello</p><b>World</b>"|removetags:"p,b" }} → "HelloWorld"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## HTML / Security Filters
|
|
119
|
+
|
|
120
|
+
### `safe`
|
|
121
|
+
Marks the value as **HTML-safe**, bypassing auto-escaping. Use with caution — never pass unsanitized user input through `|safe`.
|
|
122
|
+
```html
|
|
123
|
+
{{ "<b>Bold</b>"|safe }} → <b>Bold</b> (NOT <b>Bold</b>)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### `escape`
|
|
127
|
+
Explicitly escapes HTML entities. Useful when `autoescape` is `off`.
|
|
128
|
+
```html
|
|
129
|
+
{% autoescape off %}
|
|
130
|
+
{{ user_input|escape }} → <script>alert()</script>
|
|
131
|
+
{% endautoescape %}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `escapejs`
|
|
135
|
+
Escapes characters for safe use inside JavaScript string literals.
|
|
136
|
+
```html
|
|
137
|
+
{{ 'Test "quotes" and \backs'|escapejs }}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## URL / Encoding Filters
|
|
143
|
+
|
|
144
|
+
### `urlencode`
|
|
145
|
+
URL-encodes the string. By default, uses query-string encoding (spaces → `+`).
|
|
146
|
+
```html
|
|
147
|
+
{{ "Hello World"|urlencode }} → "Hello+World"
|
|
148
|
+
{{ "a/b c"|urlencode }} → "a%2Fb+c"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
With `path` modifier, preserves slashes:
|
|
152
|
+
```html
|
|
153
|
+
{{ "images/logo.png"|urlencode }} → "images%2Flogo.png" (standard encoding)
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### `escapeurl` (alias: `urlize`)
|
|
157
|
+
Percent-encodes all special characters in a URL.
|
|
158
|
+
```html
|
|
159
|
+
{{ "https://example.com?q=hello world"|escapeurl }}
|
|
160
|
+
→ "https%3A%2F%2Fexample.com%3Fq%3Dhello%20world"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## String Formatting Filters
|
|
166
|
+
|
|
167
|
+
### `stringformat:"fmt"`
|
|
168
|
+
Formats the value using Python-style format strings (`%s`, `%d`, etc.).
|
|
169
|
+
```html
|
|
170
|
+
{{ 42|stringformat:"d" }} → "42"
|
|
171
|
+
{{ 3.14159|stringformat:"2f" }} → "3.14"
|
|
172
|
+
{{ "x"|stringformat:"s" }} → "x"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### `center:N`
|
|
176
|
+
Centers the string in a field of width N (padding with spaces).
|
|
177
|
+
```html
|
|
178
|
+
{{ "Hi"|center:10 }} → " Hi "
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### `ljust:N` / `rjust:N`
|
|
182
|
+
Left/right-justifies the string in a field of width N.
|
|
183
|
+
```html
|
|
184
|
+
{{ "Hi"|ljust:10 }} → "Hi "
|
|
185
|
+
{{ "Hi"|rjust:10 }} → " Hi"
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## List / Sequence Filters
|
|
191
|
+
|
|
192
|
+
### `length`
|
|
193
|
+
Returns the length of an array, object, string, or any object with a `.length` property.
|
|
194
|
+
```html
|
|
195
|
+
{{ items|length }} → 5 (for arrays)
|
|
196
|
+
{{ "hello"|length }} → 5
|
|
197
|
+
{{ object|length }} → number of keys
|
|
198
|
+
{{ undefined|length }} → 0
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### `length_is:N`
|
|
202
|
+
Returns `true` if the value's length equals N, otherwise `false`.
|
|
203
|
+
```html
|
|
204
|
+
{{ "hello"|length_is:5 }} → true
|
|
205
|
+
{{ items|length_is:3 }} → true/false
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `join:separator`
|
|
209
|
+
Joins an array with the specified separator.
|
|
210
|
+
```html
|
|
211
|
+
{{ tags|join:", " }} → "js, python, rust"
|
|
212
|
+
{{ items|join:" + " }} → "a + b + c"
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### `slice:"start:end"`
|
|
216
|
+
Slices an array or string like Python (`[start:end]`). Supports negative indices.
|
|
217
|
+
```html
|
|
218
|
+
{{ items|slice:"1:3" }} → items[1], items[2]
|
|
219
|
+
{{ items|slice:":2" }} → first 2 items
|
|
220
|
+
{{ items|slice:"1:" }} → items from index 1 onwards
|
|
221
|
+
{{ "hello"|slice:"1:4" }} → "ell"
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### `first`
|
|
225
|
+
Returns the first element of a sequence.
|
|
226
|
+
```html
|
|
227
|
+
{{ items|first }} → first item
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### `last`
|
|
231
|
+
Returns the last element of a sequence.
|
|
232
|
+
```html
|
|
233
|
+
{{ items|last }} → last item
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### `dictsort:"key"`
|
|
237
|
+
Sorts an array of objects by the specified attribute (ascending).
|
|
238
|
+
```html
|
|
239
|
+
{% for item in items|dictsort:"name" %}
|
|
240
|
+
{{ item.name }}
|
|
241
|
+
{% endfor %}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `dictsortreversed:"key"`
|
|
245
|
+
Sorts an array of objects by the specified attribute (descending).
|
|
246
|
+
```html
|
|
247
|
+
{% for item in items|dictsortreversed:"price" %}
|
|
248
|
+
{{ item.name }} - ${{ item.price }}
|
|
249
|
+
{% endfor %}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Default / Fallback Filters
|
|
255
|
+
|
|
256
|
+
### `default:fallback`
|
|
257
|
+
Uses the fallback value if the original value is falsy (`null`, `undefined`, or empty string `""`).
|
|
258
|
+
```html
|
|
259
|
+
{{ user.name|default:"Anonymous" }} → "Anonymous" if name is missing
|
|
260
|
+
{{ ""|default:"empty" }} → "empty"
|
|
261
|
+
{{ 0|default:"zero" }} → "zero"
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### `default_if_none:fallback`
|
|
265
|
+
Uses the fallback value only if the original value is `null` or `undefined` (not empty string).
|
|
266
|
+
```html
|
|
267
|
+
{{ value|default_if_none:"N/A" }} → "N/A" if value === null or value === undefined
|
|
268
|
+
{{ ""|default_if_none:"N/A" }} → "" (empty string is not none)
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### `firstof`
|
|
272
|
+
Returns the first truthy value from the arguments (filter form).
|
|
273
|
+
```html
|
|
274
|
+
{{ ""|firstof:user.name:guest:default }} → user.name or "guest" or "default"
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Date / Time Filters
|
|
280
|
+
|
|
281
|
+
All date filters accept `Date` objects, ISO strings, or timestamps.
|
|
282
|
+
|
|
283
|
+
### `date:"format"`
|
|
284
|
+
Formats a date using Django-style format codes:
|
|
285
|
+
| Code | Meaning | Example |
|
|
286
|
+
|------|---------|---------|
|
|
287
|
+
| `d` | Day with leading zero | `01–31` |
|
|
288
|
+
| `j` | Day without leading zero | `1–31` |
|
|
289
|
+
| `m` | Month with leading zero | `01–12` |
|
|
290
|
+
| `n` | Month without leading zero | `1–12` |
|
|
291
|
+
| `Y` | Full year | `2026` |
|
|
292
|
+
| `y` | 2-digit year | `26` |
|
|
293
|
+
| `H` | 24-hour with leading zero | `00–23` |
|
|
294
|
+
| `i` | Minutes | `00–59` |
|
|
295
|
+
| `s` | Seconds | `00–59` |
|
|
296
|
+
| `F` | Full month name | `January` |
|
|
297
|
+
| `M` | Short month name | `Jan` |
|
|
298
|
+
|
|
299
|
+
```html
|
|
300
|
+
{{ post.published|date:"Y-m-d" }} → "2026-08-31"
|
|
301
|
+
{{ post.published|date:"F j, Y" }} → "August 31, 2026"
|
|
302
|
+
{{ post.published|date:"H:i" }} → "14:30"
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### `time:"format"`
|
|
306
|
+
Same as `date` but only outputs time portion.
|
|
307
|
+
|
|
308
|
+
### `strftime:"format"`
|
|
309
|
+
Uses `date-fns` format patterns (PPpp, yyyy-MM-dd, etc.) for full locale support.
|
|
310
|
+
```html
|
|
311
|
+
{{ now|strftime:"PPpp" }} → "Aug 31, 2026 at 2:30 PM"
|
|
312
|
+
{{ now|strftime:"yyyy-MM-dd" }} → "2026-08-31"
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### `date_format:"format"`
|
|
316
|
+
Alias for `strftime` with additional custom patterns.
|
|
317
|
+
|
|
318
|
+
### `timesince`
|
|
319
|
+
Returns a human-readable "time ago" string (e.g., "4 minutes", "2 hours", "3 days").
|
|
320
|
+
```html
|
|
321
|
+
{{ post.created|timesince }} → "2 hours"
|
|
322
|
+
{{ post.created|timesince:other_date }} → "3 days" (relative to other_date)
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### `timeuntil`
|
|
326
|
+
Returns a human-readable "time until" string.
|
|
327
|
+
```html
|
|
328
|
+
{{ event.date|timeuntil }} → "5 days"
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
## Numeric Filters
|
|
334
|
+
|
|
335
|
+
### `add:N`
|
|
336
|
+
Adds N to the value. Also works for string concatenation.
|
|
337
|
+
```html
|
|
338
|
+
{{ count|add:5 }} → count + 5
|
|
339
|
+
{{ items|add:other }} → number or concatenated
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### `divisibleby:N`
|
|
343
|
+
Returns `true` if the value is divisible by N.
|
|
344
|
+
```html
|
|
345
|
+
{{ 10|divisibleby:5 }} → true
|
|
346
|
+
{{ 7|divisibleby:2 }} → false
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
### `floatformat:N`
|
|
350
|
+
Formats a number to N decimal places. Django default is 1 decimal.
|
|
351
|
+
|
|
352
|
+
| arg | behavior |
|
|
353
|
+
|-----|----------|
|
|
354
|
+
| (none) | 1 decimal (`3.4`) |
|
|
355
|
+
| `0` | 0 decimals (`3`) |
|
|
356
|
+
| `1` | 1 decimal (`3.4`) |
|
|
357
|
+
| `2` | 2 decimals (`3.40`) |
|
|
358
|
+
| `-1` | all decimals, trimmed |
|
|
359
|
+
|
|
360
|
+
```html
|
|
361
|
+
{{ 3.14159|floatformat }} → "3.1"
|
|
362
|
+
{{ 3.14159|floatformat:2 }} → "3.14"
|
|
363
|
+
{{ 3.000|floatformat:0 }} → "3"
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Miscellaneous Filters
|
|
369
|
+
|
|
370
|
+
### `yesno:"yes,no,maybe"`
|
|
371
|
+
Maps truthy / falsy / null values to custom strings.
|
|
372
|
+
```html
|
|
373
|
+
{{ flag|yesno:"yes,no,maybe" }} → "yes" if true, "no" if false, "maybe" if null
|
|
374
|
+
{{ active|yesno:"Active,Inactive" }} → "Active" or "Inactive"
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### `pluralize:"s,plural"`
|
|
378
|
+
Returns the singular or plural suffix based on the value. Supports custom suffixes.
|
|
379
|
+
```html
|
|
380
|
+
{{ items|length }} {{ items|pluralize }} item{{ items|pluralize }}
|
|
381
|
+
<!-- 1 item | 5 items -->
|
|
382
|
+
|
|
383
|
+
{{ count|pluralize:"y,ies" }} → "1 candy" | "2 candies"
|
|
384
|
+
```
|
|
385
|
+
|
|
386
|
+
### `filesizeformat`
|
|
387
|
+
Formats a byte count as a human-readable file size.
|
|
388
|
+
```html
|
|
389
|
+
{{ 1024|filesizeformat }} → "1.0 KB"
|
|
390
|
+
{{ 1048576|filesizeformat }} → "1.0 MB"
|
|
391
|
+
{{ 0|filesizeformat }} → "0 bytes"
|
|
392
|
+
{{ 1536|filesizeformat }} → "1.5 KB"
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
---
|
|
396
|
+
|
|
397
|
+
## Custom Filters
|
|
398
|
+
|
|
399
|
+
Register custom filters with `registerFilter`:
|
|
400
|
+
|
|
401
|
+
```javascript
|
|
402
|
+
const { registerFilter } = require('miki-template');
|
|
403
|
+
|
|
404
|
+
// Simple filter
|
|
405
|
+
registerFilter('reverse', (val) => String(val).split('').reverse().join(''));
|
|
406
|
+
|
|
407
|
+
// Filter with argument
|
|
408
|
+
registerFilter('truncate', (val, length) => {
|
|
409
|
+
const str = String(val);
|
|
410
|
+
if (str.length <= length) return str;
|
|
411
|
+
return str.slice(0, length) + '...';
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
// Chaining works automatically:
|
|
415
|
+
// {{ name|reverse|truncate:5 }}
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
---
|
|
419
|
+
|
|
420
|
+
## Filter Argument Types
|
|
421
|
+
|
|
422
|
+
Filters accept the following argument types:
|
|
423
|
+
|
|
424
|
+
| Syntax | Type | Example |
|
|
425
|
+
|--------|------|---------|
|
|
426
|
+
| Unquoted | Variable lookup | `{{ value|filter:count }}` |
|
|
427
|
+
| Double-quoted | String literal | `{{ value|filter:"hello" }}` |
|
|
428
|
+
| Single-quoted | String literal | `{{ value|filter:'world' }}` |
|
|
429
|
+
| Number | Integer literal | `{{ value|truncatewords:10 }}` |
|
|
430
|
+
|
|
431
|
+
```html
|
|
432
|
+
{{ user.name|default:"Guest" }} <!-- String default -->
|
|
433
|
+
{{ items|slice:"1:3" }} <!-- Slice notation -->
|
|
434
|
+
{{ price|floatformat:2 }} <!-- Decimal places -->
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
---
|
|
438
|
+
|
|
439
|
+
## i18n Filters
|
|
440
|
+
|
|
441
|
+
### `trans:"fallback"`
|
|
442
|
+
Translate a string using the i18n registry. Falls back to the original value if no translation is found.
|
|
443
|
+
|
|
444
|
+
```html
|
|
445
|
+
{{ "Hello, World!"|trans }}
|
|
446
|
+
{{ greeting|trans:"Hello, %s!" }}
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
---
|
|
450
|
+
|
|
451
|
+
## Regroup Filter
|
|
452
|
+
|
|
453
|
+
### `regroup:"key"`
|
|
454
|
+
Group an array of objects by a common attribute. Returns an array of `{ grouper, list }` objects.
|
|
455
|
+
|
|
456
|
+
```html
|
|
457
|
+
{% for group in items|regroup:"category" %}
|
|
458
|
+
<h3>{{ group.grouper }}</h3>
|
|
459
|
+
{% for item in group.list %}
|
|
460
|
+
<p>{{ item.name }}</p>
|
|
461
|
+
{% endfor %}
|
|
462
|
+
{% endfor %}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## String Formatting Filters
|
|
468
|
+
|
|
469
|
+
### `stringformat:"fmt"`
|
|
470
|
+
Format a value using Python-style format strings (`%s`, `%d`, `%.2f`, `%x`, etc.).
|
|
471
|
+
|
|
472
|
+
```html
|
|
473
|
+
{{ 42|stringformat:"d" }} → "42"
|
|
474
|
+
{{ 3.14159|stringformat:".2f" }} → "3.14"
|
|
475
|
+
{{ "hello"|stringformat:"s" }} → "hello"
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
---
|
|
479
|
+
|
|
480
|
+
## URL / Encoding Filters
|
|
481
|
+
|
|
482
|
+
### `urlencode`
|
|
483
|
+
URL-encode a string. Supports `query`, `path`, and `utf-8` modes.
|
|
484
|
+
|
|
485
|
+
```html
|
|
486
|
+
{{ "Hello World"|urlencode }} → "Hello+World"
|
|
487
|
+
{{ "a/b c"|urlencode }} → "a%2Fb+c"
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
### `escapeuri`
|
|
491
|
+
Percent-encode a URI.
|
|
492
|
+
|
|
493
|
+
```html
|
|
494
|
+
{{ "http://example.com/path"|escapeuri }}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Text Filters
|
|
500
|
+
|
|
501
|
+
### `cut:value`
|
|
502
|
+
Remove all occurrences of a substring.
|
|
503
|
+
|
|
504
|
+
```html
|
|
505
|
+
{{ "hello hello"|cut:" " }} → "hellohello"
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
### `addslashes`
|
|
509
|
+
Add backslashes before quotes and backslashes.
|
|
510
|
+
|
|
511
|
+
```html
|
|
512
|
+
{{ 'He said "Hi"|addslashes }} → He said \"Hi\"
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### `removetags:tag1,tag2,...`
|
|
516
|
+
Remove named HTML tags and their contents.
|
|
517
|
+
|
|
518
|
+
```html
|
|
519
|
+
{{ "<p>Hello</p><b>World</b>"|removetags:"p,b" }} → "HelloWorld"
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
### `length_is:N`
|
|
523
|
+
Return `true` if the value's length equals N.
|
|
524
|
+
|
|
525
|
+
```html
|
|
526
|
+
{{ "hello"|length_is:5 }} → true
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## Date / Time Filters
|
|
532
|
+
|
|
533
|
+
### `strftime:"format"`
|
|
534
|
+
Format a Date using `date-fns` format strings. More powerful than the built-in `date` filter.
|
|
535
|
+
|
|
536
|
+
```html
|
|
537
|
+
{{ now|strftime:"PPpp" }} → "Aug 31, 2026 at 10:24 PM"
|
|
538
|
+
{{ now|strftime:"yyyy-MM-dd" }} → "2026-08-31"
|
|
539
|
+
{{ now|strftime:"HH:mm:ss" }} → "22:24:56"
|
|
540
|
+
```
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Installation
|
|
2
|
+
|
|
3
|
+
## npm
|
|
4
|
+
```bash
|
|
5
|
+
npm install miki-template
|
|
6
|
+
```
|
|
7
|
+
|
|
8
|
+
## Prerequisites
|
|
9
|
+
- **Node.js** >= 14 (ES6+ support)
|
|
10
|
+
- **npm** (or **yarn**) for package management
|
|
11
|
+
|
|
12
|
+
## Optional dependencies
|
|
13
|
+
- **express** – for server‑side rendering integration (recommended).
|
|
14
|
+
- **eslint** – for linting your project (dev dependency).
|
|
15
|
+
|
|
16
|
+
## Module System Support
|
|
17
|
+
|
|
18
|
+
`miki-template` supports both **CommonJS** (`require`) and **ESM** (`import`).
|
|
19
|
+
|
|
20
|
+
### CommonJS (CJS)
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
const { render, compile, __express, SafeString, markSafe } = require('miki-template');
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### ES Modules (ESM)
|
|
27
|
+
|
|
28
|
+
```js
|
|
29
|
+
// Named imports
|
|
30
|
+
import { render, compile, __express, SafeString, markSafe } from 'miki-template';
|
|
31
|
+
|
|
32
|
+
// Default import (all exports)
|
|
33
|
+
import miki from 'miki-template';
|
|
34
|
+
const result = miki.render('Hello {{ name }}', { name: 'World' });
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
> **Note:** When using ESM in Node.js, either name your files `.mjs` or set `"type": "module"` in your `package.json`.
|
|
38
|
+
|
|
39
|
+
## Publishing to npm
|
|
40
|
+
|
|
41
|
+
This project is configured for automatic npm publishing via GitHub Actions. When you push to `main`, the CI workflow runs tests and, if they pass, publishes the package to npm.
|
|
42
|
+
|
|
43
|
+
### Prerequisites for publishing
|
|
44
|
+
|
|
45
|
+
1. You must have an npm account and be a maintainer of the `miki-template` package on npm.
|
|
46
|
+
2. In your GitHub repository, go to **Settings → Secrets and variables → Actions**.
|
|
47
|
+
3. Add a new repository secret named `NPM_TOKEN` with your npm automation token.
|
|
48
|
+
- Generate it at https://www.npmjs.com/settings/YOUR_USERNAME/tokens
|
|
49
|
+
- Select **Automation** as the token type.
|
|
50
|
+
|
|
51
|
+
The CI workflow will then automatically publish on every push to `main`.
|
|
52
|
+
|
|
53
|
+
### Manual publishing
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
npm version patch # or minor/major
|
|
57
|
+
npm publish --access public
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
### 1. Add the engine to your project
|
|
65
|
+
|
|
66
|
+
**CJS:**
|
|
67
|
+
```js
|
|
68
|
+
const { render, compile } = require('miki-template');
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**ESM:**
|
|
72
|
+
```js
|
|
73
|
+
import { render, compile } from 'miki-template';
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. (Express) Register the view engine
|
|
77
|
+
|
|
78
|
+
**CJS:**
|
|
79
|
+
```js
|
|
80
|
+
const express = require('express');
|
|
81
|
+
const { __express: renderDtpl } = require('miki-template');
|
|
82
|
+
const app = express();
|
|
83
|
+
app.engine('html', renderDtpl);
|
|
84
|
+
app.set('view engine', 'html');
|
|
85
|
+
app.set('views', './views');
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**ESM:**
|
|
89
|
+
```js
|
|
90
|
+
import express from 'express';
|
|
91
|
+
import { __express as renderDtpl } from 'miki-template';
|
|
92
|
+
|
|
93
|
+
const app = express();
|
|
94
|
+
app.engine('html', renderDtpl);
|
|
95
|
+
app.set('view engine', 'html');
|
|
96
|
+
app.set('views', './views');
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### 3. Run the test suite to verify
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
npm test
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
package/docs/overview.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
Welcome to **miki-template** – a production‑ready, Django‑style template engine for Node.js and Express. This documentation mirrors the layout of popular open‑source libraries (e.g., Django, Jinja2, Mustache) and provides a clear, hierarchical guide for developers of all skill levels.
|
|
4
|
+
|
|
5
|
+
- **Project structure** – quick glance at the repository layout.
|
|
6
|
+
- **Feature list** – exhaustive rundown of supported tags, filters, security helpers, and the new `partialdef` system.
|
|
7
|
+
- **Getting started** – installation, basic rendering, and Express integration.
|
|
8
|
+
- **Advanced usage** – inheritance, block rendering, custom tags/filters, and performance tips.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Repository layout
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
📦 miki-template/
|
|
16
|
+
├─ 📁 src/ # Core engine source files
|
|
17
|
+
│ ├─ index.js # Entry point, compile/render APIs
|
|
18
|
+
│ ├─ lexer.js # Tokenizer
|
|
19
|
+
│ ├─ parser.js # AST builder
|
|
20
|
+
│ ├─ context.js # Scope & partial registry
|
|
21
|
+
│ └─ tags/ # Built‑in tag parsers (control, inheritance, util)
|
|
22
|
+
│ ├─ control.js # if, for, with, cycle, partialdef, …
|
|
23
|
+
│ ├─ inheritance.js # extends, block, super
|
|
24
|
+
│ └─ util.js # comment, verbatim, etc.
|
|
25
|
+
├─ 📁 filters/ # Built‑in filter implementations
|
|
26
|
+
├─ 📁 tests/ # Jest‑style test suite
|
|
27
|
+
├─ 📁 docs/ # 📖 Documentation (this folder)
|
|
28
|
+
├─ README.md # Project landing page (high‑level intro)
|
|
29
|
+
├─ AGENT.md # Agent guardrails (internal)
|
|
30
|
+
├─ ROADMAP.md # Future roadmap & milestones
|
|
31
|
+
└─ package.json # npm package definition
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Each module is deliberately **single‑responsibility** and fully typed via JSDoc comments, making it easy to extend.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Where to start
|
|
39
|
+
|
|
40
|
+
- **Installation** – see `docs/installation.md`.
|
|
41
|
+
- **Basic rendering** – see `docs/usage.md`.
|
|
42
|
+
- **Tag reference** – see `docs/tags.md`.
|
|
43
|
+
- **Filter reference** – see `docs/filters.md`.
|
|
44
|
+
- **Partial definitions** – see `docs/partialdef.md`.
|
|
45
|
+
- **Security considerations** – see `docs/security.md`.
|
|
46
|
+
|
|
47
|
+
For API‑level details (e.g., `compile().renderPartial`) check `docs/api.md`.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Contributing
|
|
52
|
+
|
|
53
|
+
We follow the standard open‑source workflow. Details are in `docs/contributing.md`.
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
> **Tip**: All documentation files are located under `c:/Users/Coder Miki/Desktop/miki-template/docs/`.
|