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.
Files changed (73) hide show
  1. package/.github/workflows/ci.yml +54 -0
  2. package/AGENT.md +71 -0
  3. package/API_REFERENCE.md +314 -0
  4. package/CHANGELOG.md +97 -0
  5. package/CODE_OF_CONDUCT.md +14 -0
  6. package/CONTRIBUTING.md +27 -0
  7. package/README.md +304 -0
  8. package/ROADMAP.md +40 -0
  9. package/benchmarks/report.json +17 -0
  10. package/benchmarks/run.js +49 -0
  11. package/benchmarks/templates/large.dtpl +7 -0
  12. package/benchmarks/templates/medium.dtpl +3 -0
  13. package/benchmarks/templates/small.dtpl +7 -0
  14. package/context/component.md +109 -0
  15. package/context/prd.md +131 -0
  16. package/context/project-structure.md +33 -0
  17. package/docs/README.md +18 -0
  18. package/docs/advanced_usage.md +71 -0
  19. package/docs/api.md +102 -0
  20. package/docs/filters.md +540 -0
  21. package/docs/installation.md +106 -0
  22. package/docs/overview.md +57 -0
  23. package/docs/partialdef.md +41 -0
  24. package/docs/security.md +27 -0
  25. package/docs/tags.md +610 -0
  26. package/docs/usage.md +599 -0
  27. package/eslint.config.mjs +34 -0
  28. package/miki-template-1.2.0.vsix +0 -0
  29. package/miki-template-extension/LICENSE +21 -0
  30. package/miki-template-extension/README.md +82 -0
  31. package/miki-template-extension/icon.png +0 -0
  32. package/miki-template-extension/icon.svg +10 -0
  33. package/miki-template-extension/package.json +46 -0
  34. package/miki-template-extension/snippets/miki-template.json +177 -0
  35. package/miki-template-extension/syntaxes/language-configuration.json +26 -0
  36. package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +146 -0
  37. package/package.json +31 -0
  38. package/snippets/miki-template.json +177 -0
  39. package/src/asyncRender.js +21 -0
  40. package/src/cache.js +41 -0
  41. package/src/context.js +122 -0
  42. package/src/context_processors.js +41 -0
  43. package/src/esm.mjs +72 -0
  44. package/src/filters.js +527 -0
  45. package/src/i18n.js +171 -0
  46. package/src/index.js +454 -0
  47. package/src/lexer.js +92 -0
  48. package/src/libraries.js +240 -0
  49. package/src/parser.js +250 -0
  50. package/src/security.js +51 -0
  51. package/src/tags/control.js +591 -0
  52. package/src/tags/helpers.js +27 -0
  53. package/src/tags/i18n.js +230 -0
  54. package/src/tags/inheritance.js +216 -0
  55. package/src/tags/registry.js +18 -0
  56. package/src/tags/util.js +322 -0
  57. package/src/types.d.ts +107 -0
  58. package/syntaxes/language-configuration.json +26 -0
  59. package/syntaxes/miki-template.tmLanguage.json +146 -0
  60. package/tests/asyncRender.test.js +17 -0
  61. package/tests/base.html +6 -0
  62. package/tests/child.html +3 -0
  63. package/tests/context_processors.test.js +13 -0
  64. package/tests/esm.test.mjs +26 -0
  65. package/tests/filters.test.js +99 -0
  66. package/tests/include_security.test.js +9 -0
  67. package/tests/lexer.test.js +45 -0
  68. package/tests/parser.test.js +55 -0
  69. package/tests/partial.html +1 -0
  70. package/tests/partialdef.test.js +40 -0
  71. package/tests/production_checks.js +57 -0
  72. package/tests/security.test.js +28 -0
  73. package/tests/tags.test.js +203 -0
