@formfeed/mcp 0.1.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.
Files changed (2) hide show
  1. package/bin/formfeed-mcp.mjs +1045 -147
  2. package/package.json +1 -1
@@ -1155,16 +1155,16 @@ var require_numeric_data = __commonJS({
1155
1155
  return NumericData.getBitsLength(this.data.length);
1156
1156
  };
1157
1157
  NumericData.prototype.write = function write(bitBuffer) {
1158
- let i, group, value;
1158
+ let i, group2, value;
1159
1159
  for (i = 0; i + 3 <= this.data.length; i += 3) {
1160
- group = this.data.substr(i, 3);
1161
- value = parseInt(group, 10);
1160
+ group2 = this.data.substr(i, 3);
1161
+ value = parseInt(group2, 10);
1162
1162
  bitBuffer.put(value, 10);
1163
1163
  }
1164
1164
  const remainingNum = this.data.length - i;
1165
1165
  if (remainingNum > 0) {
1166
- group = this.data.substr(i);
1167
- value = parseInt(group, 10);
1166
+ group2 = this.data.substr(i);
1167
+ value = parseInt(group2, 10);
1168
1168
  bitBuffer.put(value, remainingNum * 3 + 1);
1169
1169
  }
1170
1170
  };
@@ -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 tr = input[contentEnd - 1] === "-";
17448
+ const tr2 = input[contentEnd - 1] === "-";
17449
17449
  let l3 = tl ? contentBegin + 1 : contentBegin;
17450
- let r = tr ? contentEnd - 1 : contentEnd;
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 = tr || trimRight2;
17457
+ this.trimRight = tr2 || trimRight2;
17458
17458
  }
