@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/README.md +1 -1
- package/index.cjs +466 -172
- package/index.js +466 -172
- package/package.json +1 -1
- package/types/index.d.cts +2 -1
- package/types/index.d.ts +2 -1
- package/types/lib/codes/epc.d.cts +56 -0
- package/types/lib/codes/epc.d.ts +56 -0
- package/types/lib/codes/index.d.cts +7 -0
- package/types/lib/codes/index.d.ts +7 -0
- package/types/lib/codes/qr.d.cts +11 -0
- package/types/lib/codes/qr.d.ts +11 -0
- package/types/lib/helpers/codes.d.cts +0 -19
- package/types/lib/helpers/codes.d.ts +0 -19
- package/types/lib/helpers/format.d.cts +2 -1
- package/types/lib/helpers/format.d.ts +2 -1
- package/types/lib/helpers/index.d.cts +4 -6
- package/types/lib/helpers/index.d.ts +4 -6
- package/types/lib/types.d.cts +14 -1
- package/types/lib/types.d.ts +14 -1
- package/types/lib/values.d.cts +8 -0
- package/types/lib/values.d.ts +8 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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
|
};
|