@formfeed/engine 0.3.0 → 0.3.2

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 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()`: converts apitemplate.io templates |
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
- if (!/<\/?(style|link|script|meta|base)\b|<!--/i.test(source)) return { css: source, head: "" };
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
- let rest = source.replace(STYLE_BLOCK, (_, body) => {
4172
- styles.push(body.replace(/^\n+|\s+$/g, ""));
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
- const loose = rest.replace(/\n{3,}/g, "\n\n").trim();
4180
- return { css: [loose, ...styles].filter(Boolean).join("\n\n"), head: head.join("\n") };
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 (Settings \u2192 Wait for).",
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 isPlainObject(v) {
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 (isPlainObject(value) && isPlainObject(target[key]))
5137
+ if (isPlainObject2(value) && isPlainObject2(target[key]))
5095
5138
  apply(target[key], value);
5096
- else if (isPlainObject(value)) target[key] = apply({}, value);
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) => html.replace(/<span class="pageNumber"><\/span>/g, page).replace(/<span class="totalPages"><\/span>/g, 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
- if (!/<\/?(style|link|script|meta|base)\b|<!--/i.test(source)) return { css: source, head: "" };
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
- let rest = source.replace(STYLE_BLOCK, (_, body) => {
4042
- styles.push(body.replace(/^\n+|\s+$/g, ""));
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
- const loose = rest.replace(/\n{3,}/g, "\n\n").trim();
4050
- return { css: [loose, ...styles].filter(Boolean).join("\n\n"), head: head.join("\n") };
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 (Settings \u2192 Wait for).",
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 isPlainObject(v) {
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 (isPlainObject(value) && isPlainObject(target[key]))
5007
+ if (isPlainObject2(value) && isPlainObject2(target[key]))
4965
5008
  apply(target[key], value);
4966
- else if (isPlainObject(value)) target[key] = apply({}, value);
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) => html.replace(/<span class="pageNumber"><\/span>/g, page).replace(/<span class="totalPages"><\/span>/g, 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@formfeed/engine",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "The Formfeed template engine (Jinja2, Liquid, Handlebars) for the browser and Node.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
  /**
@@ -16,7 +16,11 @@ export declare function paperOf(settings: TemplateSettings): {
16
16
  width: string;
17
17
  height: string;
18
18
  };
19
- /** Chromium's header/footer placeholders replaced for the screen. */
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
@@ -16,7 +16,11 @@ export declare function paperOf(settings: TemplateSettings): {
16
16
  width: string;
17
17
  height: string;
18
18
  };
19
- /** Chromium's header/footer placeholders replaced for the screen. */
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: {
@@ -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: {