miki-template 2.0.1 → 2.2.2

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 (71) hide show
  1. package/.github/workflows/ci.yml +13 -37
  2. package/.github/workflows/docs.yml +105 -0
  3. package/.github/workflows/npm-publish-github-packages.yml +36 -0
  4. package/README.md +69 -14
  5. package/assets/logo.png +0 -0
  6. package/benchmarks/ejs-results.json +4 -4
  7. package/benchmarks/handlebars-results.json +6 -6
  8. package/benchmarks/miki-results.json +4 -4
  9. package/benchmarks/pug-results.json +4 -4
  10. package/benchmarks/stress.mjs +1 -1
  11. package/docs/api/async-render.md +85 -0
  12. package/docs/api/cache.md +87 -0
  13. package/docs/api/compile.md +128 -0
  14. package/docs/api/context-processors.md +77 -0
  15. package/docs/api/filters.md +217 -0
  16. package/docs/api/finder.md +94 -0
  17. package/docs/api/helpers.md +53 -0
  18. package/docs/api/i18n.md +157 -0
  19. package/docs/api/index.md +54 -0
  20. package/docs/api/libraries.md +207 -0
  21. package/docs/api/render-partial.md +81 -0
  22. package/docs/api/render.md +92 -0
  23. package/docs/api/security.md +145 -0
  24. package/docs/api/setup-express.md +76 -0
  25. package/docs/api/tags.md +134 -0
  26. package/docs/assets/banner.png +0 -0
  27. package/docs/assets/logo.png +0 -0
  28. package/docs/guide/advanced-usage.md +397 -0
  29. package/docs/guide/async-rendering.md +308 -0
  30. package/docs/guide/context-processors.md +257 -0
  31. package/docs/guide/custom-filters.md +311 -0
  32. package/docs/guide/custom-tags.md +271 -0
  33. package/docs/guide/filters.md +642 -0
  34. package/docs/guide/getting-started.md +102 -0
  35. package/docs/guide/installation.md +95 -0
  36. package/docs/guide/partial-templates.md +367 -0
  37. package/docs/guide/quick-start.md +222 -0
  38. package/docs/guide/security.md +345 -0
  39. package/docs/guide/tags.md +783 -0
  40. package/docs/guide/template-discovery.md +170 -0
  41. package/docs/guide/template-inheritance.md +273 -0
  42. package/docs/guide/what-is-miki-template.md +28 -0
  43. package/docs/guide/why-miki-template.md +75 -0
  44. package/docs/index.md +104 -0
  45. package/docs/integrations/elysia.md +78 -0
  46. package/docs/integrations/express.md +219 -0
  47. package/docs/integrations/fastify.md +77 -0
  48. package/docs/integrations/hono.md +78 -0
  49. package/docs/integrations/index.md +68 -0
  50. package/docs/integrations/koa.md +88 -0
  51. package/docs/integrations/nestjs.md +78 -0
  52. package/docs/integrations/tsed.md +81 -0
  53. package/docs/javascripts/extra.js +174 -0
  54. package/docs/performance.md +37 -0
  55. package/docs/stylesheets/extra.css +819 -0
  56. package/mkdocs.yml +217 -0
  57. package/overrides/main.html +26 -0
  58. package/overrides/partials/footer.html +9 -0
  59. package/package.json +4 -2
  60. package/requirements-docs.txt +1 -0
  61. package/docs/README.md +0 -18
  62. package/docs/advanced_usage.md +0 -71
  63. package/docs/api.md +0 -122
  64. package/docs/filters.md +0 -708
  65. package/docs/installation.md +0 -106
  66. package/docs/integrations.md +0 -214
  67. package/docs/overview.md +0 -79
  68. package/docs/partialdef.md +0 -70
  69. package/docs/security.md +0 -27
  70. package/docs/tags.md +0 -673
  71. package/docs/usage.md +0 -646
