miki-template 1.2.0 → 1.3.3

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.
Files changed (60) hide show
  1. package/.github/release-notes/v1.3.1.md +55 -0
  2. package/CHANGELOG.md +72 -0
  3. package/README.md +43 -26
  4. package/assets/banner.png +0 -0
  5. package/benchmarks/stress.mjs +647 -0
  6. package/dir/base.html +23 -0
  7. package/dir/cmpnt.html +11 -0
  8. package/dir/footer.html +3 -0
  9. package/dir/home.html +80 -0
  10. package/dir/navbar.html +9 -0
  11. package/docs/api.md +20 -3
  12. package/docs/filters.md +301 -133
  13. package/docs/partialdef.md +30 -1
  14. package/docs/tags.md +63 -0
  15. package/docs/usage.md +50 -3
  16. package/eslint.config.mjs +9 -1
  17. package/ex.mjs +33 -0
  18. package/miki-template-extension/.github/workflows/ci.yml +116 -0
  19. package/miki-template-extension/.vscodeignore +7 -0
  20. package/miki-template-extension/CHANGELOG.md +99 -0
  21. package/miki-template-extension/README.md +244 -53
  22. package/miki-template-extension/extension.js +1013 -0
  23. package/miki-template-extension/icon.png +0 -0
  24. package/miki-template-extension/miki-template-1.7.1.vsix +0 -0
  25. package/miki-template-extension/package.json +244 -10
  26. package/miki-template-extension/snippets/miki-template.json +612 -72
  27. package/miki-template-extension/syntaxes/language-configuration.json +101 -13
  28. package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +270 -61
  29. package/miki-template-extension/tests/grammar-tests.json +162 -0
  30. package/miki-template-extension/tests/run-grammar-tests.js +82 -0
  31. package/package.json +7 -4
  32. package/scripts/build-vsix.js +129 -0
  33. package/scripts/build-vsix.ps1 +15 -0
  34. package/src/cache.js +41 -2
  35. package/src/context.js +9 -5
  36. package/src/context_processors.js +9 -2
  37. package/src/esm.mjs +12 -0
  38. package/src/filters.js +472 -24
  39. package/src/index.js +571 -85
  40. package/src/lexer.js +76 -54
  41. package/src/libraries.js +134 -3
  42. package/src/parser.js +22 -2
  43. package/src/security.js +4 -2
  44. package/src/tags/control.js +150 -21
  45. package/src/tags/extra.js +154 -0
  46. package/src/tags/i18n.js +49 -23
  47. package/src/tags/inheritance.js +142 -23
  48. package/src/tags/util.js +102 -24
  49. package/tests/esm.test.mjs +37 -2
  50. package/tests/filters.test.js +155 -0
  51. package/tests/integration/README.md +32 -0
  52. package/tests/integration/features.test.cjs +1681 -0
  53. package/tests/integration/features.test.mjs +1697 -0
  54. package/tests/integration/templates/base.miki +6 -0
  55. package/tests/integration/templates/child.miki +6 -0
  56. package/tests/integration/templates/index.html +17 -0
  57. package/tests/parser.test.js +5 -3
  58. package/tests/partialdef.test.js +40 -1
  59. package/tests/tags.test.js +30 -0
  60. package/miki-template-1.2.0.vsix +0 -0
package/docs/filters.md CHANGED
@@ -61,12 +61,6 @@ Returns the number of words (whitespace-separated tokens).
61
61
  {{ " "|wordcount }} → 0
62
62
  ```
63
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
64
  ### `truncatewords:N`
71
65
  Truncates the string to approximately N words, appending `...`.
72
66
  ```html
@@ -80,6 +74,12 @@ Truncates to N characters (including the `...` suffix if truncation occurs).
80
74
  {{ "Hi"|truncatechars:5 }} → "Hi" (no truncation needed)
