@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.js CHANGED
@@ -449,7 +449,7 @@ var formatHelpers = [
449
449
  name: "asset",
450
450
  doc: {
451
451
  signature: "asset(name)",
452
- description: "URL of a workspace asset (logo, background) on the CDN.",
452
+ 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.",
453
453
  example: `<img src="{{ asset('logo.png') }}">`,
454
454
  category: "document"
455
455
  },
@@ -949,6 +949,12 @@ function helperDocs(registry = defaultHelpers()) {
949
949
  // packages/engine/src/lib/engines/handlebars.ts
950
950
  import Handlebars from "handlebars";
951
951
 
952
+ // packages/engine/src/lib/unavailable.ts
953
+ var UNAVAILABLE = {};
954
+ function unavailableHelper(name) {
955
+ return UNAVAILABLE[name];
956
+ }
957
+
952
958
  // packages/engine/src/lib/analysis/tags.ts
953
959
  function rangeAt(line, column, length) {
954
960
  return {
@@ -1128,7 +1134,10 @@ function missingPathDiagnostics(variables, sampleData, loopSources, builtinRoots
1128
1134
  const root = v.path[0];
1129
1135
  if (!root || builtinRoots4.has(root)) continue;
1130
1136
  let path = v.path;
1131
- if (loopSources.has(root)) {
1137
+ if (v.source !== void 0) {
1138
+ if (!v.source) continue;
1139
+ path = [...v.source, ...v.path.slice(1)];
1140
+ } else if (loopSources.has(root)) {
1132
1141
  const source = loopSources.get(root);
1133
1142
  if (!source) continue;
1134
1143
  path = [...source, ...v.path.slice(1)];
@@ -1387,6 +1396,13 @@ function analyzeHandlebars(source, opts = {}) {
1387
1396
  range: f.range,
1388
1397
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
1389
1398
  });
1399
+ } else if (unavailableHelper(f.name)) {
1400
+ diagnostics.push({
1401
+ severity: "warning",
1402
+ code: "unavailable-helper",
1403
+ message: unavailableHelper(f.name),
1404
+ range: f.range
1405
+ });
1390
1406
  } else if (!f.known) {
1391
1407
  diagnostics.push({
1392
1408
  severity: "error",
@@ -1495,6 +1511,151 @@ var handlebarsEngine = {
1495
1511
 
1496
1512
  // packages/engine/src/lib/engines/jinja2.ts
1497
1513
  import nunjucks from "nunjucks/browser/nunjucks.js";
1514
+
1515
+ // packages/engine/src/lib/engines/python-format.ts
1516
+ var SPEC = /^(?:([\s\S])?([<>=^]))?([+\- ])?(#)?(0)?(\d+)?([,_])?(?:\.(\d+))?([bcdeEfFgGnosxX%])?$/;
1517
+ function group(digits, separator) {
1518
+ return digits.replace(/\B(?=(\d{3})+(?!\d))/g, separator);
1519
+ }
1520
+ function pythonExponent(text) {
1521
+ return text.replace(/e([+-])(\d)$/i, (m, sign, digit) => `${m[0]}${sign}0${digit}`);
1522
+ }
1523
+ function formatNumber(value, type, precision, alternate) {
1524
+ if (!Number.isFinite(value)) return Number.isNaN(value) ? "nan" : "inf";
1525
+ const abs = Math.abs(value);
1526
+ switch (type) {
1527
+ case "f":
1528
+ case "F":
1529
+ return abs.toFixed(precision ?? 6);
1530
+ case "%":
1531
+ return `${(abs * 100).toFixed(precision ?? 6)}%`;
1532
+ case "e":
1533
+ case "E": {
1534
+ const text = pythonExponent(abs.toExponential(precision ?? 6));
1535
+ return type === "E" ? text.toUpperCase() : text;
1536
+ }
1537
+ case "g":
1538
+ case "G": {
1539
+ const p = precision === 0 ? 1 : precision ?? 6;
1540
+ const exp = abs === 0 ? 0 : Math.floor(Math.log10(abs));
1541
+ let text = exp < -4 || exp >= p ? pythonExponent(abs.toExponential(p - 1)) : abs.toFixed(Math.max(0, p - 1 - exp));
1542
+ if (!alternate) {
1543
+ const [mantissa = "", exponent] = text.split("e");
1544
+ const trimmed = mantissa.includes(".") ? mantissa.replace(/0+$/, "").replace(/\.$/, "") : mantissa;
1545
+ text = exponent === void 0 ? trimmed : `${trimmed}e${exponent}`;
1546
+ }
1547
+ return type === "G" ? text.toUpperCase() : text;
1548
+ }
1549
+ case "d":
1550
+ case "n":
1551
+ return String(Math.trunc(abs));
1552
+ case "b":
1553
+ return (alternate ? "0b" : "") + Math.trunc(abs).toString(2);
1554
+ case "o":
1555
+ return (alternate ? "0o" : "") + Math.trunc(abs).toString(8);
1556
+ case "x":
1557
+ return (alternate ? "0x" : "") + Math.trunc(abs).toString(16);
1558
+ case "X":
1559
+ return (alternate ? "0X" : "") + Math.trunc(abs).toString(16).toUpperCase();
1560
+ default:
1561
+ return precision === void 0 ? String(abs) : formatNumber(abs, "g", precision, alternate);
1562
+ }
1563
+ }
1564
+ function formatValue(value, spec) {
1565
+ const m = SPEC.exec(spec);
1566
+ if (!m) return String(value ?? "");
1567
+ const [, fillChar, alignChar, sign, alt, zero, widthText, grouping, precisionText, type = ""] = m;
1568
+ const precision = precisionText === void 0 ? void 0 : Number(precisionText);
1569
+ const width = widthText === void 0 ? 0 : Number(widthText);
1570
+ const numericType = /[bdeEfFgGnoxX%]/.test(type);
1571
+ 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;
1572
+ let body;
1573
+ let prefix = "";
1574
+ if (asNumber !== void 0 && type !== "s" && type !== "c") {
1575
+ body = formatNumber(asNumber, type, precision, Boolean(alt));
1576
+ if (grouping) {
1577
+ const [int = "", ...rest] = body.split(".");
1578
+ const radix = /^0[box]/i.exec(int)?.[0] ?? "";
1579
+ body = radix + group(int.slice(radix.length), grouping) + (rest.length ? `.${rest.join(".")}` : "");
1580
+ }
1581
+ const negative = asNumber < 0 || Object.is(asNumber, -0);
1582
+ prefix = negative ? "-" : sign === "+" ? "+" : sign === " " ? " " : "";
1583
+ } else if (type === "c" && asNumber !== void 0) {
1584
+ body = String.fromCodePoint(asNumber);
1585
+ } else {
1586
+ body = String(value ?? "None");
1587
+ if (precision !== void 0 && !numericType) body = body.slice(0, precision);
1588
+ }
1589
+ const isNumber = asNumber !== void 0 && type !== "s";
1590
+ const align = alignChar ?? (zero && isNumber ? "=" : isNumber ? ">" : "<");
1591
+ const fill = fillChar ?? (zero && !alignChar ? "0" : " ");
1592
+ const pad = Math.max(0, width - prefix.length - body.length);
1593
+ switch (align) {
1594
+ case "<":
1595
+ return prefix + body + fill.repeat(pad);
1596
+ case "^":
1597
+ return fill.repeat(Math.floor(pad / 2)) + prefix + body + fill.repeat(Math.ceil(pad / 2));
1598
+ case "=":
1599
+ return prefix + fill.repeat(pad) + body;
1600
+ default:
1601
+ return fill.repeat(pad) + prefix + body;
1602
+ }
1603
+ }
1604
+ function lookupField(name, positional, named, next) {
1605
+ const [head = "", ...rest] = name.split(/(?=[.[])/);
1606
+ let value = head === "" ? positional[next()] : /^\d+$/.test(head) ? positional[Number(head)] : named[head];
1607
+ for (const part of rest) {
1608
+ const key = part.startsWith("[") ? part.slice(1, -1) : part.slice(1);
1609
+ value = value !== null && typeof value === "object" ? value[Array.isArray(value) && /^\d+$/.test(key) ? Number(key) : key] : void 0;
1610
+ }
1611
+ return value;
1612
+ }
1613
+ function pythonFormat(template, positional, named = {}) {
1614
+ let auto = 0;
1615
+ const next = () => auto++;
1616
+ const render = (text) => {
1617
+ let out = "";
1618
+ for (let i = 0; i < text.length; i++) {
1619
+ const ch = text[i];
1620
+ if (ch === "}") {
1621
+ out += "}";
1622
+ if (text[i + 1] === "}") i++;
1623
+ continue;
1624
+ }
1625
+ if (ch !== "{") {
1626
+ out += ch;
1627
+ continue;
1628
+ }
1629
+ if (text[i + 1] === "{") {
1630
+ out += "{";
1631
+ i++;
1632
+ continue;
1633
+ }
1634
+ let depth = 1;
1635
+ let j = i + 1;
1636
+ for (; j < text.length && depth > 0; j++) {
1637
+ if (text[j] === "{") depth++;
1638
+ else if (text[j] === "}") depth--;
1639
+ }
1640
+ if (depth > 0) {
1641
+ out += text.slice(i);
1642
+ break;
1643
+ }
1644
+ const field = text.slice(i + 1, j - 1);
1645
+ const colon = field.indexOf(":");
1646
+ const head = colon < 0 ? field : field.slice(0, colon);
1647
+ const name = head.replace(/![rsa]$/, "");
1648
+ const value = lookupField(name, positional, named, next);
1649
+ const spec = colon < 0 ? "" : render(field.slice(colon + 1));
1650
+ out += value === void 0 ? "" : formatValue(value, spec);
1651
+ i = j - 1;
1652
+ }
1653
+ return out;
1654
+ };
1655
+ return render(template);
1656
+ }
1657
+
1658
+ // packages/engine/src/lib/engines/jinja2.ts
1498
1659
  var nunjucksBuiltinFilters = /* @__PURE__ */ new Set([
1499
1660
  "abs",
1500
1661
  "batch",
@@ -1549,7 +1710,12 @@ var builtinRoots2 = /* @__PURE__ */ new Set([
1549
1710
  "joiner",
1550
1711
  "lipsum",
1551
1712
  "varargs",
1552
- "kwargs"
1713
+ "kwargs",
1714
+ "namespace",
1715
+ // Jinja2's spelling of the literals, which the compat layer resolves at runtime
1716
+ "True",
1717
+ "False",
1718
+ "None"
1553
1719
  ]);
1554
1720
  var stringMethods = {
1555
1721
  upper: (s) => s.toUpperCase(),
@@ -1578,12 +1744,8 @@ var stringMethods = {
1578
1744
  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),
1579
1745
  join: (s, list2) => Array.isArray(list2) ? list2.join(s) : String(list2),
1580
1746
  format: (s, ...args) => {
1581
- let i = 0;
1582
- const named = args.at(-1) && typeof args.at(-1) === "object" ? args.at(-1) : {};
1583
- return s.replace(
1584
- /\{(\w*)\}/g,
1585
- (_, key) => String(key ? named[key] ?? "" : args[i++] ?? "")
1586
- );
1747
+ const named = keywordArgs(args);
1748
+ return pythonFormat(s, named ? args.slice(0, -1) : args, named ?? {});
1587
1749
  },
1588
1750
  zfill: (s, width) => s.padStart(Number(width), "0"),
1589
1751
  count: (s, sub) => s.split(String(sub)).length - 1,
@@ -1598,6 +1760,92 @@ var stringMethods = {
1598
1760
  return f.repeat(left) + s + f.repeat(total - left);
1599
1761
  }
1600
1762
  };
1763
+ function keywordArgs(args) {
1764
+ const last = args.at(-1);
1765
+ if (!last || typeof last !== "object" || !Object.prototype.hasOwnProperty.call(last, "__keywords")) return void 0;
1766
+ const { __keywords: _marker, ...named } = last;
1767
+ return named;
1768
+ }
1769
+ var JinjaNamespace = class {
1770
+ };
1771
+ function namespace(...args) {
1772
+ const ns = new JinjaNamespace();
1773
+ const named = keywordArgs(args);
1774
+ for (const arg of named ? [...args.slice(0, -1), named] : args)
1775
+ if (arg && typeof arg === "object") Object.assign(ns, arg);
1776
+ return ns;
1777
+ }
1778
+ function setNamespaceAttribute(target, key, value) {
1779
+ if (!(target instanceof JinjaNamespace)) throw new Error("Cannot assign attribute on non-namespace object");
1780
+ target[String(key)] = value;
1781
+ }
1782
+ var toList = (v) => Array.isArray(v) ? v : v === void 0 || v === null ? [] : typeof v === "string" ? [...v] : [v];
1783
+ var attributeOf = (item, path) => String(path ?? "").split(".").reduce((acc, key) => acc !== null && typeof acc === "object" ? acc[key] : void 0, item);
1784
+ var jinjaTests = {
1785
+ none: (v) => v === null,
1786
+ true: (v) => v === true,
1787
+ false: (v) => v === false,
1788
+ in: (v, container) => typeof container === "string" ? container.includes(String(v)) : Array.isArray(container) ? container.includes(v) : !!container && typeof container === "object" && String(v) in container,
1789
+ "==": (v, a) => v === a,
1790
+ "!=": (v, a) => v !== a,
1791
+ ">": (v, a) => v > a,
1792
+ ">=": (v, a) => v >= a,
1793
+ "<": (v, a) => v < a,
1794
+ "<=": (v, a) => v <= a
1795
+ };
1796
+ function runTest(self, name, value, arg) {
1797
+ const own = jinjaTests[name];
1798
+ if (own) return own(value, arg);
1799
+ return self.env.getTest(name).call(self, value, arg) === true;
1800
+ }
1801
+ function installJinjaFilters(env) {
1802
+ const lookup = env;
1803
+ const helperTruncate = lookup.getFilter("truncate");
1804
+ lookup.addFilter("truncate", function(value, ...args) {
1805
+ const named = keywordArgs(args);
1806
+ const p = named ? args.slice(0, -1) : args;
1807
+ if (!named && typeof p[1] !== "boolean" && p.length < 3) return helperTruncate.call(this, value, ...args);
1808
+ const s = String(value ?? "");
1809
+ const length = Number(p[0] ?? named?.["length"] ?? 255);
1810
+ const killwords = Boolean(p[1] ?? named?.["killwords"] ?? false);
1811
+ const end = String(p[2] ?? named?.["end"] ?? "...");
1812
+ const leeway = Number(p[3] ?? named?.["leeway"] ?? 5);
1813
+ if (s.length <= length + leeway) return s;
1814
+ const cut = s.slice(0, Math.max(0, length - end.length));
1815
+ if (killwords) return cut + end;
1816
+ const space = cut.lastIndexOf(" ");
1817
+ return (space >= 0 ? cut.slice(0, space) : cut) + end;
1818
+ });
1819
+ const helperMap = lookup.getFilter("map");
1820
+ lookup.addFilter("map", function(value, ...args) {
1821
+ const named = keywordArgs(args);
1822
+ const p = named ? args.slice(0, -1) : args;
1823
+ if (named && "attribute" in named)
1824
+ return toList(value).map((item) => {
1825
+ const v = attributeOf(item, named["attribute"]);
1826
+ return v === void 0 && "default" in named ? named["default"] : v;
1827
+ });
1828
+ const items = toList(value);
1829
+ if (typeof p[0] === "string" && !items.some((item) => item !== null && typeof item === "object")) {
1830
+ let filter;
1831
+ try {
1832
+ filter = lookup.getFilter(p[0]);
1833
+ } catch {
1834
+ filter = void 0;
1835
+ }
1836
+ if (filter) return items.map((item) => filter.call(this, item, ...p.slice(1)));
1837
+ }
1838
+ return helperMap.call(this, value, ...args);
1839
+ });
1840
+ const byAttribute = (keep) => function(value, attribute, test, arg) {
1841
+ return toList(value).filter((item) => {
1842
+ const v = attributeOf(item, attribute);
1843
+ return (test === void 0 ? Boolean(v) : runTest(this, String(test), v, arg)) === keep;
1844
+ });
1845
+ };
1846
+ lookup.addFilter("selectattr", byAttribute(true));
1847
+ lookup.addFilter("rejectattr", byAttribute(false));
1848
+ }
1601
1849
  function trimChars(s, chars, side) {
1602
1850
  if (chars === void 0 || chars === null)
1603
1851
  return side === "both" ? s.trim() : side === "left" ? s.trimStart() : s.trimEnd();
@@ -1618,6 +1866,20 @@ function installCompat() {
1618
1866
  this.compile(node.target, frame);
1619
1867
  this._emit(")");
1620
1868
  };
1869
+ const compileSet = nunjucks.compiler.Compiler.prototype.compileSet;
1870
+ nunjucks.runtime["formfeedSetAttribute"] = setNamespaceAttribute;
1871
+ nunjucks.compiler.Compiler.prototype.compileSet = function(node, frame) {
1872
+ const [target, ...more] = node.targets;
1873
+ if (!target || more.length || target.typename !== "LookupVal") return compileSet.call(this, node, frame);
1874
+ this._emit("runtime.formfeedSetAttribute(");
1875
+ this._compileExpression(target["target"], frame);
1876
+ this._emit(", ");
1877
+ this._compileExpression(target["val"], frame);
1878
+ this._emit(", ");
1879
+ if (node.value) this._compileExpression(node.value, frame);
1880
+ else this.compile(node.body, frame);
1881
+ this._emitLine(");");
1882
+ };
1621
1883
  const original = nunjucks.runtime.memberLookup;
1622
1884
  nunjucks.runtime.memberLookup = (obj, val2, ...rest) => {
1623
1885
  if (typeof obj === "string" && typeof val2 === "string" && Object.prototype.hasOwnProperty.call(stringMethods, val2)) {
@@ -1664,6 +1926,8 @@ function createEnvironment(ctx) {
1664
1926
  env.addFilter(name, wrapped);
1665
1927
  env.addGlobal(name, wrapped);
1666
1928
  }
1929
+ env.addGlobal("namespace", namespace);
1930
+ installJinjaFilters(env);
1667
1931
  return env;
1668
1932
  }
1669
1933
  var validationContext2 = {
@@ -1704,6 +1968,13 @@ function pathOf(node) {
1704
1968
  function inScope(w, name) {
1705
1969
  return w.scopes.some((s) => s.has(name));
1706
1970
  }
1971
+ function sourceOf(w, name) {
1972
+ for (let i = w.scopes.length - 1; i >= 0; i--) {
1973
+ const scope = w.scopes[i];
1974
+ if (scope?.has(name)) return scope.get(name) ?? null;
1975
+ }
1976
+ return null;
1977
+ }
1707
1978
  function walk2(node, w) {
1708
1979
  if (!node || typeof node !== "object") return;
1709
1980
  switch (node.typename) {
@@ -1712,10 +1983,12 @@ function walk2(node, w) {
1712
1983
  const p = pathOf(node);
1713
1984
  if (p) {
1714
1985
  const root = p.path[0] ?? "";
1986
+ const bound = inScope(w, root);
1715
1987
  w.variables.push({
1716
1988
  path: p.path,
1717
1989
  range: nodeRange(p.root, p.path.join(".").length),
1718
- kind: inScope(w, root) ? "loop-var" : "read"
1990
+ kind: bound ? "loop-var" : "read",
1991
+ ...bound ? { source: sourceOf(w, root) } : {}
1719
1992
  });
1720
1993
  }
1721
1994
  if (node.typename === "LookupVal" && !p) {
@@ -1761,18 +2034,18 @@ function walk2(node, w) {
1761
2034
  const source = pathOf(arr);
1762
2035
  const nameNode = node["name"];
1763
2036
  const names = nameNode.typename === "Array" ? nameNode.children ?? [] : [nameNode];
1764
- const scope = /* @__PURE__ */ new Set();
2037
+ const root = source?.path[0] ?? "";
2038
+ const items = !source ? null : inScope(w, root) ? ((s) => s ? [...s, ...source.path.slice(1)] : null)(sourceOf(w, root)) : source.path;
2039
+ const scope = /* @__PURE__ */ new Map();
1765
2040
  for (const n of names) {
1766
2041
  const name = String(n.value);
1767
- scope.add(name);
1768
- w.loopSources.set(
1769
- name,
1770
- names.length === 1 && source && !inScope(w, source.path[0] ?? "") ? source.path : names.length === 1 && source ? resolveLoopSource(w, source.path) : null
1771
- );
2042
+ const bound = names.length === 1 ? items : null;
2043
+ scope.set(name, bound);
1772
2044
  w.variables.push({
1773
2045
  path: [name],
1774
2046
  range: nodeRange(n, name.length),
1775
- kind: "loop-var"
2047
+ kind: "loop-var",
2048
+ source: bound
1776
2049
  });
1777
2050
  }
1778
2051
  w.scopes.push(scope);
@@ -1790,8 +2063,7 @@ function walk2(node, w) {
1790
2063
  range: nodeRange(p.root, p.path.join(".").length),
1791
2064
  kind: "assigned"
1792
2065
  });
1793
- w.scopes[0]?.add(p.path[0] ?? "");
1794
- w.loopSources.set(p.path[0] ?? "", null);
2066
+ if (p.path.length === 1 || !inScope(w, p.path[0] ?? "")) w.scopes[0]?.set(p.path[0] ?? "", null);
1795
2067
  }
1796
2068
  }
1797
2069
  walk2(node["value"], w);
@@ -1800,17 +2072,27 @@ function walk2(node, w) {
1800
2072
  }
1801
2073
  case "Macro": {
1802
2074
  const args = node["args"];
1803
- const scope = /* @__PURE__ */ new Set();
1804
- for (const child of args.children ?? [])
1805
- if (child.typename === "Symbol") scope.add(String(child.value));
2075
+ const scope = /* @__PURE__ */ new Map();
2076
+ for (const child of args.children ?? []) {
2077
+ if (child.typename === "Symbol") scope.set(String(child.value), null);
2078
+ if (child.typename === "KeywordArgs")
2079
+ for (const pair of child.children ?? []) {
2080
+ const key = pair["key"];
2081
+ if (key) scope.set(String(key.value), null);
2082
+ walk2(pair["value"], w);
2083
+ }
2084
+ }
1806
2085
  const nameNode = node["name"];
1807
- w.scopes[0]?.add(String(nameNode.value));
1808
- w.loopSources.set(String(nameNode.value), null);
2086
+ w.scopes[0]?.set(String(nameNode.value), null);
1809
2087
  w.scopes.push(scope);
1810
2088
  walk2(node["body"], w);
1811
2089
  w.scopes.pop();
1812
2090
  return;
1813
2091
  }
2092
+ case "Pair": {
2093
+ walk2(node["value"], w);
2094
+ return;
2095
+ }
1814
2096
  case "Include":
1815
2097
  case "Extends":
1816
2098
  case "Import":
@@ -1823,15 +2105,17 @@ function walk2(node, w) {
1823
2105
  });
1824
2106
  else walk2(tpl, w);
1825
2107
  if (node.typename === "Import")
1826
- w.scopes[0]?.add(
1827
- String(node["target"]?.value ?? "")
2108
+ w.scopes[0]?.set(
2109
+ String(node["target"]?.value ?? ""),
2110
+ null
1828
2111
  );
1829
2112
  if (node.typename === "FromImport")
1830
2113
  for (const n of node["names"].children ?? [])
1831
- w.scopes[0]?.add(
2114
+ w.scopes[0]?.set(
1832
2115
  String(
1833
2116
  n.typename === "Pair" ? n["value"].value : n.value
1834
- )
2117
+ ),
2118
+ null
1835
2119
  );
1836
2120
  return;
1837
2121
  }
@@ -1849,12 +2133,6 @@ function walk2(node, w) {
1849
2133
  walk2(value, w);
1850
2134
  }
1851
2135
  }
1852
- function resolveLoopSource(w, path) {
1853
- const root = path[0] ?? "";
1854
- if (!w.loopSources.has(root)) return path;
1855
- const source = w.loopSources.get(root);
1856
- return source ? [...source, ...path.slice(1)] : null;
1857
- }
1858
2136
  function analyzeJinja2(source, opts = {}) {
1859
2137
  const helpers = opts.helpers ?? defaultHelpers();
1860
2138
  const { blocks, diagnostics } = scanBlocks(source, jinjaTags);
@@ -1862,8 +2140,7 @@ function analyzeJinja2(source, opts = {}) {
1862
2140
  variables: [],
1863
2141
  filters: [],
1864
2142
  includes: [],
1865
- loopSources: /* @__PURE__ */ new Map(),
1866
- scopes: [/* @__PURE__ */ new Set()],
2143
+ scopes: [/* @__PURE__ */ new Map()],
1867
2144
  helpers
1868
2145
  };
1869
2146
  try {
@@ -1892,6 +2169,13 @@ function analyzeJinja2(source, opts = {}) {
1892
2169
  range: f.range,
1893
2170
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
1894
2171
  });
2172
+ } else if (unavailableHelper(f.name)) {
2173
+ diagnostics.push({
2174
+ severity: "warning",
2175
+ code: "unavailable-helper",
2176
+ message: unavailableHelper(f.name),
2177
+ range: f.range
2178
+ });
1895
2179
  } else if (!f.known) {
1896
2180
  diagnostics.push({
1897
2181
  severity: "error",
@@ -1905,7 +2189,7 @@ function analyzeJinja2(source, opts = {}) {
1905
2189
  const dataDiagnostics = missingPathDiagnostics(
1906
2190
  w.variables,
1907
2191
  opts.sampleData,
1908
- w.loopSources,
2192
+ /* @__PURE__ */ new Map(),
1909
2193
  builtin
1910
2194
  );
1911
2195
  return {
@@ -2038,6 +2322,10 @@ var liquidBuiltinFilters = /* @__PURE__ */ new Set([
2038
2322
  "url_decode",
2039
2323
  "url_encode",
2040
2324
  "where",
2325
+ "where_exp",
2326
+ "find_exp",
2327
+ "reject_exp",
2328
+ "group_by_exp",
2041
2329
  "raw",
2042
2330
  "sum",
2043
2331
  "group_by",
@@ -2292,6 +2580,13 @@ function analyzeLiquid(source, opts = {}) {
2292
2580
  range: f.range,
2293
2581
  fix: { title: `Rename to ${preferred}`, replacement: preferred }
2294
2582
  });
2583
+ } else if (unavailableHelper(f.name)) {
2584
+ diagnostics.push({
2585
+ severity: "warning",
2586
+ code: "unavailable-helper",
2587
+ message: unavailableHelper(f.name),
2588
+ range: f.range
2589
+ });
2295
2590
  } else if (!f.known) {
2296
2591
  diagnostics.push({
2297
2592
  severity: "error",
@@ -2833,7 +3128,12 @@ function mapApitemplateSettings(input) {
2833
3128
  return { settings, ignored, notes };
2834
3129
  }
2835
3130
 
2836
- // packages/engine/src/lib/import/apitemplate.ts
3131
+ // packages/api-types/src/lib/assets.ts
3132
+ var SEGMENT = "[A-Za-z0-9][A-Za-z0-9._-]*";
3133
+ var ASSET_NAME_PATTERN = new RegExp(`^${SEGMENT}(?:/${SEGMENT})*$`);
3134
+ var ASSET_MAX_BYTES = 10 * 1024 * 1024;
3135
+
3136
+ // packages/engine/src/lib/import/common.ts
2837
3137
  function slugFromName(name) {
2838
3138
  const slug = name.toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
2839
3139
  return slug.length >= 2 ? slug : `imported-${Date.now().toString(36)}`;
@@ -2847,6 +3147,85 @@ function parseSample(value) {
2847
3147
  return { data: {}, error: e instanceof Error ? e.message : String(e) };
2848
3148
  }
2849
3149
  }
3150
+ function positionOf2(source, index) {
3151
+ const before = source.slice(0, index);
3152
+ const line = before.split("\n").length;
3153
+ const column = index - before.lastIndexOf("\n");
3154
+ return { line, column };
3155
+ }
3156
+ function notesFor(source, pattern, note, { once = false } = {}) {
3157
+ const global = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
3158
+ const out = [];
3159
+ for (const m of source.matchAll(global)) {
3160
+ out.push({ ...note, ...positionOf2(source, m.index ?? 0) });
3161
+ if (once) break;
3162
+ }
3163
+ return out;
3164
+ }
3165
+ function checkTemplate(engineId, html, sampleData, name, docs) {
3166
+ const errors = [];
3167
+ const warnings = [];
3168
+ const engine = getEngine(engineId);
3169
+ try {
3170
+ engine.compile(html, { name });
3171
+ } catch (e) {
3172
+ const err = e;
3173
+ errors.push({ code: "syntax-error", message: err.message, line: err.line, column: err.column, docs: docs.syntax });
3174
+ }
3175
+ const fromDiagnostic = (d) => ({
3176
+ code: d.code,
3177
+ message: d.message,
3178
+ line: d.range.start.line,
3179
+ column: d.range.start.column,
3180
+ docs: d.code === "unknown-filter" ? docs.filters : d.code.startsWith("missing-") ? docs.sampleData : docs.syntax
3181
+ });
3182
+ for (const d of engine.analyze(html, { sampleData }).diagnostics) {
3183
+ const note = fromDiagnostic(d);
3184
+ if (d.severity === "error") {
3185
+ if (!errors.some((x) => x.code === note.code && x.line === note.line)) errors.push(note);
3186
+ } else warnings.push(note);
3187
+ }
3188
+ return { errors, warnings };
3189
+ }
3190
+ function lengthMm(value) {
3191
+ if (value === void 0 || value === null || value === "") return void 0;
3192
+ if (typeof value === "number" && Number.isFinite(value)) return `${value}mm`;
3193
+ const text = String(value).trim();
3194
+ if (/^-?\d+(\.\d+)?$/.test(text)) return `${text}mm`;
3195
+ if (/^-?\d+(\.\d+)?(mm|cm|in|px|pt)$/i.test(text)) return text.toLowerCase();
3196
+ return void 0;
3197
+ }
3198
+ function toBase64(value) {
3199
+ if (typeof value === "string") return value;
3200
+ 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;
3201
+ if (!bytes) return void 0;
3202
+ let binary = "";
3203
+ for (let i = 0; i < bytes.length; i += 32768) binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
3204
+ return btoa(binary);
3205
+ }
3206
+ var mimeByExtension = {
3207
+ png: "image/png",
3208
+ jpg: "image/jpeg",
3209
+ jpeg: "image/jpeg",
3210
+ gif: "image/gif",
3211
+ webp: "image/webp",
3212
+ svg: "image/svg+xml",
3213
+ woff: "font/woff",
3214
+ woff2: "font/woff2",
3215
+ ttf: "font/ttf",
3216
+ otf: "font/otf",
3217
+ css: "text/css",
3218
+ js: "text/javascript",
3219
+ html: "text/html",
3220
+ json: "application/json",
3221
+ txt: "text/plain"
3222
+ };
3223
+ function mimeOf(name) {
3224
+ return mimeByExtension[name.split(".").pop()?.toLowerCase() ?? ""] ?? "application/octet-stream";
3225
+ }
3226
+
3227
+ // packages/engine/src/lib/import/apitemplate.ts
3228
+ var HIDDEN_WRAPPER = "apitemplate-is-content-hidden";
2850
3229
  var pythonisms = [
2851
3230
  {
2852
3231
  pattern: /\{\{[^}]*%\s*\(/,
@@ -2879,12 +3258,6 @@ var pythonisms = [
2879
3258
  docs: "/templates/languages#partials"
2880
3259
  }
2881
3260
  ];
2882
- function positionOf2(source, index) {
2883
- const before = source.slice(0, index);
2884
- const line = before.split("\n").length;
2885
- const column = index - before.lastIndexOf("\n");
2886
- return { line, column };
2887
- }
2888
3261
  function setInLoop(source) {
2889
3262
  const notes = [];
2890
3263
  const forRe = /\{%-?\s*for\b[\s\S]*?\{%-?\s*endfor\s*-?%\}/g;
@@ -2902,12 +3275,7 @@ function setInLoop(source) {
2902
3275
  }
2903
3276
  return notes;
2904
3277
  }
2905
- function fromDiagnostic(d) {
2906
- const docs = d.code === "unknown-filter" ? "/templates/helpers" : d.code.startsWith("missing-") ? "/migrate/apitemplate-io#sample-data" : "/templates/languages#jinja2";
2907
- return { code: d.code, message: d.message, line: d.range.start.line, column: d.range.start.column, docs };
2908
- }
2909
3278
  function importApitemplate(input) {
2910
- const errors = [];
2911
3279
  const warnings = [];
2912
3280
  const changes = [];
2913
3281
  const name = (input.name ?? "").trim() || "Imported template";
@@ -2927,29 +3295,28 @@ function importApitemplate(input) {
2927
3295
  const sample = parseSample(input.sample_data);
2928
3296
  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" });
2929
3297
  const html = input.html ?? "";
2930
- const css2 = input.css ?? "";
3298
+ let css2 = input.css ?? "";
3299
+ if (html.includes(HIDDEN_WRAPPER)) {
3300
+ css2 = `.${HIDDEN_WRAPPER} { display: none !important; }
3301
+ ${css2}`;
3302
+ changes.push(`elements of the editor class "${HIDDEN_WRAPPER}" are hidden by a rule added to the CSS`);
3303
+ }
2931
3304
  for (const rule of pythonisms) {
2932
3305
  const m = rule.pattern.exec(html);
2933
3306
  if (m) warnings.push({ code: rule.code, message: rule.message, docs: rule.docs, ...positionOf2(html, m.index) });
2934
3307
  }
2935
3308
  warnings.push(...setInLoop(html));
2936
- const engine = getEngine("jinja2");
2937
- try {
2938
- engine.compile(html, { name });
2939
- } catch (e) {
2940
- const err = e;
2941
- errors.push({ code: "syntax-error", message: err.message, line: err.line, column: err.column, docs: "/templates/languages#jinja2" });
2942
- }
2943
- const analysis = engine.analyze(html, { sampleData: sample.data });
2944
- for (const d of analysis.diagnostics) {
2945
- const note = fromDiagnostic(d);
2946
- if (d.severity === "error") {
2947
- if (!errors.some((x) => x.code === note.code && x.line === note.line)) errors.push(note);
2948
- } else warnings.push(note);
2949
- }
2950
- const aliasHits = analysis.filters.filter((f) => ["currency_format", "table", "page_break", "raw", "translate"].includes(f.name));
2951
- if (aliasHits.length) changes.push(`apitemplate.io filters kept through aliases: ${[...new Set(aliasHits.map((f) => f.name))].join(", ")}`);
3309
+ const checked = checkTemplate("jinja2", html, sample.data, name, {
3310
+ syntax: "/templates/languages#jinja2",
3311
+ filters: "/templates/helpers",
3312
+ sampleData: "/migrate/apitemplate-io#sample-data"
3313
+ });
3314
+ warnings.push(...checked.warnings);
3315
+ const aliasHits = /\|\s*(currency_format|table|page_break|raw|translate)\b/g;
3316
+ const aliases = [...new Set([...html.matchAll(aliasHits)].map((m) => m[1]))];
3317
+ if (aliases.length) changes.push(`apitemplate.io filters kept through aliases: ${aliases.join(", ")}`);
2952
3318
  return {
3319
+ source: "apitemplate",
2953
3320
  name,
2954
3321
  slug: slugFromName(name),
2955
3322
  kind,
@@ -2959,12 +3326,767 @@ function importApitemplate(input) {
2959
3326
  head: "",
2960
3327
  settings,
2961
3328
  sampleData: sample.data,
2962
- errors,
3329
+ errors: checked.errors,
2963
3330
  warnings,
2964
3331
  changes
2965
3332
  };
2966
3333
  }
2967
3334
 
3335
+ // packages/engine/src/lib/import/scss.ts
3336
+ var DOCS = "/migrate/pdfmonkey#styles";
3337
+ var needsCompiler = [
3338
+ { pattern: /@(mixin|include)\b/, what: "@mixin/@include" },
3339
+ { pattern: /@extend\b/, what: "@extend" },
3340
+ { pattern: /@(if|else|each|for|while|function|return)\b/, what: "SCSS control flow and functions" },
3341
+ { pattern: /@(use|forward)\b/, what: "@use/@forward" },
3342
+ { pattern: /(^|[\s,{])%[a-zA-Z_][\w-]*\s*\{/m, what: "%placeholder selectors" },
3343
+ { 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" },
3344
+ { pattern: /#\{/, what: "interpolation #{\u2026}" }
3345
+ ];
3346
+ function isScss(source) {
3347
+ return /(^|[^\w-])\$[a-zA-Z_][\w-]*\s*:/m.test(source) || /^\s*\/\//m.test(source) || needsCompiler.some((r) => r.pattern.test(source));
3348
+ }
3349
+ function topLevelVariables(source) {
3350
+ const vars = /* @__PURE__ */ new Map();
3351
+ let depth = 0;
3352
+ let statement = "";
3353
+ for (const ch of source) {
3354
+ if (ch === "{") {
3355
+ depth++;
3356
+ statement = "";
3357
+ } else if (ch === "}") {
3358
+ depth = Math.max(0, depth - 1);
3359
+ statement = "";
3360
+ } else if (ch === ";" && depth === 0) {
3361
+ const m = /^\s*\$([a-zA-Z_][\w-]*)\s*:\s*([\s\S]+?)\s*(!default)?\s*$/.exec(statement);
3362
+ if (m && m[1] && m[2]) vars.set(m[1], m[2]);
3363
+ statement = "";
3364
+ } else statement += ch;
3365
+ }
3366
+ return vars;
3367
+ }
3368
+ function convertScss(source, compile) {
3369
+ const warnings = [];
3370
+ const changes = [];
3371
+ if (!source.trim() || !isScss(source)) return { css: source, warnings, changes };
3372
+ if (compile) {
3373
+ try {
3374
+ const css3 = compile(source);
3375
+ changes.push("SCSS compiled to CSS");
3376
+ return { css: css3, warnings, changes };
3377
+ } catch (e) {
3378
+ warnings.push({
3379
+ code: "scss-compile",
3380
+ message: `The SCSS did not compile (${e instanceof Error ? e.message.split("\n")[0] : String(e)}); it was converted without a compiler instead.`,
3381
+ docs: DOCS
3382
+ });
3383
+ }
3384
+ }
3385
+ let css2 = source.replace(/^[ \t]*\/\/.*(\r?\n)?/gm, "");
3386
+ if (css2 !== source) changes.push("SCSS line comments (//) removed");
3387
+ const vars = topLevelVariables(css2);
3388
+ if (vars.size) {
3389
+ css2 = css2.replace(/(?<=^|[;{}\s])\$([a-zA-Z_][\w-]*)\s*:[^;{}]*;[ \t]*\r?\n?/gm, (m, name) => vars.has(name) ? "" : m);
3390
+ const names = [...vars.keys()].sort((a, b) => b.length - a.length);
3391
+ for (let pass = 0; pass < 5; pass++) {
3392
+ const before = css2;
3393
+ for (const name of names) css2 = css2.replace(new RegExp(`\\$${name}(?![\\w-])`, "g"), vars.get(name) ?? "");
3394
+ if (css2 === before) break;
3395
+ }
3396
+ changes.push(`SCSS variables substituted: ${names.map((n) => `$${n}`).join(", ")}`);
3397
+ }
3398
+ for (const rule of needsCompiler)
3399
+ warnings.push(
3400
+ ...notesFor(css2, rule.pattern, {
3401
+ code: "scss-needs-compiler",
3402
+ 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.`,
3403
+ docs: DOCS
3404
+ }, { once: true })
3405
+ );
3406
+ warnings.push(
3407
+ ...notesFor(css2, /\$[a-zA-Z_][\w-]*/, {
3408
+ code: "scss-variable",
3409
+ 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.",
3410
+ docs: DOCS
3411
+ }, { once: true })
3412
+ );
3413
+ return { css: css2, warnings, changes };
3414
+ }
3415
+
3416
+ // packages/engine/src/lib/import/pdfmonkey.ts
3417
+ var DOCS2 = "/migrate/pdfmonkey";
3418
+ var strftime = {
3419
+ Y: "yyyy",
3420
+ y: "yy",
3421
+ m: "MM",
3422
+ "-m": "M",
3423
+ d: "dd",
3424
+ "-d": "d",
3425
+ e: "d",
3426
+ H: "HH",
3427
+ "-H": "H",
3428
+ k: "H",
3429
+ I: "hh",
3430
+ "-I": "h",
3431
+ l: "h",
3432
+ M: "mm",
3433
+ "-M": "m",
3434
+ S: "ss",
3435
+ "-S": "s",
3436
+ L: "SSS",
3437
+ p: "a",
3438
+ P: "aaa",
3439
+ B: "MMMM",
3440
+ b: "MMM",
3441
+ h: "MMM",
3442
+ A: "EEEE",
3443
+ a: "EEE",
3444
+ u: "i",
3445
+ z: "xx",
3446
+ F: "yyyy-MM-dd",
3447
+ D: "MM/dd/yy",
3448
+ T: "HH:mm:ss",
3449
+ R: "HH:mm",
3450
+ r: "hh:mm:ss a"
3451
+ };
3452
+ function strftimeToDateFns(format) {
3453
+ let out = "";
3454
+ let literal = "";
3455
+ const unmapped = [];
3456
+ const flushLiteral = () => {
3457
+ if (!literal) return;
3458
+ out += /[A-Za-z']/.test(literal) ? `'${literal.replace(/'/g, "''")}'` : literal;
3459
+ literal = "";
3460
+ };
3461
+ for (let i = 0; i < format.length; i++) {
3462
+ const ch = format[i] ?? "";
3463
+ if (ch !== "%") {
3464
+ literal += ch;
3465
+ continue;
3466
+ }
3467
+ const flag = format[i + 1] === "-" ? "-" : "";
3468
+ const directive = format[i + 1 + flag.length] ?? "";
3469
+ i += flag.length + 1;
3470
+ if (directive === "%") {
3471
+ literal += "%";
3472
+ continue;
3473
+ }
3474
+ const token = strftime[`${flag}${directive}`] ?? strftime[directive];
3475
+ if (token) {
3476
+ flushLiteral();
3477
+ out += token;
3478
+ } else {
3479
+ unmapped.push(`%${flag}${directive}`);
3480
+ literal += `%${flag}${directive}`;
3481
+ }
3482
+ }
3483
+ flushLiteral();
3484
+ return { format: out, unmapped };
3485
+ }
3486
+ var quote = (value) => value.includes('"') && !value.includes("'") ? `'${value}'` : `"${value}"`;
3487
+ function localeForSeparators(delimiter, separator) {
3488
+ const key = `${delimiter.replace(/\u00a0/g, " ")}|${separator}`;
3489
+ return {
3490
+ ",|.": "en-US",
3491
+ ".|,": "de-DE",
3492
+ " |,": "fr-FR",
3493
+ "'|.": "de-CH"
3494
+ }[key];
3495
+ }
3496
+ function keywordArgs2(args) {
3497
+ const out = {};
3498
+ for (const m of args.matchAll(/([a-z_]\w*)\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^,\s|]+)/g)) {
3499
+ const raw = m[2] ?? "";
3500
+ out[m[1] ?? ""] = /^["']/.test(raw) ? raw.slice(1, -1) : raw;
3501
+ }
3502
+ return out;
3503
+ }
3504
+ var unsupportedFilters = {
3505
+ to_sentence: 'use `join: ", "`, or `array_to_sentence_string` for "a, b, and c"',
3506
+ ensure_protocol: 'store the URL with its protocol, or prepend it with `prepend: "https:"`',
3507
+ format: "use `number: 2` for decimals or `round: 2`; printf padding has no equivalent",
3508
+ parse_json: "send the value as JSON in the payload instead of a string",
3509
+ in_time_zone: 'pass the zone to `date` as its second argument: `date: "dd.MM.yyyy", "Europe/Berlin"`'
3510
+ };
3511
+ function mapLiquid(source, fn) {
3512
+ return source.replace(/(\{\{-?|\{%-?)([\s\S]*?)(-?\}\}|-?%\})/g, (_m, open, inner, close) => `${open}${fn(inner)}${close}`);
3513
+ }
3514
+ function convertDialect(source, changes) {
3515
+ const errors = [];
3516
+ const warnings = [];
3517
+ let out = mapLiquid(source, (inner) => {
3518
+ let expr = inner;
3519
+ expr = expr.replace(/\|\s*([a-z_]\w*)\s*,\s*(?=[a-z_]\w*\s*:)/g, (_m, name) => {
3520
+ changes.add("filter arguments written with a colon (`| filter: key: value`) instead of a comma");
3521
+ return `| ${name}: `;
3522
+ });
3523
+ expr = expr.replace(
3524
+ /\|\s*in_time_zone\s*:\s*(["'])([^"']+)\1\s*\|\s*date\s*:\s*(["'])((?:(?!\3).)*)\3/g,
3525
+ (_m, _q, zone, _q2, fmt) => {
3526
+ changes.add("`in_time_zone` merged into `date` as its timezone argument");
3527
+ return `| date: ${quote(fmt)}, ${quote(zone)}`;
3528
+ }
3529
+ );
3530
+ expr = expr.replace(/\|\s*date\s*:\s*(["'])((?:(?!\1).)*)\1/g, (m, _q, fmt) => {
3531
+ if (!fmt.includes("%")) return m;
3532
+ const converted = strftimeToDateFns(fmt);
3533
+ changes.add("date formats converted from strftime (%d.%m.%Y) to date-fns tokens (dd.MM.yyyy)");
3534
+ if (converted.unmapped.length)
3535
+ warnings.push({
3536
+ code: "date-directive",
3537
+ message: `The date format "${fmt}" uses ${converted.unmapped.join(", ")}, which has no date-fns equivalent; it is kept literally.`,
3538
+ docs: `${DOCS2}#dates`
3539
+ });
3540
+ return `| date: ${quote(converted.format)}`;
3541
+ });
3542
+ expr = expr.replace(/(["'])today\1(\s*\|\s*date\b)/g, '"now"$2');
3543
+ expr = expr.replace(/\|\s*slice_by\b/g, () => {
3544
+ changes.add("`slice_by` renamed to `chunk`");
3545
+ return "| chunk";
3546
+ });
3547
+ expr = expr.replace(/\s*\|\s*entities\b/g, () => {
3548
+ changes.add("`entities` removed: header and footer render UTF-8 directly");
3549
+ return "";
3550
+ });
3551
+ expr = expr.replace(/\|\s*with_delimiter\b(\s*:\s*[^|]*)?/g, (m, args) => {
3552
+ const opts = keywordArgs2(args ?? "");
3553
+ const locale = localeForSeparators(opts["delimiter"] ?? ",", opts["separator"] ?? ".");
3554
+ if (!locale) {
3555
+ warnings.push({
3556
+ code: "number-separators",
3557
+ message: `with_delimiter with delimiter "${opts["delimiter"] ?? ","}" and separator "${opts["separator"] ?? "."}" has no matching locale; it was kept and will fail.`,
3558
+ docs: `${DOCS2}#numbers`
3559
+ });
3560
+ return m;
3561
+ }
3562
+ if (opts["strip_insignificant_zeros"] === "true")
3563
+ warnings.push({
3564
+ code: "number-strip-zeros",
3565
+ message: "`strip_insignificant_zeros` has no equivalent; `number` shows up to three decimals without fixed precision.",
3566
+ docs: `${DOCS2}#numbers`
3567
+ });
3568
+ changes.add("`with_delimiter` converted to `number` with a locale that has the same separators");
3569
+ const decimals = opts["strip_insignificant_zeros"] === "true" ? "nil" : opts["precision"] ?? "nil";
3570
+ return `| number: ${decimals}, ${quote(locale)} `;
3571
+ });
3572
+ return expr;
3573
+ });
3574
+ for (const [filter, instead] of Object.entries(unsupportedFilters))
3575
+ errors.push(
3576
+ ...notesFor(out, new RegExp(`\\|\\s*${filter}\\b`), {
3577
+ code: "pdfmonkey-filter",
3578
+ message: `PDFMonkey's \`${filter}\` filter has no Formfeed equivalent: ${instead}.`,
3579
+ docs: `${DOCS2}#filters`
3580
+ })
3581
+ );
3582
+ warnings.push(
3583
+ ...notesFor(out, /\|\s*divided_by\s*:\s*-?\d+\s*(?=[|}%]|-?%\}|-?\}\})/, {
3584
+ code: "integer-division",
3585
+ 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.",
3586
+ docs: `${DOCS2}#numbers`
3587
+ })
3588
+ );
3589
+ out = out.replace(/\[%\s*data_form_field[^\]]*\]/g, (m) => {
3590
+ 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)) });
3591
+ return m;
3592
+ });
3593
+ return { source: out, errors, warnings, changes };
3594
+ }
3595
+ function splitParams(params) {
3596
+ const out = [];
3597
+ for (const m of params.matchAll(/([A-Za-z_]\w*)\s*:\s*("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^,]+)/g))
3598
+ out.push([m[1] ?? "", (m[2] ?? "").trim()]);
3599
+ return out;
3600
+ }
3601
+ function inlineIncludes(source, blocks, errors, changes) {
3602
+ let out = source;
3603
+ for (let depth = 0; depth < 6; depth++) {
3604
+ let inlined = false;
3605
+ out = out.replace(/\{%-?\s*include\s+(["'])([^"']+)\1\s*(?:,\s*([\s\S]*?))?\s*-?%\}/g, (m, _q, name, params) => {
3606
+ const body = blocks.get(name);
3607
+ if (body === void 0) return m;
3608
+ inlined = true;
3609
+ changes.add("snippets and partials inlined into the template");
3610
+ const assigns = splitParams(params ?? "").map(([k, v]) => `{% assign ${k} = ${v} %}`).join("");
3611
+ return `${assigns}${body}`;
3612
+ });
3613
+ if (!inlined) break;
3614
+ }
3615
+ for (const m of out.matchAll(/\{%-?\s*include\s+(["'])([^"']+)\1/g))
3616
+ errors.push({
3617
+ code: "snippet-missing",
3618
+ 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.`,
3619
+ docs: `${DOCS2}#snippets`,
3620
+ ...positionOf2(out, m.index ?? 0)
3621
+ });
3622
+ return out;
3623
+ }
3624
+ var cssPaperFormats = { a3: "A3", a4: "A4", a5: "A5", letter: "Letter" };
3625
+ var isoSizes = { a0: [841, 1189], a1: [594, 841], a2: [420, 594], a6: [105, 148] };
3626
+ var escapeHtml2 = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
3627
+ function simpleHeaderFooter(part, margin) {
3628
+ const cells = ["left", "center", "right"].map((side) => part[side] ?? "");
3629
+ if (!cells.some((c) => c.trim())) return void 0;
3630
+ 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>`;
3631
+ 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>`;
3632
+ }
3633
+ function mapSettings(input, kind, warnings, changes) {
3634
+ const settings = {};
3635
+ const format = String(input.paper_format ?? "a4").toLowerCase();
3636
+ if (format === "custom") {
3637
+ const width = lengthMm(input.paper_width);
3638
+ const height = lengthMm(input.paper_height);
3639
+ if (width && height) settings.paper = { width, height };
3640
+ else warnings.push({ code: "setting-ignored", message: "Custom paper size without width and height; A4 is used.", docs: `${DOCS2}#settings` });
3641
+ } else if (cssPaperFormats[format]) settings.paper = { format: cssPaperFormats[format] };
3642
+ else if (isoSizes[format]) {
3643
+ const [w, h] = isoSizes[format] ?? [210, 297];
3644
+ settings.paper = { width: `${w}mm`, height: `${h}mm` };
3645
+ changes.add(`${format.toUpperCase()} kept as its size in millimetres`);
3646
+ } else warnings.push({ code: "setting-ignored", message: `Paper format "${input.paper_format}" is unknown; A4 is used.`, docs: `${DOCS2}#settings` });
3647
+ if (String(input.orientation ?? "").toLowerCase() === "landscape") settings.paper = { ...settings.paper, landscape: true };
3648
+ const m = input.margin ?? {};
3649
+ const margin = {
3650
+ top: lengthMm(m.top) ?? "10mm",
3651
+ right: lengthMm(m.right) ?? "10mm",
3652
+ bottom: lengthMm(m.bottom) ?? "10mm",
3653
+ left: lengthMm(m.left) ?? "10mm"
3654
+ };
3655
+ settings.margin = margin;
3656
+ changes.add("paper, orientation and margins mapped (empty PDFMonkey margins are 10 mm)");
3657
+ let header;
3658
+ let footer;
3659
+ for (const [key, part] of [
3660
+ ["header", input.header],
3661
+ ["footer", input.footer]
3662
+ ]) {
3663
+ if (!part) continue;
3664
+ const html = part.content?.trim() ? part.content : simpleHeaderFooter(part, margin);
3665
+ if (!html) continue;
3666
+ if (key === "header") header = html;
3667
+ else footer = html;
3668
+ changes.add(
3669
+ 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"
3670
+ );
3671
+ }
3672
+ if (kind === "image") {
3673
+ settings.image = { width: 1200, height: 630, deviceScaleFactor: 2, format: "png", ...input.transparent_background ? { transparent: true } : {} };
3674
+ changes.add("image size defaults to 1200 \xD7 630 (PDFMonkey sets it per request; change it in Settings)");
3675
+ }
3676
+ if (input.inject_javascript)
3677
+ warnings.push({
3678
+ code: "inject-javascript",
3679
+ 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.",
3680
+ docs: `${DOCS2}#javascript`
3681
+ });
3682
+ if (input.use_paged)
3683
+ 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` });
3684
+ if (input.use_forms) warnings.push({ code: "pdf-forms", message: "PDF form fields are not supported.", docs: `${DOCS2}#forms` });
3685
+ if (input.use_emojis) changes.add("`use_emojis` dropped: emoji render with the system emoji font");
3686
+ return { settings, header, footer };
3687
+ }
3688
+ function convertMarkup(html, settings, warnings, changes) {
3689
+ let out = html;
3690
+ const tailwind = /<script\b[^>]*\bsrc=["'][^"']*tailwind[^"']*["'][^>]*>\s*<\/script>\s*|<link\b[^>]*\bhref=["'][^"']*tailwind[^"']*\.css["'][^>]*>\s*/gi;
3691
+ if (tailwind.test(out)) {
3692
+ out = out.replace(tailwind, "");
3693
+ settings.tailwind = true;
3694
+ changes.add("Tailwind script removed and Tailwind mode switched on (Formfeed compiles the classes)");
3695
+ }
3696
+ warnings.push(
3697
+ ...notesFor(out, /pagedjs|paged\.polyfill/i, {
3698
+ code: "paged-js",
3699
+ message: "The template loads Paged.js; Formfeed paginates with Chromium, so check running headers and margin boxes in the true render.",
3700
+ docs: `${DOCS2}#paged-js`
3701
+ }, { once: true }),
3702
+ ...notesFor(out, /qr-code-styling|@bitjson\/qr-code|qrcode(\.min)?\.js|qrcodejs/i, {
3703
+ code: "qr-library",
3704
+ message: 'A JavaScript QR library is loaded; the `qrcode` helper renders QR codes without scripts: `<img src="{{ url | qrcode }}">`.',
3705
+ docs: "/templates/helpers#qrcode"
3706
+ }, { once: true }),
3707
+ ...notesFor(out, /\$docPayload/, {
3708
+ code: "inject-javascript",
3709
+ message: "`$docPayload` is not defined in Formfeed; render the values into the markup or write them into a script tag with the json filter.",
3710
+ docs: `${DOCS2}#javascript`
3711
+ }, { once: true })
3712
+ );
3713
+ return out;
3714
+ }
3715
+ function importPdfmonkey(input, options = {}) {
3716
+ const tpl = "document_template" in input ? input.document_template : input;
3717
+ const errors = [];
3718
+ const warnings = [];
3719
+ const changes = /* @__PURE__ */ new Set();
3720
+ const pick2 = (published, draft) => {
3721
+ const empty = (v) => v === void 0 || v === null || v === "";
3722
+ if (options.draft) return empty(draft) ? published : draft;
3723
+ if (empty(published) && !empty(draft)) {
3724
+ changes.add("the template was never published; the draft was imported");
3725
+ return draft;
3726
+ }
3727
+ return published;
3728
+ };
3729
+ const name = (tpl.identifier ?? "").trim() || "Imported template";
3730
+ const kind = String(tpl.output_type ?? "").toLowerCase() === "image" ? "image" : "pdf";
3731
+ if (tpl.edition_mode === "builder")
3732
+ errors.push({
3733
+ code: "builder-template",
3734
+ 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.",
3735
+ docs: DOCS2
3736
+ });
3737
+ const mapped = mapSettings(pick2(tpl.settings, tpl.settings_draft) ?? {}, kind, warnings, changes);
3738
+ const settings = mapped.settings;
3739
+ let body = String(pick2(tpl.body, tpl.body_draft) ?? "");
3740
+ const blocks = new Map(Object.entries(options.snippets ?? {}));
3741
+ body = body.replace(/\{%-?\s*partial\s+(["'])([^"']+)\1\s*-?%\}([\s\S]*?)\{%-?\s*endpartial\s*-?%\}/g, (_m, _q, partialName, content) => {
3742
+ blocks.set(partialName, content);
3743
+ return "";
3744
+ });
3745
+ body = body.replace(/\{%-?\s*load_snippets\b[\s\S]*?-?%\}/g, () => {
3746
+ changes.add("`load_snippets` removed");
3747
+ return "";
3748
+ });
3749
+ body = inlineIncludes(body, blocks, errors, changes);
3750
+ const converted = convertDialect(body, changes);
3751
+ errors.push(...converted.errors);
3752
+ warnings.push(...converted.warnings);
3753
+ const html = convertMarkup(converted.source, settings, warnings, changes);
3754
+ for (const key of ["header", "footer"]) {
3755
+ const part = mapped[key];
3756
+ if (!part) continue;
3757
+ const result = convertDialect(inlineIncludes(part, blocks, errors, changes), changes);
3758
+ settings[key] = { html: result.source };
3759
+ errors.push(...result.errors.map((n) => ({ ...n, message: `${key}: ${n.message}` })));
3760
+ warnings.push(...result.warnings.map((n) => ({ ...n, message: `${key}: ${n.message}` })));
3761
+ }
3762
+ const scss = convertScss(String(pick2(tpl.scss_style, tpl.scss_style_draft) ?? ""), options.compileScss);
3763
+ warnings.push(...scss.warnings);
3764
+ for (const change of scss.changes) changes.add(change);
3765
+ const sample = parseSample(pick2(tpl.sample_data, tpl.sample_data_draft));
3766
+ if (sample.error)
3767
+ warnings.push({ code: "sample-json", message: `Sample data is not valid JSON (${sample.error}); the draft starts without sample data.`, docs: `${DOCS2}#sample-data` });
3768
+ const checked = checkTemplate("liquid", html, sample.data, name, {
3769
+ syntax: "/templates/languages#liquid",
3770
+ filters: `${DOCS2}#filters`,
3771
+ sampleData: `${DOCS2}#sample-data`
3772
+ });
3773
+ const explained = new Set(errors.filter((e) => e.code === "pdfmonkey-filter").map((e) => e.line));
3774
+ const generic = (e) => e.code === "unknown-filter" || e.code === "syntax-error" && /undefined filter/i.test(e.message);
3775
+ errors.push(...checked.errors.filter((e) => !(generic(e) && explained.has(e.line))));
3776
+ warnings.push(...checked.warnings);
3777
+ return {
3778
+ source: "pdfmonkey",
3779
+ name,
3780
+ slug: slugFromName(name),
3781
+ kind,
3782
+ engine: "liquid",
3783
+ html,
3784
+ css: scss.css,
3785
+ head: "",
3786
+ settings,
3787
+ sampleData: sample.data,
3788
+ errors,
3789
+ warnings,
3790
+ changes: [...changes]
3791
+ };
3792
+ }
3793
+
3794
+ // packages/engine/src/lib/import/jsreport.ts
3795
+ var DOCS3 = "/migrate/jsreport";
3796
+ var COLLECTIONS = ["templates", "data", "assets", "components", "scripts", "folders"];
3797
+ function readJsreportExport(files) {
3798
+ const bundle = { templates: [], data: [], assets: [], components: [], scripts: [], folders: [] };
3799
+ for (const [path, text] of Object.entries(files)) {
3800
+ const normalized = path.replace(/\\/g, "/").replace(/^\.?\//, "");
3801
+ if (normalized === "metadata.json") {
3802
+ try {
3803
+ bundle.version = String(JSON.parse(text).reporterVersion ?? "");
3804
+ } catch {
3805
+ }
3806
+ continue;
3807
+ }
3808
+ const collection = normalized.split("/")[0];
3809
+ if (!COLLECTIONS.includes(collection) || !normalized.endsWith(".json")) continue;
3810
+ try {
3811
+ bundle[collection].push(JSON.parse(text));
3812
+ } catch {
3813
+ }
3814
+ }
3815
+ if (!bundle.templates.length) throw new Error("No templates found: this does not look like a jsreport export (.jsrexport).");
3816
+ return bundle;
3817
+ }
3818
+ function folderPath(bundle, entity) {
3819
+ const parts = [];
3820
+ let shortid = entity.folder?.shortid;
3821
+ for (let guard = 0; shortid && guard < 20; guard++) {
3822
+ const folder = bundle.folders.find((f) => f.shortid === shortid);
3823
+ if (!folder) break;
3824
+ parts.unshift(folder.name ?? "");
3825
+ shortid = folder.folder?.shortid;
3826
+ }
3827
+ return parts.filter(Boolean).join("/");
3828
+ }
3829
+ function pathOf2(bundle, entity) {
3830
+ const folder = folderPath(bundle, entity);
3831
+ return folder ? `${folder}/${entity.name ?? ""}` : entity.name ?? "";
3832
+ }
3833
+ function jsreportTemplates(bundle) {
3834
+ 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));
3835
+ }
3836
+ function byPath(bundle, list2, ref) {
3837
+ const wanted = ref.replace(/^\.?\/+/, "");
3838
+ return list2.find((e) => pathOf2(bundle, e) === wanted) ?? list2.find((e) => e.name === wanted.split("/").pop());
3839
+ }
3840
+ function decodeText(base64) {
3841
+ const binary = atob(base64);
3842
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
3843
+ return new TextDecoder().decode(bytes);
3844
+ }
3845
+ function assetBase64(content) {
3846
+ if (typeof content === "string") {
3847
+ const compact = content.replace(/\s/g, "");
3848
+ if (compact.length % 4 === 0 && /^[A-Za-z0-9+/]*={0,2}$/.test(compact)) return compact;
3849
+ return toBase64(new TextEncoder().encode(content));
3850
+ }
3851
+ return toBase64(content);
3852
+ }
3853
+ var escapeMustaches = (text) => text.replace(/\{\{/g, "\\{{");
3854
+ function resolveAssets(source, bundle, options, warnings, changes) {
3855
+ const v3 = /\{\{\{?\s*asset\s+(["'])([^"']+)\1(?:\s+(["'])([^"']+)\3)?\s*\}?\}\}/g;
3856
+ const v2 = /\{#asset\s+([^\s}]+)(?:\s+@encoding=(\w+))?\s*\}/g;
3857
+ const replace = (whole, ref, encoding = "utf8") => {
3858
+ const asset = byPath(bundle, bundle.assets, ref);
3859
+ if (!asset) {
3860
+ 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` });
3861
+ return `{{asset "${ref}"}}`;
3862
+ }
3863
+ if (encoding === "link" || !asset.content && asset.link) {
3864
+ if (asset.link && /^https?:\/\//.test(asset.link)) return asset.link;
3865
+ 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` });
3866
+ return `{{asset "${asset.name}"}}`;
3867
+ }
3868
+ const base64 = assetBase64(asset.content);
3869
+ if (base64 === void 0) return whole;
3870
+ const bytes = Math.floor(base64.length * 3 / 4);
3871
+ if (encoding === "utf8" || encoding === "string") {
3872
+ changes.add("text assets (CSS, scripts, snippets) inlined");
3873
+ return escapeMustaches(decodeText(base64));
3874
+ }
3875
+ if (bytes > options.inlineAssetBytes) {
3876
+ warnings.push({
3877
+ code: "asset-large",
3878
+ 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.`,
3879
+ docs: `${DOCS3}#assets`
3880
+ });
3881
+ return `{{asset "${asset.name}"}}`;
3882
+ }
3883
+ changes.add("binary assets (images, fonts) inlined as data URIs");
3884
+ return encoding === "base64" ? base64 : `data:${mimeOf(asset.name ?? "")};base64,${base64}`;
3885
+ };
3886
+ return source.replace(v3, (whole, _q, ref, _q2, encoding) => replace(whole, ref, encoding)).replace(v2, (whole, ref, encoding) => replace(whole, ref, encoding));
3887
+ }
3888
+ function inlineChildren(source, bundle, errors, warnings, changes) {
3889
+ let out = source;
3890
+ for (let depth = 0; depth < 6; depth++) {
3891
+ const before = out;
3892
+ out = out.replace(/\{\{\{?\s*childTemplate\s+(["'])([^"']+)\1[^}]*\}?\}\}|\{#child\s+([^\s}]+)[^}]*\}/g, (whole, _q, v3, v2) => {
3893
+ const ref = v3 ?? v2 ?? "";
3894
+ const child = byPath(bundle, bundle.templates, ref);
3895
+ if (!child?.content) {
3896
+ errors.push({ code: "child-missing", message: `The child template "${ref}" is not in the export.`, docs: `${DOCS3}#child-templates` });
3897
+ return whole;
3898
+ }
3899
+ changes.add("child templates inlined");
3900
+ return child.content;
3901
+ });
3902
+ out = out.replace(/\{\{\{?\s*component\s+(["'])([^"']+)\1([^}]*)\}?\}\}/g, (whole, _q, ref, props) => {
3903
+ const component = byPath(bundle, bundle.components, ref);
3904
+ if (!component?.content) {
3905
+ errors.push({ code: "component-missing", message: `The component "${ref}" is not in the export.`, docs: `${DOCS3}#components` });
3906
+ return whole;
3907
+ }
3908
+ if (props.trim())
3909
+ warnings.push({
3910
+ code: "component-props",
3911
+ message: `The component "${ref}" received properties (${props.trim()}); it was inlined with the surrounding data, so reference those values directly.`,
3912
+ docs: `${DOCS3}#components`
3913
+ });
3914
+ if (component.helpers?.trim())
3915
+ warnings.push({ code: "custom-helpers", message: `The component "${ref}" has JavaScript helpers, which Formfeed does not run.`, docs: `${DOCS3}#helpers` });
3916
+ changes.add("components inlined");
3917
+ return component.content;
3918
+ });
3919
+ if (out === before) break;
3920
+ }
3921
+ return out;
3922
+ }
3923
+ function helperNames(script) {
3924
+ const names = /* @__PURE__ */ new Set();
3925
+ for (const m of script.matchAll(/(?:^|[\s;])(?:async\s+)?function\s+([A-Za-z_$][\w$]*)\s*\(/g)) names.add(m[1] ?? "");
3926
+ 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] ?? "");
3927
+ names.delete("");
3928
+ return [...names];
3929
+ }
3930
+ function mapChrome(template, kind, warnings, changes) {
3931
+ const settings = {};
3932
+ const chrome = template.chrome ?? {};
3933
+ const str2 = (key) => typeof chrome[key] === "string" && chrome[key].trim() ? chrome[key].trim() : void 0;
3934
+ const paper = {};
3935
+ const format = str2("format");
3936
+ if (format) paper.format = format.charAt(0).toUpperCase() + format.slice(1).toLowerCase();
3937
+ const width = lengthMm(str2("width"));
3938
+ const height = lengthMm(str2("height"));
3939
+ if (width && height) {
3940
+ paper.width = width;
3941
+ paper.height = height;
3942
+ }
3943
+ if (chrome["landscape"] === true || chrome["landscape"] === "true") paper.landscape = true;
3944
+ if (Object.keys(paper).length) settings.paper = paper;
3945
+ const margin = {};
3946
+ for (const side of ["top", "right", "bottom", "left"]) {
3947
+ const value = lengthMm(str2(`margin${side[0]?.toUpperCase()}${side.slice(1)}`));
3948
+ if (value) margin[side] = value;
3949
+ }
3950
+ if (Object.keys(margin).length) settings.margin = margin;
3951
+ const showHeaderFooter = chrome["displayHeaderFooter"] === true || chrome["displayHeaderFooter"] === "true";
3952
+ if (showHeaderFooter) {
3953
+ const header = str2("headerTemplate");
3954
+ const footer = str2("footerTemplate");
3955
+ if (header) settings.header = { html: header };
3956
+ if (footer) settings.footer = { html: footer };
3957
+ if (header || footer) changes.add("Chromium header and footer templates moved to the Header and Footer tabs");
3958
+ }
3959
+ if (chrome["printBackground"] !== void 0) settings.printBackground = chrome["printBackground"] === true || chrome["printBackground"] === "true";
3960
+ if (chrome["scale"] !== void 0 && Number.isFinite(Number(chrome["scale"]))) settings.scale = Number(chrome["scale"]);
3961
+ const ranges = str2("pageRanges");
3962
+ if (ranges) settings.pageRanges = ranges;
3963
+ if (chrome["waitForNetworkIdle"] === true || chrome["waitForNetworkIdle"] === "true") settings.waitFor = { networkIdle: true };
3964
+ if (chrome["waitForJS"] === true || chrome["waitForJS"] === "true")
3965
+ warnings.push({
3966
+ code: "wait-for-js",
3967
+ message: "The template sets window.JSREPORT_READY_TO_START; Formfeed waits for a CSS selector instead (Settings \u2192 Wait for).",
3968
+ docs: `${DOCS3}#settings`
3969
+ });
3970
+ 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` });
3971
+ if (kind === "image") {
3972
+ const img = template.chromeImage ?? {};
3973
+ const type = String(img["type"] ?? "png").toLowerCase();
3974
+ settings.image = {
3975
+ width: Number(img["viewportWidth"] ?? 0) || 1200,
3976
+ height: Number(img["viewportHeight"] ?? 0) || 630,
3977
+ format: type === "jpeg" || type === "jpg" ? "jpeg" : "png",
3978
+ ...Number.isFinite(Number(img["quality"])) && img["quality"] !== void 0 ? { quality: Number(img["quality"]) } : {},
3979
+ ...img["fullPage"] === true ? { fullPage: true } : {},
3980
+ ...img["omitBackground"] === true ? { transparent: true } : {}
3981
+ };
3982
+ changes.add("chrome-image options mapped to the image settings");
3983
+ }
3984
+ const meta = template.pdfMeta ?? {};
3985
+ const metadata = Object.fromEntries(
3986
+ ["title", "author", "subject", "keywords"].filter((k) => typeof meta[k] === "string" && meta[k]).map((k) => [k, String(meta[k])])
3987
+ );
3988
+ if (Object.keys(metadata).length) {
3989
+ settings.pdf = { metadata };
3990
+ changes.add("PDF metadata (pdfMeta) mapped");
3991
+ }
3992
+ changes.add("chrome-pdf options mapped: paper, orientation, margins, background, scale, page ranges");
3993
+ return settings;
3994
+ }
3995
+ function importJsreport(bundle, ref, options = {}) {
3996
+ const opts = { inlineAssetBytes: options.inlineAssetBytes ?? 256 * 1024 };
3997
+ const template = bundle.templates.find((t) => t.shortid === ref || t._id === ref) ?? byPath(bundle, bundle.templates, ref);
3998
+ if (!template) throw new Error(`No template "${ref}" in the export.`);
3999
+ const errors = [];
4000
+ const warnings = [];
4001
+ const changes = /* @__PURE__ */ new Set();
4002
+ const name = (template.name ?? "").trim() || "Imported template";
4003
+ const engine = String(template.engine ?? "handlebars").toLowerCase();
4004
+ if (engine !== "handlebars" && engine !== "none")
4005
+ errors.push({
4006
+ code: "unsupported-engine",
4007
+ message: `The template uses the ${engine} engine; Formfeed runs Handlebars, Liquid and Jinja2. It was imported as Handlebars and needs rewriting.`,
4008
+ docs: `${DOCS3}#engines`
4009
+ });
4010
+ if (engine === "none") changes.add("template without an engine imported as Handlebars");
4011
+ const recipe = String(template.recipe ?? "chrome-pdf").toLowerCase();
4012
+ const kind = recipe === "chrome-image" ? "image" : "pdf";
4013
+ if (!["chrome-pdf", "chrome-image", "html"].includes(recipe))
4014
+ errors.push({
4015
+ code: "unsupported-recipe",
4016
+ message: `The ${recipe} recipe has no Formfeed equivalent; the template was imported as a PDF template.`,
4017
+ docs: `${DOCS3}#recipes`
4018
+ });
4019
+ if (recipe === "html") changes.add("html recipe imported as a PDF template");
4020
+ const settings = mapChrome(template, kind, warnings, changes);
4021
+ let html = inlineChildren(template.content ?? "", bundle, errors, warnings, changes);
4022
+ html = resolveAssets(html, bundle, opts, warnings, changes);
4023
+ for (const key of ["header", "footer"]) {
4024
+ const part = settings[key]?.html;
4025
+ if (part) settings[key] = { html: resolveAssets(inlineChildren(part, bundle, errors, warnings, changes), bundle, opts, warnings, changes) };
4026
+ }
4027
+ html = html.replace(/\{\{\{?\s*toJS\s+([^}]+?)\s*\}?\}\}/g, (_m, value) => {
4028
+ changes.add("`toJS` replaced by `json`");
4029
+ return `{{{json ${value}}}}`;
4030
+ });
4031
+ const helperSources = [
4032
+ template.helpers ?? "",
4033
+ ...bundle.assets.filter((a) => a.isSharedHelper).map((a) => {
4034
+ const b64 = assetBase64(a.content);
4035
+ return b64 ? decodeText(b64) : "";
4036
+ })
4037
+ ].filter((s) => s.trim());
4038
+ const helpers = helperSources.flatMap(helperNames);
4039
+ if (helpers.length)
4040
+ warnings.push({
4041
+ code: "custom-helpers",
4042
+ 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.`,
4043
+ docs: `${DOCS3}#helpers`
4044
+ });
4045
+ warnings.push(
4046
+ ...notesFor(html, /\{\{\{?\s*(pdfAddPageItem|pdfCreatePagesGroup|pdfFormField|module)\b/, {
4047
+ code: "jsreport-helper",
4048
+ message: "A jsreport system helper (pdf-utils or module) has no Formfeed equivalent.",
4049
+ docs: `${DOCS3}#helpers`
4050
+ })
4051
+ );
4052
+ if (template.scripts?.length)
4053
+ 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` });
4054
+ if (template.pdfOperations?.length)
4055
+ warnings.push({ code: "pdf-operations", message: "pdf-utils operations (merge, append, headers from another template) are not imported.", docs: `${DOCS3}#pdf-utils` });
4056
+ if (template.pdfPassword && Object.values(template.pdfPassword).some(Boolean))
4057
+ warnings.push({ code: "pdf-password", message: "PDF password protection is not imported.", docs: `${DOCS3}#settings` });
4058
+ if (template.localization?.language)
4059
+ warnings.push({ code: "localization", message: "jsreport localization files are not imported; use the template dictionaries and the `t` helper.", docs: "/templates/helpers#t" });
4060
+ const dataEntity = template.data?.shortid ? bundle.data.find((d) => d.shortid === template.data?.shortid) : void 0;
4061
+ const sample = parseSample(dataEntity?.dataJson);
4062
+ if (sample.error) warnings.push({ code: "sample-json", message: `The sample data is not valid JSON (${sample.error}).`, docs: `${DOCS3}#sample-data` });
4063
+ if (dataEntity) changes.add(`sample data "${dataEntity.name}" became the default data set`);
4064
+ if (new TextEncoder().encode(html).length > 1e6)
4065
+ 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` });
4066
+ const checked = checkTemplate("handlebars", html, sample.data, name, {
4067
+ syntax: "/templates/languages#handlebars",
4068
+ filters: `${DOCS3}#helpers`,
4069
+ sampleData: `${DOCS3}#sample-data`
4070
+ });
4071
+ errors.push(...checked.errors);
4072
+ warnings.push(...checked.warnings);
4073
+ return {
4074
+ source: "jsreport",
4075
+ name,
4076
+ slug: slugFromName(name),
4077
+ kind,
4078
+ engine: "handlebars",
4079
+ html,
4080
+ css: "",
4081
+ head: "",
4082
+ settings,
4083
+ sampleData: sample.data,
4084
+ errors,
4085
+ warnings,
4086
+ changes: [...changes]
4087
+ };
4088
+ }
4089
+
2968
4090
  // packages/engine/src/lib/assemble.ts
2969
4091
  function defaultSettings() {
2970
4092
  return {
@@ -3184,6 +4306,37 @@ document.addEventListener('click', function (event) {
3184
4306
  } catch (e) {}
3185
4307
  }, true);
3186
4308
  </script>`;
4309
+ var pageNavScript = `<script>
4310
+ (function () {
4311
+ var pages = function () { return document.querySelectorAll('.pagedjs_page'); };
4312
+ window.addEventListener('message', function (event) {
4313
+ var data = event.data;
4314
+ if (!data || data.type !== 'formfeed:goto-page') return;
4315
+ var page = pages()[(data.page || 1) - 1];
4316
+ if (page && page.scrollIntoView) page.scrollIntoView({ block: 'start' });
4317
+ });
4318
+ var current = 0;
4319
+ var report = function () {
4320
+ var list = pages();
4321
+ if (!list.length) return;
4322
+ var best = 1;
4323
+ var bestTop = Infinity;
4324
+ for (var i = 0; i < list.length; i++) {
4325
+ var top = Math.abs(list[i].getBoundingClientRect().top);
4326
+ if (top < bestTop) { bestTop = top; best = i + 1; }
4327
+ }
4328
+ if (best === current) return;
4329
+ current = best;
4330
+ try { parent.postMessage({ type: 'formfeed:page', page: best }, '*'); } catch (e) {}
4331
+ };
4332
+ var queued = false;
4333
+ window.addEventListener('scroll', function () {
4334
+ if (queued) return;
4335
+ queued = true;
4336
+ requestAnimationFrame(function () { queued = false; report(); });
4337
+ }, { passive: true });
4338
+ })();
4339
+ </script>`;
3187
4340
  function flowDocument(draft) {
3188
4341
  const { settings, kind, headerHtml, footerHtml } = draft;
3189
4342
  const margin = settings.margin ?? {};
@@ -3210,11 +4363,12 @@ html { background: #e5e7eb; }
3210
4363
  .pagedjs_pages { margin: 0 auto; }
3211
4364
  .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; }
3212
4365
  `;
4366
+ 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>`;
3213
4367
  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>`;
3214
4368
  const script = `<script src="${options.pagedScriptUrl}"></script>`;
3215
4369
  const header = headerHtml ? `<div class="ff-running-header">${pageNumberSpans(headerHtml, '<span class="ff-page-no"></span>', '<span class="ff-page-total"></span>')}</div>` : "";
3216
4370
  const footer = footerHtml ? `<div class="ff-running-footer">${pageNumberSpans(footerHtml, '<span class="ff-page-no"></span>', '<span class="ff-page-total"></span>')}</div>` : "";
3217
- return draft.document.replace("</head>", `<style data-formfeed="paged">${runningCss}</style>${config}${script}${inspectScript}</head>`).replace(/(<body[^>]*>)/, `$1${header}${footer}`);
4371
+ return draft.document.replace("</head>", `<style data-formfeed="paged">${runningCss}</style>${selectorGuard}${config}${script}${inspectScript}${pageNavScript}</head>`).replace(/(<body[^>]*>)/, `$1${header}${footer}`);
3218
4372
  }
3219
4373
  export {
3220
4374
  DefaultHelperRegistry,
@@ -3229,6 +4383,7 @@ export {
3229
4383
  chartInitScript,
3230
4384
  chartMarkup,
3231
4385
  chartPayload,
4386
+ convertScss,
3232
4387
  createHelperRegistry,
3233
4388
  defaultHelpers,
3234
4389
  defaultLimits,
@@ -3246,9 +4401,13 @@ export {
3246
4401
  helperDocs,
3247
4402
  imageMarkup,
3248
4403
  importApitemplate,
4404
+ importJsreport,
4405
+ importPdfmonkey,
3249
4406
  inferSchema,
4407
+ isScss,
3250
4408
  jinja2Engine,
3251
4409
  jinjaTags,
4410
+ jsreportTemplates,
3252
4411
  liquidBuiltinFilters,
3253
4412
  liquidEngine,
3254
4413
  liquidTags,
@@ -3261,6 +4420,7 @@ export {
3261
4420
  paperOf,
3262
4421
  printReset,
3263
4422
  qrSvg,
4423
+ readJsreportExport,
3264
4424
  renderVersion,
3265
4425
  resolvePath,
3266
4426
  scanBlocks,
@@ -3268,6 +4428,7 @@ export {
3268
4428
  slugFromName,
3269
4429
  snippets,
3270
4430
  snippetsFor,
4431
+ strftimeToDateFns,
3271
4432
  toDate,
3272
4433
  toNumber
3273
4434
  };