miki-template 1.3.3 → 1.3.7

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 (115) hide show
  1. package/.eslintrc.json +16 -0
  2. package/.github/release-notes/v1.3.1.md +55 -55
  3. package/.github/release-notes/v1.3.3.md +77 -0
  4. package/.github/workflows/ci.yml +38 -54
  5. package/.github/workflows/release.yml +106 -0
  6. package/AGENT.md +71 -71
  7. package/API_REFERENCE.md +314 -314
  8. package/CHANGELOG.md +173 -169
  9. package/CODE_OF_CONDUCT.md +14 -14
  10. package/CONTRIBUTING.md +27 -27
  11. package/README.md +342 -321
  12. package/ROADMAP.md +40 -40
  13. package/benchmarks/report.json +16 -16
  14. package/benchmarks/run.js +49 -49
  15. package/benchmarks/stress.mjs +647 -647
  16. package/benchmarks/templates/large.dtpl +7 -7
  17. package/benchmarks/templates/medium.dtpl +3 -3
  18. package/benchmarks/templates/small.dtpl +7 -7
  19. package/context/component.md +109 -109
  20. package/context/prd.md +131 -131
  21. package/context/project-structure.md +33 -33
  22. package/dir/base.html +22 -22
  23. package/dir/cmpnt.html +10 -10
  24. package/dir/footer.html +2 -2
  25. package/dir/home.html +80 -80
  26. package/dir/index.html +80 -0
  27. package/dir/navbar.html +8 -8
  28. package/docs/README.md +18 -18
  29. package/docs/advanced_usage.md +71 -71
  30. package/docs/api.md +119 -119
  31. package/docs/filters.md +708 -708
  32. package/docs/installation.md +106 -106
  33. package/docs/overview.md +79 -57
  34. package/docs/partialdef.md +70 -70
  35. package/docs/security.md +27 -27
  36. package/docs/tags.md +673 -673
  37. package/docs/usage.md +646 -646
  38. package/eslint.config.mjs +42 -42
  39. package/ex.mjs +34 -32
  40. package/live-test/package-lock.json +915 -0
  41. package/live-test/package.json +9 -0
  42. package/live-test/server.js +14 -0
  43. package/live-test/views/base.html +8 -0
  44. package/live-test/views/child.html +7 -0
  45. package/live-test/views/index.html +1 -0
  46. package/miki-template-extension/.github/workflows/ci.yml +116 -116
  47. package/miki-template-extension/.vscodeignore +7 -7
  48. package/miki-template-extension/CHANGELOG.md +99 -99
  49. package/miki-template-extension/LICENSE +21 -21
  50. package/miki-template-extension/README.md +273 -273
  51. package/miki-template-extension/extension.js +1013 -1013
  52. package/miki-template-extension/icon.svg +10 -10
  53. package/miki-template-extension/package.json +280 -280
  54. package/miki-template-extension/snippets/miki-template.json +717 -717
  55. package/miki-template-extension/syntaxes/language-configuration.json +114 -114
  56. package/miki-template-extension/syntaxes/miki-template.tmLanguage.json +355 -355
  57. package/miki-template-extension/tests/grammar-tests.json +162 -162
  58. package/miki-template-extension/tests/run-grammar-tests.js +82 -82
  59. package/package.json +40 -34
  60. package/sample-app/package-lock.json +901 -0
  61. package/sample-app/package.json +9 -0
  62. package/sample-app/server.js +14 -0
  63. package/sample-app/views/index.html +1 -0
  64. package/scripts/build-vsix.js +129 -129
  65. package/scripts/build-vsix.ps1 +15 -15
  66. package/snippets/miki-template.json +177 -177
  67. package/src/asyncRender.js +20 -20
  68. package/src/cache.js +80 -80
  69. package/src/context.js +126 -126
  70. package/src/context_processors.js +48 -48
  71. package/src/esm.mjs +89 -84
  72. package/src/filters.js +975 -975
  73. package/src/i18n.js +171 -171
  74. package/src/index.js +1112 -940
  75. package/src/lexer.js +114 -114
  76. package/src/libraries.js +371 -371
  77. package/src/parser.js +270 -270
  78. package/src/security.js +53 -53
  79. package/src/tags/control.js +719 -719
  80. package/src/tags/extra.js +154 -154
  81. package/src/tags/helpers.js +26 -26
  82. package/src/tags/i18n.js +256 -256
  83. package/src/tags/inheritance.js +335 -335
  84. package/src/tags/registry.js +18 -18
  85. package/src/tags/util.js +400 -400
  86. package/src/types.d.ts +107 -107
  87. package/syntaxes/language-configuration.json +26 -26
  88. package/syntaxes/miki-template.tmLanguage.json +146 -146
  89. package/tests/asyncRender.test.js +17 -17
  90. package/tests/base.html +6 -6
  91. package/tests/child.html +3 -3
  92. package/tests/context_processors.test.js +13 -13
  93. package/tests/esm.test.mjs +61 -61
  94. package/tests/filters.test.js +254 -254
  95. package/tests/finder-appdirs.test.js +19 -0
  96. package/tests/finder.test.js +17 -0
  97. package/tests/fixtures/views/nested/index.html +1 -0
  98. package/tests/fixtures/views/partial.html +1 -0
  99. package/tests/fixtures/views/sub/deepfile.html +1 -0
  100. package/tests/fixtures/views-appdirs/product/site/detail.html +1 -0
  101. package/tests/include_security.test.js +9 -9
  102. package/tests/integration/README.md +32 -32
  103. package/tests/integration/features.test.cjs +1681 -1681
  104. package/tests/integration/features.test.mjs +1697 -1697
  105. package/tests/integration/finder.esm.test.mjs +13 -0
  106. package/tests/integration/templates/base.miki +6 -6
  107. package/tests/integration/templates/child.miki +6 -6
  108. package/tests/integration/templates/index.html +17 -17
  109. package/tests/lexer.test.js +45 -45
  110. package/tests/parser.test.js +57 -57
  111. package/tests/partial.html +1 -1
  112. package/tests/partialdef.test.js +79 -79
  113. package/tests/production_checks.js +57 -57
  114. package/tests/security.test.js +28 -28
  115. package/tests/tags.test.js +233 -233