81
75
  ```
82
76
 
77
+ ### `truncatechars_html:N`
78
+ Like `truncatechars` but respects HTML tags — tags are preserved in full and only visible text counts toward the limit.
79
+ ```html
80
+ {{ "<p>Hello world</p>"|truncatechars_html:10 }} → "<p>Hello worl...</p>"
81
+ ```
82
+
83
83
  ### `linebreaks`
84
84
  Converts newlines into paragraphs (`<p>`) and standalone line breaks into `<br>`.
85
85
  ```html
@@ -94,6 +94,13 @@ Converts all newlines to `<br>` tags. Does **not** wrap in `<p>` tags.
94
94
  <!-- Output: Line one<br>Line two -->
95
95
  ```
96
96
 
97
+ ### `urlize`
98
+ Automatically converts URLs in text into clickable `<a>` links.
99
+ ```html
100
+ {{ "visit https://example.com for more"|urlize }}
101
+ <!-- Output: visit <a href="https://example.com">https://example.com</a> for more -->
102
+ ```
103
+
97
104
  ### `cut:value`
98
105
  Removes all occurrences of the specified value from the string.
99
106
  ```html
@@ -102,9 +109,9 @@ Removes all occurrences of the specified value from the string.
102
109
  ```
103
110
 
104
111
  ### `addslashes`
105
- Adds backslashes before single quotes, double quotes, and backslashes (for use in JavaScript strings).
112
+ Adds backslashes before single quotes, double quotes, and backslashes.
106
113
  ```html
