@formfeed/mcp 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/formfeed-mcp.mjs +638 -97
- package/package.json +1 -1
package/bin/formfeed-mcp.mjs
CHANGED
|
@@ -17445,16 +17445,16 @@ var require_liquid_node = __commonJS({
|
|
|
17445
17445
|
this.trimLeft = false;
|
|
17446
17446
|
this.trimRight = false;
|
|
17447
17447
|
const tl = input[contentBegin] === "-";
|
|
17448
|
-
const
|
|
17448
|
+
const tr2 = input[contentEnd - 1] === "-";
|
|
17449
17449
|
let l3 = tl ? contentBegin + 1 : contentBegin;
|
|
17450
|
-
let r =
|
|
17450
|
+
let r = tr2 ? contentEnd - 1 : contentEnd;
|
|
17451
17451
|
while (l3 < r && TYPES[input.charCodeAt(l3)] & BLANK)
|
|
17452
17452
|
l3++;
|
|
17453
17453
|
while (r > l3 && TYPES[input.charCodeAt(r - 1)] & BLANK)
|
|
17454
17454
|
r--;
|
|
17455
17455
|
this.contentRange = [l3, r];
|
|
17456
17456
|
this.trimLeft = tl || trimLeft2;
|
|
17457
|
-
this.trimRight =
|
|
17457
|
+
this.trimRight = tr2 || trimRight2;
|
|
17458
17458
|
}
|
|
17459
17459
|
get content() {
|
|
17460
17460
|
return this.input.slice(this.contentRange[0], this.contentRange[1]);
|
|
@@ -77456,6 +77456,11 @@ var currencyValues6 = { currency: (
|
|
|
77456
77456
|
Object.keys(nl2)
|
|
77457
77457
|
) };
|
|
77458
77458
|
|
|
77459
|
+
// packages/engine/src/lib/helpers/office.ts
|
|
77460
|
+
function officeUnsupported(name) {
|
|
77461
|
+
return `${name}() is not supported in office templates`;
|
|
77462
|
+
}
|
|
77463
|
+
|
|
77459
77464
|
// packages/engine/src/lib/helpers/format.ts
|
|
77460
77465
|
var dateLocales = {
|
|
77461
77466
|
de,
|
|
@@ -77738,7 +77743,8 @@ var formatHelpers = [
|
|
|
77738
77743
|
example: "{{ address | nl2br }}",
|
|
77739
77744
|
category: "text"
|
|
77740
77745
|
},
|
|
77741
|
-
|
|
77746
|
+
// office templates turn line breaks into breaks themselves, and escape the text once
|
|
77747
|
+
fn: (ctx, value) => ctx.mode === "office" ? str(value) : escapeHtml(str(value)).replace(/\r?\n/g, "<br>\n")
|
|
77742
77748
|
},
|
|
77743
77749
|
{
|
|
77744
77750
|
name: "markdown",
|
|
@@ -77749,11 +77755,14 @@ var formatHelpers = [
|
|
|
77749
77755
|
example: "{{ notes | markdown }}",
|
|
77750
77756
|
category: "text"
|
|
77751
77757
|
},
|
|
77752
|
-
fn: (
|
|
77753
|
-
|
|
77754
|
-
|
|
77755
|
-
|
|
77756
|
-
|
|
77758
|
+
fn: (ctx, value) => {
|
|
77759
|
+
if (ctx.mode === "office") throw new Error(officeUnsupported("markdown"));
|
|
77760
|
+
return f.parse(str(value), {
|
|
77761
|
+
async: false,
|
|
77762
|
+
gfm: true,
|
|
77763
|
+
breaks: false
|
|
77764
|
+
});
|
|
77765
|
+
}
|
|
77757
77766
|
},
|
|
77758
77767
|
{
|
|
77759
77768
|
name: "safe",
|
|
@@ -77831,7 +77840,7 @@ var formatHelpers = [
|
|
|
77831
77840
|
example: "{{ pageBreak() }}",
|
|
77832
77841
|
category: "document"
|
|
77833
77842
|
},
|
|
77834
|
-
fn: () => '<div class="page-break" style="break-after:page"></div>'
|
|
77843
|
+
fn: (ctx) => ctx.drawing ? ctx.drawing({ kind: "page-break" }) : '<div class="page-break" style="break-after:page"></div>'
|
|
77835
77844
|
}
|
|
77836
77845
|
];
|
|
77837
77846
|
|
|
@@ -77888,7 +77897,12 @@ var codeHelpers = [
|
|
|
77888
77897
|
example: '<img src="{{ qrcode(order.url, { size: 120 }) }}">',
|
|
77889
77898
|
category: "code"
|
|
77890
77899
|
},
|
|
77891
|
-
fn: (
|
|
77900
|
+
fn: (ctx, value, options) => {
|
|
77901
|
+
const o = options ?? {};
|
|
77902
|
+
const svg = qrSvg(String(value ?? ""), o);
|
|
77903
|
+
if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, width: o.size, height: o.size, alt: "QR code" });
|
|
77904
|
+
return svgDataUri(svg);
|
|
77905
|
+
}
|
|
77892
77906
|
},
|
|
77893
77907
|
{
|
|
77894
77908
|
name: "barcode",
|
|
@@ -77898,7 +77912,7 @@ var codeHelpers = [
|
|
|
77898
77912
|
example: `<img src="{{ barcode(item.sku, { type: 'code128' }) }}">`,
|
|
77899
77913
|
category: "code"
|
|
77900
77914
|
},
|
|
77901
|
-
fn: (
|
|
77915
|
+
fn: (ctx, value, options) => {
|
|
77902
77916
|
const o = options ?? {};
|
|
77903
77917
|
const bcid = barcodeTypes[String(o.type ?? "code128").toLowerCase()] ?? "code128";
|
|
77904
77918
|
const svg = ToSVG({
|
|
@@ -77910,6 +77924,7 @@ var codeHelpers = [
|
|
|
77910
77924
|
includetext: o.text !== false,
|
|
77911
77925
|
textxalign: "center"
|
|
77912
77926
|
});
|
|
77927
|
+
if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, alt: `Barcode ${String(value ?? "")}` });
|
|
77913
77928
|
return svgDataUri(svg);
|
|
77914
77929
|
}
|
|
77915
77930
|
},
|
|
@@ -77926,7 +77941,7 @@ var codeHelpers = [
|
|
|
77926
77941
|
// hash (`{{epcQr name=… iban=… size=200}}`), and Liquid, which cannot build an object literal,
|
|
77927
77942
|
// keyword arguments (`{{ company.iban | epcQr: name: company.name, amount: invoice.total }}`,
|
|
77928
77943
|
// where a string input is the IBAN). They are merged and split by field name.
|
|
77929
|
-
fn: (
|
|
77944
|
+
fn: (ctx, input, options) => {
|
|
77930
77945
|
const fields = {
|
|
77931
77946
|
...input && typeof input === "object" ? input : typeof input === "string" && input ? { iban: input } : {},
|
|
77932
77947
|
...options ?? {}
|
|
@@ -77935,12 +77950,10 @@ var codeHelpers = [
|
|
|
77935
77950
|
const qr = {};
|
|
77936
77951
|
for (const [key, value] of Object.entries(fields))
|
|
77937
77952
|
(EPC_FIELDS.has(key) ? payment : qr)[key] = value;
|
|
77938
|
-
|
|
77939
|
-
|
|
77940
|
-
|
|
77941
|
-
|
|
77942
|
-
})
|
|
77943
|
-
);
|
|
77953
|
+
const o = { ecc: "M", ...qr };
|
|
77954
|
+
const svg = qrSvg(epcPayload(payment), o);
|
|
77955
|
+
if (ctx.drawing) return ctx.drawing({ kind: "svg", svg, width: o.size, height: o.size, alt: "GiroCode" });
|
|
77956
|
+
return svgDataUri(svg);
|
|
77944
77957
|
}
|
|
77945
77958
|
}
|
|
77946
77959
|
];
|
|
@@ -78130,6 +78143,29 @@ var collectionHelpers = [
|
|
|
78130
78143
|
];
|
|
78131
78144
|
|
|
78132
78145
|
// packages/engine/src/lib/helpers/document.ts
|
|
78146
|
+
var CHART_PALETTE = ["#3E63DD", "#12A594", "#E5484D", "#F76B15", "#8E4EC6", "#FFC53D", "#0090FF", "#978365"];
|
|
78147
|
+
var SLICED = /* @__PURE__ */ new Set(["pie", "doughnut", "polarArea"]);
|
|
78148
|
+
var colour = (value) => typeof value === "string" && value ? value : Array.isArray(value) && value.length ? value.map(String) : void 0;
|
|
78149
|
+
function chartDataFromPlain(spec) {
|
|
78150
|
+
const type = spec.type ?? "bar";
|
|
78151
|
+
const series = Array.isArray(spec.series) ? spec.series.map((s) => asObject(s)) : [{ label: spec.label, values: spec.values, color: spec.color }];
|
|
78152
|
+
const labels = Array.isArray(spec.labels) ? spec.labels : [];
|
|
78153
|
+
const datasets = series.map((s, index) => {
|
|
78154
|
+
const data = (Array.isArray(s.values) ? s.values : []).map((v2) => {
|
|
78155
|
+
const n = toNumber(v2);
|
|
78156
|
+
return Number.isFinite(n) ? n : null;
|
|
78157
|
+
});
|
|
78158
|
+
const own = colour(s.color);
|
|
78159
|
+
const fill = SLICED.has(type) ? own ?? labels.map((_2, i) => CHART_PALETTE[i % CHART_PALETTE.length]) : own ?? CHART_PALETTE[index % CHART_PALETTE.length];
|
|
78160
|
+
const dataset = { data, backgroundColor: fill, borderColor: fill };
|
|
78161
|
+
if (s.label !== void 0 && s.label !== null && s.label !== "") dataset["label"] = String(s.label);
|
|
78162
|
+
if (type === "line") dataset["fill"] = false;
|
|
78163
|
+
return dataset;
|
|
78164
|
+
});
|
|
78165
|
+
return { labels, datasets };
|
|
78166
|
+
}
|
|
78167
|
+
var isPlain = (spec) => spec.data === void 0 && (Array.isArray(spec.values) || Array.isArray(spec.series));
|
|
78168
|
+
var asObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
78133
78169
|
var css = (value) => {
|
|
78134
78170
|
if (value === void 0 || value === null || value === "") return void 0;
|
|
78135
78171
|
const n = toNumber(value);
|
|
@@ -78139,7 +78175,13 @@ function chartPayload(spec) {
|
|
|
78139
78175
|
const options = { ...spec.options ?? {} };
|
|
78140
78176
|
options["animation"] = false;
|
|
78141
78177
|
options["responsive"] = false;
|
|
78142
|
-
return { type: spec.type ?? "bar", data: spec.data ?? {}, options };
|
|
78178
|
+
if (!isPlain(spec)) return { type: spec.type ?? "bar", data: spec.data ?? {}, options };
|
|
78179
|
+
const data = chartDataFromPlain(spec);
|
|
78180
|
+
const unnamed = data.datasets.length === 1 && data.datasets[0]?.["label"] === void 0;
|
|
78181
|
+
const plugins = asObject(options["plugins"]);
|
|
78182
|
+
if (unnamed && !SLICED.has(spec.type ?? "bar") && plugins["legend"] === void 0)
|
|
78183
|
+
options["plugins"] = { ...plugins, legend: { display: false } };
|
|
78184
|
+
return { type: spec.type ?? "bar", data, options };
|
|
78143
78185
|
}
|
|
78144
78186
|
function chartMarkup(spec, id) {
|
|
78145
78187
|
const width = toNumber(spec.width ?? 480) || 480;
|
|
@@ -78158,18 +78200,24 @@ function imageMarkup(url, options = {}) {
|
|
|
78158
78200
|
const style = styles ? ` style="${styles}"` : "";
|
|
78159
78201
|
return `<img src="${escapeHtml(url)}" alt="${escapeHtml(options.alt ?? "")}"${cls}${style}>`;
|
|
78160
78202
|
}
|
|
78161
|
-
|
|
78203
|
+
function resolveAsset(base, url) {
|
|
78204
|
+
if (!base || /^[a-z][a-z0-9+.-]*:/i.test(url)) return url;
|
|
78205
|
+
return `${base.replace(/\/$/, "")}/${url.replace(/^\//, "")}`;
|
|
78206
|
+
}
|
|
78162
78207
|
var documentHelpers = [
|
|
78163
78208
|
{
|
|
78164
78209
|
name: "chart",
|
|
78165
78210
|
html: true,
|
|
78166
78211
|
doc: {
|
|
78167
|
-
signature: "chart({ type, data, options, width, height })",
|
|
78168
|
-
description: "Draws a Chart.js chart (bar, line, pie, doughnut, radar, \u2026) from a
|
|
78169
|
-
example: "{{ chart({ type: '
|
|
78212
|
+
signature: "chart({ type, labels, values, label, color } | { type, labels, series } | { type, data, options }, { width, height })",
|
|
78213
|
+
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.",
|
|
78214
|
+
example: "{{ chart({ type: 'line', labels: ['Q1', 'Q2', 'Q3'], values: sales.quarters, width: 480 }) }}",
|
|
78170
78215
|
category: "document"
|
|
78171
78216
|
},
|
|
78172
|
-
fn: (
|
|
78217
|
+
fn: (ctx, spec, extra) => {
|
|
78218
|
+
if (ctx.mode === "office") throw new Error(officeUnsupported("chart"));
|
|
78219
|
+
return chartMarkup({ ...asObject(spec), ...asObject(extra) });
|
|
78220
|
+
}
|
|
78173
78221
|
},
|
|
78174
78222
|
{
|
|
78175
78223
|
name: "image",
|
|
@@ -78180,7 +78228,12 @@ var documentHelpers = [
|
|
|
78180
78228
|
example: "{{ image(product.photo, { width: 120, height: 80, fit: 'cover' }) }}",
|
|
78181
78229
|
category: "document"
|
|
78182
78230
|
},
|
|
78183
|
-
fn: (
|
|
78231
|
+
fn: (ctx, url, options) => {
|
|
78232
|
+
const o = asObject(options);
|
|
78233
|
+
if (ctx.drawing)
|
|
78234
|
+
return ctx.drawing({ kind: "url", url: resolveAsset(ctx.assetBaseUrl, String(url ?? "")), width: o.width, height: o.height, alt: o.alt });
|
|
78235
|
+
return imageMarkup(String(url ?? ""), o);
|
|
78236
|
+
}
|
|
78184
78237
|
}
|
|
78185
78238
|
];
|
|
78186
78239
|
|
|
@@ -78528,6 +78581,8 @@ var builtinHelpers2 = /* @__PURE__ */ new Set([
|
|
|
78528
78581
|
"helperMissing"
|
|
78529
78582
|
]);
|
|
78530
78583
|
var builtinRoots = /* @__PURE__ */ new Set([
|
|
78584
|
+
// the organisation's brand kit (spec 18), merged under the data
|
|
78585
|
+
"brand",
|
|
78531
78586
|
"this",
|
|
78532
78587
|
"@index",
|
|
78533
78588
|
"@key",
|
|
@@ -78536,6 +78591,11 @@ var builtinRoots = /* @__PURE__ */ new Set([
|
|
|
78536
78591
|
"@root",
|
|
78537
78592
|
"lookup"
|
|
78538
78593
|
]);
|
|
78594
|
+
function withBrand(data, ctx) {
|
|
78595
|
+
const root = data ?? {};
|
|
78596
|
+
if (!ctx.brand || typeof root !== "object" || Array.isArray(root)) return root;
|
|
78597
|
+
return { brand: ctx.brand, ...root };
|
|
78598
|
+
}
|
|
78539
78599
|
function createInstance(ctx) {
|
|
78540
78600
|
const hb = import_handlebars.default.create();
|
|
78541
78601
|
for (const [name, { fn, html }] of ctx.helpers.bind(ctx)) {
|
|
@@ -78806,7 +78866,8 @@ var HandlebarsCompiled = class {
|
|
|
78806
78866
|
hb,
|
|
78807
78867
|
template: hb.compile(this.source, {
|
|
78808
78868
|
strict: false,
|
|
78809
|
-
|
|
78869
|
+
// office text is escaped when its markup is restored (office/template-text.ts)
|
|
78870
|
+
noEscape: ctx.mode === "office",
|
|
78810
78871
|
preventIndent: true
|
|
78811
78872
|
})
|
|
78812
78873
|
};
|
|
@@ -78835,7 +78896,7 @@ var HandlebarsCompiled = class {
|
|
|
78835
78896
|
};
|
|
78836
78897
|
collect(this.source, 0);
|
|
78837
78898
|
try {
|
|
78838
|
-
const out = template(data
|
|
78899
|
+
const out = template(withBrand(data, ctx), {
|
|
78839
78900
|
partials,
|
|
78840
78901
|
allowProtoPropertiesByDefault: false,
|
|
78841
78902
|
allowProtoMethodsByDefault: false
|
|
@@ -79060,6 +79121,7 @@ var nunjucksBuiltinFilters = /* @__PURE__ */ new Set([
|
|
|
79060
79121
|
"urlize",
|
|
79061
79122
|
"wordcount"
|
|
79062
79123
|
]);
|
|
79124
|
+
var jinjaLiterals = /* @__PURE__ */ new Set(["True", "False", "None"]);
|
|
79063
79125
|
var builtinRoots2 = /* @__PURE__ */ new Set([
|
|
79064
79126
|
"loop",
|
|
79065
79127
|
"caller",
|
|
@@ -79071,6 +79133,8 @@ var builtinRoots2 = /* @__PURE__ */ new Set([
|
|
|
79071
79133
|
"varargs",
|
|
79072
79134
|
"kwargs",
|
|
79073
79135
|
"namespace",
|
|
79136
|
+
// the organisation's brand kit (spec 18), a global beside the data
|
|
79137
|
+
"brand",
|
|
79074
79138
|
// Jinja2's spelling of the literals, which the compat layer resolves at runtime
|
|
79075
79139
|
"True",
|
|
79076
79140
|
"False",
|
|
@@ -79271,7 +79335,8 @@ function createEnvironment(ctx) {
|
|
|
79271
79335
|
const env = new import_nunjucks.default.Environment(
|
|
79272
79336
|
new PartialLoader(ctx.partials),
|
|
79273
79337
|
{
|
|
79274
|
-
|
|
79338
|
+
// office text is escaped when its markup is restored, so escaping here would double it
|
|
79339
|
+
autoescape: ctx.mode !== "office",
|
|
79275
79340
|
throwOnUndefined: false,
|
|
79276
79341
|
trimBlocks: false,
|
|
79277
79342
|
lstripBlocks: false
|
|
@@ -79286,6 +79351,7 @@ function createEnvironment(ctx) {
|
|
|
79286
79351
|
env.addGlobal(name, wrapped);
|
|
79287
79352
|
}
|
|
79288
79353
|
env.addGlobal("namespace", namespace);
|
|
79354
|
+
if (ctx.brand) env.addGlobal("brand", ctx.brand);
|
|
79289
79355
|
installJinjaFilters(env);
|
|
79290
79356
|
return env;
|
|
79291
79357
|
}
|
|
@@ -79340,6 +79406,7 @@ function walk2(node, w) {
|
|
|
79340
79406
|
case "Symbol":
|
|
79341
79407
|
case "LookupVal": {
|
|
79342
79408
|
const p = pathOf(node);
|
|
79409
|
+
if (node.typename === "Symbol" && jinjaLiterals.has(String(node.value))) return;
|
|
79343
79410
|
if (p) {
|
|
79344
79411
|
const root = p.path[0] ?? "";
|
|
79345
79412
|
const bound = inScope(w, root);
|
|
@@ -79706,6 +79773,8 @@ var liquidBuiltinFilters = /* @__PURE__ */ new Set([
|
|
|
79706
79773
|
"time_zone"
|
|
79707
79774
|
]);
|
|
79708
79775
|
var builtinRoots3 = /* @__PURE__ */ new Set([
|
|
79776
|
+
// the organisation's brand kit (spec 18), a global beside the data
|
|
79777
|
+
"brand",
|
|
79709
79778
|
"forloop",
|
|
79710
79779
|
"tablerowloop",
|
|
79711
79780
|
"true",
|
|
@@ -79771,6 +79840,8 @@ function createLiquid(ctx) {
|
|
|
79771
79840
|
extname: "",
|
|
79772
79841
|
root: [""],
|
|
79773
79842
|
fs: partialFs(ctx.partials),
|
|
79843
|
+
// scope data shadows globals, so `data.brand` still wins
|
|
79844
|
+
globals: ctx.brand ? { brand: ctx.brand } : {},
|
|
79774
79845
|
renderLimit: ctx.limits.ms,
|
|
79775
79846
|
memoryLimit: ctx.limits.outputBytes * 4,
|
|
79776
79847
|
parseLimit: 2e6
|
|
@@ -80041,6 +80112,26 @@ function getEngine(id) {
|
|
|
80041
80112
|
return engine;
|
|
80042
80113
|
}
|
|
80043
80114
|
|
|
80115
|
+
// packages/engine/src/lib/schema-diff.ts
|
|
80116
|
+
var LOWER_BOUNDS = ["minimum", "exclusiveMinimum", "minLength", "minItems", "minProperties"];
|
|
80117
|
+
var UPPER_BOUNDS = ["maximum", "exclusiveMaximum", "maxLength", "maxItems", "maxProperties"];
|
|
80118
|
+
var ANALYSED = /* @__PURE__ */ new Set([
|
|
80119
|
+
"type",
|
|
80120
|
+
"properties",
|
|
80121
|
+
"required",
|
|
80122
|
+
"additionalProperties",
|
|
80123
|
+
"items",
|
|
80124
|
+
"enum",
|
|
80125
|
+
"const",
|
|
80126
|
+
"format",
|
|
80127
|
+
"pattern",
|
|
80128
|
+
"$ref",
|
|
80129
|
+
"$defs",
|
|
80130
|
+
"definitions",
|
|
80131
|
+
...LOWER_BOUNDS,
|
|
80132
|
+
...UPPER_BOUNDS
|
|
80133
|
+
]);
|
|
80134
|
+
|
|
80044
80135
|
// packages/engine/src/lib/schema.ts
|
|
80045
80136
|
var isoDateTime = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
|
|
80046
80137
|
var isoDate = /^\d{4}-\d{2}-\d{2}$/;
|
|
@@ -80131,9 +80222,12 @@ function inferSchema(sample) {
|
|
|
80131
80222
|
};
|
|
80132
80223
|
}
|
|
80133
80224
|
|
|
80225
|
+
// packages/engine/src/lib/output.ts
|
|
80226
|
+
var outputFormats = ["pdf", "png", "jpg", "webp", "docx", "pptx"];
|
|
80227
|
+
|
|
80134
80228
|
// packages/engine/src/lib/snippets.ts
|
|
80135
80229
|
var loop = (engine, item, list2, body) => engine === "handlebars" ? `{{#each ${list2}}}
|
|
80136
|
-
${body.replace(
|
|
80230
|
+
${body.replace(new RegExp(`(?<![\\w.])${item}\\.`, "g"), "")}
|
|
80137
80231
|
{{/each}}` : `{% for ${item} in ${list2} %}
|
|
80138
80232
|
${body}
|
|
80139
80233
|
{% endfor %}`;
|
|
@@ -80143,6 +80237,15 @@ var val = (engine, expr, filter) => {
|
|
|
80143
80237
|
return `{{${filter.split("(")[0]} ${expr}${filter.includes("(") ? " " + filter.slice(filter.indexOf("(") + 1, -1) : ""}}}`;
|
|
80144
80238
|
return engine === "liquid" ? `{{ ${expr} | ${filter.replace("(", ": ").replace(")", "")} }}` : `{{ ${expr} | ${filter} }}`;
|
|
80145
80239
|
};
|
|
80240
|
+
var ifElse = (engine, cond, then, otherwise) => engine === "handlebars" ? `{{#if ${cond}}}${then}{{else}}${otherwise}{{/if}}` : `{% if ${cond} %}${then}{% else %}${otherwise}{% endif %}`;
|
|
80241
|
+
var tr = (engine, key, params = {}) => {
|
|
80242
|
+
const entries = Object.entries(params);
|
|
80243
|
+
if (engine === "handlebars")
|
|
80244
|
+
return `{{t '${key}'${entries.map(([k2, v2]) => ` ${k2}=${v2}`).join("")}}}`;
|
|
80245
|
+
if (engine === "liquid")
|
|
80246
|
+
return `{{ '${key}' | t${entries.length ? ": " + entries.map(([k2, v2]) => `${k2}: ${v2}`).join(", ") : ""} }}`;
|
|
80247
|
+
return `{{ t('${key}'${entries.length ? `, { ${entries.map(([k2, v2]) => `${k2}: ${v2}`).join(", ")} }` : ""}) }}`;
|
|
80248
|
+
};
|
|
80146
80249
|
function forAll(build) {
|
|
80147
80250
|
return {
|
|
80148
80251
|
jinja2: build("jinja2"),
|
|
@@ -80150,14 +80253,101 @@ function forAll(build) {
|
|
|
80150
80253
|
handlebars: build("handlebars")
|
|
80151
80254
|
};
|
|
80152
80255
|
}
|
|
80256
|
+
var rule = "var(--brand-color-primary, #222)";
|
|
80153
80257
|
var snippets = [
|
|
80258
|
+
{
|
|
80259
|
+
id: "letterhead",
|
|
80260
|
+
group: "letter",
|
|
80261
|
+
title: "Letterhead",
|
|
80262
|
+
description: "Logo from the brand kit, or the organisation name when no logo is set.",
|
|
80263
|
+
source: forAll(
|
|
80264
|
+
(e) => `<header class="ff-letterhead">
|
|
80265
|
+
${ifElse(e, "brand.logo.primary", `<img class="logo" src="${val(e, "brand.logo.primary")}" alt="${val(e, "brand.name")}">`, `<strong class="name">${val(e, "brand.name")}</strong>`)}
|
|
80266
|
+
</header>$0`
|
|
80267
|
+
),
|
|
80268
|
+
css: `.ff-letterhead { display: flex; justify-content: flex-end; align-items: center; min-height: 48px; margin-bottom: 24px; padding-bottom: 8px; border-bottom: 2px solid ${rule}; }
|
|
80269
|
+
.ff-letterhead .logo { max-height: 56px; max-width: 200px; }
|
|
80270
|
+
.ff-letterhead .name { font-family: var(--brand-font-heading, inherit); font-size: 18pt; color: var(--brand-color-primary, inherit); }`
|
|
80271
|
+
},
|
|
80272
|
+
{
|
|
80273
|
+
id: "window-address",
|
|
80274
|
+
group: "letter",
|
|
80275
|
+
kinds: ["pdf"],
|
|
80276
|
+
title: "Window envelope address",
|
|
80277
|
+
description: "Return address and recipient in the 85 \xD7 45 mm field of a DIN 5008 window envelope.",
|
|
80278
|
+
source: forAll(
|
|
80279
|
+
(e) => `<div class="ff-window">
|
|
80280
|
+
<div class="sender">${val(e, "company.name")} \xB7 ${val(e, "company.street")} \xB7 ${val(e, "company.zip")} ${val(e, "company.city")}</div>
|
|
80281
|
+
<address>
|
|
80282
|
+
<strong>${val(e, "customer.name")}</strong><br>
|
|
80283
|
+
${val(e, "customer.street")}<br>
|
|
80284
|
+
${val(e, "customer.zip")} ${val(e, "customer.city")}
|
|
80285
|
+
</address>
|
|
80286
|
+
</div>$0`
|
|
80287
|
+
),
|
|
80288
|
+
sampleData: {
|
|
80289
|
+
company: { name: "Fennlor Studio GmbH", street: "Musterstra\xDFe 1", zip: "12345", city: "Musterstadt" },
|
|
80290
|
+
customer: { name: "Olvarest GmbH", street: "Beispielweg 2", zip: "54321", city: "Beispielstadt" }
|
|
80291
|
+
},
|
|
80292
|
+
// Form B puts the field 45 mm below the top edge and 20 mm from the left; the page margins decide where it lands
|
|
80293
|
+
css: `.ff-window { width: 85mm; height: 45mm; box-sizing: border-box; padding: 0 5mm; overflow: hidden; }
|
|
80294
|
+
.ff-window .sender { height: 5mm; line-height: 5mm; margin-bottom: 3mm; font-size: 7pt; color: #555; border-bottom: 0.5pt solid #999; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
80295
|
+
.ff-window address { font-style: normal; font-size: 10pt; line-height: 1.35; }`
|
|
80296
|
+
},
|
|
80297
|
+
{
|
|
80298
|
+
id: "reference-block",
|
|
80299
|
+
group: "letter",
|
|
80300
|
+
title: "Reference block",
|
|
80301
|
+
description: "Customer number, invoice number and date as a right-aligned table; put it in a two-column layout to sit beside the address.",
|
|
80302
|
+
source: forAll(
|
|
80303
|
+
(e) => `<table class="ff-reference">
|
|
80304
|
+
<tr><th>${tr(e, "reference.customer")}</th><td>${val(e, "customer.number")}</td></tr>
|
|
80305
|
+
<tr><th>${tr(e, "reference.invoice")}</th><td>${val(e, "invoice.number")}</td></tr>
|
|
80306
|
+
<tr><th>${tr(e, "reference.date")}</th><td>${val(e, "invoice.date", "date('P')")}</td></tr>
|
|
80307
|
+
</table>$0`
|
|
80308
|
+
),
|
|
80309
|
+
sampleData: {
|
|
80310
|
+
customer: { number: "K-1042" },
|
|
80311
|
+
invoice: { number: "2026-0042", date: "2026-09-13" }
|
|
80312
|
+
},
|
|
80313
|
+
css: `.ff-reference { margin-left: auto; border-collapse: collapse; font-size: 9pt; }
|
|
80314
|
+
.ff-reference th { text-align: left; font-weight: normal; color: #555; padding: 1px 12px 1px 0; }
|
|
80315
|
+
.ff-reference td { text-align: right; padding: 1px 0; }`,
|
|
80316
|
+
i18n: {
|
|
80317
|
+
en: { "reference.customer": "Customer no.", "reference.invoice": "Invoice no.", "reference.date": "Date" },
|
|
80318
|
+
de: { "reference.customer": "Kundennummer", "reference.invoice": "Rechnungsnummer", "reference.date": "Datum" }
|
|
80319
|
+
}
|
|
80320
|
+
},
|
|
80321
|
+
{
|
|
80322
|
+
id: "invoice-heading",
|
|
80323
|
+
group: "invoice",
|
|
80324
|
+
title: "Invoice heading",
|
|
80325
|
+
description: "Invoice number as the title and the service period below it.",
|
|
80326
|
+
source: forAll(
|
|
80327
|
+
(e) => `<div class="ff-heading">
|
|
80328
|
+
<h1>${tr(e, "heading.title", { number: "invoice.number" })}</h1>
|
|
80329
|
+
<p>${tr(e, "heading.period")} ${val(e, "invoice.period_start", "date('P')")} \u2013 ${val(e, "invoice.period_end", "date('P')")}</p>
|
|
80330
|
+
</div>$0`
|
|
80331
|
+
),
|
|
80332
|
+
sampleData: {
|
|
80333
|
+
invoice: { number: "2026-0042", period_start: "2026-09-01", period_end: "2026-09-30" }
|
|
80334
|
+
},
|
|
80335
|
+
css: `.ff-heading { margin: 24px 0 16px; }
|
|
80336
|
+
.ff-heading h1 { font-family: var(--brand-font-heading, inherit); font-size: 18pt; color: var(--brand-color-primary, inherit); margin: 0 0 4px; }
|
|
80337
|
+
.ff-heading p { margin: 0; color: #555; }`,
|
|
80338
|
+
i18n: {
|
|
80339
|
+
en: { "heading.title": "Invoice {number}", "heading.period": "Service period:" },
|
|
80340
|
+
de: { "heading.title": "Rechnung {number}", "heading.period": "Leistungszeitraum:" }
|
|
80341
|
+
}
|
|
80342
|
+
},
|
|
80154
80343
|
{
|
|
80155
80344
|
id: "invoice-table",
|
|
80345
|
+
group: "invoice",
|
|
80156
80346
|
title: "Invoice table",
|
|
80157
80347
|
description: "Line items with description, quantity, unit price and line total.",
|
|
80158
80348
|
source: forAll(
|
|
80159
80349
|
(e) => `<table class="ff-lines">
|
|
80160
|
-
<thead><tr><th
|
|
80350
|
+
<thead><tr><th>${tr(e, "lines.description")}</th><th class="num">${tr(e, "lines.qty")}</th><th class="num">${tr(e, "lines.price")}</th><th class="num">${tr(e, "lines.total")}</th></tr></thead>
|
|
80161
80351
|
<tbody>
|
|
80162
80352
|
${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</td><td class="num">${val(e, "line.qty", "number(0)")}</td><td class="num">${val(e, "line.price", "money")}</td><td class="num">${val(e, "line.total", "money")}</td></tr>`)}
|
|
80163
80353
|
</tbody>
|
|
@@ -80174,17 +80364,32 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
80174
80364
|
css: `.ff-lines { width: 100%; border-collapse: collapse; font-size: 10pt; }
|
|
80175
80365
|
.ff-lines th, .ff-lines td { padding: 6px 8px; border-bottom: 1px solid #ddd; text-align: left; }
|
|
80176
80366
|
.ff-lines .num { text-align: right; white-space: nowrap; }
|
|
80177
|
-
.ff-lines thead th { border-bottom: 2px solid
|
|
80367
|
+
.ff-lines thead th { border-bottom: 2px solid ${rule}; }`,
|
|
80368
|
+
i18n: {
|
|
80369
|
+
en: {
|
|
80370
|
+
"lines.description": "Description",
|
|
80371
|
+
"lines.qty": "Qty",
|
|
80372
|
+
"lines.price": "Unit price",
|
|
80373
|
+
"lines.total": "Total"
|
|
80374
|
+
},
|
|
80375
|
+
de: {
|
|
80376
|
+
"lines.description": "Beschreibung",
|
|
80377
|
+
"lines.qty": "Menge",
|
|
80378
|
+
"lines.price": "Einzelpreis",
|
|
80379
|
+
"lines.total": "Gesamt"
|
|
80380
|
+
}
|
|
80381
|
+
}
|
|
80178
80382
|
},
|
|
80179
80383
|
{
|
|
80180
80384
|
id: "totals",
|
|
80385
|
+
group: "invoice",
|
|
80181
80386
|
title: "Totals block",
|
|
80182
80387
|
description: "Net, VAT and gross totals right-aligned under a table.",
|
|
80183
80388
|
source: forAll(
|
|
80184
80389
|
(e) => `<table class="ff-totals">
|
|
80185
|
-
<tr><td
|
|
80186
|
-
<tr><td
|
|
80187
|
-
<tr class="grand"><td
|
|
80390
|
+
<tr><td>${tr(e, "totals.net")}</td><td class="num">${val(e, "invoice.net", "money")}</td></tr>
|
|
80391
|
+
<tr><td>${tr(e, "totals.vat", { rate: "invoice.vat_rate" })}</td><td class="num">${val(e, "invoice.vat", "money")}</td></tr>
|
|
80392
|
+
<tr class="grand"><td>${tr(e, "totals.total")}</td><td class="num">${val(e, "invoice.total", "money")}</td></tr>
|
|
80188
80393
|
</table>$0`
|
|
80189
80394
|
),
|
|
80190
80395
|
sampleData: {
|
|
@@ -80193,10 +80398,103 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
80193
80398
|
css: `.ff-totals { margin-left: auto; margin-top: 12px; border-collapse: collapse; font-size: 10pt; }
|
|
80194
80399
|
.ff-totals td { padding: 4px 8px; }
|
|
80195
80400
|
.ff-totals .num { text-align: right; min-width: 90px; }
|
|
80196
|
-
.ff-totals .grand td { border-top: 2px solid
|
|
80401
|
+
.ff-totals .grand td { border-top: 2px solid ${rule}; font-weight: 600; }`,
|
|
80402
|
+
i18n: {
|
|
80403
|
+
en: {
|
|
80404
|
+
"totals.net": "Net",
|
|
80405
|
+
"totals.vat": "VAT {rate} %",
|
|
80406
|
+
"totals.total": "Total"
|
|
80407
|
+
},
|
|
80408
|
+
de: {
|
|
80409
|
+
"totals.net": "Netto",
|
|
80410
|
+
"totals.vat": "USt. {rate} %",
|
|
80411
|
+
"totals.total": "Gesamtbetrag"
|
|
80412
|
+
}
|
|
80413
|
+
}
|
|
80414
|
+
},
|
|
80415
|
+
{
|
|
80416
|
+
id: "payment-terms",
|
|
80417
|
+
group: "invoice",
|
|
80418
|
+
title: "Payment terms and bank details",
|
|
80419
|
+
description: "Amount, due date and the account to transfer it to.",
|
|
80420
|
+
source: forAll(
|
|
80421
|
+
(e) => `<div class="ff-payment">
|
|
80422
|
+
<p>${tr(e, "payment.transfer")} <strong>${val(e, "payment.amount", "money")}</strong> ${tr(e, "payment.until")} <strong>${val(e, "invoice.due_date", "date('P')")}</strong> ${tr(e, "payment.account")}</p>
|
|
80423
|
+
<table>
|
|
80424
|
+
<tr><th>${tr(e, "payment.holder")}</th><td>${val(e, "payment.name")}</td></tr>
|
|
80425
|
+
<tr><th>IBAN</th><td>${val(e, "payment.iban")}</td></tr>
|
|
80426
|
+
<tr><th>BIC</th><td>${val(e, "payment.bic")}</td></tr>
|
|
80427
|
+
<tr><th>${tr(e, "payment.reference")}</th><td>${val(e, "payment.reference")}</td></tr>
|
|
80428
|
+
</table>
|
|
80429
|
+
</div>$0`
|
|
80430
|
+
),
|
|
80431
|
+
sampleData: {
|
|
80432
|
+
invoice: { due_date: "2026-10-13" },
|
|
80433
|
+
payment: {
|
|
80434
|
+
name: "Fennlor Studio GmbH",
|
|
80435
|
+
iban: "DE00000000000000000000",
|
|
80436
|
+
bic: "XXXXDEXXXXX",
|
|
80437
|
+
amount: 3760.4,
|
|
80438
|
+
reference: "2026-0042"
|
|
80439
|
+
}
|
|
80440
|
+
},
|
|
80441
|
+
css: `.ff-payment { margin-top: 16px; font-size: 9.5pt; break-inside: avoid; }
|
|
80442
|
+
.ff-payment p { margin: 0 0 6px; }
|
|
80443
|
+
.ff-payment table { border-collapse: collapse; }
|
|
80444
|
+
.ff-payment th { text-align: left; font-weight: normal; color: #555; padding: 1px 16px 1px 0; }`,
|
|
80445
|
+
i18n: {
|
|
80446
|
+
en: {
|
|
80447
|
+
"payment.transfer": "Please transfer",
|
|
80448
|
+
"payment.until": "by",
|
|
80449
|
+
"payment.account": "to the following account:",
|
|
80450
|
+
"payment.holder": "Account holder",
|
|
80451
|
+
"payment.reference": "Reference"
|
|
80452
|
+
},
|
|
80453
|
+
de: {
|
|
80454
|
+
"payment.transfer": "Bitte \xFCberweisen Sie",
|
|
80455
|
+
"payment.until": "bis zum",
|
|
80456
|
+
"payment.account": "auf folgendes Konto:",
|
|
80457
|
+
"payment.holder": "Kontoinhaber",
|
|
80458
|
+
"payment.reference": "Verwendungszweck"
|
|
80459
|
+
}
|
|
80460
|
+
}
|
|
80461
|
+
},
|
|
80462
|
+
{
|
|
80463
|
+
id: "small-business-note",
|
|
80464
|
+
group: "invoice",
|
|
80465
|
+
title: "Small business VAT note",
|
|
80466
|
+
description: "States that no VAT is charged under \xA7 19 UStG (Kleinunternehmerregelung).",
|
|
80467
|
+
source: forAll((e) => `<p class="ff-note">${tr(e, "vat.small_business")}</p>$0`),
|
|
80468
|
+
css: `.ff-note { margin-top: 12px; font-size: 9pt; color: #444; }`,
|
|
80469
|
+
i18n: {
|
|
80470
|
+
en: { "vat.small_business": "No VAT is charged under the small business scheme of \xA7 19 UStG." },
|
|
80471
|
+
de: { "vat.small_business": "Gem\xE4\xDF \xA7 19 UStG wird keine Umsatzsteuer berechnet." }
|
|
80472
|
+
}
|
|
80473
|
+
},
|
|
80474
|
+
{
|
|
80475
|
+
id: "reverse-charge-note",
|
|
80476
|
+
group: "invoice",
|
|
80477
|
+
title: "Reverse charge note",
|
|
80478
|
+
description: "EU business customers: the recipient owes the VAT, with their VAT ID.",
|
|
80479
|
+
source: forAll(
|
|
80480
|
+
(e) => `<p class="ff-note">${tr(e, "vat.reverse_charge")} ${tr(e, "vat.customer_id")} ${val(e, "customer.vat_id")}</p>$0`
|
|
80481
|
+
),
|
|
80482
|
+
sampleData: { customer: { vat_id: "ATU00000000" } },
|
|
80483
|
+
css: `.ff-note { margin-top: 12px; font-size: 9pt; color: #444; }`,
|
|
80484
|
+
i18n: {
|
|
80485
|
+
en: {
|
|
80486
|
+
"vat.reverse_charge": "Reverse charge: the recipient of the service is liable for VAT.",
|
|
80487
|
+
"vat.customer_id": "Customer VAT ID:"
|
|
80488
|
+
},
|
|
80489
|
+
de: {
|
|
80490
|
+
"vat.reverse_charge": "Steuerschuldnerschaft des Leistungsempf\xE4ngers.",
|
|
80491
|
+
"vat.customer_id": "USt-IdNr. des Leistungsempf\xE4ngers:"
|
|
80492
|
+
}
|
|
80493
|
+
}
|
|
80197
80494
|
},
|
|
80198
80495
|
{
|
|
80199
80496
|
id: "address-block",
|
|
80497
|
+
group: "letter",
|
|
80200
80498
|
title: "Address block",
|
|
80201
80499
|
description: "Recipient name and postal address lines.",
|
|
80202
80500
|
source: forAll(
|
|
@@ -80209,10 +80507,10 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
80209
80507
|
),
|
|
80210
80508
|
sampleData: {
|
|
80211
80509
|
customer: {
|
|
80212
|
-
name: "
|
|
80213
|
-
street: "
|
|
80214
|
-
zip: "
|
|
80215
|
-
city: "
|
|
80510
|
+
name: "Olvarest GmbH",
|
|
80511
|
+
street: "Beispielweg 2",
|
|
80512
|
+
zip: "54321",
|
|
80513
|
+
city: "Beispielstadt",
|
|
80216
80514
|
country: "Deutschland"
|
|
80217
80515
|
}
|
|
80218
80516
|
},
|
|
@@ -80220,18 +80518,38 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
80220
80518
|
},
|
|
80221
80519
|
{
|
|
80222
80520
|
id: "page-footer",
|
|
80521
|
+
group: "page",
|
|
80522
|
+
kinds: ["pdf"],
|
|
80223
80523
|
title: "Footer with page numbers",
|
|
80224
|
-
description: "
|
|
80524
|
+
description: "Company line and \u201CPage x of y\u201D in the page footer.",
|
|
80525
|
+
target: "footer",
|
|
80225
80526
|
source: forAll(
|
|
80226
80527
|
(e) => `<div style="width:100%;display:flex;justify-content:space-between;font-size:9px;color:#666">
|
|
80227
80528
|
<span>${val(e, "company.name")} \xB7 ${val(e, "company.email")}</span>
|
|
80228
|
-
<span
|
|
80529
|
+
<span>${tr(e, "footer.page")} <span class="pageNumber"></span> ${tr(e, "footer.of")} <span class="totalPages"></span></span>
|
|
80229
80530
|
</div>$0`
|
|
80230
80531
|
),
|
|
80231
|
-
sampleData: { company: { name: "
|
|
80532
|
+
sampleData: { company: { name: "Fennlor Studio GmbH", email: "hello@fennlor.example" } },
|
|
80533
|
+
i18n: {
|
|
80534
|
+
en: { "footer.page": "Page", "footer.of": "of" },
|
|
80535
|
+
de: { "footer.page": "Seite", "footer.of": "von" }
|
|
80536
|
+
}
|
|
80537
|
+
},
|
|
80538
|
+
{
|
|
80539
|
+
id: "legal-footer",
|
|
80540
|
+
group: "page",
|
|
80541
|
+
kinds: ["pdf"],
|
|
80542
|
+
title: "Legal footer",
|
|
80543
|
+
description: "The legal footer from the brand kit, centred in the page footer.",
|
|
80544
|
+
target: "footer",
|
|
80545
|
+
source: forAll(
|
|
80546
|
+
(e) => `<div style="width:100%;text-align:center;font-size:8px;color:#666">${val(e, "brand.legal_footer")}</div>$0`
|
|
80547
|
+
)
|
|
80232
80548
|
},
|
|
80233
80549
|
{
|
|
80234
80550
|
id: "cover-page",
|
|
80551
|
+
group: "report",
|
|
80552
|
+
kinds: ["pdf"],
|
|
80235
80553
|
title: "Cover page",
|
|
80236
80554
|
description: "Title, subtitle and date on a page of its own.",
|
|
80237
80555
|
source: forAll(
|
|
@@ -80250,12 +80568,13 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
80250
80568
|
}
|
|
80251
80569
|
},
|
|
80252
80570
|
css: `.ff-cover { min-height: 60vh; display: flex; flex-direction: column; justify-content: center; }
|
|
80253
|
-
.ff-cover h1 { font-size: 32pt; margin: 0 0 8px; }
|
|
80571
|
+
.ff-cover h1 { font-family: var(--brand-font-heading, inherit); font-size: 32pt; color: var(--brand-color-primary, inherit); margin: 0 0 8px; }
|
|
80254
80572
|
.ff-cover .subtitle { font-size: 16pt; color: #444; margin: 0; }
|
|
80255
80573
|
.ff-cover .date { margin-top: 24px; color: #666; }`
|
|
80256
80574
|
},
|
|
80257
80575
|
{
|
|
80258
80576
|
id: "signature",
|
|
80577
|
+
group: "letter",
|
|
80259
80578
|
title: "Signature line",
|
|
80260
80579
|
description: "Place, date and a signature line.",
|
|
80261
80580
|
source: forAll(
|
|
@@ -80268,7 +80587,7 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
80268
80587
|
signature: {
|
|
80269
80588
|
place: "Berlin",
|
|
80270
80589
|
date: "2026-09-07",
|
|
80271
|
-
name: "
|
|
80590
|
+
name: "Erika Mustermann"
|
|
80272
80591
|
}
|
|
80273
80592
|
},
|
|
80274
80593
|
css: `.ff-signature { margin-top: 48px; width: 260px; font-size: 9pt; color: #444; }
|
|
@@ -80276,33 +80595,32 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
80276
80595
|
},
|
|
80277
80596
|
{
|
|
80278
80597
|
id: "epc-qr",
|
|
80598
|
+
group: "invoice",
|
|
80279
80599
|
title: "SEPA payment QR code",
|
|
80280
80600
|
description: "GiroCode that banking apps scan to prefill the transfer.",
|
|
80281
80601
|
source: {
|
|
80282
80602
|
jinja2: `<figure class="ff-qr">
|
|
80283
|
-
<img src="{{ epcQr(
|
|
80284
|
-
<figcaption
|
|
80603
|
+
<img src="{{ epcQr(payment, { size: 120 }) }}" alt="${tr("jinja2", "qr.alt")}">
|
|
80604
|
+
<figcaption>${tr("jinja2", "qr.caption")}</figcaption>
|
|
80285
80605
|
</figure>$0`,
|
|
80286
80606
|
liquid: `<figure class="ff-qr">
|
|
80287
|
-
<img src="{{ payment | epcQr }}" alt="
|
|
80288
|
-
<figcaption
|
|
80607
|
+
<img src="{{ payment | epcQr: size: 120 }}" alt="${tr("liquid", "qr.alt")}">
|
|
80608
|
+
<figcaption>${tr("liquid", "qr.caption")}</figcaption>
|
|
80289
80609
|
</figure>$0`,
|
|
80290
80610
|
handlebars: `<figure class="ff-qr">
|
|
80291
|
-
<img src="{{epcQr payment size=120}}" alt="
|
|
80292
|
-
<figcaption
|
|
80611
|
+
<img src="{{epcQr payment size=120}}" alt="${tr("handlebars", "qr.alt")}">
|
|
80612
|
+
<figcaption>${tr("handlebars", "qr.caption")}</figcaption>
|
|
80293
80613
|
</figure>$0`
|
|
80294
80614
|
},
|
|
80615
|
+
i18n: {
|
|
80616
|
+
en: { "qr.alt": "Payment QR code", "qr.caption": "Scan to pay" },
|
|
80617
|
+
de: { "qr.alt": "QR-Code zur Zahlung", "qr.caption": "Zum Bezahlen scannen" }
|
|
80618
|
+
},
|
|
80295
80619
|
sampleData: {
|
|
80296
|
-
company: {
|
|
80297
|
-
name: "Acme GmbH",
|
|
80298
|
-
iban: "DE02120300000000202051",
|
|
80299
|
-
bic: "BYLADEM1001"
|
|
80300
|
-
},
|
|
80301
|
-
invoice: { number: "2026-0042", total: 3760.4 },
|
|
80302
80620
|
payment: {
|
|
80303
|
-
name: "
|
|
80304
|
-
iban: "
|
|
80305
|
-
bic: "
|
|
80621
|
+
name: "Fennlor Studio GmbH",
|
|
80622
|
+
iban: "DE00000000000000000000",
|
|
80623
|
+
bic: "XXXXDEXXXXX",
|
|
80306
80624
|
amount: 3760.4,
|
|
80307
80625
|
reference: "2026-0042"
|
|
80308
80626
|
}
|
|
@@ -80312,38 +80630,29 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
80312
80630
|
},
|
|
80313
80631
|
{
|
|
80314
80632
|
id: "chart",
|
|
80633
|
+
group: "report",
|
|
80315
80634
|
title: "Chart",
|
|
80316
|
-
description: "Bar chart
|
|
80635
|
+
description: "Bar chart from labels and values in the data, in the brand colour.",
|
|
80636
|
+
// the colour comes from the brand kit; without one the helper takes its palette
|
|
80317
80637
|
source: {
|
|
80318
80638
|
jinja2: `<figure class="ff-chart-box">
|
|
80319
|
-
{{ chart(
|
|
80320
|
-
<figcaption>{{
|
|
80639
|
+
{{ chart(sales, { type: '\${1:bar}', width: 480, height: 240, color: brand.colors.primary }) }}
|
|
80640
|
+
<figcaption>{{ sales.caption }}</figcaption>
|
|
80321
80641
|
</figure>$0`,
|
|
80322
80642
|
liquid: `<figure class="ff-chart-box">
|
|
80323
|
-
{{
|
|
80324
|
-
<figcaption>{{
|
|
80643
|
+
{{ sales | chart: type: '\${1:bar}', width: 480, height: 240, color: brand.colors.primary }}
|
|
80644
|
+
<figcaption>{{ sales.caption }}</figcaption>
|
|
80325
80645
|
</figure>$0`,
|
|
80326
80646
|
handlebars: `<figure class="ff-chart-box">
|
|
80327
|
-
{{chart
|
|
80328
|
-
<figcaption>{{
|
|
80647
|
+
{{chart sales type='\${1:bar}' width=480 height=240 color=brand.colors.primary}}
|
|
80648
|
+
<figcaption>{{sales.caption}}</figcaption>
|
|
80329
80649
|
</figure>$0`
|
|
80330
80650
|
},
|
|
80331
80651
|
sampleData: {
|
|
80332
|
-
|
|
80333
|
-
type: "bar",
|
|
80652
|
+
sales: {
|
|
80334
80653
|
caption: "Revenue per quarter",
|
|
80335
|
-
|
|
80336
|
-
|
|
80337
|
-
data: {
|
|
80338
|
-
labels: ["Q1", "Q2", "Q3", "Q4"],
|
|
80339
|
-
datasets: [
|
|
80340
|
-
{
|
|
80341
|
-
label: "Revenue",
|
|
80342
|
-
data: [12e3, 15500, 14200, 18900],
|
|
80343
|
-
backgroundColor: "#3E63DD"
|
|
80344
|
-
}
|
|
80345
|
-
]
|
|
80346
|
-
}
|
|
80654
|
+
labels: ["Q1", "Q2", "Q3", "Q4"],
|
|
80655
|
+
values: [12e3, 15500, 14200, 18900]
|
|
80347
80656
|
}
|
|
80348
80657
|
},
|
|
80349
80658
|
css: `.ff-chart-box { margin: 16px 0; break-inside: avoid; }
|
|
@@ -80351,6 +80660,7 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
80351
80660
|
},
|
|
80352
80661
|
{
|
|
80353
80662
|
id: "two-columns",
|
|
80663
|
+
group: "page",
|
|
80354
80664
|
title: "Two-column layout",
|
|
80355
80665
|
description: "Two equal columns that stay side by side in print.",
|
|
80356
80666
|
source: forAll(
|
|
@@ -80361,6 +80671,50 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
|
|
|
80361
80671
|
),
|
|
80362
80672
|
css: `.ff-columns { display: flex; gap: 24px; }
|
|
80363
80673
|
.ff-columns > div { flex: 1 1 0; min-width: 0; }`
|
|
80674
|
+
},
|
|
80675
|
+
{
|
|
80676
|
+
id: "keep-together",
|
|
80677
|
+
group: "page",
|
|
80678
|
+
kinds: ["pdf"],
|
|
80679
|
+
title: "Keep together",
|
|
80680
|
+
description: "Content that is never split across two pages.",
|
|
80681
|
+
source: forAll(() => `<div class="avoid-break">
|
|
80682
|
+
$0
|
|
80683
|
+
</div>`)
|
|
80684
|
+
},
|
|
80685
|
+
{
|
|
80686
|
+
id: "page-break",
|
|
80687
|
+
group: "page",
|
|
80688
|
+
kinds: ["pdf"],
|
|
80689
|
+
title: "Page break",
|
|
80690
|
+
description: "Starts a new page in the PDF.",
|
|
80691
|
+
source: forAll(() => `<div class="page-break"></div>
|
|
80692
|
+
$0`)
|
|
80693
|
+
},
|
|
80694
|
+
{
|
|
80695
|
+
id: "kpi-cards",
|
|
80696
|
+
group: "report",
|
|
80697
|
+
title: "Key figures",
|
|
80698
|
+
description: "A row of cards with label, value and change for each figure.",
|
|
80699
|
+
source: forAll(
|
|
80700
|
+
(e) => `<div class="ff-kpis">
|
|
80701
|
+
${loop(e, "kpi", "report.kpis", ` <div class="kpi"><span class="label">${val(e, "kpi.label")}</span><strong>${val(e, "kpi.value")}</strong><span class="change">${val(e, "kpi.change")}</span></div>`)}
|
|
80702
|
+
</div>$0`
|
|
80703
|
+
),
|
|
80704
|
+
sampleData: {
|
|
80705
|
+
report: {
|
|
80706
|
+
kpis: [
|
|
80707
|
+
{ label: "Revenue", value: "\u20AC 61,600", change: "+12 % on Q2" },
|
|
80708
|
+
{ label: "New customers", value: "48", change: "+9 on Q2" },
|
|
80709
|
+
{ label: "Churn", value: "1.8 %", change: "\u22120.4 pt on Q2" }
|
|
80710
|
+
]
|
|
80711
|
+
}
|
|
80712
|
+
},
|
|
80713
|
+
css: `.ff-kpis { display: flex; gap: 12px; margin: 16px 0; break-inside: avoid; }
|
|
80714
|
+
.ff-kpis .kpi { flex: 1 1 0; padding: 10px 12px; border: 1px solid #e5e5e5; border-top: 3px solid ${rule}; border-radius: 4px; }
|
|
80715
|
+
.ff-kpis .label { display: block; font-size: 8pt; color: #666; text-transform: uppercase; letter-spacing: 0.04em; }
|
|
80716
|
+
.ff-kpis strong { display: block; margin: 2px 0; font-size: 18pt; }
|
|
80717
|
+
.ff-kpis .change { font-size: 8pt; color: #555; }`
|
|
80364
80718
|
}
|
|
80365
80719
|
];
|
|
80366
80720
|
|
|
@@ -80369,7 +80723,50 @@ var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
|
|
|
80369
80723
|
var ASSET_NAME_PATTERN = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*$`);
|
|
80370
80724
|
var ASSET_MAX_BYTES = 10 * 1024 * 1024;
|
|
80371
80725
|
|
|
80726
|
+
// packages/engine/src/lib/office/errors.ts
|
|
80727
|
+
var officeLimits = {
|
|
80728
|
+
/** Entries in one container. */
|
|
80729
|
+
maxEntries: 2e3,
|
|
80730
|
+
/** Sum of the uncompressed sizes the central directory declares. */
|
|
80731
|
+
maxDeclaredBytes: 200 * 1024 * 1024,
|
|
80732
|
+
/** One part we inflate; enforced while inflating, because the declared size can lie. */
|
|
80733
|
+
maxPartBytes: 20 * 1024 * 1024,
|
|
80734
|
+
/** A conversion upload or a template file. */
|
|
80735
|
+
maxFileBytes: 20 * 1024 * 1024
|
|
80736
|
+
};
|
|
80737
|
+
|
|
80738
|
+
// packages/engine/src/lib/office/drawings.ts
|
|
80739
|
+
var OPEN = String.fromCharCode(57346);
|
|
80740
|
+
var CLOSE = String.fromCharCode(57347);
|
|
80741
|
+
var officeDrawingLimits = {
|
|
80742
|
+
/** Drawings per render. */
|
|
80743
|
+
maxDrawings: 50,
|
|
80744
|
+
/** Image bytes placed per render. */
|
|
80745
|
+
maxImageBytes: 20 * 1024 * 1024,
|
|
80746
|
+
/** Largest raster edge in pixels. */
|
|
80747
|
+
maxPixels: 2400
|
|
80748
|
+
};
|
|
80749
|
+
|
|
80750
|
+
// packages/engine/src/lib/office/render.ts
|
|
80751
|
+
var OFFICE_FILLED_MAX_BYTES = 50 * 1024 * 1024;
|
|
80752
|
+
|
|
80372
80753
|
// packages/sdk-ts/src/lib/client.ts
|
|
80754
|
+
var isOfficeInput = (input) => "file" in input;
|
|
80755
|
+
function blobOf(file) {
|
|
80756
|
+
const blob = file.data instanceof Blob ? file.data : new Blob([file.data]);
|
|
80757
|
+
const own = typeof File !== "undefined" && file.data instanceof File ? file.data.name : void 0;
|
|
80758
|
+
return { blob, name: file.name ?? own ?? "document" };
|
|
80759
|
+
}
|
|
80760
|
+
function multipart(fields, file) {
|
|
80761
|
+
const form = new FormData();
|
|
80762
|
+
const { blob, name } = blobOf(file);
|
|
80763
|
+
form.set("file", blob, name);
|
|
80764
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
80765
|
+
if (value === void 0) continue;
|
|
80766
|
+
form.set(key, typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value));
|
|
80767
|
+
}
|
|
80768
|
+
return form;
|
|
80769
|
+
}
|
|
80373
80770
|
var FormfeedError = class extends Error {
|
|
80374
80771
|
constructor(code, message2, status, problem = null, requestId = null) {
|
|
80375
80772
|
super(message2);
|
|
@@ -80461,6 +80858,11 @@ var Formfeed = class {
|
|
|
80461
80858
|
},
|
|
80462
80859
|
/** Removes the stored files of a render before they expire. Needs the `file:delete` scope. */
|
|
80463
80860
|
deleteOutputs: (id, options = {}) => this.request("DELETE", `/renders/${encodeURIComponent(id)}/outputs`, void 0, options),
|
|
80861
|
+
/**
|
|
80862
|
+
* The stored request of a render, to render it again. Needs `render:input`; throws
|
|
80863
|
+
* `render_input_expired` (410) when the workspace keeps no requests or the period ended.
|
|
80864
|
+
*/
|
|
80865
|
+
input: (id, options = {}) => this.request("GET", `/renders/${encodeURIComponent(id)}/input`, void 0, options),
|
|
80464
80866
|
batch: (request, options = {}) => this.request("POST", "/renders/batch", request, {
|
|
80465
80867
|
...options,
|
|
80466
80868
|
idempotencyKey: options.idempotencyKey ?? randomKey()
|
|
@@ -80475,7 +80877,18 @@ var Formfeed = class {
|
|
|
80475
80877
|
merge: (sources, output = {}, options = {}) => this.request("POST", "/pdf/merge", { ...output, sources: sources.map(idOf) }, withKey(options)),
|
|
80476
80878
|
protect: (source, protect, options = {}) => this.request("POST", "/pdf/protect", { ...protect, source: idOf(source) }, withKey(options)),
|
|
80477
80879
|
watermark: (source, watermark, options = {}) => this.request("POST", "/pdf/watermark", { ...watermark, source: idOf(source) }, withKey(options)),
|
|
80478
|
-
info: (source, options = {}) => this.request("POST", "/pdf/info", { source: idOf(source) }, options)
|
|
80880
|
+
info: (source, options = {}) => this.request("POST", "/pdf/info", { source: idOf(source) }, options),
|
|
80881
|
+
/**
|
|
80882
|
+
* Converts an office document to a PDF render (Starter plan and above): an uploaded file (Word,
|
|
80883
|
+
* Excel, PowerPoint, OpenDocument, RTF or HTML, up to 20 MB, not kept), or the output of a Word or
|
|
80884
|
+
* PowerPoint template render. Costs follow the PDF rule. A busy converter answers 503, which is
|
|
80885
|
+
* retried like any other.
|
|
80886
|
+
*/
|
|
80887
|
+
convert: (source, convert = {}, options = {}) => {
|
|
80888
|
+
if (typeof source === "object" && "file" in source)
|
|
80889
|
+
return this.request("POST", "/pdf/convert", multipart({ ...convert }, source.file), withKey(options));
|
|
80890
|
+
return this.request("POST", "/pdf/convert", { ...convert, source: idOf(source) }, withKey(options));
|
|
80891
|
+
}
|
|
80479
80892
|
};
|
|
80480
80893
|
/**
|
|
80481
80894
|
* The workspace file library (`/files`): images and PDFs a template references by name with
|
|
@@ -80514,6 +80927,27 @@ var Formfeed = class {
|
|
|
80514
80927
|
/** Removes the file and its bytes; templates that name it render a missing image afterwards. */
|
|
80515
80928
|
delete: (file, options = {}) => this.request("DELETE", `/files/${encodeURIComponent(idOf(file))}`, void 0, options)
|
|
80516
80929
|
};
|
|
80930
|
+
/** The organisation's brand kit, read-only through the API (it is edited in the app). Needs `template:read`. */
|
|
80931
|
+
brand = {
|
|
80932
|
+
get: (options = {}) => this.request("GET", "/brand", void 0, options)
|
|
80933
|
+
};
|
|
80934
|
+
/**
|
|
80935
|
+
* Shared partials of the organisation. Renders use the current source, so a change reaches every
|
|
80936
|
+
* template that includes the partial without a new version. Needs `template:read` or `template:write`.
|
|
80937
|
+
*/
|
|
80938
|
+
partials = {
|
|
80939
|
+
/** Every shared partial, without its source. */
|
|
80940
|
+
list: async (options = {}) => (await this.request("GET", "/partials", void 0, options)).data,
|
|
80941
|
+
/** One partial with its source. */
|
|
80942
|
+
get: (name, options = {}) => this.request("GET", `/partials/${encodeURIComponent(name)}`, void 0, options),
|
|
80943
|
+
/** Creates or replaces a partial; pass `base_version` to fail with 409 when it changed since you read it. */
|
|
80944
|
+
put: async (name, input, options = {}) => {
|
|
80945
|
+
const { status, body } = await this.send("PUT", `/partials/${encodeURIComponent(name)}`, input, options);
|
|
80946
|
+
return { partial: body, created: status === 201 };
|
|
80947
|
+
},
|
|
80948
|
+
/** Removes a partial; templates that still include it fail to render afterwards. */
|
|
80949
|
+
delete: (name, options = {}) => this.request("DELETE", `/partials/${encodeURIComponent(name)}`, void 0, options)
|
|
80950
|
+
};
|
|
80517
80951
|
jobs = {
|
|
80518
80952
|
get: (id, options = {}) => this.request("GET", `/jobs/${encodeURIComponent(id)}`, void 0, options),
|
|
80519
80953
|
waitFor: async (id, options = {}) => {
|
|
@@ -80533,7 +80967,20 @@ var Formfeed = class {
|
|
|
80533
80967
|
create: (input, options = {}) => this.request("POST", "/webhooks", input, options),
|
|
80534
80968
|
update: (id, patch, options = {}) => this.request("PUT", `/webhooks/${encodeURIComponent(id)}`, patch, options),
|
|
80535
80969
|
delete: (id, options = {}) => this.request("DELETE", `/webhooks/${encodeURIComponent(id)}`, void 0, options),
|
|
80536
|
-
test: (id, options = {}) => this.request("POST", `/webhooks/${encodeURIComponent(id)}/test`, void 0, options)
|
|
80970
|
+
test: (id, options = {}) => this.request("POST", `/webhooks/${encodeURIComponent(id)}/test`, void 0, options),
|
|
80971
|
+
/**
|
|
80972
|
+
* Sends a stored event (`evt_…`) again as a new delivery to a registered endpoint (`webhook:manage`)
|
|
80973
|
+
* or a running listen session (`webhook:listen`).
|
|
80974
|
+
*/
|
|
80975
|
+
resend: (eventId, endpointId, options = {}) => this.request("POST", `/webhooks/events/${encodeURIComponent(eventId)}/resend`, { endpoint_id: endpointId }, options),
|
|
80976
|
+
/**
|
|
80977
|
+
* Listen sessions, what `formfeed listen` uses. Needs `webhook:listen`. Starting a session ends the
|
|
80978
|
+
* previous one of the same key; the WebSocket itself is up to the caller.
|
|
80979
|
+
*/
|
|
80980
|
+
listen: {
|
|
80981
|
+
start: (input = {}, options = {}) => this.request("POST", "/webhooks/listen", input, options),
|
|
80982
|
+
end: (id, options = {}) => this.request("DELETE", `/webhooks/listen/${encodeURIComponent(id)}`, void 0, options)
|
|
80983
|
+
}
|
|
80537
80984
|
};
|
|
80538
80985
|
templates = {
|
|
80539
80986
|
list: (query = {}, options = {}) => {
|
|
@@ -80554,15 +81001,79 @@ var Formfeed = class {
|
|
|
80554
81001
|
return out;
|
|
80555
81002
|
},
|
|
80556
81003
|
get: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}`, void 0, options),
|
|
80557
|
-
|
|
81004
|
+
/** A Word or PowerPoint template (`kind` `docx` or `pptx`) is created from its `file`, sent as multipart. */
|
|
81005
|
+
create: (input, options = {}) => {
|
|
81006
|
+
if (isOfficeInput(input) && input.file) {
|
|
81007
|
+
const { file, ...fields } = input;
|
|
81008
|
+
return this.request("POST", "/templates", multipart(fields, file), options);
|
|
81009
|
+
}
|
|
81010
|
+
return this.request("POST", "/templates", input, options);
|
|
81011
|
+
},
|
|
80558
81012
|
update: (idOrSlug, patch, options = {}) => this.request("PUT", `/templates/${encodeURIComponent(idOrSlug)}`, patch, options),
|
|
80559
81013
|
archive: (idOrSlug, options = {}) => this.request("DELETE", `/templates/${encodeURIComponent(idOrSlug)}`, void 0, options),
|
|
80560
81014
|
versions: {
|
|
80561
81015
|
list: async (idOrSlug, options = {}) => (await this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions`, void 0, options)).data,
|
|
80562
|
-
/** `which`: 'published', 'latest' or a version
|
|
80563
|
-
get: (idOrSlug, which = "published", options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions/${which}`, void 0, options),
|
|
80564
|
-
|
|
80565
|
-
|
|
81016
|
+
/** `which`: 'published', 'latest', a version number or a channel name (its main version). Carries the files. */
|
|
81017
|
+
get: (idOrSlug, which = "published", options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions/${encodeURIComponent(String(which))}`, void 0, options),
|
|
81018
|
+
/** For a Word or PowerPoint template, pass `file` to replace the document; without it the latest file is kept. */
|
|
81019
|
+
create: (idOrSlug, input, options = {}) => {
|
|
81020
|
+
const path = `/templates/${encodeURIComponent(idOrSlug)}/versions`;
|
|
81021
|
+
if (isOfficeInput(input)) {
|
|
81022
|
+
const { file, ...fields } = input;
|
|
81023
|
+
if (file) return this.request("POST", path, multipart(fields, file), options);
|
|
81024
|
+
return this.request("POST", path, fields, options);
|
|
81025
|
+
}
|
|
81026
|
+
return this.request("POST", path, input, options);
|
|
81027
|
+
},
|
|
81028
|
+
/** The Word or PowerPoint file of a version (`which` as for `get`); needs `template:read`. */
|
|
81029
|
+
file: async (idOrSlug, which = "published", options = {}) => {
|
|
81030
|
+
const path = `/templates/${encodeURIComponent(idOrSlug)}/versions/${encodeURIComponent(String(which))}/file`;
|
|
81031
|
+
return (await this.send("GET", path, void 0, options, "bytes")).body;
|
|
81032
|
+
},
|
|
81033
|
+
/**
|
|
81034
|
+
* Publishes a version. When both versions store a data schema and the new one breaks callers of the
|
|
81035
|
+
* published one, the API refuses with `schema_breaking_change` unless `allowBreaking` is set.
|
|
81036
|
+
*/
|
|
81037
|
+
publish: (idOrSlug, number, options = {}) => {
|
|
81038
|
+
const { allowBreaking, ...rest } = options;
|
|
81039
|
+
return this.request(
|
|
81040
|
+
"POST",
|
|
81041
|
+
`/templates/${encodeURIComponent(idOrSlug)}/versions/${number}/publish`,
|
|
81042
|
+
allowBreaking ? { allow_breaking: true } : void 0,
|
|
81043
|
+
rest
|
|
81044
|
+
);
|
|
81045
|
+
}
|
|
81046
|
+
},
|
|
81047
|
+
/**
|
|
81048
|
+
* Release channels (`published` and named ones such as `staging`): a render with `version: 'staging'`
|
|
81049
|
+
* uses the version the channel points at. Moves reach renders within a minute.
|
|
81050
|
+
*/
|
|
81051
|
+
channels: {
|
|
81052
|
+
list: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/channels`, void 0, options),
|
|
81053
|
+
/** Creates or moves a channel; on `published` this publishes. */
|
|
81054
|
+
set: (idOrSlug, name, move, options = {}) => {
|
|
81055
|
+
const { allowBreaking, ...rest } = options;
|
|
81056
|
+
return this.request(
|
|
81057
|
+
"PUT",
|
|
81058
|
+
`/templates/${encodeURIComponent(idOrSlug)}/channels/${encodeURIComponent(name)}`,
|
|
81059
|
+
{ ...move, ...allowBreaking ? { allow_breaking: true } : {} },
|
|
81060
|
+
rest
|
|
81061
|
+
);
|
|
81062
|
+
},
|
|
81063
|
+
/** The canary becomes the main version. */
|
|
81064
|
+
promote: (idOrSlug, name, options = {}) => this.channelAction(idOrSlug, name, "promote", options),
|
|
81065
|
+
/** Back to the version before the last move. */
|
|
81066
|
+
rollback: (idOrSlug, name, options = {}) => this.channelAction(idOrSlug, name, "rollback", options),
|
|
81067
|
+
/** Refused with `channel_in_use` while renders of the last hour used the channel, unless `force`. */
|
|
81068
|
+
delete: (idOrSlug, name, options = {}) => {
|
|
81069
|
+
const { force, ...rest } = options;
|
|
81070
|
+
return this.request(
|
|
81071
|
+
"DELETE",
|
|
81072
|
+
`/templates/${encodeURIComponent(idOrSlug)}/channels/${encodeURIComponent(name)}${force ? "?force=true" : ""}`,
|
|
81073
|
+
void 0,
|
|
81074
|
+
rest
|
|
81075
|
+
);
|
|
81076
|
+
}
|
|
80566
81077
|
},
|
|
80567
81078
|
schema: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/schema`, void 0, options),
|
|
80568
81079
|
/**
|
|
@@ -80577,13 +81088,37 @@ var Formfeed = class {
|
|
|
80577
81088
|
/** Units of the current period, or of `YYYY-MM`, with a daily series and a breakdown per template. */
|
|
80578
81089
|
usage: (period = "current", options = {}) => this.request("GET", `/usage?period=${encodeURIComponent(period)}`, void 0, options)
|
|
80579
81090
|
};
|
|
81091
|
+
/** Workspaces of the key's organisation. Needs `workspace:delete`, which no key has by default. */
|
|
81092
|
+
workspaces = {
|
|
81093
|
+
/**
|
|
81094
|
+
* Deletes a workspace: it leaves the app and the API at once, its keys are revoked and its content
|
|
81095
|
+
* is removed after 30 days. The organisation's last workspace is refused with `last_workspace`.
|
|
81096
|
+
*/
|
|
81097
|
+
delete: (id, options = {}) => this.request("DELETE", `/workspaces/${encodeURIComponent(id)}`, void 0, options)
|
|
81098
|
+
};
|
|
81099
|
+
channelAction(idOrSlug, name, action, options) {
|
|
81100
|
+
const { allowBreaking, ...rest } = options;
|
|
81101
|
+
return this.request(
|
|
81102
|
+
"POST",
|
|
81103
|
+
`/templates/${encodeURIComponent(idOrSlug)}/channels/${encodeURIComponent(name)}/${action}`,
|
|
81104
|
+
allowBreaking ? { allow_breaking: true } : void 0,
|
|
81105
|
+
rest
|
|
81106
|
+
);
|
|
81107
|
+
}
|
|
80580
81108
|
/** Raw request with auth, retries and problem mapping; use it for endpoints without a helper. */
|
|
80581
81109
|
async request(method, path, body, options = {}) {
|
|
81110
|
+
return (await this.send(method, path, body, options)).body;
|
|
81111
|
+
}
|
|
81112
|
+
/**
|
|
81113
|
+
* `request` with the HTTP status, for endpoints whose answer depends on it (201 created, 200 updated).
|
|
81114
|
+
* `bytes` returns a successful body as bytes (file downloads); problems are JSON either way.
|
|
81115
|
+
*/
|
|
81116
|
+
async send(method, path, body, options = {}, as = "json") {
|
|
80582
81117
|
const url = `${this.baseUrl}${path}`;
|
|
80583
81118
|
const headers = {
|
|
80584
81119
|
authorization: `Bearer ${this.apiKey}`,
|
|
80585
|
-
accept: "application/json",
|
|
80586
|
-
"user-agent": "formfeed-sdk-ts/0.
|
|
81120
|
+
accept: as === "bytes" ? "*/*" : "application/json",
|
|
81121
|
+
"user-agent": "formfeed-sdk-ts/0.3"
|
|
80587
81122
|
};
|
|
80588
81123
|
const isForm = typeof FormData !== "undefined" && body instanceof FormData;
|
|
80589
81124
|
if (body !== void 0 && !isForm) headers["content-type"] = "application/json";
|
|
@@ -80616,9 +81151,15 @@ var Formfeed = class {
|
|
|
80616
81151
|
await sleep(this.backoff(attempt, res.headers.get("retry-after")), options.signal);
|
|
80617
81152
|
continue;
|
|
80618
81153
|
}
|
|
80619
|
-
if (res.status === 204) return void 0;
|
|
81154
|
+
if (res.status === 204) return { status: 204, body: void 0 };
|
|
81155
|
+
if (as === "bytes" && res.ok) return { status: res.status, body: new Uint8Array(await res.arrayBuffer()) };
|
|
80620
81156
|
const text2 = await res.text();
|
|
80621
|
-
|
|
81157
|
+
let json = null;
|
|
81158
|
+
try {
|
|
81159
|
+
json = text2 ? JSON.parse(text2) : null;
|
|
81160
|
+
} catch {
|
|
81161
|
+
if (res.ok) throw new FormfeedError("invalid_response", `the API answered HTTP ${res.status} with a body that is not JSON`, res.status);
|
|
81162
|
+
}
|
|
80622
81163
|
if (!res.ok) {
|
|
80623
81164
|
const problem = json ?? {};
|
|
80624
81165
|
throw new FormfeedError(
|
|
@@ -80629,7 +81170,7 @@ var Formfeed = class {
|
|
|
80629
81170
|
res.headers.get("x-request-id")
|
|
80630
81171
|
);
|
|
80631
81172
|
}
|
|
80632
|
-
return json;
|
|
81173
|
+
return { status: res.status, body: json };
|
|
80633
81174
|
}
|
|
80634
81175
|
}
|
|
80635
81176
|
backoff(attempt, retryAfter) {
|
|
@@ -80644,9 +81185,9 @@ var encoder2 = new TextEncoder();
|
|
|
80644
81185
|
|
|
80645
81186
|
// packages/mcp/src/lib/server.ts
|
|
80646
81187
|
import { z as z2 } from "zod";
|
|
80647
|
-
var SERVER_INFO = { name: "formfeed", version: "0.
|
|
81188
|
+
var SERVER_INFO = { name: "formfeed", version: "0.3.0" };
|
|
80648
81189
|
var engineSchema = z2.enum(["jinja2", "liquid", "handlebars"]);
|
|
80649
|
-
var outputSchema = z2.enum(
|
|
81190
|
+
var outputSchema = z2.enum(outputFormats);
|
|
80650
81191
|
var templateSummary = (t) => ({
|
|
80651
81192
|
id: t.id,
|
|
80652
81193
|
slug: t.slug,
|
|
@@ -80782,7 +81323,7 @@ function createFormfeedServer(options) {
|
|
|
80782
81323
|
html: z2.string().optional().describe("Raw HTML instead of a template"),
|
|
80783
81324
|
engine: engineSchema.optional().describe("Engine for html that contains template syntax"),
|
|
80784
81325
|
data: z2.record(z2.string(), z2.unknown()).optional(),
|
|
80785
|
-
output: outputSchema.
|
|
81326
|
+
output: outputSchema.optional().describe("Left out: PDF for PDF templates and html, the template's image format for image templates"),
|
|
80786
81327
|
filename: z2.string().optional(),
|
|
80787
81328
|
locale: z2.string().optional().describe("BCP 47 tag for helpers and translations, e.g. de-DE"),
|
|
80788
81329
|
wait: z2.boolean().default(true).describe("Wait for the render to finish (sync renders finish immediately)")
|
|
@@ -80797,7 +81338,7 @@ function createFormfeedServer(options) {
|
|
|
80797
81338
|
...html ? { html } : {},
|
|
80798
81339
|
...engine ? { engine } : {},
|
|
80799
81340
|
data: data ?? {},
|
|
80800
|
-
output,
|
|
81341
|
+
...output ? { output } : {},
|
|
80801
81342
|
...filename ? { filename } : {},
|
|
80802
81343
|
...locale ? { locale } : {},
|
|
80803
81344
|
meta: { source: "mcp" }
|
package/package.json
CHANGED