package/src/libraries.js CHANGED
@@ -1,371 +1,371 @@
1
- /**
2
- * Plugin/filter library system.
3
- *
4
- * A "library" is a JavaScript object exporting:
5
- * { tags?: { [name]: parserFn }, filters?: { [name]: filterFn } }
6
- *
7
- * Libraries can be:
8
- * - Built-in (e.g. 'i18n', 'cache', 'humanize', 'markdown')
9
- * - Custom user-defined via `registerLibrary(name, lib)`
10
- * - Loaded from disk via `registerLibraryFromPath(name, path)`
11
- *
12
- * Once registered, a library is activated in templates via:
13
- * {% load library_name %}
14
- * {% load i18n cache humanize %}
15
- */
16
- const fs = require('fs');
17
- const path = require('path');
18
-
19
- const libraries = new Map(); // Map<name, libraryDef>
20
-
21
- // Registration functions are injected by index.js to break the
22
- // circular dependency between libraries.js and index.js.
23
- let registrationFunctions = null;
24
- function setRegistrationFunctions(fns) {
25
- registrationFunctions = fns;
26
- }
27
-
28
- /**
29
- * Register a library.
30
- *
31
- * registerLibrary('markdown', {
32
- * tags: { markdown: parseMarkdown },
33
- * filters: { markdown: renderMarkdown }
34
- * });
35
- */
36
- function registerLibrary(name, def) {
37
- if (!name || typeof name !== 'string') {
38
- throw new TypeError('Library name must be a non-empty string');
39
- }
40
- if (!def || typeof def !== 'object') {
41
- throw new TypeError(`Library '${name}' definition must be an object`);
42
- }
43
- libraries.set(name, {
44
- tags: def.tags || {},
45
- filters: def.filters || {},
46
- helpers: def.helpers || {}
47
- });
48
- }
49
-
50
- /** Unregister a library or all libraries. */
51
- function unregisterLibrary(name) {
52
- if (!name) {
53
- libraries.clear();
54
- return;
55
- }
56
- libraries.delete(name);
57
- }
58
-
59
- /** Get a registered library by name. */
60
- function getLibrary(name) {
61
- return libraries.get(name);
62
- }
63
-
64
- /** List all registered library names. */
65
- function getLibraryNames() {
66
- return Array.from(libraries.keys());
67
- }
68
-
69
- /**
70
- * Load a library from a JavaScript file on disk.
71
- * The file must be a CommonJS module exporting `{ tags, filters, helpers }`.
72
- */
73
- function registerLibraryFromPath(name, filePath, options = {}) {
74
- const absolutePath = path.isAbsolute(filePath)
75
- ? filePath
76
- : path.resolve(options.baseDir || process.cwd(), filePath);
77
- if (!fs.existsSync(absolutePath)) {
78
- throw new Error(`Library file not found: ${absolutePath}`);
79
- }
80
- // Defensive: restrict to .js/.cjs/.mjs files
81
- const ext = path.extname(absolutePath).toLowerCase();
82
- if (ext !== '.js' && ext !== '.cjs' && ext !== '.mjs') {
83
- throw new Error(`Library file extension '${ext}' is not allowed`);
84
- }
85
- // Clear require cache if not pinned
86
- if (!options.pinned) {
87
- delete require.cache[require.resolve(absolutePath)];
88
- }
89
- const def = require(absolutePath);
90
- registerLibrary(name, def);
91
- return def;
92
- }
93
-
94
- /**
95
- * Apply the tags, filters, and helpers from a library to the runtime registries.
96
- */
97
- function activateLibrary(name) {
98
- const lib = libraries.get(name);
99
- if (!lib) {
100
- throw new Error(`Library not found: '${name}'`);
101
- }
102
- // Require lazily; index.js will pass the real registration
103
- // functions via setRegistrationFunctions() before calling us.
104
- const { registerTag, registerFilter, registerHelper } = registrationFunctions || require('./index');
105
- for (const [tagName, parserFn] of Object.entries(lib.tags || {})) {
106
- registerTag(tagName, parserFn);
107
- }
108
- for (const [filterName, filterFn] of Object.entries(lib.filters || {})) {
109
- registerFilter(filterName, filterFn);
110
- }
111
- for (const [helperName, helperFn] of Object.entries(lib.helpers || {})) {
112
- registerHelper(helperName, helperFn);
113
- }
114
- }
115
-
116
- /**
117
- * Check if a library is registered.
118
- */
119
- function hasLibrary(name) {
120
- return libraries.has(name);
121
- }
122
-
123
- // --- Built-in libraries ---
124
-
125
- /** Humanize: nice-looking text formatting filters. */
126
- registerLibrary('humanize', {
127
- filters: {
128
- intcomma: (val) => {
129
- const parts = String(val === null || val === undefined ? '' : val).split('.');
130
- parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
131
- return parts.join('.');
132
- },
133
- intword: (val) => {
134
- const n = parseFloat(val);
135
- if (isNaN(n)) return String(val);
136
- const abs = Math.abs(n);
137
- if (abs >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, '') + ' billion';
138
- if (abs >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + ' million';
139
- if (abs >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + ' thousand';
140
- return String(n);
141
- },
142
- apnumber: (val) => {
143
- const n = parseInt(val, 10);
144
- const words = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
145
- 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'];
146
- if (!isNaN(n) && n >= 0 && n < words.length) return words[n];
147
- return String(val);
148
- },
149
- ordinal: (val) => {
150
- const n = parseInt(val, 10);
151
- if (isNaN(n)) return String(val);
152
- const s = ['th', 'st', 'nd', 'rd'];
153
- const v = n % 100;
154
- return n + (s[(v - 20) % 10] || s[v] || s[0]);
155
- },
156
- naturalday: (val) => {
157
- const d = new Date(val);
158
- if (isNaN(d.getTime())) return String(val);
159
- const today = new Date();
160
- const yesterday = new Date(today);
161
- yesterday.setDate(today.getDate() - 1);
162
- const tomorrow = new Date(today);
163
- tomorrow.setDate(today.getDate() + 1);
164
- const sameDay = (a, b) =>
165
- a.getFullYear() === b.getFullYear() &&
166
- a.getMonth() === b.getMonth() &&
167
- a.getDate() === b.getDate();
168
- if (sameDay(d, today)) return 'today';
169
- if (sameDay(d, yesterday)) return 'yesterday';
170
- if (sameDay(d, tomorrow)) return 'tomorrow';
171
- return String(val);
172
- }
173
- }
174
- });
175
-
176
- /** Cache: cache expensive template fragment results. */
177
- registerLibrary('cache', {
178
- tags: {
179
- cache: (tagContent, parser) => {
180
- // {% cache timeout key1 key2 ... %}...{% endcache %}
181
- const rest = tagContent.slice(5).trim();
182
- const tokens = rest.split(/\s+/);
183
- const timeout = parseInt(tokens[0], 10) || 0;
184
- const key = tokens.slice(1).join(':') || 'default';
185
- const body = parser.parse(['endcache']);
186
- const next = parser.peek();
187
- if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endcache') {
188
- parser.advance();
189
- }
190
- return {
191
- render(context) {
192
- const ctx = context;
193
- const fullKey = `cache:${key}`;
194
- if (ctx.cacheStore && ctx.cacheStore.has(fullKey)) {
195
- const entry = ctx.cacheStore.get(fullKey);
196
- if (Date.now() - entry.time < timeout * 1000) {
197
- return entry.value;
198
- }
199
- }
200
- const value = body.map(n => n.render(context)).join('');
201
- if (ctx.cacheStore) {
202
- ctx.cacheStore.set(fullKey, { time: Date.now(), value });
203
- }
204
- return value;
205
- }
206
- };
207
- }
208
- }
209
- });
210
-
211
- /** Lorem: generate placeholder text (Django-compatible). */
212
- registerLibrary('lorem', {
213
- filters: {
214
- lorem: (val, arg) => {
215
- let n = parseInt(arg, 10);
216
- if (isNaN(n)) n = 5;
217
- if (n < 1) n = 1;
218
- if (n > 100) n = 100;
219
- const text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.';
220
- const words = text.split(' ');
221
- let result = [];
222
- for (let i = 0; i < n; i++) {
223
- result.push(words[i % words.length]);
224
- }
225
- return result.join(' ');
226
- }
227
- },
228
- tags: {
229
- // Django-compatible lorem tag.
230
- //
231
- // {% lorem [count] [method] [random] %}
232
- //
233
- // count - number of words/paragraphs to generate (default 1)
234
- // method - w (words), p (HTML paragraphs), b (plain-text blocks, default)
235
- // random - skip the common "Lorem ipsum dolor sit amet..." opening
236
- //
237
- // {% lorem %} -> one block of common Lorem ipsum
238
- // {% lorem 3 p %} -> common paragraph + 2 random <p> blocks
239
- // {% lorem 2 w random %} -> 2 random words
240
- //
241
- // The tag is built into the engine and requires no {% load lorem %}.
242
- lorem: (tagContent, _parser) => {
243
- return {
244
- render: () => {
245
- // The tag registry calls the parser with the full content
246
- // including the tag name. For "{% lorem 3 p random %}" the
247
- // tagContent is "lorem 3 p random".
248
- const parts = tagContent.trim().split(/\s+/);
249
-
250
- // count = first numeric token (or 1 if none)
251
- let n = 1;
252
- for (let i = 0; i < parts.length; i++) {
253
- const num = parseInt(parts[i], 10);
254
- if (!isNaN(num) && String(num) === parts[i]) { n = Math.max(1, num); break; }
255
- }
256
- // method = w | b | p (default b, like Django)
257
- let unit = 'b';
258
- for (let i = 0; i < parts.length; i++) {
259
- if (/^(w|b|p|words?|blocks?|paragraphs?)$/i.test(parts[i])) {
260
- unit = parts[i][0].toLowerCase();
261
- break;
262
- }
263
- }
264
- // random = presence of the literal word "random"
265
- const random = parts.some(t => t.toLowerCase() === 'random');
266
-
267
- // The famous "common paragraph" Django uses as the seed.
268
- const COMMON = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
269
- // Word bank used to assemble random text when the "random"
270
- // keyword is given. Picked from the same Latin vocabulary.
271
- const WORDS = ('lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute irure in voluptate velit esse cillum fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt culpa qui officia deserunt mollit anim id est laborum').split(' ');
272
-
273
- // The non-random blocks cycle through a fixed set of
274
- // sentences taken from the COMMON paragraph above (the
275
- // first one is the "Lorem ipsum dolor sit amet" line).
276
- const SENTENCES = [
277
- 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.',
278
- 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
279
- 'Ut enim ad minim veniam, quis nostrud exercitation.',
280
- 'Duis aute irure dolor in reprehenderit in voluptate velit.',
281
- 'Excepteur sint occaecat cupidatat non proident.'
282
- ];
283
-
284
- if (unit === 'w') {
285
- // Words
286
- if (random) {
287
- const out = [];
288
- for (let i = 0; i < n; i++) {
289
- out.push(WORDS[Math.floor(Math.random() * WORDS.length)]);
290
- }
291
- return out.join(' ');
292
- }
293
- // Non-random: first word is always "lorem", the rest
294
- // continue the common paragraph.
295
- const commonWords = COMMON.replace(/[.,]/g, '').split(/\s+/);
296
- const out = [];
297
- for (let i = 0; i < n; i++) {
298
- out.push(commonWords[i % commonWords.length]);
299
- }
300
- return out.join(' ');
301
- }
302
-
303
- if (unit === 'p') {
304
- // HTML paragraphs (each wrapped in <p>...</p>)
305
- const out = [];
306
- for (let i = 0; i < n; i++) {
307
- let paragraph;
308
- if (i === 0 && !random) {
309
- paragraph = COMMON;
310
- } else if (random) {
311
- // Generate a sentence worth of random words
312
- const len = 12 + Math.floor(Math.random() * 16);
313
- const words = [];
314
- for (let j = 0; j < len; j++) {
315
- words.push(WORDS[Math.floor(Math.random() * WORDS.length)]);
316
- }
317
- paragraph = words.join(' ') + '.';
318
- } else {
319
- paragraph = SENTENCES[(i - 1) % (SENTENCES.length - 1) + 1];
320
- }
321
- out.push('<p>' + paragraph + '</p>');
322
- }
323
- return out.join('\n');
324
- }
325
-
326
- // unit === 'b' (default): plain-text blocks separated by blank lines
327
- const out = [];
328
- for (let i = 0; i < n; i++) {
329
- let paragraph;
330
- if (i === 0 && !random) {
331
- paragraph = COMMON;
332
- } else if (random) {
333
- const len = 30 + Math.floor(Math.random() * 20);
334
- const words = [];
335
- for (let j = 0; j < len; j++) {
336
- words.push(WORDS[Math.floor(Math.random() * WORDS.length)]);
337
- }
338
- paragraph = words.join(' ') + '.';
339
- } else {
340
- paragraph = SENTENCES[(i - 1) % (SENTENCES.length - 1) + 1];
341
- }
342
- out.push(paragraph);
343
- }
344
- return out.join('\n\n');
345
- }
346
- };
347
- }
348
- }
349
- });
350
-
351
- /** Static: additional static-file helpers. */
352
- registerLibrary('static', {
353
- filters: {
354
- static: (val) => {
355
- // Returns the value prefixed with the staticUrl (configured at compile time)
356
- return val; // Implementation lives in the {% static %} tag
357
- }
358
- }
359
- });
360
-
361
- module.exports = {
362
- registerLibrary,
363
- unregisterLibrary,
364
- getLibrary,
365
- getLibraryNames,
366
- hasLibrary,
367
- registerLibraryFromPath,
368
- activateLibrary,
369
- setRegistrationFunctions,
370
- libraries
371
- };
1
+ /**
2
+ * Plugin/filter library system.
3
+ *
4
+ * A "library" is a JavaScript object exporting:
5
+ * { tags?: { [name]: parserFn }, filters?: { [name]: filterFn } }
6
+ *
7
+ * Libraries can be:
8
+ * - Built-in (e.g. 'i18n', 'cache', 'humanize', 'markdown')
9
+ * - Custom user-defined via `registerLibrary(name, lib)`
10
+ * - Loaded from disk via `registerLibraryFromPath(name, path)`
11
+ *
12
+ * Once registered, a library is activated in templates via:
13
+ * {% load library_name %}
14
+ * {% load i18n cache humanize %}
15
+ */
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ const libraries = new Map(); // Map<name, libraryDef>
20
+
21
+ // Registration functions are injected by index.js to break the
22
+ // circular dependency between libraries.js and index.js.
23
+ let registrationFunctions = null;
24
+ function setRegistrationFunctions(fns) {
25
+ registrationFunctions = fns;
26
+ }
27
+
28
+ /**
29
+ * Register a library.
30
+ *
31
+ * registerLibrary('markdown', {
32
+ * tags: { markdown: parseMarkdown },
33
+ * filters: { markdown: renderMarkdown }
34
+ * });
35
+ */
36
+ function registerLibrary(name, def) {
37
+ if (!name || typeof name !== 'string') {
38
+ throw new TypeError('Library name must be a non-empty string');
39
+ }
40
+ if (!def || typeof def !== 'object') {
41
+ throw new TypeError(`Library '${name}' definition must be an object`);
42
+ }
43
+ libraries.set(name, {
44
+ tags: def.tags || {},
45
+ filters: def.filters || {},
46
+ helpers: def.helpers || {}
47
+ });
48
+ }
49
+
50
+ /** Unregister a library or all libraries. */
51
+ function unregisterLibrary(name) {
52
+ if (!name) {
53
+ libraries.clear();
54
+ return;
55
+ }
56
+ libraries.delete(name);
57
+ }
58
+
59
+ /** Get a registered library by name. */
60
+ function getLibrary(name) {
61
+ return libraries.get(name);
62
+ }
63
+
64
+ /** List all registered library names. */
65
+ function getLibraryNames() {
66
+ return Array.from(libraries.keys());
67
+ }
68
+
69
+ /**
70
+ * Load a library from a JavaScript file on disk.
71
+ * The file must be a CommonJS module exporting `{ tags, filters, helpers }`.
72
+ */
73
+ function registerLibraryFromPath(name, filePath, options = {}) {
74
+ const absolutePath = path.isAbsolute(filePath)
75
+ ? filePath
76
+ : path.resolve(options.baseDir || process.cwd(), filePath);
77
+ if (!fs.existsSync(absolutePath)) {
78
+ throw new Error(`Library file not found: ${absolutePath}`);
79
+ }
80
+ // Defensive: restrict to .js/.cjs/.mjs files
81
+ const ext = path.extname(absolutePath).toLowerCase();
82
+ if (ext !== '.js' && ext !== '.cjs' && ext !== '.mjs') {
83
+ throw new Error(`Library file extension '${ext}' is not allowed`);
84
+ }
85
+ // Clear require cache if not pinned
86
+ if (!options.pinned) {
87
+ delete require.cache[require.resolve(absolutePath)];
88
+ }
89
+ const def = require(absolutePath);
90
+ registerLibrary(name, def);
91
+ return def;
92
+ }
93
+
94
+ /**
95
+ * Apply the tags, filters, and helpers from a library to the runtime registries.
96
+ */
97
+ function activateLibrary(name) {
98
+ const lib = libraries.get(name);
99
+ if (!lib) {
100
+ throw new Error(`Library not found: '${name}'`);
101
+ }
102
+ // Require lazily; index.js will pass the real registration
103
+ // functions via setRegistrationFunctions() before calling us.
104
+ const { registerTag, registerFilter, registerHelper } = registrationFunctions || require('./index');
105
+ for (const [tagName, parserFn] of Object.entries(lib.tags || {})) {
106
+ registerTag(tagName, parserFn);
107
+ }
108
+ for (const [filterName, filterFn] of Object.entries(lib.filters || {})) {
109
+ registerFilter(filterName, filterFn);
110
+ }
111
+ for (const [helperName, helperFn] of Object.entries(lib.helpers || {})) {
112
+ registerHelper(helperName, helperFn);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Check if a library is registered.
118
+ */
119
+ function hasLibrary(name) {
120
+ return libraries.has(name);
121
+ }
122
+
123
+ // --- Built-in libraries ---
124
+
125
+ /** Humanize: nice-looking text formatting filters. */
126
+ registerLibrary('humanize', {
127
+ filters: {
128
+ intcomma: (val) => {
129
+ const parts = String(val === null || val === undefined ? '' : val).split('.');
130
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
131
+ return parts.join('.');
132
+ },
133
+ intword: (val) => {
134
+ const n = parseFloat(val);
135
+ if (isNaN(n)) return String(val);
136
+ const abs = Math.abs(n);
137
+ if (abs >= 1e9) return (n / 1e9).toFixed(1).replace(/\.0$/, '') + ' billion';
138
+ if (abs >= 1e6) return (n / 1e6).toFixed(1).replace(/\.0$/, '') + ' million';
139
+ if (abs >= 1e3) return (n / 1e3).toFixed(1).replace(/\.0$/, '') + ' thousand';
140
+ return String(n);
141
+ },
142
+ apnumber: (val) => {
143
+ const n = parseInt(val, 10);
144
+ const words = ['zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten',
145
+ 'eleven', 'twelve', 'thirteen', 'fourteen', 'fifteen', 'sixteen', 'seventeen', 'eighteen', 'nineteen'];
146
+ if (!isNaN(n) && n >= 0 && n < words.length) return words[n];
147
+ return String(val);
148
+ },
149
+ ordinal: (val) => {
150
+ const n = parseInt(val, 10);
151
+ if (isNaN(n)) return String(val);
152
+ const s = ['th', 'st', 'nd', 'rd'];
153
+ const v = n % 100;
154
+ return n + (s[(v - 20) % 10] || s[v] || s[0]);
155
+ },
156
+ naturalday: (val) => {
157
+ const d = new Date(val);
158
+ if (isNaN(d.getTime())) return String(val);
159
+ const today = new Date();
160
+ const yesterday = new Date(today);
161
+ yesterday.setDate(today.getDate() - 1);
162
+ const tomorrow = new Date(today);
163
+ tomorrow.setDate(today.getDate() + 1);
164
+ const sameDay = (a, b) =>
165
+ a.getFullYear() === b.getFullYear() &&
166
+ a.getMonth() === b.getMonth() &&
167
+ a.getDate() === b.getDate();
168
+ if (sameDay(d, today)) return 'today';
169
+ if (sameDay(d, yesterday)) return 'yesterday';
170
+ if (sameDay(d, tomorrow)) return 'tomorrow';
171
+ return String(val);
172
+ }
173
+ }
174
+ });
175
+
176
+ /** Cache: cache expensive template fragment results. */
177
+ registerLibrary('cache', {
178
+ tags: {
179
+ cache: (tagContent, parser) => {
180
+ // {% cache timeout key1 key2 ... %}...{% endcache %}
181
+ const rest = tagContent.slice(5).trim();
182
+ const tokens = rest.split(/\s+/);
183
+ const timeout = parseInt(tokens[0], 10) || 0;
184
+ const key = tokens.slice(1).join(':') || 'default';
185
+ const body = parser.parse(['endcache']);
186
+ const next = parser.peek();
187
+ if (next && next.type === 'block' && next.content.split(/\s+/)[0] === 'endcache') {
188
+ parser.advance();
189
+ }
190
+ return {
191
+ render(context) {
192
+ const ctx = context;
193
+ const fullKey = `cache:${key}`;
194
+ if (ctx.cacheStore && ctx.cacheStore.has(fullKey)) {
195
+ const entry = ctx.cacheStore.get(fullKey);
196
+ if (Date.now() - entry.time < timeout * 1000) {
197
+ return entry.value;
198
+ }
199
+ }
200
+ const value = body.map(n => n.render(context)).join('');
201
+ if (ctx.cacheStore) {
202
+ ctx.cacheStore.set(fullKey, { time: Date.now(), value });
203
+ }
204
+ return value;
205
+ }
206
+ };
207
+ }
208
+ }
209
+ });
210
+
211
+ /** Lorem: generate placeholder text (Django-compatible). */
212
+ registerLibrary('lorem', {
213
+ filters: {
214
+ lorem: (val, arg) => {
215
+ let n = parseInt(arg, 10);
216
+ if (isNaN(n)) n = 5;
217
+ if (n < 1) n = 1;
218
+ if (n > 100) n = 100;
219
+ const text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.';
220
+ const words = text.split(' ');
221
+ let result = [];
222
+ for (let i = 0; i < n; i++) {
223
+ result.push(words[i % words.length]);
224
+ }
225
+ return result.join(' ');
226
+ }
227
+ },
228
+ tags: {
229
+ // Django-compatible lorem tag.
230
+ //
231
+ // {% lorem [count] [method] [random] %}
232
+ //
233
+ // count - number of words/paragraphs to generate (default 1)
234
+ // method - w (words), p (HTML paragraphs), b (plain-text blocks, default)
235
+ // random - skip the common "Lorem ipsum dolor sit amet..." opening
236
+ //
237
+ // {% lorem %} -> one block of common Lorem ipsum
238
+ // {% lorem 3 p %} -> common paragraph + 2 random <p> blocks
239
+ // {% lorem 2 w random %} -> 2 random words
240
+ //
241
+ // The tag is built into the engine and requires no {% load lorem %}.
242
+ lorem: (tagContent, _parser) => {
243
+ return {
244
+ render: () => {
245
+ // The tag registry calls the parser with the full content
246
+ // including the tag name. For "{% lorem 3 p random %}" the
247
+ // tagContent is "lorem 3 p random".
248
+ const parts = tagContent.trim().split(/\s+/);
249
+
250
+ // count = first numeric token (or 1 if none)
251
+ let n = 1;
252
+ for (let i = 0; i < parts.length; i++) {
253
+ const num = parseInt(parts[i], 10);
254
+ if (!isNaN(num) && String(num) === parts[i]) { n = Math.max(1, num); break; }
255
+ }
256
+ // method = w | b | p (default b, like Django)
257
+ let unit = 'b';
258
+ for (let i = 0; i < parts.length; i++) {
259
+ if (/^(w|b|p|words?|blocks?|paragraphs?)$/i.test(parts[i])) {
260
+ unit = parts[i][0].toLowerCase();
261
+ break;
262
+ }
263
+ }
264
+ // random = presence of the literal word "random"
265
+ const random = parts.some(t => t.toLowerCase() === 'random');
266
+
267
+ // The famous "common paragraph" Django uses as the seed.
268
+ const COMMON = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.';
269
+ // Word bank used to assemble random text when the "random"
270
+ // keyword is given. Picked from the same Latin vocabulary.
271
+ const WORDS = ('lorem ipsum dolor sit amet consectetur adipisicing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua enim ad minim veniam quis nostrud exercitation ullamco laboris nisi aliquip ex ea commodo consequat duis aute irure in voluptate velit esse cillum fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt culpa qui officia deserunt mollit anim id est laborum').split(' ');
272
+
273
+ // The non-random blocks cycle through a fixed set of
274
+ // sentences taken from the COMMON paragraph above (the
275
+ // first one is the "Lorem ipsum dolor sit amet" line).
276
+ const SENTENCES = [
277
+ 'Lorem ipsum dolor sit amet, consectetur adipisicing elit.',
278
+ 'Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
279
+ 'Ut enim ad minim veniam, quis nostrud exercitation.',
280
+ 'Duis aute irure dolor in reprehenderit in voluptate velit.',
281
+ 'Excepteur sint occaecat cupidatat non proident.'
282
+ ];
283
+
284
+ if (unit === 'w') {
285
+ // Words
286
+ if (random) {
287
+ const out = [];
288
+ for (let i = 0; i < n; i++) {
289
+ out.push(WORDS[Math.floor(Math.random() * WORDS.length)]);
290
+ }
291
+ return out.join(' ');
292
+ }
293
+ // Non-random: first word is always "lorem", the rest
294
+ // continue the common paragraph.
295
+ const commonWords = COMMON.replace(/[.,]/g, '').split(/\s+/);
296
+ const out = [];
297
+ for (let i = 0; i < n; i++) {
298
+ out.push(commonWords[i % commonWords.length]);
299
+ }
300
+ return out.join(' ');
301
+ }
302
+
303
+ if (unit === 'p') {
304
+ // HTML paragraphs (each wrapped in <p>...</p>)
305
+ const out = [];
306
+ for (let i = 0; i < n; i++) {
307
+ let paragraph;
308
+ if (i === 0 && !random) {
309
+ paragraph = COMMON;
310
+ } else if (random) {
311
+ // Generate a sentence worth of random words
312
+ const len = 12 + Math.floor(Math.random() * 16);
313
+ const words = [];
314
+ for (let j = 0; j < len; j++) {
315
+ words.push(WORDS[Math.floor(Math.random() * WORDS.length)]);
316
+ }
317
+ paragraph = words.join(' ') + '.';
318
+ } else {
319
+ paragraph = SENTENCES[(i - 1) % (SENTENCES.length - 1) + 1];
320
+ }
321
+ out.push('<p>' + paragraph + '</p>');
322
+ }
323
+ return out.join('\n');
324
+ }
325
+
326
+ // unit === 'b' (default): plain-text blocks separated by blank lines
327
+ const out = [];
328
+ for (let i = 0; i < n; i++) {
329
+ let paragraph;
330
+ if (i === 0 && !random) {
331
+ paragraph = COMMON;
332
+ } else if (random) {
333
+ const len = 30 + Math.floor(Math.random() * 20);
334
+ const words = [];
335
+ for (let j = 0; j < len; j++) {
336
+ words.push(WORDS[Math.floor(Math.random() * WORDS.length)]);
337
+ }
338
+ paragraph = words.join(' ') + '.';
339
+ } else {
340
+ paragraph = SENTENCES[(i - 1) % (SENTENCES.length - 1) + 1];
341
+ }
342
+ out.push(paragraph);
343
+ }
344
+ return out.join('\n\n');
345
+ }
346
+ };
347
+ }
348
+ }
349
+ });
350
+
351
+ /** Static: additional static-file helpers. */
352
+ registerLibrary('static', {
353
+ filters: {
354
+ static: (val) => {
355
+ // Returns the value prefixed with the staticUrl (configured at compile time)
356
+ return val; // Implementation lives in the {% static %} tag
357
+ }
358
+ }
359
+ });
360
+
361
+ module.exports = {
362
+ registerLibrary,
363
+ unregisterLibrary,
364
+ getLibrary,
365
+ getLibraryNames,
366
+ hasLibrary,
367
+ registerLibraryFromPath,
368
+ activateLibrary,
369
+ setRegistrationFunctions,
370
+ libraries
371
+ };