107
- {{ 'He said "Hello"|addslashes }} → 'He said \"Hello\"'
114
+ {{ 'He said "Hello"'|addslashes }} → 'He said \"Hello\"'
108
115
  ```
109
116
 
110
117
  ### `removetags:tag1,tag2,...`
@@ -113,6 +120,33 @@ Removes the named HTML tags (and their contents).
113
120
  {{ "<p>Hello</p><b>World</b>"|removetags:"p,b" }} → "HelloWorld"
114
121
  ```
115
122
 
123
+ ### `reverse`
124
+ Reverses a string or array.
125
+ ```html
126
+ {{ "hello"|reverse }} → "olleh"
127
+ {{ items|reverse }} → [3, 2, 1]
128
+ ```
129
+
130
+ ### `split:separator`
131
+ Splits a string into an array. Default separator is a single space.
132
+ ```html
133
+ {{ "a,b,c"|split:"," }} → ["a", "b", "c"]
134
+ {{ "hello world"|split }} → ["hello", "world"]
135
+ ```
136
+
137
+ ### `replace:old,new`
138
+ Replaces occurrences of `old` with `new` in the string.
139
+ ```html
140
+ {{ "hello world"|replace:"world,Earth" }} → "hello Earth"
141
+ ```
142
+
143
+ ### `length_is:N`
144
+ Returns `true` if the value's length equals N, otherwise `false`.
145
+ ```html
146
+ {{ "hello"|length_is:5 }} → true
147
+ {{ items|length_is:3 }} → true/false
148
+ ```
149
+
116
150
  ---
117
151
 
118
152
  ## HTML / Security Filters
@@ -131,12 +165,6 @@ Explicitly escapes HTML entities. Useful when `autoescape` is `off`.
131
165
  {% endautoescape %}
132
166
  ```
133
167
 
134
- ### `escapejs`
135
- Escapes characters for safe use inside JavaScript string literals.
136
- ```html
137
- {{ 'Test "quotes" and \backs'|escapejs }}
138
- ```
139
-
140
168
  ---
141
169
 
142
170
  ## URL / Encoding Filters
@@ -148,16 +176,23 @@ URL-encodes the string. By default, uses query-string encoding (spaces → `+`).
148
176
  {{ "a/b c"|urlencode }} → "a%2Fb+c"
149
177
  ```
150
178
 
151
- With `path` modifier, preserves slashes:
179
+ ### `escapeuri`
180
+ Percent-encodes all special characters in a URI.
152
181
  ```html
153
- {{ "images/logo.png"|urlencode }} → "images%2Flogo.png" (standard encoding)
182
+ {{ "https://example.com?q=hello world"|escapeuri }}
183
+ → "https%3A%2F%2Fexample.com%3Fq%3Dhello%20world"
154
184
  ```
155
185
 
156
- ### `escapeurl` (alias: `urlize`)
157
- Percent-encodes all special characters in a URL.
186
+ ### `base64_encode`
187
+ Encodes a string to Base64.
158
188
  ```html
159
- {{ "https://example.com?q=hello world"|escapeurl }}
160
- → "https%3A%2F%2Fexample.com%3Fq%3Dhello%20world"
189
+ {{ "hello"|base64_encode }} → "aGVsbG8="
190
+ ```
191
+
192
+ ### `base64_decode`
193
+ Decodes a Base64 string. Returns the original value if decoding fails or produces invalid UTF-8.
194
+ ```html
195
+ {{ "aGVsbG8="|base64_decode }} → "hello"
161
196
  ```
162
197
 
163
198
  ---
@@ -168,21 +203,15 @@ Percent-encodes all special characters in a URL.
168
203
  Formats the value using Python-style format strings (`%s`, `%d`, etc.).
169
204
  ```html
170
205
  {{ 42|stringformat:"d" }} → "42"
171
- {{ 3.14159|stringformat:"2f" }} → "3.14"
206
+ {{ 3.14159|stringformat:".2f" }} → "3.14"
172
207
  {{ "x"|stringformat:"s" }} → "x"
173
208
  ```
174
209
 
175
- ### `center:N`
176
- Centers the string in a field of width N (padding with spaces).
210
+ ### `json`
211
+ Safely serializes a value to JSON, marked safe for use inside `<script>` blocks.
177
212
  ```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"
213
+ {{ data|json }}
214
+ <!-- Output: {"users":[{"name":"Alice"}]} (not HTML-escaped) -->
186
215
  ```
187
216
 
188
217
  ---
@@ -198,13 +227,6 @@ Returns the length of an array, object, string, or any object with a `.length` p
198
227
  {{ undefined|length }} → 0
199
228
  ```
200
229
 
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
230
  ### `join:separator`
209
231
  Joins an array with the specified separator.
210
232
  ```html
@@ -221,16 +243,22 @@ Slices an array or string like Python (`[start:end]`). Supports negative indices
221
243
  {{ "hello"|slice:"1:4" }} → "ell"
222
244
  ```
223
245
 
224
- ### `first`
225
- Returns the first element of a sequence.
246
+ ### `sort`
247
+ Sorts an array in ascending order. Strings use locale-aware comparison.
226
248
  ```html
227
- {{ items|first }} → first item
249
+ {{ [3, 1, 2]|sort }} → [1, 2, 3]
228
250
  ```
229
251
 
230
- ### `last`
231
- Returns the last element of a sequence.
252
+ ### `unique`
253
+ Removes duplicate values from an array.
232
254
  ```html
233
- {{ items|last }} → last item
255
+ {{ [1, 2, 2, 3]|unique }} → [1, 2, 3]
256
+ ```
257
+
258
+ ### `random`
259
+ Returns a random element from an array.
260
+ ```html
261
+ {{ ["a", "b", "c"]|random }} → "b" (random)
234
262
  ```
235
263
 
236
264
  ### `dictsort:"key"`
@@ -269,7 +297,7 @@ Uses the fallback value only if the original value is `null` or `undefined` (not
269
297
  ```
270
298
 
271
299
  ### `firstof`
272
- Returns the first truthy value from the arguments (filter form).
300
+ Returns the first truthy value from the arguments.
273
301
  ```html
274
302
  {{ ""|firstof:user.name:guest:default }} → user.name or "guest" or "default"
275
303
  ```
@@ -294,7 +322,7 @@ Formats a date using Django-style format codes:
294
322
  | `i` | Minutes | `00–59` |
295
323
  | `s` | Seconds | `00–59` |
296
324
  | `F` | Full month name | `January` |
297
- | `M` | Short month name | `Jan` |
325
+ | `M` | Short day/month name | `Jan` |
298
326
 
299
327
  ```html
300
328
  {{ post.published|date:"Y-m-d" }} → "2026-08-31"
@@ -328,15 +356,68 @@ Returns a human-readable "time until" string.
328
356
  {{ event.date|timeuntil }} → "5 days"
329
357
  ```
330
358
 
359
+ ### `time_diff:date`
360
+ Returns the absolute time difference between two dates as a human-readable string.
361
+ ```html
362
+ {{ event.date|time_diff }} → "3 days"
363
+ {{ event.date|time_diff:now }} → difference from now
364
+ ```
365
+
366
+ ### `ago`
367
+ Returns a human-readable relative time string like "2 days ago", "1 hour ago", etc.
368
+ ```html
369
+ {{ comment.created|ago }} → "2 days ago"
370
+ {{ comment.created|ago }} → "just now" (if less than 1 minute ago)
371
+ ```
372
+
373
+ ### `until`
374
+ Returns a human-readable string representing time until the given date.
375
+ ```html
376
+ {{ event.date|until }} → "3 days"
377
+ {{ event.date|until }} → "now" (if less than 1 minute away)
378
+ ```
379
+
331
380
  ---
332
381
 
333
382
  ## Numeric Filters
334
383
 
335
384
  ### `add:N`
336
- Adds N to the value. Also works for string concatenation.
385
+ Adds N to the value. Also works for string concatenation and array concatenation.
337
386
  ```html
338
387
  {{ count|add:5 }} → count + 5
339
- {{ items|add:other }} → number or concatenated
388
+ {{ items|add:other }} → concatenated array or string
389
+ ```
390
+
391
+ ### `sub:N`
392
+ Subtracts N from the value.
393
+ ```html
394
+ {{ 10|sub:3 }} → 7
395
+ ```
396
+
397
+ ### `mult:N`
398
+ Multiplies the value by N.
399
+ ```html
400
+ {{ 4|mult:5 }} → 20
401
+ ```
402
+
403
+ ### `square`
404
+ Returns the square of the value.
405
+ ```html
406
+ {{ 6|square }} → 36
407
+ ```
408
+
409
+ ### `sqrt`
410
+ Returns the square root of the value. Returns 0 for negative numbers.
411
+ ```html
412
+ {{ 9|sqrt }} → 3
413
+ {{ -1|sqrt }} → 0
414
+ ```
415
+
416
+ ### `mod:N`
417
+ Returns the modulo (remainder) of the value divided by N. Returns 0 if N is 0.
418
+ ```html
419
+ {{ 10|mod:3 }} → 1
420
+ {{ 10|mod:0 }} → 0
340
421
  ```
341
422
 
342
423
  ### `divisibleby:N`
@@ -363,6 +444,61 @@ Formats a number to N decimal places. Django default is 1 decimal.
363
444
  {{ 3.000|floatformat:0 }} → "3"
364
445
  ```
365
446
 
447
+ ### `abs`
448
+ Returns the absolute value of a number.
449
+ ```html
450
+ {{ -5|abs }} → 5
451
+ {{ 5|abs }} → 5
452
+ ```
453
+
454
+ ### `round:N`
455
+ Rounds to N decimal places (default: 0 decimals).
456
+ ```html
457
+ {{ 3.14159|round:2 }} → 3.14
458
+ {{ 3.5|round }} → 4
459
+ {{ 3.4|round }} → 3
460
+ ```
461
+
462
+ ### `floor`
463
+ Rounds down to the nearest integer.
464
+ ```html
465
+ {{ 3.7|floor }} → 3
466
+ {{ -3.7|floor }} → -4
467
+ ```
468
+
469
+ ### `ceil`
470
+ Rounds up to the nearest integer.
471
+ ```html
472
+ {{ 3.2|ceil }} → 4
473
+ {{ -3.2|ceil }} → -3
474
+ ```
475
+
476
+ ### `min:N` / `min:array`
477
+ Returns the minimum of the value and N, or the minimum element in an array.
478
+ ```html
479
+ {{ 5|min:2 }} → 2
480
+ {{ [5, 2, 8]|min }} → 2
481
+ ```
482
+
483
+ ### `max:N` / `max:array`
484
+ Returns the maximum of the value and N, or the maximum element in an array.
485
+ ```html
486
+ {{ 5|max:2 }} → 5
487
+ {{ [5, 2, 8]|max }} → 8
488
+ ```
489
+
490
+ ### `sum`
491
+ Returns the sum of all elements in an array.
492
+ ```html
493
+ {{ [1, 2, 3, 4]|sum }} → 10
494
+ ```
495
+
496
+ ### `average`
497
+ Returns the arithmetic mean of an array. Returns 0 for empty arrays.
498
+ ```html
499
+ {{ [1, 2, 3, 4]|average }} → 2.5
500
+ ```
501
+
366
502
  ---
367
503
 
368
504
  ## Miscellaneous Filters
@@ -394,44 +530,111 @@ Formats a byte count as a human-readable file size.
394
530
 
395
531
  ---
396
532
 
397
- ## Custom Filters
533
+ ## Data Formatting Filters
398
534
 
399
- Register custom filters with `registerFilter`:
535
+ ### `currency:symbol`
536
+ Formats a number as currency with thousands separators and 2 decimal places. Default symbol is `$`.
537
+ ```html
538
+ {{ 1234.5|currency }} → "$1,234.50"
539
+ {{ 1234.5|currency:"€" }} → "€1,234.50"
540
+ ```
400
541
 
401
- ```javascript
402
- const { registerFilter } = require('miki-template');
542
+ ### `phone_number`
543
+ Formats a 10-digit US phone number as `(123) 456-7890`. Handles 11-digit numbers with leading `1` as `+1 (123) 456-7890`.
544
+ ```html
545
+ {{ "1234567890"|phone_number }} → "(123) 456-7890"
546
+ {{ "11234567890"|phone_number }} → "+1 (123) 456-7890"
547
+ ```
403
548
 
404
- // Simple filter
405
- registerFilter('reverse', (val) => String(val).split('').reverse().join(''));
549
+ ### `email`
550
+ Wraps an email address in a `mailto:` link.
551
+ ```html
552
+ {{ "user@example.com"|email }} → "mailto:user@example.com"
553
+ ```
406
554
 
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
- });
555
+ ### `url`
556
+ Ensures a URL has a protocol prefix. Prepends `https://` if missing.
557
+ ```html
558
+ {{ "example.com"|url }} → "https://example.com"
559
+ {{ "https://example.com"|url }} → "https://example.com"
560
+ ```
413
561
 
