@formfeed/engine 0.3.5 → 0.3.6

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.
package/index.js CHANGED
@@ -77,8 +77,234 @@ function depthGuard(resolve2, includeDepth) {
77
77
 
78
78
  // packages/engine/src/lib/helpers/codes.ts
79
79
  import { toSVG } from "bwip-js";
80
+
81
+ // packages/engine/src/lib/values.ts
82
+ function toNumber(value) {
83
+ if (typeof value === "number") return value;
84
+ if (typeof value === "string") {
85
+ const n = Number(value.replace(/\s/g, "").replace(",", "."));
86
+ return Number.isFinite(n) ? n : NaN;
87
+ }
88
+ if (typeof value === "bigint") return Number(value);
89
+ return NaN;
90
+ }
91
+
92
+ // packages/engine/src/lib/codes/epc.ts
93
+ function epcPayload(input) {
94
+ const amount = input.amount === void 0 || input.amount === "" ? "" : `EUR${toNumber(input.amount).toFixed(2)}`;
95
+ return [
96
+ "BCD",
97
+ "002",
98
+ "1",
99
+ "SCT",
100
+ (input.bic ?? "").replace(/\s/g, ""),
101
+ String(input.name ?? "").slice(0, 70),
102
+ String(input.iban ?? "").replace(/\s/g, ""),
103
+ amount,
104
+ (input.purpose ?? "").slice(0, 4),
105
+ (input.reference ?? "").slice(0, 35),
106
+ input.reference ? "" : (input.text ?? "").slice(0, 140),
107
+ ""
108
+ ].join("\n");
109
+ }
110
+ var IBAN_LENGTHS = {
111
+ AD: 24,
112
+ AT: 20,
113
+ BE: 16,
114
+ BG: 22,
115
+ CH: 21,
116
+ CY: 28,
117
+ CZ: 24,
118
+ DE: 22,
119
+ DK: 18,
120
+ EE: 20,
121
+ ES: 24,
122
+ FI: 18,
123
+ FO: 18,
124
+ FR: 27,
125
+ GB: 22,
126
+ GI: 23,
127
+ GL: 18,
128
+ GR: 27,
129
+ HR: 21,
130
+ HU: 28,
131
+ IE: 22,
132
+ IS: 26,
133
+ IT: 27,
134
+ LI: 21,
135
+ LT: 20,
136
+ LU: 20,
137
+ LV: 21,
138
+ MC: 27,
139
+ MT: 31,
140
+ NL: 18,
141
+ NO: 15,
142
+ PL: 28,
143
+ PT: 25,
144
+ RO: 24,
145
+ SE: 24,
146
+ SI: 19,
147
+ SK: 24,
148
+ SM: 27,
149
+ VA: 22
150
+ };
151
+ var EPC_MAX_BYTES = 331;
152
+ var byteLength = (value) => new TextEncoder().encode(value).length;
153
+ var normaliseIban = (iban) => iban.replace(/\s/g, "").toUpperCase();
154
+ function ibanChecksum(iban) {
155
+ const rearranged = normaliseIban(iban).slice(4) + normaliseIban(iban).slice(0, 4);
156
+ const digits = [...rearranged].map((c) => c >= "A" && c <= "Z" ? String(c.charCodeAt(0) - 55) : c).join("");
157
+ let remainder = 0;
158
+ for (const digit of digits) remainder = (remainder * 10 + Number(digit)) % 97;
159
+ return remainder === 1;
160
+ }
161
+ function validateEpc(input) {
162
+ const problems = [];
163
+ const name = String(input.name ?? "").trim();
164
+ if (!name) problems.push({ field: "name", code: "required" });
165
+ else if (name.length > 70) problems.push({ field: "name", code: "too_long", max: 70, actual: name.length });
166
+ const iban = normaliseIban(String(input.iban ?? ""));
167
+ if (!iban) problems.push({ field: "iban", code: "required" });
168
+ else if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/.test(iban)) problems.push({ field: "iban", code: "iban_characters" });
169
+ else {
170
+ const expected = IBAN_LENGTHS[iban.slice(0, 2)];
171
+ if (expected !== void 0 && iban.length !== expected)
172
+ problems.push({ field: "iban", code: "iban_country_length", max: expected, actual: iban.length });
173
+ else if (!ibanChecksum(iban)) problems.push({ field: "iban", code: "iban_checksum" });
174
+ }
175
+ const bic = String(input.bic ?? "").replace(/\s/g, "").toUpperCase();
176
+ if (bic && !/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/.test(bic)) problems.push({ field: "bic", code: "bic_format" });
177
+ if (input.amount !== void 0 && input.amount !== "") {
178
+ const amount = toNumber(input.amount);
179
+ if (Number.isNaN(amount)) problems.push({ field: "amount", code: "not_a_number" });
180
+ else if (amount < 0.01 || amount > 99999999999e-2) problems.push({ field: "amount", code: "out_of_range" });
181
+ }
182
+ const purpose = String(input.purpose ?? "");
183
+ if (purpose.length > 4) problems.push({ field: "purpose", code: "too_long", max: 4, actual: purpose.length });
184
+ const reference = String(input.reference ?? "").trim();
185
+ const text = String(input.text ?? "").trim();
186
+ if (reference && text) problems.push({ field: "reference", code: "reference_or_text" });
187
+ if (reference.length > 35)
188
+ problems.push({ field: "reference", code: "too_long", max: 35, actual: reference.length });
189
+ if (text.length > 140) problems.push({ field: "text", code: "too_long", max: 140, actual: text.length });
190
+ const bytes = byteLength(epcPayload(input));
191
+ if (bytes > EPC_MAX_BYTES)
192
+ problems.push({ field: "payload", code: "too_long", max: EPC_MAX_BYTES, actual: bytes });
193
+ return problems;
194
+ }
195
+
196
+ // packages/engine/src/lib/codes/qr.ts
80
197
  import { create as createQr } from "qrcode";
81
198
  import { render as renderQrSvg } from "qrcode/lib/renderer/svg-tag.js";