17459
17459
  get content() {
17460
17460
  return this.input.slice(this.contentRange[0], this.contentRange[1]);
@@ -20735,14 +20735,14 @@ var require_liquid_node = __commonJS({
20735
20735
  constructor(token, remainTokens, liquid) {
20736
20736
  super(token, remainTokens, liquid);
20737
20737
  this.candidates = [];
20738
- const group = this.tokenizer.readValue();
20738
+ const group2 = this.tokenizer.readValue();
20739
20739
  this.tokenizer.skipBlank();
20740
- if (group) {
20740
+ if (group2) {
20741
20741
  if (this.tokenizer.peek() === ":") {
20742
- this.group = group;
20742
+ this.group = group2;
20743
20743
  this.tokenizer.advance();
20744
20744
  } else
20745
- this.candidates.push(group);
20745
+ this.candidates.push(group2);
20746
20746
  }
20747
20747
  while (!this.tokenizer.end()) {
20748
20748
  const value = this.tokenizer.readValue();
@@ -20753,8 +20753,8 @@ var require_liquid_node = __commonJS({
20753
20753
  this.tokenizer.assert(this.candidates.length, () => `empty candidates: "${token.getText()}"`);
20754
20754
  }
20755
20755
  *render(ctx, emitter) {
20756
- const group = yield evalToken(this.group, ctx);
20757
- const fingerprint = `cycle:${group}:` + this.candidates.join(",");
20756
+ const group2 = yield evalToken(this.group, ctx);
20757
+ const fingerprint = `cycle:${group2}:` + this.candidates.join(",");
20758
20758
  const groups = ctx.getRegister("cycle", {});
20759
20759
  let idx = groups[fingerprint];
20760
20760
  if (idx === void 0) {
@@ -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
- fn: (_ctx, value) => escapeHtml(str(value)).replace(/\r?\n/g, "<br>\n")
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: (_ctx, value) => f.parse(str(value), {
77753
- async: false,
77754
- gfm: true,
77755
- breaks: false
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",
@@ -77815,7 +77824,7 @@ var formatHelpers = [
77815
77824
  name: "asset",
77816
77825
  doc: {
77817
77826
  signature: "asset(name)",
77818
- description: "URL of a workspace asset (logo, background) on the CDN.",
77827
+ 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.",
77819
77828
  example: `<img src="{{ asset('logo.png') }}">`,
77820
77829
  category: "document"
77821
77830
  },
@@ -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: (_ctx, value, options) => svgDataUri(qrSvg(String(value ?? ""), options ?? {}))
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: (_ctx, value, options) => {
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: (_ctx, input, options) => {
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
- return svgDataUri(
77939
- qrSvg(epcPayload(payment), {
77940
- ecc: "M",
77941
- ...qr
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
- var asObject = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
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 chart configuration; animations are disabled so PDF and preview match.",
78169
- example: "{{ chart({ type: 'bar', data: sales, width: 480, height: 240 }) }}",
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: (_ctx, spec, extra) => chartMarkup({ ...asObject(spec), ...asObject(extra) })
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: (_ctx, url, options) => imageMarkup(String(url ?? ""), asObject(options))
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
 
@@ -78308,6 +78361,12 @@ function defaultHelpers() {
78308
78361
  // packages/engine/src/lib/engines/handlebars.ts
78309
78362
  var import_handlebars = __toESM(require_lib2());
78310
78363
 
78364
+ // packages/engine/src/lib/unavailable.ts
78365
+ var UNAVAILABLE = {};
78366
+ function unavailableHelper(name) {
78367
+ return UNAVAILABLE[name];
78368
+ }
78369
+
78311
78370
  // packages/engine/src/lib/analysis/tags.ts
78312
78371
  function rangeAt(line, column, length) {
78313
78372
  return {
@@ -78487,7 +78546,10 @@ function missingPathDiagnostics(variables, sampleData, loopSources, builtinRoots
78487
78546
  const root = v2.path[0];
78488
78547
  if (!root || builtinRoots4.has(root)) continue;
78489
78548
  let path = v2.path;
78490
- if (loopSources.has(root)) {
78549
+ if (v2.source !== void 0) {
78550
+ if (!v2.source) continue;
78551
+ path = [...v2.source, ...v2.path.slice(1)];
78552
+ } else if (loopSources.has(root)) {
78491
78553
  const source = loopSources.get(root);
78492
78554
  if (!source) continue;
78493
78555
  path = [...source, ...v2.path.slice(1)];
@@ -78519,6 +78581,8 @@ var builtinHelpers2 = /* @__PURE__ */ new Set([
78519
78581
  "helperMissing"
78520
78582
  ]);
78521
78583
  var builtinRoots = /* @__PURE__ */ new Set([
78584
+ // the organisation's brand kit (spec 18), merged under the data
78585
+ "brand",
78522
78586
  "this",
78523
78587
  "@index",
78524
78588
  "@key",
@@ -78527,6 +78591,11 @@ var builtinRoots = /* @__PURE__ */ new Set([
78527
78591
  "@root",
78528
78592
  "lookup"
78529
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
+ }
78530
78599
  function createInstance(ctx) {
78531
78600
  const hb = import_handlebars.default.create();
78532
78601
  for (const [name, { fn, html }] of ctx.helpers.bind(ctx)) {
@@ -78746,6 +78815,13 @@ function analyzeHandlebars(source, opts = {}) {
78746
78815
  range: f2.range,
78747
78816
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
78748
78817
  });
78818
+ } else if (unavailableHelper(f2.name)) {
78819
+ diagnostics.push({
78820
+ severity: "warning",
78821
+ code: "unavailable-helper",
78822
+ message: unavailableHelper(f2.name),
78823
+ range: f2.range
78824
+ });
78749
78825
  } else if (!f2.known) {
78750
78826
  diagnostics.push({
78751
78827
  severity: "error",
@@ -78790,7 +78866,8 @@ var HandlebarsCompiled = class {
78790
78866
  hb,
78791
78867
  template: hb.compile(this.source, {
78792
78868
  strict: false,
78793
- noEscape: false,
78869
+ // office text is escaped when its markup is restored (office/template-text.ts)
78870
+ noEscape: ctx.mode === "office",
78794
78871
  preventIndent: true
78795
78872
  })
78796
78873
  };
@@ -78819,7 +78896,7 @@ var HandlebarsCompiled = class {
78819
78896
  };
78820
78897
  collect(this.source, 0);
78821
78898
  try {
78822
- const out = template(data ?? {}, {
78899
+ const out = template(withBrand(data, ctx), {
78823
78900
  partials,
78824
78901
  allowProtoPropertiesByDefault: false,
78825
78902
  allowProtoMethodsByDefault: false
@@ -78854,6 +78931,151 @@ var handlebarsEngine = {
78854
78931
 
78855
78932
  // packages/engine/src/lib/engines/jinja2.ts
78856
78933
  var import_nunjucks = __toESM(require_nunjucks());
78934
+
78935
+ // packages/engine/src/lib/engines/python-format.ts
78936
+ var SPEC = /^(?:([\s\S])?([<>=^]))?([+\- ])?(#)?(0)?(\d+)?([,_])?(?:\.(\d+))?([bcdeEfFgGnosxX%])?$/;
78937
+ function group(digits, separator) {
78938
+ return digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
78939
+ }
78940
+ function pythonExponent(text2) {
78941
+ return text2.replace(/e([+-])(\d)$/i, (m2, sign, digit) => `${m2[0]}${sign}0${digit}`);
78942
+ }
78943
+ function formatNumber(value, type, precision, alternate) {
78944
+ if (!Number.isFinite(value)) return Number.isNaN(value) ? "nan" : "inf";
78945
+ const abs = Math.abs(value);
78946
+ switch (type) {
78947
+ case "f":
78948
+ case "F":
78949
+ return abs.toFixed(precision ?? 6);
78950
+ case "%":
78951
+ return `${(abs * 100).toFixed(precision ?? 6)}%`;
78952
+ case "e":
78953
+ case "E": {
78954
+ const text2 = pythonExponent(abs.toExponential(precision ?? 6));
78955
+ return type === "E" ? text2.toUpperCase() : text2;
78956
+ }
78957
+ case "g":
78958
+ case "G": {
78959
+ const p = precision === 0 ? 1 : precision ?? 6;
78960
+ const exp = abs === 0 ? 0 : Math.floor(Math.log10(abs));
78961
+ let text2 = exp < -4 || exp >= p ? pythonExponent(abs.toExponential(p - 1)) : abs.toFixed(Math.max(0, p - 1 - exp));
78962
+ if (!alternate) {
78963
+ const [mantissa = "", exponent] = text2.split("e");
78964
+ const trimmed = mantissa.includes(".") ? mantissa.replace(/0+$/, "").replace(/\.$/, "") : mantissa;
78965
+ text2 = exponent === void 0 ? trimmed : `${trimmed}e${exponent}`;
78966
+ }
78967
+ return type === "G" ? text2.toUpperCase() : text2;
78968
+ }
78969
+ case "d":
78970
+ case "n":
78971
+ return String(Math.trunc(abs));
78972
+ case "b":
78973
+ return (alternate ? "0b" : "") + Math.trunc(abs).toString(2);
78974
+ case "o":
78975
+ return (alternate ? "0o" : "") + Math.trunc(abs).toString(8);
78976
+ case "x":
78977
+ return (alternate ? "0x" : "") + Math.trunc(abs).toString(16);
78978
+ case "X":
78979
+ return (alternate ? "0X" : "") + Math.trunc(abs).toString(16).toUpperCase();
78980
+ default:
78981
+ return precision === void 0 ? String(abs) : formatNumber(abs, "g", precision, alternate);
78982
+ }
78983
+ }
78984
+ function formatValue(value, spec) {
78985
+ const m2 = SPEC.exec(spec);
78986
+ if (!m2) return String(value ?? "");
78987
+ const [, fillChar, alignChar, sign, alt, zero, widthText, grouping, precisionText, type = ""] = m2;
78988
+ const precision = precisionText === void 0 ? void 0 : Number(precisionText);
78989
+ const width = widthText === void 0 ? 0 : Number(widthText);
78990
+ const numericType = /[bdeEfFgGnoxX%]/.test(type);
78991
+ const asNumber = typeof value === "number" ? value : typeof value === "boolean" ? Number(value) : numericType && value !== null && value !== void 0 && String(value).trim() !== "" && Number.isFinite(Number(value)) ? Number(value) : void 0;
78992
+ let body;
78993
+ let prefix = "";
78994
+ if (asNumber !== void 0 && type !== "s" && type !== "c") {
78995
+ body = formatNumber(asNumber, type, precision, Boolean(alt));
78996
+ if (grouping) {
78997
+ const [int = "", ...rest] = body.split(".");
78998
+ const radix = /^0[box]/i.exec(int)?.[0] ?? "";
78999
+ body = radix + group(int.slice(radix.length), grouping) + (rest.length ? `.${rest.join(".")}` : "");
79000
+ }
79001
+ const negative = asNumber < 0 || Object.is(asNumber, -0);
79002
+ prefix = negative ? "-" : sign === "+" ? "+" : sign === " " ? " " : "";
79003
+ } else if (type === "c" && asNumber !== void 0) {
79004
+ body = String.fromCodePoint(asNumber);
79005
+ } else {
79006
+ body = String(value ?? "None");
79007
+ if (precision !== void 0 && !numericType) body = body.slice(0, precision);
79008
+ }
79009
+ const isNumber = asNumber !== void 0 && type !== "s";
79010
+ const align = alignChar ?? (zero && isNumber ? "=" : isNumber ? ">" : "<");
79011
+ const fill = fillChar ?? (zero && !alignChar ? "0" : " ");
79012
+ const pad = Math.max(0, width - prefix.length - body.length);
79013
+ switch (align) {
79014
+ case "<":
79015
+ return prefix + body + fill.repeat(pad);
79016
+ case "^":
79017
+ return fill.repeat(Math.floor(pad / 2)) + prefix + body + fill.repeat(Math.ceil(pad / 2));
79018
+ case "=":
79019
+ return prefix + fill.repeat(pad) + body;
79020
+ default:
79021
+ return fill.repeat(pad) + prefix + body;
79022
+ }
79023
+ }
79024
+ function lookupField(name, positional, named, next) {
79025
+ const [head = "", ...rest] = name.split(/(?=[.[])/);
79026
+ let value = head === "" ? positional[next()] : /^\d+$/.test(head) ? positional[Number(head)] : named[head];
79027
+ for (const part of rest) {
79028
+ const key = part.startsWith("[") ? part.slice(1, -1) : part.slice(1);
79029
+ value = value !== null && typeof value === "object" ? value[Array.isArray(value) && /^\d+$/.test(key) ? Number(key) : key] : void 0;
79030
+ }
79031
+ return value;
79032
+ }
79033
+ function pythonFormat(template, positional, named = {}) {
79034
+ let auto = 0;
79035
+ const next = () => auto++;
79036
+ const render = (text2) => {
79037
+ let out = "";
79038
+ for (let i = 0; i < text2.length; i++) {
79039
+ const ch = text2[i];
79040
+ if (ch === "}") {
79041
+ out += "}";
79042
+ if (text2[i + 1] === "}") i++;
79043
+ continue;
79044
+ }
79045
+ if (ch !== "{") {
79046
+ out += ch;
79047
+ continue;
79048
+ }
79049
+ if (text2[i + 1] === "{") {
79050
+ out += "{";
79051
+ i++;
79052
+ continue;
79053
+ }
79054
+ let depth = 1;
79055
+ let j2 = i + 1;
79056
+ for (; j2 < text2.length && depth > 0; j2++) {
79057
+ if (text2[j2] === "{") depth++;
79058
+ else if (text2[j2] === "}") depth--;
79059
+ }
79060
+ if (depth > 0) {
79061
+ out += text2.slice(i);
79062
+ break;
79063
+ }
79064
+ const field = text2.slice(i + 1, j2 - 1);
79065
+ const colon = field.indexOf(":");
79066
+ const head = colon < 0 ? field : field.slice(0, colon);
79067
+ const name = head.replace(/![rsa]$/, "");
79068
+ const value = lookupField(name, positional, named, next);
79069
+ const spec = colon < 0 ? "" : render(field.slice(colon + 1));
79070
+ out += value === void 0 ? "" : formatValue(value, spec);
79071
+ i = j2 - 1;
79072
+ }
79073
+ return out;
79074
+ };
79075
+ return render(template);
79076
+ }
79077
+
79078
+ // packages/engine/src/lib/engines/jinja2.ts
78857
79079
  var nunjucksBuiltinFilters = /* @__PURE__ */ new Set([
78858
79080
  "abs",
78859
79081
  "batch",
@@ -78899,6 +79121,7 @@ var nunjucksBuiltinFilters = /* @__PURE__ */ new Set([
78899
79121
  "urlize",
78900
79122
  "wordcount"
78901
79123
  ]);
79124
+ var jinjaLiterals = /* @__PURE__ */ new Set(["True", "False", "None"]);
78902
79125
  var builtinRoots2 = /* @__PURE__ */ new Set([
78903
79126
  "loop",
78904
79127
  "caller",
@@ -78908,7 +79131,14 @@ var builtinRoots2 = /* @__PURE__ */ new Set([
78908
79131
  "joiner",
78909
79132
  "lipsum",
78910
79133
  "varargs",
78911
- "kwargs"
79134
+ "kwargs",
79135
+ "namespace",
79136
+ // the organisation's brand kit (spec 18), a global beside the data
79137
+ "brand",
79138
+ // Jinja2's spelling of the literals, which the compat layer resolves at runtime
79139
+ "True",
79140
+ "False",
79141
+ "None"
78912
79142
  ]);
78913
79143
  var stringMethods = {
78914
79144
  upper: (s) => s.toUpperCase(),
@@ -78937,12 +79167,8 @@ var stringMethods = {
78937
79167
  split: (s, sep, max) => sep === void 0 || sep === null ? s.trim().split(/\s+/) : s.split(String(sep), max === void 0 ? void 0 : Number(max) + 1),
78938
79168
  join: (s, list2) => Array.isArray(list2) ? list2.join(s) : String(list2),
78939
79169
  format: (s, ...args2) => {
78940
- let i = 0;
78941
- const named = args2.at(-1) && typeof args2.at(-1) === "object" ? args2.at(-1) : {};
78942
- return s.replace(
78943
- /\{(\w*)\}/g,
78944
- (_2, key) => String(key ? named[key] ?? "" : args2[i++] ?? "")
78945
- );
79170
+ const named = keywordArgs(args2);
79171
+ return pythonFormat(s, named ? args2.slice(0, -1) : args2, named ?? {});
78946
79172
  },
78947
79173
  zfill: (s, width) => s.padStart(Number(width), "0"),
78948
79174
  count: (s, sub) => s.split(String(sub)).length - 1,
@@ -78957,6 +79183,92 @@ var stringMethods = {
78957
79183
  return f2.repeat(left) + s + f2.repeat(total - left);
78958
79184
  }
78959
79185
  };
79186
+ function keywordArgs(args2) {
79187
+ const last = args2.at(-1);
79188
+ if (!last || typeof last !== "object" || !Object.prototype.hasOwnProperty.call(last, "__keywords")) return void 0;
79189
+ const { __keywords: _marker, ...named } = last;
79190
+ return named;
79191
+ }
79192
+ var JinjaNamespace = class {
79193
+ };
79194
+ function namespace(...args2) {
79195
+ const ns = new JinjaNamespace();
79196
+ const named = keywordArgs(args2);
79197
+ for (const arg of named ? [...args2.slice(0, -1), named] : args2)
79198
+ if (arg && typeof arg === "object") Object.assign(ns, arg);
79199
+ return ns;
79200
+ }
79201
+ function setNamespaceAttribute(target, key, value) {
79202
+ if (!(target instanceof JinjaNamespace)) throw new Error("Cannot assign attribute on non-namespace object");
79203
+ target[String(key)] = value;
79204
+ }
79205
+ var toList = (v2) => Array.isArray(v2) ? v2 : v2 === void 0 || v2 === null ? [] : typeof v2 === "string" ? [...v2] : [v2];
79206
+ var attributeOf = (item, path) => String(path ?? "").split(".").reduce((acc, key) => acc !== null && typeof acc === "object" ? acc[key] : void 0, item);
79207
+ var jinjaTests = {
79208
+ none: (v2) => v2 === null,
79209
+ true: (v2) => v2 === true,
79210
+ false: (v2) => v2 === false,
79211
+ in: (v2, container) => typeof container === "string" ? container.includes(String(v2)) : Array.isArray(container) ? container.includes(v2) : !!container && typeof container === "object" && String(v2) in container,
79212
+ "==": (v2, a) => v2 === a,
79213
+ "!=": (v2, a) => v2 !== a,
79214
+ ">": (v2, a) => v2 > a,
79215
+ ">=": (v2, a) => v2 >= a,
79216
+ "<": (v2, a) => v2 < a,
79217
+ "<=": (v2, a) => v2 <= a
79218
+ };
79219
+ function runTest(self2, name, value, arg) {
79220
+ const own = jinjaTests[name];
79221
+ if (own) return own(value, arg);
79222
+ return self2.env.getTest(name).call(self2, value, arg) === true;
79223
+ }
79224
+ function installJinjaFilters(env) {
79225
+ const lookup = env;
79226
+ const helperTruncate = lookup.getFilter("truncate");
79227
+ lookup.addFilter("truncate", function(value, ...args2) {
79228
+ const named = keywordArgs(args2);
79229
+ const p = named ? args2.slice(0, -1) : args2;
79230
+ if (!named && typeof p[1] !== "boolean" && p.length < 3) return helperTruncate.call(this, value, ...args2);
79231
+ const s = String(value ?? "");
79232
+ const length = Number(p[0] ?? named?.["length"] ?? 255);
79233
+ const killwords = Boolean(p[1] ?? named?.["killwords"] ?? false);
79234
+ const end = String(p[2] ?? named?.["end"] ?? "...");
79235
+ const leeway = Number(p[3] ?? named?.["leeway"] ?? 5);
79236
+ if (s.length <= length + leeway) return s;
79237
+ const cut = s.slice(0, Math.max(0, length - end.length));
79238
+ if (killwords) return cut + end;
79239
+ const space = cut.lastIndexOf(" ");
79240
+ return (space >= 0 ? cut.slice(0, space) : cut) + end;
79241
+ });
79242
+ const helperMap = lookup.getFilter("map");
79243
+ lookup.addFilter("map", function(value, ...args2) {
79244
+ const named = keywordArgs(args2);
79245
+ const p = named ? args2.slice(0, -1) : args2;
79246
+ if (named && "attribute" in named)
79247
+ return toList(value).map((item) => {
79248
+ const v2 = attributeOf(item, named["attribute"]);
79249
+ return v2 === void 0 && "default" in named ? named["default"] : v2;
79250
+ });
79251
+ const items = toList(value);
79252
+ if (typeof p[0] === "string" && !items.some((item) => item !== null && typeof item === "object")) {
79253
+ let filter;
79254
+ try {
79255
+ filter = lookup.getFilter(p[0]);
79256
+ } catch {
79257
+ filter = void 0;
79258
+ }
79259
+ if (filter) return items.map((item) => filter.call(this, item, ...p.slice(1)));
79260
+ }
79261
+ return helperMap.call(this, value, ...args2);
79262
+ });
79263
+ const byAttribute = (keep) => function(value, attribute, test, arg) {
79264
+ return toList(value).filter((item) => {
79265
+ const v2 = attributeOf(item, attribute);
79266
+ return (test === void 0 ? Boolean(v2) : runTest(this, String(test), v2, arg)) === keep;
79267
+ });
79268
+ };
79269
+ lookup.addFilter("selectattr", byAttribute(true));
79270
+ lookup.addFilter("rejectattr", byAttribute(false));
79271
+ }
78960
79272
  function trimChars(s, chars, side) {
78961
79273
  if (chars === void 0 || chars === null)
78962
79274
  return side === "both" ? s.trim() : side === "left" ? s.trimStart() : s.trimEnd();
@@ -78977,6 +79289,20 @@ function installCompat() {
78977
79289
  this.compile(node.target, frame);
78978
79290
  this._emit(")");
78979
79291
  };
79292
+ const compileSet = import_nunjucks.default.compiler.Compiler.prototype.compileSet;
79293
+ import_nunjucks.default.runtime["formfeedSetAttribute"] = setNamespaceAttribute;
79294
+ import_nunjucks.default.compiler.Compiler.prototype.compileSet = function(node, frame) {
79295
+ const [target, ...more] = node.targets;
79296
+ if (!target || more.length || target.typename !== "LookupVal") return compileSet.call(this, node, frame);
79297
+ this._emit("runtime.formfeedSetAttribute(");
79298
+ this._compileExpression(target["target"], frame);
79299
+ this._emit(", ");
79300
+ this._compileExpression(target["val"], frame);
79301
+ this._emit(", ");
79302
+ if (node.value) this._compileExpression(node.value, frame);
79303
+ else this.compile(node.body, frame);
79304
+ this._emitLine(");");
79305
+ };
78980
79306
  const original = import_nunjucks.default.runtime.memberLookup;
78981
79307
  import_nunjucks.default.runtime.memberLookup = (obj, val2, ...rest) => {
78982
79308
  if (typeof obj === "string" && typeof val2 === "string" && Object.prototype.hasOwnProperty.call(stringMethods, val2)) {
@@ -79009,7 +79335,8 @@ function createEnvironment(ctx) {
79009
79335
  const env = new import_nunjucks.default.Environment(
79010
79336
  new PartialLoader(ctx.partials),
79011
79337
  {
79012
- autoescape: true,
79338
+ // office text is escaped when its markup is restored, so escaping here would double it
79339
+ autoescape: ctx.mode !== "office",
79013
79340
  throwOnUndefined: false,
79014
79341
  trimBlocks: false,
79015
79342
  lstripBlocks: false
@@ -79023,6 +79350,9 @@ function createEnvironment(ctx) {
79023
79350
  env.addFilter(name, wrapped);
79024
79351
  env.addGlobal(name, wrapped);
79025
79352
  }
79353
+ env.addGlobal("namespace", namespace);
79354
+ if (ctx.brand) env.addGlobal("brand", ctx.brand);
79355
+ installJinjaFilters(env);
79026
79356
  return env;
79027
79357
  }
79028
79358
  var validationContext2 = {
@@ -79063,18 +79393,28 @@ function pathOf(node) {
79063
79393
  function inScope(w, name) {
79064
79394
  return w.scopes.some((s) => s.has(name));
79065
79395
  }
79396
+ function sourceOf(w, name) {
79397
+ for (let i = w.scopes.length - 1; i >= 0; i--) {
79398
+ const scope = w.scopes[i];
79399
+ if (scope?.has(name)) return scope.get(name) ?? null;
79400
+ }
79401
+ return null;
79402
+ }
79066
79403
  function walk2(node, w) {
79067
79404
  if (!node || typeof node !== "object") return;
79068
79405
  switch (node.typename) {
79069
79406
  case "Symbol":
79070
79407
  case "LookupVal": {
79071
79408
  const p = pathOf(node);
79409
+ if (node.typename === "Symbol" && jinjaLiterals.has(String(node.value))) return;
79072
79410
  if (p) {
79073
79411
  const root = p.path[0] ?? "";
79412
+ const bound = inScope(w, root);
79074
79413
  w.variables.push({
79075
79414
  path: p.path,
79076
79415
  range: nodeRange(p.root, p.path.join(".").length),
79077
- kind: inScope(w, root) ? "loop-var" : "read"
79416
+ kind: bound ? "loop-var" : "read",
79417
+ ...bound ? { source: sourceOf(w, root) } : {}
79078
79418
  });
79079
79419
  }
79080
79420
  if (node.typename === "LookupVal" && !p) {
@@ -79120,18 +79460,18 @@ function walk2(node, w) {
79120
79460
  const source = pathOf(arr);
79121
79461
  const nameNode = node["name"];
79122
79462
  const names = nameNode.typename === "Array" ? nameNode.children ?? [] : [nameNode];
79123
- const scope = /* @__PURE__ */ new Set();
79463
+ const root = source?.path[0] ?? "";
79464
+ const items = !source ? null : inScope(w, root) ? ((s) => s ? [...s, ...source.path.slice(1)] : null)(sourceOf(w, root)) : source.path;
79465
+ const scope = /* @__PURE__ */ new Map();
79124
79466
  for (const n of names) {
79125
79467
  const name = String(n.value);
79126
- scope.add(name);
79127
- w.loopSources.set(
79128
- name,
79129
- names.length === 1 && source && !inScope(w, source.path[0] ?? "") ? source.path : names.length === 1 && source ? resolveLoopSource(w, source.path) : null
79130
- );
79468
+ const bound = names.length === 1 ? items : null;
79469
+ scope.set(name, bound);
79131
79470
  w.variables.push({
79132
79471
  path: [name],
79133
79472
  range: nodeRange(n, name.length),
79134
- kind: "loop-var"
79473
+ kind: "loop-var",
79474
+ source: bound
79135
79475
  });
79136
79476
  }
79137
79477
  w.scopes.push(scope);
@@ -79149,8 +79489,7 @@ function walk2(node, w) {
79149
79489
  range: nodeRange(p.root, p.path.join(".").length),
79150
79490
  kind: "assigned"
79151
79491
  });
79152
- w.scopes[0]?.add(p.path[0] ?? "");
79153
- w.loopSources.set(p.path[0] ?? "", null);
79492
+ if (p.path.length === 1 || !inScope(w, p.path[0] ?? "")) w.scopes[0]?.set(p.path[0] ?? "", null);
79154
79493
  }
79155
79494
  }
79156
79495
  walk2(node["value"], w);
@@ -79159,17 +79498,27 @@ function walk2(node, w) {
79159
79498
  }
79160
79499
  case "Macro": {
79161
79500
  const args2 = node["args"];
79162
- const scope = /* @__PURE__ */ new Set();
79163
- for (const child of args2.children ?? [])
79164
- if (child.typename === "Symbol") scope.add(String(child.value));
79501
+ const scope = /* @__PURE__ */ new Map();
79502
+ for (const child of args2.children ?? []) {
79503
+ if (child.typename === "Symbol") scope.set(String(child.value), null);
79504
+ if (child.typename === "KeywordArgs")
79505
+ for (const pair of child.children ?? []) {
79506
+ const key = pair["key"];
79507
+ if (key) scope.set(String(key.value), null);
79508
+ walk2(pair["value"], w);
79509
+ }
79510
+ }
79165
79511
  const nameNode = node["name"];
79166
- w.scopes[0]?.add(String(nameNode.value));
79167
- w.loopSources.set(String(nameNode.value), null);
79512
+ w.scopes[0]?.set(String(nameNode.value), null);
79168
79513
  w.scopes.push(scope);
79169
79514
  walk2(node["body"], w);
79170
79515
  w.scopes.pop();
79171
79516
  return;
79172
79517
  }
79518
+ case "Pair": {
79519
+ walk2(node["value"], w);
79520
+ return;
79521
+ }
79173
79522
  case "Include":
79174
79523
  case "Extends":
79175
79524
  case "Import":
@@ -79182,15 +79531,17 @@ function walk2(node, w) {
79182
79531
  });
79183
79532
  else walk2(tpl, w);
79184
79533
  if (node.typename === "Import")
79185
- w.scopes[0]?.add(
79186
- String(node["target"]?.value ?? "")
79534
+ w.scopes[0]?.set(
79535
+ String(node["target"]?.value ?? ""),
79536
+ null
79187
79537
  );
79188
79538
  if (node.typename === "FromImport")
79189
79539
  for (const n of node["names"].children ?? [])
79190
- w.scopes[0]?.add(
79540
+ w.scopes[0]?.set(
79191
79541
  String(
79192
79542
  n.typename === "Pair" ? n["value"].value : n.value
79193
- )
79543
+ ),
79544
+ null
79194
79545
  );
79195
79546
  return;
79196
79547
  }
@@ -79208,12 +79559,6 @@ function walk2(node, w) {
79208
79559
  walk2(value, w);
79209
79560
  }
79210
79561
  }
79211
- function resolveLoopSource(w, path) {
79212
- const root = path[0] ?? "";
79213
- if (!w.loopSources.has(root)) return path;
79214
- const source = w.loopSources.get(root);
79215
- return source ? [...source, ...path.slice(1)] : null;
79216
- }
79217
79562
  function analyzeJinja2(source, opts = {}) {
79218
79563
  const helpers = opts.helpers ?? defaultHelpers();
79219
79564
  const { blocks, diagnostics } = scanBlocks(source, jinjaTags);
@@ -79221,8 +79566,7 @@ function analyzeJinja2(source, opts = {}) {
79221
79566
  variables: [],
79222
79567
  filters: [],
79223
79568
  includes: [],
79224
- loopSources: /* @__PURE__ */ new Map(),
79225
- scopes: [/* @__PURE__ */ new Set()],
79569
+ scopes: [/* @__PURE__ */ new Map()],
79226
79570
  helpers
79227
79571
  };
79228
79572
  try {
@@ -79251,6 +79595,13 @@ function analyzeJinja2(source, opts = {}) {
79251
79595
  range: f2.range,
79252
79596
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
79253
79597
  });
79598
+ } else if (unavailableHelper(f2.name)) {
79599
+ diagnostics.push({
79600
+ severity: "warning",
79601
+ code: "unavailable-helper",
79602
+ message: unavailableHelper(f2.name),
79603
+ range: f2.range
79604
+ });
79254
79605
  } else if (!f2.known) {
79255
79606
  diagnostics.push({
79256
79607
  severity: "error",
@@ -79264,7 +79615,7 @@ function analyzeJinja2(source, opts = {}) {
79264
79615
  const dataDiagnostics = missingPathDiagnostics(
79265
79616
  w.variables,
79266
79617
  opts.sampleData,
79267
- w.loopSources,
79618
+ /* @__PURE__ */ new Map(),
79268
79619
  builtin
79269
79620
  );
79270
79621
  return {
@@ -79397,6 +79748,10 @@ var liquidBuiltinFilters = /* @__PURE__ */ new Set([
79397
79748
  "url_decode",
79398
79749
  "url_encode",
79399
79750
  "where",
79751
+ "where_exp",
79752
+ "find_exp",
79753
+ "reject_exp",
79754
+ "group_by_exp",
79400
79755
  "raw",
79401
79756
  "sum",
79402
79757
  "group_by",
@@ -79418,6 +79773,8 @@ var liquidBuiltinFilters = /* @__PURE__ */ new Set([
79418
79773
  "time_zone"
79419
79774
  ]);
79420
79775
  var builtinRoots3 = /* @__PURE__ */ new Set([
79776
+ // the organisation's brand kit (spec 18), a global beside the data
79777
+ "brand",
79421
79778
  "forloop",
79422
79779
  "tablerowloop",
79423
79780
  "true",
@@ -79483,6 +79840,8 @@ function createLiquid(ctx) {
79483
79840
  extname: "",
79484
79841
  root: [""],
79485
79842
  fs: partialFs(ctx.partials),
79843
+ // scope data shadows globals, so `data.brand` still wins
79844
+ globals: ctx.brand ? { brand: ctx.brand } : {},
79486
79845
  renderLimit: ctx.limits.ms,
79487
79846
  memoryLimit: ctx.limits.outputBytes * 4,
79488
79847
  parseLimit: 2e6
@@ -79651,6 +80010,13 @@ function analyzeLiquid(source, opts = {}) {
79651
80010
  range: f2.range,
79652
80011
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
79653
80012
  });
80013
+ } else if (unavailableHelper(f2.name)) {
80014
+ diagnostics.push({
80015
+ severity: "warning",
80016
+ code: "unavailable-helper",
80017
+ message: unavailableHelper(f2.name),
80018
+ range: f2.range
80019
+ });
79654
80020
  } else if (!f2.known) {
79655
80021
  diagnostics.push({
79656
80022
  severity: "error",
@@ -79746,6 +80112,26 @@ function getEngine(id) {
79746
80112
  return engine;
79747
80113
  }
79748
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
+
79749
80135
  // packages/engine/src/lib/schema.ts
79750
80136
  var isoDateTime = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/;
79751
80137
  var isoDate = /^\d{4}-\d{2}-\d{2}$/;
@@ -79836,9 +80222,12 @@ function inferSchema(sample) {
79836
80222
  };
79837
80223
  }
79838
80224
 
80225
+ // packages/engine/src/lib/output.ts
80226
+ var outputFormats = ["pdf", "png", "jpg", "webp", "docx", "pptx"];
80227
+
79839
80228
  // packages/engine/src/lib/snippets.ts
79840
80229
  var loop = (engine, item, list2, body) => engine === "handlebars" ? `{{#each ${list2}}}
79841
- ${body.replace(/\{\{ item\./g, "{{ ")}
80230
+ ${body.replace(new RegExp(`(?<![\\w.])${item}\\.`, "g"), "")}
79842
80231
  {{/each}}` : `{% for ${item} in ${list2} %}
79843
80232
  ${body}
79844
80233
  {% endfor %}`;
@@ -79848,6 +80237,15 @@ var val = (engine, expr, filter) => {
79848
80237
  return `{{${filter.split("(")[0]} ${expr}${filter.includes("(") ? " " + filter.slice(filter.indexOf("(") + 1, -1) : ""}}}`;
79849
80238
  return engine === "liquid" ? `{{ ${expr} | ${filter.replace("(", ": ").replace(")", "")} }}` : `{{ ${expr} | ${filter} }}`;
79850
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
+ };
79851
80249
  function forAll(build) {
79852
80250
  return {
79853
80251
  jinja2: build("jinja2"),
@@ -79855,14 +80253,101 @@ function forAll(build) {
79855
80253
  handlebars: build("handlebars")
79856
80254
  };
79857
80255
  }
80256
+ var rule = "var(--brand-color-primary, #222)";
79858
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
+ },
79859
80343
  {
79860
80344
  id: "invoice-table",
80345
+ group: "invoice",
79861
80346
  title: "Invoice table",
79862
80347
  description: "Line items with description, quantity, unit price and line total.",
79863
80348
  source: forAll(
79864
80349
  (e) => `<table class="ff-lines">
79865
- <thead><tr><th>Description</th><th class="num">Qty</th><th class="num">Unit price</th><th class="num">Total</th></tr></thead>
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>
79866
80351
  <tbody>
79867
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>`)}
79868
80353
  </tbody>
@@ -79879,17 +80364,32 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
79879
80364
  css: `.ff-lines { width: 100%; border-collapse: collapse; font-size: 10pt; }
79880
80365
  .ff-lines th, .ff-lines td { padding: 6px 8px; border-bottom: 1px solid #ddd; text-align: left; }
79881
80366
  .ff-lines .num { text-align: right; white-space: nowrap; }
79882
- .ff-lines thead th { border-bottom: 2px solid #222; }`
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
+ }
79883
80382
  },
79884
80383
  {
79885
80384
  id: "totals",
80385
+ group: "invoice",
79886
80386
  title: "Totals block",
79887
80387
  description: "Net, VAT and gross totals right-aligned under a table.",
79888
80388
  source: forAll(
79889
80389
  (e) => `<table class="ff-totals">
79890
- <tr><td>Net</td><td class="num">${val(e, "invoice.net", "money")}</td></tr>
79891
- <tr><td>VAT ${val(e, "invoice.vat_rate")} %</td><td class="num">${val(e, "invoice.vat", "money")}</td></tr>
79892
- <tr class="grand"><td>Total</td><td class="num">${val(e, "invoice.total", "money")}</td></tr>
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>
79893
80393
  </table>$0`
79894
80394
  ),
79895
80395
  sampleData: {
@@ -79898,10 +80398,103 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
79898
80398
  css: `.ff-totals { margin-left: auto; margin-top: 12px; border-collapse: collapse; font-size: 10pt; }
79899
80399
  .ff-totals td { padding: 4px 8px; }
79900
80400
  .ff-totals .num { text-align: right; min-width: 90px; }
79901
- .ff-totals .grand td { border-top: 2px solid #222; font-weight: 600; }`
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
+ }
79902
80494
  },
79903
80495
  {
79904
80496
  id: "address-block",
80497
+ group: "letter",
79905
80498
  title: "Address block",
79906
80499
  description: "Recipient name and postal address lines.",
79907
80500
  source: forAll(
@@ -79914,10 +80507,10 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
79914
80507
  ),
79915
80508
  sampleData: {
79916
80509
  customer: {
79917
- name: "Muster GmbH",
79918
- street: "Musterstra\xDFe 1",
79919
- zip: "10115",
79920
- city: "Berlin",
80510
+ name: "Olvarest GmbH",
80511
+ street: "Beispielweg 2",
80512
+ zip: "54321",
80513
+ city: "Beispielstadt",
79921
80514
  country: "Deutschland"
79922
80515
  }
79923
80516
  },
@@ -79925,18 +80518,38 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
79925
80518
  },
79926
80519
  {
79927
80520
  id: "page-footer",
80521
+ group: "page",
80522
+ kinds: ["pdf"],
79928
80523
  title: "Footer with page numbers",
79929
- description: "Paste into the Footer tab: company line and \u201CPage x of y\u201D.",
80524
+ description: "Company line and \u201CPage x of y\u201D in the page footer.",
80525
+ target: "footer",
79930
80526
  source: forAll(
79931
80527
  (e) => `<div style="width:100%;display:flex;justify-content:space-between;font-size:9px;color:#666">
79932
80528
  <span>${val(e, "company.name")} \xB7 ${val(e, "company.email")}</span>
79933
- <span>Page <span class="pageNumber"></span> of <span class="totalPages"></span></span>
80529
+ <span>${tr(e, "footer.page")} <span class="pageNumber"></span> ${tr(e, "footer.of")} <span class="totalPages"></span></span>
79934
80530
  </div>$0`
79935
80531
  ),
79936
- sampleData: { company: { name: "Acme GmbH", email: "hello@acme.example" } }
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
+ )
79937
80548
  },
79938
80549
  {
79939
80550
  id: "cover-page",
80551
+ group: "report",
80552
+ kinds: ["pdf"],
79940
80553
  title: "Cover page",
79941
80554
  description: "Title, subtitle and date on a page of its own.",
79942
80555
  source: forAll(
@@ -79955,12 +80568,13 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
79955
80568
  }
79956
80569
  },
79957
80570
  css: `.ff-cover { min-height: 60vh; display: flex; flex-direction: column; justify-content: center; }
79958
- .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; }
79959
80572
  .ff-cover .subtitle { font-size: 16pt; color: #444; margin: 0; }
79960
80573
  .ff-cover .date { margin-top: 24px; color: #666; }`
79961
80574
  },
79962
80575
  {
79963
80576
  id: "signature",
80577
+ group: "letter",
79964
80578
  title: "Signature line",
79965
80579
  description: "Place, date and a signature line.",
79966
80580
  source: forAll(
@@ -79973,7 +80587,7 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
79973
80587
  signature: {
79974
80588
  place: "Berlin",
79975
80589
  date: "2026-09-07",
79976
- name: "Philipp Staudt"
80590
+ name: "Erika Mustermann"
79977
80591
  }
79978
80592
  },
79979
80593
  css: `.ff-signature { margin-top: 48px; width: 260px; font-size: 9pt; color: #444; }
@@ -79981,33 +80595,32 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
79981
80595
  },
79982
80596
  {
79983
80597
  id: "epc-qr",
80598
+ group: "invoice",
79984
80599
  title: "SEPA payment QR code",
79985
80600
  description: "GiroCode that banking apps scan to prefill the transfer.",
79986
80601
  source: {
79987
80602
  jinja2: `<figure class="ff-qr">
79988
- <img src="{{ epcQr({ name: company.name, iban: company.iban, bic: company.bic, amount: invoice.total, reference: invoice.number }, { size: 120 }) }}" alt="Payment QR code">
79989
- <figcaption>Scan to pay</figcaption>
80603
+ <img src="{{ epcQr(payment, { size: 120 }) }}" alt="${tr("jinja2", "qr.alt")}">
80604
+ <figcaption>${tr("jinja2", "qr.caption")}</figcaption>
79990
80605
  </figure>$0`,
79991
80606
  liquid: `<figure class="ff-qr">
79992
- <img src="{{ payment | epcQr }}" alt="Payment QR code">
79993
- <figcaption>Scan to pay</figcaption>
80607
+ <img src="{{ payment | epcQr: size: 120 }}" alt="${tr("liquid", "qr.alt")}">
80608
+ <figcaption>${tr("liquid", "qr.caption")}</figcaption>
79994
80609
  </figure>$0`,
79995
80610
  handlebars: `<figure class="ff-qr">
79996
- <img src="{{epcQr payment size=120}}" alt="Payment QR code">
79997
- <figcaption>Scan to pay</figcaption>
80611
+ <img src="{{epcQr payment size=120}}" alt="${tr("handlebars", "qr.alt")}">
80612
+ <figcaption>${tr("handlebars", "qr.caption")}</figcaption>
79998
80613
  </figure>$0`
79999
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
+ },
80000
80619
  sampleData: {
80001
- company: {
80002
- name: "Acme GmbH",
80003
- iban: "DE02120300000000202051",
80004
- bic: "BYLADEM1001"
80005
- },
80006
- invoice: { number: "2026-0042", total: 3760.4 },
80007
80620
  payment: {
80008
- name: "Acme GmbH",
80009
- iban: "DE02120300000000202051",
80010
- bic: "BYLADEM1001",
80621
+ name: "Fennlor Studio GmbH",
80622
+ iban: "DE00000000000000000000",
80623
+ bic: "XXXXDEXXXXX",
80011
80624
  amount: 3760.4,
80012
80625
  reference: "2026-0042"
80013
80626
  }
@@ -80017,38 +80630,29 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
80017
80630
  },
80018
80631
  {
80019
80632
  id: "chart",
80633
+ group: "report",
80020
80634
  title: "Chart",
80021
- description: "Bar chart drawn with Chart.js from a chart configuration in the data.",
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
80022
80637
  source: {
80023
80638
  jinja2: `<figure class="ff-chart-box">
80024
- {{ chart(salesChart) }}
80025
- <figcaption>{{ salesChart.caption }}</figcaption>
80639
+ {{ chart(sales, { type: '\${1:bar}', width: 480, height: 240, color: brand.colors.primary }) }}
80640
+ <figcaption>{{ sales.caption }}</figcaption>
80026
80641
  </figure>$0`,
80027
80642
  liquid: `<figure class="ff-chart-box">
80028
- {{ salesChart | chart }}
80029
- <figcaption>{{ salesChart.caption }}</figcaption>
80643
+ {{ sales | chart: type: '\${1:bar}', width: 480, height: 240, color: brand.colors.primary }}
80644
+ <figcaption>{{ sales.caption }}</figcaption>
80030
80645
  </figure>$0`,
80031
80646
  handlebars: `<figure class="ff-chart-box">
80032
- {{chart salesChart}}
80033
- <figcaption>{{salesChart.caption}}</figcaption>
80647
+ {{chart sales type='\${1:bar}' width=480 height=240 color=brand.colors.primary}}
80648
+ <figcaption>{{sales.caption}}</figcaption>
80034
80649
  </figure>$0`
80035
80650
  },
80036
80651
  sampleData: {
80037
- salesChart: {
80038
- type: "bar",
80652
+ sales: {
80039
80653
  caption: "Revenue per quarter",
80040
- width: 480,
80041
- height: 240,
80042
- data: {
80043
- labels: ["Q1", "Q2", "Q3", "Q4"],
80044
- datasets: [
80045
- {
80046
- label: "Revenue",
80047
- data: [12e3, 15500, 14200, 18900],
80048
- backgroundColor: "#3E63DD"
80049
- }
80050
- ]
80051
- }
80654
+ labels: ["Q1", "Q2", "Q3", "Q4"],
80655
+ values: [12e3, 15500, 14200, 18900]
80052
80656
  }
80053
80657
  },
80054
80658
  css: `.ff-chart-box { margin: 16px 0; break-inside: avoid; }
@@ -80056,6 +80660,7 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
80056
80660
  },
80057
80661
  {
80058
80662
  id: "two-columns",
80663
+ group: "page",
80059
80664
  title: "Two-column layout",
80060
80665
  description: "Two equal columns that stay side by side in print.",
80061
80666
  source: forAll(
@@ -80066,10 +80671,102 @@ ${loop(e, "line", "invoice.lines", ` <tr><td>${val(e, "line.description")}</t
80066
80671
  ),
80067
80672
  css: `.ff-columns { display: flex; gap: 24px; }
80068
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; }`
80069
80718
  }
80070
80719
  ];
80071
80720
 
80721
+ // packages/api-types/src/lib/assets.ts
80722
+ var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
80723
+ var ASSET_NAME_PATTERN = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*$`);
80724
+ var ASSET_MAX_BYTES = 10 * 1024 * 1024;
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
+
80072
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
+ }
80073
80770
  var FormfeedError = class extends Error {
80074
80771
  constructor(code, message2, status, problem = null, requestId = null) {
80075
80772
  super(message2);
@@ -80096,6 +80793,8 @@ function randomKey() {
80096
80793
  if (c?.randomUUID) return c.randomUUID();
80097
80794
  return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
80098
80795
  }
80796
+ var withKey = (options) => ({ ...options, idempotencyKey: options.idempotencyKey ?? randomKey() });
80797
+ var idOf = (source) => typeof source === "string" ? source : source.id;
80099
80798
  var Formfeed = class {
80100
80799
  baseUrl;
80101
80800
  apiKey;
@@ -80159,11 +80858,96 @@ var Formfeed = class {
80159
80858
  },
80160
80859
  /** Removes the stored files of a render before they expire. Needs the `file:delete` scope. */
80161
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),
80162
80866
  batch: (request, options = {}) => this.request("POST", "/renders/batch", request, {
80163
80867
  ...options,
80164
80868
  idempotencyKey: options.idempotencyKey ?? randomKey()
80165
80869
  })
80166
80870
  };
80871
+ /**
80872
+ * PDF tools on the outputs of earlier renders (a `Render` or its id) and on PDFs of the file library
80873
+ * (a `LibraryFile`, its id or its name). merge, protect and watermark return a new render and cost
80874
+ * 0.5 units each on live keys; info is free.
80875
+ */
80876
+ pdf = {
80877
+ merge: (sources, output = {}, options = {}) => this.request("POST", "/pdf/merge", { ...output, sources: sources.map(idOf) }, withKey(options)),
80878
+ protect: (source, protect, options = {}) => this.request("POST", "/pdf/protect", { ...protect, source: idOf(source) }, withKey(options)),
80879
+ watermark: (source, watermark, options = {}) => this.request("POST", "/pdf/watermark", { ...watermark, source: idOf(source) }, withKey(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
+ }
80892
+ };
80893
+ /**
80894
+ * The workspace file library (`/files`): images and PDFs a template references by name with
80895
+ * `asset('logo.png')`, and the sources of image watermarks and merges. Needs `file:read`,
80896
+ * `file:write` or `file:delete`. Files are served without a signature, so keep confidential
80897
+ * documents out of it.
80898
+ */
80899
+ files = {
80900
+ /** Uploads a file; the same name replaces the existing file in place and keeps its URL. */
80901
+ upload: (file, options = {}) => {
80902
+ const blob = file.data instanceof Blob ? file.data : new Blob([file.data], file.contentType ? { type: file.contentType } : {});
80903
+ const form = new FormData();
80904
+ form.set("file", blob, file.name);
80905
+ form.set("name", file.name);
80906
+ return this.request("POST", "/files", form, options);
80907
+ },
80908
+ /** Page of files, newest first. */
80909
+ list: (query = {}, options = {}) => {
80910
+ const params = new URLSearchParams();
80911
+ for (const [k2, v2] of Object.entries(query)) if (v2 !== void 0 && v2 !== null && v2 !== "") params.set(k2, String(v2));
80912
+ const qs = params.toString();
80913
+ return this.request("GET", `/files${qs ? `?${qs}` : ""}`, void 0, options);
80914
+ },
80915
+ /** Every file matching the query, following the cursor. */
80916
+ all: async (query = {}, options = {}) => {
80917
+ const out = [];
80918
+ let cursor = null;
80919
+ do {
80920
+ const page = await this.files.list({ ...query, cursor }, options);
80921
+ out.push(...page.data);
80922
+ cursor = page.next_cursor;
80923
+ } while (cursor);
80924
+ return out;
80925
+ },
80926
+ get: (id, options = {}) => this.request("GET", `/files/${encodeURIComponent(id)}`, void 0, options),
80927
+ /** Removes the file and its bytes; templates that name it render a missing image afterwards. */
80928
+ delete: (file, options = {}) => this.request("DELETE", `/files/${encodeURIComponent(idOf(file))}`, void 0, options)
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
+ };
80167
80951
  jobs = {
80168
80952
  get: (id, options = {}) => this.request("GET", `/jobs/${encodeURIComponent(id)}`, void 0, options),
80169
80953
  waitFor: async (id, options = {}) => {
@@ -80183,7 +80967,20 @@ var Formfeed = class {
80183
80967
  create: (input, options = {}) => this.request("POST", "/webhooks", input, options),
80184
80968
  update: (id, patch, options = {}) => this.request("PUT", `/webhooks/${encodeURIComponent(id)}`, patch, options),
80185
80969
  delete: (id, options = {}) => this.request("DELETE", `/webhooks/${encodeURIComponent(id)}`, void 0, options),
80186
- 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
+ }
80187
80984
  };
80188
80985
  templates = {
80189
80986
  list: (query = {}, options = {}) => {
@@ -80204,32 +81001,127 @@ var Formfeed = class {
80204
81001
  return out;
80205
81002
  },
80206
81003
  get: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}`, void 0, options),
80207
- create: (input, options = {}) => this.request("POST", "/templates", input, options),
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
+ },
80208
81012
  update: (idOrSlug, patch, options = {}) => this.request("PUT", `/templates/${encodeURIComponent(idOrSlug)}`, patch, options),
80209
81013
  archive: (idOrSlug, options = {}) => this.request("DELETE", `/templates/${encodeURIComponent(idOrSlug)}`, void 0, options),
80210
81014
  versions: {
80211
81015
  list: async (idOrSlug, options = {}) => (await this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions`, void 0, options)).data,
80212
- /** `which`: 'published', 'latest' or a version number. Carries the files. */
80213
- get: (idOrSlug, which = "published", options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/versions/${which}`, void 0, options),
80214
- create: (idOrSlug, input, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/versions`, input, options),
80215
- publish: (idOrSlug, number, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/versions/${number}/publish`, void 0, options)
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
+ }
80216
81077
  },
80217
- schema: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/schema`, void 0, options)
81078
+ schema: (idOrSlug, options = {}) => this.request("GET", `/templates/${encodeURIComponent(idOrSlug)}/schema`, void 0, options),
81079
+ /**
81080
+ * Checks a version with data without rendering: syntax, header and footer, missing partials,
81081
+ * runtime errors and the data against the stored schema. Free. Without `data`, the version's
81082
+ * sample data is checked.
81083
+ */
81084
+ validate: (idOrSlug, input = {}, options = {}) => this.request("POST", `/templates/${encodeURIComponent(idOrSlug)}/validate`, input, options)
80218
81085
  };
80219
81086
  account = {
80220
81087
  get: (options = {}) => this.request("GET", "/account", void 0, options),
80221
81088
  /** Units of the current period, or of `YYYY-MM`, with a daily series and a breakdown per template. */
80222
81089
  usage: (period = "current", options = {}) => this.request("GET", `/usage?period=${encodeURIComponent(period)}`, void 0, options)
80223
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
+ }
80224
81108
  /** Raw request with auth, retries and problem mapping; use it for endpoints without a helper. */
80225
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") {
80226
81117
  const url = `${this.baseUrl}${path}`;
80227
81118
  const headers = {
80228
81119
  authorization: `Bearer ${this.apiKey}`,
80229
- accept: "application/json",
80230
- "user-agent": "formfeed-sdk-ts/0.1"
81120
+ accept: as === "bytes" ? "*/*" : "application/json",
81121
+ "user-agent": "formfeed-sdk-ts/0.3"
80231
81122
  };
80232
- if (body !== void 0) headers["content-type"] = "application/json";
81123
+ const isForm = typeof FormData !== "undefined" && body instanceof FormData;
81124
+ if (body !== void 0 && !isForm) headers["content-type"] = "application/json";
80233
81125
  if (options.idempotencyKey) headers["idempotency-key"] = options.idempotencyKey;
80234
81126
  let attempt = 0;
80235
81127
  for (; ; ) {
@@ -80241,7 +81133,7 @@ var Formfeed = class {
80241
81133
  res = await this.fetchImpl(url, {
80242
81134
  method,
80243
81135
  headers,
80244
- body: body === void 0 ? void 0 : JSON.stringify(body),
81136
+ body: body === void 0 ? void 0 : isForm ? body : JSON.stringify(body),
80245
81137
  signal: controller.signal
80246
81138
  });
80247
81139
  } catch (e) {
@@ -80259,9 +81151,15 @@ var Formfeed = class {
80259
81151
  await sleep(this.backoff(attempt, res.headers.get("retry-after")), options.signal);
80260
81152
  continue;
80261
81153
  }
80262
- 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()) };
80263
81156
  const text2 = await res.text();
80264
- const json = text2 ? JSON.parse(text2) : null;
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
+ }
80265
81163
  if (!res.ok) {
80266
81164
  const problem = json ?? {};
80267
81165
  throw new FormfeedError(
@@ -80272,7 +81170,7 @@ var Formfeed = class {
80272
81170
  res.headers.get("x-request-id")
80273
81171
  );
80274
81172
  }
80275
- return json;
81173
+ return { status: res.status, body: json };
80276
81174
  }
80277
81175
  }
80278
81176
  backoff(attempt, retryAfter) {
@@ -80287,9 +81185,9 @@ var encoder2 = new TextEncoder();
80287
81185
 
80288
81186
  // packages/mcp/src/lib/server.ts
80289
81187
  import { z as z2 } from "zod";
80290
- var SERVER_INFO = { name: "formfeed", version: "0.1.0" };
81188
+ var SERVER_INFO = { name: "formfeed", version: "0.3.0" };
80291
81189
  var engineSchema = z2.enum(["jinja2", "liquid", "handlebars"]);
80292
- var outputSchema = z2.enum(["pdf", "png", "jpg"]);
81190
+ var outputSchema = z2.enum(outputFormats);
80293
81191
  var templateSummary = (t) => ({
80294
81192
  id: t.id,
80295
81193
  slug: t.slug,
@@ -80425,7 +81323,7 @@ function createFormfeedServer(options) {
80425
81323
  html: z2.string().optional().describe("Raw HTML instead of a template"),
80426
81324
  engine: engineSchema.optional().describe("Engine for html that contains template syntax"),
80427
81325
  data: z2.record(z2.string(), z2.unknown()).optional(),
80428
- output: outputSchema.default("pdf"),
81326
+ output: outputSchema.optional().describe("Left out: PDF for PDF templates and html, the template's image format for image templates"),
80429
81327
  filename: z2.string().optional(),
80430
81328
  locale: z2.string().optional().describe("BCP 47 tag for helpers and translations, e.g. de-DE"),
80431
81329
  wait: z2.boolean().default(true).describe("Wait for the render to finish (sync renders finish immediately)")
@@ -80440,7 +81338,7 @@ function createFormfeedServer(options) {
80440
81338
  ...html ? { html } : {},
80441
81339
  ...engine ? { engine } : {},
80442
81340
  data: data ?? {},
80443
- output,
81341
+ ...output ? { output } : {},
80444
81342
  ...filename ? { filename } : {},
80445
81343
  ...locale ? { locale } : {},
80446
81344
  meta: { source: "mcp" }