414
- // Chaining works automatically:
415
- // {{ name|reverse|truncate:5 }}
562
+ ### `mask:char`
563
+ Masks all but the last 4 characters of a string. Default mask character is `*`.
564
+ ```html
565
+ {{ "1234567890"|mask }} → "******7890"
566
+ {{ "1234567890"|mask:"#" }} → "######7890"
567
+ ```
568
+
569
+ ### `whatsapp_link:message`
570
+ Generates a WhatsApp link (`https://wa.me/NUMBER`) with an optional pre-filled message.
571
+ ```html
572
+ {{ "1234567890"|whatsapp_link }} → "https://wa.me/1234567890"
573
+ {{ "1234567890"|whatsapp_link:"Hello" }} → "https://wa.me/1234567890?text=Hello"
574
+ ```
575
+
576
+ ### `credit_card`
577
+ Formats a credit card number with dashes every 4 digits.
578
+ ```html
579
+ {{ "4111111111111111"|credit_card }} → "4111-1111-1111-1111"
580
+ ```
581
+
582
+ ### `ssn`
583
+ Formats a 9-digit Social Security Number as `XXX-XX-XXXX`.
584
+ ```html
585
+ {{ "123456789"|ssn }} → "123-45-6789"
586
+ ```
587
+
588
+ ### `ip_address`
589
+ Formats a 10 or 12 digit string as a dotted IP address.
590
+ ```html
591
+ {{ "192168011001"|ip_address }} → "192.168.11.001"
592
+ ```
593
+
594
+ ### `uuid`
595
+ Generates a random UUID v4 string.
596
+ ```html
597
+ {{ ""|uuid }} → "9787a126-cb81-4825-b63b-73345a51a1c1"
416
598
  ```
