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.
- package/README.md +78 -0
- package/dist/core.d.ts +4 -4
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +21 -200
- package/dist/core.js.map +1 -1
- package/dist/docxodus.worker.js +280 -7
- package/dist/docxodus.worker.js.map +1 -1
- package/dist/editor-reconcile.d.ts +5 -0
- package/dist/editor-reconcile.d.ts.map +1 -1
- package/dist/editor-reconcile.js.map +1 -1
- package/dist/editor.bundle.js +157 -15
- package/dist/editor.d.ts +44 -0
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +209 -22
- package/dist/editor.js.map +1 -1
- package/dist/embed.bundle.js +560 -217
- package/dist/embed.iife.js +560 -217
- package/dist/export-assets.json +20 -20
- package/dist/export-browser.bundle.js +101 -4
- package/dist/external-annotation-wire.d.ts +19 -0
- package/dist/external-annotation-wire.d.ts.map +1 -0
- package/dist/external-annotation-wire.js +139 -0
- package/dist/external-annotation-wire.js.map +1 -0
- package/dist/session.bundle.js +258 -7
- package/dist/session.d.ts +61 -12
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +234 -7
- package/dist/session.js.map +1 -1
- package/dist/types.d.ts +371 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/verification-request.d.ts +9 -0
- package/dist/verification-request.d.ts.map +1 -0
- package/dist/verification-request.js +37 -0
- package/dist/verification-request.js.map +1 -0
- package/dist/wasm/_framework/Docxodus.wasm +0 -0
- package/dist/wasm/_framework/Docxodus.wasm.br +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm +0 -0
- package/dist/wasm/_framework/DocxodusWasm.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm +0 -0
- package/dist/wasm/_framework/System.Private.CoreLib.wasm.br +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm +0 -0
- package/dist/wasm/_framework/System.Security.Cryptography.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Encodings.Web.wasm.br +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm +0 -0
- package/dist/wasm/_framework/System.Text.Json.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +9 -9
- package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm +0 -0
- package/dist/wasm/_framework/dotnet.native.wasm.br +0 -0
- package/dist/worker-proxy.bundle.js +101 -4
- package/dist/worker-proxy.d.ts +21 -3
- package/dist/worker-proxy.d.ts.map +1 -1
- package/dist/worker-proxy.js +57 -2
- package/dist/worker-proxy.js.map +1 -1
- 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
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
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
|
-
|
|
1060
|
-
|
|
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
|
/**
|