@dogsbay/minja 0.2.0-beta.100

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +623 -0
  3. package/bin/minja.js +1225 -0
  4. package/dist/browser.d.ts +12 -0
  5. package/dist/browser.d.ts.map +1 -0
  6. package/dist/browser.js +11 -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 +98 -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 +213 -0
  19. package/dist/evaluator.js.map +1 -0
  20. package/dist/index.d.ts +20 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +18 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/index.umd.js +1160 -0
  25. package/dist/index.umd.js.map +7 -0
  26. package/dist/index.umd.min.js +12 -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 +73 -0
  37. package/dist/loader.d.ts.map +1 -0
  38. package/dist/loader.js +159 -0
  39. package/dist/loader.js.map +1 -0
  40. package/dist/parser.d.ts +7 -0
  41. package/dist/parser.d.ts.map +1 -0
  42. package/dist/parser.js +609 -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 +494 -0
  47. package/dist/renderer.js.map +1 -0
  48. package/dist/scan.d.ts +46 -0
  49. package/dist/scan.d.ts.map +1 -0
  50. package/dist/scan.js +46 -0
  51. package/dist/scan.js.map +1 -0
  52. package/dist/types.d.ts +175 -0
  53. package/dist/types.d.ts.map +1 -0
  54. package/dist/types.js +5 -0
  55. package/dist/types.js.map +1 -0
  56. package/package.json +72 -0
