docxodus 12.4.1 → 12.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +78 -0
  2. package/dist/core.d.ts +4 -4
  3. package/dist/core.d.ts.map +1 -1
  4. package/dist/core.js +21 -200
  5. package/dist/core.js.map +1 -1
  6. package/dist/docxodus.worker.js +280 -7
  7. package/dist/docxodus.worker.js.map +1 -1
  8. package/dist/editor-reconcile.d.ts +5 -0
  9. package/dist/editor-reconcile.d.ts.map +1 -1
  10. package/dist/editor-reconcile.js.map +1 -1
  11. package/dist/editor.bundle.js +157 -15
  12. package/dist/editor.d.ts +44 -0
  13. package/dist/editor.d.ts.map +1 -1
  14. package/dist/editor.js +209 -22
  15. package/dist/editor.js.map +1 -1
  16. package/dist/embed.bundle.js +560 -217
  17. package/dist/embed.iife.js +560 -217
  18. package/dist/export-assets.json +20 -20
  19. package/dist/export-browser.bundle.js +101 -4
  20. package/dist/external-annotation-wire.d.ts +19 -0
  21. package/dist/external-annotation-wire.d.ts.map +1 -0
  22. package/dist/external-annotation-wire.js +139 -0
  23. package/dist/external-annotation-wire.js.map +1 -0
  24. package/dist/session.bundle.js +258 -7
  25. package/dist/session.d.ts +61 -12
  26. package/dist/session.d.ts.map +1 -1
  27. package/dist/session.js +234 -7
  28. package/dist/session.js.map +1 -1
  29. package/dist/types.d.ts +371 -4
  30. package/dist/types.d.ts.map +1 -1
  31. package/dist/types.js.map +1 -1
  32. package/dist/verification-request.d.ts +9 -0
  33. package/dist/verification-request.d.ts.map +1 -0
  34. package/dist/verification-request.js +37 -0
  35. package/dist/verification-request.js.map +1 -0
  36. package/dist/wasm/_framework/Docxodus.wasm +0 -0
  37. package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
  38. package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
  39. package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
  40. package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
  41. package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
  42. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
  43. package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
  44. package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
  45. package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
  46. package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
  47. package/dist/wasm/_framework/System.Text.Encodings.Web.wasm.br +0 -0
  48. package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
  49. package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
  50. package/dist/wasm/_framework/dotnet.boot.js +9 -9
  51. package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
  52. package/dist/wasm/_framework/dotnet.native.wasm +0 -0
  53. package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
  54. package/dist/worker-proxy.bundle.js +101 -4
  55. package/dist/worker-proxy.d.ts +21 -3
  56. package/dist/worker-proxy.d.ts.map +1 -1
  57. package/dist/worker-proxy.js +57 -2
  58. package/dist/worker-proxy.js.map +1 -1
  59. package/package.json +1 -1
