@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.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
|
|
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 (
|
|
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
|
-
|
|
1582
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
1768
|
-
|
|
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]?.
|
|
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
|
|
1804
|
-
for (const child of args.children ?? [])
|
|
1805
|
-
if (child.typename === "Symbol") scope.
|
|
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]?.
|
|
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]?.
|
|
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]?.
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
const
|
|
2944
|
-
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
};
|