package/docs/usage.md ADDED
@@ -0,0 +1,599 @@
1
+ # Usage Guide
2
+
3
+ This guide covers all usage patterns for **miki-template**, from basic variable rendering to advanced Express integration.
4
+
5
+ ---
6
+
7
+ ## Quick Reference
8
+
9
+ ### One-off Rendering
10
+
11
+ **CommonJS:**
12
+ ```javascript
13
+ const { render } = require('miki-template');
14
+
15
+ const output = render('Hello {{ name }}!', { name: 'World' });
16
+ // → "Hello World!"
17
+ ```
18
+
19
+ **ESM:**
20
+ ```javascript
21
+ import { render } from 'miki-template';
22
+
23
+ const output = render('Hello {{ name }}!', { name: 'World' });
24
+ // → "Hello World!"
25
+ ```
26
+
27
+ ### Compiled Templates (recommended for repeated use)
28
+
29
+ **CommonJS:**
30
+ ```javascript
31
+ const { compile } = require('miki-template');
32
+
33
+ const template = compile('Welcome, {{ user.name }}!');
34
+
35
+ // Render 1
36
+ console.log(template.render({ user: { name: 'Alice' } }));
37
+ // → "Welcome, Alice!"
38
+
39
+ // Render 2
40
+ console.log(template.render({ user: { name: 'Bob' } }));
41
+ // → "Welcome, Bob!"
42
+ ```
43
+
44
+ **ESM:**
45
+ ```javascript
46
+ import { compile } from 'miki-template';
47
+
48
+ const template = compile('Welcome, {{ user.name }}!');
49
+
50
+ console.log(template.render({ user: { name: 'Alice' } }));
51
+ // → "Welcome, Alice!"
52
+
53
+ console.log(template.render({ user: { name: 'Bob' } }));
54
+ // → "Welcome, Bob!"
55
+ ```
56
+
57
+ ---
58
+
59
+ ## Variables and Lookups
60
+
61
+ ### Basic Variables
62
+
63
+ ```html
64
+ <p>Hello, {{ name }}!</p>
65
+ ```
66
+
67
+ ### Dotted Lookups (nested properties)
68
+
69
+ ```html
70
+ <p>{{ user.profile.displayName }}</p>
71
+ <p>{{ config.site.title }}</p>
72
+ ```
73
+
74
+ ### Array Indexing
75
+
76
+ ```html
77
+ <p>First item: {{ items.0 }}</p>
78
+ <p>Third item: {{ items.2 }}</p>
79
+ ```
80
+
81
+ ### Function Call
82
+
83
+ If a resolved value is a function, it is called automatically with zero arguments:
84
+
85
+ ```javascript
86
+ // Context: { user: { getName: () => 'Miki' } }
87
+ {{ user.getName }} // → "Miki"
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Filters
93
+
94
+ Filters transform variable output. Apply them with the pipe `|` character:
95
+
96
+ ```html
97
+ {{ name|upper }} → "MIKI"
98
+ {{ title|slugify }} → "hello-world"
99
+ {{ text|truncatewords:20 }} → truncated to 20 words
100
+ {{ date|date:"Y-m-d" }} → "2026-08-31"
101
+ {{ user.name|default:"Anonymous" }} → "Miki" or "Anonymous"
102
+ ```
103
+
104
+ ### Filter Chaining
105
+
106
+ Filters apply left-to-right:
107
+
108
+ ```html
109
+ {{ name|lower|capfirst }} → "miki" → "Miki"
110
+ {{ bio|striptags|truncatewords:50 }} → strip HTML, then truncate
111
+ {{ price|floatformat:2|add:10 }} → format, then add 10
112
+ ```
113
+
114
+ ### Filter Arguments
115
+
116
+ Most filters accept optional arguments after a colon:
117
+
118
+ ```html
119
+ {{ items|join:", " }} → "a, b, c"
120
+ {{ text|truncatewords:10 }} → 10 words max
121
+ {{ date|date:"F j, Y" }} → "August 31, 2026"
122
+ {{ value|default:"N/A" }} → fallback if falsy
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Control Flow Tags
128
+
129
+ ### `{% if %} / {% elif %} / {% else %} / {% endif %}`
130
+
131
+ ```html
132
+ {% if user.is_active %}
133
+ <p>Welcome back!</p>
134
+ {% elif user.is_pending %}
135
+ <p>Please verify your email.</p>
136
+ {% else %}
137
+ <p>Contact support.</p>
138
+ {% endif %}
139
+ ```
140
+
141
+ Supported operators: `==`, `!=`, `<`, `<=`, `>`, `>=`, `and`, `or`, `not`, `in`, `not in`
142
+
143
+ ```html
144
+ {% if user.role == 'admin' or user.is_staff %}
145
+ <a href="/admin">Admin Panel</a>
146
+ {% endif %}
147
+
148
+ {% if item in cart_items %}
149
+ <span>In cart</span>
150
+ {% endif %}
151
+
152
+ {% if not user.is_banned %}
153
+ <p>You may post.</p>
154
+ {% endif %}
155
+ ```
156
+
157
+ ### `{% for %} / {% empty %} / {% endfor %}`
158
+
159
+ Loop over arrays:
160
+
161
+ ```html
162
+ <ul>
163
+ {% for item in items %}
164
+ <li>{{ item }}</li>
165
+ {% empty %}
166
+ <li>No items found.</li>
167
+ {% endfor %}
168
+ </ul>
169
+ ```
170
+
171
+ Loop with unpacking (arrays):
172
+
173
+ ```html
174
+ {% for name, index in items %}
175
+ {{ forloop.counter }}. {{ name }}
176
+ {% endfor %}
177
+ ```
178
+
179
+ Loop over objects (key, value):
180
+
181
+ ```html
182
+ {% for key, value in config %}
183
+ <tr>
184
+ <td>{{ key }}</td>
185
+ <td>{{ value }}</td>
186
+ </tr>
187
+ {% endfor %}
188
+ ```
189
+
190
+ Loop metadata (`forloop`):
191
+
192
+ ```html
193
+ {% for item in items %}
194
+ {% if forloop.first %}<ul>{% endif %}
195
+ <li>{% if forloop.last %}last!{% else %}{{ item }}{% endif %}</li>
196
+ {% if forloop.last %}</ul>{% endif %}
197
+ {% endfor %}
198
+ ```
199
+
200
+ Available `forloop` properties:
201
+ | Property | Description |
202
+ |----------|-------------|
203
+ | `forloop.counter` | 1-indexed position |
204
+ | `forloop.counter0` | 0-indexed position |
205
+ | `forloop.revcounter` | Countdown from end (1-indexed) |
206
+ | `forloop.revcounter0` | Countdown from end (0-indexed) |
207
+ | `forloop.first` | `true` on first iteration |
208
+ | `forloop.last` | `true` on last iteration |
209
+ | `forloop.parentloop` | Reference to parent loop's metadata |
210
+
211
+ Nested loops:
212
+
213
+ ```html
214
+ {% for group in groups %}
215
+ {% for item in group.items %}
216
+ {{ forloop.parentloop.counter }}.{{ forloop.counter }}: {{ item }}
217
+ {% endfor %}
218
+ {% endfor %}
219
+ ```
220
+
221
+ ### `{% with %} / {% endwith %}`
222
+
223
+ Create scoped aliases:
224
+
225
+ ```html
226
+ {% with user.profile.address as addr %}
227
+ <p>{{ addr.city }}, {{ addr.country }}</p>
228
+ {% endwith %}
229
+
230
+ {% with a=x b=y c=z %}
231
+ {{ a }} + {{ b }} + {{ c }}
232
+ {% endwith %}
233
+ ```
234
+
235
+ ### `{% cycle %}`
236
+
237
+ Cycle through values on each iteration:
238
+
239
+ ```html
240
+ {% for row in rows %}
241
+ <tr class="{% cycle 'row-even' 'row-odd' %}">
242
+ <td>{{ row.name }}</td>
243
+ </tr>
244
+ {% endfor %}
245
+ ```
246
+
247
+ Cycle with named state:
248
+
249
+ ```html
250
+ {% for item in items %}
251
+ {% cycle 'row1' 'row2' as row_class %}
252
+ <tr class="{{ row_class }}">{{ item }}</tr>
253
+ {% endfor %}
254
+ ```
255
+
256
+ ### `{% firstof %}`
257
+
258
+ Return the first truthy value:
259
+
260
+ ```html
261
+ {% firstof user.display_name user.username "Guest" %}
262
+ <!-- Returns first non-falsy value -->
263
+ ```
264
+
265
+ ---
266
+
267
+ ## Template Inheritance
268
+
269
+ ### Base Template
270
+
271
+ ```html
272
+ <!-- base.html -->
273
+ <html>
274
+ <head>
275
+ <title>{% block title %}Default Title{% endblock %}</title>
276
+ {% block extra_head %}{% endblock %}
277
+ </head>
278
+ <body>
279
+ <header>{% block header %}Site Header{% endblock %}</header>
280
+ <main>{% block content %}{% endblock %}</main>
281
+ <footer>{% block footer %}{% endblock %}</footer>
282
+ </body>
283
+ </html>
284
+ ```
285
+
286
+ ### Child Template
287
+
288
+ ```html
289
+ <!-- home.html -->
290
+ {% extends "base.html" %}
291
+
292
+ {% block title %}Home Page{% endblock %}
293
+
294
+ {% block content %}
295
+ <h1>Welcome!</h1>
296
+ {{ block.super }} <!-- renders parent's block content -->
297
+ {% endblock %}
298
+ ```
299
+
300
+ `{{ block.super }}` renders the parent template's block content within the override.
301
+
302
+ ### Multi-level Inheritance
303
+
304
+ ```
305
+ base.html
306
+ └── base_blog.html {% extends "base.html" %}
307
+ └── post.html {% extends "base_blog.html" %}
308
+ ```
309
+
310
+ ---
311
+
312
+ ## Include and Partials
313
+
314
+ ### `{% include %}`
315
+
316
+ Include another template file:
317
+
318
+ ```html
319
+ {% include "header.html" %}
320
+ {% include "sidebar.html" with active="home" %}
321
+ {% include user.theme|add:".html" %} <!-- dynamic template name -->
322
+ ```
323
+
324
+ Path traversal is blocked for security.
325
+
326
+ ### `{% partialdef %} / {% partial %}`
327
+
328
+ Define and render reusable partial snippets within a template:
329
+
330
+ ```html
331
+ {% partialdef card %}
332
+ <div class="card">
333
+ <h3>{{ title }}</h3>
334
+ <p>{{ description }}</p>
335
+ </div>
336
+ {% endpartialdef %}
337
+
338
+ {% partial card with title="Hello" description="World" %}
339
+ {% partial card with title="Foo" description="Bar" %}
340
+ ```
341
+
342
+ Inline partials render immediately:
343
+
344
+ ```html
345
+ {% partialdef greeting inline %}
346
+ Hello {{ name }}!
347
+ {% endpartialdef %}
348
+ <!-- Output: "Hello !" (name not yet defined) -->
349
+ ```
350
+
351
+ ### Programmatic Partial Rendering
352
+
353
+ ```javascript
354
+ const { compile } = require('miki-template');
355
+
356
+ const template = `{% partialdef my_partial %}Hello {{ who }}!{% endpartialdef %}`;
357
+ const compiled = compile(template);
358
+
359
+ console.log(compiled.renderPartial('my_partial', { who: 'World' }));
360
+ // → "Hello World!"
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Block Partial Rendering (HTMX / AJAX)
366
+
367
+ Render a specific block from a compiled template for AJAX responses:
368
+
369
+ ```javascript
370
+ const { compile } = require('miki-template');
371
+
372
+ const template = compile(`
373
+ {% extends "base.html" %}
374
+ {% block main %}
375
+ <h1>{{ title }}</h1>
376
+ <div class="content">{{ content }}</div>
377
+ {% endblock %}
378
+ `, { views: './templates' });
379
+
380
+ // Full page render
381
+ res.send(template.render({ title: 'Home', content: '...' }));
382
+
383
+ // Partial render — only the 'main' block
384
+ res.send(template.renderBlock('main', { title: 'Home', content: '...' }));
385
+ ```
386
+
387
+ ---
388
+
389
+ ## Async Rendering
390
+
391
+ For templates with async helpers (database lookups, API calls):
392
+
393
+ ```javascript
394
+ const { asyncRender, registerHelper } = require('miki-template');
395
+
396
+ registerHelper('fetch-user', async (content, ctx) => {
397
+ const userId = content.trim();
398
+ const user = await db.users.findById(userId);
399
+ return `User: ${user.name}`;
400
+ });
401
+
402
+ // Template: {% fetch-user %}123{% endfetch-user %}
403
+ const html = await asyncRender(template, { db });
404
+ ```
405
+
406
+ ---
407
+
408
+ ## Express Integration
409
+
410
+ ### Basic Setup
411
+
412
+ ```javascript
413
+ const express = require('express');
414
+ const { __express } = require('miki-template');
415
+
416
+ const app = express();
417
+
418
+ app.engine('html', __express);
419
+ app.set('view engine', 'html');
420
+ app.set('views', './views');
421
+
422
+ app.get('/', (req, res) => {
423
+ res.render('home', {
424
+ title: 'My Site',
425
+ user: req.user,
426
+ items: ['a', 'b', 'c']
427
+ });
428
+ });
429
+
430
+ app.listen(3000);
431
+ ```
432
+
433
+ ### Async Express Views
434
+
435
+ Express 5+ supports async route handlers natively:
436
+
437
+ ```javascript
438
+ app.get('/user/:id', async (req, res) => {
439
+ const user = await User.findById(req.params.id);
440
+ if (!user) return res.status(404).send('Not found');
441
+ res.render('user-profile', { user });
442
+ });
443
+ ```
444
+
445
+ ---
446
+
447
+ ## Context Processors
448
+
449
+ Context processors inject variables into every template render, like Django's custom context processors.
450
+
451
+ ```javascript
452
+ const { registerContextProcessor } = require('miki-template');
453
+
454
+ // Inject site-wide variables
455
+ registerContextProcessor(() => ({
456
+ site_name: 'MyApp',
457
+ current_year: new Date().getFullYear()
458
+ }));
459
+
460
+ // Access request-specific data
461
+ registerContextProcessor((ctx) => ({
462
+ is_authenticated: ctx.user !== null,
463
+ user_display: ctx.user ? ctx.user.name : 'Guest'
464
+ }));
465
+ ```
466
+
467
+ Now `{{ site_name }}` and `{{ current_year }}` are available in every template automatically.
468
+
469
+ ---
470
+
471
+ ## Security
472
+
473
+ ### Auto-escaping
474
+
475
+ HTML auto-escaping is **enabled by default**. All variable output is escaped:
476
+
477
+ ```html
478
+ {{ user_input }} → &lt;script&gt;alert()&lt;/script&gt;
479
+ ```
480
+
481
+ ### Marking Values as Safe
482
+
483
+ Use `|safe` for trusted HTML content:
484
+
485
+ ```html
486
+ {{ trusted_html|safe }}
487
+ ```
488
+
489
+ In JavaScript:
490
+
491
+ ```javascript
492
+ const { markSafe } = require('miki-template');
493
+
494
+ res.render('email', {
495
+ body: markSafe('<b>Welcome!</b>') // Won't be escaped
496
+ });
497
+ ```
498
+
499
+ ### CSRF Protection
500
+
501
+ ```html
502
+ <form method="post">
503
+ {% csrf_token %}
504
+ <!-- renders: <input type="hidden" name="csrfmiddlewaretoken" value="..."> -->
505
+ ...
506
+ </form>
507
+ ```
508
+
509
+ Provide `csrf_token` in context:
510
+
511
+ ```javascript
512
+ res.render('form', { csrf_token: req.csrfToken() });
513
+ ```
514
+
515
+ ### CSP Nonce
516
+
517
+ ```html
518
+ <script {% csp_nonce %} src="/app.js"></script>
519
+ ```
520
+
521
+ Provide `csp_nonce` in context:
522
+
523
+ ```javascript
524
+ res.render('page', { csp_nonce: res.locals.nonce });
525
+ ```
526
+
527
+ ---
528
+
529
+ ## Static Files and URLs
530
+
531
+ Configure the static URL prefix:
532
+
533
+ ```javascript
534
+ compile(template, { staticUrl: '/static/assets/' });
535
+ ```
536
+
537
+ Then in templates:
538
+
539
+ ```html
540
+ <img src="{% static "images/logo.png" %}" alt="Logo">
541
+ <!-- → /static/assets/images/logo.png -->
542
+
543
+ <script src="{% static "js/app.js" %}"></script>
544
+ ```
545
+
546
+ ### URL Resolution
547
+
548
+ ```javascript
549
+ compile(template, {
550
+ urlHelper: (routeName, ...args) => {
551
+ const routes = {
552
+ 'home': '/',
553
+ 'user-profile': (id) => `/users/${id}`
554
+ };
555
+ const handler = routes[routeName];
556
+ return typeof handler === 'function' ? handler(...args) : handler;
557
+ }
558
+ });
559
+ ```
560
+
561
+ ```html
562
+ <a href="{% url "home" %}">Home</a>
563
+ <a href="{% url "user-profile" user.id %}">Profile</a>
564
+ ```
565
+
566
+ ---
567
+
568
+ ## Error Handling
569
+
570
+ ### Unclosed Tags
571
+
572
+ Unclosed block tags produce an error:
573
+
574
+ ```html
575
+ {% if user.is_active %}
576
+ <p>Active</p>
577
+ <!-- Missing {% endif %} → throws "Unexpected end of template"
578
+ ```
579
+
580
+ ### Missing Partial
581
+
582
+ ```html
583
+ {% partial missing_name %}
584
+ <!-- throws: Partial 'missing_name' not found -->
585
+ ```
586
+
587
+ ### Missing Block
588
+
589
+ ```javascript
590
+ template.renderBlock('nonexistent', {});
591
+ // throws: Block 'nonexistent' not found in template
592
+ ```
593
+
594
+ ### Path Traversal Protection
595
+
596
+ ```html
597
+ {% include "../etc/passwd" %}
598
+ <!-- throws: Include tag attempted path traversal outside allowed views -->
599
+ ```
@@ -0,0 +1,34 @@
1
+ export default [
2
+ {
3
+ files: ['src/**/*.js'],
4
+ languageOptions: {
5
+ ecmaVersion: 'latest',
6
+ sourceType: 'commonjs',
7
+ globals: {
8
+ Buffer: 'readonly',
9
+ console: 'readonly',
10
+ exports: 'readonly',
11
+ module: 'readonly',
12
+ process: 'readonly',
13
+ __dirname: 'readonly',
14
+ __filename: 'readonly',
15
+ require: 'readonly',
16
+ setTimeout: 'readonly',
17
+ setInterval: 'readonly',
18
+ clearTimeout: 'readonly',
19
+ clearInterval: 'readonly',
20
+ setImmediate: 'readonly',
21
+ clearImmediate: 'readonly',
22
+ global: 'readonly'
23
+ }
24
+ },
25
+ rules: {
26
+ 'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
27
+ 'no-console': 'off',
28
+ 'no-undef': 'error',
29
+ 'semi': ['error', 'always'],
30
+ 'quotes': ['error', 'single'],
31
+ 'indent': ['error', 2]
32
+ }
33
+ }
34
+ ];
Binary file
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 miki-template contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.