@formfeed/engine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -0
  3. package/index.cjs +3306 -0
  4. package/index.js +3273 -0
  5. package/package.json +70 -0
  6. package/types/index.d.cts +17 -0
  7. package/types/index.d.ts +17 -0
  8. package/types/lib/analysis/tags.d.cts +36 -0
  9. package/types/lib/analysis/tags.d.ts +36 -0
  10. package/types/lib/assemble.d.cts +147 -0
  11. package/types/lib/assemble.d.ts +147 -0
  12. package/types/lib/engines/handlebars.d.cts +3 -0
  13. package/types/lib/engines/handlebars.d.ts +3 -0
  14. package/types/lib/engines/index.d.cts +7 -0
  15. package/types/lib/engines/index.d.ts +7 -0
  16. package/types/lib/engines/jinja2.d.cts +5 -0
  17. package/types/lib/engines/jinja2.d.ts +5 -0
  18. package/types/lib/engines/liquid.d.cts +16 -0
  19. package/types/lib/engines/liquid.d.ts +16 -0
  20. package/types/lib/errors.d.cts +24 -0
  21. package/types/lib/errors.d.ts +24 -0
  22. package/types/lib/helpers/codes.d.cts +21 -0
  23. package/types/lib/helpers/codes.d.ts +21 -0
  24. package/types/lib/helpers/collections.d.cts +2 -0
  25. package/types/lib/helpers/collections.d.ts +2 -0
  26. package/types/lib/helpers/document.d.cts +29 -0
  27. package/types/lib/helpers/document.d.ts +29 -0
  28. package/types/lib/helpers/format.d.cts +5 -0
  29. package/types/lib/helpers/format.d.ts +5 -0
  30. package/types/lib/helpers/index.d.cts +17 -0
  31. package/types/lib/helpers/index.d.ts +17 -0
  32. package/types/lib/helpers/logic.d.cts +2 -0
  33. package/types/lib/helpers/logic.d.ts +2 -0
  34. package/types/lib/helpers/registry.d.cts +18 -0
  35. package/types/lib/helpers/registry.d.ts +18 -0
  36. package/types/lib/import/apitemplate.d.cts +45 -0
  37. package/types/lib/import/apitemplate.d.ts +45 -0
  38. package/types/lib/limits.d.cts +10 -0
  39. package/types/lib/limits.d.ts +10 -0
  40. package/types/lib/preview.d.cts +30 -0
  41. package/types/lib/preview.d.ts +30 -0
  42. package/types/lib/schema.d.cts +22 -0
  43. package/types/lib/schema.d.ts +22 -0
  44. package/types/lib/snippets.d.cts +22 -0
  45. package/types/lib/snippets.d.ts +22 -0
  46. package/types/lib/types.d.cts +140 -0
  47. package/types/lib/types.d.ts +140 -0
  48. package/types/types/modules.d.cts +85 -0
  49. package/types/types/modules.d.ts +85 -0