@@ -0,0 +1,783 @@
1
+ # Tags
2
+
3
+ Tags control template logic and structure. They use `{% %}` syntax.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Control Flow](#control-flow)
8
+ - [Variable Assignment](#variable-assignment)
9
+ - [Change Detection](#change-detection)
10
+ - [Date and Time](#date-and-time)
11
+ - [Utility Tags](#utility-tags)
12
+ - [Security Tags](#security-tags)
13
+ - [Comments and Raw Output](#comments-and-raw-output)
14
+ - [Autoescape](#autoescape)
15
+ - [Library Loading](#library-loading)
16
+ - [Template Tags](#template-tags)
17
+ - [Inheritance Tags](#inheritance-tags)
18
+ - [Partial Tags](#partial-tags)
19
+ - [i18n Tags](#i18n-tags)
20
+
21
+ ---
22
+
23
+ ## Control Flow
24
+
25
+ ### if / elif / else / endif
26
+
27
+ Conditional rendering with a wide range of operators.
28
+
29
+ === "Basic if"
30
+
31
+ ```html
32
+ {% if user.is_authenticated %}
33
+ <p>Welcome back, {{ user.name }}!</p>
34
+ {% else %}
35
+ <p>Please <a href="/login">log in</a>.</p>
36
+ {% endif %}
37
+ ```
38
+
39
+ === "Multiple conditions with elif"
40
+
41
+ ```html
42
+ {% if user.role == 'admin' %}
43
+ <p>Admin panel</p>
44
+ {% elif user.is_staff %}
45
+ <p>Staff dashboard</p>
46
+ {% else %}
47
+ <p>Guest view</p>
48
+ {% endif %}
49
+ ```
50
+
51
+ === "Combined conditions with parentheses"
52
+
53
+ ```html
54
+ {% if (user.role == 'admin' or user.is_staff) and user.is_active %}
55
+ <p>Active staff member</p>
56
+ {% endif %}
57
+
58
+ {% if item not in cart_items %}
59
+ <button>Add to cart</button>
60
+ {% endif %}
61
+ ```
62
+
63
+ **Supported operators:** `==`, `!=`, `<`, `<=`, `>`, `>=`, `in`, `not in`, `and`, `or`, `not`
64
+
65
+ **Operator precedence** (highest to lowest): comparison → `and` → `or`
66
+
67
+ ### for / empty / endfor
68
+
69
+ Loop over arrays and objects. Injects `forloop` meta tracking.
70
+
71
+ === "Basic loop"
72
+
73
+ ```html
74
+ <ul>
75
+ {% for item in items %}
76
+ <li>{{ forloop.counter }}: {{ item }}</li>
77
+ {% empty %}
78
+ <li>No items found</li>
79
+ {% endfor %}
80
+ </ul>
81
+ ```
82
+
83
+ === "Loop with filters"
84
+
85
+ ```html
86
+ {% for group in items|regroup:"category" %}
87
+ <h3>{{ group.grouper }}</h3>
88
+ {% for item in group.list %}- {{ item.name }}
89
+ {% endfor %}
90
+ {% endfor %}
91
+ ```
92
+
93
+ === "Dictionary iteration"
94
+
95
+ ```html
96
+ {% for key, value in config %}
97
+ <dt>{{ key }}</dt>
98
+ <dd>{{ value }}</dd>
99
+ {% endfor %}
100
+ ```
101
+
102
+ === "Nested loops"
103
+
104
+ ```html
105
+ {% for department in departments %}
106
+ <h2>{{ department.name }}</h2>
107
+ {% for employee in department.employees %}
108
+ <span>#{{ forloop.parentloop.counter }}.{{ forloop.counter }} {{ employee.name }}</span>
109
+ {% endfor %}
110
+ {% endfor %}
111
+ ```
112
+
113
+ #### forloop Metadata
114
+
115
+ | Variable | Description |
116
+ |----------|-------------|
117
+ | `forloop.counter` | 1-based index |
118
+ | `forloop.counter0` | 0-based index |
119
+ | `forloop.revcounter` | Reverse 1-based index |
120
+ | `forloop.revcounter0` | Reverse 0-based index |
121
+ | `forloop.first` | `true` on first iteration |
122
+ | `forloop.last` | `true` on last iteration |
123
+ | `forloop.parentloop` | Parent loop context (nested loops) |
124
+
125
+ **Real-world table with alternating row classes:**
126
+
127
+ ```html
128
+ <table>
129
+ {% for row in rows %}
130
+ <tr class="{% cycle 'row-odd' 'row-even' %}">
131
+ <td>{{ row.name }}</td>
132
+ <td>{{ row.value }}</td>
133
+ </tr>
134
+ {% endfor %}
135
+ </table>
136
+ ```
137
+
138
+ ### with / endwith
139
+
140
+ Scope localized variables.
141
+
142
+ === "Alias a variable"
143
+
144
+ ```html
145
+ {% with user.profile.address as addr %}
146
+ <p>{{ addr.city }}, {{ addr.zip }}</p>
147
+ {% endwith %}
148
+ ```
149
+
150
+ === "Multiple assignments"
151
+
152
+ ```html
153
+ {% with greeting="Hello", who="World" %}
154
+ <p>{{ greeting }} {{ who }}</p>
155
+ {% endwith %}
156
+ ```
157
+
158
+ === "Combined alias"
159
+
160
+ ```html
161
+ {% with a=5, b=10 as total %}
162
+ <p>Total: {{ total }}</p>
163
+ {% endwith %}
164
+ ```
165
+
166
+ ### cycle
167
+
168
+ Cycle through values sequentially.
169
+
170
+ === "Alternating CSS classes"
171
+
172
+ ```html
173
+ {% for row in rows %}
174
+ <tr class="{% cycle 'row-odd' 'row-even' %}">...</tr>
175
+ {% endfor %}
176
+ ```
177
+
178
+ === "Store without output (as)"
179
+
180
+ ```html
181
+ {% cycle 'row-odd' 'row-even' as row_class %}
182
+ <tr class="{{ row_class }}">
183
+ ```
184
+
185
+ === "Named cycle for resumable state"
186
+
187
+ ```html
188
+ {% for item in items %}
189
+ {% cycle 'a' 'b' 'c' as marker silent %}
190
+ {% if marker == 'b' %}
191
+ <strong>{{ item }}</strong>
192
+ {% else %}
193
+ {{ item }}
194
+ {% endif %}
195
+ {% endfor %}
196
+ ```
197
+
198
+ ### firstof
199
+
200
+ Return the first truthy value.
201
+
202
+ ```html
203
+ {% firstof user.display_name user.username "Anonymous" %}
204
+ ```
205
+
206
+ ---
207
+
208
+ ## Variable Assignment
209
+
210
+ ### set
211
+
212
+ Assign a value to a variable.
213
+
214
+ === "Inline assignment"
215
+
216
+ ```html
217
+ {% set total = price * quantity %}
218
+ <p>Total: ${{ total|floatformat:2 }}</p>
219
+ ```
220
+
221
+ === "Block form (captures rendered output)"
222
+
223
+ ```html
224
+ {% set greeting %}
225
+ Hello {{ user.name|title }}, welcome to {{ site.name }}!
226
+ {% endset %}
227
+
228
+ <h1>{{ greeting|safe }}</h1>
229
+ ```
230
+
231
+ === "Multiple variables"
232
+
233
+ ```html
234
+ {% set tax_rate = 0.08, tax = subtotal|mult:tax_rate %}
235
+ ```
236
+
237
+ Variables set with `{% set %}` persist in the current scope and can be used after the tag.
238
+
239
+ ---
240
+
241
+ ## Change Detection
242
+
243
+ ### ifchanged / endifchanged
244
+
245
+ Render the body only when a value changes.
246
+
247
+ === "Basic (no argument)"
248
+
249
+ ```html
250
+ {% for item in changelog %}
251
+ {% ifchanged item.timestamp %}
252
+ <h3>{{ item.timestamp|date:"Y-m-d" }}</h3>
253
+ {% endifchanged %}
254
+ <p>{{ item.change }}</p>
255
+ {% endfor %}
256
+ ```
257
+
258
+ === "With else"
259
+
260
+ ```html
261
+ {% for item in items %}
262
+ {% ifchanged item.category %}
263
+ <h2>{{ item.category }}</h2>
264
+ {% else %}
265
+ <p>Same category as above</p>
266
+ {% endifchanged %}
267
+ {% endfor %}
268
+ ```
269
+
270
+ ---
271
+
272
+ ## Date and Time
273
+
274
+ ### now
275
+
276
+ Output the current date/time.
277
+
278
+ ```html
279
+ <p>Current time: {% now "Y-m-d H:i:s" %}</p>
280
+ <p>Pretty date: {% now "F j, Y" %}</p>
281
+ ```
282
+
283
+ Uses the same format codes as the `date` filter (Django-style tokens like `Y`, `m`, `d`, `H`, `i`, `s`, `F`).
284
+
285
+ **Real-world copyright footer:**
286
+
287
+ ```html
288
+ <footer>
289
+ &copy; {{ "now"|date:"Y" }} {{ site.name }}. All rights reserved.
290
+ </footer>
291
+ ```
292
+
293
+ ---
294
+
295
+ ## Utility Tags
296
+
297
+ ### static
298
+
299
+ Generate a static file URL.
300
+
301
+ ```html
302
+ <link rel="stylesheet" href="{% static "css/main.css" %}">
303
+ <script src="{% static "js/app.js" %}"></script>
304
+ <img src="{% static "images/logo.svg" }}" alt="{{ site.name }}">
305
+ ```
306
+
307
+ Configure the prefix at compile time:
308
+
309
+ === "CommonJS"
310
+
311
+ ```javascript
312
+ const { compile } = require('miki-template');
313
+ const template = compile(source, { staticUrl: '/assets/' });
314
+ ```
315
+
316
+ === "ES Modules"
317
+
318
+ ```javascript
319
+ import { compile } from 'miki-template';
320
+ const template = compile(source, { staticUrl: '/assets/' });
321
+ ```
322
+
323
+ ### url
324
+
325
+ Build a URL from a route name.
326
+
327
+ === "Basic"
328
+
329
+ ```html
330
+ <a href="{% url 'user.profile' user.id %}">Profile</a>
331
+ ```
332
+
333
+ === "With keyword arguments"
334
+
335
+ ```html
336
+ {% url 'posts.show' post.id tab='comments' %}
337
+ ```
338
+
339
+ === "Deep routing with dots"
340
+
341
+ ```html
342
+ {% url 'user.profile.posts.show' user.id post.id %}
343
+ ```
344
+
345
+ Configure with a custom resolver:
346
+
347
+ === "CommonJS"
348
+
349
+ ```javascript
350
+ const { compile } = require('miki-template');
351
+ const template = compile(source, {
352
+ urlHelper: (routeName, ...args) => {
353
+ // Convert "user.profile" + [42] → "/user/profile/42"
354
+ return '/' + routeName.split('.').join('/') + '/' + args.join('/');
355
+ }
356
+ });
357
+ ```
358
+
359
+ === "ES Modules"
360
+
361
+ ```javascript
362
+ import { compile } from 'miki-template';
363
+ const template = compile(source, {
364
+ urlHelper: (routeName, ...args) => {
365
+ return '/' + routeName.split('.').join('/') + '/' + args.join('/');
366
+ }
367
+ });
368
+ ```
369
+
370
+ ### regroup
371
+
372
+ Group a list by a common attribute.
373
+
374
+ ```html
375
+ {% regroup people by gender as departments %}
376
+ {% for dept in departments %}
377
+ <h3>{{ dept.grouper }}</h3>
378
+ {% for person in dept.list %}
379
+ <p>{{ person.name }}</p>
380
+ {% endfor %}
381
+ {% endfor %}
382
+ ```
383
+
384
+ You can also use `regroup` as a filter inside a `{% for %}` loop:
385
+
386
+ ```html
387
+ {% for group in items|regroup:"category" %}
388
+ <h3>{{ group.grouper }}</h3>
389
+ {% for item in group.list %}
390
+ <p>{{ item.name }}</p>
391
+ {% endfor %}
392
+ {% endfor %}
393
+ ```
394
+
395
+ ### spaceless
396
+
397
+ Remove whitespace between HTML tags.
398
+
399
+ ```html
400
+ {% spaceless %}
401
+ <div>
402
+ <span> hello </span>
403
+ </div>
404
+ {% endspaceless %}
405
+ ```
406
+
407
+ Output: `<div><span> hello </span></div>`
408
+
409
+ ### widthratio
410
+
411
+ Calculate ratios for progress bars or scaling.
412
+
413
+ ```html
414
+ <!-- Calculate 25 out of 100 scaled to max-width 150 -->
415
+ {% widthratio score 100 150 %}
416
+ <!-- → 37 (floor of 25/100*150) -->
417
+
418
+ <!-- Progress bar width -->
419
+ <div class="bar" style="width: {% widthratio value max_value 100 %}px;"></div>
420
+ ```
421
+
422
+ ### debug
423
+
424
+ Dump the current template context for debugging.
425
+
426
+ ```html
427
+ {% debug %}
428
+ ```
429
+
430
+ Outputs a `<pre>` block with all context variables.
431
+
432
+ ---
433
+
434
+ ## Security Tags
435
+
436
+ ### csrf_token
437
+
438
+ Output a hidden CSRF token input.
439
+
440
+ ```html
441
+ <form method="post">
442
+ {% csrf_token %}
443
+ <button type="submit">Submit</button>
444
+ </form>
445
+ ```
446
+
447
+ The token value is HTML-escaped to prevent attribute injection. Requires `csrf_token` to be present in the template context.
448
+
449
+ === "CommonJS (Express middleware)"
450
+
451
+ ```javascript
452
+ app.use((req, res, next) => {
453
+ res.locals.csrf_token = req.csrfToken();
454
+ next();
455
+ });
456
+ ```
457
+
458
+ === "ES Modules"
459
+
460
+ ```javascript
461
+ app.use((req, res, next) => {
462
+ res.locals.csrf_token = req.csrfToken();
463
+ next();
464
+ });
465
+ ```
466
+
467
+ ### csp_nonce_attr
468
+
469
+ Output a `nonce` attribute when `csp_nonce` is in the context.
470
+
471
+ ```html
472
+ <script {% csp_nonce_attr %} src="/js/app.js"></script>
473
+ ```
474
+
475
+ If `csp_nonce` is present in context, the output is:
476
+
477
+ ```html
478
+ <script nonce="abc123" src="/js/app.js"></script>
479
+ ```
480
+
481
+ If `csp_nonce` is not present, the tag outputs nothing.
482
+
483
+ ---
484
+
485
+ ## Comments and Raw Output
486
+
487
+ ### comment / endcomment
488
+
489
+ Block comments ignored during parsing.
490
+
491
+ ```html
492
+ {% comment %}
493
+ This is a comment.
494
+ It can span multiple lines.
495
+ {% endcomment %}
496
+ ```
497
+
498
+ ### verbatim / endverbatim
499
+
500
+ Treat content as raw text — template syntax is not parsed.
501
+
502
+ ```html
503
+ {% verbatim %}
504
+ This will NOT be parsed: {{ user.name }}
505
+ And this won't either: {% if x %}
506
+ {% endverbatim %}
507
+ ```
508
+
509
+ You can also name a verbatim block:
510
+
511
+ ```html
512
+ {% verbatim myscript %}
513
+ {{ angularExpression }}
514
+ {% endverbatim %}
515
+ ```
516
+
517
+ ---
518
+
519
+ ## Autoescape
520
+
521
+ Control HTML escaping for a block.
522
+
523
+ ```html
524
+ {% autoescape on %}
525
+ {{ user_input }} {# escaped → &lt;script&gt;... #}
526
+ {% endautoescape %}
527
+
528
+ {% autoescape off %}
529
+ {{ trusted_html }} {# not escaped → raw HTML #}
530
+ {% endautoescape %}
531
+ ```
532
+
533
+ ---
534
+
535
+ ## Library Loading
536
+
537
+ ### load
538
+
539
+ Activate a template library. Built-in libraries (`humanize`, `cache`, `lorem`) are auto-activated — you only need `{% load %}` for custom libraries you've registered.
540
+
541
+ ```html
542
+ {% load humanize %}
543
+ {{ views|intcomma }}
544
+ {{ count|ordinal }}
545
+ ```
546
+
547
+ === "CommonJS (registering a library)"
548
+
549
+ ```javascript
550
+ const { registerLibrary } = require('miki-template');
551
+
552
+ registerLibrary('myutils', {
553
+ filters: {
554
+ shout: (val) => String(val).toUpperCase() + '!'
555
+ }
556
+ });
557
+ ```
558
+
559
+ === "ES Modules"
560
+
561
+ ```javascript
562
+ import { registerLibrary } from 'miki-template';
563
+
564
+ registerLibrary('myutils', {
565
+ filters: {
566
+ shout: (val) => String(val).toUpperCase() + '!'
567
+ }
568
+ });
569
+ ```
570
+
571
+ Then use in templates:
572
+
573
+ ```html
574
+ {% load myutils %}
575
+ {{ name|shout }}
576
+ ```
577
+
578
+ **Built-in libraries:**
579
+
580
+ - `i18n` — `{% trans %}`, `{% blocktrans %}`, `{% language %}`
581
+ - `humanize` — `intcomma`, `intword`, `apnumber`, `ordinal`, `naturalday`
582
+ - `cache` — `{% cache timeout key %}...{% endcache %}`
583
+ - `lorem` — `{% lorem %}` tag and `lorem` filter
584
+
585
+ ---
586
+
587
+ ## Template Tags
588
+
589
+ ### templatetag
590
+
591
+ Output literal template tag tokens. Useful when generating documentation or when the template syntax conflicts with another templating layer.
592
+
593
+ ```html
594
+ {% templatetag openblock %} if user.is_admin {% templatetag closeblock %}
595
+ <!-- Renders: {% if user.is_admin %} -->
596
+
597
+ {% templatetag openvariable %} name {% templatetag closevariable %}
598
+ <!-- Renders: {{ name }} -->
599
+ ```
600
+
601
+ Available tokens:
602
+
603
+ | Token | Output |
604
+ |-------|--------|
605
+ | `openblock` | `{%` |
606
+ | `closeblock` | `%}` |
607
+ | `openvariable` | `{{` |
608
+ | `closevariable` | `}}` |
609
+ | `openbrace` | `{` |
610
+ | `closebrace` | `}` |
611
+ | `opencomment` | `{#` |
612
+ | `closecomment` | `#}` |
613
+
614
+ ---
615
+
616
+ ## Inheritance Tags
617
+
618
+ ### extends
619
+
620
+ Inherit from a parent template.
621
+
622
+ ```html
623
+ {% extends "base.html" %}
624
+ ```
625
+
626
+ Can use expressions for dynamic parent selection:
627
+
628
+ ```html
629
+ {% extends device|default:"desktop/base.html" %}
630
+ ```
631
+
632
+ **Security:** Path traversal is blocked — `{% extends "../../etc/passwd" %}` is rejected.
633
+
634
+ ### block / endblock
635
+
636
+ Define a block that can be overridden by child templates.
637
+
638
+ ```html
639
+ <!-- base.html -->
640
+ <html>
641
+ <body>
642
+ {% block content %}Default content{% endblock %}
643
+ </body>
644
+ </html>
645
+ ```
646
+
647
+ ```html
648
+ <!-- child.html -->
649
+ {% extends "base.html" %}
650
+ {% block content %}
651
+ <h1>Child content</h1>
652
+ {{ block.super }}
653
+ {% endblock %}
654
+ ```
655
+
656
+ ### block.super
657
+
658
+ A special variable (not a tag). When used inside a `{% block %}`, it renders the parent template's version of that block.
659
+
660
+ See [Template Inheritance](template-inheritance.md) for a detailed guide.
661
+
662
+ ### include
663
+
664
+ Include another template's content inline.
665
+
666
+ ```html
667
+ {% include "header.html" %}
668
+ {% include "header.html" with title="Hello" %}
669
+ {% include "header.html#partial_name" %}
670
+ {% include "header.html" with title="Hello" %}
671
+ ```
672
+
673
+ **Security:** Path traversal is blocked.
674
+
675
+ ---
676
+
677
+ ## Partial Tags
678
+
679
+ ### partialdef / endpartialdef
680
+
681
+ Define a reusable partial block.
682
+
683
+ ```html
684
+ {% partialdef card %}
685
+ <div class="card">
686
+ <h3>{{ title|default:"Untitled" }}</h3>
687
+ <p>{{ body|truncatewords:30 }}</p>
688
+ {% if featured %}<em>Featured</em>{% endif %}
689
+ </div>
690
+ {% endpartialdef %}
691
+ ```
692
+
693
+ **Options:**
694
+
695
+ | Option | Description |
696
+ |--------|-------------|
697
+ | `inline` | Renders the definition inline at its location during parse (the body appears in output AND registers for later use). |
698
+
699
+ ```html
700
+ {% partialdef greeting inline %}
701
+ Hello {{ name }}!
702
+ {% endpartialdef %}
703
+ <!-- Above line ALSO outputs "Hello World!" when rendered -->
704
+ ```
705
+
706
+ **Programmatic access:**
707
+
708
+ === "CommonJS"
709
+
710
+ ```javascript
711
+ const { compile } = require('miki-template');
712
+ const compiled = compile(template);
713
+ compiled.renderPartial('card', { title: 'Hi', body: 'There' });
714
+ ```
715
+
716
+ === "ES Modules"
717
+
718
+ ```javascript
719
+ import { compile } from 'miki-template';
720
+ const compiled = compile(template);
721
+ compiled.renderPartial('card', { title: 'Hi', body: 'There' });
722
+ ```
723
+
724
+ ### partial
725
+
726
+ Render a named partial.
727
+
728
+ ```html
729
+ {% partial card %}
730
+ {% partial card with title="Custom" body="World" %}
731
+ {% partial greeting with name=user.name %}
732
+ ```
733
+
734
+ See [Partial Templates](partial-templates.md) for a detailed guide.
735
+
736
+ ---
737
+
738
+ ## i18n Tags
739
+
740
+ ### trans
741
+
742
+ Translate a string.
743
+
744
+ ```html
745
+ {% trans "Hello, world!" %}
746
+ {% trans "Hello, %s!" name=user.name %}
747
+ {% trans context "verb" "He runs" %}
748
+ ```
749
+
750
+ ### blocktrans / endblocktrans
751
+
752
+ Translate a block of text with variable interpolation and pluralization.
753
+
754
+ ```html
755
+ {% blocktrans with name=user.name %}
756
+ Hello, {{ name }}!
757
+ {% endblocktrans %}
758
+
759
+ {% blocktrans count items|length %}
760
+ {{ count }} item
761
+ {% plural %}
762
+ {{ count }} items
763
+ {% endblocktrans %}
764
+ ```
765
+
766
+ ### language / endlanguage
767
+
768
+ Switch language temporarily for a block.
769
+
770
+ ```html
771
+ {% language "fr" %}
772
+ {% trans "Hello" %} → renders in French
773
+ {% endlanguage %}
774
+ ```
775
+
776
+ ---
777
+
778
+ ## Next Steps
779
+
780
+ - [Filters](./filters)
781
+ - [Template Inheritance](./template-inheritance)
782
+ - [Partial Templates](./partial-templates)
783
+ - [Custom Tags](./custom-tags)