@richscripts/richtexteditor 2.4.0 → 2.4.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.
Files changed (58) hide show
  1. package/migration/README.md +100 -0
  2. package/migration/cli.js +294 -0
  3. package/migration/index.d.ts +18 -0
  4. package/migration/index.js +242 -0
  5. package/migration/report-schema-v2.json +127 -0
  6. package/package.json +11 -1
  7. package/richtexteditor/CHANGELOG.md +326 -1
  8. package/richtexteditor/all_plugins.js +14959 -4832
  9. package/richtexteditor/all_plugins.min.js +62 -42
  10. package/richtexteditor/plugins/accessibilitychecker.js +8 -7
  11. package/richtexteditor/plugins/aitoolkit.css +17 -8
  12. package/richtexteditor/plugins/aitoolkit.js +8 -8
  13. package/richtexteditor/plugins/all_plugins.js +14959 -4832
  14. package/richtexteditor/plugins/all_plugins.min.js +62 -42
  15. package/richtexteditor/plugins/autocorrect.js +353 -0
  16. package/richtexteditor/plugins/bookmarkcard.js +12 -1
  17. package/richtexteditor/plugins/changecase.js +238 -0
  18. package/richtexteditor/plugins/comments.js +15 -28
  19. package/richtexteditor/plugins/contentminimap.js +6 -5
  20. package/richtexteditor/plugins/crossreference.js +408 -0
  21. package/richtexteditor/plugins/documentimport.js +915 -21
  22. package/richtexteditor/plugins/documentoutline.js +5 -4
  23. package/richtexteditor/plugins/docxexport.js +960 -0
  24. package/richtexteditor/plugins/draghandle.js +309 -0
  25. package/richtexteditor/plugins/footnotes.js +398 -0
  26. package/richtexteditor/plugins/formatpainter.js +298 -0
  27. package/richtexteditor/plugins/formattingmarks.js +246 -0
  28. package/richtexteditor/plugins/html2pdf.js +108 -4
  29. package/richtexteditor/plugins/imageeditor.js +37 -7
  30. package/richtexteditor/plugins/insertgallery.js +553 -170
  31. package/richtexteditor/plugins/keyboarda11y.js +316 -0
  32. package/richtexteditor/plugins/linenumbers.js +352 -0
  33. package/richtexteditor/plugins/linkchecker.js +396 -0
  34. package/richtexteditor/plugins/mentionplugin.js +4 -3
  35. package/richtexteditor/plugins/mergefields.js +328 -0
  36. package/richtexteditor/plugins/multilevellist.js +137 -0
  37. package/richtexteditor/plugins/pagination.js +505 -0
  38. package/richtexteditor/plugins/pdfexport.js +1137 -0
  39. package/richtexteditor/plugins/permanentpen.js +211 -0
  40. package/richtexteditor/plugins/revisionhistory.js +47 -34
  41. package/richtexteditor/plugins/rtlui.js +150 -0
  42. package/richtexteditor/plugins/sanitizer.js +486 -0
  43. package/richtexteditor/plugins/slashcommand.js +142 -17
  44. package/richtexteditor/plugins/spellcheck.js +6 -3
  45. package/richtexteditor/plugins/tableofcontents.js +353 -0
  46. package/richtexteditor/plugins/tabletools.js +342 -0
  47. package/richtexteditor/plugins/textdirection.js +313 -0
  48. package/richtexteditor/plugins/trackedchanges.js +37 -6
  49. package/richtexteditor/plugins/tui.image-editor/tui-code-snippet.min.js +1 -1
  50. package/richtexteditor/plugins/tui.image-editor/tui-color-picker.min.js +1 -1
  51. package/richtexteditor/plugins/tui.image-editor/tui-image-editor.min.js +1 -1
  52. package/richtexteditor/plugins/vendor/html2pdf.bundle.min.js +3 -0
  53. package/richtexteditor/plugins/watermark.js +214 -0
  54. package/richtexteditor/plugins/wordexport.js +0 -0
  55. package/richtexteditor/plugins/yjscollab.js +49 -11
  56. package/richtexteditor/rte-config.js +44 -44
  57. package/richtexteditor/rte.js +91 -63
  58. package/richtexteditor/rte_theme_default.css +462 -384
