@dogsbay/minja 0.2.0-beta.48

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 (52) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +603 -0
  3. package/bin/minja.js +1062 -0
  4. package/dist/browser.d.ts +11 -0
  5. package/dist/browser.d.ts.map +1 -0
  6. package/dist/browser.js +10 -0
  7. package/dist/browser.js.map +1 -0
  8. package/dist/cli.d.ts +5 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +97 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/context.d.ts +47 -0
  13. package/dist/context.d.ts.map +1 -0
  14. package/dist/context.js +112 -0
  15. package/dist/context.js.map +1 -0
  16. package/dist/evaluator.d.ts +20 -0
  17. package/dist/evaluator.d.ts.map +1 -0
  18. package/dist/evaluator.js +207 -0
  19. package/dist/evaluator.js.map +1 -0
  20. package/dist/index.d.ts +18 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +17 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/index.umd.js +1026 -0
  25. package/dist/index.umd.js.map +7 -0
  26. package/dist/index.umd.min.js +9 -0
  27. package/dist/index.umd.min.js.map +7 -0
  28. package/dist/loader-fetch.d.ts +8 -0
  29. package/dist/loader-fetch.d.ts.map +1 -0
  30. package/dist/loader-fetch.js +15 -0
  31. package/dist/loader-fetch.js.map +1 -0
  32. package/dist/loader-memory.d.ts +11 -0
  33. package/dist/loader-memory.d.ts.map +1 -0
  34. package/dist/loader-memory.js +36 -0
  35. package/dist/loader-memory.js.map +1 -0
  36. package/dist/loader.d.ts +54 -0
  37. package/dist/loader.d.ts.map +1 -0
  38. package/dist/loader.js +91 -0
  39. package/dist/loader.js.map +1 -0
  40. package/dist/parser.d.ts +12 -0
  41. package/dist/parser.d.ts.map +1 -0
  42. package/dist/parser.js +501 -0
  43. package/dist/parser.js.map +1 -0
  44. package/dist/renderer.d.ts +13 -0
  45. package/dist/renderer.d.ts.map +1 -0
  46. package/dist/renderer.js +415 -0
  47. package/dist/renderer.js.map +1 -0
  48. package/dist/types.d.ts +150 -0
  49. package/dist/types.d.ts.map +1 -0
  50. package/dist/types.js +5 -0
  51. package/dist/types.js.map +1 -0
  52. package/package.json +62 -0