199
+ function qrSvg(value, options = {}) {
200
+ const qr = createQr(value, { errorCorrectionLevel: options.ecc ?? "M" });
201
+ return renderQrSvg(qr, {
202
+ width: options.size ?? 160,
203
+ margin: options.margin ?? 1,
204
+ color: {
205
+ dark: (options.color ?? "#000000").replace(/^(#?)/, "#").slice(0, 9),
206
+ light: (options.background ?? "#ffffff").replace(/^(#?)/, "#")
207
+ }
208
+ });
209
+ }
210
+ var svgDataUri = (svg) => `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
211
+
212
+ // packages/engine/src/lib/helpers/codes.ts
213
+ var barcodeTypes = {
214
+ code128: "code128",
215
+ ean13: "ean13",
216
+ ean8: "ean8",
217
+ upc: "upca",
218
+ upca: "upca",
219
+ itf14: "itf14",
220
+ code39: "code39",
221
+ qrcode: "qrcode",
222
+ datamatrix: "datamatrix",
223
+ pdf417: "pdf417"
224
+ };
225
+ var codeHelpers = [
226
+ {
227
+ name: "qrcode",
228
+ aliases: ["qr"],
229
+ doc: {
230
+ signature: "qrcode(value, { size = 160, ecc = 'M', margin = 1, color, background })",
231
+ description: "QR code as an SVG data URI for an <img> src.",
232
+ examples: {
233
+ jinja2: '<img src="{{ qrcode(order.url, { size: 120 }) }}">',
234
+ liquid: '<img src="{{ order.url | qrcode: size: 120 }}">',
235
+ handlebars: '<img src="{{qrcode order.url size=120}}">'
236
+ },
237
+ category: "code"
238
+ },
239
+ fn: (ctx, value, options) => {
240
+ const o = options ?? {};
241
+ const svg = qrSvg(String(value ?? ""), o);
242
+ if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, width: o.size, height: o.size, alt: "QR code" });
243
+ return svgDataUri(svg);
244
+ }
245
+ },
246
+ {
247
+ name: "barcode",
248
+ doc: {
249
+ signature: "barcode(value, { type = 'code128', height = 12, width, text = true, scale = 2 })",
250
+ description: "Barcode (code128, ean13, ean8, upc, itf14, code39, datamatrix, pdf417) as an SVG data URI.",
251
+ examples: {
252
+ jinja2: `<img src="{{ barcode(item.sku, { type: 'code128' }) }}">`,
253
+ liquid: `<img src="{{ item.sku | barcode: type: 'code128' }}">`,
254
+ handlebars: `<img src="{{barcode item.sku type='code128'}}">`
255
+ },
256
+ category: "code"
257
+ },
258
+ fn: (ctx, value, options) => {
259
+ const o = options ?? {};
260
+ const bcid = barcodeTypes[String(o.type ?? "code128").toLowerCase()] ?? "code128";
261
+ const svg = toSVG({
262
+ bcid,
263
+ text: String(value ?? ""),
264
+ height: o.height ?? 12,
265
+ ...o.width === void 0 ? {} : { width: o.width },
266
+ scale: o.scale ?? 2,
267
+ includetext: o.text !== false,
268
+ textxalign: "center"
269
+ });
270
+ if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, alt: `Barcode ${String(value ?? "")}` });
271
+ return svgDataUri(svg);
272
+ }
273
+ },
274
+ {
275
+ name: "epcQr",
276
+ aliases: ["epc_qr", "girocode"],
277
+ doc: {
278
+ signature: "epcQr({ name, iban, bic?, amount?, reference?, text? }, { size })",
279
+ description: "SEPA credit transfer QR code (EPC069-12 / GiroCode) that banking apps scan to prefill a payment.",
280
+ examples: {
281
+ jinja2: '<img src="{{ epcQr({ name: company.name, iban: company.iban, amount: invoice.total, reference: invoice.number }) }}">',
282
+ liquid: '<img src="{{ company.iban | epcQr: name: company.name, amount: invoice.total, reference: invoice.number }}">',
283
+ handlebars: '<img src="{{epcQr name=company.name iban=company.iban amount=invoice.total reference=invoice.number}}">'
284
+ },
285
+ category: "code"
286
+ },
287
+ // Payment fields and QR options may arrive together: Jinja2 passes two objects, Handlebars one
288
+ // hash (`{{epcQr name=… iban=… size=200}}`), and Liquid, which cannot build an object literal,
289
+ // keyword arguments (`{{ company.iban | epcQr: name: company.name, amount: invoice.total }}`,
290
+ // where a string input is the IBAN). They are merged and split by field name.
291
+ fn: (ctx, input, options) => {
292
+ const fields = {
293
+ ...input && typeof input === "object" ? input : typeof input === "string" && input ? { iban: input } : {},
294
+ ...options ?? {}
295
+ };
296
+ const payment = {};
297
+ const qr = {};
298
+ for (const [key, value] of Object.entries(fields))
299
+ (EPC_FIELDS.has(key) ? payment : qr)[key] = value;
300
+ const o = { ecc: "M", ...qr };
301
+ const svg = qrSvg(epcPayload(payment), o);
302
+ if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, width: o.size, height: o.size, alt: "GiroCode" });
303
+ return svgDataUri(svg);
304
+ }
305
+ }
306
+ ];
307
+ var EPC_FIELDS = /* @__PURE__ */ new Set(["name", "iban", "bic", "amount", "reference", "text", "purpose"]);
82
308
 
83
309
  // packages/engine/src/lib/helpers/format.ts
84
310
  import { TZDate } from "@date-fns/tz";
@@ -112,15 +338,6 @@ var dateLocales = {
112
338
  function dateLocale(locale) {
113
339
  return dateLocales[locale] ?? dateLocales[locale.split("-")[0] ?? ""] ?? enGB;
114
340
  }
115
- function toNumber(value) {
116
- if (typeof value === "number") return value;
117
- if (typeof value === "string") {
118
- const n = Number(value.replace(/\s/g, "").replace(",", "."));
119
- return Number.isFinite(n) ? n : NaN;
120
- }
121
- if (typeof value === "bigint") return Number(value);
122
- return NaN;
123
- }
124
341
  function toDate(value, tz) {
125
342
  if (value === "now" || value === void 0 || value === null || value === "")
126
343
  return new TZDate(Date.now(), tz);
@@ -143,6 +360,10 @@ function escapeHtml(value) {
143
360
  var isNamedArgs = (v) => !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
144
361
  var str = (v) => v === void 0 || v === null ? "" : String(v);
145
362
  var opt = (v, fallback) => v === void 0 || v === null || v === "" ? fallback : v;
363
+ function requireFinite(helper, value) {
364
+ if (typeof value === "number" && !Number.isFinite(value))
365
+ throw new Error(`${helper}: got ${value}, the result of a calculation with a value that is missing or not a number`);
366
+ }
146
367
  var wordsByLang = {
147
368
  de: deWords,
148
369
  en: enWords,
@@ -158,11 +379,16 @@ var formatHelpers = [
158
379
  deprecatedAlias: { currency_format: "money" },
159
380
  doc: {
160
381
  signature: "money(value, currency?, locale?)",
161
- description: "Formats a number as currency with Intl.NumberFormat. Defaults come from the template settings.",
162
- example: "{{ line.total | money('EUR') }} \u2192 1.234,50 \u20AC",
382
+ description: "Formats a number as currency with Intl.NumberFormat. Defaults come from the template settings. Text that is not a number is printed as it is; a calculation that came out as NaN stops the render.",
383
+ examples: {
384
+ jinja2: "{{ line.total | money('EUR') }} \u2192 1.234,50 \u20AC",
385
+ liquid: "{{ line.total | money: 'EUR' }} \u2192 1.234,50 \u20AC",
386
+ handlebars: "{{money line.total 'EUR'}} \u2192 1.234,50 \u20AC"
387
+ },
163
388
  category: "format"
164
389
  },
165
390
  fn: (ctx, value, currency, locale) => {
391
+ requireFinite("money", value);
166
392
  const n = toNumber(value);
167
393
  if (Number.isNaN(n)) return str(value);
168
394
  return new Intl.NumberFormat(opt(locale, ctx.locale), {
@@ -176,10 +402,15 @@ var formatHelpers = [
176
402
  doc: {
177
403
  signature: "number(value, decimals?, locale?)",
178
404
  description: "Formats a number with grouping. With decimals, exactly that many (rounded half away from zero); without, the decimals the number has, at most three, so 1234.5 \u2192 1.234,5 and 2.34567 \u2192 2,346. Use number(0) for whole numbers.",
179
- example: "{{ qty | number(2) }} \u2192 1.234,00",
405
+ examples: {
406
+ jinja2: "{{ qty | number(2) }} \u2192 1.234,00",
407
+ liquid: "{{ qty | number: 2 }} \u2192 1.234,00",
408
+ handlebars: "{{number qty 2}} \u2192 1.234,00"
409
+ },
180
410
  category: "format"
181
411
  },
182
412
  fn: (ctx, value, decimals, locale) => {
413
+ requireFinite("number", value);
183
414
  const n = toNumber(value);
184
415
  if (Number.isNaN(n)) return str(value);
185
416
  const d = decimals === void 0 || decimals === null ? void 0 : Number(decimals);
@@ -194,7 +425,11 @@ var formatHelpers = [
194
425
  doc: {
195
426
  signature: "date(value, format?, tz?, locale?)",
196
427
  description: "Formats a date with date-fns tokens (e.g. 'dd.MM.yyyy', 'PPP'). Accepts ISO strings, epoch numbers and 'now'.",
197
- example: "{{ invoice.date | date('dd.MM.yyyy') }}",
428
+ examples: {
429
+ jinja2: "{{ invoice.date | date('dd.MM.yyyy') }}",
430
+ liquid: "{{ invoice.date | date: 'dd.MM.yyyy' }}",
431
+ handlebars: "{{date invoice.date 'dd.MM.yyyy'}}"
432
+ },
198
433
  category: "format"
199
434
  },
200
435
  fn: (ctx, value, format, tz, locale) => {
@@ -211,7 +446,11 @@ var formatHelpers = [
211
446
  doc: {
212
447
  signature: "dateAdd(value, amount, unit)",
213
448
  description: "Adds days, weeks, months or years to a date; returns an ISO string for further formatting.",
214
- example: "{{ invoice.date | dateAdd(14, 'days') | date('PP') }}",
449
+ examples: {
450
+ jinja2: "{{ invoice.date | dateAdd(14, 'days') | date('PP') }}",
451
+ liquid: "{{ invoice.date | dateAdd: 14, 'days' | date: 'PP' }}",
452
+ handlebars: "{{date (dateAdd invoice.date 14 'days') 'PP'}}"
453
+ },
215
454
  category: "format"
216
455
  },
217
456
  fn: (ctx, value, amount, unit) => {
@@ -228,7 +467,11 @@ var formatHelpers = [
228
467
  doc: {
229
468
  signature: "add(a, b)",
230
469
  description: "a + b (Handlebars has no arithmetic of its own).",
231
- example: "{{add @index 1}}",
470
+ examples: {
471
+ jinja2: "{{ line.qty | add(1) }}",
472
+ liquid: "{{ line.qty | add: 1 }}",
473
+ handlebars: "{{#each invoice.lines}}{{add @index 1}}{{/each}}"
474
+ },
232
475
  category: "format"
233
476
  },
234
477
  fn: (_ctx, a, b) => toNumber(a) + toNumber(b)
@@ -239,7 +482,11 @@ var formatHelpers = [
239
482
  doc: {
240
483
  signature: "subtract(a, b)",
241
484
  description: "a minus b.",
242
- example: "{{subtract total discount}}",
485
+ examples: {
486
+ jinja2: "{{ total | subtract(discount) }}",
487
+ liquid: "{{ total | subtract: discount }}",
488
+ handlebars: "{{subtract total discount}}"
489
+ },
243
490
  category: "format"
244
491
  },
245
492
  fn: (_ctx, a, b) => toNumber(a) - toNumber(b)
@@ -250,7 +497,11 @@ var formatHelpers = [
250
497
  doc: {
251
498
  signature: "multiply(a, b)",
252
499
  description: "a times b.",
253
- example: "{{money (multiply line.qty line.price)}}",
500
+ examples: {
501
+ jinja2: "{{ line.qty | multiply(line.price) | money }}",
502
+ liquid: "{{ line.qty | multiply: line.price | money }}",
503
+ handlebars: "{{money (multiply line.qty line.price)}}"
504
+ },
254
505
  category: "format"
255
506
  },
256
507
  fn: (_ctx, a, b) => toNumber(a) * toNumber(b)
@@ -261,7 +512,11 @@ var formatHelpers = [
261
512
  doc: {
262
513
  signature: "divide(a, b)",
263
514
  description: "a divided by b; empty when b is 0.",
264
- example: "{{divide total count}}",
515
+ examples: {
516
+ jinja2: "{{ total | divide(count) }}",
517
+ liquid: "{{ total | divide: count }}",
518
+ handlebars: "{{divide total count}}"
519
+ },
265
520
  category: "format"
266
521
  },
267
522
  fn: (_ctx, a, b) => toNumber(b) === 0 ? "" : toNumber(a) / toNumber(b)
@@ -271,7 +526,11 @@ var formatHelpers = [
271
526
  doc: {
272
527
  signature: "round(value, decimals = 0, method = 'common')",
273
528
  description: "Rounds to the given decimals, as Jinja2's round does: 'common' rounds half away from zero, 'floor' always down and 'ceil' always up.",
274
- example: "{{ 2.345 | round(2) }} gives 2.35, {{ 2.349 | round(2, 'floor') }} gives 2.34",
529
+ examples: {
530
+ jinja2: "{{ 2.345 | round(2) }} gives 2.35, {{ 2.349 | round(2, 'floor') }} gives 2.34",
531
+ liquid: "{{ 2.345 | round: 2 }} gives 2.35, {{ 2.349 | round: 2, 'floor' }} gives 2.34",
532
+ handlebars: "{{round 2.345 2}} gives 2.35, {{round 2.349 2 'floor'}} gives 2.34"
533
+ },
275
534
  category: "format"
276
535
  },
277
536
  // `round(2, method='floor')` (Jinja2) and `round: 2, method: 'floor'` (Liquid) arrive as a
@@ -297,7 +556,11 @@ var formatHelpers = [
297
556
  doc: {
298
557
  signature: "upper(value)",
299
558
  description: "Upper-cases a string.",
300
- example: "{{ name | upper }}",
559
+ examples: {
560
+ jinja2: "{{ name | upper }}",
561
+ liquid: "{{ name | upper }}",
562
+ handlebars: "{{upper name}}"
563
+ },
301
564
  category: "text"
302
565
  },
303
566
  fn: (_ctx, value) => str(value).toUpperCase()
@@ -308,7 +571,11 @@ var formatHelpers = [
308
571
  doc: {
309
572
  signature: "lower(value)",
310
573
  description: "Lower-cases a string.",
311
- example: "{{ name | lower }}",
574
+ examples: {
575
+ jinja2: "{{ name | lower }}",
576
+ liquid: "{{ name | lower }}",
577
+ handlebars: "{{lower name}}"
578
+ },
312
579
  category: "text"
313
580
  },
314
581
  fn: (_ctx, value) => str(value).toLowerCase()
@@ -319,7 +586,11 @@ var formatHelpers = [
319
586
  doc: {
320
587
  signature: "title(value)",
321
588
  description: "Capitalises every word.",
322
- example: "{{ 'hello world' | title }} \u2192 Hello World",
589
+ examples: {
590
+ jinja2: "{{ 'hello world' | title }} \u2192 Hello World",
591
+ liquid: "{{ 'hello world' | title }} \u2192 Hello World",
592
+ handlebars: "{{title 'hello world'}} \u2192 Hello World"
593
+ },
323
594
  category: "text"
324
595
  },
325
596
  fn: (_ctx, value) => str(value).replace(
@@ -332,7 +603,11 @@ var formatHelpers = [
332
603
  doc: {
333
604
  signature: "truncate(value, length = 255, suffix = '\u2026')",
334
605
  description: "Cuts a string to `length` characters and appends the suffix when cut.",
335
- example: "{{ description | truncate(40) }}",
606
+ examples: {
607
+ jinja2: "{{ description | truncate(40) }}",
608
+ liquid: "{{ description | truncate: 40 }}",
609
+ handlebars: "{{truncate description 40}}"
610
+ },
336
611
  category: "text"
337
612
  },
338
613
  fn: (_ctx, value, length, suffix) => {
@@ -347,7 +622,11 @@ var formatHelpers = [
347
622
  doc: {
348
623
  signature: "replace(value, search, replacement)",
349
624
  description: "Replaces every occurrence of `search`.",
350
- example: "{{ iban | replace(' ', '') }}",
625
+ examples: {
626
+ jinja2: "{{ iban | replace(' ', '') }}",
627
+ liquid: "{{ iban | replace: ' ', '' }}",
628
+ handlebars: "{{replace iban ' ' ''}}"
629
+ },
351
630
  category: "text"
352
631
  },
353
632
  fn: (_ctx, value, search, replacement) => str(value).split(str(search)).join(str(replacement))
@@ -357,7 +636,11 @@ var formatHelpers = [
357
636
  doc: {
358
637
  signature: "split(value, separator = ',')",
359
638
  description: "Splits a string into a list.",
360
- example: "{% for tag in tags | split(',') %}",
639
+ examples: {
640
+ jinja2: "{% for tag in keywords | split(',') %}{{ tag }}{% endfor %}",
641
+ liquid: "{% assign tags = keywords | split: ',' %}{% for tag in tags %}{{ tag }}{% endfor %}",
642
+ handlebars: "{{#each (split keywords ',')}}{{this}}{{/each}}"
643
+ },
361
644
  category: "text"
362
645
  },
363
646
  fn: (_ctx, value, separator) => str(value).split(str(opt(separator, ",")))
@@ -367,7 +650,11 @@ var formatHelpers = [
367
650
  doc: {
368
651
  signature: "join(list, separator = ', ')",
369
652
  description: "Joins a list into a string.",
370
- example: "{{ tags | join(' \xB7 ') }}",
653
+ examples: {
654
+ jinja2: "{{ tags | join(' \xB7 ') }}",
655
+ liquid: "{{ tags | join: ' \xB7 ' }}",
656
+ handlebars: "{{join tags ' \xB7 '}}"
657
+ },
371
658
  category: "collection"
372
659
  },
373
660
  fn: (_ctx, value, separator) => Array.isArray(value) ? value.map(str).join(str(opt(separator, ", "))) : str(value)
@@ -378,7 +665,11 @@ var formatHelpers = [
378
665
  doc: {
379
666
  signature: "default(value, fallback)",
380
667
  description: "Returns the fallback when the value is empty, null or undefined.",
381
- example: "{{ customer.vat | default('\u2014') }}",
668
+ examples: {
669
+ jinja2: "{{ customer.vat | default('\u2014') }}",
670
+ liquid: "{{ customer.vat | default: '\u2014' }}",
671
+ handlebars: "{{default customer.vat '\u2014'}}"
672
+ },
382
673
  category: "text"
383
674
  },
384
675
  fn: (_ctx, value, fallback) => value === void 0 || value === null || value === "" ? fallback : value
@@ -389,7 +680,11 @@ var formatHelpers = [
389
680
  doc: {
390
681
  signature: "nl2br(value)",
391
682
  description: "Escapes the text and turns line breaks into <br>.",
392
- example: "{{ address | nl2br }}",
683
+ examples: {
684
+ jinja2: "{{ address | nl2br }}",
685
+ liquid: "{{ address | nl2br }}",
686
+ handlebars: "{{nl2br address}}"
687
+ },
393
688
  category: "text"
394
689
  },
395
690
  // office templates turn line breaks into breaks themselves, and escape the text once
@@ -401,7 +696,11 @@ var formatHelpers = [
401
696
  doc: {
402
697
  signature: "markdown(value)",
403
698
  description: "Renders Markdown to HTML (no sanitiser: template data is trusted by the author).",
404
- example: "{{ notes | markdown }}",
699
+ examples: {
700
+ jinja2: "{{ notes | markdown }}",
701
+ liquid: "{{ notes | markdown }}",
702
+ handlebars: "{{markdown notes}}"
703
+ },
405
704
  category: "text"
406
705
  },
407
706
  fn: (ctx, value) => {
@@ -420,7 +719,11 @@ var formatHelpers = [
420
719
  doc: {
421
720
  signature: "safe(value)",
422
721
  description: "Marks a string as HTML so the engine does not escape it.",
423
- example: "{{ html_snippet | safe }}",
722
+ examples: {
723
+ jinja2: "{{ html_snippet | safe }}",
724
+ liquid: "{{ html_snippet | safe }}",
725
+ handlebars: "{{safe html_snippet}}"
726
+ },
424
727
  category: "text"
425
728
  },
426
729
  fn: (_ctx, value) => str(value)
@@ -431,7 +734,11 @@ var formatHelpers = [
431
734
  doc: {
432
735
  signature: "json(value, indent = 2)",
433
736
  description: "Serialises a value as JSON; handy while building a template.",
434
- example: "<pre>{{ invoice | json }}</pre>",
737
+ examples: {
738
+ jinja2: "<pre>{{ invoice | json }}</pre>",
739
+ liquid: "<pre>{{ invoice | json }}</pre>",
740
+ handlebars: "<pre>{{json invoice}}</pre>"
741
+ },
435
742
  category: "debug"
436
743
  },
437
744
  fn: (_ctx, value, indent) => JSON.stringify(value, null, opt(Number(indent), 2))
@@ -442,10 +749,15 @@ var formatHelpers = [
442
749
  doc: {
443
750
  signature: "numToWords(value, locale?)",
444
751
  description: "Spells a number out in words (de, en, fr, es, it, nl).",
445
- example: "{{ total | numToWords('de') }} \u2192 zweihundertdreiundvierzig",
752
+ examples: {
753
+ jinja2: "{{ total | numToWords('de') }} \u2192 zweihundertdreiundvierzig",
754
+ liquid: "{{ total | numToWords: 'de' }} \u2192 zweihundertdreiundvierzig",
755
+ handlebars: "{{numToWords total 'de'}} \u2192 zweihundertdreiundvierzig"
756
+ },
446
757
  category: "format"
447
758
  },
448
759
  fn: (ctx, value, locale) => {
760
+ requireFinite("numToWords", value);
449
761
  const n = toNumber(value);
450
762
  if (Number.isNaN(n)) return str(value);
451
763
  const lang = str(opt(locale, ctx.locale)).split("-")[0] ?? "en";
@@ -458,7 +770,11 @@ var formatHelpers = [
458
770
  doc: {
459
771
  signature: "t(key, params?)",
460
772
  description: "Looks a key up in the template's i18n dictionary for the request locale; `{name}` placeholders come from params.",
461
- example: "{{ t('invoice.title', { number: invoice.number }) }}",
773
+ examples: {
774
+ jinja2: "{{ t('invoice.title', { number: invoice.number }) }}",
775
+ liquid: "{{ 'invoice.title' | t: number: invoice.number }}",
776
+ handlebars: "{{t 'invoice.title' number=invoice.number}}"
777
+ },
462
778
  category: "text"
463
779
  },
464
780
  fn: (ctx, key, params) => {
@@ -474,7 +790,11 @@ var formatHelpers = [
474
790
  doc: {
475
791
  signature: "asset(name)",
476
792
  description: "URL of a file in the workspace library (logo, background) on the CDN. Upload it on the Files page or with `POST /files` and use the name it has there; relative image paths resolve against the same library.",
477
- example: `<img src="{{ asset('logo.png') }}">`,
793
+ examples: {
794
+ jinja2: `<img src="{{ asset('logo.png') }}">`,
795
+ liquid: `<img src="{{ 'logo.png' | asset }}">`,
796
+ handlebars: `<img src="{{asset 'logo.png'}}">`
797
+ },
478
798
  category: "document"
479
799
  },
480
800
  fn: (ctx, name) => `${(ctx.assetBaseUrl ?? "").replace(/\/$/, "")}/${str(name).replace(/^\//, "")}`
@@ -486,128 +806,17 @@ var formatHelpers = [
486
806
  doc: {
487
807
  signature: "pageBreak()",
488
808
  description: "Starts a new page in PDF output.",
489
- example: "{{ pageBreak() }}",
809
+ examples: {
810
+ jinja2: "{{ pageBreak() }}",
811
+ liquid: "{{ '' | pageBreak }}",
812
+ handlebars: "{{pageBreak}}"
813
+ },
490
814
  category: "document"
491
815
  },
492
816
  fn: (ctx) => ctx.drawing ? ctx.drawing({ kind: "page-break" }) : '<div class="page-break" style="break-after:page"></div>'
493
817
  }
494
818
  ];
495
819
 
496
- // packages/engine/src/lib/helpers/codes.ts
497
- var svgDataUri = (svg) => `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
498
- var barcodeTypes = {
499
- code128: "code128",
500
- ean13: "ean13",
501
- ean8: "ean8",
502
- upc: "upca",
503
- upca: "upca",
504
- itf14: "itf14",
505
- code39: "code39",
506
- qrcode: "qrcode",
507
- datamatrix: "datamatrix",
508
- pdf417: "pdf417"
509
- };
510
- function qrSvg(value, options = {}) {
511
- const qr = createQr(value, { errorCorrectionLevel: options.ecc ?? "M" });
512
- const svg = renderQrSvg(qr, {
513
- width: options.size ?? 160,
514
- margin: options.margin ?? 1,
515
- color: {
516
- dark: (options.color ?? "#000000").replace(/^(#?)/, "#").slice(0, 9),
517
- light: (options.background ?? "#ffffff").replace(/^(#?)/, "#")
518
- }
519
- });
520
- return svg;
521
- }
522
- function epcPayload(input) {
523
- const amount = input.amount === void 0 || input.amount === "" ? "" : `EUR${toNumber(input.amount).toFixed(2)}`;
524
- return [
525
- "BCD",
526
- "002",
527
- "1",
528
- "SCT",
529
- (input.bic ?? "").replace(/\s/g, ""),
530
- String(input.name ?? "").slice(0, 70),
531
- String(input.iban ?? "").replace(/\s/g, ""),
532
- amount,
533
- (input.purpose ?? "").slice(0, 4),
534
- (input.reference ?? "").slice(0, 35),
535
- input.reference ? "" : (input.text ?? "").slice(0, 140),
536
- ""
537
- ].join("\n");
538
- }
539
- var codeHelpers = [
540
- {
541
- name: "qrcode",
542
- aliases: ["qr"],
543
- doc: {
544
- signature: "qrcode(value, { size = 160, ecc = 'M', margin = 1, color, background })",
545
- description: "QR code as an SVG data URI for an <img> src.",
546
- example: '<img src="{{ qrcode(order.url, { size: 120 }) }}">',
547
- category: "code"
548
- },
549
- fn: (ctx, value, options) => {
550
- const o = options ?? {};
551
- const svg = qrSvg(String(value ?? ""), o);
552
- if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, width: o.size, height: o.size, alt: "QR code" });
553
- return svgDataUri(svg);
554
- }
555
- },
556
- {
557
- name: "barcode",
558
- doc: {
559
- signature: "barcode(value, { type = 'code128', height = 12, width, text = true, scale = 2 })",
560
- description: "Barcode (code128, ean13, ean8, upc, itf14, code39, datamatrix, pdf417) as an SVG data URI.",
561
- example: `<img src="{{ barcode(item.sku, { type: 'code128' }) }}">`,
562
- category: "code"
563
- },
564
- fn: (ctx, value, options) => {
565
- const o = options ?? {};
566
- const bcid = barcodeTypes[String(o.type ?? "code128").toLowerCase()] ?? "code128";
567
- const svg = toSVG({
568
- bcid,
569
- text: String(value ?? ""),
570
- height: o.height ?? 12,
571
- ...o.width === void 0 ? {} : { width: o.width },
572
- scale: o.scale ?? 2,
573
- includetext: o.text !== false,
574
- textxalign: "center"
575
- });
576
- if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, alt: `Barcode ${String(value ?? "")}` });
577
- return svgDataUri(svg);
578
- }
579
- },
580
- {
581
- name: "epcQr",
582
- aliases: ["epc_qr", "girocode"],
583
- doc: {
584
- signature: "epcQr({ name, iban, bic?, amount?, reference?, text? }, { size })",
585
- description: "SEPA credit transfer QR code (EPC069-12 / GiroCode) that banking apps scan to prefill a payment.",
586
- example: '<img src="{{ epcQr({ name: company.name, iban: company.iban, amount: invoice.total, reference: invoice.number }) }}">',
587
- category: "code"
588
- },
589
- // Payment fields and QR options may arrive together: Jinja2 passes two objects, Handlebars one
590
- // hash (`{{epcQr name=… iban=… size=200}}`), and Liquid, which cannot build an object literal,
591
- // keyword arguments (`{{ company.iban | epcQr: name: company.name, amount: invoice.total }}`,
592
- // where a string input is the IBAN). They are merged and split by field name.
593
- fn: (ctx, input, options) => {
594
- const fields = {
595
- ...input && typeof input === "object" ? input : typeof input === "string" && input ? { iban: input } : {},
596
- ...options ?? {}
597
- };
598
- const payment = {};
599
- const qr = {};
600
- for (const [key, value] of Object.entries(fields))
601
- (EPC_FIELDS.has(key) ? payment : qr)[key] = value;
602
- const o = { ecc: "M", ...qr };
603
- const svg = qrSvg(epcPayload(payment), o);
604
- if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, width: o.size, height: o.size, alt: "GiroCode" });
605
- return svgDataUri(svg);
606
- }
607
- }
608
- ];
609
- var EPC_FIELDS = /* @__PURE__ */ new Set(["name", "iban", "bic", "amount", "reference", "text", "purpose"]);
610
-
611
820
  // packages/engine/src/lib/helpers/collections.ts
612
821
  var list = (v) => Array.isArray(v) ? v : v === void 0 || v === null ? [] : [v];
613
822
  var get = (item, path) => String(path ?? "").split(".").filter(Boolean).reduce(
@@ -621,7 +830,11 @@ var collectionHelpers = [
621
830
  doc: {
622
831
  signature: "sum(list, path?)",
623
832
  description: "Sum of the items, or of `path` inside each item.",
624
- example: "{{ invoice.lines | sum('total') | money }}",
833
+ examples: {
834
+ jinja2: "{{ invoice.lines | sum('total') | money }}",
835
+ liquid: "{{ invoice.lines | sum: 'total' | money }}",
836
+ handlebars: "{{money (sum invoice.lines 'total')}}"
837
+ },
625
838
  category: "collection"
626
839
  },
627
840
  fn: (_ctx, value, path) => numbers(value, path).reduce((a, b) => a + b, 0)
@@ -631,7 +844,11 @@ var collectionHelpers = [
631
844
  doc: {
632
845
  signature: "avg(list, path?)",
633
846
  description: "Average of the items or of `path` inside each item.",
634
- example: "{{ scores | avg | number(1) }}",
847
+ examples: {
848
+ jinja2: "{{ scores | avg | number(1) }}",
849
+ liquid: "{{ scores | avg | number: 1 }}",
850
+ handlebars: "{{number (avg scores) 1}}"
851
+ },
635
852
  category: "collection"
636
853
  },
637
854
  fn: (_ctx, value, path) => {
@@ -644,7 +861,11 @@ var collectionHelpers = [
644
861
  doc: {
645
862
  signature: "min(list, path?)",
646
863
  description: "Smallest number.",
647
- example: "{{ prices | min }}",
864
+ examples: {
865
+ jinja2: "{{ prices | min }}",
866
+ liquid: "{{ prices | min }}",
867
+ handlebars: "{{min prices}}"
868
+ },
648
869
  category: "collection"
649
870
  },
650
871
  fn: (_ctx, value, path) => Math.min(...numbers(value, path))
@@ -654,7 +875,11 @@ var collectionHelpers = [
654
875
  doc: {
655
876
  signature: "max(list, path?)",
656
877
  description: "Largest number.",
657
- example: "{{ prices | max }}",
878
+ examples: {
879
+ jinja2: "{{ prices | max }}",
880
+ liquid: "{{ prices | max }}",
881
+ handlebars: "{{max prices}}"
882
+ },
658
883
  category: "collection"
659
884
  },
660
885
  fn: (_ctx, value, path) => Math.max(...numbers(value, path))
@@ -665,7 +890,11 @@ var collectionHelpers = [
665
890
  doc: {
666
891
  signature: "groupBy(list, path)",
667
892
  description: "Groups items by a field; returns a list of `{ key, items }`.",
668
- example: "{% for group in lines | groupBy('category') %}{{ group.key }}{% endfor %}",
893
+ examples: {
894
+ jinja2: "{% for group in lines | groupBy('category') %}{{ group.key }}{% endfor %}",
895
+ liquid: "{% assign groups = lines | groupBy: 'category' %}{% for group in groups %}{{ group.key }}{% endfor %}",
896
+ handlebars: "{{#each (groupBy lines 'category')}}{{key}}{{/each}}"
897
+ },
669
898
  category: "collection"
670
899
  },
671
900
  fn: (_ctx, value, path) => {
@@ -683,7 +912,11 @@ var collectionHelpers = [
683
912
  doc: {
684
913
  signature: "sortBy(list, path?, direction = 'asc')",
685
914
  description: "Sorts items by a field (numbers numerically, strings by locale).",
686
- example: "{% for l in lines | sortBy('position') %}",
915
+ examples: {
916
+ jinja2: "{% for l in lines | sortBy('position') %}{{ l.sku }}{% endfor %}",
917
+ liquid: "{% assign sorted = lines | sortBy: 'position' %}{% for l in sorted %}{{ l.sku }}{% endfor %}",
918
+ handlebars: "{{#each (sortBy lines 'position')}}{{sku}}{{/each}}"
919
+ },
687
920
  category: "collection"
688
921
  },
689
922
  fn: (ctx, value, path, direction) => {
@@ -704,7 +937,11 @@ var collectionHelpers = [
704
937
  doc: {
705
938
  signature: "where(list, path, value?)",
706
939
  description: "Keeps items whose field equals `value` (or is truthy when omitted).",
707
- example: "{% for l in lines | where('taxable', true) %}",
940
+ examples: {
941
+ jinja2: "{% for l in lines | where('taxable', true) %}{{ l.sku }}{% endfor %}",
942
+ liquid: "{% assign taxable = lines | where: 'taxable', true %}{% for l in taxable %}{{ l.sku }}{% endfor %}",
943
+ handlebars: "{{#each (where lines 'taxable' true)}}{{sku}}{{/each}}"
944
+ },
708
945
  category: "collection"
709
946
  },
710
947
  fn: (_ctx, value, path, expected) => list(value).filter((item) => {
@@ -718,7 +955,11 @@ var collectionHelpers = [
718
955
  doc: {
719
956
  signature: "pluck(list, path)",
720
957
  description: "Extracts one field from every item.",
721
- example: "{{ lines | pluck('sku') | join(', ') }}",
958
+ examples: {
959
+ jinja2: "{{ lines | pluck('sku') | join(', ') }}",
960
+ liquid: "{{ lines | pluck: 'sku' | join: ', ' }}",
961
+ handlebars: "{{join (pluck lines 'sku') ', '}}"
962
+ },
722
963
  category: "collection"
723
964
  },
724
965
  fn: (_ctx, value, path) => list(value).map((item) => get(item, path))
@@ -729,7 +970,11 @@ var collectionHelpers = [
729
970
  doc: {
730
971
  signature: "chunk(list, size)",
731
972
  description: "Splits a list into lists of `size` items (rows of labels, columns).",
732
- example: "{% for row in labels | chunk(3) %}",
973
+ examples: {
974
+ jinja2: "{% for row in labels | chunk(3) %}{{ row | join(', ') }}{% endfor %}",
975
+ liquid: "{% assign rows = labels | chunk: 3 %}{% for row in rows %}{{ row | join: ', ' }}{% endfor %}",
976
+ handlebars: "{{#each (chunk labels 3)}}{{join this ', '}}{{/each}}"
977
+ },
733
978
  category: "collection"
734
979
  },
735
980
  fn: (_ctx, value, size) => {
@@ -745,7 +990,11 @@ var collectionHelpers = [
745
990
  doc: {
746
991
  signature: "range(start, end?, step = 1)",
747
992
  description: "List of numbers from start to end (exclusive).",
748
- example: "{% for i in range(1, 4) %}",
993
+ examples: {
994
+ jinja2: "{% for i in range(1, 4) %}{{ i }}{% endfor %}",
995
+ liquid: "{% assign nums = 1 | range: 4 %}{% for i in nums %}{{ i }}{% endfor %}",
996
+ handlebars: "{{#each (range 1 4)}}{{this}}{{/each}}"
997
+ },
749
998
  category: "collection"
750
999
  },
751
1000
  fn: (_ctx, start, end, step) => {
@@ -763,7 +1012,11 @@ var collectionHelpers = [
763
1012
  doc: {
764
1013
  signature: "first(list)",
765
1014
  description: "First item.",
766
- example: "{{ lines | first }}",
1015
+ examples: {
1016
+ jinja2: "{{ tags | first }}",
1017
+ liquid: "{{ tags | first }}",
1018
+ handlebars: "{{first tags}}"
1019
+ },
767
1020
  category: "collection"
768
1021
  },
769
1022
  fn: (_ctx, value) => list(value)[0]
@@ -773,7 +1026,11 @@ var collectionHelpers = [
773
1026
  doc: {
774
1027
  signature: "last(list)",
775
1028
  description: "Last item.",
776
- example: "{{ lines | last }}",
1029
+ examples: {
1030
+ jinja2: "{{ tags | last }}",
1031
+ liquid: "{{ tags | last }}",
1032
+ handlebars: "{{last tags}}"
1033
+ },
777
1034
  category: "collection"
778
1035
  },
779
1036
  fn: (_ctx, value) => list(value).at(-1)
@@ -784,7 +1041,11 @@ var collectionHelpers = [
784
1041
  doc: {
785
1042
  signature: "length(value)",
786
1043
  description: "Number of items in a list, keys in an object or characters in a string.",
787
- example: "{{ lines | length }}",
1044
+ examples: {
1045
+ jinja2: "{{ lines | length }}",
1046
+ liquid: "{{ lines | length }}",
1047
+ handlebars: "{{length lines}}"
1048
+ },
788
1049
  category: "collection"
789
1050
  },
790
1051
  fn: (_ctx, value) => Array.isArray(value) || typeof value === "string" ? value.length : value && typeof value === "object" ? Object.keys(value).length : 0
@@ -860,7 +1121,11 @@ var documentHelpers = [
860
1121
  doc: {
861
1122
  signature: "chart({ type, labels, values, label, color } | { type, labels, series } | { type, data, options }, { width, height })",
862
1123
  description: "Draws a Chart.js chart (bar, line, pie, doughnut, radar, \u2026) from plain data, `labels` with `values` or several `series` of `{ label, values, color }`, or from a full Chart.js `data` and `options`; animations are disabled so PDF and preview match.",
863
- example: "{{ chart({ type: 'line', labels: ['Q1', 'Q2', 'Q3'], values: sales.quarters, width: 480 }) }}",
1124
+ examples: {
1125
+ jinja2: "{{ chart({ type: 'line', labels: sales.labels, values: sales.quarters, width: 480 }) }}",
1126
+ liquid: "{{ '' | chart: type: 'line', labels: sales.labels, values: sales.quarters, width: 480 }}",
1127
+ handlebars: "{{chart type='line' labels=sales.labels values=sales.quarters width=480}}"
1128
+ },
864
1129
  category: "document"
865
1130
  },
866
1131
  fn: (ctx, spec, extra) => {
@@ -874,7 +1139,11 @@ var documentHelpers = [
874
1139
  doc: {
875
1140
  signature: "image(url, { width, height, fit, alt })",
876
1141
  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.",
877
- example: "{{ image(product.photo, { width: 120, height: 80, fit: 'cover' }) }}",
1142
+ examples: {
1143
+ jinja2: "{{ image(product.photo, { width: 120, height: 80, fit: 'cover' }) }}",
1144
+ liquid: "{{ product.photo | image: width: 120, height: 80, fit: 'cover' }}",
1145
+ handlebars: "{{image product.photo width=120 height=80 fit='cover'}}"
1146
+ },
878
1147
  category: "document"
879
1148
  },
880
1149
  fn: (ctx, url, options) => {
@@ -900,7 +1169,12 @@ var comparison = (name, test, description) => ({
900
1169
  doc: {
901
1170
  signature: `${name}(a, b)`,
902
1171
  description,
903
- example: `{{#if (${name} invoice.total 1000)}}\u2026{{/if}}`,
1172
+ examples: {
1173
+ jinja2: `{% if ${name}(invoice.total, 1000) %}\u2026{% endif %}`,
1174
+ // Liquid takes no filter in a condition, so the comparison is assigned first
1175
+ liquid: `{% assign hit = invoice.total | ${name}: 1000 %}{% if hit %}\u2026{% endif %}`,
1176
+ handlebars: `{{#if (${name} invoice.total 1000)}}\u2026{{/if}}`
1177
+ },
904
1178
  category: "logic"
905
1179
  },
906
1180
  fn: (_ctx, a, b) => test(a, b)
@@ -917,7 +1191,12 @@ var logicHelpers = [
917
1191
  doc: {
918
1192
  signature: "and(a, b, \u2026)",
919
1193
  description: "True when every value is true; an empty list counts as false.",
920
- example: '{{#if (and customer.vatId (eq country "DE"))}}\u2026{{/if}}',
1194
+ examples: {
1195
+ jinja2: "{% if and(customer.vatId, eq(country, 'DE')) %}\u2026{% endif %}",
1196
+ // nesting one comparison in another needs its own assignment in Liquid
1197
+ liquid: "{% assign german = country | eq: 'DE' %}{% assign both = customer.vatId | and: german %}{% if both %}\u2026{% endif %}",
1198
+ handlebars: "{{#if (and customer.vatId (eq country 'DE'))}}\u2026{{/if}}"
1199
+ },
921
1200
  category: "logic"
922
1201
  },
923
1202
  fn: (_ctx, ...values) => values.length > 0 && values.every(truthy)
@@ -927,7 +1206,11 @@ var logicHelpers = [
927
1206
  doc: {
928
1207
  signature: "or(a, b, \u2026)",
929
1208
  description: "True when any value is true.",
930
- example: "{{#if (or invoice.paid invoice.credited)}}\u2026{{/if}}",
1209
+ examples: {
1210
+ jinja2: "{% if or(invoice.paid, invoice.credited) %}\u2026{% endif %}",
1211
+ liquid: "{% assign any = invoice.paid | or: invoice.credited %}{% if any %}\u2026{% endif %}",
1212
+ handlebars: "{{#if (or invoice.paid invoice.credited)}}\u2026{{/if}}"
1213
+ },
931
1214
  category: "logic"
932
1215
  },
933
1216
  fn: (_ctx, ...values) => values.some(truthy)
@@ -937,7 +1220,11 @@ var logicHelpers = [
937
1220
  doc: {
938
1221
  signature: "not(value)",
939
1222
  description: "The opposite truth value.",
940
- example: "{{#if (not invoice.paid)}}\u2026{{/if}}",
1223
+ examples: {
1224
+ jinja2: "{% if invoice.paid | not %}\u2026{% endif %}",
1225
+ liquid: "{% assign unpaid = invoice.paid | not %}{% if unpaid %}\u2026{% endif %}",
1226
+ handlebars: "{{#if (not invoice.paid)}}\u2026{{/if}}"
1227
+ },
941
1228
  category: "logic"
942
1229
  },
943
1230
  fn: (_ctx, value) => !truthy(value)
@@ -1010,7 +1297,9 @@ function helperDocs(registry = defaultHelpers()) {
1010
1297
  return [...registry.definitions.values()].map((d) => ({
1011
1298
  name: d.name,
1012
1299
  aliases: d.aliases ?? [],
1013
- ...d.doc
1300
+ ...d.doc,
1301
+ // derived rather than authored, so the old field cannot drift from `examples`
1302
+ example: d.doc.examples.jinja2
1014
1303
  }));
1015
1304
  }
1016
1305
 
@@ -3442,7 +3731,7 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
3442
3731
  invoice: { due_date: "2026-10-13" },
3443
3732
  payment: {
3444
3733
  name: "Fennlor Studio GmbH",
3445
- iban: "DE00000000000000000000",
3734
+ iban: "DE36000000000000000000",
3446
3735
  bic: "XXXXDEXXXXX",
3447
3736
  amount: 3760.4,
3448
3737
  reference: "2026-0042"
@@ -3629,7 +3918,7 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
3629
3918
  sampleData: {
3630
3919
  payment: {
3631
3920
  name: "Fennlor Studio GmbH",
3632
- iban: "DE00000000000000000000",
3921
+ iban: "DE36000000000000000000",
3633
3922
  bic: "XXXXDEXXXXX",
3634
3923
  amount: 3760.4,
3635
3924
  reference: "2026-0042"
@@ -8464,6 +8753,7 @@ function starterPresentation(engine) {
8464
8753
  export {
8465
8754
  DEFAULT_CHROME_PADDING,
8466
8755
  DefaultHelperRegistry,
8756
+ EPC_MAX_BYTES,
8467
8757
  EngineSyntaxError,
8468
8758
  OFFICE_FILLED_MAX_BYTES,
8469
8759
  OfficeTemplateError,
@@ -8500,6 +8790,7 @@ export {
8500
8790
  getEngine,
8501
8791
  handlebarsEngine,
8502
8792
  helperDocs,
8793
+ ibanChecksum,
8503
8794
  imageMarkup,
8504
8795
  imageOutput,
8505
8796
  importApitemplate,
@@ -8524,6 +8815,7 @@ export {
8524
8815
  mergeSettings,
8525
8816
  mergeSnippetCss,
8526
8817
  mergeSnippetI18n,
8818
+ normaliseIban,
8527
8819
  normalisePartialName,
8528
8820
  nunjucksBuiltinFilters,
8529
8821
  officeDrawingLimits,
@@ -8554,7 +8846,9 @@ export {
8554
8846
  starterDocument,
8555
8847
  starterPresentation,
8556
8848
  strftimeToDateFns,
8849
+ svgDataUri,
8557
8850
  toDate,
8558
8851
  toNumber,
8852
+ validateEpc,
8559
8853
  zoomGestureScript
8560
8854
  };