417
599
 
418
600
  ---
419
601
 
420
- ## Filter Argument Types
602
+ ## Humanize Filters (Built-in Library)
421
603
 
422
- Filters accept the following argument types:
604
+ These filters are part of the `humanize` library, activated by default or via `{% load humanize %}`.
423
605
 
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 }}` |
606
+ ### `intcomma`
607
+ Adds commas to thousands places.
608
+ ```html
609
+ {{ 1234567|intcomma }} → "1,234,567"
610
+ ```
430
611
 
612
+ ### `intword`
613
+ Converts large numbers to human-friendly strings.
431
614
  ```html
432
- {{ user.name|default:"Guest" }} <!-- String default -->
433
- {{ items|slice:"1:3" }} <!-- Slice notation -->
434
- {{ price|floatformat:2 }} <!-- Decimal places -->
615
+ {{ 1234567|intword }} → "1.2 million"
616
+ {{ 1000|intword }} → "1.0 thousand"
617
+ ```
618
+
619
+ ### `apnumber`
620
+ Converts numbers to their word equivalent (0–19).
621
+ ```html
622
+ {{ 3|apnumber }} → "three"
623
+ ```
624
+
625
+ ### `ordinal`
626
+ Returns the ordinal suffix for a number.
627
+ ```html
628
+ {{ 1|ordinal }} → "1st"
629
+ {{ 2|ordinal }} → "2nd"
630
+ {{ 11|ordinal }} → "11th"
631
+ ```
632
+
633
+ ### `naturalday`
634
+ Returns "today", "yesterday", "tomorrow", or the date string for other dates.
635
+ ```html
636
+ {{ today|naturalday }} → "today"
637
+ {{ yesterday|naturalday }} → "yesterday"
435
638
  ```
436
639
 
437
640
  ---
@@ -464,77 +667,42 @@ Group an array of objects by a common attribute. Returns an array of `{ grouper,
464
667
 
465
668
  ---
466
669
 
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
- ---
670
+ ## Custom Filters
479
671
 
480
- ## URL / Encoding Filters
672
+ Register custom filters with `registerFilter`:
481
673
 
482
- ### `urlencode`
483
- URL-encode a string. Supports `query`, `path`, and `utf-8` modes.
674
+ ```javascript
675
+ const { registerFilter } = require('miki-template');
484
676
 
485
- ```html
486
- {{ "Hello World"|urlencode }} → "Hello+World"
487
- {{ "a/b c"|urlencode }} → "a%2Fb+c"
488
- ```
677
+ // Simple filter
678
+ registerFilter('reverse', (val) => String(val).split('').reverse().join(''));
489
679
 
490
- ### `escapeuri`
491
- Percent-encode a URI.
680
+ // Filter with argument
681
+ registerFilter('truncate', (val, length) => {
682
+ const str = String(val);
683
+ if (str.length <= length) return str;
684
+ return str.slice(0, length) + '...';
685
+ });
492
686
 
493
- ```html
494
- {{ "http://example.com/path"|escapeuri }}
687
+ // Chaining works automatically:
688
+ // {{ name|reverse|truncate:5 }}
495
689
  ```
496
690
 
497
691
  ---
498
692
 
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
- ---
693
+ ## Filter Argument Types
530
694
 
531
- ## Date / Time Filters
695
+ Filters accept the following argument types:
532
696
 
533
- ### `strftime:"format"`
534
- Format a Date using `date-fns` format strings. More powerful than the built-in `date` filter.
697
+ | Syntax | Type | Example |
698
+ |--------|------|---------|
699
+ | Unquoted | Variable lookup | `{{ value|filter:count }}` |
700
+ | Double-quoted | String literal | `{{ value|filter:"hello" }}` |
701
+ | Single-quoted | String literal | `{{ value|filter:'world' }}` |
702
+ | Number | Integer literal | `{{ value|truncatewords:10 }}` |
535
703
 
536
704
  ```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"
705
+ {{ user.name|default:"Guest" }} <!-- String default -->
706
+ {{ items|slice:"1:3" }} <!-- Slice notation -->
707
+ {{ price|floatformat:2 }} <!-- Decimal places -->
540
708
  ```
@@ -1,6 +1,6 @@
1
1
  # Partial Definition (`partialdef`)
2
2
 
3
- `partialdef` is the cornerstone feature that brings Django‑style **named template fragments** to Node.js. It allows you to define a reusable block once and render it multiple times, optionally **inline** for immediate output.
3
+ `partialdef` is the cornerstone feature that brings Django‑style **named template fragments** to Node.js. It allows you to define a reusable block once and render it multiple times, optionally **inline** for immediate output. Combined with the `setupExpress` helper, partials can be served as standalone HTTP responses for HTMX-style UIs.
4
4
 
5
5
  ## Syntax
6
6
  ```html
@@ -17,6 +17,12 @@
17
17
  ```
18
18
  The engine looks up the definition in the current rendering **Context** (`context.partialDefs`) and injects the rendered output.
19
19
 
20
+ ## Including a Partial From Another File
21
+ Use the `file#partial` syntax to include just a single named partial:
22
+ ```html
23
+ {% include "home.html#card" with title="Hi" %}
24
+ ```
25
+
20
26
  ## API Usage
21
27
  ```js
22
28
  const tpl = `{% partialdef api %}API {{ data }}{% endpartialdef %}`;
@@ -24,11 +30,34 @@ const compiled = compile(tpl);
24
30
  const out = compiled.renderPartial('api', { data: 123 }); // "API 123"
25
31
  ```
26
32
 
33
+ `renderPartialFromSource` and `renderPartialFromFile` are also exported at the top level:
34
+ ```js
35
+ const miki = require('miki-template');
36
+ miki.renderPartialFromFile('views/home.html', 'card', { user: req.user });
37
+ miki.renderPartialFromSource(src, 'card', { user: req.user }, { views: 'views' });
38
+ ```
39
+
40
+ ## Serving a Partial Over HTTP (HTMX)
41
+ With `miki.setupExpress(app, { extension: 'html', views: './views' })`, the same `res.render(...)` call you use for full pages also serves a single partial by appending `#partialName` to the view name:
42
+
43
+ ```javascript
44
+ app.get('/partials/:name', (req, res) =>
45
+ res.render(`home#${req.params.name}`, { user: req.user })
46
+ );
47
+ ```
48
+
49
+ The same effect can be obtained via the lighter `expressPartialRenderer()` middleware:
50
+ ```javascript
51
+ app.use(miki.expressPartialRenderer());
52
+ app.get('/card', (req, res) => res.renderPartial('home#card', { user: req.user }));
53
+ ```
54
+
27
55
  ## Features
28
56
  - **Full tag parity** – conditionals (`if`), loops (`for`), variable scoping (`with`) work inside a `partialdef`.
29
57
  - **Nested partials** – you can define a partial inside another; inner definitions are registered first and can be used by the outer.
30
58
  - **Scope isolation** – each rendering of a partial receives its own scope, mirroring Django’s behavior.
31
59
  - **Inline rendering** – render inline without an extra `{% partial %}` tag (`{% partialdef foo inline %}…{% endpartialdef %}`).
60
+ - **`with` arguments** – bind extra context values when rendering: `{% partial card with title="Hello" description="World" %}`.
32
61
  - **Performance** – partials are compiled once per template; subsequent renders reuse the compiled AST.
33
62
 
34
63
  ## Common Pitfalls