@formfeed/engine 0.3.5 → 0.3.7
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 +532 -175
- package/index.js +532 -175
- package/package.json +1 -1
- package/types/index.d.cts +3 -2
- package/types/index.d.ts +3 -2
- 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/preview.d.cts +13 -0
- package/types/lib/preview.d.ts +13 -0
- package/types/lib/types.d.cts +19 -1
- package/types/lib/types.d.ts +19 -1
- package/types/lib/values.d.cts +8 -0
- package/types/lib/values.d.ts +8 -0
package/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ var index_exports = {};
|
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
DEFAULT_CHROME_PADDING: () => DEFAULT_CHROME_PADDING,
|
|
34
34
|
DefaultHelperRegistry: () => DefaultHelperRegistry,
|
|
35
|
+
EPC_MAX_BYTES: () => EPC_MAX_BYTES,
|
|
35
36
|
EngineSyntaxError: () => EngineSyntaxError,
|
|
36
37
|
OFFICE_FILLED_MAX_BYTES: () => OFFICE_FILLED_MAX_BYTES,
|
|
37
38
|
OfficeTemplateError: () => OfficeTemplateError,
|
|
@@ -68,6 +69,7 @@ __export(index_exports, {
|
|
|
68
69
|
getEngine: () => getEngine,
|
|
69
70
|
handlebarsEngine: () => handlebarsEngine,
|
|
70
71
|
helperDocs: () => helperDocs,
|
|
72
|
+
ibanChecksum: () => ibanChecksum,
|
|
71
73
|
imageMarkup: () => imageMarkup,
|
|
72
74
|
imageOutput: () => imageOutput,
|
|
73
75
|
importApitemplate: () => importApitemplate,
|
|
@@ -92,6 +94,7 @@ __export(index_exports, {
|
|
|
92
94
|
mergeSettings: () => mergeSettings,
|
|
93
95
|
mergeSnippetCss: () => mergeSnippetCss,
|
|
94
96
|
mergeSnippetI18n: () => mergeSnippetI18n,
|
|
97
|
+
normaliseIban: () => normaliseIban,
|
|
95
98
|
normalisePartialName: () => normalisePartialName,
|
|
96
99
|
nunjucksBuiltinFilters: () => nunjucksBuiltinFilters,
|
|
97
100
|
officeDrawingLimits: () => officeDrawingLimits,
|
|
@@ -122,8 +125,10 @@ __export(index_exports, {
|
|
|
122
125
|
starterDocument: () => starterDocument,
|
|
123
126
|
starterPresentation: () => starterPresentation,
|
|
124
127
|
strftimeToDateFns: () => strftimeToDateFns,
|
|
128
|
+
svgDataUri: () => svgDataUri,
|
|
125
129
|
toDate: () => toDate,
|
|
126
130
|
toNumber: () => toNumber,
|
|
131
|
+
validateEpc: () => validateEpc,
|
|
127
132
|
zoomGestureScript: () => zoomGestureScript
|
|
128
133
|
});
|
|
129
134
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -207,8 +212,234 @@ function depthGuard(resolve2, includeDepth) {
|
|
|
207
212
|
|
|
208
213
|
// packages/engine/src/lib/helpers/codes.ts
|
|
209
214
|
var import_bwip_js = require("bwip-js");
|
|
215
|
+
|
|
216
|
+
// packages/engine/src/lib/values.ts
|
|
217
|
+
function toNumber(value) {
|
|
218
|
+
if (typeof value === "number") return value;
|
|
219
|
+
if (typeof value === "string") {
|
|
220
|
+
const n = Number(value.replace(/\s/g, "").replace(",", "."));
|
|
221
|
+
return Number.isFinite(n) ? n : NaN;
|
|
222
|
+
}
|
|
223
|
+
if (typeof value === "bigint") return Number(value);
|
|
224
|
+
return NaN;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// packages/engine/src/lib/codes/epc.ts
|
|
228
|
+
function epcPayload(input) {
|
|
229
|
+
const amount = input.amount === void 0 || input.amount === "" ? "" : `EUR${toNumber(input.amount).toFixed(2)}`;
|
|
230
|
+
return [
|
|
231
|
+
"BCD",
|
|
232
|
+
"002",
|
|
233
|
+
"1",
|
|
234
|
+
"SCT",
|
|
235
|
+
(input.bic ?? "").replace(/\s/g, ""),
|
|
236
|
+
String(input.name ?? "").slice(0, 70),
|
|
237
|
+
String(input.iban ?? "").replace(/\s/g, ""),
|
|
238
|
+
amount,
|
|
239
|
+
(input.purpose ?? "").slice(0, 4),
|
|
240
|
+
(input.reference ?? "").slice(0, 35),
|
|
241
|
+
input.reference ? "" : (input.text ?? "").slice(0, 140),
|
|
242
|
+
""
|
|
243
|
+
].join("\n");
|
|
244
|
+
}
|
|
245
|
+
var IBAN_LENGTHS = {
|
|
246
|
+
AD: 24,
|
|
247
|
+
AT: 20,
|
|
248
|
+
BE: 16,
|
|
249
|
+
BG: 22,
|
|
250
|
+
CH: 21,
|
|
251
|
+
CY: 28,
|
|
252
|
+
CZ: 24,
|
|
253
|
+
DE: 22,
|
|
254
|
+
DK: 18,
|
|
255
|
+
EE: 20,
|
|
256
|
+
ES: 24,
|
|
257
|
+
FI: 18,
|
|
258
|
+
FO: 18,
|
|
259
|
+
FR: 27,
|
|
260
|
+
GB: 22,
|
|
261
|
+
GI: 23,
|
|
262
|
+
GL: 18,
|
|
263
|
+
GR: 27,
|
|
264
|
+
HR: 21,
|
|
265
|
+
HU: 28,
|
|
266
|
+
IE: 22,
|
|
267
|
+
IS: 26,
|
|
268
|
+
IT: 27,
|
|
269
|
+
LI: 21,
|
|
270
|
+
LT: 20,
|
|
271
|
+
LU: 20,
|
|
272
|
+
LV: 21,
|
|
273
|
+
MC: 27,
|
|
274
|
+
MT: 31,
|
|
275
|
+
NL: 18,
|
|
276
|
+
NO: 15,
|
|
277
|
+
PL: 28,
|
|
278
|
+
PT: 25,
|
|
279
|
+
RO: 24,
|
|
280
|
+
SE: 24,
|
|
281
|
+
SI: 19,
|
|
282
|
+
SK: 24,
|
|
283
|
+
SM: 27,
|
|
284
|
+
VA: 22
|
|
285
|
+
};
|
|
286
|
+
var EPC_MAX_BYTES = 331;
|
|
287
|
+
var byteLength = (value) => new TextEncoder().encode(value).length;
|
|
288
|
+
var normaliseIban = (iban) => iban.replace(/\s/g, "").toUpperCase();
|
|
289
|
+
function ibanChecksum(iban) {
|
|
290
|
+
const rearranged = normaliseIban(iban).slice(4) + normaliseIban(iban).slice(0, 4);
|
|
291
|
+
const digits = [...rearranged].map((c) => c >= "A" && c <= "Z" ? String(c.charCodeAt(0) - 55) : c).join("");
|
|
292
|
+
let remainder = 0;
|
|
293
|
+
for (const digit of digits) remainder = (remainder * 10 + Number(digit)) % 97;
|
|
294
|
+
return remainder === 1;
|
|
295
|
+
}
|
|
296
|
+
function validateEpc(input) {
|
|
297
|
+
const problems = [];
|
|
298
|
+
const name = String(input.name ?? "").trim();
|
|
299
|
+
if (!name) problems.push({ field: "name", code: "required" });
|
|
300
|
+
else if (name.length > 70) problems.push({ field: "name", code: "too_long", max: 70, actual: name.length });
|
|
301
|
+
const iban = normaliseIban(String(input.iban ?? ""));
|
|
302
|
+
if (!iban) problems.push({ field: "iban", code: "required" });
|
|
303
|
+
else if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]{11,30}$/.test(iban)) problems.push({ field: "iban", code: "iban_characters" });
|
|
304
|
+
else {
|
|
305
|
+
const expected = IBAN_LENGTHS[iban.slice(0, 2)];
|
|
306
|
+
if (expected !== void 0 && iban.length !== expected)
|
|
307
|
+
problems.push({ field: "iban", code: "iban_country_length", max: expected, actual: iban.length });
|
|
308
|
+
else if (!ibanChecksum(iban)) problems.push({ field: "iban", code: "iban_checksum" });
|
|
309
|
+
}
|
|
310
|
+
const bic = String(input.bic ?? "").replace(/\s/g, "").toUpperCase();
|
|
311
|
+
if (bic && !/^[A-Z]{6}[A-Z0-9]{2}([A-Z0-9]{3})?$/.test(bic)) problems.push({ field: "bic", code: "bic_format" });
|
|
312
|
+
if (input.amount !== void 0 && input.amount !== "") {
|
|
313
|
+
const amount = toNumber(input.amount);
|
|
314
|
+
if (Number.isNaN(amount)) problems.push({ field: "amount", code: "not_a_number" });
|
|
315
|
+
else if (amount < 0.01 || amount > 99999999999e-2) problems.push({ field: "amount", code: "out_of_range" });
|
|
316
|
+
}
|
|
317
|
+
const purpose = String(input.purpose ?? "");
|
|
318
|
+
if (purpose.length > 4) problems.push({ field: "purpose", code: "too_long", max: 4, actual: purpose.length });
|
|
319
|
+
const reference = String(input.reference ?? "").trim();
|
|
320
|
+
const text = String(input.text ?? "").trim();
|
|
321
|
+
if (reference && text) problems.push({ field: "reference", code: "reference_or_text" });
|
|
322
|
+
if (reference.length > 35)
|
|
323
|
+
problems.push({ field: "reference", code: "too_long", max: 35, actual: reference.length });
|
|
324
|
+
if (text.length > 140) problems.push({ field: "text", code: "too_long", max: 140, actual: text.length });
|
|
325
|
+
const bytes = byteLength(epcPayload(input));
|
|
326
|
+
if (bytes > EPC_MAX_BYTES)
|
|
327
|
+
problems.push({ field: "payload", code: "too_long", max: EPC_MAX_BYTES, actual: bytes });
|
|
328
|
+
return problems;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// packages/engine/src/lib/codes/qr.ts
|
|
210
332
|
var import_qrcode = require("qrcode");
|
|
211
333
|
var import_svg_tag = require("qrcode/lib/renderer/svg-tag.js");
|
|
334
|
+
function qrSvg(value, options = {}) {
|
|
335
|
+
const qr = (0, import_qrcode.create)(value, { errorCorrectionLevel: options.ecc ?? "M" });
|
|
336
|
+
return (0, import_svg_tag.render)(qr, {
|
|
337
|
+
width: options.size ?? 160,
|
|
338
|
+
margin: options.margin ?? 1,
|
|
339
|
+
color: {
|
|
340
|
+
dark: (options.color ?? "#000000").replace(/^(#?)/, "#").slice(0, 9),
|
|
341
|
+
light: (options.background ?? "#ffffff").replace(/^(#?)/, "#")
|
|
342
|
+
}
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
var svgDataUri = (svg) => `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
|
346
|
+
|
|
347
|
+
// packages/engine/src/lib/helpers/codes.ts
|
|
348
|
+
var barcodeTypes = {
|
|
349
|
+
code128: "code128",
|
|
350
|
+
ean13: "ean13",
|
|
351
|
+
ean8: "ean8",
|
|
352
|
+
upc: "upca",
|
|
353
|
+
upca: "upca",
|
|
354
|
+
itf14: "itf14",
|
|
355
|
+
code39: "code39",
|
|
356
|
+
qrcode: "qrcode",
|
|
357
|
+
datamatrix: "datamatrix",
|
|
358
|
+
pdf417: "pdf417"
|
|
359
|
+
};
|
|
360
|
+
var codeHelpers = [
|
|
361
|
+
{
|
|
362
|
+
name: "qrcode",
|
|
363
|
+
aliases: ["qr"],
|
|
364
|
+
doc: {
|
|
365
|
+
signature: "qrcode(value, { size = 160, ecc = 'M', margin = 1, color, background })",
|
|
366
|
+
description: "QR code as an SVG data URI for an <img> src.",
|
|
367
|
+
examples: {
|
|
368
|
+
jinja2: '<img src="{{ qrcode(order.url, { size: 120 }) }}">',
|
|
369
|
+
liquid: '<img src="{{ order.url | qrcode: size: 120 }}">',
|
|
370
|
+
handlebars: '<img src="{{qrcode order.url size=120}}">'
|
|
371
|
+
},
|
|
372
|
+
category: "code"
|
|
373
|
+
},
|
|
374
|
+
fn: (ctx, value, options) => {
|
|
375
|
+
const o = options ?? {};
|
|
376
|
+
const svg = qrSvg(String(value ?? ""), o);
|
|
377
|
+
if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, width: o.size, height: o.size, alt: "QR code" });
|
|
378
|
+
return svgDataUri(svg);
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "barcode",
|
|
383
|
+
doc: {
|
|
384
|
+
signature: "barcode(value, { type = 'code128', height = 12, width, text = true, scale = 2 })",
|
|
385
|
+
description: "Barcode (code128, ean13, ean8, upc, itf14, code39, datamatrix, pdf417) as an SVG data URI.",
|
|
386
|
+
examples: {
|
|
387
|
+
jinja2: `<img src="{{ barcode(item.sku, { type: 'code128' }) }}">`,
|
|
388
|
+
liquid: `<img src="{{ item.sku | barcode: type: 'code128' }}">`,
|
|
389
|
+
handlebars: `<img src="{{barcode item.sku type='code128'}}">`
|
|
390
|
+
},
|
|
391
|
+
category: "code"
|
|
392
|
+
},
|
|
393
|
+
fn: (ctx, value, options) => {
|
|
394
|
+
const o = options ?? {};
|
|
395
|
+
const bcid = barcodeTypes[String(o.type ?? "code128").toLowerCase()] ?? "code128";
|
|
396
|
+
const svg = (0, import_bwip_js.toSVG)({
|
|
397
|
+
bcid,
|
|
398
|
+
text: String(value ?? ""),
|
|
399
|
+
height: o.height ?? 12,
|
|
400
|
+
...o.width === void 0 ? {} : { width: o.width },
|
|
401
|
+
scale: o.scale ?? 2,
|
|
402
|
+
includetext: o.text !== false,
|
|
403
|
+
textxalign: "center"
|
|
404
|
+
});
|
|
405
|
+
if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, alt: `Barcode ${String(value ?? "")}` });
|
|
406
|
+
return svgDataUri(svg);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
{
|
|
410
|
+
name: "epcQr",
|
|
411
|
+
aliases: ["epc_qr", "girocode"],
|
|
412
|
+
doc: {
|
|
413
|
+
signature: "epcQr({ name, iban, bic?, amount?, reference?, text? }, { size })",
|
|
414
|
+
description: "SEPA credit transfer QR code (EPC069-12 / GiroCode) that banking apps scan to prefill a payment.",
|
|
415
|
+
examples: {
|
|
416
|
+
jinja2: '<img src="{{ epcQr({ name: company.name, iban: company.iban, amount: invoice.total, reference: invoice.number }) }}">',
|
|
417
|
+
liquid: '<img src="{{ company.iban | epcQr: name: company.name, amount: invoice.total, reference: invoice.number }}">',
|
|
418
|
+
handlebars: '<img src="{{epcQr name=company.name iban=company.iban amount=invoice.total reference=invoice.number}}">'
|
|
419
|
+
},
|
|
420
|
+
category: "code"
|
|
421
|
+
},
|
|
422
|
+
// Payment fields and QR options may arrive together: Jinja2 passes two objects, Handlebars one
|
|
423
|
+
// hash (`{{epcQr name=… iban=… size=200}}`), and Liquid, which cannot build an object literal,
|
|
424
|
+
// keyword arguments (`{{ company.iban | epcQr: name: company.name, amount: invoice.total }}`,
|
|
425
|
+
// where a string input is the IBAN). They are merged and split by field name.
|
|
426
|
+
fn: (ctx, input, options) => {
|
|
427
|
+
const fields = {
|
|
428
|
+
...input && typeof input === "object" ? input : typeof input === "string" && input ? { iban: input } : {},
|
|
429
|
+
...options ?? {}
|
|
430
|
+
};
|
|
431
|
+
const payment = {};
|
|
432
|
+
const qr = {};
|
|
433
|
+
for (const [key, value] of Object.entries(fields))
|
|
434
|
+
(EPC_FIELDS.has(key) ? payment : qr)[key] = value;
|
|
435
|
+
const o = { ecc: "M", ...qr };
|
|
436
|
+
const svg = qrSvg(epcPayload(payment), o);
|
|
437
|
+
if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, width: o.size, height: o.size, alt: "GiroCode" });
|
|
438
|
+
return svgDataUri(svg);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
];
|
|
442
|
+
var EPC_FIELDS = /* @__PURE__ */ new Set(["name", "iban", "bic", "amount", "reference", "text", "purpose"]);
|
|
212
443
|
|
|
213
444
|
// packages/engine/src/lib/helpers/format.ts
|
|
214
445
|
var import_tz = require("@date-fns/tz");
|
|
@@ -242,15 +473,6 @@ var dateLocales = {
|
|
|
242
473
|
function dateLocale(locale) {
|
|
243
474
|
return dateLocales[locale] ?? dateLocales[locale.split("-")[0] ?? ""] ?? import_locale.enGB;
|
|
244
475
|
}
|
|
245
|
-
function toNumber(value) {
|
|
246
|
-
if (typeof value === "number") return value;
|
|
247
|
-
if (typeof value === "string") {
|
|
248
|
-
const n = Number(value.replace(/\s/g, "").replace(",", "."));
|
|
249
|
-
return Number.isFinite(n) ? n : NaN;
|
|
250
|
-
}
|
|
251
|
-
if (typeof value === "bigint") return Number(value);
|
|
252
|
-
return NaN;
|
|
253
|
-
}
|
|
254
476
|
function toDate(value, tz) {
|
|
255
477
|
if (value === "now" || value === void 0 || value === null || value === "")
|
|
256
478
|
return new import_tz.TZDate(Date.now(), tz);
|
|
@@ -273,6 +495,10 @@ function escapeHtml(value) {
|
|
|
273
495
|
var isNamedArgs = (v) => !!v && typeof v === "object" && !Array.isArray(v) && !(v instanceof Date);
|
|
274
496
|
var str = (v) => v === void 0 || v === null ? "" : String(v);
|
|
275
497
|
var opt = (v, fallback) => v === void 0 || v === null || v === "" ? fallback : v;
|
|
498
|
+
function requireFinite(helper, value) {
|
|
499
|
+
if (typeof value === "number" && !Number.isFinite(value))
|
|
500
|
+
throw new Error(`${helper}: got ${value}, the result of a calculation with a value that is missing or not a number`);
|
|
501
|
+
}
|
|
276
502
|
var wordsByLang = {
|
|
277
503
|
de: import_de.toCardinal,
|
|
278
504
|
en: import_en.toCardinal,
|
|
@@ -288,11 +514,16 @@ var formatHelpers = [
|
|
|
288
514
|
deprecatedAlias: { currency_format: "money" },
|
|
289
515
|
doc: {
|
|
290
516
|
signature: "money(value, currency?, locale?)",
|
|
291
|
-
description: "Formats a number as currency with Intl.NumberFormat. Defaults come from the template settings.",
|
|
292
|
-
|
|
517
|
+
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.",
|
|
518
|
+
examples: {
|
|
519
|
+
jinja2: "{{ line.total | money('EUR') }} \u2192 1.234,50 \u20AC",
|
|
520
|
+
liquid: "{{ line.total | money: 'EUR' }} \u2192 1.234,50 \u20AC",
|
|
521
|
+
handlebars: "{{money line.total 'EUR'}} \u2192 1.234,50 \u20AC"
|
|
522
|
+
},
|
|
293
523
|
category: "format"
|
|
294
524
|
},
|
|
295
525
|
fn: (ctx, value, currency, locale) => {
|
|
526
|
+
requireFinite("money", value);
|
|
296
527
|
const n = toNumber(value);
|
|
297
528
|
if (Number.isNaN(n)) return str(value);
|
|
298
529
|
return new Intl.NumberFormat(opt(locale, ctx.locale), {
|
|
@@ -306,10 +537,15 @@ var formatHelpers = [
|
|
|
306
537
|
doc: {
|
|
307
538
|
signature: "number(value, decimals?, locale?)",
|
|
308
539
|
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.",
|
|
309
|
-
|
|
540
|
+
examples: {
|
|
541
|
+
jinja2: "{{ qty | number(2) }} \u2192 1.234,00",
|
|
542
|
+
liquid: "{{ qty | number: 2 }} \u2192 1.234,00",
|
|
543
|
+
handlebars: "{{number qty 2}} \u2192 1.234,00"
|
|
544
|
+
},
|
|
310
545
|
category: "format"
|
|
311
546
|
},
|
|
312
547
|
fn: (ctx, value, decimals, locale) => {
|
|
548
|
+
requireFinite("number", value);
|
|
313
549
|
const n = toNumber(value);
|
|
314
550
|
if (Number.isNaN(n)) return str(value);
|
|
315
551
|
const d = decimals === void 0 || decimals === null ? void 0 : Number(decimals);
|
|
@@ -324,7 +560,11 @@ var formatHelpers = [
|
|
|
324
560
|
doc: {
|
|
325
561
|
signature: "date(value, format?, tz?, locale?)",
|
|
326
562
|
description: "Formats a date with date-fns tokens (e.g. 'dd.MM.yyyy', 'PPP'). Accepts ISO strings, epoch numbers and 'now'.",
|
|
327
|
-
|
|
563
|
+
examples: {
|
|
564
|
+
jinja2: "{{ invoice.date | date('dd.MM.yyyy') }}",
|
|
565
|
+
liquid: "{{ invoice.date | date: 'dd.MM.yyyy' }}",
|
|
566
|
+
handlebars: "{{date invoice.date 'dd.MM.yyyy'}}"
|
|
567
|
+
},
|
|
328
568
|
category: "format"
|
|
329
569
|
},
|
|
330
570
|
fn: (ctx, value, format, tz, locale) => {
|
|
@@ -341,7 +581,11 @@ var formatHelpers = [
|
|
|
341
581
|
doc: {
|
|
342
582
|
signature: "dateAdd(value, amount, unit)",
|
|
343
583
|
description: "Adds days, weeks, months or years to a date; returns an ISO string for further formatting.",
|
|
344
|
-
|
|
584
|
+
examples: {
|
|
585
|
+
jinja2: "{{ invoice.date | dateAdd(14, 'days') | date('PP') }}",
|
|
586
|
+
liquid: "{{ invoice.date | dateAdd: 14, 'days' | date: 'PP' }}",
|
|
587
|
+
handlebars: "{{date (dateAdd invoice.date 14 'days') 'PP'}}"
|
|
588
|
+
},
|
|
345
589
|
category: "format"
|
|
346
590
|
},
|
|
347
591
|
fn: (ctx, value, amount, unit) => {
|
|
@@ -358,7 +602,11 @@ var formatHelpers = [
|
|
|
358
602
|
doc: {
|
|
359
603
|
signature: "add(a, b)",
|
|
360
604
|
description: "a + b (Handlebars has no arithmetic of its own).",
|
|
361
|
-
|
|
605
|
+
examples: {
|
|
606
|
+
jinja2: "{{ line.qty | add(1) }}",
|
|
607
|
+
liquid: "{{ line.qty | add: 1 }}",
|
|
608
|
+
handlebars: "{{#each invoice.lines}}{{add @index 1}}{{/each}}"
|
|
609
|
+
},
|
|
362
610
|
category: "format"
|
|
363
611
|
},
|
|
364
612
|
fn: (_ctx, a, b) => toNumber(a) + toNumber(b)
|
|
@@ -369,7 +617,11 @@ var formatHelpers = [
|
|
|
369
617
|
doc: {
|
|
370
618
|
signature: "subtract(a, b)",
|
|
371
619
|
description: "a minus b.",
|
|
372
|
-
|
|
620
|
+
examples: {
|
|
621
|
+
jinja2: "{{ total | subtract(discount) }}",
|
|
622
|
+
liquid: "{{ total | subtract: discount }}",
|
|
623
|
+
handlebars: "{{subtract total discount}}"
|
|
624
|
+
},
|
|
373
625
|
category: "format"
|
|
374
626
|
},
|
|
375
627
|
fn: (_ctx, a, b) => toNumber(a) - toNumber(b)
|
|
@@ -380,7 +632,11 @@ var formatHelpers = [
|
|
|
380
632
|
doc: {
|
|
381
633
|
signature: "multiply(a, b)",
|
|
382
634
|
description: "a times b.",
|
|
383
|
-
|
|
635
|
+
examples: {
|
|
636
|
+
jinja2: "{{ line.qty | multiply(line.price) | money }}",
|
|
637
|
+
liquid: "{{ line.qty | multiply: line.price | money }}",
|
|
638
|
+
handlebars: "{{money (multiply line.qty line.price)}}"
|
|
639
|
+
},
|
|
384
640
|
category: "format"
|
|
385
641
|
},
|
|
386
642
|
fn: (_ctx, a, b) => toNumber(a) * toNumber(b)
|
|
@@ -391,7 +647,11 @@ var formatHelpers = [
|
|
|
391
647
|
doc: {
|
|
392
648
|
signature: "divide(a, b)",
|
|
393
649
|
description: "a divided by b; empty when b is 0.",
|
|
394
|
-
|
|
650
|
+
examples: {
|
|
651
|
+
jinja2: "{{ total | divide(count) }}",
|
|
652
|
+
liquid: "{{ total | divide: count }}",
|
|
653
|
+
handlebars: "{{divide total count}}"
|
|
654
|
+
},
|
|
395
655
|
category: "format"
|
|
396
656
|
},
|
|
397
657
|
fn: (_ctx, a, b) => toNumber(b) === 0 ? "" : toNumber(a) / toNumber(b)
|
|
@@ -401,7 +661,11 @@ var formatHelpers = [
|
|
|
401
661
|
doc: {
|
|
402
662
|
signature: "round(value, decimals = 0, method = 'common')",
|
|
403
663
|
description: "Rounds to the given decimals, as Jinja2's round does: 'common' rounds half away from zero, 'floor' always down and 'ceil' always up.",
|
|
404
|
-
|
|
664
|
+
examples: {
|
|
665
|
+
jinja2: "{{ 2.345 | round(2) }} gives 2.35, {{ 2.349 | round(2, 'floor') }} gives 2.34",
|
|
666
|
+
liquid: "{{ 2.345 | round: 2 }} gives 2.35, {{ 2.349 | round: 2, 'floor' }} gives 2.34",
|
|
667
|
+
handlebars: "{{round 2.345 2}} gives 2.35, {{round 2.349 2 'floor'}} gives 2.34"
|
|
668
|
+
},
|
|
405
669
|
category: "format"
|
|
406
670
|
},
|
|
407
671
|
// `round(2, method='floor')` (Jinja2) and `round: 2, method: 'floor'` (Liquid) arrive as a
|
|
@@ -427,7 +691,11 @@ var formatHelpers = [
|
|
|
427
691
|
doc: {
|
|
428
692
|
signature: "upper(value)",
|
|
429
693
|
description: "Upper-cases a string.",
|
|
430
|
-
|
|
694
|
+
examples: {
|
|
695
|
+
jinja2: "{{ name | upper }}",
|
|
696
|
+
liquid: "{{ name | upper }}",
|
|
697
|
+
handlebars: "{{upper name}}"
|
|
698
|
+
},
|
|
431
699
|
category: "text"
|
|
432
700
|
},
|
|
433
701
|
fn: (_ctx, value) => str(value).toUpperCase()
|
|
@@ -438,7 +706,11 @@ var formatHelpers = [
|
|
|
438
706
|
doc: {
|
|
439
707
|
signature: "lower(value)",
|
|
440
708
|
description: "Lower-cases a string.",
|
|
441
|
-
|
|
709
|
+
examples: {
|
|
710
|
+
jinja2: "{{ name | lower }}",
|
|
711
|
+
liquid: "{{ name | lower }}",
|
|
712
|
+
handlebars: "{{lower name}}"
|
|
713
|
+
},
|
|
442
714
|
category: "text"
|
|
443
715
|
},
|
|
444
716
|
fn: (_ctx, value) => str(value).toLowerCase()
|
|
@@ -449,7 +721,11 @@ var formatHelpers = [
|
|
|
449
721
|
doc: {
|
|
450
722
|
signature: "title(value)",
|
|
451
723
|
description: "Capitalises every word.",
|
|
452
|
-
|
|
724
|
+
examples: {
|
|
725
|
+
jinja2: "{{ 'hello world' | title }} \u2192 Hello World",
|
|
726
|
+
liquid: "{{ 'hello world' | title }} \u2192 Hello World",
|
|
727
|
+
handlebars: "{{title 'hello world'}} \u2192 Hello World"
|
|
728
|
+
},
|
|
453
729
|
category: "text"
|
|
454
730
|
},
|
|
455
731
|
fn: (_ctx, value) => str(value).replace(
|
|
@@ -462,7 +738,11 @@ var formatHelpers = [
|
|
|
462
738
|
doc: {
|
|
463
739
|
signature: "truncate(value, length = 255, suffix = '\u2026')",
|
|
464
740
|
description: "Cuts a string to `length` characters and appends the suffix when cut.",
|
|
465
|
-
|
|
741
|
+
examples: {
|
|
742
|
+
jinja2: "{{ description | truncate(40) }}",
|
|
743
|
+
liquid: "{{ description | truncate: 40 }}",
|
|
744
|
+
handlebars: "{{truncate description 40}}"
|
|
745
|
+
},
|
|
466
746
|
category: "text"
|
|
467
747
|
},
|
|
468
748
|
fn: (_ctx, value, length, suffix) => {
|
|
@@ -477,7 +757,11 @@ var formatHelpers = [
|
|
|
477
757
|
doc: {
|
|
478
758
|
signature: "replace(value, search, replacement)",
|
|
479
759
|
description: "Replaces every occurrence of `search`.",
|
|
480
|
-
|
|
760
|
+
examples: {
|
|
761
|
+
jinja2: "{{ iban | replace(' ', '') }}",
|
|
762
|
+
liquid: "{{ iban | replace: ' ', '' }}",
|
|
763
|
+
handlebars: "{{replace iban ' ' ''}}"
|
|
764
|
+
},
|
|
481
765
|
category: "text"
|
|
482
766
|
},
|
|
483
767
|
fn: (_ctx, value, search, replacement) => str(value).split(str(search)).join(str(replacement))
|
|
@@ -487,7 +771,11 @@ var formatHelpers = [
|
|
|
487
771
|
doc: {
|
|
488
772
|
signature: "split(value, separator = ',')",
|
|
489
773
|
description: "Splits a string into a list.",
|
|
490
|
-
|
|
774
|
+
examples: {
|
|
775
|
+
jinja2: "{% for tag in keywords | split(',') %}{{ tag }}{% endfor %}",
|
|
776
|
+
liquid: "{% assign tags = keywords | split: ',' %}{% for tag in tags %}{{ tag }}{% endfor %}",
|
|
777
|
+
handlebars: "{{#each (split keywords ',')}}{{this}}{{/each}}"
|
|
778
|
+
},
|
|
491
779
|
category: "text"
|
|
492
780
|
},
|
|
493
781
|
fn: (_ctx, value, separator) => str(value).split(str(opt(separator, ",")))
|
|
@@ -497,7 +785,11 @@ var formatHelpers = [
|
|
|
497
785
|
doc: {
|
|
498
786
|
signature: "join(list, separator = ', ')",
|
|
499
787
|
description: "Joins a list into a string.",
|
|
500
|
-
|
|
788
|
+
examples: {
|
|
789
|
+
jinja2: "{{ tags | join(' \xB7 ') }}",
|
|
790
|
+
liquid: "{{ tags | join: ' \xB7 ' }}",
|
|
791
|
+
handlebars: "{{join tags ' \xB7 '}}"
|
|
792
|
+
},
|
|
501
793
|
category: "collection"
|
|
502
794
|
},
|
|
503
795
|
fn: (_ctx, value, separator) => Array.isArray(value) ? value.map(str).join(str(opt(separator, ", "))) : str(value)
|
|
@@ -508,7 +800,11 @@ var formatHelpers = [
|
|
|
508
800
|
doc: {
|
|
509
801
|
signature: "default(value, fallback)",
|
|
510
802
|
description: "Returns the fallback when the value is empty, null or undefined.",
|
|
511
|
-
|
|
803
|
+
examples: {
|
|
804
|
+
jinja2: "{{ customer.vat | default('\u2014') }}",
|
|
805
|
+
liquid: "{{ customer.vat | default: '\u2014' }}",
|
|
806
|
+
handlebars: "{{default customer.vat '\u2014'}}"
|
|
807
|
+
},
|
|
512
808
|
category: "text"
|
|
513
809
|
},
|
|
514
810
|
fn: (_ctx, value, fallback) => value === void 0 || value === null || value === "" ? fallback : value
|
|
@@ -519,7 +815,11 @@ var formatHelpers = [
|
|
|
519
815
|
doc: {
|
|
520
816
|
signature: "nl2br(value)",
|
|
521
817
|
description: "Escapes the text and turns line breaks into <br>.",
|
|
522
|
-
|
|
818
|
+
examples: {
|
|
819
|
+
jinja2: "{{ address | nl2br }}",
|
|
820
|
+
liquid: "{{ address | nl2br }}",
|
|
821
|
+
handlebars: "{{nl2br address}}"
|
|
822
|
+
},
|
|
523
823
|
category: "text"
|
|
524
824
|
},
|
|
525
825
|
// office templates turn line breaks into breaks themselves, and escape the text once
|
|
@@ -531,7 +831,11 @@ var formatHelpers = [
|
|
|
531
831
|
doc: {
|
|
532
832
|
signature: "markdown(value)",
|
|
533
833
|
description: "Renders Markdown to HTML (no sanitiser: template data is trusted by the author).",
|
|
534
|
-
|
|
834
|
+
examples: {
|
|
835
|
+
jinja2: "{{ notes | markdown }}",
|
|
836
|
+
liquid: "{{ notes | markdown }}",
|
|
837
|
+
handlebars: "{{markdown notes}}"
|
|
838
|
+
},
|
|
535
839
|
category: "text"
|
|
536
840
|
},
|
|
537
841
|
fn: (ctx, value) => {
|
|
@@ -550,7 +854,11 @@ var formatHelpers = [
|
|
|
550
854
|
doc: {
|
|
551
855
|
signature: "safe(value)",
|
|
552
856
|
description: "Marks a string as HTML so the engine does not escape it.",
|
|
553
|
-
|
|
857
|
+
examples: {
|
|
858
|
+
jinja2: "{{ html_snippet | safe }}",
|
|
859
|
+
liquid: "{{ html_snippet | safe }}",
|
|
860
|
+
handlebars: "{{safe html_snippet}}"
|
|
861
|
+
},
|
|
554
862
|
category: "text"
|
|
555
863
|
},
|
|
556
864
|
fn: (_ctx, value) => str(value)
|
|
@@ -561,7 +869,11 @@ var formatHelpers = [
|
|
|
561
869
|
doc: {
|
|
562
870
|
signature: "json(value, indent = 2)",
|
|
563
871
|
description: "Serialises a value as JSON; handy while building a template.",
|
|
564
|
-
|
|
872
|
+
examples: {
|
|
873
|
+
jinja2: "<pre>{{ invoice | json }}</pre>",
|
|
874
|
+
liquid: "<pre>{{ invoice | json }}</pre>",
|
|
875
|
+
handlebars: "<pre>{{json invoice}}</pre>"
|
|
876
|
+
},
|
|
565
877
|
category: "debug"
|
|
566
878
|
},
|
|
567
879
|
fn: (_ctx, value, indent) => JSON.stringify(value, null, opt(Number(indent), 2))
|
|
@@ -572,10 +884,15 @@ var formatHelpers = [
|
|
|
572
884
|
doc: {
|
|
573
885
|
signature: "numToWords(value, locale?)",
|
|
574
886
|
description: "Spells a number out in words (de, en, fr, es, it, nl).",
|
|
575
|
-
|
|
887
|
+
examples: {
|
|
888
|
+
jinja2: "{{ total | numToWords('de') }} \u2192 zweihundertdreiundvierzig",
|
|
889
|
+
liquid: "{{ total | numToWords: 'de' }} \u2192 zweihundertdreiundvierzig",
|
|
890
|
+
handlebars: "{{numToWords total 'de'}} \u2192 zweihundertdreiundvierzig"
|
|
891
|
+
},
|
|
576
892
|
category: "format"
|
|
577
893
|
},
|
|
578
894
|
fn: (ctx, value, locale) => {
|
|
895
|
+
requireFinite("numToWords", value);
|
|
579
896
|
const n = toNumber(value);
|
|
580
897
|
if (Number.isNaN(n)) return str(value);
|
|
581
898
|
const lang = str(opt(locale, ctx.locale)).split("-")[0] ?? "en";
|
|
@@ -588,7 +905,11 @@ var formatHelpers = [
|
|
|
588
905
|
doc: {
|
|
589
906
|
signature: "t(key, params?)",
|
|
590
907
|
description: "Looks a key up in the template's i18n dictionary for the request locale; `{name}` placeholders come from params.",
|
|
591
|
-
|
|
908
|
+
examples: {
|
|
909
|
+
jinja2: "{{ t('invoice.title', { number: invoice.number }) }}",
|
|
910
|
+
liquid: "{{ 'invoice.title' | t: number: invoice.number }}",
|
|
911
|
+
handlebars: "{{t 'invoice.title' number=invoice.number}}"
|
|
912
|
+
},
|
|
592
913
|
category: "text"
|
|
593
914
|
},
|
|
594
915
|
fn: (ctx, key, params) => {
|
|
@@ -604,7 +925,11 @@ var formatHelpers = [
|
|
|
604
925
|
doc: {
|
|
605
926
|
signature: "asset(name)",
|
|
606
927
|
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.",
|
|
607
|
-
|
|
928
|
+
examples: {
|
|
929
|
+
jinja2: `<img src="{{ asset('logo.png') }}">`,
|
|
930
|
+
liquid: `<img src="{{ 'logo.png' | asset }}">`,
|
|
931
|
+
handlebars: `<img src="{{asset 'logo.png'}}">`
|
|
932
|
+
},
|
|
608
933
|
category: "document"
|
|
609
934
|
},
|
|
610
935
|
fn: (ctx, name) => `${(ctx.assetBaseUrl ?? "").replace(/\/$/, "")}/${str(name).replace(/^\//, "")}`
|
|
@@ -616,128 +941,17 @@ var formatHelpers = [
|
|
|
616
941
|
doc: {
|
|
617
942
|
signature: "pageBreak()",
|
|
618
943
|
description: "Starts a new page in PDF output.",
|
|
619
|
-
|
|
944
|
+
examples: {
|
|
945
|
+
jinja2: "{{ pageBreak() }}",
|
|
946
|
+
liquid: "{{ '' | pageBreak }}",
|
|
947
|
+
handlebars: "{{pageBreak}}"
|
|
948
|
+
},
|
|
620
949
|
category: "document"
|
|
621
950
|
},
|
|
622
951
|
fn: (ctx) => ctx.drawing ? ctx.drawing({ kind: "page-break" }) : '<div class="page-break" style="break-after:page"></div>'
|
|
623
952
|
}
|
|
624
953
|
];
|
|
625
954
|
|
|
626
|
-
// packages/engine/src/lib/helpers/codes.ts
|
|
627
|
-
var svgDataUri = (svg) => `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
|
|
628
|
-
var barcodeTypes = {
|
|
629
|
-
code128: "code128",
|
|
630
|
-
ean13: "ean13",
|
|
631
|
-
ean8: "ean8",
|
|
632
|
-
upc: "upca",
|
|
633
|
-
upca: "upca",
|
|
634
|
-
itf14: "itf14",
|
|
635
|
-
code39: "code39",
|
|
636
|
-
qrcode: "qrcode",
|
|
637
|
-
datamatrix: "datamatrix",
|
|
638
|
-
pdf417: "pdf417"
|
|
639
|
-
};
|
|
640
|
-
function qrSvg(value, options = {}) {
|
|
641
|
-
const qr = (0, import_qrcode.create)(value, { errorCorrectionLevel: options.ecc ?? "M" });
|
|
642
|
-
const svg = (0, import_svg_tag.render)(qr, {
|
|
643
|
-
width: options.size ?? 160,
|
|
644
|
-
margin: options.margin ?? 1,
|
|
645
|
-
color: {
|
|
646
|
-
dark: (options.color ?? "#000000").replace(/^(#?)/, "#").slice(0, 9),
|
|
647
|
-
light: (options.background ?? "#ffffff").replace(/^(#?)/, "#")
|
|
648
|
-
}
|
|
649
|
-
});
|
|
650
|
-
return svg;
|
|
651
|
-
}
|
|
652
|
-
function epcPayload(input) {
|
|
653
|
-
const amount = input.amount === void 0 || input.amount === "" ? "" : `EUR${toNumber(input.amount).toFixed(2)}`;
|
|
654
|
-
return [
|
|
655
|
-
"BCD",
|
|
656
|
-
"002",
|
|
657
|
-
"1",
|
|
658
|
-
"SCT",
|
|
659
|
-
(input.bic ?? "").replace(/\s/g, ""),
|
|
660
|
-
String(input.name ?? "").slice(0, 70),
|
|
661
|
-
String(input.iban ?? "").replace(/\s/g, ""),
|
|
662
|
-
amount,
|
|
663
|
-
(input.purpose ?? "").slice(0, 4),
|
|
664
|
-
(input.reference ?? "").slice(0, 35),
|
|
665
|
-
input.reference ? "" : (input.text ?? "").slice(0, 140),
|
|
666
|
-
""
|
|
667
|
-
].join("\n");
|
|
668
|
-
}
|
|
669
|
-
var codeHelpers = [
|
|
670
|
-
{
|
|
671
|
-
name: "qrcode",
|
|
672
|
-
aliases: ["qr"],
|
|
673
|
-
doc: {
|
|
674
|
-
signature: "qrcode(value, { size = 160, ecc = 'M', margin = 1, color, background })",
|
|
675
|
-
description: "QR code as an SVG data URI for an <img> src.",
|
|
676
|
-
example: '<img src="{{ qrcode(order.url, { size: 120 }) }}">',
|
|
677
|
-
category: "code"
|
|
678
|
-
},
|
|
679
|
-
fn: (ctx, value, options) => {
|
|
680
|
-
const o = options ?? {};
|
|
681
|
-
const svg = qrSvg(String(value ?? ""), o);
|
|
682
|
-
if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, width: o.size, height: o.size, alt: "QR code" });
|
|
683
|
-
return svgDataUri(svg);
|
|
684
|
-
}
|
|
685
|
-
},
|
|
686
|
-
{
|
|
687
|
-
name: "barcode",
|
|
688
|
-
doc: {
|
|
689
|
-
signature: "barcode(value, { type = 'code128', height = 12, width, text = true, scale = 2 })",
|
|
690
|
-
description: "Barcode (code128, ean13, ean8, upc, itf14, code39, datamatrix, pdf417) as an SVG data URI.",
|
|
691
|
-
example: `<img src="{{ barcode(item.sku, { type: 'code128' }) }}">`,
|
|
692
|
-
category: "code"
|
|
693
|
-
},
|
|
694
|
-
fn: (ctx, value, options) => {
|
|
695
|
-
const o = options ?? {};
|
|
696
|
-
const bcid = barcodeTypes[String(o.type ?? "code128").toLowerCase()] ?? "code128";
|
|
697
|
-
const svg = (0, import_bwip_js.toSVG)({
|
|
698
|
-
bcid,
|
|
699
|
-
text: String(value ?? ""),
|
|
700
|
-
height: o.height ?? 12,
|
|
701
|
-
...o.width === void 0 ? {} : { width: o.width },
|
|
702
|
-
scale: o.scale ?? 2,
|
|
703
|
-
includetext: o.text !== false,
|
|
704
|
-
textxalign: "center"
|
|
705
|
-
});
|
|
706
|
-
if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, alt: `Barcode ${String(value ?? "")}` });
|
|
707
|
-
return svgDataUri(svg);
|
|
708
|
-
}
|
|
709
|
-
},
|
|
710
|
-
{
|
|
711
|
-
name: "epcQr",
|
|
712
|
-
aliases: ["epc_qr", "girocode"],
|
|
713
|
-
doc: {
|
|
714
|
-
signature: "epcQr({ name, iban, bic?, amount?, reference?, text? }, { size })",
|
|
715
|
-
description: "SEPA credit transfer QR code (EPC069-12 / GiroCode) that banking apps scan to prefill a payment.",
|
|
716
|
-
example: '<img src="{{ epcQr({ name: company.name, iban: company.iban, amount: invoice.total, reference: invoice.number }) }}">',
|
|
717
|
-
category: "code"
|
|
718
|
-
},
|
|
719
|
-
// Payment fields and QR options may arrive together: Jinja2 passes two objects, Handlebars one
|
|
720
|
-
// hash (`{{epcQr name=… iban=… size=200}}`), and Liquid, which cannot build an object literal,
|
|
721
|
-
// keyword arguments (`{{ company.iban | epcQr: name: company.name, amount: invoice.total }}`,
|
|
722
|
-
// where a string input is the IBAN). They are merged and split by field name.
|
|
723
|
-
fn: (ctx, input, options) => {
|
|
724
|
-
const fields = {
|
|
725
|
-
...input && typeof input === "object" ? input : typeof input === "string" && input ? { iban: input } : {},
|
|
726
|
-
...options ?? {}
|
|
727
|
-
};
|
|
728
|
-
const payment = {};
|
|
729
|
-
const qr = {};
|
|
730
|
-
for (const [key, value] of Object.entries(fields))
|
|
731
|
-
(EPC_FIELDS.has(key) ? payment : qr)[key] = value;
|
|
732
|
-
const o = { ecc: "M", ...qr };
|
|
733
|
-
const svg = qrSvg(epcPayload(payment), o);
|
|
734
|
-
if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, width: o.size, height: o.size, alt: "GiroCode" });
|
|
735
|
-
return svgDataUri(svg);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
];
|
|
739
|
-
var EPC_FIELDS = /* @__PURE__ */ new Set(["name", "iban", "bic", "amount", "reference", "text", "purpose"]);
|
|
740
|
-
|
|
741
955
|
// packages/engine/src/lib/helpers/collections.ts
|
|
742
956
|
var list = (v) => Array.isArray(v) ? v : v === void 0 || v === null ? [] : [v];
|
|
743
957
|
var get = (item, path) => String(path ?? "").split(".").filter(Boolean).reduce(
|
|
@@ -751,7 +965,11 @@ var collectionHelpers = [
|
|
|
751
965
|
doc: {
|
|
752
966
|
signature: "sum(list, path?)",
|
|
753
967
|
description: "Sum of the items, or of `path` inside each item.",
|
|
754
|
-
|
|
968
|
+
examples: {
|
|
969
|
+
jinja2: "{{ invoice.lines | sum('total') | money }}",
|
|
970
|
+
liquid: "{{ invoice.lines | sum: 'total' | money }}",
|
|
971
|
+
handlebars: "{{money (sum invoice.lines 'total')}}"
|
|
972
|
+
},
|
|
755
973
|
category: "collection"
|
|
756
974
|
},
|
|
757
975
|
fn: (_ctx, value, path) => numbers(value, path).reduce((a, b) => a + b, 0)
|
|
@@ -761,7 +979,11 @@ var collectionHelpers = [
|
|
|
761
979
|
doc: {
|
|
762
980
|
signature: "avg(list, path?)",
|
|
763
981
|
description: "Average of the items or of `path` inside each item.",
|
|
764
|
-
|
|
982
|
+
examples: {
|
|
983
|
+
jinja2: "{{ scores | avg | number(1) }}",
|
|
984
|
+
liquid: "{{ scores | avg | number: 1 }}",
|
|
985
|
+
handlebars: "{{number (avg scores) 1}}"
|
|
986
|
+
},
|
|
765
987
|
category: "collection"
|
|
766
988
|
},
|
|
767
989
|
fn: (_ctx, value, path) => {
|
|
@@ -774,7 +996,11 @@ var collectionHelpers = [
|
|
|
774
996
|
doc: {
|
|
775
997
|
signature: "min(list, path?)",
|
|
776
998
|
description: "Smallest number.",
|
|
777
|
-
|
|
999
|
+
examples: {
|
|
1000
|
+
jinja2: "{{ prices | min }}",
|
|
1001
|
+
liquid: "{{ prices | min }}",
|
|
1002
|
+
handlebars: "{{min prices}}"
|
|
1003
|
+
},
|
|
778
1004
|
category: "collection"
|
|
779
1005
|
},
|
|
780
1006
|
fn: (_ctx, value, path) => Math.min(...numbers(value, path))
|
|
@@ -784,7 +1010,11 @@ var collectionHelpers = [
|
|
|
784
1010
|
doc: {
|
|
785
1011
|
signature: "max(list, path?)",
|
|
786
1012
|
description: "Largest number.",
|
|
787
|
-
|
|
1013
|
+
examples: {
|
|
1014
|
+
jinja2: "{{ prices | max }}",
|
|
1015
|
+
liquid: "{{ prices | max }}",
|
|
1016
|
+
handlebars: "{{max prices}}"
|
|
1017
|
+
},
|
|
788
1018
|
category: "collection"
|
|
789
1019
|
},
|
|
790
1020
|
fn: (_ctx, value, path) => Math.max(...numbers(value, path))
|
|
@@ -795,7 +1025,11 @@ var collectionHelpers = [
|
|
|
795
1025
|
doc: {
|
|
796
1026
|
signature: "groupBy(list, path)",
|
|
797
1027
|
description: "Groups items by a field; returns a list of `{ key, items }`.",
|
|
798
|
-
|
|
1028
|
+
examples: {
|
|
1029
|
+
jinja2: "{% for group in lines | groupBy('category') %}{{ group.key }}{% endfor %}",
|
|
1030
|
+
liquid: "{% assign groups = lines | groupBy: 'category' %}{% for group in groups %}{{ group.key }}{% endfor %}",
|
|
1031
|
+
handlebars: "{{#each (groupBy lines 'category')}}{{key}}{{/each}}"
|
|
1032
|
+
},
|
|
799
1033
|
category: "collection"
|
|
800
1034
|
},
|
|
801
1035
|
fn: (_ctx, value, path) => {
|
|
@@ -813,7 +1047,11 @@ var collectionHelpers = [
|
|
|
813
1047
|
doc: {
|
|
814
1048
|
signature: "sortBy(list, path?, direction = 'asc')",
|
|
815
1049
|
description: "Sorts items by a field (numbers numerically, strings by locale).",
|
|
816
|
-
|
|
1050
|
+
examples: {
|
|
1051
|
+
jinja2: "{% for l in lines | sortBy('position') %}{{ l.sku }}{% endfor %}",
|
|
1052
|
+
liquid: "{% assign sorted = lines | sortBy: 'position' %}{% for l in sorted %}{{ l.sku }}{% endfor %}",
|
|
1053
|
+
handlebars: "{{#each (sortBy lines 'position')}}{{sku}}{{/each}}"
|
|
1054
|
+
},
|
|
817
1055
|
category: "collection"
|
|
818
1056
|
},
|
|
819
1057
|
fn: (ctx, value, path, direction) => {
|
|
@@ -834,7 +1072,11 @@ var collectionHelpers = [
|
|
|
834
1072
|
doc: {
|
|
835
1073
|
signature: "where(list, path, value?)",
|
|
836
1074
|
description: "Keeps items whose field equals `value` (or is truthy when omitted).",
|
|
837
|
-
|
|
1075
|
+
examples: {
|
|
1076
|
+
jinja2: "{% for l in lines | where('taxable', true) %}{{ l.sku }}{% endfor %}",
|
|
1077
|
+
liquid: "{% assign taxable = lines | where: 'taxable', true %}{% for l in taxable %}{{ l.sku }}{% endfor %}",
|
|
1078
|
+
handlebars: "{{#each (where lines 'taxable' true)}}{{sku}}{{/each}}"
|
|
1079
|
+
},
|
|
838
1080
|
category: "collection"
|
|
839
1081
|
},
|
|
840
1082
|
fn: (_ctx, value, path, expected) => list(value).filter((item) => {
|
|
@@ -848,7 +1090,11 @@ var collectionHelpers = [
|
|
|
848
1090
|
doc: {
|
|
849
1091
|
signature: "pluck(list, path)",
|
|
850
1092
|
description: "Extracts one field from every item.",
|
|
851
|
-
|
|
1093
|
+
examples: {
|
|
1094
|
+
jinja2: "{{ lines | pluck('sku') | join(', ') }}",
|
|
1095
|
+
liquid: "{{ lines | pluck: 'sku' | join: ', ' }}",
|
|
1096
|
+
handlebars: "{{join (pluck lines 'sku') ', '}}"
|
|
1097
|
+
},
|
|
852
1098
|
category: "collection"
|
|
853
1099
|
},
|
|
854
1100
|
fn: (_ctx, value, path) => list(value).map((item) => get(item, path))
|
|
@@ -859,7 +1105,11 @@ var collectionHelpers = [
|
|
|
859
1105
|
doc: {
|
|
860
1106
|
signature: "chunk(list, size)",
|
|
861
1107
|
description: "Splits a list into lists of `size` items (rows of labels, columns).",
|
|
862
|
-
|
|
1108
|
+
examples: {
|
|
1109
|
+
jinja2: "{% for row in labels | chunk(3) %}{{ row | join(', ') }}{% endfor %}",
|
|
1110
|
+
liquid: "{% assign rows = labels | chunk: 3 %}{% for row in rows %}{{ row | join: ', ' }}{% endfor %}",
|
|
1111
|
+
handlebars: "{{#each (chunk labels 3)}}{{join this ', '}}{{/each}}"
|
|
1112
|
+
},
|
|
863
1113
|
category: "collection"
|
|
864
1114
|
},
|
|
865
1115
|
fn: (_ctx, value, size) => {
|
|
@@ -875,7 +1125,11 @@ var collectionHelpers = [
|
|
|
875
1125
|
doc: {
|
|
876
1126
|
signature: "range(start, end?, step = 1)",
|
|
877
1127
|
description: "List of numbers from start to end (exclusive).",
|
|
878
|
-
|
|
1128
|
+
examples: {
|
|
1129
|
+
jinja2: "{% for i in range(1, 4) %}{{ i }}{% endfor %}",
|
|
1130
|
+
liquid: "{% assign nums = 1 | range: 4 %}{% for i in nums %}{{ i }}{% endfor %}",
|
|
1131
|
+
handlebars: "{{#each (range 1 4)}}{{this}}{{/each}}"
|
|
1132
|
+
},
|
|
879
1133
|
category: "collection"
|
|
880
1134
|
},
|
|
881
1135
|
fn: (_ctx, start, end, step) => {
|
|
@@ -893,7 +1147,11 @@ var collectionHelpers = [
|
|
|
893
1147
|
doc: {
|
|
894
1148
|
signature: "first(list)",
|
|
895
1149
|
description: "First item.",
|
|
896
|
-
|
|
1150
|
+
examples: {
|
|
1151
|
+
jinja2: "{{ tags | first }}",
|
|
1152
|
+
liquid: "{{ tags | first }}",
|
|
1153
|
+
handlebars: "{{first tags}}"
|
|
1154
|
+
},
|
|
897
1155
|
category: "collection"
|
|
898
1156
|
},
|
|
899
1157
|
fn: (_ctx, value) => list(value)[0]
|
|
@@ -903,7 +1161,11 @@ var collectionHelpers = [
|
|
|
903
1161
|
doc: {
|
|
904
1162
|
signature: "last(list)",
|
|
905
1163
|
description: "Last item.",
|
|
906
|
-
|
|
1164
|
+
examples: {
|
|
1165
|
+
jinja2: "{{ tags | last }}",
|
|
1166
|
+
liquid: "{{ tags | last }}",
|
|
1167
|
+
handlebars: "{{last tags}}"
|
|
1168
|
+
},
|
|
907
1169
|
category: "collection"
|
|
908
1170
|
},
|
|
909
1171
|
fn: (_ctx, value) => list(value).at(-1)
|
|
@@ -914,7 +1176,11 @@ var collectionHelpers = [
|
|
|
914
1176
|
doc: {
|
|
915
1177
|
signature: "length(value)",
|
|
916
1178
|
description: "Number of items in a list, keys in an object or characters in a string.",
|
|
917
|
-
|
|
1179
|
+
examples: {
|
|
1180
|
+
jinja2: "{{ lines | length }}",
|
|
1181
|
+
liquid: "{{ lines | length }}",
|
|
1182
|
+
handlebars: "{{length lines}}"
|
|
1183
|
+
},
|
|
918
1184
|
category: "collection"
|
|
919
1185
|
},
|
|
920
1186
|
fn: (_ctx, value) => Array.isArray(value) || typeof value === "string" ? value.length : value && typeof value === "object" ? Object.keys(value).length : 0
|
|
@@ -990,7 +1256,11 @@ var documentHelpers = [
|
|
|
990
1256
|
doc: {
|
|
991
1257
|
signature: "chart({ type, labels, values, label, color } | { type, labels, series } | { type, data, options }, { width, height })",
|
|
992
1258
|
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.",
|
|
993
|
-
|
|
1259
|
+
examples: {
|
|
1260
|
+
jinja2: "{{ chart({ type: 'line', labels: sales.labels, values: sales.quarters, width: 480 }) }}",
|
|
1261
|
+
liquid: "{{ '' | chart: type: 'line', labels: sales.labels, values: sales.quarters, width: 480 }}",
|
|
1262
|
+
handlebars: "{{chart type='line' labels=sales.labels values=sales.quarters width=480}}"
|
|
1263
|
+
},
|
|
994
1264
|
category: "document"
|
|
995
1265
|
},
|
|
996
1266
|
fn: (ctx, spec, extra) => {
|
|
@@ -1004,7 +1274,11 @@ var documentHelpers = [
|
|
|
1004
1274
|
doc: {
|
|
1005
1275
|
signature: "image(url, { width, height, fit, alt })",
|
|
1006
1276
|
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.",
|
|
1007
|
-
|
|
1277
|
+
examples: {
|
|
1278
|
+
jinja2: "{{ image(product.photo, { width: 120, height: 80, fit: 'cover' }) }}",
|
|
1279
|
+
liquid: "{{ product.photo | image: width: 120, height: 80, fit: 'cover' }}",
|
|
1280
|
+
handlebars: "{{image product.photo width=120 height=80 fit='cover'}}"
|
|
1281
|
+
},
|
|
1008
1282
|
category: "document"
|
|
1009
1283
|
},
|
|
1010
1284
|
fn: (ctx, url, options) => {
|
|
@@ -1030,7 +1304,12 @@ var comparison = (name, test, description) => ({
|
|
|
1030
1304
|
doc: {
|
|
1031
1305
|
signature: `${name}(a, b)`,
|
|
1032
1306
|
description,
|
|
1033
|
-
|
|
1307
|
+
examples: {
|
|
1308
|
+
jinja2: `{% if ${name}(invoice.total, 1000) %}\u2026{% endif %}`,
|
|
1309
|
+
// Liquid takes no filter in a condition, so the comparison is assigned first
|
|
1310
|
+
liquid: `{% assign hit = invoice.total | ${name}: 1000 %}{% if hit %}\u2026{% endif %}`,
|
|
1311
|
+
handlebars: `{{#if (${name} invoice.total 1000)}}\u2026{{/if}}`
|
|
1312
|
+
},
|
|
1034
1313
|
category: "logic"
|
|
1035
1314
|
},
|
|
1036
1315
|
fn: (_ctx, a, b) => test(a, b)
|
|
@@ -1047,7 +1326,12 @@ var logicHelpers = [
|
|
|
1047
1326
|
doc: {
|
|
1048
1327
|
signature: "and(a, b, \u2026)",
|
|
1049
1328
|
description: "True when every value is true; an empty list counts as false.",
|
|
1050
|
-
|
|
1329
|
+
examples: {
|
|
1330
|
+
jinja2: "{% if and(customer.vatId, eq(country, 'DE')) %}\u2026{% endif %}",
|
|
1331
|
+
// nesting one comparison in another needs its own assignment in Liquid
|
|
1332
|
+
liquid: "{% assign german = country | eq: 'DE' %}{% assign both = customer.vatId | and: german %}{% if both %}\u2026{% endif %}",
|
|
1333
|
+
handlebars: "{{#if (and customer.vatId (eq country 'DE'))}}\u2026{{/if}}"
|
|
1334
|
+
},
|
|
1051
1335
|
category: "logic"
|
|
1052
1336
|
},
|
|
1053
1337
|
fn: (_ctx, ...values) => values.length > 0 && values.every(truthy)
|
|
@@ -1057,7 +1341,11 @@ var logicHelpers = [
|
|
|
1057
1341
|
doc: {
|
|
1058
1342
|
signature: "or(a, b, \u2026)",
|
|
1059
1343
|
description: "True when any value is true.",
|
|
1060
|
-
|
|
1344
|
+
examples: {
|
|
1345
|
+
jinja2: "{% if or(invoice.paid, invoice.credited) %}\u2026{% endif %}",
|
|
1346
|
+
liquid: "{% assign any = invoice.paid | or: invoice.credited %}{% if any %}\u2026{% endif %}",
|
|
1347
|
+
handlebars: "{{#if (or invoice.paid invoice.credited)}}\u2026{{/if}}"
|
|
1348
|
+
},
|
|
1061
1349
|
category: "logic"
|
|
1062
1350
|
},
|
|
1063
1351
|
fn: (_ctx, ...values) => values.some(truthy)
|
|
@@ -1067,7 +1355,11 @@ var logicHelpers = [
|
|
|
1067
1355
|
doc: {
|
|
1068
1356
|
signature: "not(value)",
|
|
1069
1357
|
description: "The opposite truth value.",
|
|
1070
|
-
|
|
1358
|
+
examples: {
|
|
1359
|
+
jinja2: "{% if invoice.paid | not %}\u2026{% endif %}",
|
|
1360
|
+
liquid: "{% assign unpaid = invoice.paid | not %}{% if unpaid %}\u2026{% endif %}",
|
|
1361
|
+
handlebars: "{{#if (not invoice.paid)}}\u2026{{/if}}"
|
|
1362
|
+
},
|
|
1071
1363
|
category: "logic"
|
|
1072
1364
|
},
|
|
1073
1365
|
fn: (_ctx, value) => !truthy(value)
|
|
@@ -1140,7 +1432,9 @@ function helperDocs(registry = defaultHelpers()) {
|
|
|
1140
1432
|
return [...registry.definitions.values()].map((d) => ({
|
|
1141
1433
|
name: d.name,
|
|
1142
1434
|
aliases: d.aliases ?? [],
|
|
1143
|
-
...d.doc
|
|
1435
|
+
...d.doc,
|
|
1436
|
+
// derived rather than authored, so the old field cannot drift from `examples`
|
|
1437
|
+
example: d.doc.examples.jinja2
|
|
1144
1438
|
}));
|
|
1145
1439
|
}
|
|
1146
1440
|
|
|
@@ -1262,6 +1556,7 @@ function scanBlocks(source, spec) {
|
|
|
1262
1556
|
severity: "error",
|
|
1263
1557
|
code: "unexpected-end-tag",
|
|
1264
1558
|
message: `Unexpected {% ${keyword} %} without an open block`,
|
|
1559
|
+
args: { keyword },
|
|
1265
1560
|
range
|
|
1266
1561
|
});
|
|
1267
1562
|
} else if (top.keyword !== expected) {
|
|
@@ -1269,6 +1564,7 @@ function scanBlocks(source, spec) {
|
|
|
1269
1564
|
severity: "error",
|
|
1270
1565
|
code: "mismatched-end-tag",
|
|
1271
1566
|
message: `Expected {% end${top.keyword} %} to close the {% ${top.keyword} %} at line ${top.block.open.start.line}, found {% ${keyword} %}`,
|
|
1567
|
+
args: { open: top.keyword, line: top.block.open.start.line, found: keyword },
|
|
1272
1568
|
range
|
|
1273
1569
|
});
|
|
1274
1570
|
const idx = stack.map((s) => s.keyword).lastIndexOf(expected);
|
|
@@ -1285,6 +1581,7 @@ function scanBlocks(source, spec) {
|
|
|
1285
1581
|
severity: "error",
|
|
1286
1582
|
code: "unexpected-tag",
|
|
1287
1583
|
message: `{% ${keyword} %} outside of a block`,
|
|
1584
|
+
args: { keyword },
|
|
1288
1585
|
range
|
|
1289
1586
|
});
|
|
1290
1587
|
}
|
|
@@ -1294,6 +1591,7 @@ function scanBlocks(source, spec) {
|
|
|
1294
1591
|
severity: "error",
|
|
1295
1592
|
code: "unclosed-block",
|
|
1296
1593
|
message: `{% ${open.keyword} %} is never closed; add {% end${open.keyword} %}`,
|
|
1594
|
+
args: { keyword: open.keyword },
|
|
1297
1595
|
range: open.block.open
|
|
1298
1596
|
});
|
|
1299
1597
|
}
|
|
@@ -1348,6 +1646,7 @@ function missingPathDiagnostics(variables, sampleData, loopSources, builtinRoots
|
|
|
1348
1646
|
severity: "warning",
|
|
1349
1647
|
code: "unknown-variable",
|
|
1350
1648
|
message: `"${v.path.join(".")}" is not present in the sample data`,
|
|
1649
|
+
args: { name: v.path.join(".") },
|
|
1351
1650
|
range: v.range
|
|
1352
1651
|
});
|
|
1353
1652
|
}
|
|
@@ -1598,6 +1897,7 @@ function analyzeHandlebars(source, opts = {}) {
|
|
|
1598
1897
|
severity: "warning",
|
|
1599
1898
|
code: "deprecated-filter",
|
|
1600
1899
|
message: `"${f.name}" is a compatibility alias; use "${preferred}"`,
|
|
1900
|
+
args: { name: f.name, preferred },
|
|
1601
1901
|
range: f.range,
|
|
1602
1902
|
fix: { title: `Rename to ${preferred}`, replacement: preferred }
|
|
1603
1903
|
});
|
|
@@ -1613,6 +1913,7 @@ function analyzeHandlebars(source, opts = {}) {
|
|
|
1613
1913
|
severity: "error",
|
|
1614
1914
|
code: "unknown-filter",
|
|
1615
1915
|
message: `Unknown helper "${f.name}"`,
|
|
1916
|
+
args: { name: f.name, kind: "helper" },
|
|
1616
1917
|
range: f.range
|
|
1617
1918
|
});
|
|
1618
1919
|
}
|
|
@@ -2407,6 +2708,7 @@ function analyzeJinja2(source, opts = {}) {
|
|
|
2407
2708
|
severity: "warning",
|
|
2408
2709
|
code: "deprecated-filter",
|
|
2409
2710
|
message: `"${f.name}" is an apitemplate.io compatibility alias; use "${preferred}"`,
|
|
2711
|
+
args: { name: f.name, preferred },
|
|
2410
2712
|
range: f.range,
|
|
2411
2713
|
fix: { title: `Rename to ${preferred}`, replacement: preferred }
|
|
2412
2714
|
});
|
|
@@ -2422,6 +2724,7 @@ function analyzeJinja2(source, opts = {}) {
|
|
|
2422
2724
|
severity: "error",
|
|
2423
2725
|
code: "unknown-filter",
|
|
2424
2726
|
message: `Unknown filter or helper "${f.name}"`,
|
|
2727
|
+
args: { name: f.name, kind: "filter-or-helper" },
|
|
2425
2728
|
range: f.range
|
|
2426
2729
|
});
|
|
2427
2730
|
}
|
|
@@ -2822,6 +3125,7 @@ function analyzeLiquid(source, opts = {}) {
|
|
|
2822
3125
|
severity: "warning",
|
|
2823
3126
|
code: "deprecated-filter",
|
|
2824
3127
|
message: `"${f.name}" is a compatibility alias; use "${preferred}"`,
|
|
3128
|
+
args: { name: f.name, preferred },
|
|
2825
3129
|
range: f.range,
|
|
2826
3130
|
fix: { title: `Rename to ${preferred}`, replacement: preferred }
|
|
2827
3131
|
});
|
|
@@ -2837,6 +3141,7 @@ function analyzeLiquid(source, opts = {}) {
|
|
|
2837
3141
|
severity: "error",
|
|
2838
3142
|
code: "unknown-filter",
|
|
2839
3143
|
message: `Unknown filter "${f.name}"`,
|
|
3144
|
+
args: { name: f.name, kind: "filter" },
|
|
2840
3145
|
range: f.range
|
|
2841
3146
|
});
|
|
2842
3147
|
}
|
|
@@ -3572,7 +3877,7 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
3572
3877
|
invoice: { due_date: "2026-10-13" },
|
|
3573
3878
|
payment: {
|
|
3574
3879
|
name: "Fennlor Studio GmbH",
|
|
3575
|
-
iban: "
|
|
3880
|
+
iban: "DE36000000000000000000",
|
|
3576
3881
|
bic: "XXXXDEXXXXX",
|
|
3577
3882
|
amount: 3760.4,
|
|
3578
3883
|
reference: "2026-0042"
|
|
@@ -3759,7 +4064,7 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
3759
4064
|
sampleData: {
|
|
3760
4065
|
payment: {
|
|
3761
4066
|
name: "Fennlor Studio GmbH",
|
|
3762
|
-
iban: "
|
|
4067
|
+
iban: "DE36000000000000000000",
|
|
3763
4068
|
bic: "XXXXDEXXXXX",
|
|
3764
4069
|
amount: 3760.4,
|
|
3765
4070
|
reference: "2026-0042"
|
|
@@ -5369,6 +5674,58 @@ var pageNavScript = `<script>
|
|
|
5369
5674
|
}, { passive: true });
|
|
5370
5675
|
})();
|
|
5371
5676
|
</script>`;
|
|
5677
|
+
function overflowScript(mode) {
|
|
5678
|
+
return `<script>
|
|
5679
|
+
(function () {
|
|
5680
|
+
var clipped = function (el, stop) {
|
|
5681
|
+
for (var p = el.parentElement; p && p !== stop; p = p.parentElement)
|
|
5682
|
+
if (getComputedStyle(p).overflowX !== 'visible') return true;
|
|
5683
|
+
return false;
|
|
5684
|
+
};
|
|
5685
|
+
var measure = function () {
|
|
5686
|
+
try {
|
|
5687
|
+
var boxes = ${mode === "paged" ? "document.querySelectorAll('.pagedjs_page_content')" : "[document.body]"};
|
|
5688
|
+
var worst = null;
|
|
5689
|
+
for (var i = 0; i < boxes.length; i++) {
|
|
5690
|
+
var box = boxes[i], rect = box.getBoundingClientRect(), style = getComputedStyle(box);
|
|
5691
|
+
var left = rect.left + parseFloat(style.paddingLeft || '0') + parseFloat(style.borderLeftWidth || '0');
|
|
5692
|
+
var right = rect.right - parseFloat(style.paddingRight || '0') - parseFloat(style.borderRightWidth || '0');
|
|
5693
|
+
if (right - left <= 0) continue;
|
|
5694
|
+
var all = box.querySelectorAll('*');
|
|
5695
|
+
for (var j = 0; j < all.length; j++) {
|
|
5696
|
+
var el = all[j];
|
|
5697
|
+
// Paged.js wraps each page's content in a div of its own that spans its layout columns
|
|
5698
|
+
if (${mode === "paged"} && el.parentElement === box && !el.className) continue;
|
|
5699
|
+
if (el.closest('.formfeed-chrome, .ff-running-header, .ff-running-footer, script, style, head')) continue;
|
|
5700
|
+
var over = el.getBoundingClientRect().right - right;
|
|
5701
|
+
if (over <= 1 || (worst && over <= worst.over)) continue;
|
|
5702
|
+
if (getComputedStyle(el).position === 'fixed' || clipped(el, box)) continue;
|
|
5703
|
+
worst = { over: over, width: right - left, el: el };
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
var overflow = null;
|
|
5707
|
+
if (worst) {
|
|
5708
|
+
var ref = document.createElement('div');
|
|
5709
|
+
ref.style.cssText = 'position:absolute;visibility:hidden;width:100mm;height:0';
|
|
5710
|
+
document.body.appendChild(ref);
|
|
5711
|
+
var pxPerMm = ref.getBoundingClientRect().width / 100 || 1;
|
|
5712
|
+
ref.remove();
|
|
5713
|
+
var annotated = worst.el.closest('[data-ff-src]');
|
|
5714
|
+
overflow = {
|
|
5715
|
+
overflowMm: Math.round((worst.over / pxPerMm) * 10) / 10,
|
|
5716
|
+
scale: Math.round((worst.width / (worst.width + worst.over)) * 1000) / 1000,
|
|
5717
|
+
tag: worst.el.tagName.toLowerCase(),
|
|
5718
|
+
src: annotated ? annotated.getAttribute('data-ff-src') : null,
|
|
5719
|
+
};
|
|
5720
|
+
}
|
|
5721
|
+
parent.postMessage({ type: 'formfeed:overflow', overflow: overflow }, '*');
|
|
5722
|
+
} catch (e) {}
|
|
5723
|
+
};
|
|
5724
|
+
window.formfeedCheckOverflow = measure;
|
|
5725
|
+
${mode === "flow" ? "window.addEventListener('load', function () { (document.fonts ? document.fonts.ready : Promise.resolve()).then(measure); });" : ""}
|
|
5726
|
+
})();
|
|
5727
|
+
</script>`;
|
|
5728
|
+
}
|
|
5372
5729
|
var zoomGestureScript = `<script>
|
|
5373
5730
|
(function () {
|
|
5374
5731
|
var say = function (message) { try { parent.postMessage(message, '*'); } catch (e) {} };
|
|
@@ -5415,7 +5772,7 @@ body.formfeed-preview { position: relative; box-sizing: border-box; width: ${pap
|
|
|
5415
5772
|
</style>` : `<style data-formfeed="preview">html { background: #e5e7eb; } body.formfeed-preview { margin: 24px auto; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2); }</style>`;
|
|
5416
5773
|
const header = headerHtml ? `<div class="formfeed-chrome" style="${pdf ? flowChromeStyle(settings, "header") : `${chromeBoxStyle(settings, "header")}margin-bottom:8px`}">${headerHtml}</div>` : "";
|
|
5417
5774
|
const footer = footerHtml ? `<div class="formfeed-chrome" style="${pdf ? flowChromeStyle(settings, "footer") : `${chromeBoxStyle(settings, "footer")}margin-top:8px`}">${pageNumberSpans(footerHtml, "1", "1")}</div>` : "";
|
|
5418
|
-
return intoHead(draft.document, `${chrome}${inspectScript}${zoomGestureScript}`).replace(/(<body[^>]*>)/, `$1${header}`).replace("</body>", `${footer}</body>`);
|
|
5775
|
+
return intoHead(draft.document, `${chrome}${inspectScript}${zoomGestureScript}${pdf ? overflowScript("flow") : ""}`).replace(/(<body[^>]*>)/, `$1${header}`).replace("</body>", `${footer}</body>`);
|
|
5419
5776
|
}
|
|
5420
5777
|
function pagedDocument(draft, options) {
|
|
5421
5778
|
const { headerHtml, footerHtml, settings } = draft;
|
|
@@ -5437,13 +5794,13 @@ html { background: #e5e7eb; }
|
|
|
5437
5794
|
`;
|
|
5438
5795
|
const selectorGuard = `<script>(function(){var q=DocumentFragment.prototype.querySelectorAll;DocumentFragment.prototype.querySelectorAll=function(s){try{return q.call(this,s);}catch(e){console.warn('formfeed paged preview: skipped a selector Paged.js could not use:',s);return q.call(this,':not(*)');}};})();</script>`;
|
|
5439
5796
|
const boxSizingGuard = `<script>(function(){var rule='.pagedjs_pagebox *';var o=new MutationObserver(function(ms){ms.forEach(function(m){m.addedNodes.forEach(function(n){if(n.nodeName==='STYLE'&&n.textContent&&n.textContent.indexOf(rule)>=0){n.textContent=n.textContent.split(rule).join('.pagedjs_pagebox [class*="pagedjs_"]');o.disconnect();}});});});o.observe(document.documentElement,{childList:true,subtree:true});})();</script>`;
|
|
5440
|
-
const config = `<script>window.PagedConfig = { auto: true, after: function (flow) { try { if (window.formfeedDrawCharts) window.formfeedDrawCharts(); } catch (e) {} try { parent.postMessage({ type: 'formfeed:pages', pages: flow.total }, '*'); } catch (e) {} } };</script>`;
|
|
5797
|
+
const config = `<script>window.PagedConfig = { auto: true, after: function (flow) { try { if (window.formfeedDrawCharts) window.formfeedDrawCharts(); } catch (e) {} try { parent.postMessage({ type: 'formfeed:pages', pages: flow.total }, '*'); } catch (e) {} if (window.formfeedCheckOverflow) window.formfeedCheckOverflow(); } };</script>`;
|
|
5441
5798
|
const script = `<script src="${options.pagedScriptUrl}"></script>`;
|
|
5442
5799
|
const header = headerHtml ? `<div class="ff-running-header">${pageNumberSpans(headerHtml, '<span class="ff-page-no"></span>', '<span class="ff-page-total"></span>')}</div>` : "";
|
|
5443
5800
|
const footer = footerHtml ? `<div class="ff-running-footer">${pageNumberSpans(footerHtml, '<span class="ff-page-no"></span>', '<span class="ff-page-total"></span>')}</div>` : "";
|
|
5444
5801
|
return intoHead(
|
|
5445
5802
|
draft.document,
|
|
5446
|
-
`<style data-formfeed="paged">${runningCss}</style>${selectorGuard}${boxSizingGuard}${config}${script}${inspectScript}${pageNavScript}${zoomGestureScript}`
|
|
5803
|
+
`<style data-formfeed="paged">${runningCss}</style>${selectorGuard}${boxSizingGuard}${overflowScript("paged")}${config}${script}${inspectScript}${pageNavScript}${zoomGestureScript}`
|
|
5447
5804
|
).replace(/(<body[^>]*>)/, `$1${header}${footer}`);
|
|
5448
5805
|
}
|
|
5449
5806
|
|