@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 +1229 -68
- package/index.js +1229 -68
- package/package.json +1 -1
- package/types/index.d.cts +7 -1
- package/types/index.d.ts +7 -1
- package/types/lib/engines/python-format.d.cts +14 -0
- package/types/lib/engines/python-format.d.ts +14 -0
- package/types/lib/import/apitemplate.d.cts +3 -27
- package/types/lib/import/apitemplate.d.ts +3 -27
- package/types/lib/import/common.d.cts +67 -0
- package/types/lib/import/common.d.ts +67 -0
- package/types/lib/import/jsreport.d.cts +90 -0
- package/types/lib/import/jsreport.d.ts +90 -0
- package/types/lib/import/pdfmonkey.d.cts +77 -0
- package/types/lib/import/pdfmonkey.d.ts +77 -0
- package/types/lib/import/scss.d.cts +17 -0
- package/types/lib/import/scss.d.ts +17 -0
- package/types/lib/types.d.cts +6 -0
- package/types/lib/types.d.ts +6 -0
- package/types/lib/unavailable.d.cts +2 -0
- package/types/lib/unavailable.d.ts +2 -0
- package/types/types/modules.d.cts +10 -1
- package/types/types/modules.d.ts +10 -1
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
|
|
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 (
|
|
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
|
-
|
|
1670
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
1856
|
-
|
|
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]?.
|
|
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
|
|
1892
|
-
for (const child of args.children ?? [])
|
|
1893
|
-
if (child.typename === "Symbol") scope.
|
|
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]?.
|
|
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]?.
|
|
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]?.
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
const
|
|
3032
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
}
|