@formfeed/engine 0.3.0 → 0.3.1
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/README.md +2 -1
- package/index.cjs +88 -12
- package/index.js +88 -12
- package/package.json +1 -1
- package/types/lib/import/apitemplate.d.cts +3 -0
- package/types/lib/import/apitemplate.d.ts +3 -0
- package/types/lib/preview.d.cts +5 -1
- package/types/lib/preview.d.ts +5 -1
- package/types/types/modules.d.cts +1 -0
- package/types/types/modules.d.ts +1 -0
package/README.md
CHANGED
|
@@ -42,7 +42,8 @@ The supported surface is the `Engine` interface (`compile`, `analyze`, `render`)
|
|
|
42
42
|
| `analysis/` | Block pairing and sample-data checks behind every `analyze()` |
|
|
43
43
|
| `schema.ts` | `inferSchema(sample)` to JSON Schema 2020-12 |
|
|
44
44
|
| `assemble.ts` | Template settings, print reset, `assembleDocument()` and `renderVersion()` (body, header, footer, title) |
|
|
45
|
-
| `import/` | `importApitemplate()`:
|
|
45
|
+
| `import/` | `importApitemplate()` and `importApitemplateFromApi()`, `importPdfmonkey()` and `importJsreport()`: convert templates of apitemplate.io, PDFMonkey and jsreport into Formfeed drafts |
|
|
46
|
+
| `office/` | Word and PowerPoint templates: `renderOffice()` fills a `.docx` or `.pptx` file, `analyzeOffice()` lists its tags, variables and findings per part and paragraph, `OfficeTemplateError` carries the findings of a document that cannot be filled, and `starterDocument()` and `starterPresentation()` return example files with their sample data |
|
|
46
47
|
|
|
47
48
|
## Documentation
|
|
48
49
|
|
package/index.cjs
CHANGED
|
@@ -2013,6 +2013,14 @@ function installJinjaFilters(env) {
|
|
|
2013
2013
|
const space = cut.lastIndexOf(" ");
|
|
2014
2014
|
return (space >= 0 ? cut.slice(0, space) : cut) + end;
|
|
2015
2015
|
});
|
|
2016
|
+
const helperSum = lookup.getFilter("sum");
|
|
2017
|
+
lookup.addFilter("sum", function(value, ...args) {
|
|
2018
|
+
const named = keywordArgs(args);
|
|
2019
|
+
if (!named) return helperSum.call(this, value, ...args);
|
|
2020
|
+
const p = args.slice(0, -1);
|
|
2021
|
+
const total = helperSum.call(this, value, p[0] ?? named["attribute"]);
|
|
2022
|
+
return total + Number(p[1] ?? named["start"] ?? 0);
|
|
2023
|
+
});
|
|
2016
2024
|
const helperMap = lookup.getFilter("map");
|
|
2017
2025
|
lookup.addFilter("map", function(value, ...args) {
|
|
2018
2026
|
const named = keywordArgs(args);
|
|
@@ -2053,6 +2061,25 @@ function trimChars(s, chars, side) {
|
|
|
2053
2061
|
if (side !== "left") while (end > start && set.has(s[end - 1] ?? "")) end--;
|
|
2054
2062
|
return s.slice(start, end);
|
|
2055
2063
|
}
|
|
2064
|
+
function isPlainObject(v) {
|
|
2065
|
+
if (v === null || typeof v !== "object") return false;
|
|
2066
|
+
const proto = Object.getPrototypeOf(v);
|
|
2067
|
+
return proto === Object.prototype || proto === null;
|
|
2068
|
+
}
|
|
2069
|
+
function pythonRepr(v) {
|
|
2070
|
+
if (v === null || v === void 0) return "None";
|
|
2071
|
+
if (v === true) return "True";
|
|
2072
|
+
if (v === false) return "False";
|
|
2073
|
+
if (typeof v === "number") return Number.isNaN(v) ? "nan" : Number.isFinite(v) ? String(v) : v > 0 ? "inf" : "-inf";
|
|
2074
|
+
if (typeof v === "string") {
|
|
2075
|
+
const quote2 = v.includes("'") && !v.includes('"') ? '"' : "'";
|
|
2076
|
+
const escaped = v.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
2077
|
+
return quote2 + (quote2 === "'" ? escaped.replace(/'/g, "\\'") : escaped) + quote2;
|
|
2078
|
+
}
|
|
2079
|
+
if (Array.isArray(v)) return `[${v.map(pythonRepr).join(", ")}]`;
|
|
2080
|
+
if (isPlainObject(v)) return `{${Object.entries(v).map(([k, item]) => `${pythonRepr(k)}: ${pythonRepr(item)}`).join(", ")}}`;
|
|
2081
|
+
return String(v);
|
|
2082
|
+
}
|
|
2056
2083
|
var compatInstalled = false;
|
|
2057
2084
|
function installCompat() {
|
|
2058
2085
|
if (compatInstalled) return;
|
|
@@ -2077,6 +2104,8 @@ function installCompat() {
|
|
|
2077
2104
|
else this.compile(node.body, frame);
|
|
2078
2105
|
this._emitLine(");");
|
|
2079
2106
|
};
|
|
2107
|
+
const suppressValue = import_nunjucks.default.runtime.suppressValue;
|
|
2108
|
+
import_nunjucks.default.runtime.suppressValue = (val2, autoescape) => suppressValue(Array.isArray(val2) || isPlainObject(val2) ? pythonRepr(val2) : val2, autoescape);
|
|
2080
2109
|
const original = import_nunjucks.default.runtime.memberLookup;
|
|
2081
2110
|
import_nunjucks.default.runtime.memberLookup = (obj, val2, ...rest) => {
|
|
2082
2111
|
if (typeof obj === "string" && typeof val2 === "string" && Object.prototype.hasOwnProperty.call(stringMethods, val2)) {
|
|
@@ -4164,20 +4193,31 @@ function setInLoop(source) {
|
|
|
4164
4193
|
}
|
|
4165
4194
|
var STYLE_BLOCK = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
|
|
4166
4195
|
var HEAD_MARKUP = /<script\b[\s\S]*?<\/script\s*>|<!--[\s\S]*?-->|<(?:link|meta|base)\b[^>]*>/gi;
|
|
4196
|
+
var TEMPLATED = /\{\{|\{%/;
|
|
4167
4197
|
function splitApitemplateCss(source) {
|
|
4168
|
-
|
|
4198
|
+
const styleElement = (rules) => `<style>
|
|
4199
|
+
${rules}
|
|
4200
|
+
</style>`;
|
|
4201
|
+
if (!/<\/?(style|link|script|meta|base)\b|<!--/i.test(source))
|
|
4202
|
+
return TEMPLATED.test(source) ? { css: "", head: "", body: styleElement(source.trim()) } : { css: source, head: "", body: "" };
|
|
4169
4203
|
const styles = [];
|
|
4170
4204
|
const head = [];
|
|
4171
|
-
|
|
4172
|
-
|
|
4205
|
+
const body = [];
|
|
4206
|
+
let rest = source.replace(STYLE_BLOCK, (block, rules) => {
|
|
4207
|
+
if (TEMPLATED.test(rules)) body.push(block.trim());
|
|
4208
|
+
else styles.push(rules.replace(/^\n+|\s+$/g, ""));
|
|
4173
4209
|
return "\n";
|
|
4174
4210
|
});
|
|
4175
4211
|
rest = rest.replace(HEAD_MARKUP, (tag) => {
|
|
4176
|
-
head.push(tag.trim());
|
|
4212
|
+
(TEMPLATED.test(tag) && !tag.startsWith("<!--") ? body : head).push(tag.trim());
|
|
4177
4213
|
return "\n";
|
|
4178
4214
|
});
|
|
4179
|
-
|
|
4180
|
-
|
|
4215
|
+
let loose = rest.replace(/\n{3,}/g, "\n\n").trim();
|
|
4216
|
+
if (TEMPLATED.test(loose)) {
|
|
4217
|
+
body.unshift(styleElement(loose));
|
|
4218
|
+
loose = "";
|
|
4219
|
+
}
|
|
4220
|
+
return { css: [loose, ...styles].filter(Boolean).join("\n\n"), head: head.join("\n"), body: body.join("\n") };
|
|
4181
4221
|
}
|
|
4182
4222
|
function importApitemplate(input) {
|
|
4183
4223
|
const warnings = [];
|
|
@@ -4200,11 +4240,14 @@ function importApitemplate(input) {
|
|
|
4200
4240
|
if (noSample) changes.push("no sample data: the preview stays empty and missing fields are not checked until you add some");
|
|
4201
4241
|
const sample = parseSample(input.sample_data);
|
|
4202
4242
|
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" });
|
|
4203
|
-
const html = input.html ?? "";
|
|
4204
4243
|
const split = splitApitemplateCss(input.css ?? "");
|
|
4244
|
+
const html = split.body ? `${(input.html ?? "").trimEnd()}
|
|
4245
|
+
${split.body}
|
|
4246
|
+
` : input.html ?? "";
|
|
4205
4247
|
let css2 = split.css;
|
|
4206
4248
|
const head = split.head;
|
|
4207
4249
|
if (head) changes.push("<link> and <script> tags of the CSS field moved to the Head tab, <style> contents kept as CSS");
|
|
4250
|
+
if (split.body) changes.push("script and style blocks of the CSS field that use template syntax moved to the end of the template, where they are filled with the data");
|
|
4208
4251
|
if (html.includes(HIDDEN_WRAPPER)) {
|
|
4209
4252
|
css2 = `.${HIDDEN_WRAPPER} { display: none !important; }
|
|
4210
4253
|
${css2}`;
|
|
@@ -4923,7 +4966,7 @@ function mapChrome(template, kind, warnings, changes) {
|
|
|
4923
4966
|
if (chrome["waitForJS"] === true || chrome["waitForJS"] === "true")
|
|
4924
4967
|
warnings.push({
|
|
4925
4968
|
code: "wait-for-js",
|
|
4926
|
-
message: "The template sets window.JSREPORT_READY_TO_START; Formfeed waits for a CSS selector instead
|
|
4969
|
+
message: "The template sets window.JSREPORT_READY_TO_START; Formfeed waits for a CSS selector instead: set waitFor.selector in settings.json or in the settings of the API request.",
|
|
4927
4970
|
docs: `${DOCS3}#settings`
|
|
4928
4971
|
});
|
|
4929
4972
|
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` });
|
|
@@ -5083,7 +5126,7 @@ function defaultSettings() {
|
|
|
5083
5126
|
watermark: null
|
|
5084
5127
|
};
|
|
5085
5128
|
}
|
|
5086
|
-
function
|
|
5129
|
+
function isPlainObject2(v) {
|
|
5087
5130
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
5088
5131
|
}
|
|
5089
5132
|
function mergeSettings(...layers) {
|
|
@@ -5091,9 +5134,9 @@ function mergeSettings(...layers) {
|
|
|
5091
5134
|
const apply = (target, source) => {
|
|
5092
5135
|
for (const [key, value] of Object.entries(source)) {
|
|
5093
5136
|
if (value === void 0) continue;
|
|
5094
|
-
if (
|
|
5137
|
+
if (isPlainObject2(value) && isPlainObject2(target[key]))
|
|
5095
5138
|
apply(target[key], value);
|
|
5096
|
-
else if (
|
|
5139
|
+
else if (isPlainObject2(value)) target[key] = apply({}, value);
|
|
5097
5140
|
else target[key] = value;
|
|
5098
5141
|
}
|
|
5099
5142
|
return target;
|
|
@@ -5258,7 +5301,13 @@ function paperOf(settings) {
|
|
|
5258
5301
|
const h = size?.[1] ?? "297mm";
|
|
5259
5302
|
return paper.landscape ? { width: h, height: w } : { width: w, height: h };
|
|
5260
5303
|
}
|
|
5261
|
-
var pageNumberSpans = (html, page, total) =>
|
|
5304
|
+
var pageNumberSpans = (html, page, total) => {
|
|
5305
|
+
const fill = (source, name, value) => source.replace(
|
|
5306
|
+
new RegExp(`<([a-z][a-z0-9]*)(\\s[^>]*?\\bclass=(["'])(?:[^"']*\\s)?${name}(?:\\s[^"']*)?\\3[^>]*)>[^<]*</\\1>`, "gi"),
|
|
5307
|
+
(_match, tag, attrs) => `<${tag}${attrs}>${value}</${tag}>`
|
|
5308
|
+
);
|
|
5309
|
+
return fill(fill(html, "pageNumber", page), "totalPages", total);
|
|
5310
|
+
};
|
|
5262
5311
|
var inspectScript = `<script>
|
|
5263
5312
|
document.addEventListener('click', function (event) {
|
|
5264
5313
|
var el = event.target;
|
|
@@ -7644,8 +7693,35 @@ function normaliseTags(template, part) {
|
|
|
7644
7693
|
});
|
|
7645
7694
|
}
|
|
7646
7695
|
}
|
|
7696
|
+
const reported = /* @__PURE__ */ new Set();
|
|
7697
|
+
for (const p of placeholders) {
|
|
7698
|
+
const raw = template.markup[p.index]?.raw ?? "";
|
|
7699
|
+
for (const m of raw.matchAll(/<w:instrText\b[^>]*>([^<]*)<\/w:instrText>/g)) {
|
|
7700
|
+
const code = decodeEntities(m[1] ?? "");
|
|
7701
|
+
if (!/\{\{|\{%|\{#/.test(code)) continue;
|
|
7702
|
+
const paragraph2 = paragraphOf(template, placeholders, p.at);
|
|
7703
|
+
if (reported.has(paragraph2)) continue;
|
|
7704
|
+
reported.add(paragraph2);
|
|
7705
|
+
diagnostics.push({
|
|
7706
|
+
severity: "warning",
|
|
7707
|
+
code: "office-tag-in-field",
|
|
7708
|
+
message: `The field code ${short(code.trim())} contains a tag, which stays as typed: fields are not filled. Put the tag in the document text instead.`,
|
|
7709
|
+
part,
|
|
7710
|
+
paragraph: paragraph2,
|
|
7711
|
+
text: short(code.trim())
|
|
7712
|
+
});
|
|
7713
|
+
}
|
|
7714
|
+
}
|
|
7647
7715
|
return { template: { ...template, source: applyEdits(template.source, edits) }, diagnostics };
|
|
7648
7716
|
}
|
|
7717
|
+
var ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
7718
|
+
function decodeEntities(text) {
|
|
7719
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi, (whole, name) => {
|
|
7720
|
+
if (name[0] !== "#") return ENTITIES[name.toLowerCase()] ?? whole;
|
|
7721
|
+
const code = name[1] === "x" || name[1] === "X" ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
|
|
7722
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
|
|
7723
|
+
});
|
|
7724
|
+
}
|
|
7649
7725
|
function stripPlaceholders(template) {
|
|
7650
7726
|
const pattern = placeholderPattern(template.nonce);
|
|
7651
7727
|
let text = "";
|
package/index.js
CHANGED
|
@@ -1883,6 +1883,14 @@ function installJinjaFilters(env) {
|
|
|
1883
1883
|
const space = cut.lastIndexOf(" ");
|
|
1884
1884
|
return (space >= 0 ? cut.slice(0, space) : cut) + end;
|
|
1885
1885
|
});
|
|
1886
|
+
const helperSum = lookup.getFilter("sum");
|
|
1887
|
+
lookup.addFilter("sum", function(value, ...args) {
|
|
1888
|
+
const named = keywordArgs(args);
|
|
1889
|
+
if (!named) return helperSum.call(this, value, ...args);
|
|
1890
|
+
const p = args.slice(0, -1);
|
|
1891
|
+
const total = helperSum.call(this, value, p[0] ?? named["attribute"]);
|
|
1892
|
+
return total + Number(p[1] ?? named["start"] ?? 0);
|
|
1893
|
+
});
|
|
1886
1894
|
const helperMap = lookup.getFilter("map");
|
|
1887
1895
|
lookup.addFilter("map", function(value, ...args) {
|
|
1888
1896
|
const named = keywordArgs(args);
|
|
@@ -1923,6 +1931,25 @@ function trimChars(s, chars, side) {
|
|
|
1923
1931
|
if (side !== "left") while (end > start && set.has(s[end - 1] ?? "")) end--;
|
|
1924
1932
|
return s.slice(start, end);
|
|
1925
1933
|
}
|
|
1934
|
+
function isPlainObject(v) {
|
|
1935
|
+
if (v === null || typeof v !== "object") return false;
|
|
1936
|
+
const proto = Object.getPrototypeOf(v);
|
|
1937
|
+
return proto === Object.prototype || proto === null;
|
|
1938
|
+
}
|
|
1939
|
+
function pythonRepr(v) {
|
|
1940
|
+
if (v === null || v === void 0) return "None";
|
|
1941
|
+
if (v === true) return "True";
|
|
1942
|
+
if (v === false) return "False";
|
|
1943
|
+
if (typeof v === "number") return Number.isNaN(v) ? "nan" : Number.isFinite(v) ? String(v) : v > 0 ? "inf" : "-inf";
|
|
1944
|
+
if (typeof v === "string") {
|
|
1945
|
+
const quote2 = v.includes("'") && !v.includes('"') ? '"' : "'";
|
|
1946
|
+
const escaped = v.replace(/\\/g, "\\\\").replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
1947
|
+
return quote2 + (quote2 === "'" ? escaped.replace(/'/g, "\\'") : escaped) + quote2;
|
|
1948
|
+
}
|
|
1949
|
+
if (Array.isArray(v)) return `[${v.map(pythonRepr).join(", ")}]`;
|
|
1950
|
+
if (isPlainObject(v)) return `{${Object.entries(v).map(([k, item]) => `${pythonRepr(k)}: ${pythonRepr(item)}`).join(", ")}}`;
|
|
1951
|
+
return String(v);
|
|
1952
|
+
}
|
|
1926
1953
|
var compatInstalled = false;
|
|
1927
1954
|
function installCompat() {
|
|
1928
1955
|
if (compatInstalled) return;
|
|
@@ -1947,6 +1974,8 @@ function installCompat() {
|
|
|
1947
1974
|
else this.compile(node.body, frame);
|
|
1948
1975
|
this._emitLine(");");
|
|
1949
1976
|
};
|
|
1977
|
+
const suppressValue = nunjucks.runtime.suppressValue;
|
|
1978
|
+
nunjucks.runtime.suppressValue = (val2, autoescape) => suppressValue(Array.isArray(val2) || isPlainObject(val2) ? pythonRepr(val2) : val2, autoescape);
|
|
1950
1979
|
const original = nunjucks.runtime.memberLookup;
|
|
1951
1980
|
nunjucks.runtime.memberLookup = (obj, val2, ...rest) => {
|
|
1952
1981
|
if (typeof obj === "string" && typeof val2 === "string" && Object.prototype.hasOwnProperty.call(stringMethods, val2)) {
|
|
@@ -4034,20 +4063,31 @@ function setInLoop(source) {
|
|
|
4034
4063
|
}
|
|
4035
4064
|
var STYLE_BLOCK = /<style\b[^>]*>([\s\S]*?)<\/style\s*>/gi;
|
|
4036
4065
|
var HEAD_MARKUP = /<script\b[\s\S]*?<\/script\s*>|<!--[\s\S]*?-->|<(?:link|meta|base)\b[^>]*>/gi;
|
|
4066
|
+
var TEMPLATED = /\{\{|\{%/;
|
|
4037
4067
|
function splitApitemplateCss(source) {
|
|
4038
|
-
|
|
4068
|
+
const styleElement = (rules) => `<style>
|
|
4069
|
+
${rules}
|
|
4070
|
+
</style>`;
|
|
4071
|
+
if (!/<\/?(style|link|script|meta|base)\b|<!--/i.test(source))
|
|
4072
|
+
return TEMPLATED.test(source) ? { css: "", head: "", body: styleElement(source.trim()) } : { css: source, head: "", body: "" };
|
|
4039
4073
|
const styles = [];
|
|
4040
4074
|
const head = [];
|
|
4041
|
-
|
|
4042
|
-
|
|
4075
|
+
const body = [];
|
|
4076
|
+
let rest = source.replace(STYLE_BLOCK, (block, rules) => {
|
|
4077
|
+
if (TEMPLATED.test(rules)) body.push(block.trim());
|
|
4078
|
+
else styles.push(rules.replace(/^\n+|\s+$/g, ""));
|
|
4043
4079
|
return "\n";
|
|
4044
4080
|
});
|
|
4045
4081
|
rest = rest.replace(HEAD_MARKUP, (tag) => {
|
|
4046
|
-
head.push(tag.trim());
|
|
4082
|
+
(TEMPLATED.test(tag) && !tag.startsWith("<!--") ? body : head).push(tag.trim());
|
|
4047
4083
|
return "\n";
|
|
4048
4084
|
});
|
|
4049
|
-
|
|
4050
|
-
|
|
4085
|
+
let loose = rest.replace(/\n{3,}/g, "\n\n").trim();
|
|
4086
|
+
if (TEMPLATED.test(loose)) {
|
|
4087
|
+
body.unshift(styleElement(loose));
|
|
4088
|
+
loose = "";
|
|
4089
|
+
}
|
|
4090
|
+
return { css: [loose, ...styles].filter(Boolean).join("\n\n"), head: head.join("\n"), body: body.join("\n") };
|
|
4051
4091
|
}
|
|
4052
4092
|
function importApitemplate(input) {
|
|
4053
4093
|
const warnings = [];
|
|
@@ -4070,11 +4110,14 @@ function importApitemplate(input) {
|
|
|
4070
4110
|
if (noSample) changes.push("no sample data: the preview stays empty and missing fields are not checked until you add some");
|
|
4071
4111
|
const sample = parseSample(input.sample_data);
|
|
4072
4112
|
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" });
|
|
4073
|
-
const html = input.html ?? "";
|
|
4074
4113
|
const split = splitApitemplateCss(input.css ?? "");
|
|
4114
|
+
const html = split.body ? `${(input.html ?? "").trimEnd()}
|
|
4115
|
+
${split.body}
|
|
4116
|
+
` : input.html ?? "";
|
|
4075
4117
|
let css2 = split.css;
|
|
4076
4118
|
const head = split.head;
|
|
4077
4119
|
if (head) changes.push("<link> and <script> tags of the CSS field moved to the Head tab, <style> contents kept as CSS");
|
|
4120
|
+
if (split.body) changes.push("script and style blocks of the CSS field that use template syntax moved to the end of the template, where they are filled with the data");
|
|
4078
4121
|
if (html.includes(HIDDEN_WRAPPER)) {
|
|
4079
4122
|
css2 = `.${HIDDEN_WRAPPER} { display: none !important; }
|
|
4080
4123
|
${css2}`;
|
|
@@ -4793,7 +4836,7 @@ function mapChrome(template, kind, warnings, changes) {
|
|
|
4793
4836
|
if (chrome["waitForJS"] === true || chrome["waitForJS"] === "true")
|
|
4794
4837
|
warnings.push({
|
|
4795
4838
|
code: "wait-for-js",
|
|
4796
|
-
message: "The template sets window.JSREPORT_READY_TO_START; Formfeed waits for a CSS selector instead
|
|
4839
|
+
message: "The template sets window.JSREPORT_READY_TO_START; Formfeed waits for a CSS selector instead: set waitFor.selector in settings.json or in the settings of the API request.",
|
|
4797
4840
|
docs: `${DOCS3}#settings`
|
|
4798
4841
|
});
|
|
4799
4842
|
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` });
|
|
@@ -4953,7 +4996,7 @@ function defaultSettings() {
|
|
|
4953
4996
|
watermark: null
|
|
4954
4997
|
};
|
|
4955
4998
|
}
|
|
4956
|
-
function
|
|
4999
|
+
function isPlainObject2(v) {
|
|
4957
5000
|
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
4958
5001
|
}
|
|
4959
5002
|
function mergeSettings(...layers) {
|
|
@@ -4961,9 +5004,9 @@ function mergeSettings(...layers) {
|
|
|
4961
5004
|
const apply = (target, source) => {
|
|
4962
5005
|
for (const [key, value] of Object.entries(source)) {
|
|
4963
5006
|
if (value === void 0) continue;
|
|
4964
|
-
if (
|
|
5007
|
+
if (isPlainObject2(value) && isPlainObject2(target[key]))
|
|
4965
5008
|
apply(target[key], value);
|
|
4966
|
-
else if (
|
|
5009
|
+
else if (isPlainObject2(value)) target[key] = apply({}, value);
|
|
4967
5010
|
else target[key] = value;
|
|
4968
5011
|
}
|
|
4969
5012
|
return target;
|
|
@@ -5128,7 +5171,13 @@ function paperOf(settings) {
|
|
|
5128
5171
|
const h = size?.[1] ?? "297mm";
|
|
5129
5172
|
return paper.landscape ? { width: h, height: w } : { width: w, height: h };
|
|
5130
5173
|
}
|
|
5131
|
-
var pageNumberSpans = (html, page, total) =>
|
|
5174
|
+
var pageNumberSpans = (html, page, total) => {
|
|
5175
|
+
const fill = (source, name, value) => source.replace(
|
|
5176
|
+
new RegExp(`<([a-z][a-z0-9]*)(\\s[^>]*?\\bclass=(["'])(?:[^"']*\\s)?${name}(?:\\s[^"']*)?\\3[^>]*)>[^<]*</\\1>`, "gi"),
|
|
5177
|
+
(_match, tag, attrs) => `<${tag}${attrs}>${value}</${tag}>`
|
|
5178
|
+
);
|
|
5179
|
+
return fill(fill(html, "pageNumber", page), "totalPages", total);
|
|
5180
|
+
};
|
|
5132
5181
|
var inspectScript = `<script>
|
|
5133
5182
|
document.addEventListener('click', function (event) {
|
|
5134
5183
|
var el = event.target;
|
|
@@ -7514,8 +7563,35 @@ function normaliseTags(template, part) {
|
|
|
7514
7563
|
});
|
|
7515
7564
|
}
|
|
7516
7565
|
}
|
|
7566
|
+
const reported = /* @__PURE__ */ new Set();
|
|
7567
|
+
for (const p of placeholders) {
|
|
7568
|
+
const raw = template.markup[p.index]?.raw ?? "";
|
|
7569
|
+
for (const m of raw.matchAll(/<w:instrText\b[^>]*>([^<]*)<\/w:instrText>/g)) {
|
|
7570
|
+
const code = decodeEntities(m[1] ?? "");
|
|
7571
|
+
if (!/\{\{|\{%|\{#/.test(code)) continue;
|
|
7572
|
+
const paragraph2 = paragraphOf(template, placeholders, p.at);
|
|
7573
|
+
if (reported.has(paragraph2)) continue;
|
|
7574
|
+
reported.add(paragraph2);
|
|
7575
|
+
diagnostics.push({
|
|
7576
|
+
severity: "warning",
|
|
7577
|
+
code: "office-tag-in-field",
|
|
7578
|
+
message: `The field code ${short(code.trim())} contains a tag, which stays as typed: fields are not filled. Put the tag in the document text instead.`,
|
|
7579
|
+
part,
|
|
7580
|
+
paragraph: paragraph2,
|
|
7581
|
+
text: short(code.trim())
|
|
7582
|
+
});
|
|
7583
|
+
}
|
|
7584
|
+
}
|
|
7517
7585
|
return { template: { ...template, source: applyEdits(template.source, edits) }, diagnostics };
|
|
7518
7586
|
}
|
|
7587
|
+
var ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'" };
|
|
7588
|
+
function decodeEntities(text) {
|
|
7589
|
+
return text.replace(/&(#x[0-9a-f]+|#\d+|amp|lt|gt|quot|apos);/gi, (whole, name) => {
|
|
7590
|
+
if (name[0] !== "#") return ENTITIES[name.toLowerCase()] ?? whole;
|
|
7591
|
+
const code = name[1] === "x" || name[1] === "X" ? parseInt(name.slice(2), 16) : parseInt(name.slice(1), 10);
|
|
7592
|
+
return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
|
|
7593
|
+
});
|
|
7594
|
+
}
|
|
7519
7595
|
function stripPlaceholders(template) {
|
|
7520
7596
|
const pattern = placeholderPattern(template.nonce);
|
|
7521
7597
|
let text = "";
|
package/package.json
CHANGED
|
@@ -22,10 +22,13 @@ export interface ApitemplateExport {
|
|
|
22
22
|
* apitemplate.io puts its CSS field into the document head as is, so it often carries `<link>`,
|
|
23
23
|
* `<script>` and `<style>` tags. Formfeed wraps CSS in a `<style>` element, where such tags break
|
|
24
24
|
* the stylesheet and never load: style blocks become CSS, the rest of the markup goes to the head.
|
|
25
|
+
* apitemplate.io also fills that field with the data, while Formfeed's CSS and head are static, so
|
|
26
|
+
* blocks that use template syntax (chart scripts with `{{ data }}`) go to the end of the body instead.
|
|
25
27
|
*/
|
|
26
28
|
export declare function splitApitemplateCss(source: string): {
|
|
27
29
|
css: string;
|
|
28
30
|
head: string;
|
|
31
|
+
body: string;
|
|
29
32
|
};
|
|
30
33
|
export declare function importApitemplate(input: ApitemplateExport): ImportResult;
|
|
31
34
|
/**
|
|
@@ -22,10 +22,13 @@ export interface ApitemplateExport {
|
|
|
22
22
|
* apitemplate.io puts its CSS field into the document head as is, so it often carries `<link>`,
|
|
23
23
|
* `<script>` and `<style>` tags. Formfeed wraps CSS in a `<style>` element, where such tags break
|
|
24
24
|
* the stylesheet and never load: style blocks become CSS, the rest of the markup goes to the head.
|
|
25
|
+
* apitemplate.io also fills that field with the data, while Formfeed's CSS and head are static, so
|
|
26
|
+
* blocks that use template syntax (chart scripts with `{{ data }}`) go to the end of the body instead.
|
|
25
27
|
*/
|
|
26
28
|
export declare function splitApitemplateCss(source: string): {
|
|
27
29
|
css: string;
|
|
28
30
|
head: string;
|
|
31
|
+
body: string;
|
|
29
32
|
};
|
|
30
33
|
export declare function importApitemplate(input: ApitemplateExport): ImportResult;
|
|
31
34
|
/**
|
package/types/lib/preview.d.cts
CHANGED
|
@@ -16,7 +16,11 @@ export declare function paperOf(settings: TemplateSettings): {
|
|
|
16
16
|
width: string;
|
|
17
17
|
height: string;
|
|
18
18
|
};
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Chromium's header/footer placeholders filled for the screen. Like Chromium, any element whose class
|
|
21
|
+
* list names the placeholder counts, whatever else it carries (the editor adds `data-ff-src`), and
|
|
22
|
+
* the element stays so its styling applies.
|
|
23
|
+
*/
|
|
20
24
|
export declare const pageNumberSpans: (html: string, page: string, total: string) => string;
|
|
21
25
|
/**
|
|
22
26
|
* Zoom gestures inside a preview frame: Ctrl+wheel (a touchpad pinch arrives as one too) and
|
package/types/lib/preview.d.ts
CHANGED
|
@@ -16,7 +16,11 @@ export declare function paperOf(settings: TemplateSettings): {
|
|
|
16
16
|
width: string;
|
|
17
17
|
height: string;
|
|
18
18
|
};
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Chromium's header/footer placeholders filled for the screen. Like Chromium, any element whose class
|
|
21
|
+
* list names the placeholder counts, whatever else it carries (the editor adds `data-ff-src`), and
|
|
22
|
+
* the element stays so its styling applies.
|
|
23
|
+
*/
|
|
20
24
|
export declare const pageNumberSpans: (html: string, page: string, total: string) => string;
|
|
21
25
|
/**
|
|
22
26
|
* Zoom gestures inside a preview frame: Ctrl+wheel (a touchpad pinch arrives as one too) and
|
|
@@ -33,6 +33,7 @@ declare module 'nunjucks/browser/nunjucks.js' {
|
|
|
33
33
|
Loader: typeof N.Loader;
|
|
34
34
|
runtime: typeof N.runtime & {
|
|
35
35
|
memberLookup: (obj: unknown, val: unknown, ...rest: unknown[]) => unknown;
|
|
36
|
+
suppressValue: (val: unknown, autoescape: boolean) => unknown;
|
|
36
37
|
};
|
|
37
38
|
/** The code generator; patched for `not` precedence and namespace attributes (engines/jinja2.ts). */
|
|
38
39
|
compiler: {
|
package/types/types/modules.d.ts
CHANGED
|
@@ -33,6 +33,7 @@ declare module 'nunjucks/browser/nunjucks.js' {
|
|
|
33
33
|
Loader: typeof N.Loader;
|
|
34
34
|
runtime: typeof N.runtime & {
|
|
35
35
|
memberLookup: (obj: unknown, val: unknown, ...rest: unknown[]) => unknown;
|
|
36
|
+
suppressValue: (val: unknown, autoescape: boolean) => unknown;
|
|
36
37
|
};
|
|
37
38
|
/** The code generator; patched for `not` precedence and namespace attributes (engines/jinja2.ts). */
|
|
38
39
|
compiler: {
|