@@ -0,0 +1,1137 @@
1
+ if (!window.RTE_DefaultConfig) window.RTE_DefaultConfig = {};
2
+
3
+ // 2026-08-02 PDF export that produces REAL TEXT.
4
+ //
5
+ // The existing html2pdf path renders the document with html2canvas and drops a
6
+ // JPEG onto each page. It is pixel-accurate, which is genuinely the right answer
7
+ // when the layout is the point — but the resulting PDF is a picture:
8
+ //
9
+ // - nothing is selectable, copyable or searchable
10
+ // - screen readers find an empty document (a hard accessibility failure, and
11
+ // an outright blocker for anyone shipping into government or education)
12
+ // - links are painted, not clickable
13
+ // - a text-only page costs hundreds of KB instead of a few
14
+ // - zooming or printing shows resampling artefacts
15
+ //
16
+ // This writes the PDF file format directly: no library, no canvas, no network.
17
+ // Both exporters stay available and neither changes the other's behaviour —
18
+ // `exportpdf` is vector, `html2pdf` remains raster.
19
+ //
20
+ // Why writing PDF by hand is reasonable here: a text PDF is a small object graph
21
+ // plus a content stream, and the 14 standard fonts need no embedding at all. The
22
+ // hard part of a PDF generator is font subsetting, and using the base-14 fonts
23
+ // removes it entirely.
24
+ //
25
+ // API:
26
+ // editor.getPdfBytes(options) -> Uint8Array
27
+ // editor.exportToPdf(filename, options)-> triggers a download
28
+ // Command: exec_command "exportpdf".
29
+ // Config:
30
+ // config.pdfExport = false // disable
31
+ // config.pdfExportFileName = "report"
32
+ // config.pdfExportFont = "helvetica" // helvetica | times | courier
33
+ // config.pdfExportBaseFontSize = 11 // points
34
+ // config.pdfExportTitle / pdfExportAuthor // document metadata
35
+ RTE_DefaultConfig.plugin_pdfexport = RTE_Plugin_PdfExport;
36
+ if (typeof RTE_DefaultConfig.pdfExport === "undefined") RTE_DefaultConfig.pdfExport = true;
37
+
38
+ function RTE_Plugin_PdfExport() {
39
+ var obj = this;
40
+ var config, editor;
41
+
42
+ obj.PluginName = "PdfExport";
43
+
44
+ obj.InitConfig = function (argconfig) { config = argconfig; };
45
+
46
+ obj.InitEditor = function (argeditor) {
47
+ editor = argeditor;
48
+ if (config.pdfExport === false) return;
49
+
50
+ editor.getPdfBytes = function (options) { return buildPdf(options || {}); };
51
+ editor.exportToPdf = function (filename, options) {
52
+ var bytes = buildPdf(options || {});
53
+ var base = sanitizeName(filename) || sanitizeName(config.pdfExportFileName) || defaultBase();
54
+ return download(bytes, base + ".pdf");
55
+ };
56
+ editor.downloadPdf = editor.exportToPdf;
57
+
58
+ editor.attachEvent("exec_command_exportpdf", function (state) {
59
+ state.returnValue = true;
60
+ state.stopBubble = true;
61
+ try { editor.exportToPdf(); }
62
+ catch (e) { if (window.console) console.error("pdfexport:", e); }
63
+ });
64
+
65
+ // The slash-menu entry lives in slashcommand.js, gated on
66
+ // `typeof editor.exportToPdf === "function"`. Registering from here
67
+ // instead would silently do nothing: plugins initialise in bundle
68
+ // order, "pdfexport" sorts before "slashcommand", and
69
+ // editor.slashCommands does not exist yet at this point.
70
+ };
71
+
72
+ // ---------------------------------------------------------------- units
73
+ var PT_PER_IN = 72;
74
+ var PAGE_SIZES = { // points, portrait
75
+ letter: [612, 792], legal: [612, 1008], tabloid: [792, 1224],
76
+ a3: [842, 1191], a4: [595, 842], a5: [420, 595]
77
+ };
78
+
79
+ function toPoints(v, fallback) {
80
+ if (typeof v === "number" && isFinite(v)) return v * PT_PER_IN; // bare number = inches
81
+ if (typeof v !== "string") return fallback;
82
+ var m = /^\s*(-?[\d.]+)\s*(mm|cm|in|px|pt)?\s*$/.exec(v);
83
+ if (!m) return fallback;
84
+ var n = parseFloat(m[1]);
85
+ if (!isFinite(n)) return fallback;
86
+ switch (m[2]) {
87
+ case "mm": return n * PT_PER_IN / 25.4;
88
+ case "cm": return n * PT_PER_IN / 2.54;
89
+ case "pt": return n;
90
+ case "px": return n * 0.75; // CSS px at 96dpi
91
+ default: return n * PT_PER_IN;
92
+ }
93
+ }
94
+
95
+ function pageGeometry(options) {
96
+ var setup = null;
97
+ try { if (typeof editor.getDocumentPageSetup === "function") setup = editor.getDocumentPageSetup(); }
98
+ catch (e) { setup = null; }
99
+ setup = setup || {};
100
+ var fmt = String(options.format || setup.format || "letter").toLowerCase();
101
+ var size = PAGE_SIZES[fmt] || PAGE_SIZES.letter;
102
+ var landscape = String(options.orientation || setup.orientation || "").toLowerCase() === "landscape";
103
+ var w = landscape ? size[1] : size[0];
104
+ var h = landscape ? size[0] : size[1];
105
+ var m = options.margins || setup.margins || {};
106
+ var half = PT_PER_IN / 2;
107
+ return {
108
+ width: w, height: h,
109
+ margin: {
110
+ top: toPoints(m.top, half), right: toPoints(m.right, half),
111
+ bottom: toPoints(m.bottom, half), left: toPoints(m.left, half)
112
+ }
113
+ };
114
+ }
115
+
116
+ // ---------------------------------------------------------------- fonts
117
+ //
118
+ // The base-14 fonts need no embedding, but a viewer would then lay text out
119
+ // with ITS metrics while this code wrapped lines using its own. The two
120
+ // disagree and text overruns the margin. Fixing it by shipping AFM width
121
+ // tables would add ~1500 lines of data; instead the widths are measured here
122
+ // and written into the font object as /Widths, which a viewer is required to
123
+ // honour. Layout and rendering then use the same numbers by construction.
124
+ //
125
+ // Measurement is accurate because the base-14 fonts are metric-compatible
126
+ // with fonts every browser has: Helvetica/Arial, Times/Times New Roman,
127
+ // Courier/Courier New were designed to share advance widths.
128
+ var FONT_FAMILIES = {
129
+ helvetica: { css: "Arial, Helvetica, sans-serif", faces: ["Helvetica", "Helvetica-Bold", "Helvetica-Oblique", "Helvetica-BoldOblique"] },
130
+ times: { css: '"Times New Roman", Times, serif', faces: ["Times-Roman", "Times-Bold", "Times-Italic", "Times-BoldItalic"] },
131
+ courier: { css: '"Courier New", Courier, monospace', faces: ["Courier", "Courier-Bold", "Courier-Oblique", "Courier-BoldOblique"] }
132
+ };
133
+ var FIRST_CHAR = 32, LAST_CHAR = 255;
134
+
135
+ var widthCache = {};
136
+ function widthsFor(familyKey, styleIndex) {
137
+ var key = familyKey + ":" + styleIndex;
138
+ if (widthCache[key]) return widthCache[key];
139
+ var fam = FONT_FAMILIES[familyKey] || FONT_FAMILIES.helvetica;
140
+ var canvas = widthsFor.canvas || (widthsFor.canvas = document.createElement("canvas"));
141
+ var g = canvas.getContext("2d");
142
+ // 1000 units per em is the PDF glyph space; measuring at 100px and
143
+ // scaling by 10 keeps sub-unit rounding away from the result.
144
+ var weight = (styleIndex & 1) ? "bold " : "";
145
+ var slant = (styleIndex & 2) ? "italic " : "";
146
+ g.font = weight + slant + "100px " + fam.css;
147
+ var out = [];
148
+ for (var c = FIRST_CHAR; c <= LAST_CHAR; c++) {
149
+ var w = g.measureText(String.fromCharCode(c)).width;
150
+ out.push(Math.round(w * 10));
151
+ }
152
+ widthCache[key] = out;
153
+ return out;
154
+ }
155
+
156
+ function charWidth(familyKey, styleIndex, code) {
157
+ if (code < FIRST_CHAR || code > LAST_CHAR) code = 63; // '?'
158
+ return widthsFor(familyKey, styleIndex)[code - FIRST_CHAR] || 500;
159
+ }
160
+
161
+ function textWidth(str, familyKey, styleIndex, size) {
162
+ var total = 0;
163
+ for (var i = 0; i < str.length; i++) total += charWidth(familyKey, styleIndex, str.charCodeAt(i));
164
+ return total * size / 1000;
165
+ }
166
+
167
+ // ------------------------------------------------------------ PDF string
168
+ //
169
+ // WinAnsiEncoding covers Latin-1 plus the printable range of CP1252. A
170
+ // character outside it has no glyph in a base-14 font, so it is transliterated
171
+ // rather than written as a byte the viewer would render as garbage.
172
+ var WINANSI_EXTRA = {
173
+ 0x20AC: 128, 0x201A: 130, 0x0192: 131, 0x201E: 132, 0x2026: 133, 0x2020: 134,
174
+ 0x2021: 135, 0x02C6: 136, 0x2030: 137, 0x0160: 138, 0x2039: 139, 0x0152: 140,
175
+ 0x017D: 142, 0x2018: 145, 0x2019: 146, 0x201C: 147, 0x201D: 148, 0x2022: 149,
176
+ 0x2013: 150, 0x2014: 151, 0x02DC: 152, 0x2122: 153, 0x0161: 154, 0x203A: 155,
177
+ 0x0153: 156, 0x017E: 158, 0x0178: 159
178
+ };
179
+ function toWinAnsi(str) {
180
+ var out = "";
181
+ for (var i = 0; i < str.length; i++) {
182
+ var cp = str.charCodeAt(i);
183
+ if (cp < 256) { out += String.fromCharCode(cp); continue; }
184
+ var mapped = WINANSI_EXTRA[cp];
185
+ if (mapped) { out += String.fromCharCode(mapped); continue; }
186
+ // Common shapes that would otherwise become "?" for no good reason.
187
+ if (cp === 0x00A0) { out += " "; continue; }
188
+ if (cp >= 0x2000 && cp <= 0x200A) { out += " "; continue; }
189
+ if (cp === 0x2212) { out += "-"; continue; }
190
+ out += "?";
191
+ }
192
+ return out;
193
+ }
194
+ function escapeString(str) {
195
+ return str.replace(/\\/g, "\\\\").replace(/\(/g, "\\(").replace(/\)/g, "\\)");
196
+ }
197
+
198
+ // ------------------------------------------------------------- PDF writer
199
+ function PdfWriter() {
200
+ this.objects = []; // 1-based; objects[i] is the body of object i+1
201
+ }
202
+ PdfWriter.prototype.alloc = function () { this.objects.push(null); return this.objects.length; };
203
+ PdfWriter.prototype.set = function (id, body) { this.objects[id - 1] = body; };
204
+ PdfWriter.prototype.add = function (body) { var id = this.alloc(); this.set(id, body); return id; };
205
+
206
+ // Assembled as BYTES, not as a string: a content stream can hold binary
207
+ // (an embedded JPEG), and /Length plus the xref offsets are byte counts. A
208
+ // string-based assembler is correct right up until the first image, then
209
+ // silently produces a file no reader will open.
210
+ PdfWriter.prototype.build = function (rootId, infoId) {
211
+ var chunks = [], length = 0;
212
+ function push(part) {
213
+ var bytes = (typeof part === "string") ? latin1Bytes(part) : part;
214
+ chunks.push(bytes); length += bytes.length;
215
+ return length;
216
+ }
217
+ push("%PDF-1.4\n");
218
+ // A binary comment marks the file as binary for transfer agents.
219
+ push(new Uint8Array([0x25, 0xE2, 0xE3, 0xCF, 0xD3, 0x0A]));
220
+
221
+ var offsets = [];
222
+ for (var i = 0; i < this.objects.length; i++) {
223
+ offsets[i] = length;
224
+ push((i + 1) + " 0 obj\n");
225
+ var body = this.objects[i];
226
+ if (body && body.stream) {
227
+ var data = (typeof body.stream === "string") ? latin1Bytes(body.stream) : body.stream;
228
+ push(body.dict.replace("/Length 0", "/Length " + data.length) + "\nstream\n");
229
+ push(data);
230
+ push("\nendstream\n");
231
+ } else {
232
+ push(body + "\n");
233
+ }
234
+ push("endobj\n");
235
+ }
236
+ var xrefAt = length;
237
+ var xref = "xref\n0 " + (this.objects.length + 1) + "\n0000000000 65535 f \n";
238
+ for (var j = 0; j < offsets.length; j++) {
239
+ xref += pad10(offsets[j]) + " 00000 n \n";
240
+ }
241
+ push(xref);
242
+ push("trailer\n<< /Size " + (this.objects.length + 1) + " /Root " + rootId + " 0 R /Info " + infoId + " 0 R >>\n");
243
+ push("startxref\n" + xrefAt + "\n%%EOF\n");
244
+
245
+ var out = new Uint8Array(length), at = 0;
246
+ for (var k = 0; k < chunks.length; k++) { out.set(chunks[k], at); at += chunks[k].length; }
247
+ return out;
248
+ };
249
+ function pad10(n) { var s = String(n); while (s.length < 10) s = "0" + s; return s; }
250
+ function latin1Bytes(str) {
251
+ var out = new Uint8Array(str.length);
252
+ for (var i = 0; i < str.length; i++) out[i] = str.charCodeAt(i) & 0xFF;
253
+ return out;
254
+ }
255
+
256
+ // ---------------------------------------------------------------- layout
257
+ //
258
+ // A flat list of positioned draw operations is produced first, then paged.
259
+ // Keeping measurement and pagination apart is what makes "keep this heading
260
+ // with the paragraph under it" and "repeat a table header row" possible at
261
+ // all; doing both in one pass forces every decision to be final immediately.
262
+
263
+ function styleOf(node, inherited) {
264
+ var s = {
265
+ bold: inherited.bold, italic: inherited.italic, underline: inherited.underline,
266
+ strike: inherited.strike, size: inherited.size, color: inherited.color,
267
+ mono: inherited.mono, link: inherited.link
268
+ };
269
+ var tag = node.tagName ? node.tagName.toLowerCase() : "";
270
+ if (tag === "b" || tag === "strong") s.bold = true;
271
+ if (tag === "i" || tag === "em") s.italic = true;
272
+ if (tag === "u" || tag === "ins") s.underline = true;
273
+ if (tag === "s" || tag === "strike" || tag === "del") s.strike = true;
274
+ if (tag === "code" || tag === "kbd" || tag === "samp" || tag === "pre") s.mono = true;
275
+ if (tag === "a" && node.getAttribute("href")) { s.link = node.getAttribute("href"); s.color = [0, 0.33, 0.8]; s.underline = true; }
276
+ if (tag === "sup" || tag === "sub") s.size = s.size * 0.75;
277
+
278
+ var css = node.style;
279
+ if (css) {
280
+ if (/bold|^[6-9]00$/.test(css.fontWeight || "")) s.bold = true;
281
+ if (css.fontStyle === "italic" || css.fontStyle === "oblique") s.italic = true;
282
+ if ((css.textDecorationLine || css.textDecoration || "").indexOf("underline") >= 0) s.underline = true;
283
+ if ((css.textDecorationLine || css.textDecoration || "").indexOf("line-through") >= 0) s.strike = true;
284
+ if (css.color) { var c = parseColor(css.color); if (c) s.color = c; }
285
+ if (css.fontSize) { var fs = toPoints(css.fontSize, null); if (fs) s.size = fs; }
286
+ }
287
+ return s;
288
+ }
289
+
290
+ function parseColor(value) {
291
+ var m = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(value.replace(/\s/g, ""));
292
+ if (m) {
293
+ var hex = m[1];
294
+ if (hex.length === 3) hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
295
+ return [parseInt(hex.substr(0, 2), 16) / 255, parseInt(hex.substr(2, 2), 16) / 255, parseInt(hex.substr(4, 2), 16) / 255];
296
+ }
297
+ var rgb = /^rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i.exec(value);
298
+ if (rgb) return [rgb[1] / 255, rgb[2] / 255, rgb[3] / 255];
299
+ return null;
300
+ }
301
+
302
+ var HEADING_SCALE = { h1: 2, h2: 1.6, h3: 1.35, h4: 1.15, h5: 1, h6: 0.9 };
303
+
304
+ function Layout(geom, baseSize, familyKey) {
305
+ this.geom = geom;
306
+ this.baseSize = baseSize;
307
+ this.family = familyKey;
308
+ this.contentWidth = geom.width - geom.margin.left - geom.margin.right;
309
+ this.blocks = []; // { type, ... } in document order
310
+ }
311
+
312
+ // Collect the inline runs of a block, splitting into words for wrapping.
313
+ Layout.prototype.runsOf = function (node, inherited) {
314
+ var runs = [];
315
+ var self = this;
316
+ (function walk(n, style) {
317
+ for (var i = 0; i < n.childNodes.length; i++) {
318
+ var c = n.childNodes[i];
319
+ if (c.nodeType === 3) {
320
+ var text = (c.nodeValue || "").replace(/\s+/g, " ");
321
+ if (text) runs.push({ text: text, style: style });
322
+ continue;
323
+ }
324
+ if (c.nodeType !== 1) continue;
325
+ var tag = c.tagName.toLowerCase();
326
+ if (tag === "br") { runs.push({ br: true, style: style }); continue; }
327
+ if (tag === "img") { runs.push({ img: c, style: style }); continue; }
328
+ if (tag === "script" || tag === "style") continue;
329
+ walk(c, styleOf(c, style));
330
+ }
331
+ })(node, inherited);
332
+ return runs;
333
+ };
334
+
335
+ // Break runs into laid-out lines that fit `width`.
336
+ Layout.prototype.wrap = function (runs, width, lineHeightFactor) {
337
+ var lines = [], line = [], used = 0, self = this;
338
+ var maxSize = this.baseSize;
339
+
340
+ function flush() {
341
+ // Trailing spaces must not count toward a line's width, or a
342
+ // right-aligned or centred line drifts by the width of the space.
343
+ while (line.length && /^\s+$/.test(line[line.length - 1].text || "")) line.pop();
344
+ lines.push({ parts: line, width: used, size: maxSize });
345
+ line = []; used = 0; maxSize = self.baseSize;
346
+ }
347
+
348
+ for (var r = 0; r < runs.length; r++) {
349
+ var run = runs[r];
350
+ if (run.br) { flush(); continue; }
351
+ if (run.img) {
352
+ var dims = imageDims(run.img, width);
353
+ if (line.length) flush();
354
+ lines.push({ parts: [{ img: run.img, w: dims.w, h: dims.h, style: run.style }], width: dims.w, size: dims.h, image: true });
355
+ continue;
356
+ }
357
+ var styleIndex = (run.style.bold ? 1 : 0) | (run.style.italic ? 2 : 0);
358
+ var family = run.style.mono ? "courier" : this.family;
359
+ // Split keeping the separators, so a space that ends a line can be
360
+ // dropped while a space between two words on the same line is kept.
361
+ var tokens = run.text.split(/(\s+)/);
362
+ for (var t = 0; t < tokens.length; t++) {
363
+ var tok = tokens[t];
364
+ if (!tok) continue;
365
+ var w = textWidth(toWinAnsi(tok), family, styleIndex, run.style.size);
366
+ if (used + w > width && line.length && !/^\s+$/.test(tok)) {
367
+ flush();
368
+ // A wrapped line never starts with the space that caused it.
369
+ if (/^\s+$/.test(tok)) continue;
370
+ }
371
+ line.push({ text: tok, style: run.style, family: family, styleIndex: styleIndex, w: w });
372
+ used += w;
373
+ if (run.style.size > maxSize) maxSize = run.style.size;
374
+ }
375
+ }
376
+ if (line.length) flush();
377
+ if (!lines.length) lines.push({ parts: [], width: 0, size: this.baseSize });
378
+ for (var i = 0; i < lines.length; i++) lines[i].height = lines[i].image ? lines[i].size : lines[i].size * lineHeightFactor;
379
+ return lines;
380
+ };
381
+
382
+ function imageDims(img, maxWidth) {
383
+ var w = img.naturalWidth || img.width || 0;
384
+ var h = img.naturalHeight || img.height || 0;
385
+ if (!w || !h) return { w: 0, h: 0 };
386
+ var pw = w * 0.75, ph = h * 0.75; // CSS px -> points
387
+ if (pw > maxWidth) { ph = ph * maxWidth / pw; pw = maxWidth; }
388
+ return { w: pw, h: ph };
389
+ }
390
+
391
+ // ------------------------------------------------------------ block walk
392
+ Layout.prototype.run = function (root) {
393
+ var self = this;
394
+ var base = { bold: false, italic: false, underline: false, strike: false, size: this.baseSize, color: [0, 0, 0], mono: false, link: null };
395
+
396
+ function block(node, style, indent, listMarker, listId, listOrdered) {
397
+ var tag = node.tagName.toLowerCase();
398
+ var s = styleOf(node, style);
399
+ var align = "left";
400
+ var ta = (node.style && node.style.textAlign) || "";
401
+ if (ta === "center" || ta === "right" || ta === "justify") align = ta;
402
+
403
+ if (HEADING_SCALE[tag]) { s.size = self.baseSize * HEADING_SCALE[tag]; s.bold = true; }
404
+ if (tag === "pre") s.mono = true;
405
+
406
+ var width = self.contentWidth - indent;
407
+ var runs = self.runsOf(node, s);
408
+ var lines = self.wrap(runs, width, tag === "pre" ? 1.25 : 1.4);
409
+ self.blocks.push({
410
+ type: "text", lines: lines, indent: indent, align: align,
411
+ spaceBefore: HEADING_SCALE[tag] ? s.size * 0.6 : self.baseSize * 0.35,
412
+ spaceAfter: HEADING_SCALE[tag] ? s.size * 0.3 : self.baseSize * 0.35,
413
+ marker: listMarker || null, markerStyle: s,
414
+ // A heading alone at the foot of a page is the classic ugly
415
+ // break; it is pinned to what follows it.
416
+ keepWithNext: !!HEADING_SCALE[tag],
417
+ rule: tag === "blockquote" ? "quote" : null,
418
+ // Semantics for the tag tree. A PDF whose text is selectable but
419
+ // untagged still fails an accessibility audit: assistive
420
+ // technology gets a flat stream with no headings, no list
421
+ // structure and no reliable reading order.
422
+ role: HEADING_SCALE[tag] ? "H" + tag.charAt(1) : (tag === "blockquote" ? "BlockQuote" : "P"),
423
+ listId: listId || null, listOrdered: !!listOrdered
424
+ });
425
+ }
426
+
427
+ function walk(node, style, indent) {
428
+ for (var i = 0; i < node.childNodes.length; i++) {
429
+ var c = node.childNodes[i];
430
+ if (c.nodeType === 3) {
431
+ if (!(c.nodeValue || "").trim()) continue;
432
+ // Loose text directly under the root still deserves a block.
433
+ self.blocks.push({ type: "text", lines: self.wrap([{ text: c.nodeValue.replace(/\s+/g, " "), style: style }], self.contentWidth - indent, 1.4), indent: indent, align: "left", spaceBefore: 0, spaceAfter: self.baseSize * 0.35 });
434
+ continue;
435
+ }
436
+ if (c.nodeType !== 1) continue;
437
+ var tag = c.tagName.toLowerCase();
438
+ if (tag === "script" || tag === "style" || tag === "noscript") continue;
439
+
440
+ if (c.getAttribute && c.getAttribute("data-rte-page-break") !== null) { self.blocks.push({ type: "pagebreak" }); continue; }
441
+ if (c.style && /always|page/.test(c.style.pageBreakBefore || c.style.breakBefore || "")) self.blocks.push({ type: "pagebreak" });
442
+
443
+ if (tag === "hr") { self.blocks.push({ type: "rule", indent: indent }); continue; }
444
+ if (tag === "ul" || tag === "ol") {
445
+ var counter = parseInt(c.getAttribute("start") || "1", 10) || 1;
446
+ // Every item of one list shares an id so the tag tree can
447
+ // rebuild a single <L> around them; without it each item
448
+ // becomes an isolated paragraph and a screen reader never
449
+ // announces "list, 4 items".
450
+ var listId = "L" + (self.listSeq = (self.listSeq || 0) + 1);
451
+ for (var li = 0; li < c.childNodes.length; li++) {
452
+ var item = c.childNodes[li];
453
+ if (item.nodeType !== 1 || item.tagName.toLowerCase() !== "li") continue;
454
+ var marker = tag === "ol" ? (counter++) + "." : "•";
455
+ // The item's own text, then any nested list one level in.
456
+ block(item, styleOf(item, style), indent + self.baseSize * 1.6, marker, listId, tag === "ol");
457
+ walkNestedLists(item, styleOf(item, style), indent + self.baseSize * 1.6);
458
+ }
459
+ continue;
460
+ }
461
+ if (tag === "table") { self.table(c, styleOf(c, style), indent); continue; }
462
+ if (tag === "img") {
463
+ var dims = imageDims(c, self.contentWidth - indent);
464
+ if (dims.w) self.blocks.push({ type: "text", role: "Figure", alt: c.getAttribute("alt") || "", lines: [{ parts: [{ img: c, w: dims.w, h: dims.h, style: style }], width: dims.w, size: dims.h, height: dims.h, image: true }], indent: indent, align: "left", spaceBefore: 4, spaceAfter: 4 });
465
+ continue;
466
+ }
467
+ if (isBlockTag(tag)) {
468
+ if (hasBlockChildren(c)) { walk(c, styleOf(c, style), indent + (tag === "blockquote" ? self.baseSize * 1.5 : 0)); continue; }
469
+ block(c, style, indent + (tag === "blockquote" ? self.baseSize * 1.5 : 0), null);
470
+ continue;
471
+ }
472
+ // Inline content sitting directly under a container.
473
+ block(c, style, indent, null);
474
+ }
475
+ }
476
+
477
+ function walkNestedLists(item, style, indent) {
478
+ for (var i = 0; i < item.childNodes.length; i++) {
479
+ var c = item.childNodes[i];
480
+ if (c.nodeType !== 1) continue;
481
+ var tag = c.tagName.toLowerCase();
482
+ if (tag === "ul" || tag === "ol") walk({ childNodes: [c] }, style, indent);
483
+ }
484
+ }
485
+
486
+ walk(root, base, 0);
487
+ return this.blocks;
488
+ };
489
+
490
+ function isBlockTag(tag) {
491
+ return /^(p|div|h[1-6]|blockquote|pre|section|article|header|footer|main|aside|figure|figcaption|dl|dd|dt|li|address)$/.test(tag);
492
+ }
493
+ function hasBlockChildren(node) {
494
+ for (var i = 0; i < node.childNodes.length; i++) {
495
+ var c = node.childNodes[i];
496
+ if (c.nodeType === 1 && (isBlockTag(c.tagName.toLowerCase()) || /^(ul|ol|table|hr)$/.test(c.tagName.toLowerCase()))) return true;
497
+ }
498
+ return false;
499
+ }
500
+
501
+ Layout.prototype.table = function (node, style, indent) {
502
+ var self = this;
503
+ var rows = [];
504
+ var trs = node.getElementsByTagName("tr");
505
+ var maxCells = 0;
506
+ for (var r = 0; r < trs.length; r++) {
507
+ var cells = [];
508
+ for (var c = 0; c < trs[r].childNodes.length; c++) {
509
+ var cell = trs[r].childNodes[c];
510
+ if (cell.nodeType !== 1) continue;
511
+ var tag = cell.tagName.toLowerCase();
512
+ if (tag !== "td" && tag !== "th") continue;
513
+ cells.push({ node: cell, header: tag === "th" });
514
+ }
515
+ if (cells.length) { rows.push(cells); if (cells.length > maxCells) maxCells = cells.length; }
516
+ }
517
+ if (!rows.length) return;
518
+
519
+ var width = this.contentWidth - indent;
520
+ var colWidth = width / maxCells;
521
+ var pad = 4;
522
+ var laid = [];
523
+ for (var i = 0; i < rows.length; i++) {
524
+ var out = [], tallest = 0;
525
+ for (var j = 0; j < rows[i].length; j++) {
526
+ var cs = styleOf(rows[i][j].node, style);
527
+ if (rows[i][j].header) cs.bold = true;
528
+ var lines = self.wrap(self.runsOf(rows[i][j].node, cs), colWidth - pad * 2, 1.35);
529
+ var h = 0;
530
+ for (var k = 0; k < lines.length; k++) h += lines[k].height;
531
+ if (h > tallest) tallest = h;
532
+ out.push({ lines: lines, header: rows[i][j].header });
533
+ }
534
+ laid.push({ cells: out, height: tallest + pad * 2, header: rows[i][0] && rows[i][0].header });
535
+ }
536
+ this.blocks.push({
537
+ type: "table", rows: laid, colWidth: colWidth, cols: maxCells,
538
+ indent: indent, pad: pad,
539
+ spaceBefore: this.baseSize * 0.5, spaceAfter: this.baseSize * 0.5
540
+ });
541
+ };
542
+
543
+ // ------------------------------------------------------------- rendering
544
+ function buildPdf(options) {
545
+ var familyKey = String(options.font || config.pdfExportFont || "helvetica").toLowerCase();
546
+ if (!FONT_FAMILIES[familyKey]) familyKey = "helvetica";
547
+ var baseSize = Number(options.fontSize || config.pdfExportBaseFontSize || 11);
548
+ if (!isFinite(baseSize) || baseSize <= 0) baseSize = 11;
549
+ var geom = pageGeometry(options);
550
+
551
+ // getHTMLCode() is the SERIALIZED document: presentational plugins strip
552
+ // their own chrome from it. Reading the live DOM instead would bake page
553
+ // overlays, watermarks and formatting marks into the PDF.
554
+ var host = document.createElement("div");
555
+ host.innerHTML = editor.getHTMLCode() || "";
556
+
557
+ var layout = new Layout(geom, baseSize, familyKey);
558
+ var blocks = layout.run(host);
559
+
560
+ var pages = paginate(blocks, geom, baseSize);
561
+ return emit(pages, geom, familyKey, baseSize, options);
562
+ }
563
+
564
+ function paginate(blocks, geom, baseSize) {
565
+ var usableTop = geom.height - geom.margin.top;
566
+ var usableBottom = geom.margin.bottom;
567
+ var pages = [], current = [], y = usableTop;
568
+
569
+ function newPage() { pages.push(current); current = []; y = usableTop; }
570
+
571
+ for (var i = 0; i < blocks.length; i++) {
572
+ var b = blocks[i];
573
+ if (b.type === "pagebreak") { if (current.length) newPage(); continue; }
574
+
575
+ if (b.type === "rule") {
576
+ if (y - baseSize < usableBottom) newPage();
577
+ y -= baseSize * 0.6;
578
+ current.push({ op: "rule", y: y, indent: b.indent });
579
+ y -= baseSize * 0.6;
580
+ continue;
581
+ }
582
+
583
+ if (b.type === "table") {
584
+ y -= b.spaceBefore;
585
+ for (var r = 0; r < b.rows.length; r++) {
586
+ var row = b.rows[r];
587
+ if (y - row.height < usableBottom) {
588
+ newPage();
589
+ // Repeat the header row on the continuation page, or the
590
+ // rest of the table arrives as unlabelled numbers.
591
+ if (b.rows[0] && b.rows[0].header && r > 0) {
592
+ current.push({ op: "row", row: b.rows[0], y: y, block: b });
593
+ y -= b.rows[0].height;
594
+ }
595
+ }
596
+ current.push({ op: "row", row: row, y: y, block: b });
597
+ y -= row.height;
598
+ }
599
+ y -= b.spaceAfter;
600
+ continue;
601
+ }
602
+
603
+ y -= b.spaceBefore;
604
+ for (var l = 0; l < b.lines.length; l++) {
605
+ var line = b.lines[l];
606
+ if (y - line.height < usableBottom) newPage();
607
+ current.push({ op: "line", line: line, y: y, block: b, first: l === 0 });
608
+ y -= line.height;
609
+ }
610
+ y -= b.spaceAfter;
611
+
612
+ // keepWithNext: if a heading ended up as the last thing on the page,
613
+ // move it to the next one rather than orphaning it.
614
+ if (b.keepWithNext && current.length && blocks[i + 1] && blocks[i + 1].type !== "pagebreak") {
615
+ var nextHeight = estimateFirstLine(blocks[i + 1]);
616
+ if (y - nextHeight < usableBottom) {
617
+ var moved = [];
618
+ while (current.length && current[current.length - 1].block === b) moved.unshift(current.pop());
619
+ if (current.length) {
620
+ newPage();
621
+ for (var m = 0; m < moved.length; m++) {
622
+ moved[m].y = y;
623
+ current.push(moved[m]);
624
+ y -= moved[m].line.height;
625
+ }
626
+ } else {
627
+ // Already at the top of a page: nothing to gain by moving.
628
+ for (var m2 = 0; m2 < moved.length; m2++) current.push(moved[m2]);
629
+ }
630
+ }
631
+ }
632
+ }
633
+ if (current.length || !pages.length) pages.push(current);
634
+ return pages;
635
+ }
636
+
637
+ function estimateFirstLine(block) {
638
+ if (block.type === "table") return block.rows[0] ? block.rows[0].height : 0;
639
+ if (block.lines && block.lines[0]) return block.lines[0].height;
640
+ return 0;
641
+ }
642
+
643
+ function emit(pages, geom, familyKey, baseSize, options) {
644
+ var pdf = new PdfWriter();
645
+ var fam = FONT_FAMILIES[familyKey];
646
+ var mono = FONT_FAMILIES.courier;
647
+
648
+ // Font objects: four faces of the body family plus four of Courier, each
649
+ // carrying the widths this layout was computed with.
650
+ var fontIds = {}, fontRes = [];
651
+ function addFont(name, key, styleIndex, alias) {
652
+ var widths = widthsFor(key, styleIndex);
653
+ var id = pdf.add("<< /Type /Font /Subtype /Type1 /BaseFont /" + name +
654
+ " /Encoding /WinAnsiEncoding /FirstChar " + FIRST_CHAR + " /LastChar " + LAST_CHAR +
655
+ " /Widths [" + widths.join(" ") + "] >>");
656
+ fontIds[alias] = id;
657
+ fontRes.push("/" + alias + " " + id + " 0 R");
658
+ }
659
+ for (var s = 0; s < 4; s++) {
660
+ addFont(fam.faces[s], familyKey, s, "F" + s);
661
+ addFont(mono.faces[s], "courier", s, "M" + s);
662
+ }
663
+
664
+ var images = {}, imageRes = [];
665
+ function imageRef(img) {
666
+ var src = img.getAttribute("src") || "";
667
+ if (images[src]) return images[src];
668
+ var data = encodeImage(img);
669
+ if (!data) return null;
670
+ var id = pdf.add({
671
+ dict: "<< /Type /XObject /Subtype /Image /Width " + data.width + " /Height " + data.height +
672
+ " /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode /Length 0 >>",
673
+ stream: data.bytes
674
+ });
675
+ var alias = "Im" + (imageRes.length + 1);
676
+ images[src] = { id: id, alias: alias };
677
+ imageRes.push("/" + alias + " " + id + " 0 R");
678
+ return images[src];
679
+ }
680
+
681
+ var pageIds = [], contentIds = [], annotIds = [];
682
+ for (var p = 0; p < pages.length; p++) {
683
+ pageIds.push(pdf.alloc());
684
+ contentIds.push(pdf.alloc());
685
+ annotIds.push([]);
686
+ }
687
+ var pagesId = pdf.alloc();
688
+
689
+ var allMarks = [];
690
+ for (var pi = 0; pi < pages.length; pi++) {
691
+ var tagger = new Tagger(pi);
692
+ var draw = renderPage(pages[pi], geom, familyKey, baseSize, imageRef, pi + 1, pages.length, options, tagger);
693
+ pdf.set(contentIds[pi], { dict: "<< /Length 0 >>", stream: draw.content });
694
+ allMarks.push(draw.marks);
695
+ for (var a = 0; a < draw.links.length; a++) {
696
+ var lk = draw.links[a];
697
+ annotIds[pi].push(pdf.add("<< /Type /Annot /Subtype /Link /Rect [" +
698
+ fixed(lk.x0) + " " + fixed(lk.y0) + " " + fixed(lk.x1) + " " + fixed(lk.y1) +
699
+ "] /Border [0 0 0] /A << /S /URI /URI (" + escapeString(toWinAnsi(lk.href)) + ") >> >>"));
700
+ }
701
+ }
702
+
703
+ var struct = buildStructTree(pdf, allMarks, pageIds);
704
+
705
+ for (var pj = 0; pj < pages.length; pj++) {
706
+ pdf.set(pageIds[pj], "<< /Type /Page /Parent " + pagesId + " 0 R /MediaBox [0 0 " +
707
+ fixed(geom.width) + " " + fixed(geom.height) + "] /Resources << /Font << " + fontRes.join(" ") + " >>" +
708
+ (imageRes.length ? " /XObject << " + imageRes.join(" ") + " >>" : "") +
709
+ " >> /Contents " + contentIds[pj] + " 0 R /StructParents " + pj +
710
+ (annotIds[pj].length ? " /Annots [" + annotIds[pj].map(function (id) { return id + " 0 R"; }).join(" ") + "]" : "") +
711
+ " >>");
712
+ }
713
+ pdf.set(pagesId, "<< /Type /Pages /Count " + pages.length + " /Kids [" +
714
+ pageIds.map(function (id) { return id + " 0 R"; }).join(" ") + "] >>");
715
+
716
+ var title = options.title || config.pdfExportTitle || document.title || "Document";
717
+ var author = options.author || config.pdfExportAuthor || "";
718
+ // /MarkInfo and /StructTreeRoot declare the file tagged. DisplayDocTitle
719
+ // makes the viewer show the document's title rather than its filename,
720
+ // which PDF/UA requires and which is the difference between a screen
721
+ // reader announcing "Quarterly Report" and "Export-260802-1431.pdf".
722
+ var catalogId = pdf.add("<< /Type /Catalog /Pages " + pagesId + " 0 R" +
723
+ " /Lang (" + escapeString(String(options.lang || document.documentElement.lang || "en")) + ")" +
724
+ " /MarkInfo << /Marked true >>" +
725
+ " /StructTreeRoot " + struct.rootId + " 0 R" +
726
+ " /ViewerPreferences << /DisplayDocTitle true >> >>");
727
+ var infoId = pdf.add("<< /Title (" + escapeString(toWinAnsi(String(title))) + ")" +
728
+ (author ? " /Author (" + escapeString(toWinAnsi(String(author))) + ")" : "") +
729
+ " /Producer (RichTextEditor) /Creator (RichTextEditor) /CreationDate (" + pdfDate(new Date()) + ") >>");
730
+
731
+ return pdf.build(catalogId, infoId);
732
+ }
733
+
734
+ // Turn the per-page marked-content records into a structure tree.
735
+ //
736
+ // The tree is built from the marks rather than from the block list because a
737
+ // single block can be split across pages: its structure element then holds
738
+ // several marked-content references, each naming the page it lives on.
739
+ function buildStructTree(pdf, allMarks, pageIds) {
740
+ var rootId = pdf.alloc();
741
+ var nodes = []; // { id, role, kids, extra, children, parent }
742
+ var parentTree = []; // per page: array of struct ids by MCID
743
+
744
+ function node(role, extra) {
745
+ var n = { id: pdf.alloc(), role: role, kids: [], extra: extra || "", children: [], parent: null };
746
+ nodes.push(n);
747
+ return n;
748
+ }
749
+
750
+ var docNode = node("Document");
751
+ var listNodes = new Map(); // listId -> L node
752
+ var itemNodes = new Map(); // block -> LI node
753
+ var labelNodes = new Map(); // block -> Lbl node
754
+ var bodyNodes = new Map(); // block -> P / LBody / Figure node
755
+ var tableNodes = new Map(); // block -> Table node
756
+ var rowNodes = new Map(); // row -> TR node
757
+ var cellNodes = new Map(); // row -> { col -> TD/TH node }
758
+
759
+ for (var p = 0; p < allMarks.length; p++) {
760
+ var marks = allMarks[p];
761
+ parentTree[p] = [];
762
+ for (var m = 0; m < marks.length; m++) {
763
+ var owner = marks[m].owner, mcid = marks[m].mcid;
764
+ var target = resolve(owner);
765
+ target.kids.push({ page: p, mcid: mcid });
766
+ parentTree[p][mcid] = target.id;
767
+ }
768
+ }
769
+
770
+ // The parent is recorded when the child is attached. Searching for it
771
+ // afterwards would be a scan of every node for every node.
772
+ function attach(parent, child) {
773
+ if (child.parent) return;
774
+ child.parent = parent;
775
+ parent.children.push(child);
776
+ }
777
+
778
+ function resolve(owner) {
779
+ if (owner.kind === "cell") {
780
+ var table = tableNodes.get(owner.block);
781
+ if (!table) { table = node("Table"); tableNodes.set(owner.block, table); attach(docNode, table); }
782
+ var row = rowNodes.get(owner.row);
783
+ if (!row) { row = node("TR"); rowNodes.set(owner.row, row); attach(table, row); }
784
+ var perRow = cellNodes.get(owner.row);
785
+ if (!perRow) { perRow = {}; cellNodes.set(owner.row, perRow); }
786
+ if (!perRow[owner.col]) {
787
+ // A header cell must be TH, not TD: it is what lets a screen
788
+ // reader announce "Revenue, 1,240" instead of just "1,240".
789
+ perRow[owner.col] = node(owner.header ? "TH" : "TD", owner.header ? " /Scope /Column" : "");
790
+ attach(row, perRow[owner.col]);
791
+ }
792
+ return perRow[owner.col];
793
+ }
794
+
795
+ var block = owner.block;
796
+ var container = docNode;
797
+ if (block.listId) {
798
+ var listNode = listNodes.get(block.listId);
799
+ if (!listNode) { listNode = node("L"); listNodes.set(block.listId, listNode); attach(docNode, listNode); }
800
+ var itemNode = itemNodes.get(block);
801
+ if (!itemNode) { itemNode = node("LI"); itemNodes.set(block, itemNode); attach(listNode, itemNode); }
802
+ container = itemNode;
803
+ }
804
+
805
+ if (owner.kind === "lbl") {
806
+ var lbl = labelNodes.get(block);
807
+ if (!lbl) { lbl = node("Lbl"); labelNodes.set(block, lbl); attach(container, lbl); }
808
+ return lbl;
809
+ }
810
+
811
+ var body = bodyNodes.get(block);
812
+ if (!body) {
813
+ if (block.listId) {
814
+ body = node("LBody");
815
+ } else {
816
+ // A figure with no alt text is an accessibility failure, but
817
+ // an EMPTY /Alt is a worse one: it tells assistive tech the
818
+ // image is decorative when nobody decided that.
819
+ var extra = block.role === "Figure"
820
+ ? " /Alt (" + escapeString(toWinAnsi(block.alt || "Image")) + ")"
821
+ : "";
822
+ body = node(block.role || "P", extra);
823
+ }
824
+ bodyNodes.set(block, body);
825
+ attach(container, body);
826
+ }
827
+ return body;
828
+ }
829
+
830
+ // Emit. /K holds child elements first, then marked-content references.
831
+ for (var n = 0; n < nodes.length; n++) {
832
+ var nd = nodes[n];
833
+ var kids = [];
834
+ for (var c = 0; c < nd.children.length; c++) kids.push(nd.children[c].id + " 0 R");
835
+ for (var k = 0; k < nd.kids.length; k++) {
836
+ kids.push("<< /Type /MCR /Pg " + pageIds[nd.kids[k].page] + " 0 R /MCID " + nd.kids[k].mcid + " >>");
837
+ }
838
+ var parentRef = nd.parent ? nd.parent.id : rootId;
839
+ pdf.set(nd.id, "<< /Type /StructElem /S /" + nd.role + " /P " + parentRef + " 0 R" + nd.extra +
840
+ (kids.length ? " /K [" + kids.join(" ") + "]" : "") + " >>");
841
+ }
842
+
843
+ // ParentTree: a number tree keyed by each page's /StructParents value,
844
+ // whose value is the list of owning structure elements indexed by MCID.
845
+ var numsParts = [];
846
+ for (var pt = 0; pt < parentTree.length; pt++) {
847
+ var arr = parentTree[pt] || [];
848
+ var refs = [];
849
+ for (var q = 0; q < arr.length; q++) refs.push((arr[q] || docNode.id) + " 0 R");
850
+ numsParts.push(pt + " [" + refs.join(" ") + "]");
851
+ }
852
+ var parentTreeId = pdf.add("<< /Nums [" + numsParts.join(" ") + "] >>");
853
+
854
+ pdf.set(rootId, "<< /Type /StructTreeRoot /K [" + docNode.id + " 0 R]" +
855
+ " /ParentTree " + parentTreeId + " 0 R /ParentTreeNextKey " + parentTree.length + " >>");
856
+
857
+ return { rootId: rootId };
858
+ }
859
+
860
+ function pdfDate(d) {
861
+ function two(n) { return (n < 10 ? "0" : "") + n; }
862
+ return "D:" + d.getFullYear() + two(d.getMonth() + 1) + two(d.getDate()) +
863
+ two(d.getHours()) + two(d.getMinutes()) + two(d.getSeconds()) + "Z";
864
+ }
865
+ function fixed(n) { return (Math.round(n * 100) / 100).toString(); }
866
+
867
+ // Marked content for the structure tree.
868
+ //
869
+ // Tagging is what separates "a screen reader can read the words" from "a
870
+ // screen reader knows this is a level-2 heading, followed by a 4-item list,
871
+ // followed by a table with header cells". Section 508 and EN 301 549 both
872
+ // require the second. Purely decorative output (rules, cell borders, the
873
+ // running header) is marked as an Artifact instead, which removes it from
874
+ // the reading order rather than reading "line, line, line" to the user.
875
+ function Tagger(pageIndex) {
876
+ this.pageIndex = pageIndex;
877
+ this.next = 0;
878
+ this.marks = []; // { owner, mcid } in reading order
879
+ }
880
+ Tagger.prototype.open = function (out, role, owner) {
881
+ var mcid = this.next++;
882
+ this.marks.push({ owner: owner, mcid: mcid, role: role });
883
+ out.push("/" + role + " << /MCID " + mcid + " >> BDC");
884
+ return mcid;
885
+ };
886
+ Tagger.prototype.close = function (out) { out.push("EMC"); };
887
+ Tagger.prototype.artifact = function (out, fn) {
888
+ out.push("/Artifact BMC");
889
+ fn();
890
+ out.push("EMC");
891
+ };
892
+
893
+ function renderPage(ops, geom, familyKey, baseSize, imageRef, pageNumber, pageCount, options, tagger) {
894
+ var out = [], links = [];
895
+ var left = geom.margin.left;
896
+
897
+ for (var i = 0; i < ops.length; i++) {
898
+ var op = ops[i];
899
+ if (op.op === "rule") {
900
+ // A horizontal rule is decoration, not content.
901
+ tagger.artifact(out, function () {
902
+ out.push("0.8 0.8 0.8 RG 0.5 w " + fixed(left + op.indent) + " " + fixed(op.y) + " m " +
903
+ fixed(geom.width - geom.margin.right) + " " + fixed(op.y) + " l S");
904
+ });
905
+ continue;
906
+ }
907
+ if (op.op === "row") { renderRow(op, geom, out, left, imageRef, links, tagger); continue; }
908
+ renderLine(op, geom, out, left, familyKey, imageRef, links, tagger);
909
+ }
910
+
911
+ // Running header/footer, drawn after the body so they always sit on top.
912
+ // Both are artifacts: repeating them into the reading order on every
913
+ // page is one of the most common findings in a PDF accessibility audit.
914
+ var hf = headerFooter(options);
915
+ if (hf.header || hf.footer) {
916
+ tagger.artifact(out, function () {
917
+ if (hf.header) drawRunning(out, hf.header, geom, geom.height - geom.margin.top * 0.55, familyKey, baseSize * 0.85, pageNumber, pageCount);
918
+ if (hf.footer) drawRunning(out, hf.footer, geom, geom.margin.bottom * 0.55, familyKey, baseSize * 0.85, pageNumber, pageCount);
919
+ });
920
+ }
921
+
922
+ return { content: out.join("\n"), links: links, marks: tagger.marks };
923
+ }
924
+
925
+ function headerFooter(options) {
926
+ var setup = null;
927
+ try { if (typeof editor.getDocumentPageSetup === "function") setup = editor.getDocumentPageSetup(); } catch (e) {}
928
+ setup = setup || {};
929
+ return {
930
+ header: options.headerText || config.pdfExportHeader || plainText(setup.headerHtml) || "",
931
+ footer: options.footerText || config.pdfExportFooter || plainText(setup.footerHtml) || ""
932
+ };
933
+ }
934
+ function plainText(html) {
935
+ if (!html || typeof html !== "string") return "";
936
+ var d = document.createElement("div");
937
+ d.innerHTML = html;
938
+ return (d.textContent || "").replace(/\s+/g, " ").trim();
939
+ }
940
+
941
+ function drawRunning(out, template, geom, y, familyKey, size, pageNumber, pageCount) {
942
+ var text = String(template).replace(/\{page\}/g, pageNumber).replace(/\{total\}/g, pageCount);
943
+ var win = toWinAnsi(text);
944
+ var w = textWidth(win, familyKey, 0, size);
945
+ var x = (geom.width - w) / 2;
946
+ out.push("BT /F0 " + fixed(size) + " Tf 0.4 0.4 0.4 rg " + fixed(x) + " " + fixed(y) + " Td (" + escapeString(win) + ") Tj ET");
947
+ }
948
+
949
+ function renderLine(op, geom, out, left, familyKey, imageRef, links, tagger) {
950
+ var line = op.line, block = op.block;
951
+ var x = left + (block.indent || 0);
952
+ var avail = geom.width - geom.margin.right - x;
953
+ if (block.align === "center") x += (avail - line.width) / 2;
954
+ else if (block.align === "right") x += avail - line.width;
955
+
956
+ if (block.rule === "quote" && !line.image) {
957
+ // The quote bar is decoration.
958
+ var barX = fixed(left + block.indent - 8);
959
+ var run = function () {
960
+ out.push("0.8 0.8 0.85 RG 2 w " + barX + " " + fixed(op.y - line.height + 4) +
961
+ " m " + barX + " " + fixed(op.y + line.size * 0.8) + " l S");
962
+ };
963
+ if (tagger) tagger.artifact(out, run); else run();
964
+ }
965
+
966
+ // A list marker belongs to the FIRST line only; repeating it on a wrapped
967
+ // continuation line turns one bullet into several.
968
+ if (op.first && block.marker) {
969
+ var ms = block.markerStyle || { size: line.size, color: [0, 0, 0] };
970
+ var mw = textWidth(block.marker, familyKey, 0, ms.size);
971
+ if (tagger) tagger.open(out, "Lbl", { kind: "lbl", block: block });
972
+ out.push("BT /F0 " + fixed(ms.size) + " Tf " + colorOp(ms.color) + " " +
973
+ fixed(x - mw - 6) + " " + fixed(op.y - line.size * 0.85) + " Td (" + escapeString(toWinAnsi(block.marker)) + ") Tj ET");
974
+ if (tagger) tagger.close(out);
975
+ }
976
+
977
+ // Text of the block itself. Figures carry /Alt on the structure element
978
+ // rather than in the content stream.
979
+ //
980
+ // A table cell renders through this function with a SYNTHETIC block
981
+ // (just indent/align), so the owner has to be the cell descriptor the
982
+ // caller supplied. Passing the synthetic block instead makes every cell
983
+ // a top-level paragraph and the table loses its structure entirely.
984
+ if (tagger) tagger.open(out, line.image ? "Figure" : "Span", op.cell || { kind: "body", block: block });
985
+
986
+ // Wrapping split the text into words, but emitting one BT/Tj block per
987
+ // WORD is wrong twice over: the content stream balloons, and text
988
+ // extractors that infer spacing from separate show-operations can lose
989
+ // the spaces between them, so copying a paragraph out of the PDF yields
990
+ // "onelongrunofwords". Adjacent parts that share a font, size and colour
991
+ // are merged back into a single run, spaces included.
992
+ var segments = [];
993
+ var cursor = x;
994
+ for (var i = 0; i < line.parts.length; i++) {
995
+ var part = line.parts[i];
996
+ if (part.img) { segments.push({ img: part, x: cursor, w: part.w }); cursor += part.w; continue; }
997
+ var st = part.style;
998
+ var alias = (st.mono ? "M" : "F") + part.styleIndex;
999
+ var last = segments.length ? segments[segments.length - 1] : null;
1000
+ var mergeable = last && !last.img && last.alias === alias && last.style.size === st.size &&
1001
+ sameColor(last.style.color, st.color) && last.style.underline === st.underline &&
1002
+ last.style.strike === st.strike && last.style.link === st.link;
1003
+ if (mergeable) { last.text += part.text; last.w += part.w; }
1004
+ else { segments.push({ text: part.text, style: st, alias: alias, x: cursor, w: part.w }); }
1005
+ cursor += part.w;
1006
+ }
1007
+
1008
+ var baseline = op.y - line.size * 0.85;
1009
+ for (var s = 0; s < segments.length; s++) {
1010
+ var seg = segments[s];
1011
+ if (seg.img) {
1012
+ var ref = imageRef(seg.img.img);
1013
+ if (ref) {
1014
+ out.push("q " + fixed(seg.w) + " 0 0 " + fixed(seg.img.h) + " " + fixed(seg.x) + " " +
1015
+ fixed(op.y - seg.img.h) + " cm /" + ref.alias + " Do Q");
1016
+ }
1017
+ continue;
1018
+ }
1019
+ // A run of pure whitespace still advanced the cursor; drawing it
1020
+ // would only add an empty operation.
1021
+ if (!seg.text || /^\s+$/.test(seg.text)) continue;
1022
+ var sst = seg.style;
1023
+ out.push("BT /" + seg.alias + " " + fixed(sst.size) + " Tf " + colorOp(sst.color) + " " +
1024
+ fixed(seg.x) + " " + fixed(baseline) + " Td (" + escapeString(toWinAnsi(seg.text)) + ") Tj ET");
1025
+ if (sst.underline) {
1026
+ out.push(colorStroke(sst.color) + " 0.5 w " + fixed(seg.x) + " " + fixed(baseline - sst.size * 0.12) +
1027
+ " m " + fixed(seg.x + seg.w) + " " + fixed(baseline - sst.size * 0.12) + " l S");
1028
+ }
1029
+ if (sst.strike) {
1030
+ out.push(colorStroke(sst.color) + " 0.5 w " + fixed(seg.x) + " " + fixed(baseline + sst.size * 0.28) +
1031
+ " m " + fixed(seg.x + seg.w) + " " + fixed(baseline + sst.size * 0.28) + " l S");
1032
+ }
1033
+ if (sst.link) {
1034
+ links.push({ href: sst.link, x0: seg.x, y0: baseline - 2, x1: seg.x + seg.w, y1: baseline + sst.size });
1035
+ }
1036
+ }
1037
+ if (tagger) tagger.close(out);
1038
+ }
1039
+
1040
+ function sameColor(a, b) {
1041
+ a = a || [0, 0, 0]; b = b || [0, 0, 0];
1042
+ return a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
1043
+ }
1044
+
1045
+ function renderRow(op, geom, out, left, imageRef, links, tagger) {
1046
+ var block = op.block, row = op.row;
1047
+ var x = left + block.indent;
1048
+ var top = op.y, bottom = op.y - row.height;
1049
+
1050
+ for (var c = 0; c < row.cells.length; c++) {
1051
+ var cellX = x + c * block.colWidth;
1052
+ // Shading and borders are decoration; only the cell TEXT belongs to
1053
+ // the reading order.
1054
+ var paint = function () {
1055
+ if (row.header) {
1056
+ out.push("0.94 0.95 0.97 rg " + fixed(cellX) + " " + fixed(bottom) + " " +
1057
+ fixed(block.colWidth) + " " + fixed(row.height) + " re f");
1058
+ }
1059
+ out.push("0.75 0.78 0.82 RG 0.5 w " + fixed(cellX) + " " + fixed(bottom) + " " +
1060
+ fixed(block.colWidth) + " " + fixed(row.height) + " re S");
1061
+ };
1062
+ if (tagger) tagger.artifact(out, paint); else paint();
1063
+
1064
+ // One structure element per CELL, identified by the row object and
1065
+ // column index so a table split across pages still resolves to the
1066
+ // right TD (and a repeated header row to the right TH).
1067
+ var cellOwner = { kind: "cell", block: block, row: row, col: c, header: !!row.cells[c].header };
1068
+ var cy = top - block.pad;
1069
+ var lines = row.cells[c].lines;
1070
+ for (var l = 0; l < lines.length; l++) {
1071
+ renderLine({ line: lines[l], y: cy, block: { indent: 0, align: "left" }, first: l === 0, cell: cellOwner },
1072
+ { width: cellX + block.colWidth + geom.margin.right, margin: { right: geom.margin.right } },
1073
+ out, cellX + block.pad, "helvetica", imageRef, links, tagger);
1074
+ cy -= lines[l].height;
1075
+ }
1076
+ }
1077
+ }
1078
+
1079
+ function colorOp(c) { c = c || [0, 0, 0]; return fixed(c[0]) + " " + fixed(c[1]) + " " + fixed(c[2]) + " rg"; }
1080
+ function colorStroke(c) { c = c || [0, 0, 0]; return fixed(c[0]) + " " + fixed(c[1]) + " " + fixed(c[2]) + " RG"; }
1081
+
1082
+ // Every image is re-encoded to JPEG through a canvas. PNG could be embedded
1083
+ // as FlateDecode without re-encoding, but only for a narrow set of colour
1084
+ // types and never with an alpha channel; routing everything through one path
1085
+ // means GIF, WEBP, BMP and SVG all work rather than only some of them.
1086
+ // Transparent pixels are composited onto white, which is what the page is.
1087
+ function encodeImage(img) {
1088
+ try {
1089
+ var w = img.naturalWidth || img.width, h = img.naturalHeight || img.height;
1090
+ if (!w || !h) return null;
1091
+ var max = 2000;
1092
+ var scale = Math.min(1, max / Math.max(w, h));
1093
+ var cw = Math.max(1, Math.round(w * scale)), ch = Math.max(1, Math.round(h * scale));
1094
+ var canvas = document.createElement("canvas");
1095
+ canvas.width = cw; canvas.height = ch;
1096
+ var g = canvas.getContext("2d");
1097
+ g.fillStyle = "#ffffff";
1098
+ g.fillRect(0, 0, cw, ch);
1099
+ g.drawImage(img, 0, 0, cw, ch);
1100
+ var uri = canvas.toDataURL("image/jpeg", 0.92);
1101
+ var comma = uri.indexOf(",");
1102
+ if (comma < 0) return null;
1103
+ var binary = atob(uri.slice(comma + 1));
1104
+ var bytes = new Uint8Array(binary.length);
1105
+ for (var i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
1106
+ return { bytes: bytes, width: cw, height: ch };
1107
+ } catch (e) {
1108
+ // A cross-origin image taints the canvas. Skipping it is correct:
1109
+ // there is no way to read its pixels, and failing the whole export
1110
+ // over one decorative image would be worse.
1111
+ return null;
1112
+ }
1113
+ }
1114
+
1115
+ // ------------------------------------------------------------- downloads
1116
+ function sanitizeName(name) {
1117
+ if (!name || typeof name !== "string") return "";
1118
+ return name.replace(/\.pdf$/i, "").replace(/[\\/:*?"<>|]+/g, "").trim().slice(0, 120);
1119
+ }
1120
+ function defaultBase() {
1121
+ var d = new Date();
1122
+ function two(n) { return (n < 10 ? "0" : "") + n; }
1123
+ return "Export-" + String(d.getFullYear()).slice(2) + two(d.getMonth() + 1) + two(d.getDate()) +
1124
+ "-" + two(d.getHours()) + two(d.getMinutes()) + two(d.getSeconds());
1125
+ }
1126
+ function download(bytes, filename) {
1127
+ var blob = new Blob([bytes], { type: "application/pdf" });
1128
+ var url = URL.createObjectURL(blob);
1129
+ var a = document.createElement("a");
1130
+ a.href = url; a.download = filename;
1131
+ document.body.appendChild(a);
1132
+ a.click();
1133
+ document.body.removeChild(a);
1134
+ setTimeout(function () { URL.revokeObjectURL(url); }, 4000);
1135
+ return filename;
1136
+ }
1137
+ }