package/bin/minja.js ADDED
@@ -0,0 +1,1062 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * minja v0.2.0-beta.48
4
+ * Minimal, secure Jinja2/Nunjucks subset for documentation preprocessing
5
+ * @license MIT
6
+ */
7
+
8
+ // src/cli.ts
9
+ import { readFile, writeFile } from "fs/promises";
10
+ import { resolve, dirname } from "path";
11
+ import { Command } from "commander";
12
+ import { parse as parseYAML } from "yaml";
13
+
14
+ // src/evaluator.ts
15
+ function parseExpression(expr) {
16
+ expr = expr.trim();
17
+ if (expr === "true") {
18
+ return { type: "literal", value: true };
19
+ }
20
+ if (expr === "false") {
21
+ return { type: "literal", value: false };
22
+ }
23
+ if (expr === "null") {
24
+ return { type: "literal", value: null };
25
+ }
26
+ if (expr.startsWith('"') && expr.endsWith('"')) {
27
+ return { type: "literal", value: expr.slice(1, -1) };
28
+ }
29
+ if (expr.startsWith("'") && expr.endsWith("'")) {
30
+ return { type: "literal", value: expr.slice(1, -1) };
31
+ }
32
+ if (/^-?\d+(\.\d+)?$/.test(expr)) {
33
+ return { type: "literal", value: parseFloat(expr) };
34
+ }
35
+ if (expr.startsWith("(") && expr.endsWith(")")) {
36
+ let depth = 0;
37
+ let balanced = true;
38
+ for (let i = 0; i < expr.length; i++) {
39
+ if (expr[i] === "(") {
40
+ depth++;
41
+ } else if (expr[i] === ")") {
42
+ depth--;
43
+ }
44
+ if (depth === 0 && i < expr.length - 1) {
45
+ balanced = false;
46
+ break;
47
+ }
48
+ }
49
+ if (balanced && depth === 0) {
50
+ return parseExpression(expr.slice(1, -1));
51
+ }
52
+ }
53
+ if (expr.startsWith("not ")) {
54
+ return {
55
+ type: "unary",
56
+ operator: "not",
57
+ operand: parseExpression(expr.substring(4))
58
+ };
59
+ }
60
+ const orMatch = findOperator(expr, " or ");
61
+ if (orMatch !== -1) {
62
+ return {
63
+ type: "binary",
64
+ operator: "or",
65
+ left: parseExpression(expr.substring(0, orMatch)),
66
+ right: parseExpression(expr.substring(orMatch + 4))
67
+ };
68
+ }
69
+ const andMatch = findOperator(expr, " and ");
70
+ if (andMatch !== -1) {
71
+ return {
72
+ type: "binary",
73
+ operator: "and",
74
+ left: parseExpression(expr.substring(0, andMatch)),
75
+ right: parseExpression(expr.substring(andMatch + 5))
76
+ };
77
+ }
78
+ const comparisonOps = ["==", "!=", "<=", ">=", "<", ">"];
79
+ for (const op of comparisonOps) {
80
+ const opMatch = findOperator(expr, ` ${op} `);
81
+ if (opMatch !== -1) {
82
+ return {
83
+ type: "binary",
84
+ operator: op,
85
+ left: parseExpression(expr.substring(0, opMatch)),
86
+ right: parseExpression(expr.substring(opMatch + op.length + 2))
87
+ };
88
+ }
89
+ }
90
+ if (/^[\w.]+$/.test(expr)) {
91
+ return { type: "variable", name: expr };
92
+ }
93
+ throw new Error(`Invalid expression: ${expr}`);
94
+ }
95
+ function findOperator(expr, operator) {
96
+ let inString = null;
97
+ let depth = 0;
98
+ for (let i = 0; i < expr.length; i++) {
99
+ const char = expr[i];
100
+ if ((char === '"' || char === "'") && (i === 0 || expr[i - 1] !== "\\")) {
101
+ if (inString === char) {
102
+ inString = null;
103
+ } else if (inString === null) {
104
+ inString = char;
105
+ }
106
+ continue;
107
+ }
108
+ if (!inString) {
109
+ if (char === "(") {
110
+ depth++;
111
+ } else if (char === ")") {
112
+ depth--;
113
+ }
114
+ }
115
+ if (!inString && depth === 0) {
116
+ if (expr.substring(i, i + operator.length) === operator) {
117
+ return i;
118
+ }
119
+ }
120
+ }
121
+ return -1;
122
+ }
123
+ function evaluateExpression(expr, context) {
124
+ switch (expr.type) {
125
+ case "literal":
126
+ return expr.value;
127
+ case "variable":
128
+ return context.get(expr.name);
129
+ case "binary": {
130
+ const left = evaluateExpression(expr.left, context);
131
+ const right = evaluateExpression(expr.right, context);
132
+ switch (expr.operator) {
133
+ case "==":
134
+ return left == right;
135
+ case "!=":
136
+ return left != right;
137
+ case "<":
138
+ return left < right;
139
+ case ">":
140
+ return left > right;
141
+ case "<=":
142
+ return left <= right;
143
+ case ">=":
144
+ return left >= right;
145
+ case "and":
146
+ return isTruthy(left) && isTruthy(right);
147
+ case "or":
148
+ return isTruthy(left) || isTruthy(right);
149
+ }
150
+ break;
151
+ }
152
+ case "unary": {
153
+ const operand = evaluateExpression(expr.operand, context);
154
+ switch (expr.operator) {
155
+ case "not":
156
+ return !isTruthy(operand);
157
+ }
158
+ break;
159
+ }
160
+ }
161
+ return void 0;
162
+ }
163
+ function isTruthy(value) {
164
+ if (value === void 0 || value === null || value === false) {
165
+ return false;
166
+ }
167
+ if (value === 0 || value === "") {
168
+ return false;
169
+ }
170
+ return true;
171
+ }
172
+
173
+ // src/parser.ts
174
+ function parse(template) {
175
+ const errors = [];
176
+ const ast = [];
177
+ let position = 0;
178
+ let line = 1;
179
+ let column = 1;
180
+ function createError(message) {
181
+ return { message, position, line, column };
182
+ }
183
+ function advance(count) {
184
+ for (let i = 0; i < count; i++) {
185
+ if (template[position + i] === "\n") {
186
+ line++;
187
+ column = 1;
188
+ } else {
189
+ column++;
190
+ }
191
+ }
192
+ position += count;
193
+ }
194
+ function findNext(str, from = position) {
195
+ return template.indexOf(str, from);
196
+ }
197
+ function extractTo(target) {
198
+ const text = template.substring(position, target);
199
+ advance(target - position);
200
+ return text;
201
+ }
202
+ while (position < template.length) {
203
+ const varStart = findNext("{{", position);
204
+ const tagStart = findNext("{%", position);
205
+ const commentStart = findNext("{#", position);
206
+ const candidates = [
207
+ { pos: varStart, type: "var" },
208
+ { pos: tagStart, type: "tag" },
209
+ { pos: commentStart, type: "comment" }
210
+ ].filter((c) => c.pos !== -1);
211
+ if (candidates.length === 0) {
212
+ const text = template.substring(position);
213
+ if (text) {
214
+ ast.push({ type: "text", value: text });
215
+ }
216
+ break;
217
+ }
218
+ candidates.sort((a, b) => a.pos - b.pos);
219
+ const nearest = candidates[0];
220
+ if (nearest.pos > position) {
221
+ ast.push({ type: "text", value: extractTo(nearest.pos) });
222
+ }
223
+ if (nearest.type === "var") {
224
+ advance(2);
225
+ const endPos = findNext("}}", position);
226
+ if (endPos === -1) {
227
+ errors.push(createError("Unclosed variable tag"));
228
+ break;
229
+ }
230
+ const varName = extractTo(endPos).trim();
231
+ advance(2);
232
+ ast.push({ type: "variable", name: varName });
233
+ } else if (nearest.type === "comment") {
234
+ if (ast.length > 0 && ast[ast.length - 1].type === "text") {
235
+ const lastNode = ast[ast.length - 1];
236
+ if (/\n[ \t]*$/.test(lastNode.value)) {
237
+ lastNode.value = lastNode.value.replace(/[ \t]*\n[ \t]*$/, "");
238
+ }
239
+ }
240
+ advance(2);
241
+ const endPos = findNext("#}", position);
242
+ if (endPos === -1) {
243
+ errors.push(createError("Unclosed comment tag"));
244
+ break;
245
+ }
246
+ const commentText = extractTo(endPos);
247
+ advance(2);
248
+ ast.push({ type: "comment", value: commentText });
249
+ if (position < template.length && template[position] === "\n") {
250
+ advance(1);
251
+ while (position < template.length && /[ \t]/.test(template[position])) {
252
+ advance(1);
253
+ }
254
+ }
255
+ } else if (nearest.type === "tag") {
256
+ advance(2);
257
+ const endPos = findNext("%}", position);
258
+ if (endPos === -1) {
259
+ errors.push(createError("Unclosed statement tag"));
260
+ break;
261
+ }
262
+ let statement = extractTo(endPos).trim();
263
+ const hasLeftStrip = statement.startsWith("-");
264
+ const hasRightStrip = statement.endsWith("-");
265
+ if (hasLeftStrip) {
266
+ statement = statement.substring(1).trim();
267
+ if (ast.length > 0 && ast[ast.length - 1].type === "text") {
268
+ const lastNode = ast[ast.length - 1];
269
+ lastNode.value = lastNode.value.replace(/[ \t]*\n[ \t\n]*$/, "");
270
+ }
271
+ }
272
+ if (hasRightStrip) {
273
+ statement = statement.substring(0, statement.length - 1).trim();
274
+ }
275
+ advance(2);
276
+ if (hasRightStrip) {
277
+ while (position < template.length && /[ \t\n]/.test(template[position])) {
278
+ advance(1);
279
+ }
280
+ }
281
+ if (statement.startsWith("set ")) {
282
+ const setMatch = /^set\s+(\w+)\s*=\s*(.+)$/.exec(statement);
283
+ if (setMatch) {
284
+ try {
285
+ const expr = parseExpression(setMatch[2]);
286
+ ast.push({
287
+ type: "set",
288
+ name: setMatch[1],
289
+ value: expr
290
+ });
291
+ } catch (error) {
292
+ errors.push(createError(`Invalid set expression: ${error.message}`));
293
+ }
294
+ } else {
295
+ errors.push(createError("Invalid set syntax"));
296
+ }
297
+ } else if (statement.startsWith("if ")) {
298
+ const condition = statement.substring(3).trim();
299
+ try {
300
+ const expr = parseExpression(condition);
301
+ const { trueBranch, elifBranches, elseBranch, parseErrors } = parseIfBlock();
302
+ errors.push(...parseErrors);
303
+ ast.push({
304
+ type: "if",
305
+ condition: expr,
306
+ trueBranch,
307
+ elifBranches: elifBranches.length > 0 ? elifBranches : void 0,
308
+ elseBranch: elseBranch.length > 0 ? elseBranch : void 0
309
+ });
310
+ } catch (error) {
311
+ errors.push(createError(`Invalid if expression: ${error.message}`));
312
+ }
313
+ } else if (statement.startsWith("include ")) {
314
+ const includeMatch = /^include\s+["']([^"']+)["']/.exec(statement);
315
+ if (includeMatch) {
316
+ ast.push({
317
+ type: "include",
318
+ path: includeMatch[1]
319
+ });
320
+ } else {
321
+ errors.push(createError("Invalid include syntax"));
322
+ }
323
+ } else if (statement.startsWith("switch ")) {
324
+ const switchExpr = statement.substring(7).trim();
325
+ try {
326
+ const expr = parseExpression(switchExpr);
327
+ const { cases, parseErrors } = parseSwitchBlock();
328
+ errors.push(...parseErrors);
329
+ ast.push({
330
+ type: "switch",
331
+ expression: expr,
332
+ cases
333
+ });
334
+ } catch (error) {
335
+ errors.push(createError(`Invalid switch expression: ${error.message}`));
336
+ }
337
+ } else if (statement.startsWith("leveloffset ")) {
338
+ const offsetMatch = /^leveloffset\s+([-+]?\d+)$/.exec(statement);
339
+ if (!offsetMatch) {
340
+ errors.push(createError("Invalid leveloffset syntax"));
341
+ } else {
342
+ const offsetStr = offsetMatch[1];
343
+ const isRelative = offsetStr.startsWith("+") || offsetStr.startsWith("-");
344
+ const offset = parseInt(offsetStr, 10);
345
+ if (isNaN(offset)) {
346
+ errors.push(createError("Invalid leveloffset value"));
347
+ } else {
348
+ let depth = 0;
349
+ let searchPos = position;
350
+ let endPos2 = -1;
351
+ while (searchPos < template.length) {
352
+ const leveloffsetMatch = /{%-?\s*leveloffset\s+[-+]?\d+\s*-?%}/.exec(template.substring(searchPos));
353
+ const endleveloffsetMatch = /{%-?\s*endleveloffset\s*-?%}/.exec(template.substring(searchPos));
354
+ const leveloffsetPos = leveloffsetMatch ? searchPos + leveloffsetMatch.index : Infinity;
355
+ const endleveloffsetPos = endleveloffsetMatch ? searchPos + endleveloffsetMatch.index : Infinity;
356
+ if (leveloffsetPos < endleveloffsetPos) {
357
+ depth++;
358
+ searchPos = leveloffsetPos + (leveloffsetMatch?.[0].length || 0);
359
+ } else if (endleveloffsetPos < Infinity) {
360
+ if (depth === 0) {
361
+ endPos2 = endleveloffsetPos;
362
+ break;
363
+ } else {
364
+ depth--;
365
+ searchPos = endleveloffsetPos + (endleveloffsetMatch?.[0].length || 0);
366
+ }
367
+ } else {
368
+ break;
369
+ }
370
+ }
371
+ if (endPos2 === -1) {
372
+ errors.push(createError("Missing endleveloffset"));
373
+ } else {
374
+ const bodyTemplate = extractTo(endPos2);
375
+ const endMatch = /{%-?\s*endleveloffset\s*-?%}/.exec(template.substring(position));
376
+ if (endMatch) {
377
+ advance(endMatch[0].length);
378
+ }
379
+ const bodyResult = parse(bodyTemplate);
380
+ errors.push(...bodyResult.errors);
381
+ ast.push({
382
+ type: "leveloffset",
383
+ offset,
384
+ isRelative,
385
+ body: bodyResult.ast
386
+ });
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
392
+ }
393
+ return { ast, errors };
394
+ function findEndTag(tagName) {
395
+ const pattern = new RegExp(`{%\\s*${tagName}\\s*%}`);
396
+ const match = pattern.exec(template.substring(position));
397
+ if (match) {
398
+ return {
399
+ start: position + match.index,
400
+ length: match[0].length
401
+ };
402
+ }
403
+ return null;
404
+ }
405
+ function parseIfBlock() {
406
+ const elifBranches = [];
407
+ const parseErrors = [];
408
+ let elseBranch = [];
409
+ let depth = 0;
410
+ let searchPos = position;
411
+ while (searchPos < template.length) {
412
+ const ifMatch = /{%-?\s*if\s+/.exec(template.substring(searchPos));
413
+ const elifMatch = /{%-?\s*elif\s+/.exec(template.substring(searchPos));
414
+ const elseMatch = /{%-?\s*else\s*-?%}/.exec(template.substring(searchPos));
415
+ const endifMatch = /{%-?\s*endif\s*-?%}/.exec(template.substring(searchPos));
416
+ const matches = [
417
+ { type: "if", match: ifMatch, pos: ifMatch ? searchPos + ifMatch.index : Infinity },
418
+ { type: "elif", match: elifMatch, pos: elifMatch ? searchPos + elifMatch.index : Infinity },
419
+ { type: "else", match: elseMatch, pos: elseMatch ? searchPos + elseMatch.index : Infinity },
420
+ { type: "endif", match: endifMatch, pos: endifMatch ? searchPos + endifMatch.index : Infinity }
421
+ ].sort((a, b) => a.pos - b.pos);
422
+ const nearest = matches[0];
423
+ if (!nearest.match || nearest.pos === Infinity) {
424
+ parseErrors.push(createError("Missing endif"));
425
+ break;
426
+ }
427
+ if (nearest.type === "if") {
428
+ depth++;
429
+ searchPos = nearest.pos + nearest.match[0].length;
430
+ } else if (nearest.type === "endif") {
431
+ if (depth === 0) {
432
+ const bodyTemplate = extractTo(nearest.pos);
433
+ advance(nearest.match[0].length);
434
+ const bodyResult = parse(bodyTemplate);
435
+ parseErrors.push(...bodyResult.errors);
436
+ return {
437
+ trueBranch: bodyResult.ast,
438
+ elifBranches,
439
+ elseBranch,
440
+ parseErrors
441
+ };
442
+ } else {
443
+ depth--;
444
+ searchPos = nearest.pos + nearest.match[0].length;
445
+ }
446
+ } else if (depth === 0 && (nearest.type === "elif" || nearest.type === "else")) {
447
+ const bodyTemplate = extractTo(nearest.pos);
448
+ const bodyResult = parse(bodyTemplate);
449
+ if (elifBranches.length === 0 && elseBranch.length === 0) {
450
+ parseErrors.push(...bodyResult.errors);
451
+ const trueBranch = bodyResult.ast;
452
+ if (nearest.type === "elif") {
453
+ advance(nearest.match[0].length);
454
+ const condMatch = /^([^%]+)%}/.exec(template.substring(position));
455
+ if (condMatch) {
456
+ const condStr = condMatch[1].trim();
457
+ advance(condMatch[0].length);
458
+ try {
459
+ const elifCondition = parseExpression(condStr);
460
+ const elifResult = parseIfBlock();
461
+ parseErrors.push(...elifResult.parseErrors);
462
+ elifBranches.push({
463
+ condition: elifCondition,
464
+ body: elifResult.trueBranch
465
+ });
466
+ elifBranches.push(...elifResult.elifBranches || []);
467
+ elseBranch = elifResult.elseBranch;
468
+ return {
469
+ trueBranch,
470
+ elifBranches,
471
+ elseBranch,
472
+ parseErrors
473
+ };
474
+ } catch (error) {
475
+ parseErrors.push(createError(`Invalid elif expression: ${error.message}`));
476
+ }
477
+ }
478
+ } else {
479
+ advance(nearest.match[0].length);
480
+ const endifMatch2 = findEndTag("endif");
481
+ if (!endifMatch2) {
482
+ parseErrors.push(createError("Missing endif after else"));
483
+ } else {
484
+ const elseBodyTemplate = extractTo(endifMatch2.start);
485
+ advance(endifMatch2.length);
486
+ const elseBodyResult = parse(elseBodyTemplate);
487
+ parseErrors.push(...elseBodyResult.errors);
488
+ elseBranch = elseBodyResult.ast;
489
+ }
490
+ return {
491
+ trueBranch,
492
+ elifBranches,
493
+ elseBranch,
494
+ parseErrors
495
+ };
496
+ }
497
+ }
498
+ } else {
499
+ searchPos = nearest.pos + nearest.match[0].length;
500
+ }
501
+ }
502
+ return {
503
+ trueBranch: [],
504
+ elifBranches,
505
+ elseBranch,
506
+ parseErrors
507
+ };
508
+ }
509
+ function parseSwitchBlock() {
510
+ const cases = [];
511
+ const parseErrors = [];
512
+ while (position < template.length) {
513
+ const caseMatch = /{%-?\s*case\s+/.exec(template.substring(position));
514
+ const endswitchMatch = /{%-?\s*endswitch\s*-?%}/.exec(template.substring(position));
515
+ const casePos = caseMatch ? position + caseMatch.index : Infinity;
516
+ const endswitchPos = endswitchMatch ? position + endswitchMatch.index : Infinity;
517
+ if (endswitchPos < casePos) {
518
+ if (cases.length > 0 && position < endswitchPos) {
519
+ const bodyTemplate = extractTo(endswitchPos);
520
+ const bodyResult = parse(bodyTemplate);
521
+ parseErrors.push(...bodyResult.errors);
522
+ cases[cases.length - 1].body = bodyResult.ast;
523
+ }
524
+ advance(endswitchPos - position + (endswitchMatch?.[0].length || 0));
525
+ break;
526
+ } else if (casePos < Infinity) {
527
+ if (cases.length > 0 && position < casePos) {
528
+ const bodyTemplate = extractTo(casePos);
529
+ const bodyResult = parse(bodyTemplate);
530
+ parseErrors.push(...bodyResult.errors);
531
+ cases[cases.length - 1].body = bodyResult.ast;
532
+ } else if (position < casePos) {
533
+ extractTo(casePos);
534
+ }
535
+ advance(caseMatch[0].length);
536
+ const valueMatch = /([^%]+)%}/.exec(template.substring(position));
537
+ if (valueMatch) {
538
+ const valueStr = valueMatch[1].trim();
539
+ advance(valueMatch[0].length);
540
+ try {
541
+ const caseValue = parseExpression(valueStr);
542
+ cases.push({
543
+ value: caseValue,
544
+ body: []
545
+ // Will be filled in next iteration or at endswitch
546
+ });
547
+ } catch (error) {
548
+ parseErrors.push(createError(`Invalid case value: ${error.message}`));
549
+ }
550
+ } else {
551
+ parseErrors.push(createError("Invalid case syntax"));
552
+ break;
553
+ }
554
+ } else {
555
+ parseErrors.push(createError("Missing endswitch"));
556
+ break;
557
+ }
558
+ }
559
+ return { cases, parseErrors };
560
+ }
561
+ }
562
+
563
+ // src/context.ts
564
+ var Context = class _Context {
565
+ constructor(parent, options) {
566
+ this.variables = /* @__PURE__ */ new Map();
567
+ this.parent = parent || null;
568
+ this.options = options || parent?.options || {};
569
+ }
570
+ /**
571
+ * Get a variable from the context (supports dot notation)
572
+ * @param name - Variable name (can use dot notation like "obj.prop.subprop")
573
+ * @returns The variable value or undefined
574
+ */
575
+ get(name) {
576
+ const normalizedName = this.options.hyphenToUnderscore ? name.replace(/-/g, "_") : name;
577
+ const parts = normalizedName.split(".");
578
+ const rootName = parts[0];
579
+ let value;
580
+ if (this.variables.has(rootName)) {
581
+ value = this.variables.get(rootName);
582
+ } else if (this.parent) {
583
+ value = this.parent.get(rootName);
584
+ } else {
585
+ return void 0;
586
+ }
587
+ for (let i = 1; i < parts.length; i++) {
588
+ if (value === null || value === void 0) {
589
+ return void 0;
590
+ }
591
+ if (typeof value !== "object" || Array.isArray(value)) {
592
+ return void 0;
593
+ }
594
+ if (!Object.prototype.hasOwnProperty.call(value, parts[i])) {
595
+ return void 0;
596
+ }
597
+ value = value[parts[i]];
598
+ }
599
+ return value;
600
+ }
601
+ /**
602
+ * Set a variable in the current context
603
+ * @param name - Variable name (can use dot notation)
604
+ * @param value - Value to set
605
+ */
606
+ set(name, value) {
607
+ const parts = name.split(".");
608
+ if (parts.length === 1) {
609
+ this.variables.set(name, value);
610
+ return;
611
+ }
612
+ const rootName = parts[0];
613
+ let target = this.variables.get(rootName);
614
+ if (!target || typeof target !== "object" || Array.isArray(target)) {
615
+ target = {};
616
+ this.variables.set(rootName, target);
617
+ }
618
+ for (let i = 1; i < parts.length - 1; i++) {
619
+ const key = parts[i];
620
+ if (!target[key] || typeof target[key] !== "object" || Array.isArray(target[key])) {
621
+ target[key] = {};
622
+ }
623
+ target = target[key];
624
+ }
625
+ target[parts[parts.length - 1]] = value;
626
+ }
627
+ /**
628
+ * Create a child scope
629
+ * @returns A new Context with this context as parent
630
+ */
631
+ push() {
632
+ return new _Context(this, this.options);
633
+ }
634
+ /**
635
+ * Return to parent scope
636
+ * @returns The parent context or null if at root
637
+ */
638
+ pop() {
639
+ return this.parent;
640
+ }
641
+ /**
642
+ * Create a context from a plain object
643
+ * @param data - Plain object to convert to context
644
+ * @param options - Context options
645
+ * @returns New Context instance
646
+ */
647
+ static from(data, options) {
648
+ const ctx = new _Context(void 0, options);
649
+ for (const [key, value] of Object.entries(data)) {
650
+ ctx.set(key, value);
651
+ }
652
+ return ctx;
653
+ }
654
+ };
655
+
656
+ // src/loader-fetch.ts
657
+ var FetchLoader = class {
658
+ async load(path, basePath) {
659
+ const url = basePath ? new URL(path, basePath).href : path;
660
+ const fetchUrl = `${url}?preventCache=${Date.now()}`;
661
+ const response = await fetch(fetchUrl);
662
+ if (!response.ok) {
663
+ throw new Error(`Failed to load ${url}: ${response.status} ${response.statusText}`);
664
+ }
665
+ return await response.text();
666
+ }
667
+ };
668
+
669
+ // src/renderer.ts
670
+ function transformHeadings(text, offset) {
671
+ if (offset === 0) {
672
+ return text;
673
+ }
674
+ return text.replace(/^(#{1,6})(\s+)/gm, (_match, hashes, space) => {
675
+ const currentLevel = hashes.length;
676
+ const newLevel = Math.max(1, Math.min(6, currentLevel + offset));
677
+ return "#".repeat(newLevel) + space;
678
+ });
679
+ }
680
+ function resolvePath(path, basePath) {
681
+ if (path.startsWith("/")) {
682
+ return path;
683
+ }
684
+ const base = basePath.endsWith("/") ? basePath : basePath + "/";
685
+ const combined = base + path;
686
+ const parts = combined.split("/");
687
+ const resolved = [];
688
+ for (const part of parts) {
689
+ if (part === "." || part === "") {
690
+ continue;
691
+ } else if (part === "..") {
692
+ resolved.pop();
693
+ } else {
694
+ resolved.push(part);
695
+ }
696
+ }
697
+ const prefix = combined.startsWith("/") ? "/" : "";
698
+ return prefix + resolved.join("/");
699
+ }
700
+ async function render(template, options = {}) {
701
+ const {
702
+ loader = new FetchLoader(),
703
+ context: initialContext = {},
704
+ basePath = "",
705
+ maxIncludeDepth = 10,
706
+ timeout = 5e3,
707
+ hyphenToUnderscore = false,
708
+ undefinedBehavior = "empty"
709
+ } = options;
710
+ const timeoutPromise = new Promise((_, reject) => {
711
+ setTimeout(() => reject(new Error("Template rendering timeout")), timeout);
712
+ });
713
+ const renderPromise = renderInternal(template, {
714
+ loader,
715
+ context: Context.from(
716
+ {
717
+ ...initialContext,
718
+ // Built-in variables
719
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
720
+ timestamp: Date.now(),
721
+ _levelOffset: 0
722
+ },
723
+ { hyphenToUnderscore }
724
+ ),
725
+ includeDepth: 0,
726
+ maxIncludeDepth,
727
+ basePath,
728
+ undefinedBehavior
729
+ });
730
+ return await Promise.race([renderPromise, timeoutPromise]);
731
+ }
732
+ async function renderInternal(template, options) {
733
+ const { includeDepth, maxIncludeDepth } = options;
734
+ if (includeDepth > maxIncludeDepth) {
735
+ throw new Error(`Maximum include depth (${maxIncludeDepth}) exceeded`);
736
+ }
737
+ const parseResult = parse(template);
738
+ if (parseResult.errors.length > 0) {
739
+ const errorMessages = parseResult.errors.map((e) => e.message).join(", ");
740
+ throw new Error(`Parse errors: ${errorMessages}`);
741
+ }
742
+ return await renderNodes(parseResult.ast, options);
743
+ }
744
+ async function renderNodes(nodes, options) {
745
+ const parts = [];
746
+ for (const node of nodes) {
747
+ parts.push(await renderNode(node, options));
748
+ }
749
+ return parts.join("");
750
+ }
751
+ async function renderNode(node, options) {
752
+ const { loader, context, includeDepth, maxIncludeDepth, basePath } = options;
753
+ switch (node.type) {
754
+ case "text": {
755
+ const currentOffset = context.get("_levelOffset") || 0;
756
+ if (currentOffset === 0) {
757
+ return node.value;
758
+ }
759
+ return transformHeadings(node.value, currentOffset);
760
+ }
761
+ case "variable": {
762
+ const value = context.get(node.name);
763
+ if (value === void 0 || value === null) {
764
+ switch (options.undefinedBehavior) {
765
+ case "throw":
766
+ throw new Error(`Undefined variable: ${node.name}`);
767
+ case "preserve":
768
+ return `{{ ${node.name} }}`;
769
+ case "empty":
770
+ default:
771
+ return "";
772
+ }
773
+ }
774
+ const str = String(value);
775
+ if (str.includes("{{") || str.includes("{%")) {
776
+ return await renderInternal(str, options);
777
+ }
778
+ return str;
779
+ }
780
+ case "set": {
781
+ const value = evaluateExpression(node.value, context);
782
+ context.set(node.name, value);
783
+ return "";
784
+ }
785
+ case "comment":
786
+ return "";
787
+ case "if": {
788
+ if (options.undefinedBehavior === "preserve") {
789
+ const undefinedRefs = findUndefinedRefs(node.condition, context);
790
+ if (undefinedRefs.length > 0) {
791
+ return ifNodeToSource(node);
792
+ }
793
+ if (node.elifBranches) {
794
+ for (const elif of node.elifBranches) {
795
+ if (findUndefinedRefs(elif.condition, context).length > 0) {
796
+ return ifNodeToSource(node);
797
+ }
798
+ }
799
+ }
800
+ }
801
+ const condition = evaluateExpression(node.condition, context);
802
+ if (isTruthy2(condition)) {
803
+ return await renderNodes(node.trueBranch, options);
804
+ }
805
+ if (node.elifBranches) {
806
+ for (const elifBranch of node.elifBranches) {
807
+ const elifCondition = evaluateExpression(elifBranch.condition, context);
808
+ if (isTruthy2(elifCondition)) {
809
+ return await renderNodes(elifBranch.body, options);
810
+ }
811
+ }
812
+ }
813
+ if (node.elseBranch) {
814
+ return await renderNodes(node.elseBranch, options);
815
+ }
816
+ return "";
817
+ }
818
+ case "include": {
819
+ try {
820
+ const includedContent = await loader.load(node.path, basePath);
821
+ let newBasePath = basePath;
822
+ if (node.path.includes("://")) {
823
+ const url = new URL(node.path);
824
+ newBasePath = url.href.substring(0, url.href.lastIndexOf("/") + 1);
825
+ } else if (basePath) {
826
+ if (basePath.includes("://")) {
827
+ const url = new URL(node.path, basePath);
828
+ newBasePath = url.href.substring(0, url.href.lastIndexOf("/") + 1);
829
+ } else {
830
+ const resolvedPath = resolvePath(node.path, basePath);
831
+ const lastSlash = resolvedPath.lastIndexOf("/");
832
+ newBasePath = lastSlash >= 0 ? resolvedPath.substring(0, lastSlash + 1) : basePath;
833
+ }
834
+ } else if (node.path.includes("/")) {
835
+ const lastSlash = node.path.lastIndexOf("/");
836
+ newBasePath = node.path.substring(0, lastSlash + 1);
837
+ }
838
+ return await renderInternal(includedContent, {
839
+ loader,
840
+ context,
841
+ includeDepth: includeDepth + 1,
842
+ maxIncludeDepth,
843
+ basePath: newBasePath,
844
+ undefinedBehavior: options.undefinedBehavior
845
+ });
846
+ } catch (error) {
847
+ if (options.undefinedBehavior === "preserve") {
848
+ return `{% include "${node.path}" %}`;
849
+ }
850
+ const message = error instanceof Error ? error.message : String(error);
851
+ console.error(`Failed to include ${node.path}:`, message);
852
+ return `<!-- Include error: ${node.path} -->`;
853
+ }
854
+ }
855
+ case "switch": {
856
+ const switchValue = evaluateExpression(node.expression, context);
857
+ for (const caseItem of node.cases) {
858
+ const caseValue = evaluateExpression(caseItem.value, context);
859
+ if (switchValue == caseValue) {
860
+ return await renderNodes(caseItem.body, options);
861
+ }
862
+ }
863
+ return "";
864
+ }
865
+ case "leveloffset": {
866
+ const parentOffset = context.get("_levelOffset") || 0;
867
+ const newOffset = node.isRelative ? parentOffset + node.offset : node.offset;
868
+ const previousOffset = parentOffset;
869
+ context.set("_levelOffset", newOffset);
870
+ try {
871
+ const result = await renderNodes(node.body, options);
872
+ return result;
873
+ } finally {
874
+ context.set("_levelOffset", previousOffset);
875
+ }
876
+ }
877
+ default:
878
+ const _exhaustive = node;
879
+ return _exhaustive;
880
+ }
881
+ }
882
+ function isTruthy2(value) {
883
+ if (value === void 0 || value === null || value === false) {
884
+ return false;
885
+ }
886
+ if (value === 0 || value === "") {
887
+ return false;
888
+ }
889
+ return true;
890
+ }
891
+ function findUndefinedRefs(expr, context) {
892
+ const out = [];
893
+ function walk(e) {
894
+ switch (e.type) {
895
+ case "variable":
896
+ if (context.get(e.name) === void 0)
897
+ out.push(e.name);
898
+ break;
899
+ case "binary":
900
+ walk(e.left);
901
+ walk(e.right);
902
+ break;
903
+ case "unary":
904
+ walk(e.operand);
905
+ break;
906
+ case "literal":
907
+ break;
908
+ }
909
+ }
910
+ walk(expr);
911
+ return out;
912
+ }
913
+ function expressionToSource(expr) {
914
+ switch (expr.type) {
915
+ case "literal":
916
+ if (typeof expr.value === "string")
917
+ return JSON.stringify(expr.value);
918
+ if (expr.value === null)
919
+ return "null";
920
+ return String(expr.value);
921
+ case "variable":
922
+ return expr.name;
923
+ case "unary":
924
+ return `not ${expressionToSource(expr.operand)}`;
925
+ case "binary":
926
+ return `(${expressionToSource(expr.left)} ${expr.operator} ${expressionToSource(expr.right)})`;
927
+ }
928
+ }
929
+ function nodesToSource(nodes) {
930
+ return nodes.map(nodeToSource).join("");
931
+ }
932
+ function nodeToSource(node) {
933
+ switch (node.type) {
934
+ case "text":
935
+ return node.value;
936
+ case "variable":
937
+ return `{{ ${node.name} }}`;
938
+ case "set":
939
+ return `{% set ${node.name} = ${expressionToSource(node.value)} %}`;
940
+ case "comment":
941
+ return "{# \u2026 #}";
942
+ case "if":
943
+ return ifNodeToSource(node);
944
+ case "include":
945
+ return `{% include "${node.path}" %}`;
946
+ case "leveloffset": {
947
+ const sign = node.isRelative && node.offset >= 0 ? "+" : "";
948
+ return `{% leveloffset ${sign}${node.offset} %}${nodesToSource(node.body)}{% endleveloffset %}`;
949
+ }
950
+ case "switch": {
951
+ const cases = node.cases.map((c) => `{% case ${expressionToSource(c.value)} %}${nodesToSource(c.body)}`).join("");
952
+ return `{% switch ${expressionToSource(node.expression)} %}${cases}{% endswitch %}`;
953
+ }
954
+ }
955
+ }
956
+ function ifNodeToSource(node) {
957
+ const parts = [`{% if ${expressionToSource(node.condition)} %}`, nodesToSource(node.trueBranch)];
958
+ if (node.elifBranches) {
959
+ for (const elif of node.elifBranches) {
960
+ parts.push(`{% elif ${expressionToSource(elif.condition)} %}`, nodesToSource(elif.body));
961
+ }
962
+ }
963
+ if (node.elseBranch) {
964
+ parts.push(`{% else %}`, nodesToSource(node.elseBranch));
965
+ }
966
+ parts.push(`{% endif %}`);
967
+ return parts.join("");
968
+ }
969
+
970
+ // src/loader.ts
971
+ var FileSystemLoader = class {
972
+ /**
973
+ * Create a filesystem loader
974
+ * @param basePath - Base directory for resolving relative paths
975
+ */
976
+ constructor(basePath = process.cwd()) {
977
+ this.basePath = basePath;
978
+ }
979
+ /**
980
+ * Load a file from the filesystem
981
+ * @param path - Path to load (relative or absolute)
982
+ * @param basePath - Base path to resolve relative paths (overrides constructor basePath)
983
+ * @returns File contents
984
+ */
985
+ async load(path, basePath) {
986
+ const fs = await import("fs/promises");
987
+ const pathModule = await import("path");
988
+ const resolvedBasePath = basePath || this.basePath;
989
+ const resolvedPath = pathModule.isAbsolute(path) ? path : pathModule.resolve(resolvedBasePath, path);
990
+ try {
991
+ return await fs.readFile(resolvedPath, "utf-8");
992
+ } catch (error) {
993
+ const message = error instanceof Error ? error.message : String(error);
994
+ throw new Error(`Failed to load ${resolvedPath}: ${message}`);
995
+ }
996
+ }
997
+ };
998
+
999
+ // src/cli.ts
1000
+ var program = new Command();
1001
+ program.name("minja").description("Minimal, secure Jinja2/Nunjucks subset for documentation preprocessing").version("0.1.0").argument("[template]", "Template file to render (or read from stdin)").option("-c, --context <file>", "Context file (JSON or YAML)").option("-o, --output <file>", "Output file (default: stdout)").option("-d, --max-depth <number>", "Maximum include depth", "10").option("-t, --timeout <ms>", "Rendering timeout in milliseconds", "5000").option("-v, --vars <json>", "Inline context variables as JSON").action(async (templatePath, options) => {
1002
+ try {
1003
+ let template;
1004
+ let baseDir = process.cwd();
1005
+ if (templatePath) {
1006
+ const fullPath = resolve(templatePath);
1007
+ template = await readFile(fullPath, "utf-8");
1008
+ baseDir = dirname(fullPath);
1009
+ } else {
1010
+ template = await readStdin();
1011
+ }
1012
+ let context = {};
1013
+ if (options.context) {
1014
+ const contextPath = resolve(options.context);
1015
+ const contextContent = await readFile(contextPath, "utf-8");
1016
+ if (contextPath.endsWith(".json")) {
1017
+ context = JSON.parse(contextContent);
1018
+ } else if (contextPath.endsWith(".yaml") || contextPath.endsWith(".yml")) {
1019
+ context = parseYAML(contextContent);
1020
+ } else {
1021
+ throw new Error("Context file must be .json, .yaml, or .yml");
1022
+ }
1023
+ }
1024
+ if (options.vars) {
1025
+ const inlineVars = JSON.parse(options.vars);
1026
+ context = { ...context, ...inlineVars };
1027
+ }
1028
+ const loader = new FileSystemLoader(baseDir);
1029
+ const result = await render(template, {
1030
+ loader,
1031
+ basePath: baseDir,
1032
+ context,
1033
+ maxIncludeDepth: parseInt(options.maxDepth || "10"),
1034
+ timeout: parseInt(options.timeout || "5000")
1035
+ });
1036
+ if (options.output) {
1037
+ const outputPath = resolve(options.output);
1038
+ await writeFile(outputPath, result, "utf-8");
1039
+ console.error(`\u2713 Rendered to ${outputPath}`);
1040
+ } else {
1041
+ process.stdout.write(result);
1042
+ }
1043
+ } catch (error) {
1044
+ console.error("Error:", error instanceof Error ? error.message : String(error));
1045
+ process.exit(1);
1046
+ }
1047
+ });
1048
+ program.parse();
1049
+ async function readStdin() {
1050
+ return new Promise((resolve2, reject) => {
1051
+ const chunks = [];
1052
+ process.stdin.on("data", (chunk) => {
1053
+ chunks.push(chunk);
1054
+ });
1055
+ process.stdin.on("end", () => {
1056
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
1057
+ });
1058
+ process.stdin.on("error", (error) => {
1059
+ reject(error);
1060
+ });
1061
+ });
1062
+ }