package/index.js ADDED
@@ -0,0 +1,3273 @@
1
+ // packages/engine/src/lib/errors.ts
2
+ var EngineSyntaxError = class extends Error {
3
+ constructor(engine, message, line, column) {
4
+ super(message);
5
+ this.engine = engine;
6
+ this.line = line;
7
+ this.column = column;
8
+ }
9
+ name = "EngineSyntaxError";
10
+ get position() {
11
+ return { line: this.line, column: this.column };
12
+ }
13
+ };
14
+ var RenderLimitError = class extends Error {
15
+ constructor(limit, message) {
16
+ super(message);
17
+ this.limit = limit;
18
+ }
19
+ name = "RenderLimitError";
20
+ };
21
+ var RenderError = class extends Error {
22
+ constructor(engine, message, line, column) {
23
+ super(message);
24
+ this.engine = engine;
25
+ this.line = line;
26
+ this.column = column;
27
+ }
28
+ name = "RenderError";
29
+ };
30
+
31
+ // packages/engine/src/lib/limits.ts
32
+ var defaultLimits = {
33
+ ms: 5e3,
34
+ outputBytes: 20 * 1024 * 1024,
35
+ includeDepth: 8
36
+ };
37
+ var encoder = new TextEncoder();
38
+ function enforceLimits(output, startedAt, limits) {
39
+ const elapsed = Date.now() - startedAt;
40
+ if (elapsed > limits.ms)
41
+ throw new RenderLimitError(
42
+ "ms",
43
+ `Template render took ${elapsed} ms; the limit is ${limits.ms} ms`
44
+ );
45
+ if (output.length > limits.outputBytes / 4) {
46
+ const bytes = encoder.encode(output).length;
47
+ if (bytes > limits.outputBytes) {
48
+ throw new RenderLimitError(
49
+ "outputBytes",
50
+ `Rendered HTML is ${bytes} bytes; the limit is ${limits.outputBytes} bytes`
51
+ );
52
+ }
53
+ }
54
+ return output;
55
+ }
56
+ function depthGuard(resolve2, includeDepth) {
57
+ const stack = [];
58
+ return (name) => {
59
+ if (stack.length >= includeDepth)
60
+ throw new RenderLimitError(
61
+ "includeDepth",
62
+ `Includes nest deeper than ${includeDepth} (${[...stack, name].join(" \u2192 ")})`
63
+ );
64
+ if (stack.includes(name))
65
+ throw new RenderLimitError(
66
+ "includeDepth",
67
+ `Partial "${name}" includes itself`
68
+ );
69
+ stack.push(name);
70
+ try {
71
+ return resolve2(name);
72
+ } finally {
73
+ stack.pop();
74
+ }
75
+ };
76
+ }
77
+
78
+ // packages/engine/src/lib/helpers/codes.ts
79
+ import { toSVG } from "bwip-js";
80
+ import { create as createQr } from "qrcode";
81
+ import { render as renderQrSvg } from "qrcode/lib/renderer/svg-tag.js";
82
+
83
+ // packages/engine/src/lib/helpers/format.ts
84
+ import { TZDate } from "@date-fns/tz";
85
+ import { add, format as formatDate, isValid, parseISO } from "date-fns";
86
+ import { de, enGB, enUS, es, fr, it, nl } from "date-fns/locale";
87
+ import { marked } from "marked";
88
+ import { toCardinal as deWords } from "n2words/de";
89
+ import { toCardinal as enWords } from "n2words/en";
90
+ import { toCardinal as esWords } from "n2words/es";
91
+ import { toCardinal as frWords } from "n2words/fr";
92
+ import { toCardinal as itWords } from "n2words/it";
93
+ import { toCardinal as nlWords } from "n2words/nl";
94
+ var dateLocales = {
95
+ de,
96
+ en: enGB,
97
+ "en-GB": enGB,
98
+ "en-US": enUS,
99
+ fr,
100
+ es,
101
+ it,
102
+ nl
103
+ };
104
+ function dateLocale(locale) {
105
+ return dateLocales[locale] ?? dateLocales[locale.split("-")[0] ?? ""] ?? enGB;
106
+ }
107
+ function toNumber(value) {
108
+ if (typeof value === "number") return value;
109
+ if (typeof value === "string") {
110
+ const n = Number(value.replace(/\s/g, "").replace(",", "."));
111
+ return Number.isFinite(n) ? n : NaN;
112
+ }
113
+ if (typeof value === "bigint") return Number(value);
114
+ return NaN;
115
+ }
116
+ function toDate(value, tz) {
117
+ if (value === "now" || value === void 0 || value === null || value === "")
118
+ return new TZDate(Date.now(), tz);
119
+ if (value instanceof Date) return new TZDate(value.getTime(), tz);
120
+ if (typeof value === "number")
121
+ return new TZDate(value < 1e12 ? value * 1e3 : value, tz);
122
+ if (typeof value === "string") {
123
+ const iso = parseISO(value);
124
+ if (isValid(iso)) {
125
+ return /^\d{4}-\d{2}-\d{2}$/.test(value) ? new TZDate(iso.getFullYear(), iso.getMonth(), iso.getDate(), tz) : new TZDate(iso.getTime(), tz);
126
+ }
127
+ const parsed = new Date(value);
128
+ if (isValid(parsed)) return new TZDate(parsed.getTime(), tz);
129
+ }
130
+ return null;
131
+ }
132
+ function escapeHtml(value) {
133
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
134
+ }
135
+ var str = (v) => v === void 0 || v === null ? "" : String(v);
136
+ var opt = (v, fallback) => v === void 0 || v === null || v === "" ? fallback : v;
137
+ var wordsByLang = {
138
+ de: deWords,
139
+ en: enWords,
140
+ es: esWords,
141
+ fr: frWords,
142
+ it: itWords,
143
+ nl: nlWords
144
+ };
145
+ var formatHelpers = [
146
+ {
147
+ name: "money",
148
+ aliases: ["currency"],
149
+ deprecatedAlias: { currency_format: "money" },
150
+ doc: {
151
+ signature: "money(value, currency?, locale?)",
152
+ description: "Formats a number as currency with Intl.NumberFormat. Defaults come from the template settings.",
153
+ example: "{{ line.total | money('EUR') }} \u2192 1.234,50 \u20AC",
154
+ category: "format"
155
+ },
156
+ fn: (ctx, value, currency, locale) => {
157
+ const n = toNumber(value);
158
+ if (Number.isNaN(n)) return str(value);
159
+ return new Intl.NumberFormat(opt(locale, ctx.locale), {
160
+ style: "currency",
161
+ currency: opt(currency, ctx.currency)
162
+ }).format(n);
163
+ }
164
+ },
165
+ {
166
+ name: "number",
167
+ doc: {
168
+ signature: "number(value, decimals?, locale?)",
169
+ description: "Formats a number with grouping and a fixed number of decimals.",
170
+ example: "{{ qty | number(2) }} \u2192 1.234,00",
171
+ category: "format"
172
+ },
173
+ fn: (ctx, value, decimals, locale) => {
174
+ const n = toNumber(value);
175
+ if (Number.isNaN(n)) return str(value);
176
+ const d = decimals === void 0 || decimals === null ? void 0 : Number(decimals);
177
+ return new Intl.NumberFormat(opt(locale, ctx.locale), {
178
+ minimumFractionDigits: d,
179
+ maximumFractionDigits: d ?? 3
180
+ }).format(n);
181
+ }
182
+ },
183
+ {
184
+ name: "date",
185
+ doc: {
186
+ signature: "date(value, format?, tz?, locale?)",
187
+ description: "Formats a date with date-fns tokens (e.g. 'dd.MM.yyyy', 'PPP'). Accepts ISO strings, epoch numbers and 'now'.",
188
+ example: "{{ invoice.date | date('dd.MM.yyyy') }}",
189
+ category: "format"
190
+ },
191
+ fn: (ctx, value, format, tz, locale) => {
192
+ const d = toDate(value, opt(tz, ctx.timezone));
193
+ if (!d) return str(value);
194
+ return formatDate(d, opt(format, "PP"), {
195
+ locale: dateLocale(opt(locale, ctx.locale))
196
+ });
197
+ }
198
+ },
199
+ {
200
+ name: "dateAdd",
201
+ aliases: ["date_add"],
202
+ doc: {
203
+ signature: "dateAdd(value, amount, unit)",
204
+ description: "Adds days, weeks, months or years to a date; returns an ISO string for further formatting.",
205
+ example: "{{ invoice.date | dateAdd(14, 'days') | date('PP') }}",
206
+ category: "format"
207
+ },
208
+ fn: (ctx, value, amount, unit) => {
209
+ const d = toDate(value, ctx.timezone);
210
+ if (!d) return str(value);
211
+ const n = toNumber(amount);
212
+ const u = str(unit || "days").replace(/s?$/, "s");
213
+ return add(d, { [u]: n }).toISOString();
214
+ }
215
+ },
216
+ {
217
+ name: "add",
218
+ aliases: ["plus"],
219
+ doc: {
220
+ signature: "add(a, b)",
221
+ description: "a + b (Handlebars has no arithmetic of its own).",
222
+ example: "{{add @index 1}}",
223
+ category: "format"
224
+ },
225
+ fn: (_ctx, a, b) => toNumber(a) + toNumber(b)
226
+ },
227
+ {
228
+ name: "subtract",
229
+ aliases: ["minus", "sub"],
230
+ doc: {
231
+ signature: "subtract(a, b)",
232
+ description: "a minus b.",
233
+ example: "{{subtract total discount}}",
234
+ category: "format"
235
+ },
236
+ fn: (_ctx, a, b) => toNumber(a) - toNumber(b)
237
+ },
238
+ {
239
+ name: "multiply",
240
+ aliases: ["times", "mul"],
241
+ doc: {
242
+ signature: "multiply(a, b)",
243
+ description: "a times b.",
244
+ example: "{{money (multiply line.qty line.price)}}",
245
+ category: "format"
246
+ },
247
+ fn: (_ctx, a, b) => toNumber(a) * toNumber(b)
248
+ },
249
+ {
250
+ name: "divide",
251
+ aliases: ["divided_by", "div"],
252
+ doc: {
253
+ signature: "divide(a, b)",
254
+ description: "a divided by b; empty when b is 0.",
255
+ example: "{{divide total count}}",
256
+ category: "format"
257
+ },
258
+ fn: (_ctx, a, b) => toNumber(b) === 0 ? "" : toNumber(a) / toNumber(b)
259
+ },
260
+ {
261
+ name: "round",
262
+ doc: {
263
+ signature: "round(value, decimals = 0)",
264
+ description: "Rounds half away from zero.",
265
+ example: "{{ 2.345 | round(2) }} gives 2.35",
266
+ category: "format"
267
+ },
268
+ fn: (_ctx, value, decimals) => {
269
+ const n = toNumber(value);
270
+ const f = 10 ** (decimals === void 0 || decimals === null ? 0 : Number(decimals));
271
+ return Number.isNaN(n) ? "" : Math.round((Math.abs(n) + Number.EPSILON) * f) / f * Math.sign(n);
272
+ }
273
+ },
274
+ {
275
+ name: "upper",
276
+ aliases: ["upcase"],
277
+ doc: {
278
+ signature: "upper(value)",
279
+ description: "Upper-cases a string.",
280
+ example: "{{ name | upper }}",
281
+ category: "text"
282
+ },
283
+ fn: (_ctx, value) => str(value).toUpperCase()
284
+ },
285
+ {
286
+ name: "lower",
287
+ aliases: ["downcase"],
288
+ doc: {
289
+ signature: "lower(value)",
290
+ description: "Lower-cases a string.",
291
+ example: "{{ name | lower }}",
292
+ category: "text"
293
+ },
294
+ fn: (_ctx, value) => str(value).toLowerCase()
295
+ },
296
+ {
297
+ name: "title",
298
+ aliases: ["capitalize_words"],
299
+ doc: {
300
+ signature: "title(value)",
301
+ description: "Capitalises every word.",
302
+ example: "{{ 'hello world' | title }} \u2192 Hello World",
303
+ category: "text"
304
+ },
305
+ fn: (_ctx, value) => str(value).replace(
306
+ /\p{L}[\p{L}\p{M}]*/gu,
307
+ (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
308
+ )
309
+ },
310
+ {
311
+ name: "truncate",
312
+ doc: {
313
+ signature: "truncate(value, length = 255, suffix = '\u2026')",
314
+ description: "Cuts a string to `length` characters and appends the suffix when cut.",
315
+ example: "{{ description | truncate(40) }}",
316
+ category: "text"
317
+ },
318
+ fn: (_ctx, value, length, suffix) => {
319
+ const s = str(value);
320
+ const max = opt(Number(length), 255);
321
+ const end = opt(suffix, "\u2026");
322
+ return s.length <= max ? s : s.slice(0, Math.max(0, max - end.length)) + end;
323
+ }
324
+ },
325
+ {
326
+ name: "replace",
327
+ doc: {
328
+ signature: "replace(value, search, replacement)",
329
+ description: "Replaces every occurrence of `search`.",
330
+ example: "{{ iban | replace(' ', '') }}",
331
+ category: "text"
332
+ },
333
+ fn: (_ctx, value, search, replacement) => str(value).split(str(search)).join(str(replacement))
334
+ },
335
+ {
336
+ name: "split",
337
+ doc: {
338
+ signature: "split(value, separator = ',')",
339
+ description: "Splits a string into a list.",
340
+ example: "{% for tag in tags | split(',') %}",
341
+ category: "text"
342
+ },
343
+ fn: (_ctx, value, separator) => str(value).split(str(opt(separator, ",")))
344
+ },
345
+ {
346
+ name: "join",
347
+ doc: {
348
+ signature: "join(list, separator = ', ')",
349
+ description: "Joins a list into a string.",
350
+ example: "{{ tags | join(' \xB7 ') }}",
351
+ category: "collection"
352
+ },
353
+ fn: (_ctx, value, separator) => Array.isArray(value) ? value.map(str).join(str(opt(separator, ", "))) : str(value)
354
+ },
355
+ {
356
+ name: "default",
357
+ aliases: ["d"],
358
+ doc: {
359
+ signature: "default(value, fallback)",
360
+ description: "Returns the fallback when the value is empty, null or undefined.",
361
+ example: "{{ customer.vat | default('\u2014') }}",
362
+ category: "text"
363
+ },
364
+ fn: (_ctx, value, fallback) => value === void 0 || value === null || value === "" ? fallback : value
365
+ },
366
+ {
367
+ name: "nl2br",
368
+ html: true,
369
+ doc: {
370
+ signature: "nl2br(value)",
371
+ description: "Escapes the text and turns line breaks into <br>.",
372
+ example: "{{ address | nl2br }}",
373
+ category: "text"
374
+ },
375
+ fn: (_ctx, value) => escapeHtml(str(value)).replace(/\r?\n/g, "<br>\n")
376
+ },
377
+ {
378
+ name: "markdown",
379
+ html: true,
380
+ doc: {
381
+ signature: "markdown(value)",
382
+ description: "Renders Markdown to HTML (no sanitiser: template data is trusted by the author).",
383
+ example: "{{ notes | markdown }}",
384
+ category: "text"
385
+ },
386
+ fn: (_ctx, value) => marked.parse(str(value), {
387
+ async: false,
388
+ gfm: true,
389
+ breaks: false
390
+ })
391
+ },
392
+ {
393
+ name: "safe",
394
+ aliases: ["raw"],
395
+ html: true,
396
+ doc: {
397
+ signature: "safe(value)",
398
+ description: "Marks a string as HTML so the engine does not escape it.",
399
+ example: "{{ html_snippet | safe }}",
400
+ category: "text"
401
+ },
402
+ fn: (_ctx, value) => str(value)
403
+ },
404
+ {
405
+ name: "json",
406
+ aliases: ["tojson", "dump"],
407
+ doc: {
408
+ signature: "json(value, indent = 2)",
409
+ description: "Serialises a value as JSON; handy while building a template.",
410
+ example: "<pre>{{ invoice | json }}</pre>",
411
+ category: "debug"
412
+ },
413
+ fn: (_ctx, value, indent) => JSON.stringify(value, null, opt(Number(indent), 2))
414
+ },
415
+ {
416
+ name: "numToWords",
417
+ aliases: ["num_to_words", "spellout"],
418
+ doc: {
419
+ signature: "numToWords(value, locale?)",
420
+ description: "Spells a number out in words (de, en, fr, es, it, nl).",
421
+ example: "{{ total | numToWords('de') }} \u2192 zweihundertdreiundvierzig",
422
+ category: "format"
423
+ },
424
+ fn: (ctx, value, locale) => {
425
+ const n = toNumber(value);
426
+ if (Number.isNaN(n)) return str(value);
427
+ const lang = str(opt(locale, ctx.locale)).split("-")[0] ?? "en";
428
+ return (wordsByLang[lang] ?? enWords)(n);
429
+ }
430
+ },
431
+ {
432
+ name: "t",
433
+ aliases: ["translate"],
434
+ doc: {
435
+ signature: "t(key, params?)",
436
+ description: "Looks a key up in the template's i18n dictionary for the request locale; `{name}` placeholders come from params.",
437
+ example: "{{ t('invoice.title', { number: invoice.number }) }}",
438
+ category: "text"
439
+ },
440
+ fn: (ctx, key, params) => {
441
+ const dictionaries = ctx.i18n ?? {};
442
+ const lang = ctx.locale.split("-")[0] ?? ctx.locale;
443
+ const text = dictionaries[ctx.locale]?.[str(key)] ?? dictionaries[lang]?.[str(key)] ?? dictionaries["en"]?.[str(key)] ?? str(key);
444
+ const p = params ?? {};
445
+ return text.replace(/\{(\w+)\}/g, (_, name) => str(p[name]));
446
+ }
447
+ },
448
+ {
449
+ name: "asset",
450
+ doc: {
451
+ signature: "asset(name)",
452
+ description: "URL of a workspace asset (logo, background) on the CDN.",
453
+ example: `<img src="{{ asset('logo.png') }}">`,
454
+ category: "document"
455
+ },
456
+ fn: (ctx, name) => `${(ctx.assetBaseUrl ?? "").replace(/\/$/, "")}/${str(name).replace(/^\//, "")}`
457
+ },
458
+ {
459
+ name: "pageBreak",
460
+ aliases: ["page_break"],
461
+ html: true,
462
+ doc: {
463
+ signature: "pageBreak()",
464
+ description: "Starts a new page in PDF output.",
465
+ example: "{{ pageBreak() }}",
466
+ category: "document"
467
+ },
468
+ fn: () => '<div class="page-break" style="break-after:page"></div>'
469
+ }
470
+ ];
471
+
472
+ // packages/engine/src/lib/helpers/codes.ts
473
+ var svgDataUri = (svg) => `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
474
+ var barcodeTypes = {
475
+ code128: "code128",
476
+ ean13: "ean13",
477
+ ean8: "ean8",
478
+ upc: "upca",
479
+ upca: "upca",
480
+ itf14: "itf14",
481
+ code39: "code39",
482
+ qrcode: "qrcode",
483
+ datamatrix: "datamatrix",
484
+ pdf417: "pdf417"
485
+ };
486
+ function qrSvg(value, options = {}) {
487
+ const qr = createQr(value, { errorCorrectionLevel: options.ecc ?? "M" });
488
+ const svg = renderQrSvg(qr, {
489
+ width: options.size ?? 160,
490
+ margin: options.margin ?? 1,
491
+ color: {
492
+ dark: (options.color ?? "#000000").replace(/^(#?)/, "#").slice(0, 9),
493
+ light: (options.background ?? "#ffffff").replace(/^(#?)/, "#")
494
+ }
495
+ });
496
+ return svg;
497
+ }
498
+ function epcPayload(input) {
499
+ const amount = input.amount === void 0 || input.amount === "" ? "" : `EUR${toNumber(input.amount).toFixed(2)}`;
500
+ return [
501
+ "BCD",
502
+ "002",
503
+ "1",
504
+ "SCT",
505
+ (input.bic ?? "").replace(/\s/g, ""),
506
+ String(input.name ?? "").slice(0, 70),
507
+ String(input.iban ?? "").replace(/\s/g, ""),
508
+ amount,
509
+ (input.purpose ?? "").slice(0, 4),
510
+ (input.reference ?? "").slice(0, 35),
511
+ input.reference ? "" : (input.text ?? "").slice(0, 140),
512
+ ""
513
+ ].join("\n");
514
+ }
515
+ var codeHelpers = [
516
+ {
517
+ name: "qrcode",
518
+ aliases: ["qr"],
519
+ doc: {
520
+ signature: "qrcode(value, { size = 160, ecc = 'M', margin = 1, color, background })",
521
+ description: "QR code as an SVG data URI for an <img> src.",
522
+ example: '<img src="{{ qrcode(order.url, { size: 120 }) }}">',
523
+ category: "code"
524
+ },
525
+ fn: (_ctx, value, options) => svgDataUri(qrSvg(String(value ?? ""), options ?? {}))
526
+ },
527
+ {
528
+ name: "barcode",
529
+ doc: {
530
+ signature: "barcode(value, { type = 'code128', height = 12, width, text = true, scale = 2 })",
531
+ description: "Barcode (code128, ean13, ean8, upc, itf14, code39, datamatrix, pdf417) as an SVG data URI.",
532
+ example: `<img src="{{ barcode(item.sku, { type: 'code128' }) }}">`,
533
+ category: "code"
534
+ },
535
+ fn: (_ctx, value, options) => {
536
+ const o = options ?? {};
537
+ const bcid = barcodeTypes[String(o.type ?? "code128").toLowerCase()] ?? "code128";
538
+ const svg = toSVG({
539
+ bcid,
540
+ text: String(value ?? ""),
541
+ height: o.height ?? 12,
542
+ ...o.width === void 0 ? {} : { width: o.width },
543
+ scale: o.scale ?? 2,
544
+ includetext: o.text !== false,
545
+ textxalign: "center"
546
+ });
547
+ return svgDataUri(svg);
548
+ }
549
+ },
550
+ {
551
+ name: "epcQr",
552
+ aliases: ["epc_qr", "girocode"],
553
+ doc: {
554
+ signature: "epcQr({ name, iban, bic?, amount?, reference?, text? }, { size })",
555
+ description: "SEPA credit transfer QR code (EPC069-12 / GiroCode) that banking apps scan to prefill a payment.",
556
+ example: '<img src="{{ epcQr({ name: company.name, iban: company.iban, amount: invoice.total, reference: invoice.number }) }}">',
557
+ category: "code"
558
+ },
559
+ // Payment fields and QR options may arrive together: Jinja2 passes two objects, Handlebars one
560
+ // hash (`{{epcQr name=… iban=… size=200}}`), and Liquid, which cannot build an object literal,
561
+ // keyword arguments (`{{ company.iban | epcQr: name: company.name, amount: invoice.total }}`,
562
+ // where a string input is the IBAN). They are merged and split by field name.
563
+ fn: (_ctx, input, options) => {
564
+ const fields = {
565
+ ...input && typeof input === "object" ? input : typeof input === "string" && input ? { iban: input } : {},
566
+ ...options ?? {}
567
+ };
568
+ const payment = {};
569
+ const qr = {};
570
+ for (const [key, value] of Object.entries(fields))
571
+ (EPC_FIELDS.has(key) ? payment : qr)[key] = value;
572
+ return svgDataUri(
573
+ qrSvg(epcPayload(payment), {
574
+ ecc: "M",
575
+ ...qr
576
+ })
577
+ );
578
+ }
579
+ }
580
+ ];
581
+ var EPC_FIELDS = /* @__PURE__ */ new Set(["name", "iban", "bic", "amount", "reference", "text", "purpose"]);
582
+
583
+ // packages/engine/src/lib/helpers/collections.ts
584
+ var list = (v) => Array.isArray(v) ? v : v === void 0 || v === null ? [] : [v];
585
+ var get = (item, path) => String(path ?? "").split(".").filter(Boolean).reduce(
586
+ (acc, key) => acc && typeof acc === "object" ? acc[key] : void 0,
587
+ item
588
+ );
589
+ var numbers = (v, path) => list(v).map((item) => toNumber(path ? get(item, path) : item)).filter((n) => !Number.isNaN(n));
590
+ var collectionHelpers = [
591
+ {
592
+ name: "sum",
593
+ doc: {
594
+ signature: "sum(list, path?)",
595
+ description: "Sum of the items, or of `path` inside each item.",
596
+ example: "{{ invoice.lines | sum('total') | money }}",
597
+ category: "collection"
598
+ },
599
+ fn: (_ctx, value, path) => numbers(value, path).reduce((a, b) => a + b, 0)
600
+ },
601
+ {
602
+ name: "avg",
603
+ doc: {
604
+ signature: "avg(list, path?)",
605
+ description: "Average of the items or of `path` inside each item.",
606
+ example: "{{ scores | avg | number(1) }}",
607
+ category: "collection"
608
+ },
609
+ fn: (_ctx, value, path) => {
610
+ const ns = numbers(value, path);
611
+ return ns.length ? ns.reduce((a, b) => a + b, 0) / ns.length : 0;
612
+ }
613
+ },
614
+ {
615
+ name: "min",
616
+ doc: {
617
+ signature: "min(list, path?)",
618
+ description: "Smallest number.",
619
+ example: "{{ prices | min }}",
620
+ category: "collection"
621
+ },
622
+ fn: (_ctx, value, path) => Math.min(...numbers(value, path))
623
+ },
624
+ {
625
+ name: "max",
626
+ doc: {
627
+ signature: "max(list, path?)",
628
+ description: "Largest number.",
629
+ example: "{{ prices | max }}",
630
+ category: "collection"
631
+ },
632
+ fn: (_ctx, value, path) => Math.max(...numbers(value, path))
633
+ },
634
+ {
635
+ name: "groupBy",
636
+ aliases: ["group_by", "groupby"],
637
+ doc: {
638
+ signature: "groupBy(list, path)",
639
+ description: "Groups items by a field; returns a list of `{ key, items }`.",
640
+ example: "{% for group in lines | groupBy('category') %}{{ group.key }}{% endfor %}",
641
+ category: "collection"
642
+ },
643
+ fn: (_ctx, value, path) => {
644
+ const groups = /* @__PURE__ */ new Map();
645
+ for (const item of list(value)) {
646
+ const key = String(get(item, path) ?? "");
647
+ groups.set(key, [...groups.get(key) ?? [], item]);
648
+ }
649
+ return [...groups.entries()].map(([key, items]) => ({ key, items }));
650
+ }
651
+ },
652
+ {
653
+ name: "sortBy",
654
+ aliases: ["sort_by", "sort"],
655
+ doc: {
656
+ signature: "sortBy(list, path?, direction = 'asc')",
657
+ description: "Sorts items by a field (numbers numerically, strings by locale).",
658
+ example: "{% for l in lines | sortBy('position') %}",
659
+ category: "collection"
660
+ },
661
+ fn: (ctx, value, path, direction) => {
662
+ const dir = String(direction ?? "asc").toLowerCase() === "desc" ? -1 : 1;
663
+ const collator = new Intl.Collator(ctx.locale, { numeric: true });
664
+ return [...list(value)].sort((a, b) => {
665
+ const x = path ? get(a, path) : a;
666
+ const y = path ? get(b, path) : b;
667
+ if (typeof x === "number" && typeof y === "number")
668
+ return (x - y) * dir;
669
+ return collator.compare(String(x ?? ""), String(y ?? "")) * dir;
670
+ });
671
+ }
672
+ },
673
+ {
674
+ name: "where",
675
+ aliases: ["filter_by"],
676
+ doc: {
677
+ signature: "where(list, path, value?)",
678
+ description: "Keeps items whose field equals `value` (or is truthy when omitted).",
679
+ example: "{% for l in lines | where('taxable', true) %}",
680
+ category: "collection"
681
+ },
682
+ fn: (_ctx, value, path, expected) => list(value).filter((item) => {
683
+ const v = get(item, path);
684
+ return expected === void 0 ? Boolean(v) : v === expected || String(v) === String(expected);
685
+ })
686
+ },
687
+ {
688
+ name: "pluck",
689
+ aliases: ["map"],
690
+ doc: {
691
+ signature: "pluck(list, path)",
692
+ description: "Extracts one field from every item.",
693
+ example: "{{ lines | pluck('sku') | join(', ') }}",
694
+ category: "collection"
695
+ },
696
+ fn: (_ctx, value, path) => list(value).map((item) => get(item, path))
697
+ },
698
+ {
699
+ name: "chunk",
700
+ aliases: ["batch"],
701
+ doc: {
702
+ signature: "chunk(list, size)",
703
+ description: "Splits a list into lists of `size` items (rows of labels, columns).",
704
+ example: "{% for row in labels | chunk(3) %}",
705
+ category: "collection"
706
+ },
707
+ fn: (_ctx, value, size) => {
708
+ const n = Math.max(1, toNumber(size) || 1);
709
+ const items = list(value);
710
+ const out = [];
711
+ for (let i = 0; i < items.length; i += n) out.push(items.slice(i, i + n));
712
+ return out;
713
+ }
714
+ },
715
+ {
716
+ name: "range",
717
+ doc: {
718
+ signature: "range(start, end?, step = 1)",
719
+ description: "List of numbers from start to end (exclusive).",
720
+ example: "{% for i in range(1, 4) %}",
721
+ category: "collection"
722
+ },
723
+ fn: (_ctx, start, end, step) => {
724
+ let from = toNumber(start);
725
+ const to = end === void 0 || end === null ? from : toNumber(end);
726
+ if (end === void 0 || end === null) from = 0;
727
+ const by = toNumber(step) || 1;
728
+ const out = [];
729
+ for (let i = from; by > 0 ? i < to : i > to; i += by) out.push(i);
730
+ return out;
731
+ }
732
+ },
733
+ {
734
+ name: "first",
735
+ doc: {
736
+ signature: "first(list)",
737
+ description: "First item.",
738
+ example: "{{ lines | first }}",
739
+ category: "collection"
740
+ },
741
+ fn: (_ctx, value) => list(value)[0]
742
+ },
743
+ {
744
+ name: "last",
745
+ doc: {
746
+ signature: "last(list)",
747
+ description: "Last item.",
748
+ example: "{{ lines | last }}",
749
+ category: "collection"
750
+ },
751
+ fn: (_ctx, value) => list(value).at(-1)
752
+ },
753
+ {
754
+ name: "length",
755
+ aliases: ["size", "count"],
756
+ doc: {
757
+ signature: "length(value)",
758
+ description: "Number of items in a list, keys in an object or characters in a string.",
759
+ example: "{{ lines | length }}",
760
+ category: "collection"
761
+ },
762
+ fn: (_ctx, value) => Array.isArray(value) || typeof value === "string" ? value.length : value && typeof value === "object" ? Object.keys(value).length : 0
763
+ }
764
+ ];
765
+
766
+ // packages/engine/src/lib/helpers/document.ts
767
+ var css = (value) => {
768
+ if (value === void 0 || value === null || value === "") return void 0;
769
+ const n = toNumber(value);
770
+ return Number.isFinite(n) && String(value).trim() === String(n) ? `${n}px` : String(value);
771
+ };
772
+ function chartPayload(spec) {
773
+ const options = { ...spec.options ?? {} };
774
+ options["animation"] = false;
775
+ options["responsive"] = false;
776
+ return { type: spec.type ?? "bar", data: spec.data ?? {}, options };
777
+ }
778
+ function chartMarkup(spec, id) {
779
+ const width = toNumber(spec.width ?? 480) || 480;
780
+ const height = toNumber(spec.height ?? 240) || 240;
781
+ const payload = escapeHtml(JSON.stringify(chartPayload(spec)));
782
+ const idAttr = id ? ` id="${escapeHtml(id)}"` : "";
783
+ return `<canvas class="ff-chart"${idAttr} width="${width}" height="${height}" style="width:${width}px;height:${height}px" data-ff-chart="${payload}"></canvas>`;
784
+ }
785
+ function imageMarkup(url, options = {}) {
786
+ const styles = [
787
+ css(options.width) ? `width:${css(options.width)}` : "",
788
+ css(options.height) ? `height:${css(options.height)}` : "",
789
+ options.fit ? `object-fit:${options.fit}` : ""
790
+ ].filter(Boolean).join(";");
791
+ const cls = options.class ? ` class="${escapeHtml(options.class)}"` : "";
792
+ const style = styles ? ` style="${styles}"` : "";
793
+ return `<img src="${escapeHtml(url)}" alt="${escapeHtml(options.alt ?? "")}"${cls}${style}>`;
794
+ }
795
+ var asObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
796
+ var documentHelpers = [
797
+ {
798
+ name: "chart",
799
+ html: true,
800
+ doc: {
801
+ signature: "chart({ type, data, options, width, height })",
802
+ description: "Draws a Chart.js chart (bar, line, pie, doughnut, radar, \u2026) from a chart configuration; animations are disabled so PDF and preview match.",
803
+ example: "{{ chart({ type: 'bar', data: sales, width: 480, height: 240 }) }}",
804
+ category: "document"
805
+ },
806
+ fn: (_ctx, spec, extra) => chartMarkup({ ...asObject(spec), ...asObject(extra) })
807
+ },
808
+ {
809
+ name: "image",
810
+ html: true,
811
+ doc: {
812
+ signature: "image(url, { width, height, fit, alt })",
813
+ description: "Places an image with fixed size and crop mode (`cover`, `contain`, `fill`); the render-worker fetches it through its cache and blocks private hosts.",
814
+ example: "{{ image(product.photo, { width: 120, height: 80, fit: 'cover' }) }}",
815
+ category: "document"
816
+ },
817
+ fn: (_ctx, url, options) => imageMarkup(String(url ?? ""), asObject(options))
818
+ }
819
+ ];
820
+
821
+ // packages/engine/src/lib/helpers/logic.ts
822
+ var same = (a, b) => a === b || a !== null && b !== null && a !== void 0 && b !== void 0 && String(a) === String(b);
823
+ function compare(a, b) {
824
+ const x = typeof a === "number" ? a : Number(a);
825
+ const y = typeof b === "number" ? b : Number(b);
826
+ if (a !== "" && b !== "" && Number.isFinite(x) && Number.isFinite(y)) return x - y;
827
+ return String(a ?? "").localeCompare(String(b ?? ""));
828
+ }
829
+ var truthy = (v) => Array.isArray(v) ? v.length > 0 : Boolean(v);
830
+ var comparison = (name, test, description) => ({
831
+ name,
832
+ doc: {
833
+ signature: `${name}(a, b)`,
834
+ description,
835
+ example: `{{#if (${name} invoice.total 1000)}}\u2026{{/if}}`,
836
+ category: "logic"
837
+ },
838
+ fn: (_ctx, a, b) => test(a, b)
839
+ });
840
+ var logicHelpers = [
841
+ comparison("eq", same, 'True when both values are equal; `1` equals `"1"`.'),
842
+ comparison("ne", (a, b) => !same(a, b), "True when the values differ."),
843
+ comparison("gt", (a, b) => compare(a, b) > 0, "Greater than; numbers as numbers, anything else as text."),
844
+ comparison("gte", (a, b) => compare(a, b) >= 0, "Greater than or equal."),
845
+ comparison("lt", (a, b) => compare(a, b) < 0, "Less than."),
846
+ comparison("lte", (a, b) => compare(a, b) <= 0, "Less than or equal."),
847
+ {
848
+ name: "and",
849
+ doc: {
850
+ signature: "and(a, b, \u2026)",
851
+ description: "True when every value is true; an empty list counts as false.",
852
+ example: '{{#if (and customer.vatId (eq country "DE"))}}\u2026{{/if}}',
853
+ category: "logic"
854
+ },
855
+ fn: (_ctx, ...values) => values.length > 0 && values.every(truthy)
856
+ },
857
+ {
858
+ name: "or",
859
+ doc: {
860
+ signature: "or(a, b, \u2026)",
861
+ description: "True when any value is true.",
862
+ example: "{{#if (or invoice.paid invoice.credited)}}\u2026{{/if}}",
863
+ category: "logic"
864
+ },
865
+ fn: (_ctx, ...values) => values.some(truthy)
866
+ },
867
+ {
868
+ name: "not",
869
+ doc: {
870
+ signature: "not(value)",
871
+ description: "The opposite truth value.",
872
+ example: "{{#if (not invoice.paid)}}\u2026{{/if}}",
873
+ category: "logic"
874
+ },
875
+ fn: (_ctx, value) => !truthy(value)
876
+ }
877
+ ];
878
+
879
+ // packages/engine/src/lib/helpers/registry.ts
880
+ var DefaultHelperRegistry = class {
881
+ byName = /* @__PURE__ */ new Map();
882
+ aliasToName = /* @__PURE__ */ new Map();
883
+ constructor(definitions = []) {
884
+ for (const d of definitions) this.register(d);
885
+ }
886
+ get definitions() {
887
+ return this.byName;
888
+ }
889
+ register(definition) {
890
+ this.byName.set(definition.name, definition);
891
+ for (const alias of definition.aliases ?? [])
892
+ this.aliasToName.set(alias, definition.name);
893
+ for (const alias of Object.keys(definition.deprecatedAlias ?? {}))
894
+ this.aliasToName.set(alias, definition.name);
895
+ }
896
+ get(name) {
897
+ return this.byName.get(name) ?? this.byName.get(this.aliasToName.get(name) ?? "");
898
+ }
899
+ has(name) {
900
+ return this.get(name) !== void 0;
901
+ }
902
+ names() {
903
+ return [...this.byName.keys(), ...this.aliasToName.keys()];
904
+ }
905
+ /** The preferred name when `name` is a deprecated alias, otherwise undefined. */
906
+ deprecationOf(name) {
907
+ const def = this.get(name);
908
+ return def?.deprecatedAlias?.[name];
909
+ }
910
+ bind(ctx) {
911
+ const bound = /* @__PURE__ */ new Map();
912
+ for (const def of this.byName.values()) {
913
+ const entry = {
914
+ fn: (...args) => def.fn(ctx, ...args),
915
+ html: def.html ?? false
916
+ };
917
+ bound.set(def.name, entry);
918
+ for (const alias of def.aliases ?? []) bound.set(alias, entry);
919
+ for (const alias of Object.keys(def.deprecatedAlias ?? {}))
920
+ bound.set(alias, entry);
921
+ }
922
+ return bound;
923
+ }
924
+ };
925
+
926
+ // packages/engine/src/lib/helpers/index.ts
927
+ var builtinHelpers = [
928
+ ...formatHelpers,
929
+ ...collectionHelpers,
930
+ ...logicHelpers,
931
+ ...codeHelpers,
932
+ ...documentHelpers
933
+ ];
934
+ function createHelperRegistry(extra = []) {
935
+ return new DefaultHelperRegistry([...builtinHelpers, ...extra]);
936
+ }
937
+ var shared;
938
+ function defaultHelpers() {
939
+ return shared ??= createHelperRegistry();
940
+ }
941
+ function helperDocs(registry = defaultHelpers()) {
942
+ return [...registry.definitions.values()].map((d) => ({
943
+ name: d.name,
944
+ aliases: d.aliases ?? [],
945
+ ...d.doc
946
+ }));
947
+ }
948
+
949
+ // packages/engine/src/lib/engines/handlebars.ts
950
+ import Handlebars from "handlebars";
951
+
952
+ // packages/engine/src/lib/analysis/tags.ts
953
+ function rangeAt(line, column, length) {
954
+ return {
955
+ start: { line, column },
956
+ end: { line, column: column + Math.max(1, length) }
957
+ };
958
+ }
959
+ function positionOf(source, offset) {
960
+ let line = 1;
961
+ let last = -1;
962
+ for (let i = 0; i < offset && i < source.length; i++) {
963
+ if (source.charCodeAt(i) === 10) {
964
+ line++;
965
+ last = i;
966
+ }
967
+ }
968
+ return { line, column: offset - last };
969
+ }
970
+ function rangeOf(source, start, end) {
971
+ return { start: positionOf(source, start), end: positionOf(source, end) };
972
+ }
973
+ var jinjaTags = {
974
+ pattern: /\{%-?\s*(\w+)([\s\S]*?)-?%\}/g,
975
+ openers: {
976
+ for: "for",
977
+ if: "if",
978
+ block: "block",
979
+ raw: "raw",
980
+ macro: "macro",
981
+ set: "other",
982
+ call: "other",
983
+ filter: "other",
984
+ autoescape: "other",
985
+ verbatim: "raw"
986
+ },
987
+ closers: {
988
+ endfor: "for",
989
+ endif: "if",
990
+ endblock: "block",
991
+ endraw: "raw",
992
+ endmacro: "macro",
993
+ endset: "set",
994
+ endcall: "call",
995
+ endfilter: "filter",
996
+ endautoescape: "autoescape",
997
+ endverbatim: "verbatim"
998
+ },
999
+ continuations: ["elif", "else"]
1000
+ };
1001
+ var liquidTags = {
1002
+ pattern: /\{%-?\s*(\w+)([\s\S]*?)-?%\}/g,
1003
+ openers: {
1004
+ for: "for",
1005
+ if: "if",
1006
+ unless: "if",
1007
+ case: "if",
1008
+ raw: "raw",
1009
+ capture: "other",
1010
+ tablerow: "for",
1011
+ comment: "other"
1012
+ },
1013
+ closers: {
1014
+ endfor: "for",
1015
+ endif: "if",
1016
+ endunless: "unless",
1017
+ endcase: "case",
1018
+ endraw: "raw",
1019
+ endcapture: "capture",
1020
+ endtablerow: "tablerow",
1021
+ endcomment: "comment"
1022
+ },
1023
+ continuations: ["elsif", "else", "when"]
1024
+ };
1025
+ function scanBlocks(source, spec) {
1026
+ const blocks = [];
1027
+ const diagnostics = [];
1028
+ const stack = [];
1029
+ const pattern = new RegExp(spec.pattern.source, "g");
1030
+ let m;
1031
+ let inRaw = null;
1032
+ const isBlockOpener = (keyword, rest) => keyword === "set" ? !rest.includes("=") : keyword in spec.openers;
1033
+ while (m = pattern.exec(source)) {
1034
+ const keyword = m[1] ?? "";
1035
+ const rest = m[2] ?? "";
1036
+ const range = rangeOf(source, m.index, m.index + m[0].length);
1037
+ if (inRaw) {
1038
+ if (spec.closers[keyword] === inRaw) {
1039
+ const open = stack.pop();
1040
+ if (open) open.block.close = range;
1041
+ inRaw = null;
1042
+ }
1043
+ continue;
1044
+ }
1045
+ if (isBlockOpener(keyword, rest)) {
1046
+ const block = {
1047
+ type: spec.openers[keyword] ?? "other",
1048
+ open: range
1049
+ };
1050
+ blocks.push(block);
1051
+ stack.push({ keyword, block });
1052
+ if (block.type === "raw" || keyword === "comment") inRaw = keyword;
1053
+ } else if (keyword in spec.closers) {
1054
+ const expected = spec.closers[keyword];
1055
+ const top = stack.at(-1);
1056
+ if (!top) {
1057
+ diagnostics.push({
1058
+ severity: "error",
1059
+ code: "unexpected-end-tag",
1060
+ message: `Unexpected {% ${keyword} %} without an open block`,
1061
+ range
1062
+ });
1063
+ } else if (top.keyword !== expected) {
1064
+ diagnostics.push({
1065
+ severity: "error",
1066
+ code: "mismatched-end-tag",
1067
+ message: `Expected {% end${top.keyword} %} to close the {% ${top.keyword} %} at line ${top.block.open.start.line}, found {% ${keyword} %}`,
1068
+ range
1069
+ });
1070
+ const idx = stack.map((s) => s.keyword).lastIndexOf(expected);
1071
+ if (idx >= 0) {
1072
+ const closed = stack.splice(idx, 1)[0];
1073
+ if (closed) closed.block.close = range;
1074
+ }
1075
+ } else {
1076
+ stack.pop();
1077
+ top.block.close = range;
1078
+ }
1079
+ } else if (spec.continuations.includes(keyword) && !stack.length) {
1080
+ diagnostics.push({
1081
+ severity: "error",
1082
+ code: "unexpected-tag",
1083
+ message: `{% ${keyword} %} outside of a block`,
1084
+ range
1085
+ });
1086
+ }
1087
+ }
1088
+ for (const open of stack) {
1089
+ diagnostics.push({
1090
+ severity: "error",
1091
+ code: "unclosed-block",
1092
+ message: `{% ${open.keyword} %} is never closed; add {% end${open.keyword} %}`,
1093
+ range: open.block.open
1094
+ });
1095
+ }
1096
+ return { blocks, diagnostics };
1097
+ }
1098
+ function resolvePath(data, path) {
1099
+ let current = data;
1100
+ for (const key of path) {
1101
+ if (Array.isArray(current)) {
1102
+ if (/^\d+$/.test(key)) {
1103
+ current = current[Number(key)];
1104
+ continue;
1105
+ }
1106
+ const hit = current.find(
1107
+ (item) => item && typeof item === "object" && key in item
1108
+ );
1109
+ if (!hit) return { found: false, value: void 0 };
1110
+ current = hit[key];
1111
+ continue;
1112
+ }
1113
+ if (current && typeof current === "object" && key in current) {
1114
+ current = current[key];
1115
+ } else {
1116
+ return { found: false, value: void 0 };
1117
+ }
1118
+ }
1119
+ return { found: true, value: current };
1120
+ }
1121
+ function missingPathDiagnostics(variables, sampleData, loopSources, builtinRoots4) {
1122
+ if (sampleData === void 0 || sampleData === null || typeof sampleData !== "object")
1123
+ return [];
1124
+ const out = [];
1125
+ const seen = /* @__PURE__ */ new Set();
1126
+ for (const v of variables) {
1127
+ if (v.kind === "assigned") continue;
1128
+ const root = v.path[0];
1129
+ if (!root || builtinRoots4.has(root)) continue;
1130
+ let path = v.path;
1131
+ if (loopSources.has(root)) {
1132
+ const source = loopSources.get(root);
1133
+ if (!source) continue;
1134
+ path = [...source, ...v.path.slice(1)];
1135
+ }
1136
+ const key = path.join(".");
1137
+ if (seen.has(key)) continue;
1138
+ seen.add(key);
1139
+ if (!resolvePath(sampleData, path).found) {
1140
+ out.push({
1141
+ severity: "warning",
1142
+ code: "unknown-variable",
1143
+ message: `"${v.path.join(".")}" is not present in the sample data`,
1144
+ range: v.range
1145
+ });
1146
+ }
1147
+ }
1148
+ return out;
1149
+ }
1150
+
1151
+ // packages/engine/src/lib/engines/handlebars.ts
1152
+ var builtinHelpers2 = /* @__PURE__ */ new Set([
1153
+ "if",
1154
+ "unless",
1155
+ "each",
1156
+ "with",
1157
+ "lookup",
1158
+ "log",
1159
+ "blockHelperMissing",
1160
+ "helperMissing"
1161
+ ]);
1162
+ var builtinRoots = /* @__PURE__ */ new Set([
1163
+ "this",
1164
+ "@index",
1165
+ "@key",
1166
+ "@first",
1167
+ "@last",
1168
+ "@root",
1169
+ "lookup"
1170
+ ]);
1171
+ function createInstance(ctx) {
1172
+ const hb = Handlebars.create();
1173
+ for (const [name, { fn, html }] of ctx.helpers.bind(ctx)) {
1174
+ hb.registerHelper(name, function(...args) {
1175
+ const options = args.at(-1);
1176
+ const params = args.slice(0, -1);
1177
+ const hasHash = !!options?.hash && Object.keys(options.hash).length > 0;
1178
+ if (params.length === 0 && !hasHash && this !== null && this !== void 0 && Object.prototype.hasOwnProperty.call(Object(this), name))
1179
+ return this[name];
1180
+ if (hasHash) params.push(options.hash);
1181
+ const result = fn(...params);
1182
+ return html ? new hb.SafeString(String(result ?? "")) : result;
1183
+ });
1184
+ }
1185
+ return hb;
1186
+ }
1187
+ var validationContext = {
1188
+ locale: "en",
1189
+ timezone: "UTC",
1190
+ currency: "EUR",
1191
+ partials: () => void 0,
1192
+ helpers: defaultHelpers(),
1193
+ limits: { ms: 2e3, outputBytes: 20 * 1024 * 1024, includeDepth: 8 }
1194
+ };
1195
+ function toSyntaxError(e) {
1196
+ const err = e;
1197
+ const message = String(err.message ?? e);
1198
+ const m = /Parse error on line (\d+)/.exec(message);
1199
+ const line = err.lineNumber ?? (m ? Number(m[1]) : 1);
1200
+ const column = typeof err.column === "number" ? err.column + 1 : 1;
1201
+ return new EngineSyntaxError(
1202
+ "handlebars",
1203
+ message.split("\n")[0] ?? message,
1204
+ line,
1205
+ column
1206
+ );
1207
+ }
1208
+ function toRange(loc) {
1209
+ if (!loc) return rangeAt(1, 1, 1);
1210
+ return {
1211
+ start: { line: loc.start.line, column: loc.start.column + 1 },
1212
+ end: { line: loc.end.line, column: loc.end.column + 1 }
1213
+ };
1214
+ }
1215
+ function pathParts(path) {
1216
+ const parts = path.parts.map(String);
1217
+ if (path.data) return [`@${parts[0] ?? ""}`, ...parts.slice(1)];
1218
+ return parts;
1219
+ }
1220
+ function resolve(w, path) {
1221
+ const parts = pathParts(path);
1222
+ const original = path.original;
1223
+ let depth = 0;
1224
+ let rest = parts;
1225
+ while (original.startsWith("../".repeat(depth + 1))) depth++;
1226
+ const rootScope = w.scopes[0] ?? {
1227
+ source: [],
1228
+ params: /* @__PURE__ */ new Map(),
1229
+ loop: false
1230
+ };
1231
+ const scope = w.scopes[w.scopes.length - 1 - depth] ?? rootScope;
1232
+ if (rest[0] === "this") rest = rest.slice(1);
1233
+ if (original.startsWith("@root"))
1234
+ return { abs: rest, kind: "read", display: parts };
1235
+ const first = rest[0] ?? "";
1236
+ for (let i = w.scopes.length - 1 - depth; i >= 0; i--) {
1237
+ const s = w.scopes[i];
1238
+ if (s?.params.has(first)) {
1239
+ const src = s.params.get(first);
1240
+ return {
1241
+ abs: src ? [...src, ...rest.slice(1)] : null,
1242
+ kind: "loop-var",
1243
+ display: parts
1244
+ };
1245
+ }
1246
+ }
1247
+ const inLoop = w.scopes.slice(0, w.scopes.length - depth).some((s) => s.loop);
1248
+ return {
1249
+ abs: scope.source ? [...scope.source, ...rest] : null,
1250
+ kind: inLoop || scope !== w.scopes[0] ? "loop-var" : "read",
1251
+ display: parts
1252
+ };
1253
+ }
1254
+ function walk(node, w) {
1255
+ if (!node) return;
1256
+ switch (node.type) {
1257
+ case "Program":
1258
+ for (const s of node.body) walk(s, w);
1259
+ return;
1260
+ case "MustacheStatement":
1261
+ case "SubExpression": {
1262
+ const n = node;
1263
+ const path = n.path;
1264
+ const name = path.original;
1265
+ const bare = n.params.length === 0 && Object.keys(n.hash?.pairs ?? {}).length === 0;
1266
+ const isHelper = !bare || builtinHelpers2.has(name) || takesNoArguments(w.helpers, name);
1267
+ if (isHelper && path.type === "PathExpression") {
1268
+ w.filters.push({
1269
+ name,
1270
+ range: toRange(path.loc),
1271
+ known: w.helpers.has(name) || builtinHelpers2.has(name)
1272
+ });
1273
+ } else if (path.type === "PathExpression") {
1274
+ recordVariable(w, path);
1275
+ } else {
1276
+ walk(path, w);
1277
+ }
1278
+ for (const p of n.params) walk(p, w);
1279
+ for (const pair of n.hash?.pairs ?? []) walk(pair.value, w);
1280
+ return;
1281
+ }
1282
+ case "BlockStatement": {
1283
+ const n = node;
1284
+ const path = n.path;
1285
+ const name = path.original;
1286
+ const type = name === "each" ? "for" : name === "if" || name === "unless" ? "if" : "block";
1287
+ w.blocks.push({ type, open: toRange(n.loc), close: toRange(n.loc) });
1288
+ if (!builtinHelpers2.has(name)) {
1289
+ w.filters.push({
1290
+ name,
1291
+ range: toRange(path.loc),
1292
+ known: w.helpers.has(name)
1293
+ });
1294
+ }
1295
+ for (const p of n.params) walk(p, w);
1296
+ const subject = n.params[0];
1297
+ const subjectPath = subject?.type === "PathExpression" ? resolve(w, subject).abs : null;
1298
+ const scope = {
1299
+ source: w.scopes.at(-1)?.source ?? [],
1300
+ params: /* @__PURE__ */ new Map(),
1301
+ loop: false
1302
+ };
1303
+ if (name === "each") {
1304
+ scope.loop = true;
1305
+ scope.source = subjectPath;
1306
+ const [item, index] = n.program?.blockParams ?? [];
1307
+ if (item) scope.params.set(item, subjectPath);
1308
+ if (index) scope.params.set(index, null);
1309
+ } else if (name === "with") {
1310
+ scope.source = subjectPath;
1311
+ const [alias] = n.program?.blockParams ?? [];
1312
+ if (alias) scope.params.set(alias, subjectPath);
1313
+ } else {
1314
+ for (const p of n.program?.blockParams ?? []) scope.params.set(p, null);
1315
+ }
1316
+ w.scopes.push(scope);
1317
+ walk(n.program, w);
1318
+ w.scopes.pop();
1319
+ walk(n.inverse, w);
1320
+ return;
1321
+ }
1322
+ case "PartialStatement":
1323
+ case "PartialBlockStatement": {
1324
+ const n = node;
1325
+ const nameNode = n.name;
1326
+ if (nameNode.type === "PathExpression")
1327
+ w.includes.push({ name: nameNode.original, range: toRange(n.loc) });
1328
+ for (const p of n.params) walk(p, w);
1329
+ for (const pair of n.hash?.pairs ?? []) walk(pair.value, w);
1330
+ return;
1331
+ }
1332
+ case "PathExpression":
1333
+ recordVariable(w, node);
1334
+ return;
1335
+ default:
1336
+ return;
1337
+ }
1338
+ }
1339
+ function takesNoArguments(helpers, name) {
1340
+ return /^\w+\(\)$/.test(helpers.get(name)?.doc.signature ?? "");
1341
+ }
1342
+ function recordVariable(w, path) {
1343
+ const parts = pathParts(path);
1344
+ if (parts.length === 0 || builtinRoots.has(parts[0] ?? "") || path.data)
1345
+ return;
1346
+ const r = resolve(w, path);
1347
+ const ref = {
1348
+ path: r.display,
1349
+ range: toRange(path.loc),
1350
+ kind: r.kind
1351
+ };
1352
+ w.variables.push(ref);
1353
+ const key = r.display.join(".");
1354
+ if (r.kind === "loop-var") w.loopSources.set(key, r.abs);
1355
+ }
1356
+ function analyzeHandlebars(source, opts = {}) {
1357
+ const helpers = opts.helpers ?? defaultHelpers();
1358
+ const w = {
1359
+ variables: [],
1360
+ filters: [],
1361
+ includes: [],
1362
+ blocks: [],
1363
+ scopes: [{ source: [], params: /* @__PURE__ */ new Map(), loop: false }],
1364
+ helpers,
1365
+ loopSources: /* @__PURE__ */ new Map()
1366
+ };
1367
+ const diagnostics = [];
1368
+ try {
1369
+ walk(Handlebars.parse(source), w);
1370
+ } catch (e) {
1371
+ const err = toSyntaxError(e);
1372
+ diagnostics.push({
1373
+ severity: "error",
1374
+ code: "syntax-error",
1375
+ message: err.message,
1376
+ range: rangeAt(err.line, err.column, 1)
1377
+ });
1378
+ }
1379
+ const registry = helpers;
1380
+ for (const f of w.filters) {
1381
+ const preferred = registry.deprecationOf?.(f.name);
1382
+ if (preferred) {
1383
+ diagnostics.push({
1384
+ severity: "warning",
1385
+ code: "deprecated-filter",
1386
+ message: `"${f.name}" is a compatibility alias; use "${preferred}"`,
1387
+ range: f.range,
1388
+ fix: { title: `Rename to ${preferred}`, replacement: preferred }
1389
+ });
1390
+ } else if (!f.known) {
1391
+ diagnostics.push({
1392
+ severity: "error",
1393
+ code: "unknown-filter",
1394
+ message: `Unknown helper "${f.name}"`,
1395
+ range: f.range
1396
+ });
1397
+ }
1398
+ }
1399
+ const forCheck = w.variables.map(
1400
+ (v) => v.kind === "loop-var" ? { ...v, path: [v.path.join(".")] } : v
1401
+ );
1402
+ const dataDiagnostics = missingPathDiagnostics(
1403
+ forCheck,
1404
+ opts.sampleData,
1405
+ w.loopSources,
1406
+ /* @__PURE__ */ new Set([...builtinRoots, ...helpers.names()])
1407
+ ).map((d) => ({
1408
+ ...d,
1409
+ message: d.message.replace(/"([^"]+)"/, (_, p) => `"${p}"`)
1410
+ }));
1411
+ return {
1412
+ variables: w.variables,
1413
+ filters: w.filters,
1414
+ includes: w.includes,
1415
+ blocks: w.blocks,
1416
+ diagnostics: [...diagnostics, ...dataDiagnostics]
1417
+ };
1418
+ }
1419
+ var HandlebarsCompiled = class {
1420
+ constructor(name, source) {
1421
+ this.name = name;
1422
+ this.source = source;
1423
+ }
1424
+ engine = "handlebars";
1425
+ runtimes = /* @__PURE__ */ new WeakMap();
1426
+ runtime(ctx) {
1427
+ let rt = this.runtimes.get(ctx);
1428
+ if (!rt) {
1429
+ const hb = createInstance(ctx);
1430
+ rt = {
1431
+ hb,
1432
+ template: hb.compile(this.source, {
1433
+ strict: false,
1434
+ noEscape: false,
1435
+ preventIndent: true
1436
+ })
1437
+ };
1438
+ this.runtimes.set(ctx, rt);
1439
+ }
1440
+ return rt;
1441
+ }
1442
+ async render(data, ctx) {
1443
+ const started = Date.now();
1444
+ const { template } = this.runtime(ctx);
1445
+ const partials = {};
1446
+ const collect = (source, depth) => {
1447
+ for (const inc of analyzeHandlebars(source).includes) {
1448
+ if (inc.name in partials) continue;
1449
+ if (depth >= ctx.limits.includeDepth) {
1450
+ throw new RenderLimitError(
1451
+ "includeDepth",
1452
+ `Partials nest deeper than ${ctx.limits.includeDepth} (${inc.name})`
1453
+ );
1454
+ }
1455
+ const src = ctx.partials(inc.name);
1456
+ if (src === void 0) continue;
1457
+ partials[inc.name] = src;
1458
+ collect(src, depth + 1);
1459
+ }
1460
+ };
1461
+ collect(this.source, 0);
1462
+ try {
1463
+ const out = template(data ?? {}, {
1464
+ partials,
1465
+ allowProtoPropertiesByDefault: false,
1466
+ allowProtoMethodsByDefault: false
1467
+ });
1468
+ return enforceLimits(out, started, ctx.limits);
1469
+ } catch (e) {
1470
+ if (e instanceof Error && e.name === "RenderLimitError") throw e;
1471
+ const err = e;
1472
+ throw new RenderError(
1473
+ "handlebars",
1474
+ String(err.message ?? e),
1475
+ err.lineNumber,
1476
+ err.column === void 0 ? void 0 : err.column + 1
1477
+ );
1478
+ }
1479
+ }
1480
+ };
1481
+ var handlebarsEngine = {
1482
+ id: "handlebars",
1483
+ compile(source, opts = {}) {
1484
+ const name = opts.name ?? "template";
1485
+ try {
1486
+ createInstance(validationContext).precompile(source, { strict: false });
1487
+ } catch (e) {
1488
+ throw toSyntaxError(e);
1489
+ }
1490
+ return new HandlebarsCompiled(name, source);
1491
+ },
1492
+ analyze: analyzeHandlebars,
1493
+ render: (tpl, data, ctx) => tpl.render(data, ctx)
1494
+ };
1495
+
1496
+ // packages/engine/src/lib/engines/jinja2.ts
1497
+ import nunjucks from "nunjucks/browser/nunjucks.js";
1498
+ var nunjucksBuiltinFilters = /* @__PURE__ */ new Set([
1499
+ "abs",
1500
+ "batch",
1501
+ "capitalize",
1502
+ "center",
1503
+ "default",
1504
+ "d",
1505
+ "dictsort",
1506
+ "dump",
1507
+ "escape",
1508
+ "e",
1509
+ "first",
1510
+ "float",
1511
+ "forceescape",
1512
+ "groupby",
1513
+ "indent",
1514
+ "int",
1515
+ "join",
1516
+ "last",
1517
+ "length",
1518
+ "list",
1519
+ "lower",
1520
+ "nl2br",
1521
+ "random",
1522
+ "reject",
1523
+ "rejectattr",
1524
+ "replace",
1525
+ "reverse",
1526
+ "round",
1527
+ "safe",
1528
+ "select",
1529
+ "selectattr",
1530
+ "slice",
1531
+ "sort",
1532
+ "string",
1533
+ "striptags",
1534
+ "sum",
1535
+ "title",
1536
+ "trim",
1537
+ "truncate",
1538
+ "upper",
1539
+ "urlencode",
1540
+ "urlize",
1541
+ "wordcount"
1542
+ ]);
1543
+ var builtinRoots2 = /* @__PURE__ */ new Set([
1544
+ "loop",
1545
+ "caller",
1546
+ "super",
1547
+ "range",
1548
+ "cycler",
1549
+ "joiner",
1550
+ "lipsum",
1551
+ "varargs",
1552
+ "kwargs"
1553
+ ]);
1554
+ var stringMethods = {
1555
+ upper: (s) => s.toUpperCase(),
1556
+ lower: (s) => s.toLowerCase(),
1557
+ strip: (s, chars) => trimChars(s, chars, "both"),
1558
+ lstrip: (s, chars) => trimChars(s, chars, "left"),
1559
+ rstrip: (s, chars) => trimChars(s, chars, "right"),
1560
+ title: (s) => s.replace(
1561
+ /\p{L}[\p{L}\p{M}]*/gu,
1562
+ (w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()
1563
+ ),
1564
+ capitalize: (s) => s.charAt(0).toUpperCase() + s.slice(1).toLowerCase(),
1565
+ startswith: (s, p) => s.startsWith(String(p)),
1566
+ endswith: (s, p) => s.endsWith(String(p)),
1567
+ replace: (s, a, b, count) => {
1568
+ const n = count === void 0 ? Infinity : Number(count);
1569
+ let out = s;
1570
+ let done = 0;
1571
+ while (done < n && out.includes(String(a))) {
1572
+ out = out.replace(String(a), String(b));
1573
+ done++;
1574
+ if (String(a) === "") break;
1575
+ }
1576
+ return out;
1577
+ },
1578
+ split: (s, sep, max) => sep === void 0 || sep === null ? s.trim().split(/\s+/) : s.split(String(sep), max === void 0 ? void 0 : Number(max) + 1),
1579
+ join: (s, list2) => Array.isArray(list2) ? list2.join(s) : String(list2),
1580
+ format: (s, ...args) => {
1581
+ let i = 0;
1582
+ const named = args.at(-1) && typeof args.at(-1) === "object" ? args.at(-1) : {};
1583
+ return s.replace(
1584
+ /\{(\w*)\}/g,
1585
+ (_, key) => String(key ? named[key] ?? "" : args[i++] ?? "")
1586
+ );
1587
+ },
1588
+ zfill: (s, width) => s.padStart(Number(width), "0"),
1589
+ count: (s, sub) => s.split(String(sub)).length - 1,
1590
+ find: (s, sub) => s.indexOf(String(sub)),
1591
+ isdigit: (s) => /^\d+$/.test(s),
1592
+ isalpha: (s) => /^\p{L}+$/u.test(s),
1593
+ splitlines: (s) => s.split(/\r?\n/),
1594
+ center: (s, width, fill) => {
1595
+ const total = Math.max(0, Number(width) - s.length);
1596
+ const left = Math.floor(total / 2);
1597
+ const f = String(fill ?? " ");
1598
+ return f.repeat(left) + s + f.repeat(total - left);
1599
+ }
1600
+ };
1601
+ function trimChars(s, chars, side) {
1602
+ if (chars === void 0 || chars === null)
1603
+ return side === "both" ? s.trim() : side === "left" ? s.trimStart() : s.trimEnd();
1604
+ const set = new Set(String(chars));
1605
+ let start = 0;
1606
+ let end = s.length;
1607
+ if (side !== "right") while (start < end && set.has(s[start] ?? "")) start++;
1608
+ if (side !== "left") while (end > start && set.has(s[end - 1] ?? "")) end--;
1609
+ return s.slice(start, end);
1610
+ }
1611
+ var compatInstalled = false;
1612
+ function installCompat() {
1613
+ if (compatInstalled) return;
1614
+ compatInstalled = true;
1615
+ nunjucks.installJinjaCompat();
1616
+ nunjucks.compiler.Compiler.prototype.compileNot = function(node, frame) {
1617
+ this._emit("!(");
1618
+ this.compile(node.target, frame);
1619
+ this._emit(")");
1620
+ };
1621
+ const original = nunjucks.runtime.memberLookup;
1622
+ nunjucks.runtime.memberLookup = (obj, val2, ...rest) => {
1623
+ if (typeof obj === "string" && typeof val2 === "string" && Object.prototype.hasOwnProperty.call(stringMethods, val2)) {
1624
+ const method = stringMethods[val2];
1625
+ if (method) return (...args) => method(obj, ...args);
1626
+ }
1627
+ if (obj && typeof obj === "object" && !Array.isArray(obj) && typeof val2 === "string" && Object.prototype.hasOwnProperty.call(obj, val2)) {
1628
+ const own = obj[val2];
1629
+ return typeof own === "function" ? (...args) => own.apply(obj, args) : own;
1630
+ }
1631
+ if (obj && typeof obj === "object" && !Array.isArray(obj) && val2 === "get" && !("get" in obj)) {
1632
+ return (key, fallback) => String(key) in obj ? obj[String(key)] : fallback;
1633
+ }
1634
+ return original(obj, val2, ...rest);
1635
+ };
1636
+ }
1637
+ var PartialLoader = class extends nunjucks.Loader {
1638
+ constructor(lookup) {
1639
+ super();
1640
+ this.lookup = lookup;
1641
+ }
1642
+ async = false;
1643
+ getSource(name) {
1644
+ const src = this.lookup(name);
1645
+ return src === void 0 ? null : { src, path: name, noCache: true };
1646
+ }
1647
+ };
1648
+ function createEnvironment(ctx) {
1649
+ installCompat();
1650
+ const env = new nunjucks.Environment(
1651
+ new PartialLoader(ctx.partials),
1652
+ {
1653
+ autoescape: true,
1654
+ throwOnUndefined: false,
1655
+ trimBlocks: false,
1656
+ lstripBlocks: false
1657
+ }
1658
+ );
1659
+ for (const [name, { fn, html }] of ctx.helpers.bind(ctx)) {
1660
+ const wrapped = (...args) => {
1661
+ const result = fn(...args);
1662
+ return html ? new nunjucks.runtime.SafeString(String(result ?? "")) : result;
1663
+ };
1664
+ env.addFilter(name, wrapped);
1665
+ env.addGlobal(name, wrapped);
1666
+ }
1667
+ return env;
1668
+ }
1669
+ var validationContext2 = {
1670
+ locale: "en",
1671
+ timezone: "UTC",
1672
+ currency: "EUR",
1673
+ partials: () => void 0,
1674
+ helpers: defaultHelpers(),
1675
+ limits: { ms: 2e3, outputBytes: 20 * 1024 * 1024, includeDepth: 8 },
1676
+ i18n: {}
1677
+ };
1678
+ function toSyntaxError2(e, fallbackLine = 1) {
1679
+ const err = e;
1680
+ const message = String(err.message ?? e).replace(/^\(.*?\)\s*/, "").replace(/^\[Line \d+, Column \d+\]\s*/, "").trim();
1681
+ const fromMessage = /\[Line (\d+), Column (\d+)\]/.exec(
1682
+ String(err.message ?? "")
1683
+ );
1684
+ const line = typeof err.lineno === "number" ? err.lineno + 1 : fromMessage ? Number(fromMessage[1]) : fallbackLine;
1685
+ const column = typeof err.colno === "number" ? err.colno + 1 : fromMessage ? Number(fromMessage[2]) : 1;
1686
+ return new EngineSyntaxError("jinja2", message, line, column);
1687
+ }
1688
+ function nodeRange(node, length = 1) {
1689
+ return rangeAt(node.lineno + 1, node.colno + 1, length);
1690
+ }
1691
+ function pathOf(node) {
1692
+ if (node.typename === "Symbol")
1693
+ return { path: [String(node.value)], root: node };
1694
+ if (node.typename === "LookupVal") {
1695
+ const target = pathOf(node["target"]);
1696
+ const val2 = node["val"];
1697
+ if (!target) return void 0;
1698
+ if (val2.typename === "Literal")
1699
+ return { path: [...target.path, String(val2.value)], root: target.root };
1700
+ return target;
1701
+ }
1702
+ return void 0;
1703
+ }
1704
+ function inScope(w, name) {
1705
+ return w.scopes.some((s) => s.has(name));
1706
+ }
1707
+ function walk2(node, w) {
1708
+ if (!node || typeof node !== "object") return;
1709
+ switch (node.typename) {
1710
+ case "Symbol":
1711
+ case "LookupVal": {
1712
+ const p = pathOf(node);
1713
+ if (p) {
1714
+ const root = p.path[0] ?? "";
1715
+ w.variables.push({
1716
+ path: p.path,
1717
+ range: nodeRange(p.root, p.path.join(".").length),
1718
+ kind: inScope(w, root) ? "loop-var" : "read"
1719
+ });
1720
+ }
1721
+ if (node.typename === "LookupVal" && !p) {
1722
+ walk2(node["target"], w);
1723
+ walk2(node["val"], w);
1724
+ } else if (node.typename === "LookupVal") {
1725
+ const val2 = node["val"];
1726
+ if (val2.typename !== "Literal") walk2(val2, w);
1727
+ }
1728
+ return;
1729
+ }
1730
+ case "Filter": {
1731
+ const nameNode = node["name"];
1732
+ const name = String(nameNode.value);
1733
+ w.filters.push({
1734
+ name,
1735
+ range: nodeRange(nameNode, name.length),
1736
+ known: w.helpers.has(name) || nunjucksBuiltinFilters.has(name)
1737
+ });
1738
+ const args = node["args"];
1739
+ for (const child of args.children ?? []) walk2(child, w);
1740
+ return;
1741
+ }
1742
+ case "FunCall": {
1743
+ const nameNode = node["name"];
1744
+ if (nameNode.typename === "Symbol" && (w.helpers.has(String(nameNode.value)) || builtinRoots2.has(String(nameNode.value)))) {
1745
+ const name = String(nameNode.value);
1746
+ w.filters.push({
1747
+ name,
1748
+ range: nodeRange(nameNode, name.length),
1749
+ known: true
1750
+ });
1751
+ } else {
1752
+ walk2(nameNode, w);
1753
+ }
1754
+ const args = node["args"];
1755
+ for (const child of args.children ?? []) walk2(child, w);
1756
+ return;
1757
+ }
1758
+ case "For": {
1759
+ const arr = node["arr"];
1760
+ walk2(arr, w);
1761
+ const source = pathOf(arr);
1762
+ const nameNode = node["name"];
1763
+ const names = nameNode.typename === "Array" ? nameNode.children ?? [] : [nameNode];
1764
+ const scope = /* @__PURE__ */ new Set();
1765
+ for (const n of names) {
1766
+ const name = String(n.value);
1767
+ scope.add(name);
1768
+ w.loopSources.set(
1769
+ name,
1770
+ names.length === 1 && source && !inScope(w, source.path[0] ?? "") ? source.path : names.length === 1 && source ? resolveLoopSource(w, source.path) : null
1771
+ );
1772
+ w.variables.push({
1773
+ path: [name],
1774
+ range: nodeRange(n, name.length),
1775
+ kind: "loop-var"
1776
+ });
1777
+ }
1778
+ w.scopes.push(scope);
1779
+ walk2(node["body"], w);
1780
+ w.scopes.pop();
1781
+ walk2(node["else_"], w);
1782
+ return;
1783
+ }
1784
+ case "Set": {
1785
+ for (const target of node["targets"] ?? []) {
1786
+ const p = pathOf(target);
1787
+ if (p) {
1788
+ w.variables.push({
1789
+ path: p.path,
1790
+ range: nodeRange(p.root, p.path.join(".").length),
1791
+ kind: "assigned"
1792
+ });
1793
+ w.scopes[0]?.add(p.path[0] ?? "");
1794
+ w.loopSources.set(p.path[0] ?? "", null);
1795
+ }
1796
+ }
1797
+ walk2(node["value"], w);
1798
+ walk2(node["body"], w);
1799
+ return;
1800
+ }
1801
+ case "Macro": {
1802
+ const args = node["args"];
1803
+ const scope = /* @__PURE__ */ new Set();
1804
+ for (const child of args.children ?? [])
1805
+ if (child.typename === "Symbol") scope.add(String(child.value));
1806
+ const nameNode = node["name"];
1807
+ w.scopes[0]?.add(String(nameNode.value));
1808
+ w.loopSources.set(String(nameNode.value), null);
1809
+ w.scopes.push(scope);
1810
+ walk2(node["body"], w);
1811
+ w.scopes.pop();
1812
+ return;
1813
+ }
1814
+ case "Include":
1815
+ case "Extends":
1816
+ case "Import":
1817
+ case "FromImport": {
1818
+ const tpl = node["template"];
1819
+ if (tpl.typename === "Literal")
1820
+ w.includes.push({
1821
+ name: String(tpl.value),
1822
+ range: nodeRange(tpl, String(tpl.value).length + 2)
1823
+ });
1824
+ else walk2(tpl, w);
1825
+ if (node.typename === "Import")
1826
+ w.scopes[0]?.add(
1827
+ String(node["target"]?.value ?? "")
1828
+ );
1829
+ if (node.typename === "FromImport")
1830
+ for (const n of node["names"].children ?? [])
1831
+ w.scopes[0]?.add(
1832
+ String(
1833
+ n.typename === "Pair" ? n["value"].value : n.value
1834
+ )
1835
+ );
1836
+ return;
1837
+ }
1838
+ default:
1839
+ break;
1840
+ }
1841
+ if (node.children) {
1842
+ for (const child of node.children) walk2(child, w);
1843
+ return;
1844
+ }
1845
+ for (const field of node.fields ?? []) {
1846
+ const value = node[field];
1847
+ if (Array.isArray(value)) for (const v of value) walk2(v, w);
1848
+ else if (value && typeof value === "object" && "typename" in value)
1849
+ walk2(value, w);
1850
+ }
1851
+ }
1852
+ function resolveLoopSource(w, path) {
1853
+ const root = path[0] ?? "";
1854
+ if (!w.loopSources.has(root)) return path;
1855
+ const source = w.loopSources.get(root);
1856
+ return source ? [...source, ...path.slice(1)] : null;
1857
+ }
1858
+ function analyzeJinja2(source, opts = {}) {
1859
+ const helpers = opts.helpers ?? defaultHelpers();
1860
+ const { blocks, diagnostics } = scanBlocks(source, jinjaTags);
1861
+ const w = {
1862
+ variables: [],
1863
+ filters: [],
1864
+ includes: [],
1865
+ loopSources: /* @__PURE__ */ new Map(),
1866
+ scopes: [/* @__PURE__ */ new Set()],
1867
+ helpers
1868
+ };
1869
+ try {
1870
+ installCompat();
1871
+ const root = nunjucks.parser.parse(source, [], {});
1872
+ walk2(root, w);
1873
+ } catch (e) {
1874
+ const err = toSyntaxError2(e);
1875
+ if (!diagnostics.some((d) => d.severity === "error")) {
1876
+ diagnostics.push({
1877
+ severity: "error",
1878
+ code: "syntax-error",
1879
+ message: err.message,
1880
+ range: rangeAt(err.line, err.column, 1)
1881
+ });
1882
+ }
1883
+ }
1884
+ const registry = helpers;
1885
+ for (const f of w.filters) {
1886
+ const preferred = registry.deprecationOf?.(f.name);
1887
+ if (preferred) {
1888
+ diagnostics.push({
1889
+ severity: "warning",
1890
+ code: "deprecated-filter",
1891
+ message: `"${f.name}" is an apitemplate.io compatibility alias; use "${preferred}"`,
1892
+ range: f.range,
1893
+ fix: { title: `Rename to ${preferred}`, replacement: preferred }
1894
+ });
1895
+ } else if (!f.known) {
1896
+ diagnostics.push({
1897
+ severity: "error",
1898
+ code: "unknown-filter",
1899
+ message: `Unknown filter or helper "${f.name}"`,
1900
+ range: f.range
1901
+ });
1902
+ }
1903
+ }
1904
+ const builtin = /* @__PURE__ */ new Set([...builtinRoots2, ...helpers.names()]);
1905
+ const dataDiagnostics = missingPathDiagnostics(
1906
+ w.variables,
1907
+ opts.sampleData,
1908
+ w.loopSources,
1909
+ builtin
1910
+ );
1911
+ return {
1912
+ variables: w.variables,
1913
+ filters: w.filters,
1914
+ includes: w.includes,
1915
+ blocks,
1916
+ diagnostics: [...diagnostics, ...dataDiagnostics]
1917
+ };
1918
+ }
1919
+ var Jinja2Template = class {
1920
+ constructor(name, source) {
1921
+ this.name = name;
1922
+ this.source = source;
1923
+ }
1924
+ engine = "jinja2";
1925
+ runtimes = /* @__PURE__ */ new WeakMap();
1926
+ runtime(ctx) {
1927
+ let tpl = this.runtimes.get(ctx);
1928
+ if (!tpl) {
1929
+ const env = createEnvironment(ctx);
1930
+ tpl = new nunjucks.Template(this.source, env, this.name, true);
1931
+ this.runtimes.set(ctx, tpl);
1932
+ }
1933
+ return tpl;
1934
+ }
1935
+ render(data, ctx) {
1936
+ const started = Date.now();
1937
+ const tpl = this.runtime(ctx);
1938
+ return new Promise((resolve2, reject) => {
1939
+ try {
1940
+ tpl.render(
1941
+ data ?? {},
1942
+ (err, res) => {
1943
+ if (err) {
1944
+ const e = err;
1945
+ reject(
1946
+ new RenderError(
1947
+ "jinja2",
1948
+ String(e.message ?? err).replace(/^\(.*?\)\s*/, ""),
1949
+ e.lineno === void 0 ? void 0 : e.lineno + 1,
1950
+ e.colno === void 0 ? void 0 : e.colno + 1
1951
+ )
1952
+ );
1953
+ return;
1954
+ }
1955
+ try {
1956
+ resolve2(enforceLimits(res ?? "", started, ctx.limits));
1957
+ } catch (limit) {
1958
+ reject(limit);
1959
+ }
1960
+ }
1961
+ );
1962
+ } catch (e) {
1963
+ reject(e instanceof Error ? e : new RenderError("jinja2", String(e)));
1964
+ }
1965
+ });
1966
+ }
1967
+ };
1968
+ var jinja2Engine = {
1969
+ id: "jinja2",
1970
+ compile(source, opts = {}) {
1971
+ const name = opts.name ?? "template";
1972
+ try {
1973
+ installCompat();
1974
+ new nunjucks.Template(
1975
+ source,
1976
+ createEnvironment(validationContext2),
1977
+ name,
1978
+ true
1979
+ );
1980
+ } catch (e) {
1981
+ throw toSyntaxError2(e);
1982
+ }
1983
+ return new Jinja2Template(name, source);
1984
+ },
1985
+ analyze: analyzeJinja2,
1986
+ render: (tpl, data, ctx) => tpl.render(data, ctx)
1987
+ };
1988
+
1989
+ // packages/engine/src/lib/engines/liquid.ts
1990
+ import { Liquid } from "liquidjs";
1991
+ var liquidBuiltinFilters = /* @__PURE__ */ new Set([
1992
+ "abs",
1993
+ "append",
1994
+ "at_least",
1995
+ "at_most",
1996
+ "capitalize",
1997
+ "ceil",
1998
+ "compact",
1999
+ "concat",
2000
+ "date",
2001
+ "default",
2002
+ "divided_by",
2003
+ "downcase",
2004
+ "escape",
2005
+ "escape_once",
2006
+ "first",
2007
+ "floor",
2008
+ "join",
2009
+ "json",
2010
+ "last",
2011
+ "lstrip",
2012
+ "map",
2013
+ "minus",
2014
+ "modulo",
2015
+ "newline_to_br",
2016
+ "plus",
2017
+ "prepend",
2018
+ "remove",
2019
+ "remove_first",
2020
+ "replace",
2021
+ "replace_first",
2022
+ "reverse",
2023
+ "round",
2024
+ "rstrip",
2025
+ "size",
2026
+ "slice",
2027
+ "sort",
2028
+ "sort_natural",
2029
+ "split",
2030
+ "strip",
2031
+ "strip_html",
2032
+ "strip_newlines",
2033
+ "times",
2034
+ "truncate",
2035
+ "truncatewords",
2036
+ "uniq",
2037
+ "upcase",
2038
+ "url_decode",
2039
+ "url_encode",
2040
+ "where",
2041
+ "raw",
2042
+ "sum",
2043
+ "group_by",
2044
+ "find",
2045
+ "reject",
2046
+ "has",
2047
+ "find_index",
2048
+ "to_integer",
2049
+ "array_to_sentence_string",
2050
+ "normalize_whitespace",
2051
+ "number_of_words",
2052
+ "push",
2053
+ "pop",
2054
+ "shift",
2055
+ "unshift",
2056
+ "inspect",
2057
+ "type",
2058
+ "sample",
2059
+ "time_zone"
2060
+ ]);
2061
+ var builtinRoots3 = /* @__PURE__ */ new Set([
2062
+ "forloop",
2063
+ "tablerowloop",
2064
+ "true",
2065
+ "false",
2066
+ "nil",
2067
+ "null",
2068
+ "empty",
2069
+ "blank",
2070
+ "continue",
2071
+ "break"
2072
+ ]);
2073
+ var keywords = /* @__PURE__ */ new Set([
2074
+ "and",
2075
+ "or",
2076
+ "not",
2077
+ "in",
2078
+ "contains",
2079
+ "with",
2080
+ "for",
2081
+ "as",
2082
+ "reversed",
2083
+ "limit",
2084
+ "offset",
2085
+ "by",
2086
+ "cols",
2087
+ "else"
2088
+ ]);
2089
+ function partialFs(resolve2) {
2090
+ const read = (file) => {
2091
+ const src = resolve2(file);
2092
+ if (src === void 0) throw new Error(`Partial "${file}" not found`);
2093
+ return src;
2094
+ };
2095
+ return {
2096
+ readFileSync: read,
2097
+ readFile: (file) => Promise.resolve(read(file)),
2098
+ existsSync: (file) => resolve2(file) !== void 0,
2099
+ exists: (file) => Promise.resolve(resolve2(file) !== void 0),
2100
+ resolve: (_root, file, ext) => ext && !file.endsWith(ext) ? `${file}${ext}` : file,
2101
+ contains: () => Promise.resolve(true),
2102
+ sep: "/",
2103
+ dirname: () => ""
2104
+ };
2105
+ }
2106
+ function collectKeywordArgs(args, isKeyword) {
2107
+ const positional = [];
2108
+ const named = {};
2109
+ let sawNamed = false;
2110
+ args.forEach((arg, index) => {
2111
+ if (isKeyword(index) && Array.isArray(arg) && typeof arg[0] === "string") {
2112
+ named[arg[0]] = arg[1];
2113
+ sawNamed = true;
2114
+ } else positional.push(arg);
2115
+ });
2116
+ return sawNamed ? [...positional, named] : positional;
2117
+ }
2118
+ function createLiquid(ctx) {
2119
+ const liquid = new Liquid({
2120
+ strictFilters: true,
2121
+ strictVariables: false,
2122
+ ownPropertyOnly: true,
2123
+ relativeReference: false,
2124
+ extname: "",
2125
+ root: [""],
2126
+ fs: partialFs(ctx.partials),
2127
+ renderLimit: ctx.limits.ms,
2128
+ memoryLimit: ctx.limits.outputBytes * 4,
2129
+ parseLimit: 2e6
2130
+ });
2131
+ for (const [name, { fn }] of ctx.helpers.bind(ctx))
2132
+ liquid.registerFilter(name, function(input, ...argv) {
2133
+ const tokens = this.token?.args ?? [];
2134
+ return fn(
2135
+ ...collectKeywordArgs([input, ...argv], (i) => i > 0 && Array.isArray(tokens[i - 1]))
2136
+ );
2137
+ });
2138
+ return liquid;
2139
+ }
2140
+ var validationContext3 = {
2141
+ locale: "en",
2142
+ timezone: "UTC",
2143
+ currency: "EUR",
2144
+ partials: () => void 0,
2145
+ helpers: defaultHelpers(),
2146
+ limits: { ms: 2e3, outputBytes: 20 * 1024 * 1024, includeDepth: 8 }
2147
+ };
2148
+ function toSyntaxError3(e) {
2149
+ const err = e;
2150
+ let line = err.line ?? 1;
2151
+ let column = err.col ?? 1;
2152
+ const pos = err.token?.getPosition?.();
2153
+ if (pos) [line, column] = pos;
2154
+ const message = String(err.message ?? e).replace(/,\s*line:\d+,\s*col:\d+/, "").trim();
2155
+ return new EngineSyntaxError("liquid", message, line, column);
2156
+ }
2157
+ var identifier = /(?<![\w."'])([A-Za-z_][\w]*)((?:\.[A-Za-z_]\w*|\[\d+\])*)/g;
2158
+ function pathsIn(expression, base, source, out, scopes) {
2159
+ const stripped = expression.replace(
2160
+ /(["'])(?:\\.|(?!\1).)*\1/g,
2161
+ (m) => " ".repeat(m.length)
2162
+ );
2163
+ for (const m of stripped.matchAll(identifier)) {
2164
+ const root = m[1] ?? "";
2165
+ if (keywords.has(root) || builtinRoots3.has(root) || /^\d/.test(root))
2166
+ continue;
2167
+ const path = [root, ...(m[2] ?? "").split(/[.[\]]+/).filter(Boolean)];
2168
+ const start = base + (m.index ?? 0);
2169
+ out.push({
2170
+ path,
2171
+ range: rangeOf(source, start, start + m[0].length),
2172
+ kind: scopes.has(root) ? "loop-var" : "read"
2173
+ });
2174
+ }
2175
+ }
2176
+ function analyzeLiquid(source, opts = {}) {
2177
+ const helpers = opts.helpers ?? defaultHelpers();
2178
+ const { blocks, diagnostics } = scanBlocks(source, liquidTags);
2179
+ const variables = [];
2180
+ const filters = [];
2181
+ const includes = [];
2182
+ const loopSources = /* @__PURE__ */ new Map();
2183
+ try {
2184
+ new Liquid({ strictFilters: false, fs: partialFs(() => "") }).parse(source);
2185
+ } catch (e) {
2186
+ const err = toSyntaxError3(e);
2187
+ if (!diagnostics.some((d) => d.severity === "error")) {
2188
+ diagnostics.push({
2189
+ severity: "error",
2190
+ code: "syntax-error",
2191
+ message: err.message,
2192
+ range: {
2193
+ start: { line: err.line, column: err.column },
2194
+ end: { line: err.line, column: err.column + 1 }
2195
+ }
2196
+ });
2197
+ }
2198
+ }
2199
+ const filterNames = (expr, base) => {
2200
+ for (const m of expr.matchAll(/\|\s*([A-Za-z_]\w*)/g)) {
2201
+ const name = m[1] ?? "";
2202
+ const start = base + (m.index ?? 0) + m[0].indexOf(name);
2203
+ filters.push({
2204
+ name,
2205
+ range: rangeOf(source, start, start + name.length),
2206
+ known: helpers.has(name) || liquidBuiltinFilters.has(name)
2207
+ });
2208
+ }
2209
+ };
2210
+ const withoutFilters = (expr) => expr.split("|")[0] ?? expr;
2211
+ for (const m of source.matchAll(/\{\{-?([\s\S]*?)-?\}\}/g)) {
2212
+ const inner = m[1] ?? "";
2213
+ const base = (m.index ?? 0) + m[0].indexOf(inner);
2214
+ pathsIn(withoutFilters(inner), base, source, variables, loopSources);
2215
+ filterNames(inner, base);
2216
+ }
2217
+ for (const m of source.matchAll(/\{%-?\s*(\w+)([\s\S]*?)-?%\}/g)) {
2218
+ const keyword = m[1] ?? "";
2219
+ const rest = m[2] ?? "";
2220
+ const base = (m.index ?? 0) + m[0].indexOf(rest, 2 + keyword.length);
2221
+ if (keyword === "for" || keyword === "tablerow") {
2222
+ const fm = /^\s*([A-Za-z_]\w*)\s+in\s+([\w.[\]]+)/.exec(rest);
2223
+ if (fm) {
2224
+ const name = fm[1] ?? "";
2225
+ const src = (fm[2] ?? "").split(/[.[\]]+/).filter(Boolean);
2226
+ const root = src[0] ?? "";
2227
+ loopSources.set(
2228
+ name,
2229
+ loopSources.has(root) ? loopSources.get(root) ? [...loopSources.get(root), ...src.slice(1)] : null : src
2230
+ );
2231
+ const start = base + rest.indexOf(name);
2232
+ variables.push({
2233
+ path: [name],
2234
+ range: rangeOf(source, start, start + name.length),
2235
+ kind: "loop-var"
2236
+ });
2237
+ pathsIn(
2238
+ rest.slice(fm[0].length - (fm[2] ?? "").length),
2239
+ base + fm[0].length - (fm[2] ?? "").length,
2240
+ source,
2241
+ variables,
2242
+ loopSources
2243
+ );
2244
+ }
2245
+ } else if (keyword === "assign" || keyword === "capture" || keyword === "increment" || keyword === "decrement") {
2246
+ const am = /^\s*([A-Za-z_]\w*)/.exec(rest);
2247
+ if (am) {
2248
+ const name = am[1] ?? "";
2249
+ const start = base + rest.indexOf(name);
2250
+ variables.push({
2251
+ path: [name],
2252
+ range: rangeOf(source, start, start + name.length),
2253
+ kind: "assigned"
2254
+ });
2255
+ loopSources.set(name, null);
2256
+ if (keyword === "assign") {
2257
+ const value = rest.slice(am[0].length).replace(/^\s*=/, "");
2258
+ pathsIn(
2259
+ withoutFilters(value),
2260
+ base + rest.length - value.length,
2261
+ source,
2262
+ variables,
2263
+ loopSources
2264
+ );
2265
+ filterNames(value, base + rest.length - value.length);
2266
+ }
2267
+ }
2268
+ } else if (keyword === "render" || keyword === "include") {
2269
+ const im = /^\s*(["'])([^"']+)\1/.exec(rest);
2270
+ if (im) {
2271
+ const start = base + rest.indexOf(im[0]);
2272
+ includes.push({
2273
+ name: im[2] ?? "",
2274
+ range: rangeOf(source, start, start + im[0].length)
2275
+ });
2276
+ }
2277
+ } else if (["if", "elsif", "unless", "case", "when", "echo", "cycle"].includes(
2278
+ keyword
2279
+ )) {
2280
+ pathsIn(withoutFilters(rest), base, source, variables, loopSources);
2281
+ if (keyword === "echo") filterNames(rest, base);
2282
+ }
2283
+ }
2284
+ const registry = helpers;
2285
+ for (const f of filters) {
2286
+ const preferred = registry.deprecationOf?.(f.name);
2287
+ if (preferred) {
2288
+ diagnostics.push({
2289
+ severity: "warning",
2290
+ code: "deprecated-filter",
2291
+ message: `"${f.name}" is a compatibility alias; use "${preferred}"`,
2292
+ range: f.range,
2293
+ fix: { title: `Rename to ${preferred}`, replacement: preferred }
2294
+ });
2295
+ } else if (!f.known) {
2296
+ diagnostics.push({
2297
+ severity: "error",
2298
+ code: "unknown-filter",
2299
+ message: `Unknown filter "${f.name}"`,
2300
+ range: f.range
2301
+ });
2302
+ }
2303
+ }
2304
+ const byPosition = (a, b) => a.range.start.line - b.range.start.line || a.range.start.column - b.range.start.column;
2305
+ variables.sort(byPosition);
2306
+ filters.sort(byPosition);
2307
+ const dataDiagnostics = missingPathDiagnostics(
2308
+ variables,
2309
+ opts.sampleData,
2310
+ loopSources,
2311
+ /* @__PURE__ */ new Set([...builtinRoots3, ...helpers.names()])
2312
+ );
2313
+ return {
2314
+ variables,
2315
+ filters,
2316
+ includes,
2317
+ blocks,
2318
+ diagnostics: [...diagnostics, ...dataDiagnostics]
2319
+ };
2320
+ }
2321
+ var LiquidCompiled = class {
2322
+ constructor(name, source) {
2323
+ this.name = name;
2324
+ this.source = source;
2325
+ }
2326
+ engine = "liquid";
2327
+ runtimes = /* @__PURE__ */ new WeakMap();
2328
+ runtime(ctx) {
2329
+ let rt = this.runtimes.get(ctx);
2330
+ if (!rt) {
2331
+ const liquid = createLiquid(ctx);
2332
+ rt = { liquid, templates: liquid.parse(this.source, this.name) };
2333
+ this.runtimes.set(ctx, rt);
2334
+ }
2335
+ return rt;
2336
+ }
2337
+ async render(data, ctx) {
2338
+ const started = Date.now();
2339
+ const { liquid, templates } = this.runtime(ctx);
2340
+ try {
2341
+ const out = await liquid.render(
2342
+ templates,
2343
+ data ?? {}
2344
+ );
2345
+ return enforceLimits(out, started, ctx.limits);
2346
+ } catch (e) {
2347
+ if (e instanceof Error && e.name === "RenderLimitError") throw e;
2348
+ const err = e;
2349
+ const pos = err.token?.getPosition?.();
2350
+ throw new RenderError(
2351
+ "liquid",
2352
+ String(err.message ?? e),
2353
+ pos?.[0],
2354
+ pos?.[1]
2355
+ );
2356
+ }
2357
+ }
2358
+ };
2359
+ var liquidEngine = {
2360
+ id: "liquid",
2361
+ compile(source, opts = {}) {
2362
+ const name = opts.name ?? "template";
2363
+ try {
2364
+ createLiquid(validationContext3).parse(source, name);
2365
+ } catch (e) {
2366
+ throw toSyntaxError3(e);
2367
+ }
2368
+ return new LiquidCompiled(name, source);
2369
+ },
2370
+ analyze: analyzeLiquid,
2371
+ render: (tpl, data, ctx) => tpl.render(data, ctx)
2372
+ };
2373
+
2374
+ // packages/engine/src/lib/engines/index.ts
2375
+ var engines = {
2376
+ jinja2: jinja2Engine,
2377
+ liquid: liquidEngine,
2378
+ handlebars: handlebarsEngine
2379
+ };
2380
+ var engineIds = ["jinja2", "liquid", "handlebars"];
2381
+ function getEngine(id) {
2382
+ const engine = engines[id];
2383
+ if (!engine)
2384
+ throw new Error(
2385
+ `Unknown template engine "${id}"; expected one of ${engineIds.join(", ")}`
2386
+ );
2387
+ return engine;
2388
+ }
2389
+
2390
+ // packages/engine/src/lib/schema.ts
2391
+ var isoDateTime = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
2392
+ var isoDate = /^\d{4}-\d{2}-\d{2}$/;
2393
+ var email = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
2394
+ var uri = /^https?:\/\/\S+$/;
2395
+ function typeOf(value) {
2396
+ if (value === null || value === void 0) return "null";
2397
+ if (Array.isArray(value)) return "array";
2398
+ if (typeof value === "number")
2399
+ return Number.isInteger(value) ? "integer" : "number";
2400
+ return typeof value === "object" ? "object" : typeof value;
2401
+ }
2402
+ function formatOf(value) {
2403
+ if (isoDateTime.test(value)) return "date-time";
2404
+ if (isoDate.test(value)) return "date";
2405
+ if (email.test(value)) return "email";
2406
+ if (uri.test(value)) return "uri";
2407
+ return void 0;
2408
+ }
2409
+ function unionTypes(a, b) {
2410
+ const set = /* @__PURE__ */ new Set([
2411
+ ...Array.isArray(a) ? a : a ? [a] : [],
2412
+ ...Array.isArray(b) ? b : b ? [b] : []
2413
+ ]);
2414
+ if (set.has("number")) set.delete("integer");
2415
+ const list2 = [...set];
2416
+ return list2.length === 1 ? list2[0] : list2;
2417
+ }
2418
+ function merge(a, b) {
2419
+ if (!a) return b;
2420
+ const out = { type: unionTypes(a.type, b.type) };
2421
+ if (a.format && a.format === b.format) out.format = a.format;
2422
+ if (a.properties || b.properties) {
2423
+ out.properties = { ...a.properties };
2424
+ for (const [key, schema] of Object.entries(b.properties ?? {}))
2425
+ out.properties[key] = merge(out.properties[key], schema);
2426
+ const req = new Set(a.required ?? []);
2427
+ out.required = (b.required ?? []).filter((k) => req.has(k));
2428
+ if (out.required.length === 0) delete out.required;
2429
+ }
2430
+ if (a.items || b.items) out.items = merge(a.items, b.items ?? {});
2431
+ const examples = [...a.examples ?? [], ...b.examples ?? []].filter(
2432
+ (v, i, arr) => arr.findIndex((x) => JSON.stringify(x) === JSON.stringify(v)) === i
2433
+ );
2434
+ if (examples.length) out.examples = examples.slice(0, 3);
2435
+ return out;
2436
+ }
2437
+ function infer(value) {
2438
+ const type = typeOf(value);
2439
+ switch (type) {
2440
+ case "array": {
2441
+ const items = value.map(infer);
2442
+ const schema = { type: "array" };
2443
+ if (items.length)
2444
+ schema.items = items.reduce(
2445
+ (acc, s) => merge(acc, s),
2446
+ void 0
2447
+ );
2448
+ return schema;
2449
+ }
2450
+ case "object": {
2451
+ const properties = {};
2452
+ for (const [key, v] of Object.entries(value))
2453
+ properties[key] = infer(v);
2454
+ return {
2455
+ type: "object",
2456
+ properties,
2457
+ required: Object.keys(properties),
2458
+ additionalProperties: true
2459
+ };
2460
+ }
2461
+ case "string": {
2462
+ const schema = { type: "string", examples: [value] };
2463
+ const format = formatOf(value);
2464
+ if (format) schema.format = format;
2465
+ return schema;
2466
+ }
2467
+ case "null":
2468
+ return { type: "null" };
2469
+ default:
2470
+ return { type, examples: [value] };
2471
+ }
2472
+ }
2473
+ function inferSchema(sample) {
2474
+ return {
2475
+ $schema: "https://json-schema.org/draft/2020-12/schema",
2476
+ ...infer(sample)
2477
+ };
2478
+ }
2479
+ function schemaPaths(schema, prefix = []) {
2480
+ const out = [];
2481
+ const type = Array.isArray(schema.type) ? schema.type.join(" | ") : schema.type ?? "any";
2482
+ if (prefix.length)
2483
+ out.push({
2484
+ path: prefix,
2485
+ type,
2486
+ format: schema.format,
2487
+ example: schema.examples?.[0]
2488
+ });
2489
+ for (const [key, child] of Object.entries(schema.properties ?? {}))
2490
+ out.push(...schemaPaths(child, [...prefix, key]));
2491
+ if (schema.items) out.push(...schemaPaths(schema.items, [...prefix, "[]"]));
2492
+ return out;
2493
+ }
2494
+
2495
+ // packages/engine/src/lib/snippets.ts
2496
+ var loop = (engine, item, list2, body) => engine === "handlebars" ? `{{#each ${list2}}}
2497
+ ${body.replace(/\{\{ item\./g, "{{ ")}
2498
+ {{/each}}` : `{% for ${item} in ${list2} %}
2499
+ ${body}
2500
+ {% endfor %}`;
2501
+ var val = (engine, expr, filter) => {
2502
+ if (!filter) return `{{ ${expr} }}`;
2503
+ if (engine === "handlebars")
2504
+ return `{{${filter.split("(")[0]} ${expr}${filter.includes("(") ? " " + filter.slice(filter.indexOf("(") + 1, -1) : ""}}}`;
2505
+ return engine === "liquid" ? `{{ ${expr} | ${filter.replace("(", ": ").replace(")", "")} }}` : `{{ ${expr} | ${filter} }}`;
2506
+ };
2507
+ function forAll(build) {
2508
+ return {
2509
+ jinja2: build("jinja2"),
2510
+ liquid: build("liquid"),
2511
+ handlebars: build("handlebars")
2512
+ };
2513
+ }
2514
+ var snippets = [
2515
+ {
2516
+ id: "invoice-table",
2517
+ title: "Invoice table",
2518
+ description: "Line items with description, quantity, unit price and line total.",
2519
+ source: forAll(
2520
+ (e) => `<table class="ff-lines">
2521
+ <thead><tr><th>Description</th><th class="num">Qty</th><th class="num">Unit price</th><th class="num">Total</th></tr></thead>
2522
+ <tbody>
2523
+ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</td><td class="num">${val(e, "line.qty", "number(0)")}</td><td class="num">${val(e, "line.price", "money")}</td><td class="num">${val(e, "line.total", "money")}</td></tr>`)}
2524
+ </tbody>
2525
+ </table>$0`
2526
+ ),
2527
+ sampleData: {
2528
+ invoice: {
2529
+ lines: [
2530
+ { description: "Consulting", qty: 8, price: 120, total: 960 },
2531
+ { description: "Implementation", qty: 20, price: 110, total: 2200 }
2532
+ ]
2533
+ }
2534
+ },
2535
+ css: `.ff-lines { width: 100%; border-collapse: collapse; font-size: 10pt; }
2536
+ .ff-lines th, .ff-lines td { padding: 6px 8px; border-bottom: 1px solid #ddd; text-align: left; }
2537
+ .ff-lines .num { text-align: right; white-space: nowrap; }
2538
+ .ff-lines thead th { border-bottom: 2px solid #222; }`
2539
+ },
2540
+ {
2541
+ id: "totals",
2542
+ title: "Totals block",
2543
+ description: "Net, VAT and gross totals right-aligned under a table.",
2544
+ source: forAll(
2545
+ (e) => `<table class="ff-totals">
2546
+ <tr><td>Net</td><td class="num">${val(e, "invoice.net", "money")}</td></tr>
2547
+ <tr><td>VAT ${val(e, "invoice.vat_rate")} %</td><td class="num">${val(e, "invoice.vat", "money")}</td></tr>
2548
+ <tr class="grand"><td>Total</td><td class="num">${val(e, "invoice.total", "money")}</td></tr>
2549
+ </table>$0`
2550
+ ),
2551
+ sampleData: {
2552
+ invoice: { net: 3160, vat_rate: 19, vat: 600.4, total: 3760.4 }
2553
+ },
2554
+ css: `.ff-totals { margin-left: auto; margin-top: 12px; border-collapse: collapse; font-size: 10pt; }
2555
+ .ff-totals td { padding: 4px 8px; }
2556
+ .ff-totals .num { text-align: right; min-width: 90px; }
2557
+ .ff-totals .grand td { border-top: 2px solid #222; font-weight: 600; }`
2558
+ },
2559
+ {
2560
+ id: "address-block",
2561
+ title: "Address block",
2562
+ description: "Recipient name and postal address lines.",
2563
+ source: forAll(
2564
+ (e) => `<address class="ff-address">
2565
+ <strong>${val(e, "customer.name")}</strong><br>
2566
+ ${val(e, "customer.street")}<br>
2567
+ ${val(e, "customer.zip")} ${val(e, "customer.city")}<br>
2568
+ ${val(e, "customer.country")}
2569
+ </address>$0`
2570
+ ),
2571
+ sampleData: {
2572
+ customer: {
2573
+ name: "Muster GmbH",
2574
+ street: "Musterstra\xDFe 1",
2575
+ zip: "10115",
2576
+ city: "Berlin",
2577
+ country: "Deutschland"
2578
+ }
2579
+ },
2580
+ css: `.ff-address { font-style: normal; line-height: 1.4; }`
2581
+ },
2582
+ {
2583
+ id: "page-footer",
2584
+ title: "Footer with page numbers",
2585
+ description: "Paste into the Footer tab: company line and \u201CPage x of y\u201D.",
2586
+ source: forAll(
2587
+ (e) => `<div style="width:100%;display:flex;justify-content:space-between;font-size:9px;color:#666">
2588
+ <span>${val(e, "company.name")} \xB7 ${val(e, "company.email")}</span>
2589
+ <span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
2590
+ </div>$0`
2591
+ ),
2592
+ sampleData: { company: { name: "Acme GmbH", email: "hello@acme.example" } }
2593
+ },
2594
+ {
2595
+ id: "cover-page",
2596
+ title: "Cover page",
2597
+ description: "Title, subtitle and date on a page of its own.",
2598
+ source: forAll(
2599
+ (e) => `<section class="ff-cover">
2600
+ <h1>${val(e, "report.title")}</h1>
2601
+ <p class="subtitle">${val(e, "report.subtitle")}</p>
2602
+ <p class="date">${val(e, "report.date", "date('PPP')")}</p>
2603
+ </section>
2604
+ <div class="page-break"></div>$0`
2605
+ ),
2606
+ sampleData: {
2607
+ report: {
2608
+ title: "Quarterly report",
2609
+ subtitle: "Q3 2026",
2610
+ date: "2026-09-30"
2611
+ }
2612
+ },
2613
+ css: `.ff-cover { min-height: 60vh; display: flex; flex-direction: column; justify-content: center; }
2614
+ .ff-cover h1 { font-size: 32pt; margin: 0 0 8px; }
2615
+ .ff-cover .subtitle { font-size: 16pt; color: #444; margin: 0; }
2616
+ .ff-cover .date { margin-top: 24px; color: #666; }`
2617
+ },
2618
+ {
2619
+ id: "signature",
2620
+ title: "Signature line",
2621
+ description: "Place, date and a signature line.",
2622
+ source: forAll(
2623
+ (e) => `<div class="ff-signature">
2624
+ <div class="line"></div>
2625
+ <div>${val(e, "signature.place")}, ${val(e, "signature.date", "date('PP')")} \xB7 ${val(e, "signature.name")}</div>
2626
+ </div>$0`
2627
+ ),
2628
+ sampleData: {
2629
+ signature: {
2630
+ place: "Berlin",
2631
+ date: "2026-09-07",
2632
+ name: "Philipp Staudt"
2633
+ }
2634
+ },
2635
+ css: `.ff-signature { margin-top: 48px; width: 260px; font-size: 9pt; color: #444; }
2636
+ .ff-signature .line { border-top: 1px solid #222; margin-bottom: 6px; }`
2637
+ },
2638
+ {
2639
+ id: "epc-qr",
2640
+ title: "SEPA payment QR code",
2641
+ description: "GiroCode that banking apps scan to prefill the transfer.",
2642
+ source: {
2643
+ jinja2: `<figure class="ff-qr">
2644
+ <img src="{{ epcQr({ name: company.name, iban: company.iban, bic: company.bic, amount: invoice.total, reference: invoice.number }, { size: 120 }) }}" alt="Payment QR code">
2645
+ <figcaption>Scan to pay</figcaption>
2646
+ </figure>$0`,
2647
+ liquid: `<figure class="ff-qr">
2648
+ <img src="{{ payment | epcQr }}" alt="Payment QR code">
2649
+ <figcaption>Scan to pay</figcaption>
2650
+ </figure>$0`,
2651
+ handlebars: `<figure class="ff-qr">
2652
+ <img src="{{epcQr payment size=120}}" alt="Payment QR code">
2653
+ <figcaption>Scan to pay</figcaption>
2654
+ </figure>$0`
2655
+ },
2656
+ sampleData: {
2657
+ company: {
2658
+ name: "Acme GmbH",
2659
+ iban: "DE02120300000000202051",
2660
+ bic: "BYLADEM1001"
2661
+ },
2662
+ invoice: { number: "2026-0042", total: 3760.4 },
2663
+ payment: {
2664
+ name: "Acme GmbH",
2665
+ iban: "DE02120300000000202051",
2666
+ bic: "BYLADEM1001",
2667
+ amount: 3760.4,
2668
+ reference: "2026-0042"
2669
+ }
2670
+ },
2671
+ css: `.ff-qr { margin: 16px 0; text-align: center; width: 140px; }
2672
+ .ff-qr figcaption { font-size: 8pt; color: #666; }`
2673
+ },
2674
+ {
2675
+ id: "chart",
2676
+ title: "Chart",
2677
+ description: "Bar chart drawn with Chart.js from a chart configuration in the data.",
2678
+ source: {
2679
+ jinja2: `<figure class="ff-chart-box">
2680
+ {{ chart(salesChart) }}
2681
+ <figcaption>{{ salesChart.caption }}</figcaption>
2682
+ </figure>$0`,
2683
+ liquid: `<figure class="ff-chart-box">
2684
+ {{ salesChart | chart }}
2685
+ <figcaption>{{ salesChart.caption }}</figcaption>
2686
+ </figure>$0`,
2687
+ handlebars: `<figure class="ff-chart-box">
2688
+ {{chart salesChart}}
2689
+ <figcaption>{{salesChart.caption}}</figcaption>
2690
+ </figure>$0`
2691
+ },
2692
+ sampleData: {
2693
+ salesChart: {
2694
+ type: "bar",
2695
+ caption: "Revenue per quarter",
2696
+ width: 480,
2697
+ height: 240,
2698
+ data: {
2699
+ labels: ["Q1", "Q2", "Q3", "Q4"],
2700
+ datasets: [
2701
+ {
2702
+ label: "Revenue",
2703
+ data: [12e3, 15500, 14200, 18900],
2704
+ backgroundColor: "#3E63DD"
2705
+ }
2706
+ ]
2707
+ }
2708
+ }
2709
+ },
2710
+ css: `.ff-chart-box { margin: 16px 0; break-inside: avoid; }
2711
+ .ff-chart-box figcaption { font-size: 9pt; color: #666; margin-top: 4px; }`
2712
+ },
2713
+ {
2714
+ id: "two-columns",
2715
+ title: "Two-column layout",
2716
+ description: "Two equal columns that stay side by side in print.",
2717
+ source: forAll(
2718
+ () => `<div class="ff-columns">
2719
+ <div>$1</div>
2720
+ <div>$0</div>
2721
+ </div>`
2722
+ ),
2723
+ css: `.ff-columns { display: flex; gap: 24px; }
2724
+ .ff-columns > div { flex: 1 1 0; min-width: 0; }`
2725
+ }
2726
+ ];
2727
+ function snippetsFor(engine) {
2728
+ return snippets.map((s) => ({ ...s, code: s.source[engine] }));
2729
+ }
2730
+ function mergeSampleData(base, extra) {
2731
+ if (!extra) return base;
2732
+ const target = base && typeof base === "object" && !Array.isArray(base) ? { ...base } : {};
2733
+ for (const [key, value] of Object.entries(extra)) {
2734
+ const existing = target[key];
2735
+ if (existing === void 0) target[key] = value;
2736
+ else if (existing && typeof existing === "object" && !Array.isArray(existing) && value && typeof value === "object" && !Array.isArray(value)) {
2737
+ target[key] = mergeSampleData(existing, value);
2738
+ }
2739
+ }
2740
+ return target;
2741
+ }
2742
+
2743
+ // packages/api-types/src/lib/apitemplate.ts
2744
+ var paperFormats = {
2745
+ a3: "A3",
2746
+ a4: "A4",
2747
+ a5: "A5",
2748
+ letter: "Letter",
2749
+ legal: "Legal",
2750
+ tabloid: "Tabloid",
2751
+ ledger: "Ledger"
2752
+ };
2753
+ function pick(input, ...keys) {
2754
+ for (const key of keys) if (input[key] !== void 0 && input[key] !== null && input[key] !== "") return input[key];
2755
+ return void 0;
2756
+ }
2757
+ function toLength(value) {
2758
+ if (value === void 0 || value === null || value === "") return void 0;
2759
+ if (typeof value === "number") return `${value}mm`;
2760
+ const text = String(value).trim();
2761
+ if (/^-?\d+(\.\d+)?$/.test(text)) return `${text}mm`;
2762
+ if (/^-?\d+(\.\d+)?(mm|cm|in|px|pt)$/i.test(text)) return text.toLowerCase();
2763
+ return void 0;
2764
+ }
2765
+ var truthy2 = (value) => value === true || value === 1 || value === "1" || value === "true" || value === "yes";
2766
+ function convertHeaderFooter(html) {
2767
+ if (typeof html !== "string" || !html.trim()) return void 0;
2768
+ return html.replace(/\{\{\s*page_number\s*\}\}/gi, '<span class="pageNumber"></span>').replace(/\{\{\s*total_pages\s*\}\}/gi, '<span class="totalPages"></span>');
2769
+ }
2770
+ function mapApitemplateSettings(input) {
2771
+ const settings = {};
2772
+ const notes = [];
2773
+ const used = /* @__PURE__ */ new Set();
2774
+ const take = (...keys) => {
2775
+ const value = pick(input ?? {}, ...keys);
2776
+ for (const key of keys) if ((input ?? {})[key] !== void 0) used.add(key);
2777
+ return value;
2778
+ };
2779
+ if (!input) return { settings, ignored: [], notes };
2780
+ const paperSize = take("paper_size", "paperSize", "page_size", "format");
2781
+ const orientation = take("orientation");
2782
+ const width = toLength(take("custom_width", "customWidth", "width"));
2783
+ const height = toLength(take("custom_height", "customHeight", "height"));
2784
+ const paper = {};
2785
+ if (typeof paperSize === "string") {
2786
+ const format = paperFormats[paperSize.toLowerCase()];
2787
+ if (format) paper["format"] = format;
2788
+ else notes.push(`paper size "${paperSize}" is not a known format; A4 is used`);
2789
+ }
2790
+ if (width && height) {
2791
+ paper["width"] = width;
2792
+ paper["height"] = height;
2793
+ notes.push("custom page size kept as explicit width and height");
2794
+ }
2795
+ if (orientation !== void 0) {
2796
+ const landscape = orientation === "landscape" || orientation === 2 || orientation === "2" || orientation === "Landscape";
2797
+ paper["landscape"] = landscape;
2798
+ }
2799
+ if (Object.keys(paper).length) settings["paper"] = paper;
2800
+ const margin = {};
2801
+ for (const side of ["top", "right", "bottom", "left"]) {
2802
+ const value = toLength(take(`margin_${side}`, `margin${side[0].toUpperCase()}${side.slice(1)}`));
2803
+ if (value) margin[side] = value;
2804
+ }
2805
+ if (Object.keys(margin).length) settings["margin"] = margin;
2806
+ const header = convertHeaderFooter(take("header_template", "headerTemplate", "header_html", "header"));
2807
+ const footer = convertHeaderFooter(take("footer_template", "footerTemplate", "footer_html", "footer"));
2808
+ const displayHeaderFooter = take("displayHeaderFooter", "display_header_footer", "print_header_footer");
2809
+ if (header && (displayHeaderFooter === void 0 || truthy2(displayHeaderFooter))) settings["header"] = { html: header };
2810
+ if (footer && (displayHeaderFooter === void 0 || truthy2(displayHeaderFooter))) settings["footer"] = { html: footer };
2811
+ const printBackground = take("print_background", "printBackground");
2812
+ if (printBackground !== void 0) settings["printBackground"] = truthy2(printBackground);
2813
+ const scale = take("scale");
2814
+ if (scale !== void 0 && Number.isFinite(Number(scale))) settings["scale"] = Number(scale);
2815
+ const pageRanges = take("page_ranges", "pageRanges");
2816
+ if (typeof pageRanges === "string" && pageRanges.trim()) settings["pageRanges"] = pageRanges.trim();
2817
+ const preferCss = take("prefer_css_page_size", "preferCSSPageSize", "preferCssPageSize");
2818
+ if (preferCss !== void 0) settings["preferCssPageSize"] = truthy2(preferCss);
2819
+ const imageType = take("output_image_type", "outputImageType", "image_type");
2820
+ const viewportWidth = take("viewport_width", "viewportWidth", "image_width");
2821
+ const viewportHeight = take("viewport_height", "viewportHeight", "image_height");
2822
+ const deviceScale = take("device_scale", "deviceScaleFactor", "image_resample_scale");
2823
+ const image = {};
2824
+ if (imageType !== void 0) {
2825
+ const t = String(imageType).toLowerCase();
2826
+ image["format"] = t === "1" || t === "jpeg" || t === "jpg" ? "jpeg" : "png";
2827
+ }
2828
+ if (viewportWidth !== void 0 && Number.isFinite(Number(viewportWidth))) image["width"] = Number(viewportWidth);
2829
+ if (viewportHeight !== void 0 && Number.isFinite(Number(viewportHeight))) image["height"] = Number(viewportHeight);
2830
+ if (deviceScale !== void 0 && Number.isFinite(Number(deviceScale))) image["deviceScaleFactor"] = Number(deviceScale);
2831
+ if (Object.keys(image).length) settings["image"] = image;
2832
+ const ignored = Object.keys(input).filter((k) => !used.has(k));
2833
+ return { settings, ignored, notes };
2834
+ }
2835
+
2836
+ // packages/engine/src/lib/import/apitemplate.ts
2837
+ function slugFromName(name) {
2838
+ const slug = name.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
2839
+ return slug.length >= 2 ? slug : `imported-${Date.now().toString(36)}`;
2840
+ }
2841
+ function parseSample(value) {
2842
+ if (value === void 0 || value === null || value === "") return { data: {} };
2843
+ if (typeof value !== "string") return { data: value };
2844
+ try {
2845
+ return { data: JSON.parse(value) };
2846
+ } catch (e) {
2847
+ return { data: {}, error: e instanceof Error ? e.message : String(e) };
2848
+ }
2849
+ }
2850
+ var pythonisms = [
2851
+ {
2852
+ pattern: /\{\{[^}]*%\s*\(/,
2853
+ code: "python-format-operator",
2854
+ message: 'Python "%" string formatting is not supported; use the format() shim or a filter such as number() or money().',
2855
+ docs: "/migrate/apitemplate-io#strings"
2856
+ },
2857
+ {
2858
+ pattern: /loop\.cycle\s*\(/,
2859
+ code: "loop-cycle",
2860
+ message: "`loop.cycle()` is not available; use `cycler()` or `loop.index % n`.",
2861
+ docs: "/migrate/apitemplate-io#loops"
2862
+ },
2863
+ {
2864
+ pattern: /\{%-?\s*(macro|call|filter|autoescape)\b/,
2865
+ code: "partial-support",
2866
+ message: "Jinja2 macros, call blocks, filter blocks and autoescape blocks run with Nunjucks semantics; check the output.",
2867
+ docs: "/templates/languages#jinja2"
2868
+ },
2869
+ {
2870
+ pattern: /\bis\s+(divisibleby|sameas|escaped|mapping|sequence|callable)\b/,
2871
+ code: "jinja-test",
2872
+ message: "This Jinja2 test is not available; rewrite it with an expression (for example `x % 2 == 0`).",
2873
+ docs: "/migrate/apitemplate-io#tests"
2874
+ },
2875
+ {
2876
+ pattern: /\{%-?\s*(import|from)\b/,
2877
+ code: "imports",
2878
+ message: "Jinja2 imports refer to files apitemplate.io kept next to the template; move the macros into this template or a Formfeed partial.",
2879
+ docs: "/templates/languages#partials"
2880
+ }
2881
+ ];
2882
+ function positionOf2(source, index) {
2883
+ const before = source.slice(0, index);
2884
+ const line = before.split("\n").length;
2885
+ const column = index - before.lastIndexOf("\n");
2886
+ return { line, column };
2887
+ }
2888
+ function setInLoop(source) {
2889
+ const notes = [];
2890
+ const forRe = /\{%-?\s*for\b[\s\S]*?\{%-?\s*endfor\s*-?%\}/g;
2891
+ for (const m of source.matchAll(forRe)) {
2892
+ const setAt = m[0].search(/\{%-?\s*set\s+\w+\s*=/);
2893
+ if (setAt >= 0) {
2894
+ const pos = positionOf2(source, m.index + setAt);
2895
+ notes.push({
2896
+ code: "set-in-loop",
2897
+ message: "A {% set %} inside a loop: in Python Jinja2 the value is lost after the loop, in Nunjucks it is kept. Check code that relies on either behaviour.",
2898
+ docs: "/migrate/apitemplate-io#loops",
2899
+ ...pos
2900
+ });
2901
+ }
2902
+ }
2903
+ return notes;
2904
+ }
2905
+ function fromDiagnostic(d) {
2906
+ const docs = d.code === "unknown-filter" ? "/templates/helpers" : d.code.startsWith("missing-") ? "/migrate/apitemplate-io#sample-data" : "/templates/languages#jinja2";
2907
+ return { code: d.code, message: d.message, line: d.range.start.line, column: d.range.start.column, docs };
2908
+ }
2909
+ function importApitemplate(input) {
2910
+ const errors = [];
2911
+ const warnings = [];
2912
+ const changes = [];
2913
+ const name = (input.name ?? "").trim() || "Imported template";
2914
+ const format = String(input.format ?? "").toUpperCase();
2915
+ const kind = format === "JPEG" || format === "PNG" || format === "JPG" ? "image" : "pdf";
2916
+ if (kind === "image") changes.push(`${format} template imported as an image template`);
2917
+ const mapped = mapApitemplateSettings(input.settings ?? void 0);
2918
+ const settings = mapped.settings;
2919
+ for (const note of mapped.notes) changes.push(note);
2920
+ for (const key of mapped.ignored)
2921
+ warnings.push({ code: "setting-ignored", message: `Setting "${key}" has no Formfeed equivalent and was dropped.`, docs: "/migrate/apitemplate-io#settings" });
2922
+ if (settings.header?.html || settings.footer?.html) changes.push("header and footer templates moved to the Header and Footer tabs");
2923
+ if (kind === "image" && !settings.image) {
2924
+ settings.image = { width: 1200, height: 630, deviceScaleFactor: 2, format: format === "PNG" ? "png" : "jpeg" };
2925
+ changes.push("image size defaults to 1200 \xD7 630 (change it in Settings)");
2926
+ }
2927
+ const sample = parseSample(input.sample_data);
2928
+ if (sample.error) warnings.push({ code: "sample-json", message: `Sample data is not valid JSON (${sample.error}); the draft starts without sample data.`, docs: "/migrate/apitemplate-io#sample-data" });
2929
+ const html = input.html ?? "";
2930
+ const css2 = input.css ?? "";
2931
+ for (const rule of pythonisms) {
2932
+ const m = rule.pattern.exec(html);
2933
+ if (m) warnings.push({ code: rule.code, message: rule.message, docs: rule.docs, ...positionOf2(html, m.index) });
2934
+ }
2935
+ warnings.push(...setInLoop(html));
2936
+ const engine = getEngine("jinja2");
2937
+ try {
2938
+ engine.compile(html, { name });
2939
+ } catch (e) {
2940
+ const err = e;
2941
+ errors.push({ code: "syntax-error", message: err.message, line: err.line, column: err.column, docs: "/templates/languages#jinja2" });
2942
+ }
2943
+ const analysis = engine.analyze(html, { sampleData: sample.data });
2944
+ for (const d of analysis.diagnostics) {
2945
+ const note = fromDiagnostic(d);
2946
+ if (d.severity === "error") {
2947
+ if (!errors.some((x) => x.code === note.code && x.line === note.line)) errors.push(note);
2948
+ } else warnings.push(note);
2949
+ }
2950
+ const aliasHits = analysis.filters.filter((f) => ["currency_format", "table", "page_break", "raw", "translate"].includes(f.name));
2951
+ if (aliasHits.length) changes.push(`apitemplate.io filters kept through aliases: ${[...new Set(aliasHits.map((f) => f.name))].join(", ")}`);
2952
+ return {
2953
+ name,
2954
+ slug: slugFromName(name),
2955
+ kind,
2956
+ engine: "jinja2",
2957
+ html,
2958
+ css: css2,
2959
+ head: "",
2960
+ settings,
2961
+ sampleData: sample.data,
2962
+ errors,
2963
+ warnings,
2964
+ changes
2965
+ };
2966
+ }
2967
+
2968
+ // packages/engine/src/lib/assemble.ts
2969
+ function defaultSettings() {
2970
+ return {
2971
+ paper: {
2972
+ format: "A4",
2973
+ width: null,
2974
+ height: null,
2975
+ unit: "mm",
2976
+ landscape: false
2977
+ },
2978
+ margin: { top: "20mm", right: "15mm", bottom: "20mm", left: "15mm" },
2979
+ header: null,
2980
+ footer: null,
2981
+ printBackground: true,
2982
+ scale: 1,
2983
+ preferCssPageSize: false,
2984
+ pageRanges: "",
2985
+ waitFor: { networkIdle: true, selector: null, timeoutMs: 5e3, delayMs: 0 },
2986
+ pdf: { tagged: false, outline: false, metadata: { title: "", author: "" } },
2987
+ image: {
2988
+ width: 1200,
2989
+ height: 630,
2990
+ deviceScaleFactor: 2,
2991
+ format: "png",
2992
+ quality: 90,
2993
+ fullPage: false,
2994
+ transparent: false,
2995
+ clipSelector: null
2996
+ },
2997
+ locale: "en-GB",
2998
+ timezone: "Europe/Berlin",
2999
+ currency: "EUR",
3000
+ tailwind: false,
3001
+ watermark: null
3002
+ };
3003
+ }
3004
+ function isPlainObject(v) {
3005
+ return !!v && typeof v === "object" && !Array.isArray(v);
3006
+ }
3007
+ function mergeSettings(...layers) {
3008
+ const out = {};
3009
+ const apply = (target, source) => {
3010
+ for (const [key, value] of Object.entries(source)) {
3011
+ if (value === void 0) continue;
3012
+ if (isPlainObject(value) && isPlainObject(target[key]))
3013
+ apply(target[key], value);
3014
+ else if (isPlainObject(value)) target[key] = apply({}, value);
3015
+ else target[key] = value;
3016
+ }
3017
+ return target;
3018
+ };
3019
+ for (const layer of [defaultSettings(), ...layers])
3020
+ if (layer) apply(out, layer);
3021
+ return out;
3022
+ }
3023
+ function pageSize(settings) {
3024
+ const paper = settings.paper ?? {};
3025
+ if (paper.width && paper.height) return `${paper.width} ${paper.height}`;
3026
+ return `${paper.format ?? "A4"}${paper.landscape ? " landscape" : ""}`;
3027
+ }
3028
+ function printReset(settings, kind = "pdf") {
3029
+ const m = settings.margin ?? {};
3030
+ const page = kind === "pdf" ? `@page { size: ${pageSize(settings)}; margin: ${m.top ?? "0"} ${m.right ?? "0"} ${m.bottom ?? "0"} ${m.left ?? "0"}; }
3031
+ ` : "";
3032
+ return `${page}html, body { margin: 0; padding: 0; }
3033
+ body { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
3034
+ img, svg { max-width: 100%; }
3035
+ .page-break, .new-page { break-after: page; }
3036
+ .avoid-break { break-inside: avoid; }
3037
+ table { break-inside: auto; border-collapse: collapse; }
3038
+ tr { break-inside: avoid; }
3039
+ thead { display: table-header-group; }
3040
+ tfoot { display: table-footer-group; }
3041
+ ${kind === "image" ? `html, body { width: ${settings.image?.width ?? 1200}px; min-height: ${settings.image?.height ?? 630}px; overflow: hidden; }
3042
+ ` : ""}`;
3043
+ }
3044
+ var fontFormats = {
3045
+ woff2: "woff2",
3046
+ woff: "woff",
3047
+ ttf: "truetype",
3048
+ otf: "opentype"
3049
+ };
3050
+ function fontFaceCss(fonts, referencedIn) {
3051
+ if (!fonts?.length) return "";
3052
+ const haystack = referencedIn.toLowerCase();
3053
+ return fonts.filter((f) => haystack.includes(f.family.toLowerCase())).map((f) => {
3054
+ const format = fontFormats[f.format ?? "woff2"] ?? f.format ?? "woff2";
3055
+ return `@font-face { font-family: "${f.family.replace(/"/g, "")}"; font-weight: ${f.weight ?? 400}; font-style: ${f.style ?? "normal"}; font-display: block; src: url("${f.url.replace(/"/g, "%22")}") format("${format}"); }`;
3056
+ }).join("\n");
3057
+ }
3058
+ function chartInitScript() {
3059
+ return `<script>window.formfeedDrawCharts=function(){if(typeof Chart==='undefined')return;var all=Chart.instances||{};for(var k in all){if(all[k]&&all[k].canvas&&!all[k].canvas.isConnected){try{all[k].destroy();}catch(e){}}}var cs=document.querySelectorAll('canvas[data-ff-chart]');for(var i=0;i<cs.length;i++){var c=cs[i];try{if(Chart.getChart(c))continue;var s=JSON.parse(c.getAttribute('data-ff-chart')||'{}');new Chart(c.getContext('2d'),s);c.setAttribute('data-ff-chart-ready','1');}catch(e){console.error('formfeed chart',e);}}};if(!window.PagedConfig){window.formfeedDrawCharts();}</script>`;
3060
+ }
3061
+ function chartScripts(html, vendor) {
3062
+ if (!vendor?.chartJs || !html.includes("data-ff-chart"))
3063
+ return { head: "", body: "" };
3064
+ const lib = "inline" in vendor.chartJs ? `<script>${vendor.chartJs.inline}</script>` : `<script src="${escapeHtml(vendor.chartJs.src)}"></script>`;
3065
+ return { head: `${lib}
3066
+ `, body: `${chartInitScript()}
3067
+ ` };
3068
+ }
3069
+ function assembleDocument(input) {
3070
+ const settings = mergeSettings(input.settings);
3071
+ const kind = input.kind ?? "pdf";
3072
+ const locale = settings.locale ?? "en";
3073
+ const base = input.assetBaseUrl ? `<base href="${escapeHtml(input.assetBaseUrl.replace(/\/?$/, "/"))}">
3074
+ ` : "";
3075
+ const title = input.title ? `<title>${escapeHtml(input.title)}</title>
3076
+ ` : "";
3077
+ const fonts = fontFaceCss(
3078
+ input.fonts,
3079
+ `${input.css ?? ""}
3080
+ ${input.head ?? ""}
3081
+ ${input.html}`
3082
+ );
3083
+ const fontStyle = fonts ? `<style data-formfeed="fonts">${fonts}</style>
3084
+ ` : "";
3085
+ const tailwind = settings.tailwind ? input.extraCss ? `<style data-formfeed="tailwind">${input.extraCss}</style>
3086
+ ` : input.vendor?.tailwind ? `<script src="${escapeHtml(input.vendor.tailwind.src)}"></script>
3087
+ ` : "" : input.extraCss ? `<style data-formfeed="extra">${input.extraCss}</style>
3088
+ ` : "";
3089
+ const charts = chartScripts(input.html, input.vendor);
3090
+ return `<!doctype html>
3091
+ <html lang="${escapeHtml(locale.split("_")[0] ?? locale)}">
3092
+ <head>
3093
+ <meta charset="utf-8">
3094
+ <meta name="viewport" content="width=device-width, initial-scale=1">
3095
+ ${base}${title}${input.head ?? ""}
3096
+ ${fontStyle}<style data-formfeed="reset">${printReset(settings, kind)}</style>
3097
+ ${tailwind}<style data-formfeed="template">${input.css ?? ""}</style>
3098
+ ${charts.head}</head>
3099
+ <body class="formfeed-body formfeed-${kind}${input.mode === "preview" ? " formfeed-preview" : ""}">
3100
+ ${input.html}
3101
+ ${charts.body}</body>
3102
+ </html>
3103
+ `;
3104
+ }
3105
+ async function renderVersion(version, data, ctx, mode = "print", extras = {}) {
3106
+ const engine = getEngine(version.engine);
3107
+ const settings = mergeSettings(version.settings);
3108
+ const renderCtx = {
3109
+ ...ctx,
3110
+ locale: settings.locale ?? ctx.locale,
3111
+ timezone: settings.timezone ?? ctx.timezone,
3112
+ currency: settings.currency ?? ctx.currency
3113
+ };
3114
+ const renderPart = async (source, name) => source ? engine.render(engine.compile(source, { name }), data, renderCtx) : void 0;
3115
+ const [html, headerHtml, footerHtml, title] = await Promise.all([
3116
+ engine.render(
3117
+ engine.compile(version.html, { name: "template" }),
3118
+ data,
3119
+ renderCtx
3120
+ ),
3121
+ renderPart(settings.header?.html, "header"),
3122
+ renderPart(settings.footer?.html, "footer"),
3123
+ renderPart(settings.pdf?.metadata?.title, "title")
3124
+ ]);
3125
+ const inlineCss = [
3126
+ fontFaceCss(
3127
+ extras.fonts,
3128
+ `${version.css ?? ""}
3129
+ ${version.head ?? ""}
3130
+ ${html}
3131
+ ${headerHtml ?? ""}
3132
+ ${footerHtml ?? ""}`
3133
+ ),
3134
+ extras.extraCss ?? ""
3135
+ ].filter(Boolean).join("\n");
3136
+ return {
3137
+ document: assembleDocument({
3138
+ html,
3139
+ css: version.css,
3140
+ head: version.head,
3141
+ settings,
3142
+ kind: version.kind,
3143
+ assetBaseUrl: ctx.assetBaseUrl,
3144
+ mode,
3145
+ title: title?.trim() || void 0,
3146
+ ...extras
3147
+ }),
3148
+ headerHtml,
3149
+ footerHtml,
3150
+ title: title?.trim() || void 0,
3151
+ settings,
3152
+ inlineCss
3153
+ };
3154
+ }
3155
+
3156
+ // packages/engine/src/lib/preview.ts
3157
+ var paperSizes = {
3158
+ A4: ["210mm", "297mm"],
3159
+ A5: ["148mm", "210mm"],
3160
+ A3: ["297mm", "420mm"],
3161
+ Letter: ["8.5in", "11in"],
3162
+ Legal: ["8.5in", "14in"]
3163
+ };
3164
+ function paperOf(settings) {
3165
+ const paper = settings.paper ?? {};
3166
+ const size = paper.width && paper.height ? [paper.width, paper.height] : paperSizes[paper.format ?? "A4"] ?? paperSizes["A4"];
3167
+ const w = size?.[0] ?? "210mm";
3168
+ const h = size?.[1] ?? "297mm";
3169
+ return paper.landscape ? { width: h, height: w } : { width: w, height: h };
3170
+ }
3171
+ var pageNumberSpans = (html, page, total) => html.replace(/<span class="pageNumber"><\/span>/g, page).replace(/<span class="totalPages"><\/span>/g, total);
3172
+ var inspectScript = `<script>
3173
+ document.addEventListener('click', function (event) {
3174
+ var el = event.target;
3175
+ if (!el || el.nodeType !== 1) return;
3176
+ try {
3177
+ parent.postMessage({
3178
+ type: 'formfeed:inspect',
3179
+ tag: el.tagName.toLowerCase(),
3180
+ id: el.id || null,
3181
+ classes: (el.className && el.className.baseVal !== undefined ? el.className.baseVal : el.className || '').toString().trim().split(' ').filter(Boolean),
3182
+ text: (el.textContent || '').trim().slice(0, 80),
3183
+ }, '*');
3184
+ } catch (e) {}
3185
+ }, true);
3186
+ </script>`;
3187
+ function flowDocument(draft) {
3188
+ const { settings, kind, headerHtml, footerHtml } = draft;
3189
+ const margin = settings.margin ?? {};
3190
+ const paper = paperOf(settings);
3191
+ const chrome = kind === "pdf" ? `<style data-formfeed="preview">
3192
+ html { background: #e5e7eb; }
3193
+ body.formfeed-preview { box-sizing: border-box; width: ${paper.width}; min-height: ${paper.height}; margin: 24px auto; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2), 0 8px 24px rgba(0,0,0,.08); padding: ${margin.top ?? "0"} ${margin.right ?? "0"} ${margin.bottom ?? "0"} ${margin.left ?? "0"}; }
3194
+ .formfeed-chrome { color: #6b7280; font: 10px/1.3 system-ui, sans-serif; }
3195
+ </style>` : `<style data-formfeed="preview">html { background: #e5e7eb; } body.formfeed-preview { margin: 24px auto; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2); }</style>`;
3196
+ const header = headerHtml ? `<div class="formfeed-chrome" style="margin-bottom:8px">${headerHtml}</div>` : "";
3197
+ const footer = footerHtml ? `<div class="formfeed-chrome" style="margin-top:8px">${pageNumberSpans(footerHtml, "1", "1")}</div>` : "";
3198
+ return draft.document.replace("</head>", `${chrome}${inspectScript}</head>`).replace(/(<body[^>]*>)/, `$1${header}`).replace("</body>", `${footer}</body>`);
3199
+ }
3200
+ function pagedDocument(draft, options) {
3201
+ const { headerHtml, footerHtml } = draft;
3202
+ const runningCss = `
3203
+ @page { ${headerHtml ? "@top-center { content: element(ffHeader); }" : ""} ${footerHtml ? "@bottom-center { content: element(ffFooter); }" : ""} }
3204
+ .ff-running-header { position: running(ffHeader); }
3205
+ .ff-running-footer { position: running(ffFooter); }
3206
+ .ff-running-header, .ff-running-footer { color: #6b7280; font: 10px/1.3 system-ui, sans-serif; width: 100%; }
3207
+ .ff-page-no::after { content: counter(page); }
3208
+ .ff-page-total::after { content: counter(pages); }
3209
+ html { background: #e5e7eb; }
3210
+ .pagedjs_pages { margin: 0 auto; }
3211
+ .pagedjs_page { background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2), 0 8px 24px rgba(0,0,0,.08); margin: 16px auto; }
3212
+ `;
3213
+ const config = `<script>window.PagedConfig = { auto: true, after: function (flow) { try { if (window.formfeedDrawCharts) window.formfeedDrawCharts(); } catch (e) {} try { parent.postMessage({ type: 'formfeed:pages', pages: flow.total }, '*'); } catch (e) {} } };</script>`;
3214
+ const script = `<script src="${options.pagedScriptUrl}"></script>`;
3215
+ const header = headerHtml ? `<div class="ff-running-header">${pageNumberSpans(headerHtml, '<span class="ff-page-no"></span>', '<span class="ff-page-total"></span>')}</div>` : "";
3216
+ const footer = footerHtml ? `<div class="ff-running-footer">${pageNumberSpans(footerHtml, '<span class="ff-page-no"></span>', '<span class="ff-page-total"></span>')}</div>` : "";
3217
+ return draft.document.replace("</head>", `<style data-formfeed="paged">${runningCss}</style>${config}${script}${inspectScript}</head>`).replace(/(<body[^>]*>)/, `$1${header}${footer}`);
3218
+ }
3219
+ export {
3220
+ DefaultHelperRegistry,
3221
+ EngineSyntaxError,
3222
+ RenderError,
3223
+ RenderLimitError,
3224
+ analyzeHandlebars,
3225
+ analyzeJinja2,
3226
+ analyzeLiquid,
3227
+ assembleDocument,
3228
+ builtinHelpers,
3229
+ chartInitScript,
3230
+ chartMarkup,
3231
+ chartPayload,
3232
+ createHelperRegistry,
3233
+ defaultHelpers,
3234
+ defaultLimits,
3235
+ defaultSettings,
3236
+ depthGuard,
3237
+ enforceLimits,
3238
+ engineIds,
3239
+ engines,
3240
+ epcPayload,
3241
+ escapeHtml,
3242
+ flowDocument,
3243
+ fontFaceCss,
3244
+ getEngine,
3245
+ handlebarsEngine,
3246
+ helperDocs,
3247
+ imageMarkup,
3248
+ importApitemplate,
3249
+ inferSchema,
3250
+ jinja2Engine,
3251
+ jinjaTags,
3252
+ liquidBuiltinFilters,
3253
+ liquidEngine,
3254
+ liquidTags,
3255
+ mergeSampleData,
3256
+ mergeSettings,
3257
+ nunjucksBuiltinFilters,
3258
+ pageNumberSpans,
3259
+ pageSize,
3260
+ pagedDocument,
3261
+ paperOf,
3262
+ printReset,
3263
+ qrSvg,
3264
+ renderVersion,
3265
+ resolvePath,
3266
+ scanBlocks,
3267
+ schemaPaths,
3268
+ slugFromName,
3269
+ snippets,
3270
+ snippetsFor,
3271
+ toDate,
3272
+ toNumber
3273
+ };