package/dist/editor.js CHANGED
@@ -97,6 +97,30 @@ function ensureBlockDragStyles(doc) {
97
97
  `;
98
98
  (doc.head ?? doc.documentElement).appendChild(style);
99
99
  }
100
+ /**
101
+ * Let the event loop run — input, rendering, and any other task — before the next window
102
+ * of a mount. `scheduler.yield` where the browser has it (it keeps the continuation ahead of
103
+ * other tasks), otherwise a frame followed by a macrotask.
104
+ */
105
+ /**
106
+ * Give the page a turn between mount windows: a frame, so a paint and any input handling
107
+ * happen, then a macrotask, so the host's own timers run too. `scheduler.yield()` is not used
108
+ * on purpose — its continuation is scheduled ahead of timer tasks, so a progress spinner or a
109
+ * poll the host drives with `setInterval` would not tick until the whole mount was done.
110
+ */
111
+ /** The anchor of the empty paragraphs a chrome render leaves where body content was
112
+ * (`HtmlConversionOps.ChromeCarrierAnchor`); a windowed mount removes them before it fills
113
+ * the sections. */
114
+ const CHROME_CARRIER_ANCHOR = "chrome-carrier";
115
+ function yieldToEventLoop() {
116
+ return new Promise((resolve) => {
117
+ const settle = () => { setTimeout(resolve, 0); };
118
+ if (typeof requestAnimationFrame === "function")
119
+ requestAnimationFrame(settle);
120
+ else
121
+ settle();
122
+ });
123
+ }
100
124
  function trackedChangeWireName(mode) {
101
125
  if (mode === TrackedChangeMode.RenderInline)
102
126
  return "render_inline";
@@ -1007,7 +1031,72 @@ export class DocxEditor {
1007
1031
  }
1008
1032
  /** Open a document, render it into `container`, and wire up editing. */
1009
1033
  static open(container, bytes, exports, options = {}) {
1010
- const opts = {
1034
+ const opts = DocxEditor.resolveOptions(options);
1035
+ const editor = DocxEditor.openSession(container, bytes, exports, opts);
1036
+ try {
1037
+ editor.refreshAnchorMap();
1038
+ if (opts.headerFooter)
1039
+ editor.createRegion();
1040
+ // First paint goes through the session-attached editor render when the bundle has it, so
1041
+ // the comment markup (and everything else in the profile) is exactly what a remount will
1042
+ // produce; older bundles take the bytes path with the same profile.
1043
+ const fullHtml = editor.renderFullHtml(bytes);
1044
+ if (opts.paginated)
1045
+ editor.mountPaginated(fullHtml);
1046
+ else
1047
+ editor.mountHtml(fullHtml);
1048
+ editor.finishMount();
1049
+ return editor;
1050
+ }
1051
+ catch (error) {
1052
+ editor.close();
1053
+ throw error;
1054
+ }
1055
+ }
1056
+ /**
1057
+ * Open a document without holding the main thread for the whole mount (issue #776).
1058
+ *
1059
+ * {@link open} renders and wires the entire document in one synchronous task, which on a
1060
+ * long document is the largest single block of a reading-and-editing session. This variant
1061
+ * pays only the session open up front, then mounts the document in windows of body units,
1062
+ * yielding to the event loop between them: the chrome — stylesheet, section wrappers with
1063
+ * their page geometry, footnote and endnote sections — comes from one cheap engine render
1064
+ * that sees no body content, and each window from the engine's block renderer, which lays
1065
+ * units out exactly as the full render does (the plan's border-box grouping is never split).
1066
+ * The DOM that results is the one {@link open} produces; blocks already mounted are editable
1067
+ * while later windows are still landing, and edits made meanwhile are honoured because every
1068
+ * window renders from the live session.
1069
+ *
1070
+ * Paginated mounts assemble the windows off-screen and paginate once at the end, since
1071
+ * pagination is a one-shot flow over the whole document; the main thread is still free
1072
+ * between windows, and `onProgress` lets a host show the fill.
1073
+ *
1074
+ * A bundle that predates the windowed renders mounts synchronously, exactly like {@link open}.
1075
+ */
1076
+ static async openAsync(container, bytes, exports, options = {}) {
1077
+ const bridge = exports.DocxSessionBridge;
1078
+ if (typeof bridge.RenderEditorChromeHtml !== "function"
1079
+ || typeof bridge.RenderEditorRangeHtml !== "function"
1080
+ || typeof bridge.ListRenderedBlocks !== "function") {
1081
+ return DocxEditor.open(container, bytes, exports, options);
1082
+ }
1083
+ const opts = DocxEditor.resolveOptions(options);
1084
+ const editor = DocxEditor.openSession(container, bytes, exports, opts);
1085
+ try {
1086
+ editor.refreshAnchorMap();
1087
+ if (opts.headerFooter)
1088
+ editor.createRegion();
1089
+ await editor.mountWindowed(Math.max(1, Math.floor(options.windowSize ?? 24)), options.onProgress);
1090
+ editor.finishMount();
1091
+ return editor;
1092
+ }
1093
+ catch (error) {
1094
+ editor.close();
1095
+ throw error;
1096
+ }
1097
+ }
1098
+ static resolveOptions(options) {
1099
+ return {
1011
1100
  cssPrefix: options.cssPrefix ?? "docx-",
1012
1101
  fabricateClasses: options.fabricateClasses ?? false,
1013
1102
  editable: options.editable ?? true,
@@ -1026,6 +1115,8 @@ export class DocxEditor {
1026
1115
  onStoryChange: options.onStoryChange,
1027
1116
  onCommentsChange: options.onCommentsChange,
1028
1117
  };
1118
+ }
1119
+ static openSession(container, bytes, exports, opts) {
1029
1120
  // NOT persistAnchorIds: that setting applies to every Save on the session, so it put the
1030
1121
  // projector's Unid bookkeeping into the bytes the USER downloads — ~6x the file size for
1031
1122
  // attributes no renderer reads. Only the remount's re-render needs id stability across a
@@ -1037,28 +1128,124 @@ export class DocxEditor {
1037
1128
  trackedChanges: trackedChangeWireName(opts.trackedChanges),
1038
1129
  revisionAuthor: opts.revisionAuthor,
1039
1130
  }));
1040
- const editor = new DocxEditor(container, exports, handle, opts);
1041
- try {
1042
- editor.refreshAnchorMap();
1043
- if (opts.headerFooter)
1044
- editor.createRegion();
1045
- // First paint goes through the session-attached editor render when the bundle has it, so
1046
- // the comment markup (and everything else in the profile) is exactly what a remount will
1047
- // produce; older bundles take the bytes path with the same profile.
1048
- const fullHtml = editor.renderFullHtml(bytes);
1049
- if (opts.paginated)
1050
- editor.mountPaginated(fullHtml);
1051
- else
1052
- editor.mountHtml(fullHtml);
1053
- editor.syncRegionToBody();
1054
- editor.setupBlockDrag();
1055
- if (opts.comments)
1056
- editor.createGutter();
1057
- return editor;
1131
+ return new DocxEditor(container, exports, handle, opts);
1132
+ }
1133
+ /** The steps every mount ends with once the body is in the DOM. */
1134
+ finishMount() {
1135
+ this.syncRegionToBody();
1136
+ this.setupBlockDrag();
1137
+ if (this.options.comments)
1138
+ this.createGutter();
1139
+ }
1140
+ /**
1141
+ * The windowed mount behind {@link openAsync}: chrome first, then body units in plan order,
1142
+ * a group-aligned window per task. See {@link openAsync} for the contract.
1143
+ */
1144
+ async mountWindowed(windowSize, onProgress) {
1145
+ const bridge = this.exports.DocxSessionBridge;
1146
+ // The full render stamps source anchor ids on every unit; the windows must too, or the DOM
1147
+ // would differ from open()'s by exactly that attribute.
1148
+ const profile = JSON.stringify({ ...JSON.parse(this.editorRenderProfile()), stampAnchors: true });
1149
+ const chrome = bridge.RenderEditorChromeHtml(this.handle, profile);
1150
+ if (chrome.trimStart().startsWith("{")) {
1151
+ throw new Error(JSON.parse(chrome).error ?? "chrome render failed");
1152
+ }
1153
+ const parsed = new DOMParser().parseFromString(chrome, "text/html");
1154
+ // The chrome renders an empty carrier paragraph where each section break and note reference
1155
+ // was; those are not units and go before anything lands. Everything else inside a section
1156
+ // wrapper is chrome that stays — page view puts the endnotes section in the last one.
1157
+ parsed.body.querySelectorAll(`[data-section-index] > [data-anchor="${CHROME_CARRIER_ANCHOR}"]`)
1158
+ .forEach((el) => el.remove());
1159
+ const styles = Array.from(parsed.querySelectorAll("style")).map((s) => s.outerHTML).join("");
1160
+ const flow = document.createElement("div");
1161
+ flow.className = "docx-body-flow";
1162
+ flow.innerHTML = parsed.body.innerHTML;
1163
+ const hosts = new Map();
1164
+ flow.querySelectorAll("[data-section-index]").forEach((el) => hosts.set(Number(el.getAttribute("data-section-index")), el));
1165
+ const attachNow = !this.options.paginated;
1166
+ // A flow mount wires blocks as they land — the chrome's own (footnote and endnote
1167
+ // paragraphs) now, each window's as it arrives — the way open() wires everything under
1168
+ // the flow. A paginated mount hands the paginator unwired HTML and wires the page clones
1169
+ // afterwards, exactly as open() does; wiring first would leave the paginator's
1170
+ // contenteditable="false" stamp on every band paragraph.
1171
+ const wireNow = attachNow && this.options.editable;
1172
+ if (wireNow)
1173
+ this.wireBlocks(flow);
1174
+ if (attachNow) {
1175
+ this.region?.detachPages();
1176
+ this.container.innerHTML = styles;
1177
+ this.readoptGutter();
1178
+ this.container.appendChild(flow);
1179
+ this.editRoot = flow;
1058
1180
  }
1059
- catch (error) {
1060
- editor.close();
1061
- throw error;
1181
+ const plan = JSON.parse(this.renderPlanJson());
1182
+ if (plan.error)
1183
+ throw new Error(plan.error);
1184
+ const units = plan.body;
1185
+ const sectionOf = new Map(units.map((u) => [unidOf(u.id), u.section ?? 0]));
1186
+ const mounted = new Set();
1187
+ const place = (el, sameHostAs) => {
1188
+ // Per-block converter output carries the XHTML xmlns; a full render only has it on the
1189
+ // document root, and open()'s DOM is the contract.
1190
+ el.removeAttribute("xmlns");
1191
+ const unit = el.hasAttribute("data-anchor") ? el : el.querySelector("[data-anchor]");
1192
+ const unid = unit?.getAttribute("data-anchor") ?? "";
1193
+ const host = sameHostAs ?? hosts.get(sectionOf.get(unid) ?? 0) ?? hosts.values().next().value ?? flow;
1194
+ // Units precede the chrome a section wrapper already holds (page view's endnotes section).
1195
+ host.insertBefore(el, host.querySelector(":scope > section.endnotes"));
1196
+ if (wireNow) {
1197
+ // wireBlocks wires a root's descendants; the node itself may be the unit.
1198
+ if (el.hasAttribute("data-anchor"))
1199
+ this.wireBlock(el);
1200
+ this.wireBlocks(el);
1201
+ }
1202
+ unit && mounted.add(unid);
1203
+ el.querySelectorAll("[data-anchor]").forEach((n) => mounted.add(n.getAttribute("data-anchor")));
1204
+ return host;
1205
+ };
1206
+ for (let start = 0; start < units.length;) {
1207
+ let end = Math.min(start + windowSize, units.length);
1208
+ // Never cut through a border box: extend to the end of the group the window stopped in.
1209
+ while (end < units.length && units[end].group !== undefined && units[end].group === units[end - 1].group)
1210
+ end++;
1211
+ const window = units.slice(start, end);
1212
+ const rendered = JSON.parse(bridge.RenderEditorRangeHtml(this.handle, JSON.stringify(window.map((u) => u.id)), profile));
1213
+ if (!Array.isArray(rendered))
1214
+ throw new Error(rendered.error ?? "window render failed");
1215
+ for (const html of rendered) {
1216
+ // One rendered node can parse into several elements: the HTML parser closes a paragraph
1217
+ // at a block-level child (page view's page-break marker div), exactly as it does for the
1218
+ // full render's string, so every element it produced lands in order in the unit's section.
1219
+ let host;
1220
+ for (const el of Array.from(new DOMParser().parseFromString(html, "text/html").body.children)) {
1221
+ host = place(el, host);
1222
+ }
1223
+ }
1224
+ // A unit the range render could not place (a box that would have straddled the window)
1225
+ // still lands, on its own, through the per-block renderer.
1226
+ for (const unit of window) {
1227
+ if (mounted.has(unidOf(unit.id)))
1228
+ continue;
1229
+ const el = this.renderInto(unit.id);
1230
+ if (el)
1231
+ place(el);
1232
+ }
1233
+ start = end;
1234
+ onProgress?.(start, units.length);
1235
+ if (start < units.length)
1236
+ await yieldToEventLoop();
1237
+ }
1238
+ if (attachNow) {
1239
+ this.stampPlanState();
1240
+ if (this.region)
1241
+ this.dockBands(flow);
1242
+ this.viewport.attach(flow, true);
1243
+ }
1244
+ else {
1245
+ // Pagination is a one-shot flow over the whole document: hand it the assembled document
1246
+ // the way open() hands it the full render — head content (the meta tags, the title and
1247
+ // the stylesheet, which the paginator keeps in the flow) followed by the body.
1248
+ this.mountPaginated(parsed.head.innerHTML + flow.innerHTML);
1062
1249
  }
1063
1250
  }
1064
1251
  /**