@formfeed/engine 0.1.0 → 0.2.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/index.cjs CHANGED
@@ -42,6 +42,7 @@ __export(index_exports, {
42
42
  chartInitScript: () => chartInitScript,
43
43
  chartMarkup: () => chartMarkup,
44
44
  chartPayload: () => chartPayload,
45
+ convertScss: () => convertScss,
45
46
  createHelperRegistry: () => createHelperRegistry,
46
47
  defaultHelpers: () => defaultHelpers,
47
48
  defaultLimits: () => defaultLimits,
@@ -59,9 +60,13 @@ __export(index_exports, {
59
60
  helperDocs: () => helperDocs,
60
61
  imageMarkup: () => imageMarkup,
61
62
  importApitemplate: () => importApitemplate,
63
+ importJsreport: () => importJsreport,
64
+ importPdfmonkey: () => importPdfmonkey,
62
65
  inferSchema: () => inferSchema,
66
+ isScss: () => isScss,
63
67
  jinja2Engine: () => jinja2Engine,
64
68
  jinjaTags: () => jinjaTags,
69
+ jsreportTemplates: () => jsreportTemplates,
65
70
  liquidBuiltinFilters: () => liquidBuiltinFilters,
66
71
  liquidEngine: () => liquidEngine,
67
72
  liquidTags: () => liquidTags,
@@ -74,6 +79,7 @@ __export(index_exports, {
74
79
  paperOf: () => paperOf,
75
80
  printReset: () => printReset,
76
81
  qrSvg: () => qrSvg,
82
+ readJsreportExport: () => readJsreportExport,
77
83
  renderVersion: () => renderVersion,
78
84
  resolvePath: () => resolvePath,
79
85
  scanBlocks: () => scanBlocks,
@@ -81,6 +87,7 @@ __export(index_exports, {
81
87
  slugFromName: () => slugFromName,
82
88
  snippets: () => snippets,
83
89
  snippetsFor: () => snippetsFor,
90
+ strftimeToDateFns: () => strftimeToDateFns,
84
91
  toDate: () => toDate,
85
92
  toNumber: () => toNumber
86
93
  });
@@ -537,7 +544,7 @@ var formatHelpers = [
537
544
  name: "asset",
538
545
  doc: {
539
546
  signature: "asset(name)",
540
- description: "URL of a workspace asset (logo, background) on the CDN.",
547
+ 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.",
541
548
  example: `<img src="{{ asset('logo.png') }}">`,
542
549
  category: "document"
543
550
  },
@@ -1037,6 +1044,12 @@ function helperDocs(registry = defaultHelpers()) {
1037
1044
  // packages/engine/src/lib/engines/handlebars.ts
1038
1045
  var import_handlebars = __toESM(require("handlebars"));
1039
1046
 
1047
+ // packages/engine/src/lib/unavailable.ts
1048
+ var UNAVAILABLE = {};
1049
+ function unavailableHelper(name) {
1050
+ return UNAVAILABLE[name];
1051
+ }
1052
+
1040
1053
  // packages/engine/src/lib/analysis/tags.ts
1041
1054
  function rangeAt(line, column, length) {
1042
1055
  return {
@@ -1216,7 +1229,10 @@ function missingPathDiagnostics(variables, sampleData, loopSources, builtinRoots
1216
1229
  const root = v.path[0];
1217
1230
  if (!root || builtinRoots4.has(root)) continue;
1218
1231
  let path = v.path;
1219
- if (loopSources.has(root)) {
1232
+ if (v.source !== void 0) {
1233
+ if (!v.source) continue;
1234
+ path = [...v.source, ...v.path.slice(1)];
1235
+ } else if (loopSources.has(root)) {
1220
1236
  const source = loopSources.get(root);
1221
1237
  if (!source) continue;
1222
1238
  path = [...source, ...v.path.slice(1)];
@@ -1475,6 +1491,13 @@ function analyzeHandlebars(source, opts = {}) {
1475
1491
  range: f.range,
1476
1492
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
1477
1493
  });
1494
+ } else if (unavailableHelper(f.name)) {
1495
+ diagnostics.push({
1496
+ severity: "warning",
1497
+ code: "unavailable-helper",
1498
+ message: unavailableHelper(f.name),
1499
+ range: f.range
1500
+ });
1478
1501
  } else if (!f.known) {
1479
1502
  diagnostics.push({
1480
1503
  severity: "error",
@@ -1583,6 +1606,151 @@ var handlebarsEngine = {
1583
1606
 
1584
1607
  // packages/engine/src/lib/engines/jinja2.ts
1585
1608
  var import_nunjucks = __toESM(require("nunjucks/browser/nunjucks.js"));
1609
+
1610
+ // packages/engine/src/lib/engines/python-format.ts
1611
+ var SPEC = /^(?:([\s\S])?([<>=^]))?([+\- ])?(#)?(0)?(\d+)?([,_])?(?:\.(\d+))?([bcdeEfFgGnosxX%])?$/;
1612
+ function group(digits, separator) {
1613
+ return digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
1614
+ }
1615
+ function pythonExponent(text) {
1616
+ return text.replace(/e([+-])(\d)$/i, (m, sign, digit) => `${m[0]}${sign}0${digit}`);
1617
+ }
1618
+ function formatNumber(value, type, precision, alternate) {
1619
+ if (!Number.isFinite(value)) return Number.isNaN(value) ? "nan" : "inf";
1620
+ const abs = Math.abs(value);
1621
+ switch (type) {
1622
+ case "f":
1623
+ case "F":
1624
+ return abs.toFixed(precision ?? 6);
1625
+ case "%":
1626
+ return `${(abs * 100).toFixed(precision ?? 6)}%`;
1627
+ case "e":
1628
+ case "E": {
1629
+ const text = pythonExponent(abs.toExponential(precision ?? 6));
1630
+ return type === "E" ? text.toUpperCase() : text;
1631
+ }
1632
+ case "g":
1633
+ case "G": {
1634
+ const p = precision === 0 ? 1 : precision ?? 6;
1635
+ const exp = abs === 0 ? 0 : Math.floor(Math.log10(abs));
1636
+ let text = exp < -4 || exp >= p ? pythonExponent(abs.toExponential(p - 1)) : abs.toFixed(Math.max(0, p - 1 - exp));
1637
+ if (!alternate) {
1638
+ const [mantissa = "", exponent] = text.split("e");
1639
+ const trimmed = mantissa.includes(".") ? mantissa.replace(/0+$/, "").replace(/\.$/, "") : mantissa;
1640
+ text = exponent === void 0 ? trimmed : `${trimmed}e${exponent}`;
1641
+ }
1642
+ return type === "G" ? text.toUpperCase() : text;
1643
+ }
1644
+ case "d":
1645
+ case "n":
1646
+ return String(Math.trunc(abs));
1647
+ case "b":
1648
+ return (alternate ? "0b" : "") + Math.trunc(abs).toString(2);
1649
+ case "o":
1650
+ return (alternate ? "0o" : "") + Math.trunc(abs).toString(8);
1651
+ case "x":
1652
+ return (alternate ? "0x" : "") + Math.trunc(abs).toString(16);
1653
+ case "X":
1654
+ return (alternate ? "0X" : "") + Math.trunc(abs).toString(16).toUpperCase();
1655
+ default:
1656
+ return precision === void 0 ? String(abs) : formatNumber(abs, "g", precision, alternate);
1657
+ }
1658
+ }
1659
+ function formatValue(value, spec) {
1660
+ const m = SPEC.exec(spec);
1661
+ if (!m) return String(value ?? "");
1662
+ const [, fillChar, alignChar, sign, alt, zero, widthText, grouping, precisionText, type = ""] = m;
1663
+ const precision = precisionText === void 0 ? void 0 : Number(precisionText);
1664
+ const width = widthText === void 0 ? 0 : Number(widthText);
1665
+ const numericType = /[bdeEfFgGnoxX%]/.test(type);
1666
+ 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;
1667
+ let body;
1668
+ let prefix = "";
1669
+ if (asNumber !== void 0 && type !== "s" && type !== "c") {
1670
+ body = formatNumber(asNumber, type, precision, Boolean(alt));
1671
+ if (grouping) {
1672
+ const [int = "", ...rest] = body.split(".");
1673
+ const radix = /^0[box]/i.exec(int)?.[0] ?? "";
1674
+ body = radix + group(int.slice(radix.length), grouping) + (rest.length ? `.${rest.join(".")}` : "");
1675
+ }
1676
+ const negative = asNumber < 0 || Object.is(asNumber, -0);
1677
+ prefix = negative ? "-" : sign === "+" ? "+" : sign === " " ? " " : "";
1678
+ } else if (type === "c" && asNumber !== void 0) {
1679
+ body = String.fromCodePoint(asNumber);
1680
+ } else {
1681
+ body = String(value ?? "None");
1682
+ if (precision !== void 0 && !numericType) body = body.slice(0, precision);
1683
+ }
1684
+ const isNumber = asNumber !== void 0 && type !== "s";
1685
+ const align = alignChar ?? (zero && isNumber ? "=" : isNumber ? ">" : "<");
1686
+ const fill = fillChar ?? (zero && !alignChar ? "0" : " ");
1687
+ const pad = Math.max(0, width - prefix.length - body.length);
1688
+ switch (align) {
1689
+ case "<":
1690
+ return prefix + body + fill.repeat(pad);
1691
+ case "^":
1692
+ return fill.repeat(Math.floor(pad / 2)) + prefix + body + fill.repeat(Math.ceil(pad / 2));
1693
+ case "=":
1694
+ return prefix + fill.repeat(pad) + body;
1695
+ default:
1696
+ return fill.repeat(pad) + prefix + body;
1697
+ }
1698
+ }
1699
+ function lookupField(name, positional, named, next) {
1700
+ const [head = "", ...rest] = name.split(/(?=[.[])/);
1701
+ let value = head === "" ? positional[next()] : /^\d+$/.test(head) ? positional[Number(head)] : named[head];
1702
+ for (const part of rest) {
1703
+ const key = part.startsWith("[") ? part.slice(1, -1) : part.slice(1);
1704
+ value = value !== null && typeof value === "object" ? value[Array.isArray(value) && /^\d+$/.test(key) ? Number(key) : key] : void 0;
1705
+ }
1706
+ return value;
1707
+ }
1708
+ function pythonFormat(template, positional, named = {}) {
1709
+ let auto = 0;
1710
+ const next = () => auto++;
1711
+ const render = (text) => {
1712
+ let out = "";
1713
+ for (let i = 0; i < text.length; i++) {
1714
+ const ch = text[i];
1715
+ if (ch === "}") {
1716
+ out += "}";
1717
+ if (text[i + 1] === "}") i++;
1718
+ continue;
1719
+ }
1720
+ if (ch !== "{") {
1721
+ out += ch;
1722
+ continue;
1723
+ }
1724
+ if (text[i + 1] === "{") {
1725
+ out += "{";
1726
+ i++;
1727
+ continue;
1728
+ }
1729
+ let depth = 1;
1730
+ let j = i + 1;
1731
+ for (; j < text.length && depth > 0; j++) {
1732
+ if (text[j] === "{") depth++;
1733
+ else if (text[j] === "}") depth--;
1734
+ }
1735
+ if (depth > 0) {
1736
+ out += text.slice(i);
1737
+ break;
1738
+ }
1739
+ const field = text.slice(i + 1, j - 1);
1740
+ const colon = field.indexOf(":");
1741
+ const head = colon < 0 ? field : field.slice(0, colon);
1742
+ const name = head.replace(/![rsa]$/, "");
1743
+ const value = lookupField(name, positional, named, next);
1744
+ const spec = colon < 0 ? "" : render(field.slice(colon + 1));
1745
+ out += value === void 0 ? "" : formatValue(value, spec);
1746
+ i = j - 1;
1747
+ }
1748
+ return out;
1749
+ };
1750
+ return render(template);
1751
+ }
1752
+
1753
+ // packages/engine/src/lib/engines/jinja2.ts
1586
1754
  var nunjucksBuiltinFilters = /* @__PURE__ */ new Set([
1587
1755
  "abs",
1588
1756
  "batch",
@@ -1637,7 +1805,12 @@ var builtinRoots2 = /* @__PURE__ */ new Set([
1637
1805
  "joiner",
1638
1806
  "lipsum",
1639
1807
  "varargs",
1640
- "kwargs"
1808
+ "kwargs",
1809
+ "namespace",
1810
+ // Jinja2's spelling of the literals, which the compat layer resolves at runtime
1811
+ "True",
1812
+ "False",
1813
+ "None"
1641
1814
  ]);
1642
1815
  var stringMethods = {
1643
1816
  upper: (s) => s.toUpperCase(),
@@ -1666,12 +1839,8 @@ var stringMethods = {
1666
1839
  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),
1667
1840
  join: (s, list2) => Array.isArray(list2) ? list2.join(s) : String(list2),
1668
1841
  format: (s, ...args) => {
1669
- let i = 0;
1670
- const named = args.at(-1) && typeof args.at(-1) === "object" ? args.at(-1) : {};
1671
- return s.replace(
1672
- /\{(\w*)\}/g,
1673
- (_, key) => String(key ? named[key] ?? "" : args[i++] ?? "")
1674
- );
1842
+ const named = keywordArgs(args);
1843
+ return pythonFormat(s, named ? args.slice(0, -1) : args, named ?? {});
1675
1844
  },
1676
1845
  zfill: (s, width) => s.padStart(Number(width), "0"),
1677
1846
  count: (s, sub) => s.split(String(sub)).length - 1,
@@ -1686,6 +1855,92 @@ var stringMethods = {
1686
1855
  return f.repeat(left) + s + f.repeat(total - left);
1687
1856
  }
1688
1857
  };
1858
+ function keywordArgs(args) {
1859
+ const last = args.at(-1);
1860
+ if (!last || typeof last !== "object" || !Object.prototype.hasOwnProperty.call(last, "__keywords")) return void 0;
1861
+ const { __keywords: _marker, ...named } = last;
1862
+ return named;
1863
+ }
1864
+ var JinjaNamespace = class {
1865
+ };
1866
+ function namespace(...args) {
1867
+ const ns = new JinjaNamespace();
1868
+ const named = keywordArgs(args);
1869
+ for (const arg of named ? [...args.slice(0, -1), named] : args)
1870
+ if (arg && typeof arg === "object") Object.assign(ns, arg);
1871
+ return ns;
1872
+ }
1873
+ function setNamespaceAttribute(target, key, value) {
1874
+ if (!(target instanceof JinjaNamespace)) throw new Error("Cannot assign attribute on non-namespace object");
1875
+ target[String(key)] = value;
1876
+ }
1877
+ var toList = (v) => Array.isArray(v) ? v : v === void 0 || v === null ? [] : typeof v === "string" ? [...v] : [v];
1878
+ var attributeOf = (item, path) => String(path ?? "").split(".").reduce((acc, key) => acc !== null && typeof acc === "object" ? acc[key] : void 0, item);
1879
+ var jinjaTests = {
1880
+ none: (v) => v === null,
1881
+ true: (v) => v === true,
1882
+ false: (v) => v === false,
1883
+ in: (v, container) => typeof container === "string" ? container.includes(String(v)) : Array.isArray(container) ? container.includes(v) : !!container && typeof container === "object" && String(v) in container,
1884
+ "==": (v, a) => v === a,
1885
+ "!=": (v, a) => v !== a,
1886
+ ">": (v, a) => v > a,
1887
+ ">=": (v, a) => v >= a,
1888
+ "<": (v, a) => v < a,
1889
+ "<=": (v, a) => v <= a
1890
+ };
1891
+ function runTest(self, name, value, arg) {
1892
+ const own = jinjaTests[name];
1893
+ if (own) return own(value, arg);
1894
+ return self.env.getTest(name).call(self, value, arg) === true;
1895
+ }
1896
+ function installJinjaFilters(env) {
1897
+ const lookup = env;
1898
+ const helperTruncate = lookup.getFilter("truncate");
1899
+ lookup.addFilter("truncate", function(value, ...args) {
1900
+ const named = keywordArgs(args);
1901
+ const p = named ? args.slice(0, -1) : args;
1902
+ if (!named && typeof p[1] !== "boolean" && p.length < 3) return helperTruncate.call(this, value, ...args);
1903
+ const s = String(value ?? "");
1904
+ const length = Number(p[0] ?? named?.["length"] ?? 255);
1905
+ const killwords = Boolean(p[1] ?? named?.["killwords"] ?? false);
1906
+ const end = String(p[2] ?? named?.["end"] ?? "...");
1907
+ const leeway = Number(p[3] ?? named?.["leeway"] ?? 5);
1908
+ if (s.length <= length + leeway) return s;
1909
+ const cut = s.slice(0, Math.max(0, length - end.length));
1910
+ if (killwords) return cut + end;
1911
+ const space = cut.lastIndexOf(" ");
1912
+ return (space >= 0 ? cut.slice(0, space) : cut) + end;
1913
+ });
1914
+ const helperMap = lookup.getFilter("map");
1915
+ lookup.addFilter("map", function(value, ...args) {
1916
+ const named = keywordArgs(args);
1917
+ const p = named ? args.slice(0, -1) : args;
1918
+ if (named && "attribute" in named)
1919
+ return toList(value).map((item) => {
1920
+ const v = attributeOf(item, named["attribute"]);
1921
+ return v === void 0 && "default" in named ? named["default"] : v;
1922
+ });
1923
+ const items = toList(value);
1924
+ if (typeof p[0] === "string" && !items.some((item) => item !== null && typeof item === "object")) {
1925
+ let filter;
1926
+ try {
1927
+ filter = lookup.getFilter(p[0]);
1928
+ } catch {
1929
+ filter = void 0;
1930
+ }
1931
+ if (filter) return items.map((item) => filter.call(this, item, ...p.slice(1)));
1932
+ }
1933
+ return helperMap.call(this, value, ...args);
1934
+ });
1935
+ const byAttribute = (keep) => function(value, attribute, test, arg) {
1936
+ return toList(value).filter((item) => {
1937
+ const v = attributeOf(item, attribute);
1938
+ return (test === void 0 ? Boolean(v) : runTest(this, String(test), v, arg)) === keep;
1939
+ });
1940
+ };
1941
+ lookup.addFilter("selectattr", byAttribute(true));
1942
+ lookup.addFilter("rejectattr", byAttribute(false));
1943
+ }
1689
1944
  function trimChars(s, chars, side) {
1690
1945
  if (chars === void 0 || chars === null)
1691
1946
  return side === "both" ? s.trim() : side === "left" ? s.trimStart() : s.trimEnd();
@@ -1706,6 +1961,20 @@ function installCompat() {
1706
1961
  this.compile(node.target, frame);
1707
1962
  this._emit(")");
1708
1963
  };
1964
+ const compileSet = import_nunjucks.default.compiler.Compiler.prototype.compileSet;
1965
+ import_nunjucks.default.runtime["formfeedSetAttribute"] = setNamespaceAttribute;
1966
+ import_nunjucks.default.compiler.Compiler.prototype.compileSet = function(node, frame) {
1967
+ const [target, ...more] = node.targets;
1968
+ if (!target || more.length || target.typename !== "LookupVal") return compileSet.call(this, node, frame);
1969
+ this._emit("runtime.formfeedSetAttribute(");
1970
+ this._compileExpression(target["target"], frame);
1971
+ this._emit(", ");
1972
+ this._compileExpression(target["val"], frame);
1973
+ this._emit(", ");
1974
+ if (node.value) this._compileExpression(node.value, frame);
1975
+ else this.compile(node.body, frame);
1976
+ this._emitLine(");");
1977
+ };
1709
1978
  const original = import_nunjucks.default.runtime.memberLookup;
1710
1979
  import_nunjucks.default.runtime.memberLookup = (obj, val2, ...rest) => {
1711
1980
  if (typeof obj === "string" && typeof val2 === "string" && Object.prototype.hasOwnProperty.call(stringMethods, val2)) {
@@ -1752,6 +2021,8 @@ function createEnvironment(ctx) {
1752
2021
  env.addFilter(name, wrapped);
1753
2022
  env.addGlobal(name, wrapped);
1754
2023
  }
2024
+ env.addGlobal("namespace", namespace);
2025
+ installJinjaFilters(env);
1755
2026
  return env;
1756
2027
  }
1757
2028
  var validationContext2 = {
@@ -1792,6 +2063,13 @@ function pathOf(node) {
1792
2063
  function inScope(w, name) {
1793
2064
  return w.scopes.some((s) => s.has(name));
1794
2065
  }
2066
+ function sourceOf(w, name) {
2067
+ for (let i = w.scopes.length - 1; i >= 0; i--) {
2068
+ const scope = w.scopes[i];
2069
+ if (scope?.has(name)) return scope.get(name) ?? null;
2070
+ }
2071
+ return null;
2072
+ }
1795
2073
  function walk2(node, w) {
1796
2074
  if (!node || typeof node !== "object") return;
1797
2075
  switch (node.typename) {
@@ -1800,10 +2078,12 @@ function walk2(node, w) {
1800
2078
  const p = pathOf(node);
1801
2079
  if (p) {
1802
2080
  const root = p.path[0] ?? "";
2081
+ const bound = inScope(w, root);
1803
2082
  w.variables.push({
1804
2083
  path: p.path,
1805
2084
  range: nodeRange(p.root, p.path.join(".").length),
1806
- kind: inScope(w, root) ? "loop-var" : "read"
2085
+ kind: bound ? "loop-var" : "read",
2086
+ ...bound ? { source: sourceOf(w, root) } : {}
1807
2087
  });
1808
2088
  }
1809
2089
  if (node.typename === "LookupVal" && !p) {
@@ -1849,18 +2129,18 @@ function walk2(node, w) {
1849
2129
  const source = pathOf(arr);
1850
2130
  const nameNode = node["name"];
1851
2131
  const names = nameNode.typename === "Array" ? nameNode.children ?? [] : [nameNode];
1852
- const scope = /* @__PURE__ */ new Set();
2132
+ const root = source?.path[0] ?? "";
2133
+ const items = !source ? null : inScope(w, root) ? ((s) => s ? [...s, ...source.path.slice(1)] : null)(sourceOf(w, root)) : source.path;
2134
+ const scope = /* @__PURE__ */ new Map();
1853
2135
  for (const n of names) {
1854
2136
  const name = String(n.value);
1855
- scope.add(name);
1856
- w.loopSources.set(
1857
- name,
1858
- names.length === 1 && source && !inScope(w, source.path[0] ?? "") ? source.path : names.length === 1 && source ? resolveLoopSource(w, source.path) : null
1859
- );
2137
+ const bound = names.length === 1 ? items : null;
2138
+ scope.set(name, bound);
1860
2139
  w.variables.push({
1861
2140
  path: [name],
1862
2141
  range: nodeRange(n, name.length),
1863
- kind: "loop-var"
2142
+ kind: "loop-var",
2143
+ source: bound
1864
2144
  });
1865
2145
  }
1866
2146
  w.scopes.push(scope);
@@ -1878,8 +2158,7 @@ function walk2(node, w) {
1878
2158
  range: nodeRange(p.root, p.path.join(".").length),
1879
2159
  kind: "assigned"
1880
2160
  });
1881
- w.scopes[0]?.add(p.path[0] ?? "");
1882
- w.loopSources.set(p.path[0] ?? "", null);
2161
+ if (p.path.length === 1 || !inScope(w, p.path[0] ?? "")) w.scopes[0]?.set(p.path[0] ?? "", null);
1883
2162
  }
1884
2163
  }
1885
2164
  walk2(node["value"], w);
@@ -1888,17 +2167,27 @@ function walk2(node, w) {
1888
2167
  }
1889
2168
  case "Macro": {
1890
2169
  const args = node["args"];
1891
- const scope = /* @__PURE__ */ new Set();
1892
- for (const child of args.children ?? [])
1893
- if (child.typename === "Symbol") scope.add(String(child.value));
2170
+ const scope = /* @__PURE__ */ new Map();
2171
+ for (const child of args.children ?? []) {
2172
+ if (child.typename === "Symbol") scope.set(String(child.value), null);
2173
+ if (child.typename === "KeywordArgs")
2174
+ for (const pair of child.children ?? []) {
2175
+ const key = pair["key"];
2176
+ if (key) scope.set(String(key.value), null);
2177
+ walk2(pair["value"], w);
2178
+ }
2179
+ }
1894
2180
  const nameNode = node["name"];
1895
- w.scopes[0]?.add(String(nameNode.value));
1896
- w.loopSources.set(String(nameNode.value), null);
2181
+ w.scopes[0]?.set(String(nameNode.value), null);
1897
2182
  w.scopes.push(scope);
1898
2183
  walk2(node["body"], w);
1899
2184
  w.scopes.pop();
1900
2185
  return;
1901
2186
  }
2187
+ case "Pair": {
2188
+ walk2(node["value"], w);
2189
+ return;
2190
+ }
1902
2191
  case "Include":
1903
2192
  case "Extends":
1904
2193
  case "Import":
@@ -1911,15 +2200,17 @@ function walk2(node, w) {
1911
2200
  });
1912
2201
  else walk2(tpl, w);
1913
2202
  if (node.typename === "Import")
1914
- w.scopes[0]?.add(
1915
- String(node["target"]?.value ?? "")
2203
+ w.scopes[0]?.set(
2204
+ String(node["target"]?.value ?? ""),
2205
+ null
1916
2206
  );
1917
2207
  if (node.typename === "FromImport")
1918
2208
  for (const n of node["names"].children ?? [])
1919
- w.scopes[0]?.add(
2209
+ w.scopes[0]?.set(
1920
2210
  String(
1921
2211
  n.typename === "Pair" ? n["value"].value : n.value
1922
- )
2212
+ ),
2213
+ null
1923
2214
  );
1924
2215
  return;
1925
2216
  }
@@ -1937,12 +2228,6 @@ function walk2(node, w) {
1937
2228
  walk2(value, w);
1938
2229
  }
1939
2230
  }
1940
- function resolveLoopSource(w, path) {
1941
- const root = path[0] ?? "";
1942
- if (!w.loopSources.has(root)) return path;
1943
- const source = w.loopSources.get(root);
1944
- return source ? [...source, ...path.slice(1)] : null;
1945
- }
1946
2231
  function analyzeJinja2(source, opts = {}) {
1947
2232
  const helpers = opts.helpers ?? defaultHelpers();
1948
2233
  const { blocks, diagnostics } = scanBlocks(source, jinjaTags);
@@ -1950,8 +2235,7 @@ function analyzeJinja2(source, opts = {}) {
1950
2235
  variables: [],
1951
2236
  filters: [],
1952
2237
  includes: [],
1953
- loopSources: /* @__PURE__ */ new Map(),
1954
- scopes: [/* @__PURE__ */ new Set()],
2238
+ scopes: [/* @__PURE__ */ new Map()],
1955
2239
  helpers
1956
2240
  };
1957
2241
  try {
@@ -1980,6 +2264,13 @@ function analyzeJinja2(source, opts = {}) {
1980
2264
  range: f.range,
1981
2265
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
1982
2266
  });
2267
+ } else if (unavailableHelper(f.name)) {
2268
+ diagnostics.push({
2269
+ severity: "warning",
2270
+ code: "unavailable-helper",
2271
+ message: unavailableHelper(f.name),
2272
+ range: f.range
2273
+ });
1983
2274
  } else if (!f.known) {
1984
2275
  diagnostics.push({
1985
2276
  severity: "error",
@@ -1993,7 +2284,7 @@ function analyzeJinja2(source, opts = {}) {
1993
2284
  const dataDiagnostics = missingPathDiagnostics(
1994
2285
  w.variables,
1995
2286
  opts.sampleData,
1996
- w.loopSources,
2287
+ /* @__PURE__ */ new Map(),
1997
2288
  builtin
1998
2289
  );
1999
2290
  return {
@@ -2126,6 +2417,10 @@ var liquidBuiltinFilters = /* @__PURE__ */ new Set([
2126
2417
  "url_decode",
2127
2418
  "url_encode",
2128
2419
  "where",
2420
+ "where_exp",
2421
+ "find_exp",
2422
+ "reject_exp",
2423
+ "group_by_exp",
2129
2424
  "raw",
2130
2425
  "sum",
2131
2426
  "group_by",
@@ -2380,6 +2675,13 @@ function analyzeLiquid(source, opts = {}) {
2380
2675
  range: f.range,
2381
2676
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
2382
2677
  });
2678
+ } else if (unavailableHelper(f.name)) {
2679
+ diagnostics.push({
2680
+ severity: "warning",
2681
+ code: "unavailable-helper",
2682
+ message: unavailableHelper(f.name),
2683
+ range: f.range
2684
+ });
2383
2685
  } else if (!f.known) {
2384
2686
  diagnostics.push({
2385
2687
  severity: "error",
@@ -2921,7 +3223,12 @@ function mapApitemplateSettings(input) {
2921
3223
  return { settings, ignored, notes };
2922
3224
  }
2923
3225
 
2924
- // packages/engine/src/lib/import/apitemplate.ts
3226
+ // packages/api-types/src/lib/assets.ts
3227
+ var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
3228
+ var ASSET_NAME_PATTERN = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*$`);
3229
+ var ASSET_MAX_BYTES = 10 * 1024 * 1024;
3230
+
3231
+ // packages/engine/src/lib/import/common.ts
2925
3232
  function slugFromName(name) {
2926
3233
  const slug = name.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
2927
3234
  return slug.length >= 2 ? slug : `imported-${Date.now().toString(36)}`;
@@ -2935,6 +3242,85 @@ function parseSample(value) {
2935
3242
  return { data: {}, error: e instanceof Error ? e.message : String(e) };
2936
3243
  }
2937
3244
  }
3245
+ function positionOf2(source, index) {
3246
+ const before = source.slice(0, index);
3247
+ const line = before.split("\n").length;
3248
+ const column = index - before.lastIndexOf("\n");
3249
+ return { line, column };
3250
+ }
3251
+ function notesFor(source, pattern, note, { once = false } = {}) {
3252
+ const global = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
3253
+ const out = [];
3254
+ for (const m of source.matchAll(global)) {
3255
+ out.push({ ...note, ...positionOf2(source, m.index ?? 0) });
3256
+ if (once) break;
3257
+ }
3258
+ return out;
3259
+ }
3260
+ function checkTemplate(engineId, html, sampleData, name, docs) {
3261
+ const errors = [];
3262
+ const warnings = [];
3263
+ const engine = getEngine(engineId);
3264
+ try {
3265
+ engine.compile(html, { name });
3266
+ } catch (e) {
3267
+ const err = e;
3268
+ errors.push({ code: "syntax-error", message: err.message, line: err.line, column: err.column, docs: docs.syntax });
3269
+ }
3270
+ const fromDiagnostic = (d) => ({
3271
+ code: d.code,
3272
+ message: d.message,
3273
+ line: d.range.start.line,
3274
+ column: d.range.start.column,
3275
+ docs: d.code === "unknown-filter" ? docs.filters : d.code.startsWith("missing-") ? docs.sampleData : docs.syntax
3276
+ });
3277
+ for (const d of engine.analyze(html, { sampleData }).diagnostics) {
3278
+ const note = fromDiagnostic(d);
3279
+ if (d.severity === "error") {
3280
+ if (!errors.some((x) => x.code === note.code && x.line === note.line)) errors.push(note);
3281
+ } else warnings.push(note);
3282
+ }
3283
+ return { errors, warnings };
3284
+ }
3285
+ function lengthMm(value) {
3286
+ if (value === void 0 || value === null || value === "") return void 0;
3287
+ if (typeof value === "number" && Number.isFinite(value)) return `${value}mm`;
3288
+ const text = String(value).trim();
3289
+ if (/^-?\d+(\.\d+)?$/.test(text)) return `${text}mm`;
3290
+ if (/^-?\d+(\.\d+)?(mm|cm|in|px|pt)$/i.test(text)) return text.toLowerCase();
3291
+ return void 0;
3292
+ }
3293
+ function toBase64(value) {
3294
+ if (typeof value === "string") return value;
3295
+ const bytes = value instanceof Uint8Array ? value : value && typeof value === "object" && Array.isArray(value.data) ? Uint8Array.from(value.data) : Array.isArray(value) ? Uint8Array.from(value) : void 0;
3296
+ if (!bytes) return void 0;
3297
+ let binary = "";
3298
+ for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
3299
+ return btoa(binary);
3300
+ }
3301
+ var mimeByExtension = {
3302
+ png: "image/png",
3303
+ jpg: "image/jpeg",
3304
+ jpeg: "image/jpeg",
3305
+ gif: "image/gif",
3306
+ webp: "image/webp",
3307
+ svg: "image/svg+xml",
3308
+ woff: "font/woff",
3309
+ woff2: "font/woff2",
3310
+ ttf: "font/ttf",
3311
+ otf: "font/otf",
3312
+ css: "text/css",
3313
+ js: "text/javascript",
3314
+ html: "text/html",
3315
+ json: "application/json",
3316
+ txt: "text/plain"
3317
+ };
3318
+ function mimeOf(name) {
3319
+ return mimeByExtension[name.split(".").pop()?.toLowerCase() ?? ""] ?? "application/octet-stream";
3320
+ }
3321
+
3322
+ // packages/engine/src/lib/import/apitemplate.ts
3323
+ var HIDDEN_WRAPPER = "apitemplate-is-content-hidden";
2938
3324
  var pythonisms = [
2939
3325
  {
2940
3326
  pattern: /\{\{[^}]*%\s*\(/,
@@ -2967,12 +3353,6 @@ var pythonisms = [
2967
3353
  docs: "/templates/languages#partials"
2968
3354
  }
2969
3355
  ];
2970
- function positionOf2(source, index) {
2971
- const before = source.slice(0, index);
2972
- const line = before.split("\n").length;
2973
- const column = index - before.lastIndexOf("\n");
2974
- return { line, column };
2975
- }
2976
3356
  function setInLoop(source) {
2977
3357
  const notes = [];
2978
3358
  const forRe = /\{%-?\s*for\b[\s\S]*?\{%-?\s*endfor\s*-?%\}/g;
@@ -2990,12 +3370,7 @@ function setInLoop(source) {
2990
3370
  }
2991
3371
  return notes;
2992
3372
  }
2993
- function fromDiagnostic(d) {
2994
- const docs = d.code === "unknown-filter" ? "/templates/helpers" : d.code.startsWith("missing-") ? "/migrate/apitemplate-io#sample-data" : "/templates/languages#jinja2";
2995
- return { code: d.code, message: d.message, line: d.range.start.line, column: d.range.start.column, docs };
2996
- }
2997
3373
  function importApitemplate(input) {
2998
- const errors = [];
2999
3374
  const warnings = [];
3000
3375
  const changes = [];
3001
3376
  const name = (input.name ?? "").trim() || "Imported template";
@@ -3015,29 +3390,28 @@ function importApitemplate(input) {
3015
3390
  const sample = parseSample(input.sample_data);
3016
3391
  if (sample.error) warnings.push({ code: "sample-json", message: `Sample data is not valid JSON (${sample.error}); the draft starts without sample data.`, docs: "/migrate/apitemplate-io#sample-data" });
3017
3392
  const html = input.html ?? "";
3018
- const css2 = input.css ?? "";
3393
+ let css2 = input.css ?? "";
3394
+ if (html.includes(HIDDEN_WRAPPER)) {
3395
+ css2 = `.${HIDDEN_WRAPPER} { display: none !important; }
3396
+ ${css2}`;
3397
+ changes.push(`elements of the editor class "${HIDDEN_WRAPPER}" are hidden by a rule added to the CSS`);
3398
+ }
3019
3399
  for (const rule of pythonisms) {
3020
3400
  const m = rule.pattern.exec(html);
3021
3401
  if (m) warnings.push({ code: rule.code, message: rule.message, docs: rule.docs, ...positionOf2(html, m.index) });
3022
3402
  }
3023
3403
  warnings.push(...setInLoop(html));
3024
- const engine = getEngine("jinja2");
3025
- try {
3026
- engine.compile(html, { name });
3027
- } catch (e) {
3028
- const err = e;
3029
- errors.push({ code: "syntax-error", message: err.message, line: err.line, column: err.column, docs: "/templates/languages#jinja2" });
3030
- }
3031
- const analysis = engine.analyze(html, { sampleData: sample.data });
3032
- for (const d of analysis.diagnostics) {
3033
- const note = fromDiagnostic(d);
3034
- if (d.severity === "error") {
3035
- if (!errors.some((x) => x.code === note.code && x.line === note.line)) errors.push(note);
3036
- } else warnings.push(note);
3037
- }
3038
- const aliasHits = analysis.filters.filter((f) => ["currency_format", "table", "page_break", "raw", "translate"].includes(f.name));
3039
- if (aliasHits.length) changes.push(`apitemplate.io filters kept through aliases: ${[...new Set(aliasHits.map((f) => f.name))].join(", ")}`);
3404
+ const checked = checkTemplate("jinja2", html, sample.data, name, {
3405
+ syntax: "/templates/languages#jinja2",
3406
+ filters: "/templates/helpers",
3407
+ sampleData: "/migrate/apitemplate-io#sample-data"
3408
+ });
3409
+ warnings.push(...checked.warnings);
3410
+ const aliasHits = /\|\s*(currency_format|table|page_break|raw|translate)\b/g;
3411
+ const aliases = [...new Set([...html.matchAll(aliasHits)].map((m) => m[1]))];
3412
+ if (aliases.length) changes.push(`apitemplate.io filters kept through aliases: ${aliases.join(", ")}`);
3040
3413
  return {
3414
+ source: "apitemplate",
3041
3415
  name,
3042
3416
  slug: slugFromName(name),
3043
3417
  kind,
@@ -3047,12 +3421,767 @@ function importApitemplate(input) {
3047
3421
  head: "",
3048
3422
  settings,
3049
3423
  sampleData: sample.data,
3050
- errors,
3424
+ errors: checked.errors,
3051
3425
  warnings,
3052
3426
  changes
3053
3427
  };
3054
3428
  }
3055
3429
 
3430
+ // packages/engine/src/lib/import/scss.ts
3431
+ var DOCS = "/migrate/pdfmonkey#styles";
3432
+ var needsCompiler = [
3433
+ { pattern: /@(mixin|include)\b/, what: "@mixin/@include" },
3434
+ { pattern: /@extend\b/, what: "@extend" },
3435
+ { pattern: /@(if|else|each|for|while|function|return)\b/, what: "SCSS control flow and functions" },
3436
+ { pattern: /@(use|forward)\b/, what: "@use/@forward" },
3437
+ { pattern: /(^|[\s,{])%[a-zA-Z_][\w-]*\s*\{/m, what: "%placeholder selectors" },
3438
+ { pattern: /\b(darken|lighten|saturate|desaturate|adjust-hue|mix|transparentize|opacify|fade-in|fade-out|percentage|map-get|nth)\s*\(/, what: "Sass colour and map functions" },
3439
+ { pattern: /#\{/, what: "interpolation #{\u2026}" }
3440
+ ];
3441
+ function isScss(source) {
3442
+ return /(^|[^\w-])\$[a-zA-Z_][\w-]*\s*:/m.test(source) || /^\s*\/\//m.test(source) || needsCompiler.some((r) => r.pattern.test(source));
3443
+ }
3444
+ function topLevelVariables(source) {
3445
+ const vars = /* @__PURE__ */ new Map();
3446
+ let depth = 0;
3447
+ let statement = "";
3448
+ for (const ch of source) {
3449
+ if (ch === "{") {
3450
+ depth++;
3451
+ statement = "";
3452
+ } else if (ch === "}") {
3453
+ depth = Math.max(0, depth - 1);
3454
+ statement = "";
3455
+ } else if (ch === ";" && depth === 0) {
3456
+ const m = /^\s*\$([a-zA-Z_][\w-]*)\s*:\s*([\s\S]+?)\s*(!default)?\s*$/.exec(statement);
3457
+ if (m && m[1] && m[2]) vars.set(m[1], m[2]);
3458
+ statement = "";
3459
+ } else statement += ch;
3460
+ }
3461
+ return vars;
3462
+ }
3463
+ function convertScss(source, compile) {
3464
+ const warnings = [];
3465
+ const changes = [];
3466
+ if (!source.trim() || !isScss(source)) return { css: source, warnings, changes };
3467
+ if (compile) {
3468
+ try {
3469
+ const css3 = compile(source);
3470
+ changes.push("SCSS compiled to CSS");
3471
+ return { css: css3, warnings, changes };
3472
+ } catch (e) {
3473
+ warnings.push({
3474
+ code: "scss-compile",
3475
+ message: `The SCSS did not compile (${e instanceof Error ? e.message.split("\n")[0] : String(e)}); it was converted without a compiler instead.`,
3476
+ docs: DOCS
3477
+ });
3478
+ }
3479
+ }
3480
+ let css2 = source.replace(/^[ \t]*\/\/.*(\r?\n)?/gm, "");
3481
+ if (css2 !== source) changes.push("SCSS line comments (//) removed");
3482
+ const vars = topLevelVariables(css2);
3483
+ if (vars.size) {
3484
+ css2 = css2.replace(/(?<=^|[;{}\s])\$([a-zA-Z_][\w-]*)\s*:[^;{}]*;[ \t]*\r?\n?/gm, (m, name) => vars.has(name) ? "" : m);
3485
+ const names = [...vars.keys()].sort((a, b) => b.length - a.length);
3486
+ for (let pass = 0; pass < 5; pass++) {
3487
+ const before = css2;
3488
+ for (const name of names) css2 = css2.replace(new RegExp(`\\$${name}(?![\\w-])`, "g"), vars.get(name) ?? "");
3489
+ if (css2 === before) break;
3490
+ }
3491
+ changes.push(`SCSS variables substituted: ${names.map((n) => `$${n}`).join(", ")}`);
3492
+ }
3493
+ for (const rule of needsCompiler)
3494
+ warnings.push(
3495
+ ...notesFor(css2, rule.pattern, {
3496
+ code: "scss-needs-compiler",
3497
+ message: `The stylesheet uses ${rule.what}, which only a Sass compiler resolves; convert it to CSS by hand or import with the Formfeed app, which compiles SCSS.`,
3498
+ docs: DOCS
3499
+ }, { once: true })
3500
+ );
3501
+ warnings.push(
3502
+ ...notesFor(css2, /\$[a-zA-Z_][\w-]*/, {
3503
+ code: "scss-variable",
3504
+ message: "A SCSS variable that is not declared at the top level was left in place; replace it with its value or a CSS custom property.",
3505
+ docs: DOCS
3506
+ }, { once: true })
3507
+ );
3508
+ return { css: css2, warnings, changes };
3509
+ }
3510
+
3511
+ // packages/engine/src/lib/import/pdfmonkey.ts
3512
+ var DOCS2 = "/migrate/pdfmonkey";
3513
+ var strftime = {
3514
+ Y: "yyyy",
3515
+ y: "yy",
3516
+ m: "MM",
3517
+ "-m": "M",
3518
+ d: "dd",
3519
+ "-d": "d",
3520
+ e: "d",
3521
+ H: "HH",
3522
+ "-H": "H",
3523
+ k: "H",
3524
+ I: "hh",
3525
+ "-I": "h",
3526
+ l: "h",
3527
+ M: "mm",
3528
+ "-M": "m",
3529
+ S: "ss",
3530
+ "-S": "s",
3531
+ L: "SSS",
3532
+ p: "a",
3533
+ P: "aaa",
3534
+ B: "MMMM",
3535
+ b: "MMM",
3536
+ h: "MMM",
3537
+ A: "EEEE",
3538
+ a: "EEE",
3539
+ u: "i",
3540
+ z: "xx",
3541
+ F: "yyyy-MM-dd",
3542
+ D: "MM/dd/yy",
3543
+ T: "HH:mm:ss",
3544
+ R: "HH:mm",
3545
+ r: "hh:mm:ss a"
3546
+ };
3547
+ function strftimeToDateFns(format) {
3548
+ let out = "";
3549
+ let literal = "";
3550
+ const unmapped = [];
3551
+ const flushLiteral = () => {
3552
+ if (!literal) return;
3553
+ out += /[A-Za-z']/.test(literal) ? `'${literal.replace(/'/g, "''")}'` : literal;
3554
+ literal = "";
3555
+ };
3556
+ for (let i = 0; i < format.length; i++) {
3557
+ const ch = format[i] ?? "";
3558
+ if (ch !== "%") {
3559
+ literal += ch;
3560
+ continue;
3561
+ }
3562
+ const flag = format[i + 1] === "-" ? "-" : "";
3563
+ const directive = format[i + 1 + flag.length] ?? "";
3564
+ i += flag.length + 1;
3565
+ if (directive === "%") {
3566
+ literal += "%";
3567
+ continue;
3568
+ }
3569
+ const token = strftime[`${flag}${directive}`] ?? strftime[directive];
3570
+ if (token) {
3571
+ flushLiteral();
3572
+ out += token;
3573
+ } else {
3574
+ unmapped.push(`%${flag}${directive}`);
3575
+ literal += `%${flag}${directive}`;
3576
+ }
3577
+ }
3578
+ flushLiteral();
3579
+ return { format: out, unmapped };
3580
+ }
3581
+ var quote = (value) => value.includes('"') && !value.includes("'") ? `'${value}'` : `"${value}"`;
3582
+ function localeForSeparators(delimiter, separator) {
3583
+ const key = `${delimiter.replace(/\u00a0/g, " ")}|${separator}`;
3584
+ return {
3585
+ ",|.": "en-US",
3586
+ ".|,": "de-DE",
3587
+ " |,": "fr-FR",
3588
+ "'|.": "de-CH"
3589
+ }[key];
3590
+ }
3591
+ function keywordArgs2(args) {
3592
+ const out = {};
3593
+ for (const m of args.matchAll(/([a-z_]\w*)\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^,\s|]+)/g)) {
3594
+ const raw = m[2] ?? "";
3595
+ out[m[1] ?? ""] = /^["']/.test(raw) ? raw.slice(1, -1) : raw;
3596
+ }
3597
+ return out;
3598
+ }
3599
+ var unsupportedFilters = {
3600
+ to_sentence: 'use `join: ", "`, or `array_to_sentence_string` for "a, b, and c"',
3601
+ ensure_protocol: 'store the URL with its protocol, or prepend it with `prepend: "https:"`',
3602
+ format: "use `number: 2` for decimals or `round: 2`; printf padding has no equivalent",
3603
+ parse_json: "send the value as JSON in the payload instead of a string",
3604
+ in_time_zone: 'pass the zone to `date` as its second argument: `date: "dd.MM.yyyy", "Europe/Berlin"`'
3605
+ };
3606
+ function mapLiquid(source, fn) {
3607
+ return source.replace(/(\{\{-?|\{%-?)([\s\S]*?)(-?\}\}|-?%\})/g, (_m, open, inner, close) => `${open}${fn(inner)}${close}`);
3608
+ }
3609
+ function convertDialect(source, changes) {
3610
+ const errors = [];
3611
+ const warnings = [];
3612
+ let out = mapLiquid(source, (inner) => {
3613
+ let expr = inner;
3614
+ expr = expr.replace(/\|\s*([a-z_]\w*)\s*,\s*(?=[a-z_]\w*\s*:)/g, (_m, name) => {
3615
+ changes.add("filter arguments written with a colon (`| filter: key: value`) instead of a comma");
3616
+ return `| ${name}: `;
3617
+ });
3618
+ expr = expr.replace(
3619
+ /\|\s*in_time_zone\s*:\s*(["'])([^"']+)\1\s*\|\s*date\s*:\s*(["'])((?:(?!\3).)*)\3/g,
3620
+ (_m, _q, zone, _q2, fmt) => {
3621
+ changes.add("`in_time_zone` merged into `date` as its timezone argument");
3622
+ return `| date: ${quote(fmt)}, ${quote(zone)}`;
3623
+ }
3624
+ );
3625
+ expr = expr.replace(/\|\s*date\s*:\s*(["'])((?:(?!\1).)*)\1/g, (m, _q, fmt) => {
3626
+ if (!fmt.includes("%")) return m;
3627
+ const converted = strftimeToDateFns(fmt);
3628
+ changes.add("date formats converted from strftime (%d.%m.%Y) to date-fns tokens (dd.MM.yyyy)");
3629
+ if (converted.unmapped.length)
3630
+ warnings.push({
3631
+ code: "date-directive",
3632
+ message: `The date format "${fmt}" uses ${converted.unmapped.join(", ")}, which has no date-fns equivalent; it is kept literally.`,
3633
+ docs: `${DOCS2}#dates`
3634
+ });
3635
+ return `| date: ${quote(converted.format)}`;
3636
+ });
3637
+ expr = expr.replace(/(["'])today\1(\s*\|\s*date\b)/g, '"now"$2');
3638
+ expr = expr.replace(/\|\s*slice_by\b/g, () => {
3639
+ changes.add("`slice_by` renamed to `chunk`");
3640
+ return "| chunk";
3641
+ });
3642
+ expr = expr.replace(/\s*\|\s*entities\b/g, () => {
3643
+ changes.add("`entities` removed: header and footer render UTF-8 directly");
3644
+ return "";
3645
+ });
3646
+ expr = expr.replace(/\|\s*with_delimiter\b(\s*:\s*[^|]*)?/g, (m, args) => {
3647
+ const opts = keywordArgs2(args ?? "");
3648
+ const locale = localeForSeparators(opts["delimiter"] ?? ",", opts["separator"] ?? ".");
3649
+ if (!locale) {
3650
+ warnings.push({
3651
+ code: "number-separators",
3652
+ message: `with_delimiter with delimiter "${opts["delimiter"] ?? ","}" and separator "${opts["separator"] ?? "."}" has no matching locale; it was kept and will fail.`,
3653
+ docs: `${DOCS2}#numbers`
3654
+ });
3655
+ return m;
3656
+ }
3657
+ if (opts["strip_insignificant_zeros"] === "true")
3658
+ warnings.push({
3659
+ code: "number-strip-zeros",
3660
+ message: "`strip_insignificant_zeros` has no equivalent; `number` shows up to three decimals without fixed precision.",
3661
+ docs: `${DOCS2}#numbers`
3662
+ });
3663
+ changes.add("`with_delimiter` converted to `number` with a locale that has the same separators");
3664
+ const decimals = opts["strip_insignificant_zeros"] === "true" ? "nil" : opts["precision"] ?? "nil";
3665
+ return `| number: ${decimals}, ${quote(locale)} `;
3666
+ });
3667
+ return expr;
3668
+ });
3669
+ for (const [filter, instead] of Object.entries(unsupportedFilters))
3670
+ errors.push(
3671
+ ...notesFor(out, new RegExp(`\\|\\s*${filter}\\b`), {
3672
+ code: "pdfmonkey-filter",
3673
+ message: `PDFMonkey's \`${filter}\` filter has no Formfeed equivalent: ${instead}.`,
3674
+ docs: `${DOCS2}#filters`
3675
+ })
3676
+ );
3677
+ warnings.push(
3678
+ ...notesFor(out, /\|\s*divided_by\s*:\s*-?\d+\s*(?=[|}%]|-?%\}|-?\}\})/, {
3679
+ code: "integer-division",
3680
+ message: "PDFMonkey (Ruby Liquid) divides two integers without remainder: `7 | divided_by: 2` is 3 there and 3.5 here. Add `| floor` if the template relies on it.",
3681
+ docs: `${DOCS2}#numbers`
3682
+ })
3683
+ );
3684
+ out = out.replace(/\[%\s*data_form_field[^\]]*\]/g, (m) => {
3685
+ warnings.push({ code: "pdf-forms", message: "PDF form markers are not supported; the marker is kept as text.", docs: `${DOCS2}#forms`, ...positionOf2(out, out.indexOf(m)) });
3686
+ return m;
3687
+ });
3688
+ return { source: out, errors, warnings, changes };
3689
+ }
3690
+ function splitParams(params) {
3691
+ const out = [];
3692
+ for (const m of params.matchAll(/([A-Za-z_]\w*)\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^,]+)/g))
3693
+ out.push([m[1] ?? "", (m[2] ?? "").trim()]);
3694
+ return out;
3695
+ }
3696
+ function inlineIncludes(source, blocks, errors, changes) {
3697
+ let out = source;
3698
+ for (let depth = 0; depth < 6; depth++) {
3699
+ let inlined = false;
3700
+ out = out.replace(/\{%-?\s*include\s+(["'])([^"']+)\1\s*(?:,\s*([\s\S]*?))?\s*-?%\}/g, (m, _q, name, params) => {
3701
+ const body = blocks.get(name);
3702
+ if (body === void 0) return m;
3703
+ inlined = true;
3704
+ changes.add("snippets and partials inlined into the template");
3705
+ const assigns = splitParams(params ?? "").map(([k, v]) => `{% assign ${k} = ${v} %}`).join("");
3706
+ return `${assigns}${body}`;
3707
+ });
3708
+ if (!inlined) break;
3709
+ }
3710
+ for (const m of out.matchAll(/\{%-?\s*include\s+(["'])([^"']+)\1/g))
3711
+ errors.push({
3712
+ code: "snippet-missing",
3713
+ message: `The snippet "${m[2]}" is not part of the template; paste its code in the import dialog (Snippets) or with --snippet ${m[2]}=file.liquid.`,
3714
+ docs: `${DOCS2}#snippets`,
3715
+ ...positionOf2(out, m.index ?? 0)
3716
+ });
3717
+ return out;
3718
+ }
3719
+ var cssPaperFormats = { a3: "A3", a4: "A4", a5: "A5", letter: "Letter" };
3720
+ var isoSizes = { a0: [841, 1189], a1: [594, 841], a2: [420, 594], a6: [105, 148] };
3721
+ var escapeHtml2 = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3722
+ function simpleHeaderFooter(part, margin) {
3723
+ const cells = ["left", "center", "right"].map((side) => part[side] ?? "");
3724
+ if (!cells.some((c) => c.trim())) return void 0;
3725
+ const cell = (text, align) => `<div style="flex: 1; text-align: ${align}">${escapeHtml2(text).replace(/\[page\]/g, '<span class="pageNumber"></span>').replace(/\[topage\]/g, '<span class="totalPages"></span>')}</div>`;
3726
+ return `<div style="display: flex; width: 100%; font-size: 10px; padding: 0 ${margin.right} 0 ${margin.left}">${cell(cells[0] ?? "", "left")}${cell(cells[1] ?? "", "center")}${cell(cells[2] ?? "", "right")}</div>`;
3727
+ }
3728
+ function mapSettings(input, kind, warnings, changes) {
3729
+ const settings = {};
3730
+ const format = String(input.paper_format ?? "a4").toLowerCase();
3731
+ if (format === "custom") {
3732
+ const width = lengthMm(input.paper_width);
3733
+ const height = lengthMm(input.paper_height);
3734
+ if (width && height) settings.paper = { width, height };
3735
+ else warnings.push({ code: "setting-ignored", message: "Custom paper size without width and height; A4 is used.", docs: `${DOCS2}#settings` });
3736
+ } else if (cssPaperFormats[format]) settings.paper = { format: cssPaperFormats[format] };
3737
+ else if (isoSizes[format]) {
3738
+ const [w, h] = isoSizes[format] ?? [210, 297];
3739
+ settings.paper = { width: `${w}mm`, height: `${h}mm` };
3740
+ changes.add(`${format.toUpperCase()} kept as its size in millimetres`);
3741
+ } else warnings.push({ code: "setting-ignored", message: `Paper format "${input.paper_format}" is unknown; A4 is used.`, docs: `${DOCS2}#settings` });
3742
+ if (String(input.orientation ?? "").toLowerCase() === "landscape") settings.paper = { ...settings.paper, landscape: true };
3743
+ const m = input.margin ?? {};
3744
+ const margin = {
3745
+ top: lengthMm(m.top) ?? "10mm",
3746
+ right: lengthMm(m.right) ?? "10mm",
3747
+ bottom: lengthMm(m.bottom) ?? "10mm",
3748
+ left: lengthMm(m.left) ?? "10mm"
3749
+ };
3750
+ settings.margin = margin;
3751
+ changes.add("paper, orientation and margins mapped (empty PDFMonkey margins are 10 mm)");
3752
+ let header;
3753
+ let footer;
3754
+ for (const [key, part] of [
3755
+ ["header", input.header],
3756
+ ["footer", input.footer]
3757
+ ]) {
3758
+ if (!part) continue;
3759
+ const html = part.content?.trim() ? part.content : simpleHeaderFooter(part, margin);
3760
+ if (!html) continue;
3761
+ if (key === "header") header = html;
3762
+ else footer = html;
3763
+ changes.add(
3764
+ part.content?.trim() ? "header and footer HTML moved to the Header and Footer tabs" : "simple header and footer rebuilt as HTML with page numbers; check the font size"
3765
+ );
3766
+ }
3767
+ if (kind === "image") {
3768
+ settings.image = { width: 1200, height: 630, deviceScaleFactor: 2, format: "png", ...input.transparent_background ? { transparent: true } : {} };
3769
+ changes.add("image size defaults to 1200 \xD7 630 (PDFMonkey sets it per request; change it in Settings)");
3770
+ }
3771
+ if (input.inject_javascript)
3772
+ warnings.push({
3773
+ code: "inject-javascript",
3774
+ message: "PDFMonkey exposed the payload to scripts as `$docPayload`; Formfeed does not. Render the values into the markup, or write them into a script tag with the json filter.",
3775
+ docs: `${DOCS2}#javascript`
3776
+ });
3777
+ if (input.use_paged)
3778
+ warnings.push({ code: "paged-js", message: "The template waited for Paged.js; check page breaks and margin boxes in the true render.", docs: `${DOCS2}#paged-js` });
3779
+ if (input.use_forms) warnings.push({ code: "pdf-forms", message: "PDF form fields are not supported.", docs: `${DOCS2}#forms` });
3780
+ if (input.use_emojis) changes.add("`use_emojis` dropped: emoji render with the system emoji font");
3781
+ return { settings, header, footer };
3782
+ }
3783
+ function convertMarkup(html, settings, warnings, changes) {
3784
+ let out = html;
3785
+ const tailwind = /<script\b[^>]*\bsrc=["'][^"']*tailwind[^"']*["'][^>]*>\s*<\/script>\s*|<link\b[^>]*\bhref=["'][^"']*tailwind[^"']*\.css["'][^>]*>\s*/gi;
3786
+ if (tailwind.test(out)) {
3787
+ out = out.replace(tailwind, "");
3788
+ settings.tailwind = true;
3789
+ changes.add("Tailwind script removed and Tailwind mode switched on (Formfeed compiles the classes)");
3790
+ }
3791
+ warnings.push(
3792
+ ...notesFor(out, /pagedjs|paged\.polyfill/i, {
3793
+ code: "paged-js",
3794
+ message: "The template loads Paged.js; Formfeed paginates with Chromium, so check running headers and margin boxes in the true render.",
3795
+ docs: `${DOCS2}#paged-js`
3796
+ }, { once: true }),
3797
+ ...notesFor(out, /qr-code-styling|@bitjson\/qr-code|qrcode(\.min)?\.js|qrcodejs/i, {
3798
+ code: "qr-library",
3799
+ message: 'A JavaScript QR library is loaded; the `qrcode` helper renders QR codes without scripts: `<img src="{{ url | qrcode }}">`.',
3800
+ docs: "/templates/helpers#qrcode"
3801
+ }, { once: true }),
3802
+ ...notesFor(out, /\$docPayload/, {
3803
+ code: "inject-javascript",
3804
+ message: "`$docPayload` is not defined in Formfeed; render the values into the markup or write them into a script tag with the json filter.",
3805
+ docs: `${DOCS2}#javascript`
3806
+ }, { once: true })
3807
+ );
3808
+ return out;
3809
+ }
3810
+ function importPdfmonkey(input, options = {}) {
3811
+ const tpl = "document_template" in input ? input.document_template : input;
3812
+ const errors = [];
3813
+ const warnings = [];
3814
+ const changes = /* @__PURE__ */ new Set();
3815
+ const pick2 = (published, draft) => {
3816
+ const empty = (v) => v === void 0 || v === null || v === "";
3817
+ if (options.draft) return empty(draft) ? published : draft;
3818
+ if (empty(published) && !empty(draft)) {
3819
+ changes.add("the template was never published; the draft was imported");
3820
+ return draft;
3821
+ }
3822
+ return published;
3823
+ };
3824
+ const name = (tpl.identifier ?? "").trim() || "Imported template";
3825
+ const kind = String(tpl.output_type ?? "").toLowerCase() === "image" ? "image" : "pdf";
3826
+ if (tpl.edition_mode === "builder")
3827
+ errors.push({
3828
+ code: "builder-template",
3829
+ message: "This is a PDFMonkey Builder template; its format is not documented, so only code templates can be imported. Recreate it in the Formfeed editor.",
3830
+ docs: DOCS2
3831
+ });
3832
+ const mapped = mapSettings(pick2(tpl.settings, tpl.settings_draft) ?? {}, kind, warnings, changes);
3833
+ const settings = mapped.settings;
3834
+ let body = String(pick2(tpl.body, tpl.body_draft) ?? "");
3835
+ const blocks = new Map(Object.entries(options.snippets ?? {}));
3836
+ body = body.replace(/\{%-?\s*partial\s+(["'])([^"']+)\1\s*-?%\}([\s\S]*?)\{%-?\s*endpartial\s*-?%\}/g, (_m, _q, partialName, content) => {
3837
+ blocks.set(partialName, content);
3838
+ return "";
3839
+ });
3840
+ body = body.replace(/\{%-?\s*load_snippets\b[\s\S]*?-?%\}/g, () => {
3841
+ changes.add("`load_snippets` removed");
3842
+ return "";
3843
+ });
3844
+ body = inlineIncludes(body, blocks, errors, changes);
3845
+ const converted = convertDialect(body, changes);
3846
+ errors.push(...converted.errors);
3847
+ warnings.push(...converted.warnings);
3848
+ const html = convertMarkup(converted.source, settings, warnings, changes);
3849
+ for (const key of ["header", "footer"]) {
3850
+ const part = mapped[key];
3851
+ if (!part) continue;
3852
+ const result = convertDialect(inlineIncludes(part, blocks, errors, changes), changes);
3853
+ settings[key] = { html: result.source };
3854
+ errors.push(...result.errors.map((n) => ({ ...n, message: `${key}: ${n.message}` })));
3855
+ warnings.push(...result.warnings.map((n) => ({ ...n, message: `${key}: ${n.message}` })));
3856
+ }
3857
+ const scss = convertScss(String(pick2(tpl.scss_style, tpl.scss_style_draft) ?? ""), options.compileScss);
3858
+ warnings.push(...scss.warnings);
3859
+ for (const change of scss.changes) changes.add(change);
3860
+ const sample = parseSample(pick2(tpl.sample_data, tpl.sample_data_draft));
3861
+ if (sample.error)
3862
+ warnings.push({ code: "sample-json", message: `Sample data is not valid JSON (${sample.error}); the draft starts without sample data.`, docs: `${DOCS2}#sample-data` });
3863
+ const checked = checkTemplate("liquid", html, sample.data, name, {
3864
+ syntax: "/templates/languages#liquid",
3865
+ filters: `${DOCS2}#filters`,
3866
+ sampleData: `${DOCS2}#sample-data`
3867
+ });
3868
+ const explained = new Set(errors.filter((e) => e.code === "pdfmonkey-filter").map((e) => e.line));
3869
+ const generic = (e) => e.code === "unknown-filter" || e.code === "syntax-error" && /undefined filter/i.test(e.message);
3870
+ errors.push(...checked.errors.filter((e) => !(generic(e) && explained.has(e.line))));
3871
+ warnings.push(...checked.warnings);
3872
+ return {
3873
+ source: "pdfmonkey",
3874
+ name,
3875
+ slug: slugFromName(name),
3876
+ kind,
3877
+ engine: "liquid",
3878
+ html,
3879
+ css: scss.css,
3880
+ head: "",
3881
+ settings,
3882
+ sampleData: sample.data,
3883
+ errors,
3884
+ warnings,
3885
+ changes: [...changes]
3886
+ };
3887
+ }
3888
+
3889
+ // packages/engine/src/lib/import/jsreport.ts
3890
+ var DOCS3 = "/migrate/jsreport";
3891
+ var COLLECTIONS = ["templates", "data", "assets", "components", "scripts", "folders"];
3892
+ function readJsreportExport(files) {
3893
+ const bundle = { templates: [], data: [], assets: [], components: [], scripts: [], folders: [] };
3894
+ for (const [path, text] of Object.entries(files)) {
3895
+ const normalized = path.replace(/\\/g, "/").replace(/^\.?\//, "");
3896
+ if (normalized === "metadata.json") {
3897
+ try {
3898
+ bundle.version = String(JSON.parse(text).reporterVersion ?? "");
3899
+ } catch {
3900
+ }
3901
+ continue;
3902
+ }
3903
+ const collection = normalized.split("/")[0];
3904
+ if (!COLLECTIONS.includes(collection) || !normalized.endsWith(".json")) continue;
3905
+ try {
3906
+ bundle[collection].push(JSON.parse(text));
3907
+ } catch {
3908
+ }
3909
+ }
3910
+ if (!bundle.templates.length) throw new Error("No templates found: this does not look like a jsreport export (.jsrexport).");
3911
+ return bundle;
3912
+ }
3913
+ function folderPath(bundle, entity) {
3914
+ const parts = [];
3915
+ let shortid = entity.folder?.shortid;
3916
+ for (let guard = 0; shortid && guard < 20; guard++) {
3917
+ const folder = bundle.folders.find((f) => f.shortid === shortid);
3918
+ if (!folder) break;
3919
+ parts.unshift(folder.name ?? "");
3920
+ shortid = folder.folder?.shortid;
3921
+ }
3922
+ return parts.filter(Boolean).join("/");
3923
+ }
3924
+ function pathOf2(bundle, entity) {
3925
+ const folder = folderPath(bundle, entity);
3926
+ return folder ? `${folder}/${entity.name ?? ""}` : entity.name ?? "";
3927
+ }
3928
+ function jsreportTemplates(bundle) {
3929
+ return bundle.templates.map((t) => ({ ref: t.shortid ?? t._id ?? t.name ?? "", name: t.name ?? "", path: pathOf2(bundle, t), engine: t.engine ?? "", recipe: t.recipe ?? "" })).sort((a, b) => a.path.localeCompare(b.path));
3930
+ }
3931
+ function byPath(bundle, list2, ref) {
3932
+ const wanted = ref.replace(/^\.?\/+/, "");
3933
+ return list2.find((e) => pathOf2(bundle, e) === wanted) ?? list2.find((e) => e.name === wanted.split("/").pop());
3934
+ }
3935
+ function decodeText(base64) {
3936
+ const binary = atob(base64);
3937
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
3938
+ return new TextDecoder().decode(bytes);
3939
+ }
3940
+ function assetBase64(content) {
3941
+ if (typeof content === "string") {
3942
+ const compact = content.replace(/\s/g, "");
3943
+ if (compact.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(compact)) return compact;
3944
+ return toBase64(new TextEncoder().encode(content));
3945
+ }
3946
+ return toBase64(content);
3947
+ }
3948
+ var escapeMustaches = (text) => text.replace(/\{\{/g, "\\{{");
3949
+ function resolveAssets(source, bundle, options, warnings, changes) {
3950
+ const v3 = /\{\{\{?\s*asset\s+(["'])([^"']+)\1(?:\s+(["'])([^"']+)\3)?\s*\}?\}\}/g;
3951
+ const v2 = /\{#asset\s+([^\s}]+)(?:\s+@encoding=(\w+))?\s*\}/g;
3952
+ const replace = (whole, ref, encoding = "utf8") => {
3953
+ const asset = byPath(bundle, bundle.assets, ref);
3954
+ if (!asset) {
3955
+ warnings.push({ code: "asset-missing", message: `The asset "${ref}" is not in the export. Upload it to the workspace file library under that name, or replace the call with a full https URL.`, docs: `${DOCS3}#assets` });
3956
+ return `{{asset "${ref}"}}`;
3957
+ }
3958
+ if (encoding === "link" || !asset.content && asset.link) {
3959
+ if (asset.link && /^https?:\/\//.test(asset.link)) return asset.link;
3960
+ warnings.push({ code: "asset-link", message: `The asset "${ref}" is linked from the jsreport server, which this import cannot read. Upload the file to the workspace file library under that name, or replace the call with a full https URL.`, docs: `${DOCS3}#assets` });
3961
+ return `{{asset "${asset.name}"}}`;
3962
+ }
3963
+ const base64 = assetBase64(asset.content);
3964
+ if (base64 === void 0) return whole;
3965
+ const bytes = Math.floor(base64.length * 3 / 4);
3966
+ if (encoding === "utf8" || encoding === "string") {
3967
+ changes.add("text assets (CSS, scripts, snippets) inlined");
3968
+ return escapeMustaches(decodeText(base64));
3969
+ }
3970
+ if (bytes > options.inlineAssetBytes) {
3971
+ warnings.push({
3972
+ code: "asset-large",
3973
+ message: `The asset "${asset.name}" (${Math.round(bytes / 1024)} KB) is larger than the inline limit. Upload it to the workspace file library under that name, import with a higher inline limit, or replace the call with a full https URL.`,
3974
+ docs: `${DOCS3}#assets`
3975
+ });
3976
+ return `{{asset "${asset.name}"}}`;
3977
+ }
3978
+ changes.add("binary assets (images, fonts) inlined as data URIs");
3979
+ return encoding === "base64" ? base64 : `data:${mimeOf(asset.name ?? "")};base64,${base64}`;
3980
+ };
3981
+ return source.replace(v3, (whole, _q, ref, _q2, encoding) => replace(whole, ref, encoding)).replace(v2, (whole, ref, encoding) => replace(whole, ref, encoding));
3982
+ }
3983
+ function inlineChildren(source, bundle, errors, warnings, changes) {
3984
+ let out = source;
3985
+ for (let depth = 0; depth < 6; depth++) {
3986
+ const before = out;
3987
+ out = out.replace(/\{\{\{?\s*childTemplate\s+(["'])([^"']+)\1[^}]*\}?\}\}|\{#child\s+([^\s}]+)[^}]*\}/g, (whole, _q, v3, v2) => {
3988
+ const ref = v3 ?? v2 ?? "";
3989
+ const child = byPath(bundle, bundle.templates, ref);
3990
+ if (!child?.content) {
3991
+ errors.push({ code: "child-missing", message: `The child template "${ref}" is not in the export.`, docs: `${DOCS3}#child-templates` });
3992
+ return whole;
3993
+ }
3994
+ changes.add("child templates inlined");
3995
+ return child.content;
3996
+ });
3997
+ out = out.replace(/\{\{\{?\s*component\s+(["'])([^"']+)\1([^}]*)\}?\}\}/g, (whole, _q, ref, props) => {
3998
+ const component = byPath(bundle, bundle.components, ref);
3999
+ if (!component?.content) {
4000
+ errors.push({ code: "component-missing", message: `The component "${ref}" is not in the export.`, docs: `${DOCS3}#components` });
4001
+ return whole;
4002
+ }
4003
+ if (props.trim())
4004
+ warnings.push({
4005
+ code: "component-props",
4006
+ message: `The component "${ref}" received properties (${props.trim()}); it was inlined with the surrounding data, so reference those values directly.`,
4007
+ docs: `${DOCS3}#components`
4008
+ });
4009
+ if (component.helpers?.trim())
4010
+ warnings.push({ code: "custom-helpers", message: `The component "${ref}" has JavaScript helpers, which Formfeed does not run.`, docs: `${DOCS3}#helpers` });
4011
+ changes.add("components inlined");
4012
+ return component.content;
4013
+ });
4014
+ if (out === before) break;
4015
+ }
4016
+ return out;
4017
+ }
4018
+ function helperNames(script) {
4019
+ const names = /* @__PURE__ */ new Set();
4020
+ for (const m of script.matchAll(/(?:^|[\s;])(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g)) names.add(m[1] ?? "");
4021
+ for (const m of script.matchAll(/(?:^|[\s;])(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s*)?(?:function\b|\([^)]*\)\s*=>|[A-Za-z_$][\w$]*\s*=>)/g)) names.add(m[1] ?? "");
4022
+ names.delete("");
4023
+ return [...names];
4024
+ }
4025
+ function mapChrome(template, kind, warnings, changes) {
4026
+ const settings = {};
4027
+ const chrome = template.chrome ?? {};
4028
+ const str2 = (key) => typeof chrome[key] === "string" && chrome[key].trim() ? chrome[key].trim() : void 0;
4029
+ const paper = {};
4030
+ const format = str2("format");
4031
+ if (format) paper.format = format.charAt(0).toUpperCase() + format.slice(1).toLowerCase();
4032
+ const width = lengthMm(str2("width"));
4033
+ const height = lengthMm(str2("height"));
4034
+ if (width && height) {
4035
+ paper.width = width;
4036
+ paper.height = height;
4037
+ }
4038
+ if (chrome["landscape"] === true || chrome["landscape"] === "true") paper.landscape = true;
4039
+ if (Object.keys(paper).length) settings.paper = paper;
4040
+ const margin = {};
4041
+ for (const side of ["top", "right", "bottom", "left"]) {
4042
+ const value = lengthMm(str2(`margin${side[0]?.toUpperCase()}${side.slice(1)}`));
4043
+ if (value) margin[side] = value;
4044
+ }
4045
+ if (Object.keys(margin).length) settings.margin = margin;
4046
+ const showHeaderFooter = chrome["displayHeaderFooter"] === true || chrome["displayHeaderFooter"] === "true";
4047
+ if (showHeaderFooter) {
4048
+ const header = str2("headerTemplate");
4049
+ const footer = str2("footerTemplate");
4050
+ if (header) settings.header = { html: header };
4051
+ if (footer) settings.footer = { html: footer };
4052
+ if (header || footer) changes.add("Chromium header and footer templates moved to the Header and Footer tabs");
4053
+ }
4054
+ if (chrome["printBackground"] !== void 0) settings.printBackground = chrome["printBackground"] === true || chrome["printBackground"] === "true";
4055
+ if (chrome["scale"] !== void 0 && Number.isFinite(Number(chrome["scale"]))) settings.scale = Number(chrome["scale"]);
4056
+ const ranges = str2("pageRanges");
4057
+ if (ranges) settings.pageRanges = ranges;
4058
+ if (chrome["waitForNetworkIdle"] === true || chrome["waitForNetworkIdle"] === "true") settings.waitFor = { networkIdle: true };
4059
+ if (chrome["waitForJS"] === true || chrome["waitForJS"] === "true")
4060
+ warnings.push({
4061
+ code: "wait-for-js",
4062
+ message: "The template sets window.JSREPORT_READY_TO_START; Formfeed waits for a CSS selector instead (Settings \u2192 Wait for).",
4063
+ docs: `${DOCS3}#settings`
4064
+ });
4065
+ if (str2("mediaType") === "screen") warnings.push({ code: "setting-ignored", message: 'mediaType "screen" is not supported; Formfeed prints with the print media type.', docs: `${DOCS3}#settings` });
4066
+ if (kind === "image") {
4067
+ const img = template.chromeImage ?? {};
4068
+ const type = String(img["type"] ?? "png").toLowerCase();
4069
+ settings.image = {
4070
+ width: Number(img["viewportWidth"] ?? 0) || 1200,
4071
+ height: Number(img["viewportHeight"] ?? 0) || 630,
4072
+ format: type === "jpeg" || type === "jpg" ? "jpeg" : "png",
4073
+ ...Number.isFinite(Number(img["quality"])) && img["quality"] !== void 0 ? { quality: Number(img["quality"]) } : {},
4074
+ ...img["fullPage"] === true ? { fullPage: true } : {},
4075
+ ...img["omitBackground"] === true ? { transparent: true } : {}
4076
+ };
4077
+ changes.add("chrome-image options mapped to the image settings");
4078
+ }
4079
+ const meta = template.pdfMeta ?? {};
4080
+ const metadata = Object.fromEntries(
4081
+ ["title", "author", "subject", "keywords"].filter((k) => typeof meta[k] === "string" && meta[k]).map((k) => [k, String(meta[k])])
4082
+ );
4083
+ if (Object.keys(metadata).length) {
4084
+ settings.pdf = { metadata };
4085
+ changes.add("PDF metadata (pdfMeta) mapped");
4086
+ }
4087
+ changes.add("chrome-pdf options mapped: paper, orientation, margins, background, scale, page ranges");
4088
+ return settings;
4089
+ }
4090
+ function importJsreport(bundle, ref, options = {}) {
4091
+ const opts = { inlineAssetBytes: options.inlineAssetBytes ?? 256 * 1024 };
4092
+ const template = bundle.templates.find((t) => t.shortid === ref || t._id === ref) ?? byPath(bundle, bundle.templates, ref);
4093
+ if (!template) throw new Error(`No template "${ref}" in the export.`);
4094
+ const errors = [];
4095
+ const warnings = [];
4096
+ const changes = /* @__PURE__ */ new Set();
4097
+ const name = (template.name ?? "").trim() || "Imported template";
4098
+ const engine = String(template.engine ?? "handlebars").toLowerCase();
4099
+ if (engine !== "handlebars" && engine !== "none")
4100
+ errors.push({
4101
+ code: "unsupported-engine",
4102
+ message: `The template uses the ${engine} engine; Formfeed runs Handlebars, Liquid and Jinja2. It was imported as Handlebars and needs rewriting.`,
4103
+ docs: `${DOCS3}#engines`
4104
+ });
4105
+ if (engine === "none") changes.add("template without an engine imported as Handlebars");
4106
+ const recipe = String(template.recipe ?? "chrome-pdf").toLowerCase();
4107
+ const kind = recipe === "chrome-image" ? "image" : "pdf";
4108
+ if (!["chrome-pdf", "chrome-image", "html"].includes(recipe))
4109
+ errors.push({
4110
+ code: "unsupported-recipe",
4111
+ message: `The ${recipe} recipe has no Formfeed equivalent; the template was imported as a PDF template.`,
4112
+ docs: `${DOCS3}#recipes`
4113
+ });
4114
+ if (recipe === "html") changes.add("html recipe imported as a PDF template");
4115
+ const settings = mapChrome(template, kind, warnings, changes);
4116
+ let html = inlineChildren(template.content ?? "", bundle, errors, warnings, changes);
4117
+ html = resolveAssets(html, bundle, opts, warnings, changes);
4118
+ for (const key of ["header", "footer"]) {
4119
+ const part = settings[key]?.html;
4120
+ if (part) settings[key] = { html: resolveAssets(inlineChildren(part, bundle, errors, warnings, changes), bundle, opts, warnings, changes) };
4121
+ }
4122
+ html = html.replace(/\{\{\{?\s*toJS\s+([^}]+?)\s*\}?\}\}/g, (_m, value) => {
4123
+ changes.add("`toJS` replaced by `json`");
4124
+ return `{{{json ${value}}}}`;
4125
+ });
4126
+ const helperSources = [
4127
+ template.helpers ?? "",
4128
+ ...bundle.assets.filter((a) => a.isSharedHelper).map((a) => {
4129
+ const b64 = assetBase64(a.content);
4130
+ return b64 ? decodeText(b64) : "";
4131
+ })
4132
+ ].filter((s) => s.trim());
4133
+ const helpers = helperSources.flatMap(helperNames);
4134
+ if (helpers.length)
4135
+ warnings.push({
4136
+ code: "custom-helpers",
4137
+ message: `The template defines JavaScript helpers (${helpers.join(", ")}); Formfeed does not run custom code. Replace each call with a built-in helper, or compute the value before sending the data.`,
4138
+ docs: `${DOCS3}#helpers`
4139
+ });
4140
+ warnings.push(
4141
+ ...notesFor(html, /\{\{\{?\s*(pdfAddPageItem|pdfCreatePagesGroup|pdfFormField|module)\b/, {
4142
+ code: "jsreport-helper",
4143
+ message: "A jsreport system helper (pdf-utils or module) has no Formfeed equivalent.",
4144
+ docs: `${DOCS3}#helpers`
4145
+ })
4146
+ );
4147
+ if (template.scripts?.length)
4148
+ warnings.push({ code: "scripts", message: "The template runs jsreport scripts (beforeRender/afterRender); load that data in your application and send it with the render request.", docs: `${DOCS3}#scripts` });
4149
+ if (template.pdfOperations?.length)
4150
+ warnings.push({ code: "pdf-operations", message: "pdf-utils operations (merge, append, headers from another template) are not imported.", docs: `${DOCS3}#pdf-utils` });
4151
+ if (template.pdfPassword && Object.values(template.pdfPassword).some(Boolean))
4152
+ warnings.push({ code: "pdf-password", message: "PDF password protection is not imported.", docs: `${DOCS3}#settings` });
4153
+ if (template.localization?.language)
4154
+ warnings.push({ code: "localization", message: "jsreport localization files are not imported; use the template dictionaries and the `t` helper.", docs: "/templates/helpers#t" });
4155
+ const dataEntity = template.data?.shortid ? bundle.data.find((d) => d.shortid === template.data?.shortid) : void 0;
4156
+ const sample = parseSample(dataEntity?.dataJson);
4157
+ if (sample.error) warnings.push({ code: "sample-json", message: `The sample data is not valid JSON (${sample.error}).`, docs: `${DOCS3}#sample-data` });
4158
+ if (dataEntity) changes.add(`sample data "${dataEntity.name}" became the default data set`);
4159
+ if (new TextEncoder().encode(html).length > 1e6)
4160
+ errors.push({ code: "too-large", message: 'The template is larger than 1 MB after inlining its assets; upload the large files as workspace assets and reference them with {{asset "name"}}.', docs: `${DOCS3}#assets` });
4161
+ const checked = checkTemplate("handlebars", html, sample.data, name, {
4162
+ syntax: "/templates/languages#handlebars",
4163
+ filters: `${DOCS3}#helpers`,
4164
+ sampleData: `${DOCS3}#sample-data`
4165
+ });
4166
+ errors.push(...checked.errors);
4167
+ warnings.push(...checked.warnings);
4168
+ return {
4169
+ source: "jsreport",
4170
+ name,
4171
+ slug: slugFromName(name),
4172
+ kind,
4173
+ engine: "handlebars",
4174
+ html,
4175
+ css: "",
4176
+ head: "",
4177
+ settings,
4178
+ sampleData: sample.data,
4179
+ errors,
4180
+ warnings,
4181
+ changes: [...changes]
4182
+ };
4183
+ }
4184
+
3056
4185
  // packages/engine/src/lib/assemble.ts
3057
4186
  function defaultSettings() {
3058
4187
  return {
@@ -3272,6 +4401,37 @@ document.addEventListener('click', function (event) {
3272
4401
  } catch (e) {}
3273
4402
  }, true);
3274
4403
  </script>`;
4404
+ var pageNavScript = `<script>
4405
+ (function () {
4406
+ var pages = function () { return document.querySelectorAll('.pagedjs_page'); };
4407
+ window.addEventListener('message', function (event) {
4408
+ var data = event.data;
4409
+ if (!data || data.type !== 'formfeed:goto-page') return;
4410
+ var page = pages()[(data.page || 1) - 1];
4411
+ if (page && page.scrollIntoView) page.scrollIntoView({ block: 'start' });
4412
+ });
4413
+ var current = 0;
4414
+ var report = function () {
4415
+ var list = pages();
4416
+ if (!list.length) return;
4417
+ var best = 1;
4418
+ var bestTop = Infinity;
4419
+ for (var i = 0; i < list.length; i++) {
4420
+ var top = Math.abs(list[i].getBoundingClientRect().top);
4421
+ if (top < bestTop) { bestTop = top; best = i + 1; }
4422
+ }
4423
+ if (best === current) return;
4424
+ current = best;
4425
+ try { parent.postMessage({ type: 'formfeed:page', page: best }, '*'); } catch (e) {}
4426
+ };
4427
+ var queued = false;
4428
+ window.addEventListener('scroll', function () {
4429
+ if (queued) return;
4430
+ queued = true;
4431
+ requestAnimationFrame(function () { queued = false; report(); });
4432
+ }, { passive: true });
4433
+ })();
4434
+ </script>`;
3275
4435
  function flowDocument(draft) {
3276
4436
  const { settings, kind, headerHtml, footerHtml } = draft;
3277
4437
  const margin = settings.margin ?? {};
@@ -3298,9 +4458,10 @@ html { background: #e5e7eb; }
3298
4458
  .pagedjs_pages { margin: 0 auto; }
3299
4459
  .pagedjs_page { background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.2), 0 8px 24px rgba(0,0,0,.08); margin: 16px auto; }
3300
4460
  `;
4461
+ const selectorGuard = `<script>(function(){var q=DocumentFragment.prototype.querySelectorAll;DocumentFragment.prototype.querySelectorAll=function(s){try{return q.call(this,s);}catch(e){console.warn('formfeed paged preview: skipped a selector Paged.js could not use:',s);return q.call(this,':not(*)');}};})();</script>`;
3301
4462
  const config = `<script>window.PagedConfig = { auto: true, after: function (flow) { try { if (window.formfeedDrawCharts) window.formfeedDrawCharts(); } catch (e) {} try { parent.postMessage({ type: 'formfeed:pages', pages: flow.total }, '*'); } catch (e) {} } };</script>`;
3302
4463
  const script = `<script src="${options.pagedScriptUrl}"></script>`;
3303
4464
  const header = headerHtml ? `<div class="ff-running-header">${pageNumberSpans(headerHtml, '<span class="ff-page-no"></span>', '<span class="ff-page-total"></span>')}</div>` : "";
3304
4465
  const footer = footerHtml ? `<div class="ff-running-footer">${pageNumberSpans(footerHtml, '<span class="ff-page-no"></span>', '<span class="ff-page-total"></span>')}</div>` : "";
3305
- return draft.document.replace("</head>", `<style data-formfeed="paged">${runningCss}</style>${config}${script}${inspectScript}</head>`).replace(/(<body[^>]*>)/, `$1${header}${footer}`);
4466
+ return draft.document.replace("</head>", `<style data-formfeed="paged">${runningCss}</style>${selectorGuard}${config}${script}${inspectScript}${pageNavScript}</head>`).replace(/(<body[^>]*>)/, `$1${header}${footer}`);
3306
4467
  }