package/bin/minja.js ADDED
@@ -0,0 +1,1225 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * minja v0.2.0-beta.100
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
+ var KNOWN_TAG_KEYWORDS = /* @__PURE__ */ new Set([
175
+ "set",
176
+ "if",
177
+ "include",
178
+ "switch",
179
+ "leveloffset",
180
+ "raw",
181
+ "endif",
182
+ "elif",
183
+ "else",
184
+ "endswitch",
185
+ "case",
186
+ "endleveloffset",
187
+ "endraw"
188
+ ]);
189
+ function stripBodyForEndTag(body, endTag) {
190
+ return endTag.startsWith("{%-") ? body.replace(/[ \t]*\n[ \t\n]*$/, "") : body;
191
+ }
192
+ function parse(template) {
193
+ const errors = [];
194
+ const ast = [];
195
+ let position = 0;
196
+ let line = 1;
197
+ let column = 1;
198
+ function createError(message) {
199
+ return { message, position, line, column };
200
+ }
201
+ function advance(count) {
202
+ for (let i = 0; i < count; i++) {
203
+ if (template[position + i] === "\n") {
204
+ line++;
205
+ column = 1;
206
+ } else {
207
+ column++;
208
+ }
209
+ }
210
+ position += count;
211
+ }
212
+ function findNext(str, from = position) {
213
+ return template.indexOf(str, from);
214
+ }
215
+ function extractTo(target) {
216
+ const text = template.substring(position, target);
217
+ advance(target - position);
218
+ return text;
219
+ }
220
+ while (position < template.length) {
221
+ const varStart = findNext("{{", position);
222
+ const tagStart = findNext("{%", position);
223
+ const commentStart = findNext("{#", position);
224
+ const candidates = [
225
+ { pos: varStart, type: "var" },
226
+ { pos: tagStart, type: "tag" },
227
+ { pos: commentStart, type: "comment" }
228
+ ].filter((c) => c.pos !== -1);
229
+ if (candidates.length === 0) {
230
+ const text = template.substring(position);
231
+ if (text) {
232
+ ast.push({ type: "text", value: text });
233
+ }
234
+ break;
235
+ }
236
+ candidates.sort((a, b) => a.pos - b.pos);
237
+ const nearest = candidates[0];
238
+ if (nearest.pos > position) {
239
+ ast.push({ type: "text", value: extractTo(nearest.pos) });
240
+ }
241
+ if (nearest.type === "var") {
242
+ advance(2);
243
+ const endPos = findNext("}}", position);
244
+ if (endPos === -1) {
245
+ errors.push(createError("Unclosed variable tag"));
246
+ break;
247
+ }
248
+ const varName = extractTo(endPos).trim();
249
+ advance(2);
250
+ ast.push({ type: "variable", name: varName });
251
+ } else if (nearest.type === "comment") {
252
+ if (ast.length > 0 && ast[ast.length - 1].type === "text") {
253
+ const lastNode = ast[ast.length - 1];
254
+ if (/\n[ \t]*$/.test(lastNode.value)) {
255
+ lastNode.value = lastNode.value.replace(/[ \t]*\n[ \t]*$/, "");
256
+ }
257
+ }
258
+ advance(2);
259
+ const endPos = findNext("#}", position);
260
+ if (endPos === -1) {
261
+ errors.push(createError("Unclosed comment tag"));
262
+ break;
263
+ }
264
+ const commentText = extractTo(endPos);
265
+ advance(2);
266
+ ast.push({ type: "comment", value: commentText });
267
+ if (position < template.length && template[position] === "\n") {
268
+ advance(1);
269
+ while (position < template.length && /[ \t]/.test(template[position])) {
270
+ advance(1);
271
+ }
272
+ }
273
+ } else if (nearest.type === "tag") {
274
+ const tagStart2 = position;
275
+ advance(2);
276
+ const endPos = findNext("%}", position);
277
+ if (endPos === -1) {
278
+ errors.push(createError("Unclosed statement tag"));
279
+ break;
280
+ }
281
+ const peeked = template.substring(position, endPos).trim();
282
+ const peekKeyword = peeked.replace(/^-\s*/, "").replace(/\s*-$/, "").split(/\s+/)[0] ?? "";
283
+ if (peekKeyword === "raw") {
284
+ const openerLeftStrip = peeked.startsWith("-");
285
+ const openerRightStrip = peeked.endsWith("-");
286
+ if (openerLeftStrip && ast.length > 0 && ast[ast.length - 1].type === "text") {
287
+ const lastNode = ast[ast.length - 1];
288
+ lastNode.value = lastNode.value.replace(/[ \t]*\n[ \t\n]*$/, "");
289
+ }
290
+ extractTo(endPos);
291
+ advance(2);
292
+ if (openerRightStrip) {
293
+ while (position < template.length && /[ \t\n]/.test(template[position])) {
294
+ advance(1);
295
+ }
296
+ }
297
+ const endrawMatch = /\{%-?\s*endraw\s*-?%\}/.exec(template.substring(position));
298
+ if (!endrawMatch) {
299
+ errors.push(createError("Unclosed raw block"));
300
+ break;
301
+ }
302
+ const endrawAbs = position + endrawMatch.index;
303
+ let content = extractTo(endrawAbs);
304
+ const endrawTag = endrawMatch[0];
305
+ if (endrawTag.startsWith("{%-")) {
306
+ content = content.replace(/[ \t]*\n[ \t\n]*$/, "");
307
+ }
308
+ advance(endrawTag.length);
309
+ if (endrawTag.endsWith("-%}")) {
310
+ while (position < template.length && /[ \t\n]/.test(template[position])) {
311
+ advance(1);
312
+ }
313
+ }
314
+ if (content) {
315
+ ast.push({ type: "text", value: content });
316
+ }
317
+ continue;
318
+ }
319
+ if (!KNOWN_TAG_KEYWORDS.has(peekKeyword)) {
320
+ extractTo(endPos);
321
+ advance(2);
322
+ ast.push({ type: "text", value: template.substring(tagStart2, endPos + 2) });
323
+ continue;
324
+ }
325
+ let statement = extractTo(endPos).trim();
326
+ const hasLeftStrip = statement.startsWith("-");
327
+ const hasRightStrip = statement.endsWith("-");
328
+ if (hasLeftStrip) {
329
+ statement = statement.substring(1).trim();
330
+ if (ast.length > 0 && ast[ast.length - 1].type === "text") {
331
+ const lastNode = ast[ast.length - 1];
332
+ lastNode.value = lastNode.value.replace(/[ \t]*\n[ \t\n]*$/, "");
333
+ }
334
+ }
335
+ if (hasRightStrip) {
336
+ statement = statement.substring(0, statement.length - 1).trim();
337
+ }
338
+ advance(2);
339
+ if (hasRightStrip) {
340
+ while (position < template.length && /[ \t\n]/.test(template[position])) {
341
+ advance(1);
342
+ }
343
+ }
344
+ if (statement.startsWith("set ")) {
345
+ const setMatch = /^set\s+(\w+)\s*=\s*(.+)$/.exec(statement);
346
+ if (setMatch) {
347
+ try {
348
+ const expr = parseExpression(setMatch[2]);
349
+ ast.push({
350
+ type: "set",
351
+ name: setMatch[1],
352
+ value: expr
353
+ });
354
+ } catch (error) {
355
+ errors.push(createError(`Invalid set expression: ${error.message}`));
356
+ }
357
+ } else {
358
+ errors.push(createError("Invalid set syntax"));
359
+ }
360
+ } else if (statement.startsWith("if ")) {
361
+ const condition = statement.substring(3).trim();
362
+ try {
363
+ const expr = parseExpression(condition);
364
+ const { trueBranch, elifBranches, elseBranch, parseErrors } = parseIfBlock();
365
+ errors.push(...parseErrors);
366
+ ast.push({
367
+ type: "if",
368
+ condition: expr,
369
+ trueBranch,
370
+ elifBranches: elifBranches.length > 0 ? elifBranches : void 0,
371
+ elseBranch: elseBranch.length > 0 ? elseBranch : void 0
372
+ });
373
+ } catch (error) {
374
+ errors.push(createError(`Invalid if expression: ${error.message}`));
375
+ }
376
+ } else if (statement.startsWith("include ")) {
377
+ const includeMatch = /^include\s+["']([^"']+)["']/.exec(statement);
378
+ if (includeMatch) {
379
+ ast.push({
380
+ type: "include",
381
+ path: includeMatch[1]
382
+ });
383
+ } else {
384
+ errors.push(createError("Invalid include syntax"));
385
+ }
386
+ } else if (statement.startsWith("switch ")) {
387
+ const switchExpr = statement.substring(7).trim();
388
+ try {
389
+ const expr = parseExpression(switchExpr);
390
+ const { cases, parseErrors } = parseSwitchBlock();
391
+ errors.push(...parseErrors);
392
+ ast.push({
393
+ type: "switch",
394
+ expression: expr,
395
+ cases
396
+ });
397
+ } catch (error) {
398
+ errors.push(createError(`Invalid switch expression: ${error.message}`));
399
+ }
400
+ } else if (statement.startsWith("leveloffset ")) {
401
+ const offsetMatch = /^leveloffset\s+([-+]?\d+)$/.exec(statement);
402
+ if (!offsetMatch) {
403
+ errors.push(createError("Invalid leveloffset syntax"));
404
+ } else {
405
+ const offsetStr = offsetMatch[1];
406
+ const isRelative = offsetStr.startsWith("+") || offsetStr.startsWith("-");
407
+ const offset = parseInt(offsetStr, 10);
408
+ if (isNaN(offset)) {
409
+ errors.push(createError("Invalid leveloffset value"));
410
+ } else {
411
+ let depth = 0;
412
+ let searchPos = position;
413
+ let endPos2 = -1;
414
+ while (searchPos < template.length) {
415
+ const leveloffsetMatch = /{%-?\s*leveloffset\s+[-+]?\d+\s*-?%}/.exec(template.substring(searchPos));
416
+ const endleveloffsetMatch = /{%-?\s*endleveloffset\s*-?%}/.exec(template.substring(searchPos));
417
+ const leveloffsetPos = leveloffsetMatch ? searchPos + leveloffsetMatch.index : Infinity;
418
+ const endleveloffsetPos = endleveloffsetMatch ? searchPos + endleveloffsetMatch.index : Infinity;
419
+ if (leveloffsetPos < endleveloffsetPos) {
420
+ depth++;
421
+ searchPos = leveloffsetPos + (leveloffsetMatch?.[0].length || 0);
422
+ } else if (endleveloffsetPos < Infinity) {
423
+ if (depth === 0) {
424
+ endPos2 = endleveloffsetPos;
425
+ break;
426
+ } else {
427
+ depth--;
428
+ searchPos = endleveloffsetPos + (endleveloffsetMatch?.[0].length || 0);
429
+ }
430
+ } else {
431
+ break;
432
+ }
433
+ }
434
+ if (endPos2 === -1) {
435
+ errors.push(createError("Missing endleveloffset"));
436
+ } else {
437
+ const bodyTemplate = extractTo(endPos2);
438
+ const endMatch = /{%-?\s*endleveloffset\s*-?%}/.exec(template.substring(position));
439
+ if (endMatch) {
440
+ advance(endMatch[0].length);
441
+ }
442
+ const bodyResult = parse(bodyTemplate);
443
+ errors.push(...bodyResult.errors);
444
+ ast.push({
445
+ type: "leveloffset",
446
+ offset,
447
+ isRelative,
448
+ body: bodyResult.ast
449
+ });
450
+ }
451
+ }
452
+ }
453
+ }
454
+ }
455
+ }
456
+ return { ast, errors };
457
+ function findEndTag(tagName) {
458
+ const pattern = new RegExp(`{%-?\\s*${tagName}\\s*-?%}`);
459
+ const match = pattern.exec(template.substring(position));
460
+ if (match) {
461
+ return {
462
+ start: position + match.index,
463
+ length: match[0].length,
464
+ tag: match[0]
465
+ };
466
+ }
467
+ return null;
468
+ }
469
+ function parseIfBlock() {
470
+ const elifBranches = [];
471
+ const parseErrors = [];
472
+ let elseBranch = [];
473
+ let depth = 0;
474
+ let searchPos = position;
475
+ while (searchPos < template.length) {
476
+ const ifMatch = /{%-?\s*if\s+/.exec(template.substring(searchPos));
477
+ const elifMatch = /{%-?\s*elif\s+/.exec(template.substring(searchPos));
478
+ const elseMatch = /{%-?\s*else\s*-?%}/.exec(template.substring(searchPos));
479
+ const endifMatch = /{%-?\s*endif\s*-?%}/.exec(template.substring(searchPos));
480
+ const matches = [
481
+ { type: "if", match: ifMatch, pos: ifMatch ? searchPos + ifMatch.index : Infinity },
482
+ { type: "elif", match: elifMatch, pos: elifMatch ? searchPos + elifMatch.index : Infinity },
483
+ { type: "else", match: elseMatch, pos: elseMatch ? searchPos + elseMatch.index : Infinity },
484
+ { type: "endif", match: endifMatch, pos: endifMatch ? searchPos + endifMatch.index : Infinity }
485
+ ].sort((a, b) => a.pos - b.pos);
486
+ const nearest = matches[0];
487
+ if (!nearest.match || nearest.pos === Infinity) {
488
+ parseErrors.push(createError("Missing endif"));
489
+ break;
490
+ }
491
+ if (nearest.type === "if") {
492
+ depth++;
493
+ searchPos = nearest.pos + nearest.match[0].length;
494
+ } else if (nearest.type === "endif") {
495
+ if (depth === 0) {
496
+ const bodyTemplate = stripBodyForEndTag(extractTo(nearest.pos), nearest.match[0]);
497
+ advance(nearest.match[0].length);
498
+ if (nearest.match[0].endsWith("-%}")) {
499
+ while (position < template.length && /[ \t\n]/.test(template[position]))
500
+ advance(1);
501
+ }
502
+ const bodyResult = parse(bodyTemplate);
503
+ parseErrors.push(...bodyResult.errors);
504
+ return {
505
+ trueBranch: bodyResult.ast,
506
+ elifBranches,
507
+ elseBranch,
508
+ parseErrors
509
+ };
510
+ } else {
511
+ depth--;
512
+ searchPos = nearest.pos + nearest.match[0].length;
513
+ }
514
+ } else if (depth === 0 && (nearest.type === "elif" || nearest.type === "else")) {
515
+ const bodyTemplate = stripBodyForEndTag(extractTo(nearest.pos), nearest.match[0]);
516
+ const bodyResult = parse(bodyTemplate);
517
+ if (elifBranches.length === 0 && elseBranch.length === 0) {
518
+ parseErrors.push(...bodyResult.errors);
519
+ const trueBranch = bodyResult.ast;
520
+ if (nearest.type === "elif") {
521
+ advance(nearest.match[0].length);
522
+ const condMatch = /^([^%]+)%}/.exec(template.substring(position));
523
+ if (condMatch) {
524
+ const condStr = condMatch[1].trim();
525
+ advance(condMatch[0].length);
526
+ try {
527
+ const elifCondition = parseExpression(condStr);
528
+ const elifResult = parseIfBlock();
529
+ parseErrors.push(...elifResult.parseErrors);
530
+ elifBranches.push({
531
+ condition: elifCondition,
532
+ body: elifResult.trueBranch
533
+ });
534
+ elifBranches.push(...elifResult.elifBranches || []);
535
+ elseBranch = elifResult.elseBranch;
536
+ return {
537
+ trueBranch,
538
+ elifBranches,
539
+ elseBranch,
540
+ parseErrors
541
+ };
542
+ } catch (error) {
543
+ parseErrors.push(createError(`Invalid elif expression: ${error.message}`));
544
+ }
545
+ }
546
+ } else {
547
+ advance(nearest.match[0].length);
548
+ const endifMatch2 = findEndTag("endif");
549
+ if (!endifMatch2) {
550
+ parseErrors.push(createError("Missing endif after else"));
551
+ } else {
552
+ const elseBodyTemplate = stripBodyForEndTag(
553
+ extractTo(endifMatch2.start),
554
+ endifMatch2.tag
555
+ );
556
+ advance(endifMatch2.length);
557
+ const elseBodyResult = parse(elseBodyTemplate);
558
+ parseErrors.push(...elseBodyResult.errors);
559
+ elseBranch = elseBodyResult.ast;
560
+ }
561
+ return {
562
+ trueBranch,
563
+ elifBranches,
564
+ elseBranch,
565
+ parseErrors
566
+ };
567
+ }
568
+ }
569
+ } else {
570
+ searchPos = nearest.pos + nearest.match[0].length;
571
+ }
572
+ }
573
+ return {
574
+ trueBranch: [],
575
+ elifBranches,
576
+ elseBranch,
577
+ parseErrors
578
+ };
579
+ }
580
+ function parseSwitchBlock() {
581
+ const cases = [];
582
+ const parseErrors = [];
583
+ while (position < template.length) {
584
+ const caseMatch = /{%-?\s*case\s+/.exec(template.substring(position));
585
+ const endswitchMatch = /{%-?\s*endswitch\s*-?%}/.exec(template.substring(position));
586
+ const casePos = caseMatch ? position + caseMatch.index : Infinity;
587
+ const endswitchPos = endswitchMatch ? position + endswitchMatch.index : Infinity;
588
+ if (endswitchPos < casePos) {
589
+ if (cases.length > 0 && position < endswitchPos) {
590
+ const bodyTemplate = extractTo(endswitchPos);
591
+ const bodyResult = parse(bodyTemplate);
592
+ parseErrors.push(...bodyResult.errors);
593
+ cases[cases.length - 1].body = bodyResult.ast;
594
+ }
595
+ advance(endswitchPos - position + (endswitchMatch?.[0].length || 0));
596
+ break;
597
+ } else if (casePos < Infinity) {
598
+ if (cases.length > 0 && position < casePos) {
599
+ const bodyTemplate = extractTo(casePos);
600
+ const bodyResult = parse(bodyTemplate);
601
+ parseErrors.push(...bodyResult.errors);
602
+ cases[cases.length - 1].body = bodyResult.ast;
603
+ } else if (position < casePos) {
604
+ extractTo(casePos);
605
+ }
606
+ advance(caseMatch[0].length);
607
+ const valueMatch = /([^%]+)%}/.exec(template.substring(position));
608
+ if (valueMatch) {
609
+ const valueStr = valueMatch[1].trim();
610
+ advance(valueMatch[0].length);
611
+ try {
612
+ const caseValue = parseExpression(valueStr);
613
+ cases.push({
614
+ value: caseValue,
615
+ body: []
616
+ // Will be filled in next iteration or at endswitch
617
+ });
618
+ } catch (error) {
619
+ parseErrors.push(createError(`Invalid case value: ${error.message}`));
620
+ }
621
+ } else {
622
+ parseErrors.push(createError("Invalid case syntax"));
623
+ break;
624
+ }
625
+ } else {
626
+ parseErrors.push(createError("Missing endswitch"));
627
+ break;
628
+ }
629
+ }
630
+ return { cases, parseErrors };
631
+ }
632
+ }
633
+
634
+ // src/context.ts
635
+ var Context = class _Context {
636
+ constructor(parent, options) {
637
+ this.variables = /* @__PURE__ */ new Map();
638
+ this.parent = parent || null;
639
+ this.options = options || parent?.options || {};
640
+ }
641
+ /**
642
+ * Get a variable from the context (supports dot notation)
643
+ * @param name - Variable name (can use dot notation like "obj.prop.subprop")
644
+ * @returns The variable value or undefined
645
+ */
646
+ get(name) {
647
+ const normalizedName = this.options.hyphenToUnderscore ? name.replace(/-/g, "_") : name;
648
+ const parts = normalizedName.split(".");
649
+ const rootName = parts[0];
650
+ let value;
651
+ if (this.variables.has(rootName)) {
652
+ value = this.variables.get(rootName);
653
+ } else if (this.parent) {
654
+ value = this.parent.get(rootName);
655
+ } else {
656
+ return void 0;
657
+ }
658
+ for (let i = 1; i < parts.length; i++) {
659
+ if (value === null || value === void 0) {
660
+ return void 0;
661
+ }
662
+ if (typeof value !== "object" || Array.isArray(value)) {
663
+ return void 0;
664
+ }
665
+ if (!Object.prototype.hasOwnProperty.call(value, parts[i])) {
666
+ return void 0;
667
+ }
668
+ value = value[parts[i]];
669
+ }
670
+ return value;
671
+ }
672
+ /**
673
+ * Set a variable in the current context
674
+ * @param name - Variable name (can use dot notation)
675
+ * @param value - Value to set
676
+ */
677
+ set(name, value) {
678
+ const parts = name.split(".");
679
+ if (parts.length === 1) {
680
+ this.variables.set(name, value);
681
+ return;
682
+ }
683
+ const rootName = parts[0];
684
+ let target = this.variables.get(rootName);
685
+ if (!target || typeof target !== "object" || Array.isArray(target)) {
686
+ target = {};
687
+ this.variables.set(rootName, target);
688
+ }
689
+ for (let i = 1; i < parts.length - 1; i++) {
690
+ const key = parts[i];
691
+ if (!target[key] || typeof target[key] !== "object" || Array.isArray(target[key])) {
692
+ target[key] = {};
693
+ }
694
+ target = target[key];
695
+ }
696
+ target[parts[parts.length - 1]] = value;
697
+ }
698
+ /**
699
+ * Create a child scope
700
+ * @returns A new Context with this context as parent
701
+ */
702
+ push() {
703
+ return new _Context(this, this.options);
704
+ }
705
+ /**
706
+ * Return to parent scope
707
+ * @returns The parent context or null if at root
708
+ */
709
+ pop() {
710
+ return this.parent;
711
+ }
712
+ /**
713
+ * Create a context from a plain object
714
+ * @param data - Plain object to convert to context
715
+ * @param options - Context options
716
+ * @returns New Context instance
717
+ */
718
+ static from(data, options) {
719
+ const ctx = new _Context(void 0, options);
720
+ for (const [key, value] of Object.entries(data)) {
721
+ ctx.set(key, value);
722
+ }
723
+ return ctx;
724
+ }
725
+ };
726
+
727
+ // src/loader-fetch.ts
728
+ var FetchLoader = class {
729
+ async load(path, basePath) {
730
+ const url = basePath ? new URL(path, basePath).href : path;
731
+ const fetchUrl = `${url}?preventCache=${Date.now()}`;
732
+ const response = await fetch(fetchUrl);
733
+ if (!response.ok) {
734
+ throw new Error(`Failed to load ${url}: ${response.status} ${response.statusText}`);
735
+ }
736
+ return await response.text();
737
+ }
738
+ };
739
+
740
+ // src/renderer.ts
741
+ function transformHeadings(text, offset, initialFence = null, fenceOut) {
742
+ if (offset === 0) {
743
+ if (fenceOut)
744
+ fenceOut.fence = initialFence;
745
+ return text;
746
+ }
747
+ const lines = text.split("\n");
748
+ let fence = initialFence;
749
+ for (let i = 0; i < lines.length; i++) {
750
+ const line = lines[i];
751
+ const fenceMatch = line.match(/^\s*(`{3,}|~{3,})/);
752
+ if (fenceMatch) {
753
+ const run = fenceMatch[1];
754
+ if (fence === null) {
755
+ fence = run;
756
+ } else if (run[0] === fence[0] && run.length >= fence.length && line.trim() === run) {
757
+ fence = null;
758
+ }
759
+ continue;
760
+ }
761
+ if (fence !== null) {
762
+ continue;
763
+ }
764
+ const m = line.match(/^(#{1,6})(\s+)/);
765
+ if (m) {
766
+ const newLevel = Math.max(1, Math.min(6, m[1].length + offset));
767
+ lines[i] = "#".repeat(newLevel) + line.slice(m[1].length);
768
+ }
769
+ }
770
+ if (fenceOut)
771
+ fenceOut.fence = fence;
772
+ return lines.join("\n");
773
+ }
774
+ function resolvePath(path, basePath) {
775
+ if (path.startsWith("/")) {
776
+ return path;
777
+ }
778
+ const base = basePath.endsWith("/") ? basePath : basePath + "/";
779
+ const combined = base + path;
780
+ const parts = combined.split("/");
781
+ const resolved = [];
782
+ for (const part of parts) {
783
+ if (part === "." || part === "") {
784
+ continue;
785
+ } else if (part === "..") {
786
+ resolved.pop();
787
+ } else {
788
+ resolved.push(part);
789
+ }
790
+ }
791
+ const prefix = combined.startsWith("/") ? "/" : "";
792
+ return prefix + resolved.join("/");
793
+ }
794
+ async function render(template, options = {}) {
795
+ const {
796
+ loader = new FetchLoader(),
797
+ context: initialContext = {},
798
+ basePath = "",
799
+ maxIncludeDepth = 10,
800
+ timeout = 5e3,
801
+ hyphenToUnderscore = false,
802
+ undefinedBehavior = "empty",
803
+ undefinedConditions = "preserve"
804
+ } = options;
805
+ const timeoutPromise = new Promise((_, reject) => {
806
+ setTimeout(() => reject(new Error("Template rendering timeout")), timeout);
807
+ });
808
+ const renderPromise = renderInternal(template, {
809
+ loader,
810
+ context: Context.from(
811
+ {
812
+ ...initialContext,
813
+ // Built-in variables
814
+ date: (/* @__PURE__ */ new Date()).toISOString().split("T")[0],
815
+ timestamp: Date.now(),
816
+ _levelOffset: 0
817
+ },
818
+ { hyphenToUnderscore }
819
+ ),
820
+ includeDepth: 0,
821
+ maxIncludeDepth,
822
+ basePath,
823
+ undefinedBehavior,
824
+ undefinedConditions
825
+ });
826
+ return await Promise.race([renderPromise, timeoutPromise]);
827
+ }
828
+ async function renderInternal(template, options) {
829
+ const { includeDepth, maxIncludeDepth } = options;
830
+ if (includeDepth > maxIncludeDepth) {
831
+ throw new Error(`Maximum include depth (${maxIncludeDepth}) exceeded`);
832
+ }
833
+ const parseResult = parse(template);
834
+ if (parseResult.errors.length > 0) {
835
+ const errorMessages = parseResult.errors.map((e) => e.message).join(", ");
836
+ throw new Error(`Parse errors: ${errorMessages}`);
837
+ }
838
+ return await renderNodes(parseResult.ast, options);
839
+ }
840
+ async function renderNodes(nodes, options) {
841
+ const parts = [];
842
+ for (const node of nodes) {
843
+ let rendered = await renderNode(node, options);
844
+ if (node.type === "include" && rendered && parts.length > 0) {
845
+ const tail = /(?:^|\n)([ \t]+)$/.exec(parts[parts.length - 1]);
846
+ if (tail) {
847
+ rendered = rendered.replace(/\n(?=[^\n])/g, "\n" + tail[1]);
848
+ }
849
+ }
850
+ parts.push(rendered);
851
+ }
852
+ return parts.join("");
853
+ }
854
+ async function renderNode(node, options) {
855
+ const { loader, context, includeDepth, maxIncludeDepth, basePath } = options;
856
+ switch (node.type) {
857
+ case "text": {
858
+ const currentOffset = context.get("_levelOffset") || 0;
859
+ if (currentOffset === 0) {
860
+ return node.value;
861
+ }
862
+ const fenceOut = { fence: null };
863
+ const out = transformHeadings(
864
+ node.value,
865
+ currentOffset,
866
+ context.get("_levelOffsetFence") ?? null,
867
+ fenceOut
868
+ );
869
+ context.set("_levelOffsetFence", fenceOut.fence);
870
+ return out;
871
+ }
872
+ case "variable": {
873
+ const value = context.get(node.name);
874
+ if (value === void 0 || value === null) {
875
+ switch (options.undefinedBehavior) {
876
+ case "throw":
877
+ throw new Error(`Undefined variable: ${node.name}`);
878
+ case "preserve":
879
+ return `{{ ${node.name} }}`;
880
+ case "asciidoc-literal":
881
+ return `{${node.name}}`;
882
+ case "empty":
883
+ default:
884
+ return "";
885
+ }
886
+ }
887
+ const str = String(value);
888
+ if (str.includes("{{") || str.includes("{%")) {
889
+ return await renderInternal(str, options);
890
+ }
891
+ return str;
892
+ }
893
+ case "set": {
894
+ const value = evaluateExpression(node.value, context);
895
+ context.set(node.name, value);
896
+ return "";
897
+ }
898
+ case "comment":
899
+ return "";
900
+ case "if": {
901
+ if ((options.undefinedBehavior === "preserve" || options.undefinedBehavior === "asciidoc-literal") && options.undefinedConditions !== "falsy") {
902
+ const undefinedRefs = findUndefinedRefs(node.condition, context);
903
+ if (undefinedRefs.length > 0) {
904
+ return ifNodeToSource(node);
905
+ }
906
+ if (node.elifBranches) {
907
+ for (const elif of node.elifBranches) {
908
+ if (findUndefinedRefs(elif.condition, context).length > 0) {
909
+ return ifNodeToSource(node);
910
+ }
911
+ }
912
+ }
913
+ }
914
+ const condition = evaluateExpression(node.condition, context);
915
+ if (isTruthy2(condition)) {
916
+ return await renderNodes(node.trueBranch, options);
917
+ }
918
+ if (node.elifBranches) {
919
+ for (const elifBranch of node.elifBranches) {
920
+ const elifCondition = evaluateExpression(elifBranch.condition, context);
921
+ if (isTruthy2(elifCondition)) {
922
+ return await renderNodes(elifBranch.body, options);
923
+ }
924
+ }
925
+ }
926
+ if (node.elseBranch) {
927
+ return await renderNodes(node.elseBranch, options);
928
+ }
929
+ return "";
930
+ }
931
+ case "include": {
932
+ try {
933
+ const includedContent = await loader.load(node.path, basePath);
934
+ let newBasePath = basePath;
935
+ if (node.path.includes("://")) {
936
+ const url = new URL(node.path);
937
+ newBasePath = url.href.substring(0, url.href.lastIndexOf("/") + 1);
938
+ } else if (basePath) {
939
+ if (basePath.includes("://")) {
940
+ const url = new URL(node.path, basePath);
941
+ newBasePath = url.href.substring(0, url.href.lastIndexOf("/") + 1);
942
+ } else {
943
+ const resolvedPath = resolvePath(node.path, basePath);
944
+ const lastSlash = resolvedPath.lastIndexOf("/");
945
+ newBasePath = lastSlash >= 0 ? resolvedPath.substring(0, lastSlash + 1) : basePath;
946
+ }
947
+ } else if (node.path.includes("/")) {
948
+ const lastSlash = node.path.lastIndexOf("/");
949
+ newBasePath = node.path.substring(0, lastSlash + 1);
950
+ }
951
+ return await renderInternal(includedContent, {
952
+ loader,
953
+ context,
954
+ includeDepth: includeDepth + 1,
955
+ maxIncludeDepth,
956
+ basePath: newBasePath,
957
+ undefinedBehavior: options.undefinedBehavior,
958
+ undefinedConditions: options.undefinedConditions
959
+ });
960
+ } catch (error) {
961
+ if (error instanceof Error && error.name === "IncludeContainmentError") {
962
+ throw error;
963
+ }
964
+ if (options.undefinedBehavior === "preserve" || options.undefinedBehavior === "asciidoc-literal") {
965
+ return `{% include "${node.path}" %}`;
966
+ }
967
+ const message = error instanceof Error ? error.message : String(error);
968
+ console.error(`Failed to include ${node.path}:`, message);
969
+ return `<!-- Include error: ${node.path} -->`;
970
+ }
971
+ }
972
+ case "switch": {
973
+ const switchValue = evaluateExpression(node.expression, context);
974
+ for (const caseItem of node.cases) {
975
+ const caseValue = evaluateExpression(caseItem.value, context);
976
+ if (switchValue == caseValue) {
977
+ return await renderNodes(caseItem.body, options);
978
+ }
979
+ }
980
+ return "";
981
+ }
982
+ case "leveloffset": {
983
+ const parentOffset = context.get("_levelOffset") || 0;
984
+ const newOffset = node.isRelative ? parentOffset + node.offset : node.offset;
985
+ const previousOffset = parentOffset;
986
+ context.set("_levelOffset", newOffset);
987
+ const previousFence = context.get("_levelOffsetFence") ?? null;
988
+ context.set("_levelOffsetFence", null);
989
+ try {
990
+ const result = await renderNodes(node.body, options);
991
+ return result;
992
+ } finally {
993
+ context.set("_levelOffset", previousOffset);
994
+ context.set("_levelOffsetFence", previousFence);
995
+ }
996
+ }
997
+ default:
998
+ const _exhaustive = node;
999
+ return _exhaustive;
1000
+ }
1001
+ }
1002
+ function isTruthy2(value) {
1003
+ if (value === void 0 || value === null || value === false) {
1004
+ return false;
1005
+ }
1006
+ if (value === 0 || value === "") {
1007
+ return false;
1008
+ }
1009
+ return true;
1010
+ }
1011
+ function findUndefinedRefs(expr, context) {
1012
+ const out = [];
1013
+ function walk(e) {
1014
+ switch (e.type) {
1015
+ case "variable":
1016
+ if (context.get(e.name) === void 0)
1017
+ out.push(e.name);
1018
+ break;
1019
+ case "binary":
1020
+ walk(e.left);
1021
+ walk(e.right);
1022
+ break;
1023
+ case "unary":
1024
+ walk(e.operand);
1025
+ break;
1026
+ case "literal":
1027
+ break;
1028
+ }
1029
+ }
1030
+ walk(expr);
1031
+ return out;
1032
+ }
1033
+ function expressionToSource(expr) {
1034
+ switch (expr.type) {
1035
+ case "literal":
1036
+ if (typeof expr.value === "string")
1037
+ return JSON.stringify(expr.value);
1038
+ if (expr.value === null)
1039
+ return "null";
1040
+ return String(expr.value);
1041
+ case "variable":
1042
+ return expr.name;
1043
+ case "unary":
1044
+ return `not ${expressionToSource(expr.operand)}`;
1045
+ case "binary":
1046
+ return `(${expressionToSource(expr.left)} ${expr.operator} ${expressionToSource(expr.right)})`;
1047
+ }
1048
+ }
1049
+ function nodesToSource(nodes) {
1050
+ return nodes.map(nodeToSource).join("");
1051
+ }
1052
+ function nodeToSource(node) {
1053
+ switch (node.type) {
1054
+ case "text":
1055
+ return node.value;
1056
+ case "variable":
1057
+ return `{{ ${node.name} }}`;
1058
+ case "set":
1059
+ return `{% set ${node.name} = ${expressionToSource(node.value)} %}`;
1060
+ case "comment":
1061
+ return "{# \u2026 #}";
1062
+ case "if":
1063
+ return ifNodeToSource(node);
1064
+ case "include":
1065
+ return `{% include "${node.path}" %}`;
1066
+ case "leveloffset": {
1067
+ const sign = node.isRelative && node.offset >= 0 ? "+" : "";
1068
+ return `{% leveloffset ${sign}${node.offset} %}${nodesToSource(node.body)}{% endleveloffset %}`;
1069
+ }
1070
+ case "switch": {
1071
+ const cases = node.cases.map((c) => `{% case ${expressionToSource(c.value)} %}${nodesToSource(c.body)}`).join("");
1072
+ return `{% switch ${expressionToSource(node.expression)} %}${cases}{% endswitch %}`;
1073
+ }
1074
+ }
1075
+ }
1076
+ function ifNodeToSource(node) {
1077
+ const parts = [`{% if ${expressionToSource(node.condition)} %}`, nodesToSource(node.trueBranch)];
1078
+ if (node.elifBranches) {
1079
+ for (const elif of node.elifBranches) {
1080
+ parts.push(`{% elif ${expressionToSource(elif.condition)} %}`, nodesToSource(elif.body));
1081
+ }
1082
+ }
1083
+ if (node.elseBranch) {
1084
+ parts.push(`{% else %}`, nodesToSource(node.elseBranch));
1085
+ }
1086
+ parts.push(`{% endif %}`);
1087
+ return parts.join("");
1088
+ }
1089
+
1090
+ // src/loader.ts
1091
+ var FileSystemLoader = class {
1092
+ /**
1093
+ * Create a filesystem loader
1094
+ * @param basePath - Base directory for resolving relative paths
1095
+ * @param options - See {@link FileSystemLoaderOptions}
1096
+ */
1097
+ constructor(basePath = process.cwd(), options = {}) {
1098
+ this.basePath = basePath;
1099
+ this.root = options.root;
1100
+ }
1101
+ /**
1102
+ * Load a file from the filesystem
1103
+ * @param path - Path to load (relative or absolute)
1104
+ * @param basePath - Base path to resolve relative paths (overrides constructor basePath)
1105
+ * @returns File contents
1106
+ */
1107
+ async load(path, basePath) {
1108
+ const fs = await import("fs/promises");
1109
+ const pathModule = await import("path");
1110
+ const resolvedBasePath = basePath || this.basePath;
1111
+ const resolvedPath = pathModule.isAbsolute(path) ? path : pathModule.resolve(resolvedBasePath, path);
1112
+ if (this.root !== void 0) {
1113
+ const refuse = (detail) => {
1114
+ const err = new Error(`Refusing include outside the include root: ${detail}`);
1115
+ err.name = "IncludeContainmentError";
1116
+ throw err;
1117
+ };
1118
+ if (this.realRoot === void 0) {
1119
+ try {
1120
+ this.realRoot = await fs.realpath(this.root);
1121
+ } catch (error) {
1122
+ const message = error instanceof Error ? error.message : String(error);
1123
+ refuse(`include root ${this.root} does not resolve (${message})`);
1124
+ }
1125
+ }
1126
+ const lexical = pathModule.resolve(resolvedPath);
1127
+ if (lexical !== this.realRoot && !lexical.startsWith(this.realRoot + pathModule.sep) && lexical !== this.root && !lexical.startsWith(pathModule.resolve(this.root) + pathModule.sep)) {
1128
+ refuse(`"${path}" resolves to ${lexical}, which is not inside ${this.realRoot}`);
1129
+ }
1130
+ let realTarget;
1131
+ try {
1132
+ realTarget = await fs.realpath(resolvedPath);
1133
+ } catch (error) {
1134
+ const message = error instanceof Error ? error.message : String(error);
1135
+ throw new Error(`Failed to load ${resolvedPath}: ${message}`);
1136
+ }
1137
+ if (realTarget !== this.realRoot && !realTarget.startsWith(this.realRoot + pathModule.sep)) {
1138
+ refuse(`"${path}" resolves to ${realTarget}, which is not inside ${this.realRoot}`);
1139
+ }
1140
+ try {
1141
+ return await fs.readFile(realTarget, "utf-8");
1142
+ } catch (error) {
1143
+ const message = error instanceof Error ? error.message : String(error);
1144
+ throw new Error(`Failed to load ${resolvedPath}: ${message}`);
1145
+ }
1146
+ }
1147
+ try {
1148
+ return await fs.readFile(resolvedPath, "utf-8");
1149
+ } catch (error) {
1150
+ const message = error instanceof Error ? error.message : String(error);
1151
+ throw new Error(`Failed to load ${resolvedPath}: ${message}`);
1152
+ }
1153
+ }
1154
+ };
1155
+
1156
+ // src/cli.ts
1157
+ var program = new Command();
1158
+ 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").option(
1159
+ "-r, --include-root <dir>",
1160
+ "Refuse {% include %} targets that resolve (after symlinks) outside this directory"
1161
+ ).action(async (templatePath, options) => {
1162
+ try {
1163
+ let template;
1164
+ let baseDir = process.cwd();
1165
+ if (templatePath) {
1166
+ const fullPath = resolve(templatePath);
1167
+ template = await readFile(fullPath, "utf-8");
1168
+ baseDir = dirname(fullPath);
1169
+ } else {
1170
+ template = await readStdin();
1171
+ }
1172
+ let context = {};
1173
+ if (options.context) {
1174
+ const contextPath = resolve(options.context);
1175
+ const contextContent = await readFile(contextPath, "utf-8");
1176
+ if (contextPath.endsWith(".json")) {
1177
+ context = JSON.parse(contextContent);
1178
+ } else if (contextPath.endsWith(".yaml") || contextPath.endsWith(".yml")) {
1179
+ context = parseYAML(contextContent);
1180
+ } else {
1181
+ throw new Error("Context file must be .json, .yaml, or .yml");
1182
+ }
1183
+ }
1184
+ if (options.vars) {
1185
+ const inlineVars = JSON.parse(options.vars);
1186
+ context = { ...context, ...inlineVars };
1187
+ }
1188
+ const loader = new FileSystemLoader(
1189
+ baseDir,
1190
+ options.includeRoot ? { root: resolve(options.includeRoot) } : {}
1191
+ );
1192
+ const result = await render(template, {
1193
+ loader,
1194
+ basePath: baseDir,
1195
+ context,
1196
+ maxIncludeDepth: parseInt(options.maxDepth || "10"),
1197
+ timeout: parseInt(options.timeout || "5000")
1198
+ });
1199
+ if (options.output) {
1200
+ const outputPath = resolve(options.output);
1201
+ await writeFile(outputPath, result, "utf-8");
1202
+ console.error(`\u2713 Rendered to ${outputPath}`);
1203
+ } else {
1204
+ process.stdout.write(result);
1205
+ }
1206
+ } catch (error) {
1207
+ console.error("Error:", error instanceof Error ? error.message : String(error));
1208
+ process.exit(1);
1209
+ }
1210
+ });
1211
+ program.parse();
1212
+ async function readStdin() {
1213
+ return new Promise((resolve2, reject) => {
1214
+ const chunks = [];
1215
+ process.stdin.on("data", (chunk) => {
1216
+ chunks.push(chunk);
1217
+ });
1218
+ process.stdin.on("end", () => {
1219
+ resolve2(Buffer.concat(chunks).toString("utf-8"));
1220
+ });
1221
+ process.stdin.on("error", (error) => {
1222
+ reject(error);
1223
+ });
1224
+ });
1225
+ }