docxodus 12.3.0 → 12.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.
- package/README.md +31 -6
- package/dist/core.d.ts +924 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +2094 -0
- package/dist/core.js.map +1 -0
- package/dist/editor.bundle.js +1206 -27
- package/dist/editor.d.ts +10 -1
- package/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +48 -16
- package/dist/editor.js.map +1 -1
- package/dist/embed.bundle.js +12371 -11779
- package/dist/embed.d.ts +5 -2
- package/dist/embed.d.ts.map +1 -1
- package/dist/embed.iife.js +12282 -11690
- package/dist/embed.js +10 -3
- package/dist/embed.js.map +1 -1
- package/dist/export-assets.json +39 -39
- package/dist/history-checkpoints.d.ts +2 -0
- package/dist/history-checkpoints.d.ts.map +1 -1
- package/dist/history-checkpoints.js +2 -0
- package/dist/history-checkpoints.js.map +1 -1
- package/dist/history-controls.d.ts +6 -3
- package/dist/history-controls.d.ts.map +1 -1
- package/dist/history-controls.js +71 -29
- package/dist/history-controls.js.map +1 -1
- package/dist/index.d.ts +3 -923
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2093
- package/dist/index.js.map +1 -1
- package/dist/ribbon-chrome.d.ts +1 -1
- package/dist/ribbon-chrome.d.ts.map +1 -1
- package/dist/ribbon-chrome.js +6 -3
- package/dist/ribbon-chrome.js.map +1 -1
- package/dist/ribbon-history.d.ts +85 -0
- package/dist/ribbon-history.d.ts.map +1 -0
- package/dist/ribbon-history.js +508 -0
- package/dist/ribbon-history.js.map +1 -0
- package/dist/ribbon.d.ts +6 -0
- package/dist/ribbon.d.ts.map +1 -1
- package/dist/ribbon.js +47 -18
- package/dist/ribbon.js.map +1 -1
- package/dist/viewport.d.ts +3 -1
- package/dist/viewport.d.ts.map +1 -1
- package/dist/viewport.js +14 -0
- package/dist/viewport.js.map +1 -1
- 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.Collections.Concurrent.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Concurrent.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Immutable.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.NonGeneric.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.Specialized.wasm.br +0 -0
- package/dist/wasm/_framework/System.Collections.wasm +0 -0
- package/dist/wasm/_framework/System.Collections.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.Primitives.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.TypeConverter.wasm.br +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm +0 -0
- package/dist/wasm/_framework/System.ComponentModel.wasm.br +0 -0
- package/dist/wasm/_framework/System.Console.wasm +0 -0
- package/dist/wasm/_framework/System.Console.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Compression.wasm.br +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm +0 -0
- package/dist/wasm/_framework/System.IO.Pipelines.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.Expressions.wasm.br +0 -0
- package/dist/wasm/_framework/System.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Memory.wasm +0 -0
- package/dist/wasm/_framework/System.Memory.wasm.br +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Http.wasm.br +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm +0 -0
- package/dist/wasm/_framework/System.Net.Primitives.wasm.br +0 -0
- package/dist/wasm/_framework/System.ObjectModel.wasm +0 -0
- package/dist/wasm/_framework/System.ObjectModel.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.Uri.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Uri.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.Private.Xml.wasm +0 -0
- package/dist/wasm/_framework/System.Private.Xml.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.InteropServices.JavaScript.wasm.br +0 -0
- package/dist/wasm/_framework/System.Runtime.wasm +0 -0
- package/dist/wasm/_framework/System.Runtime.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/System.Text.RegularExpressions.wasm +0 -0
- package/dist/wasm/_framework/System.Text.RegularExpressions.wasm.br +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.Linq.wasm.br +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm +0 -0
- package/dist/wasm/_framework/System.Xml.XDocument.wasm.br +0 -0
- package/dist/wasm/_framework/System.wasm +0 -0
- package/dist/wasm/_framework/System.wasm.br +0 -0
- package/dist/wasm/_framework/dotnet.boot.js +34 -34
- package/dist/wasm/_framework/dotnet.boot.js.br +0 -0
- package/dist/wasm/_framework/dotnet.js +1 -1
- package/dist/wasm/_framework/dotnet.js.br +0 -0
- package/dist/wasm/_framework/dotnet.native.js +3 -3
- package/dist/wasm/_framework/dotnet.native.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/wasm/_framework/dotnet.runtime.js +1 -1
- package/dist/wasm/_framework/dotnet.runtime.js.br +0 -0
- package/package.json +14 -8
package/dist/core.js
ADDED
|
@@ -0,0 +1,2094 @@
|
|
|
1
|
+
import { openDocxSession as openDocxSessionImpl } from "./session.js";
|
|
2
|
+
import { DocxHistoryArchive, DocxHistoryClient, installHistoryStorageImports } from './history.js';
|
|
3
|
+
export * from './history.js';
|
|
4
|
+
export * from './history-checkpoints.js';
|
|
5
|
+
export * from './history-indexeddb.js';
|
|
6
|
+
export * from './history-controls.js';
|
|
7
|
+
/** Open history over host-owned storage after initialize(). No network layer is installed. */
|
|
8
|
+
export function openDocxHistory(storage) {
|
|
9
|
+
const bridge = ensureInitialized().HistoryBridge;
|
|
10
|
+
if (!bridge)
|
|
11
|
+
throw new Error('This WASM build does not include history bindings.');
|
|
12
|
+
return new DocxHistoryClient(bridge, storage);
|
|
13
|
+
}
|
|
14
|
+
/** Open a self-contained readonly .docxhistory file after initialize(); no storage adapter needed. */
|
|
15
|
+
export function openDocxHistoryArchive(bytes) {
|
|
16
|
+
const bridge = ensureInitialized().HistoryBridge;
|
|
17
|
+
if (!bridge)
|
|
18
|
+
throw new Error('This WASM build does not include history bindings.');
|
|
19
|
+
return DocxHistoryArchive.open(bridge, bytes);
|
|
20
|
+
}
|
|
21
|
+
export { DocxSession } from "./session.js";
|
|
22
|
+
export { PlaceholderKinds, ContextBoundary } from "./types.js";
|
|
23
|
+
export { DiffFormat } from "./types.js";
|
|
24
|
+
/**
|
|
25
|
+
* Open a {@link DocxSession} for surgical mutation of a DOCX. Requires
|
|
26
|
+
* {@link initialize} to have been called and awaited.
|
|
27
|
+
*
|
|
28
|
+
* The returned session keeps the document in WASM memory; call
|
|
29
|
+
* {@link DocxSession.close} when done.
|
|
30
|
+
*/
|
|
31
|
+
export function openDocxSession(bytes, settings) {
|
|
32
|
+
const wasm = ensureInitialized();
|
|
33
|
+
return openDocxSessionImpl(bytes, wasm, settings);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Mint a complete, blank single-paragraph DOCX (Normal style, US-Letter section) as bytes —
|
|
37
|
+
* a "New document" seed for editors that draft from scratch. Requires {@link initialize}.
|
|
38
|
+
*/
|
|
39
|
+
export function createBlankDocx() {
|
|
40
|
+
return ensureInitialized().DocxSessionBridge.CreateBlankDocx();
|
|
41
|
+
}
|
|
42
|
+
import { CommentRenderMode, PaginationMode, AnnotationLabelMode, RevisionType, DocxDiffRevisionGranularity, DocxDiffFormatComparison, ConflictResolution, ProjectionScopes, AnchorRenderMode, TableRenderMode, TrackedChangeMode, EmptyParagraphMode, AnchorIdRendering, ProjectionDepth, DocumentElementType, ComparisonLogLevel, ComparisonLogCodes, isInsertion, isDeletion, isMove, isFormatChange, findElementById, findElementsByType, getParagraphs, getTables, getTableColumns, targetElement, targetParagraph, targetParagraphRange, targetRun, targetTable, targetTableRow, targetTableCell, targetTableColumn, targetSearch, targetSearchInElement, } from "./types.js";
|
|
43
|
+
export { PaginationEngine, clearPageCitationHighlight, createUnavailablePageMap, navigateToPageCitation, paginateHtml, } from "./pagination.js";
|
|
44
|
+
// Page geometry is the document's own page setup (w:sectPr), read off the section wrappers
|
|
45
|
+
// the converter stamps in every render mode, plus the fit-to-width zoom a view applies to it.
|
|
46
|
+
export { DEFAULT_MARGIN, DEFAULT_PAGE_HEIGHT, DEFAULT_PAGE_WIDTH, fitScale, parseSectionDimensions, ptToPx, pxToPt, } from "./page-geometry.js";
|
|
47
|
+
export { DocumentViewport } from "./viewport.js";
|
|
48
|
+
export { CommentRenderMode, PaginationMode, AnnotationLabelMode, RevisionType, DocxDiffRevisionGranularity, DocxDiffFormatComparison, ConflictResolution, ProjectionScopes, AnchorRenderMode, TableRenderMode, TrackedChangeMode, EmptyParagraphMode, AnchorIdRendering, ProjectionDepth, DocumentElementType, ComparisonLogLevel, ComparisonLogCodes, isInsertion, isDeletion, isMove, isFormatChange,
|
|
49
|
+
// Document structure helpers
|
|
50
|
+
findElementById, findElementsByType, getParagraphs, getTables, getTableColumns,
|
|
51
|
+
// Annotation target factory functions
|
|
52
|
+
targetElement, targetParagraph, targetParagraphRange, targetRun, targetTable, targetTableRow, targetTableCell, targetTableColumn, targetSearch, targetSearchInElement, };
|
|
53
|
+
let wasmExports = null;
|
|
54
|
+
let initPromise = null;
|
|
55
|
+
/**
|
|
56
|
+
* Yields to the browser's main thread, allowing pending UI updates to render.
|
|
57
|
+
*
|
|
58
|
+
* This is critical for WASM operations: since WASM runs synchronously on the
|
|
59
|
+
* main thread, React state updates (like loading spinners) won't paint unless
|
|
60
|
+
* we yield before the blocking work begins.
|
|
61
|
+
*
|
|
62
|
+
* Uses requestAnimationFrame which fires just before the next paint, ensuring
|
|
63
|
+
* any queued state updates are committed to the DOM.
|
|
64
|
+
*
|
|
65
|
+
* @internal
|
|
66
|
+
*/
|
|
67
|
+
async function yieldToMain() {
|
|
68
|
+
// In non-browser environments (SSR, tests), skip yielding
|
|
69
|
+
if (typeof requestAnimationFrame === "undefined") {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Double-rAF ensures the browser has fully painted before we continue
|
|
73
|
+
// First rAF: scheduled for next frame
|
|
74
|
+
// Second rAF: ensures first frame actually painted
|
|
75
|
+
await new Promise((resolve) => {
|
|
76
|
+
requestAnimationFrame(() => {
|
|
77
|
+
requestAnimationFrame(() => resolve());
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Derive the WASM base path from this module's URL.
|
|
83
|
+
* Works whether loaded from node_modules, CDN, or bundled.
|
|
84
|
+
*/
|
|
85
|
+
function getDefaultWasmBasePath() {
|
|
86
|
+
try {
|
|
87
|
+
// import.meta.url gives us the URL of this module
|
|
88
|
+
// e.g., "https://cdn.jsdelivr.net/npm/docxodus@12.3.0/dist/core.js"
|
|
89
|
+
// or "file:///path/to/node_modules/docxodus/dist/core.js"
|
|
90
|
+
const moduleUrl = import.meta.url;
|
|
91
|
+
// Remove the filename to get the directory
|
|
92
|
+
const baseDir = moduleUrl.substring(0, moduleUrl.lastIndexOf('/') + 1);
|
|
93
|
+
// WASM files are in ./wasm/ relative to dist/
|
|
94
|
+
return baseDir + "wasm/";
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
// Fallback if import.meta.url is not available
|
|
98
|
+
return "";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Current base path for WASM files.
|
|
103
|
+
* Empty string means auto-detect from module URL.
|
|
104
|
+
*/
|
|
105
|
+
export let wasmBasePath = "";
|
|
106
|
+
/**
|
|
107
|
+
* Set custom base path for WASM files.
|
|
108
|
+
* Pass empty string or don't call this to auto-detect from module location.
|
|
109
|
+
*
|
|
110
|
+
* @param path - Custom path to WASM files, or empty string for auto-detection
|
|
111
|
+
*/
|
|
112
|
+
export function setWasmBasePath(path) {
|
|
113
|
+
wasmBasePath = path && !path.endsWith("/") ? path + "/" : path;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Initialize the Docxodus WASM runtime.
|
|
117
|
+
* Must be called before using any conversion/comparison functions.
|
|
118
|
+
* Safe to call multiple times - will only initialize once.
|
|
119
|
+
*
|
|
120
|
+
* By default, WASM files are auto-detected from the module's location
|
|
121
|
+
* (works with CDN, npm, or local hosting).
|
|
122
|
+
* Pass a basePath to load from a custom location instead.
|
|
123
|
+
*
|
|
124
|
+
* @param basePath - Optional custom path to WASM files. Leave empty for auto-detection.
|
|
125
|
+
*/
|
|
126
|
+
export async function initialize(basePath) {
|
|
127
|
+
if (wasmExports)
|
|
128
|
+
return;
|
|
129
|
+
if (initPromise) {
|
|
130
|
+
return initPromise;
|
|
131
|
+
}
|
|
132
|
+
if (basePath !== undefined) {
|
|
133
|
+
setWasmBasePath(basePath);
|
|
134
|
+
}
|
|
135
|
+
// Clear the cached promise on failure so a caller can retry with a
|
|
136
|
+
// different base path (a rejected initialize() used to be permanent).
|
|
137
|
+
initPromise = loadWasm().catch((e) => {
|
|
138
|
+
initPromise = null;
|
|
139
|
+
throw e;
|
|
140
|
+
});
|
|
141
|
+
return initPromise;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Try to load WASM from a specific base path
|
|
145
|
+
*/
|
|
146
|
+
async function tryLoadFromPath(basePath) {
|
|
147
|
+
try {
|
|
148
|
+
const dotnetPath = basePath + "_framework/dotnet.js";
|
|
149
|
+
const { dotnet } = await import(/* webpackIgnore: true */ /* @vite-ignore */ dotnetPath);
|
|
150
|
+
const { getAssemblyExports, getConfig, setModuleImports } = await dotnet
|
|
151
|
+
.withDiagnosticTracing(false)
|
|
152
|
+
.create();
|
|
153
|
+
installHistoryStorageImports(setModuleImports);
|
|
154
|
+
const config = getConfig();
|
|
155
|
+
const exports = await getAssemblyExports(config.mainAssemblyName);
|
|
156
|
+
wasmExports = {
|
|
157
|
+
DocumentConverter: exports.DocxodusWasm.DocumentConverter,
|
|
158
|
+
DocumentComparer: exports.DocxodusWasm.DocumentComparer,
|
|
159
|
+
DocxDiffBridge: exports.DocxodusWasm.DocxDiffBridge,
|
|
160
|
+
DocxSessionBridge: exports.DocxodusWasm.DocxSessionBridge,
|
|
161
|
+
HistoryBridge: exports.DocxodusWasm.HistoryBridge,
|
|
162
|
+
};
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
async function loadWasm() {
|
|
170
|
+
// If a custom path is set, use it directly
|
|
171
|
+
if (wasmBasePath) {
|
|
172
|
+
const success = await tryLoadFromPath(wasmBasePath);
|
|
173
|
+
if (success)
|
|
174
|
+
return;
|
|
175
|
+
throw new Error(`Failed to load WASM from custom path: ${wasmBasePath}. ` +
|
|
176
|
+
`Ensure the WASM files are served at this location.`);
|
|
177
|
+
}
|
|
178
|
+
// Try to auto-detect from module URL (works for CDN and local imports)
|
|
179
|
+
const autoDetectedPath = getDefaultWasmBasePath();
|
|
180
|
+
if (autoDetectedPath) {
|
|
181
|
+
const success = await tryLoadFromPath(autoDetectedPath);
|
|
182
|
+
if (success) {
|
|
183
|
+
wasmBasePath = autoDetectedPath;
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
// Auto-detection failed
|
|
188
|
+
throw new Error(`Failed to load WASM files. ` +
|
|
189
|
+
`Auto-detected path: ${autoDetectedPath || "(none)"}. ` +
|
|
190
|
+
`You can specify a custom path by calling initialize("/path/to/wasm/").`);
|
|
191
|
+
}
|
|
192
|
+
function ensureInitialized() {
|
|
193
|
+
if (!wasmExports) {
|
|
194
|
+
throw new Error("Docxodus not initialized. Call initialize() first and await it.");
|
|
195
|
+
}
|
|
196
|
+
return wasmExports;
|
|
197
|
+
}
|
|
198
|
+
function isErrorResponse(result) {
|
|
199
|
+
try {
|
|
200
|
+
const parsed = JSON.parse(result);
|
|
201
|
+
return typeof parsed === "object" && "Error" in parsed;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
function parseError(result) {
|
|
208
|
+
const parsed = JSON.parse(result);
|
|
209
|
+
return {
|
|
210
|
+
error: parsed.Error || parsed.error,
|
|
211
|
+
type: parsed.Type || parsed.type,
|
|
212
|
+
stackTrace: parsed.StackTrace || parsed.stackTrace,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Convert a File or Uint8Array to Uint8Array
|
|
217
|
+
*/
|
|
218
|
+
async function toBytes(input) {
|
|
219
|
+
if (input instanceof Uint8Array) {
|
|
220
|
+
return input;
|
|
221
|
+
}
|
|
222
|
+
const buffer = await input.arrayBuffer();
|
|
223
|
+
return new Uint8Array(buffer);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Generate a deterministic, non-mutating verification manifest from DOCX bytes.
|
|
227
|
+
* Invalid, malformed, and encrypted packages are represented by structured findings.
|
|
228
|
+
*/
|
|
229
|
+
export async function generatePackageManifest(document) {
|
|
230
|
+
const exports = ensureInitialized();
|
|
231
|
+
const bytes = await toBytes(document);
|
|
232
|
+
await yieldToMain();
|
|
233
|
+
return JSON.parse(exports.DocumentConverter.GeneratePackageManifest(bytes));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Run the default bounded deliverable-verification policy on exact DOCX bytes.
|
|
237
|
+
* Invalid, malformed, encrypted, and safety-limited packages are returned as
|
|
238
|
+
* structured report findings rather than editable-session errors. When supplied,
|
|
239
|
+
* the exact baseline bytes are used to classify pre-existing, new, and resolved findings.
|
|
240
|
+
*/
|
|
241
|
+
export async function verifyDeliverable(document, baseline) {
|
|
242
|
+
const exports = ensureInitialized();
|
|
243
|
+
const bytes = await toBytes(document);
|
|
244
|
+
const baselineBytes = baseline === undefined ? undefined : await toBytes(baseline);
|
|
245
|
+
await yieldToMain();
|
|
246
|
+
return JSON.parse(baselineBytes === undefined
|
|
247
|
+
? exports.DocumentConverter.VerifyDeliverable(bytes)
|
|
248
|
+
: exports.DocumentConverter.VerifyDeliverableWithBaseline(baselineBytes, bytes));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Verify a portable JSON delivery change receipt against supplied artifact bytes.
|
|
252
|
+
*
|
|
253
|
+
* The receipt travels as its JSON envelope string; `artifacts` maps each artifact id
|
|
254
|
+
* the receipt records to the exact bytes to independently re-hash against it. Omitted
|
|
255
|
+
* artifacts report `"missing"`. Malformed input yields a structured invalid verdict
|
|
256
|
+
* whose findings carry the reason — never a thrown error.
|
|
257
|
+
*/
|
|
258
|
+
export async function verifyDeliveryReceipt(receiptJson, artifacts) {
|
|
259
|
+
const exports = ensureInitialized();
|
|
260
|
+
const artifactsJson = artifacts === undefined
|
|
261
|
+
? ""
|
|
262
|
+
: JSON.stringify(Object.fromEntries(Object.entries(artifacts).map(([artifactId, bytes]) => [
|
|
263
|
+
artifactId,
|
|
264
|
+
bytesToBase64(bytes),
|
|
265
|
+
])));
|
|
266
|
+
await yieldToMain();
|
|
267
|
+
return JSON.parse(exports.DocumentConverter.VerifyDeliveryReceipt(receiptJson, artifactsJson));
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Prove that a redline's generated revisions accept to the intended final and reject to the
|
|
271
|
+
* selected baseline without consuming pre-existing review state.
|
|
272
|
+
*
|
|
273
|
+
* Three packages are inspected and two rebuilt, so on a UI thread prefer the worker proxy's
|
|
274
|
+
* `proveRedlineReversibility`. Malformed, encrypted, and safety-limited packages are reported as
|
|
275
|
+
* structured proof findings rather than thrown errors. The rebuilt packages are not returned:
|
|
276
|
+
* the proof carries their digests and the divergences between them and each expected document.
|
|
277
|
+
*
|
|
278
|
+
* @param baseline - The document the redline was generated against
|
|
279
|
+
* @param intendedFinal - The document accepting the generated revisions must reproduce
|
|
280
|
+
* @param redline - The generated redline under proof
|
|
281
|
+
*/
|
|
282
|
+
export async function proveRedlineReversibility(baseline, intendedFinal, redline) {
|
|
283
|
+
const exports = ensureInitialized();
|
|
284
|
+
const baselineBytes = await toBytes(baseline);
|
|
285
|
+
const intendedFinalBytes = await toBytes(intendedFinal);
|
|
286
|
+
const redlineBytes = await toBytes(redline);
|
|
287
|
+
await yieldToMain();
|
|
288
|
+
return JSON.parse(exports.DocumentConverter.ProveRedlineReversibility(baselineBytes, intendedFinalBytes, redlineBytes));
|
|
289
|
+
}
|
|
290
|
+
/**
|
|
291
|
+
* Convert a DOCX document to HTML.
|
|
292
|
+
*
|
|
293
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
294
|
+
* @param options - Conversion options
|
|
295
|
+
* @returns HTML string
|
|
296
|
+
* @throws Error if conversion fails
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```typescript
|
|
300
|
+
* // Basic conversion
|
|
301
|
+
* const html = await convertDocxToHtml(docxFile);
|
|
302
|
+
*
|
|
303
|
+
* // With pagination (PDF.js-style page view)
|
|
304
|
+
* const html = await convertDocxToHtml(docxFile, {
|
|
305
|
+
* paginationMode: PaginationMode.Paginated,
|
|
306
|
+
* paginationScale: 0.8
|
|
307
|
+
* });
|
|
308
|
+
*
|
|
309
|
+
* // With annotations rendered
|
|
310
|
+
* const html = await convertDocxToHtml(docxFile, {
|
|
311
|
+
* renderAnnotations: true,
|
|
312
|
+
* annotationLabelMode: AnnotationLabelMode.Above
|
|
313
|
+
* });
|
|
314
|
+
*
|
|
315
|
+
* // With footnotes and endnotes
|
|
316
|
+
* const html = await convertDocxToHtml(docxFile, {
|
|
317
|
+
* renderFootnotesAndEndnotes: true
|
|
318
|
+
* });
|
|
319
|
+
*
|
|
320
|
+
* // With headers and footers
|
|
321
|
+
* const html = await convertDocxToHtml(docxFile, {
|
|
322
|
+
* renderHeadersAndFooters: true
|
|
323
|
+
* });
|
|
324
|
+
*
|
|
325
|
+
* // With tracked changes (redlines visible)
|
|
326
|
+
* const html = await convertDocxToHtml(docxFile, {
|
|
327
|
+
* renderTrackedChanges: true,
|
|
328
|
+
* showDeletedContent: true,
|
|
329
|
+
* renderMoveOperations: true
|
|
330
|
+
* });
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
/**
|
|
334
|
+
* Render a single document block to faithful HTML, addressed by its anchor.
|
|
335
|
+
*
|
|
336
|
+
* The anchor is the `data-anchor` value stamped on a block during a full
|
|
337
|
+
* conversion (a bare 32-hex Unid), or a full `kind:scope:unid` anchor — either
|
|
338
|
+
* form works. Powers the editor's incremental per-block re-render: apply an edit
|
|
339
|
+
* to a DocxSession, then re-render only the changed block instead of the whole
|
|
340
|
+
* document. Returns the block's HTML element (no `<html>`/`<head>` wrapper).
|
|
341
|
+
*/
|
|
342
|
+
export async function renderBlockHtml(document, anchorId, options) {
|
|
343
|
+
const exports = ensureInitialized();
|
|
344
|
+
const bytes = await toBytes(document);
|
|
345
|
+
await yieldToMain();
|
|
346
|
+
const result = exports.DocumentConverter.RenderBlockHtml(bytes, anchorId, options?.cssPrefix ?? "docx-", options?.fabricateClasses ?? false);
|
|
347
|
+
if (isErrorResponse(result)) {
|
|
348
|
+
throw new Error(`Block rendering failed: ${parseError(result).error}`);
|
|
349
|
+
}
|
|
350
|
+
return result;
|
|
351
|
+
}
|
|
352
|
+
export async function convertDocxToHtml(document, options) {
|
|
353
|
+
const exports = ensureInitialized();
|
|
354
|
+
const bytes = await toBytes(document);
|
|
355
|
+
// Yield to browser before heavy WASM work - allows loading states to render
|
|
356
|
+
await yieldToMain();
|
|
357
|
+
let result;
|
|
358
|
+
// Check if any of the new complete options are specified
|
|
359
|
+
const needsCompleteMethod = options?.renderFootnotesAndEndnotes !== undefined ||
|
|
360
|
+
options?.renderHeadersAndFooters !== undefined ||
|
|
361
|
+
options?.renderTrackedChanges !== undefined ||
|
|
362
|
+
options?.showDeletedContent !== undefined ||
|
|
363
|
+
options?.renderMoveOperations !== undefined ||
|
|
364
|
+
options?.renderUnsupportedContentPlaceholders !== undefined ||
|
|
365
|
+
options?.documentLanguage !== undefined ||
|
|
366
|
+
options?.stampAnchors !== undefined;
|
|
367
|
+
// Use complete method when any new options are specified (most comprehensive)
|
|
368
|
+
if (needsCompleteMethod || options?.renderAnnotations) {
|
|
369
|
+
result = exports.DocumentConverter.ConvertDocxToHtmlComplete(bytes, options?.pageTitle ?? "Document", options?.cssPrefix ?? "docx-", options?.fabricateClasses ?? true, options?.additionalCss ?? "", options?.commentRenderMode ?? CommentRenderMode.Disabled, options?.commentCssClassPrefix ?? "comment-", options?.paginationMode ?? PaginationMode.None, options?.paginationScale ?? 1.0, options?.paginationCssClassPrefix ?? "page-", options?.renderAnnotations ?? false, options?.annotationLabelMode ?? AnnotationLabelMode.Above, options?.annotationCssClassPrefix ?? "annot-", options?.renderFootnotesAndEndnotes ?? false, options?.renderHeadersAndFooters ?? false, options?.renderTrackedChanges ?? false, options?.showDeletedContent ?? true, options?.renderMoveOperations ?? true, options?.renderUnsupportedContentPlaceholders ?? false, options?.documentLanguage ?? null, options?.stampAnchors ?? false);
|
|
370
|
+
}
|
|
371
|
+
// Use pagination-aware method when pagination is requested
|
|
372
|
+
else if (options?.paginationMode !== undefined && options.paginationMode !== PaginationMode.None) {
|
|
373
|
+
result = exports.DocumentConverter.ConvertDocxToHtmlWithPagination(bytes, options.pageTitle ?? "Document", options.cssPrefix ?? "docx-", options.fabricateClasses ?? true, options.additionalCss ?? "", options.commentRenderMode ?? CommentRenderMode.Disabled, options.commentCssClassPrefix ?? "comment-", options.paginationMode, options.paginationScale ?? 1.0, options.paginationCssClassPrefix ?? "page-");
|
|
374
|
+
}
|
|
375
|
+
else if (options) {
|
|
376
|
+
result = exports.DocumentConverter.ConvertDocxToHtmlWithOptions(bytes, options.pageTitle ?? "Document", options.cssPrefix ?? "docx-", options.fabricateClasses ?? true, options.additionalCss ?? "", options.commentRenderMode ?? CommentRenderMode.Disabled, options.commentCssClassPrefix ?? "comment-");
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
result = exports.DocumentConverter.ConvertDocxToHtml(bytes);
|
|
380
|
+
}
|
|
381
|
+
if (isErrorResponse(result)) {
|
|
382
|
+
const error = parseError(result);
|
|
383
|
+
throw new Error(`Conversion failed: ${error.error}`);
|
|
384
|
+
}
|
|
385
|
+
return result;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Compare two DOCX documents and return the redlined result as a DOCX.
|
|
389
|
+
*
|
|
390
|
+
* @param original - Original DOCX document
|
|
391
|
+
* @param modified - Modified DOCX document
|
|
392
|
+
* @param options - Comparison options
|
|
393
|
+
* @returns Redlined DOCX as Uint8Array
|
|
394
|
+
* @throws Error if comparison fails
|
|
395
|
+
*/
|
|
396
|
+
export async function compareDocuments(original, modified, options) {
|
|
397
|
+
const exports = ensureInitialized();
|
|
398
|
+
const originalBytes = await toBytes(original);
|
|
399
|
+
const modifiedBytes = await toBytes(modified);
|
|
400
|
+
// Yield to browser before heavy WASM work - allows loading states to render
|
|
401
|
+
await yieldToMain();
|
|
402
|
+
let result;
|
|
403
|
+
if (options?.caseInsensitive) {
|
|
404
|
+
result = exports.DocumentComparer.CompareDocumentsWithOptions(originalBytes, modifiedBytes, options?.authorName ?? "Docxodus", options.caseInsensitive);
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
result = exports.DocumentComparer.CompareDocuments(originalBytes, modifiedBytes, options?.authorName ?? "Docxodus");
|
|
408
|
+
}
|
|
409
|
+
if (result.length === 0) {
|
|
410
|
+
throw new Error("Comparison failed - empty result");
|
|
411
|
+
}
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Compare two DOCX documents and return the result as HTML.
|
|
416
|
+
*
|
|
417
|
+
* @param original - Original DOCX document
|
|
418
|
+
* @param modified - Modified DOCX document
|
|
419
|
+
* @param options - Comparison options
|
|
420
|
+
* @returns HTML string with redlined content
|
|
421
|
+
* @throws Error if comparison fails
|
|
422
|
+
*/
|
|
423
|
+
export async function compareDocumentsToHtml(original, modified, options) {
|
|
424
|
+
const exports = ensureInitialized();
|
|
425
|
+
const originalBytes = await toBytes(original);
|
|
426
|
+
const modifiedBytes = await toBytes(modified);
|
|
427
|
+
// Yield to browser before heavy WASM work - allows loading states to render
|
|
428
|
+
await yieldToMain();
|
|
429
|
+
const renderTrackedChanges = options?.renderTrackedChanges ?? true;
|
|
430
|
+
let result;
|
|
431
|
+
if (options?.caseInsensitive !== undefined) {
|
|
432
|
+
result = exports.DocumentComparer.CompareDocumentsToHtmlFull(originalBytes, modifiedBytes, options?.authorName ?? "Docxodus", options.caseInsensitive, renderTrackedChanges);
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
result = exports.DocumentComparer.CompareDocumentsToHtmlWithOptions(originalBytes, modifiedBytes, options?.authorName ?? "Docxodus", renderTrackedChanges);
|
|
436
|
+
}
|
|
437
|
+
if (isErrorResponse(result)) {
|
|
438
|
+
const error = parseError(result);
|
|
439
|
+
throw new Error(`Comparison failed: ${error.error}`);
|
|
440
|
+
}
|
|
441
|
+
return result;
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Get revisions from a compared document.
|
|
445
|
+
*
|
|
446
|
+
* @param document - A document that has been through comparison (has tracked changes)
|
|
447
|
+
* @param options - Optional move detection configuration
|
|
448
|
+
* @returns Array of revisions
|
|
449
|
+
* @throws Error if operation fails
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```typescript
|
|
453
|
+
* // Default settings (move detection enabled, 80% threshold)
|
|
454
|
+
* const revisions = await getRevisions(comparedDoc);
|
|
455
|
+
*
|
|
456
|
+
* // Custom move detection settings
|
|
457
|
+
* const revisions = await getRevisions(comparedDoc, {
|
|
458
|
+
* detectMoves: true,
|
|
459
|
+
* moveSimilarityThreshold: 0.9, // Require 90% word overlap
|
|
460
|
+
* moveMinimumWordCount: 5, // Only consider phrases of 5+ words
|
|
461
|
+
* caseInsensitive: true // Ignore case when matching
|
|
462
|
+
* });
|
|
463
|
+
*
|
|
464
|
+
* // Disable move detection entirely
|
|
465
|
+
* const revisions = await getRevisions(comparedDoc, { detectMoves: false });
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
468
|
+
export async function getRevisions(document) {
|
|
469
|
+
const exports = ensureInitialized();
|
|
470
|
+
const bytes = await toBytes(document);
|
|
471
|
+
// Yield to browser before WASM work - allows loading states to render
|
|
472
|
+
await yieldToMain();
|
|
473
|
+
const result = exports.DocumentComparer.GetRevisionsJson(bytes);
|
|
474
|
+
if (isErrorResponse(result)) {
|
|
475
|
+
const error = parseError(result);
|
|
476
|
+
throw new Error(`Failed to get revisions: ${error.error}`);
|
|
477
|
+
}
|
|
478
|
+
// The payload is the session's own revision wire shape, so it needs no remapping.
|
|
479
|
+
return JSON.parse(result);
|
|
480
|
+
}
|
|
481
|
+
// ─── DocxDiff (IR diff engine) ──────────────────────────────────────────────
|
|
482
|
+
//
|
|
483
|
+
// The structure-aware default comparison engine. Its specialized APIs add
|
|
484
|
+
// anchor-addressed revisions and the diff-as-data edit script. Settings flow as
|
|
485
|
+
// a JSON object; an empty `{}` (or omitted options) uses the engine defaults.
|
|
486
|
+
/** Serialize DocxDiffSettings to the wire JSON the bridge parses (empty string when undefined). */
|
|
487
|
+
function docxDiffSettingsJson(settings) {
|
|
488
|
+
return settings ? JSON.stringify(settings) : "";
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Compare two DOCX documents with the IR diff engine and return the redlined
|
|
492
|
+
* result as a DOCX (native w:ins/w:del/w:moveFrom/w:moveTo/w:rPrChange markup).
|
|
493
|
+
*
|
|
494
|
+
* @param left - The earlier/original document.
|
|
495
|
+
* @param right - The later/revised document.
|
|
496
|
+
* @param settings - Optional {@link DocxDiffSettings}; omit for engine defaults.
|
|
497
|
+
* @returns Redlined DOCX as Uint8Array.
|
|
498
|
+
* @throws Error if comparison fails.
|
|
499
|
+
*/
|
|
500
|
+
export async function docxDiffCompare(left, right, settings) {
|
|
501
|
+
const exports = ensureInitialized();
|
|
502
|
+
const leftBytes = await toBytes(left);
|
|
503
|
+
const rightBytes = await toBytes(right);
|
|
504
|
+
await yieldToMain();
|
|
505
|
+
const result = exports.DocxDiffBridge.Compare(leftBytes, rightBytes, docxDiffSettingsJson(settings));
|
|
506
|
+
if (result.length === 0) {
|
|
507
|
+
throw new Error("DocxDiff comparison failed - empty result");
|
|
508
|
+
}
|
|
509
|
+
return result;
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* Compare two DOCX documents with the IR diff engine and return the
|
|
513
|
+
* anchor-addressed revision list.
|
|
514
|
+
*
|
|
515
|
+
* @param left - The earlier/original document.
|
|
516
|
+
* @param right - The later/revised document.
|
|
517
|
+
* @param settings - Optional {@link DocxDiffSettings}; omit for engine defaults.
|
|
518
|
+
* @returns Array of {@link DocxDiffRevision} (each carrying its left/right block anchors).
|
|
519
|
+
* @throws Error if the operation fails.
|
|
520
|
+
*/
|
|
521
|
+
export async function docxDiffGetRevisions(left, right, settings) {
|
|
522
|
+
const exports = ensureInitialized();
|
|
523
|
+
const leftBytes = await toBytes(left);
|
|
524
|
+
const rightBytes = await toBytes(right);
|
|
525
|
+
await yieldToMain();
|
|
526
|
+
const result = exports.DocxDiffBridge.GetRevisionsJson(leftBytes, rightBytes, docxDiffSettingsJson(settings));
|
|
527
|
+
if (isErrorResponse(result)) {
|
|
528
|
+
const error = parseError(result);
|
|
529
|
+
throw new Error(`Failed to get DocxDiff revisions: ${error.error}`);
|
|
530
|
+
}
|
|
531
|
+
const parsed = JSON.parse(result);
|
|
532
|
+
return (parsed.revisions || parsed.Revisions || []).map(mapDocxDiffRevision);
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Compare two DOCX documents ONCE and return every requested data product from
|
|
536
|
+
* that single memoized pass (issue #594). Where a review pipeline calling
|
|
537
|
+
* {@link docxDiffCompare}, {@link docxDiffGetRevisions}, and
|
|
538
|
+
* {@link docxDiffGetEditScript} separately pays for the alignment per call, this
|
|
539
|
+
* runs it once — each product identical to its standalone counterpart (the edit
|
|
540
|
+
* script is handed over parsed rather than as the serialized string).
|
|
541
|
+
*
|
|
542
|
+
* @param left - The earlier/original document.
|
|
543
|
+
* @param right - The later/revised document.
|
|
544
|
+
* @param settings - Optional {@link DocxDiffSettings}; omit for engine defaults.
|
|
545
|
+
* @param products - Products to compute; omit for all four.
|
|
546
|
+
* @throws Error if the operation fails.
|
|
547
|
+
*/
|
|
548
|
+
export async function docxDiffCompareProducts(left, right, settings, products) {
|
|
549
|
+
const exports = ensureInitialized();
|
|
550
|
+
const leftBytes = await toBytes(left);
|
|
551
|
+
const rightBytes = await toBytes(right);
|
|
552
|
+
await yieldToMain();
|
|
553
|
+
const result = exports.DocxDiffBridge.CompareProductsJson(leftBytes, rightBytes, docxDiffSettingsJson(settings), products ? JSON.stringify(products) : "");
|
|
554
|
+
if (isErrorResponse(result)) {
|
|
555
|
+
const error = parseError(result);
|
|
556
|
+
throw new Error(`Failed to compare products: ${error.error}`);
|
|
557
|
+
}
|
|
558
|
+
const parsed = JSON.parse(result);
|
|
559
|
+
return {
|
|
560
|
+
redline: typeof parsed.redlineB64 === "string"
|
|
561
|
+
? Uint8Array.from(atob(parsed.redlineB64), c => c.charCodeAt(0))
|
|
562
|
+
: undefined,
|
|
563
|
+
revisions: Array.isArray(parsed.revisions)
|
|
564
|
+
? parsed.revisions.map(mapDocxDiffRevision)
|
|
565
|
+
: undefined,
|
|
566
|
+
editScript: parsed.editScript !== undefined
|
|
567
|
+
? parsed.editScript
|
|
568
|
+
: undefined,
|
|
569
|
+
semanticChanges: parsed.semanticChanges !== undefined
|
|
570
|
+
? parsed.semanticChanges
|
|
571
|
+
: undefined,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
/**
|
|
575
|
+
* Compare ONE baseline against MANY candidates, reading the baseline once (issue #617).
|
|
576
|
+
*
|
|
577
|
+
* The read is the single largest stage of a comparison, and a fan-out — one negotiated
|
|
578
|
+
* draft against every counterparty's markup — otherwise re-reads the baseline for each
|
|
579
|
+
* one. This reads it once and compares every candidate against that snapshot; each
|
|
580
|
+
* result is identical to what {@link docxDiffCompareProducts} returns for the same pair.
|
|
581
|
+
*
|
|
582
|
+
* A candidate that fails carries an `error` instead of products; the rest of the batch
|
|
583
|
+
* still comes back, because one malformed markup should not cost the other ninety-nine.
|
|
584
|
+
*
|
|
585
|
+
* @param baseline - The shared left-hand document.
|
|
586
|
+
* @param candidates - The documents to compare against it, in order.
|
|
587
|
+
* @param settings - Optional {@link DocxDiffSettings}; omit for engine defaults.
|
|
588
|
+
* @param products - Products to compute; omit for all four.
|
|
589
|
+
* @throws Error if the batch itself fails (a bad baseline, malformed settings).
|
|
590
|
+
*/
|
|
591
|
+
export async function docxDiffCompareBatch(baseline, candidates, settings, products) {
|
|
592
|
+
const exports = ensureInitialized();
|
|
593
|
+
const baselineBytes = await toBytes(baseline);
|
|
594
|
+
const payload = [];
|
|
595
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
596
|
+
const bytes = await toBytes(candidates[i].document);
|
|
597
|
+
payload.push({
|
|
598
|
+
name: candidates[i].name ?? String(i),
|
|
599
|
+
docB64: bytesToBase64(bytes),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
await yieldToMain();
|
|
603
|
+
const result = exports.DocxDiffBridge.CompareBatchJson(baselineBytes, JSON.stringify(payload), docxDiffSettingsJson(settings), products ? JSON.stringify(products) : "");
|
|
604
|
+
if (isErrorResponse(result)) {
|
|
605
|
+
const error = parseError(result);
|
|
606
|
+
throw new Error(`Failed to compare batch: ${error.error}`);
|
|
607
|
+
}
|
|
608
|
+
const parsed = JSON.parse(result);
|
|
609
|
+
return (parsed.results ?? []).map((entry) => ({
|
|
610
|
+
name: String(entry.name ?? ""),
|
|
611
|
+
error: typeof entry.error === "string" ? entry.error : undefined,
|
|
612
|
+
redline: typeof entry.redlineB64 === "string"
|
|
613
|
+
? Uint8Array.from(atob(entry.redlineB64), c => c.charCodeAt(0))
|
|
614
|
+
: undefined,
|
|
615
|
+
revisions: Array.isArray(entry.revisions)
|
|
616
|
+
? entry.revisions.map(mapDocxDiffRevision)
|
|
617
|
+
: undefined,
|
|
618
|
+
editScript: entry.editScript !== undefined
|
|
619
|
+
? entry.editScript
|
|
620
|
+
: undefined,
|
|
621
|
+
semanticChanges: entry.semanticChanges !== undefined
|
|
622
|
+
? entry.semanticChanges
|
|
623
|
+
: undefined,
|
|
624
|
+
}));
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Compare two DOCX documents with the IR diff engine and return the edit script
|
|
628
|
+
* as a JSON string — the diff-as-data differentiator. The script is the
|
|
629
|
+
* anchor-addressed list of block operations the markup and revision renderers
|
|
630
|
+
* both consume: stable and machine-readable for storage, transport, and audit.
|
|
631
|
+
*
|
|
632
|
+
* @param left - The earlier/original document.
|
|
633
|
+
* @param right - The later/revised document.
|
|
634
|
+
* @param settings - Optional {@link DocxDiffSettings}; omit for engine defaults.
|
|
635
|
+
* @returns The edit script serialized as indented JSON.
|
|
636
|
+
* @throws Error if the operation fails.
|
|
637
|
+
*/
|
|
638
|
+
export async function docxDiffGetEditScript(left, right, settings) {
|
|
639
|
+
const exports = ensureInitialized();
|
|
640
|
+
const leftBytes = await toBytes(left);
|
|
641
|
+
const rightBytes = await toBytes(right);
|
|
642
|
+
await yieldToMain();
|
|
643
|
+
const result = exports.DocxDiffBridge.GetEditScriptJson(leftBytes, rightBytes, docxDiffSettingsJson(settings));
|
|
644
|
+
if (isErrorResponse(result)) {
|
|
645
|
+
const error = parseError(result);
|
|
646
|
+
throw new Error(`Failed to get DocxDiff edit script: ${error.error}`);
|
|
647
|
+
}
|
|
648
|
+
return result;
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Compare two DOCX documents and return the stable, versioned semantic-change
|
|
652
|
+
* schema. This is the audit/verification surface; it classifies document meaning
|
|
653
|
+
* beyond the renderer's internal edit script and preserves unknown package changes.
|
|
654
|
+
*/
|
|
655
|
+
export async function docxDiffGetSemanticChanges(left, right, settings) {
|
|
656
|
+
const exports = ensureInitialized();
|
|
657
|
+
const leftBytes = await toBytes(left);
|
|
658
|
+
const rightBytes = await toBytes(right);
|
|
659
|
+
await yieldToMain();
|
|
660
|
+
const result = exports.DocxDiffBridge.GetSemanticChangesJson(leftBytes, rightBytes, docxDiffSettingsJson(settings));
|
|
661
|
+
if (isErrorResponse(result)) {
|
|
662
|
+
const error = parseError(result);
|
|
663
|
+
throw new Error(`Failed to get semantic changes: ${error.error}`);
|
|
664
|
+
}
|
|
665
|
+
return JSON.parse(result);
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* Accept every tracked revision in a redlined DOCX and return the resulting bytes
|
|
669
|
+
* (materializes the "right"/revised side). The byte-in, byte-out counterpart of
|
|
670
|
+
* {@link docxDiffCompare}: `docxDiffAcceptRevisions(await docxDiffCompare(left, right))`
|
|
671
|
+
* equals `right` at the per-block text level — so callers can verify the round-trip
|
|
672
|
+
* contract of a redline, not just inspect its shape.
|
|
673
|
+
*
|
|
674
|
+
* @param redline - A DOCX carrying tracked-changes markup (e.g. {@link docxDiffCompare} output).
|
|
675
|
+
* @returns The DOCX bytes with all revisions accepted.
|
|
676
|
+
* @throws Error if the operation fails.
|
|
677
|
+
*/
|
|
678
|
+
export async function docxDiffAcceptRevisions(redline) {
|
|
679
|
+
const exports = ensureInitialized();
|
|
680
|
+
const bytes = await toBytes(redline);
|
|
681
|
+
await yieldToMain();
|
|
682
|
+
const result = exports.DocxDiffBridge.AcceptRevisions(bytes);
|
|
683
|
+
if (result.length === 0) {
|
|
684
|
+
throw new Error("DocxDiff accept-revisions failed - empty result");
|
|
685
|
+
}
|
|
686
|
+
return result;
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Reject every tracked revision in a redlined DOCX and return the resulting bytes
|
|
690
|
+
* (materializes the "left"/original side): `docxDiffRejectRevisions(await
|
|
691
|
+
* docxDiffCompare(left, right))` equals `left` at the per-block text level.
|
|
692
|
+
*
|
|
693
|
+
* @param redline - A DOCX carrying tracked-changes markup (e.g. {@link docxDiffCompare} output).
|
|
694
|
+
* @returns The DOCX bytes with all revisions rejected.
|
|
695
|
+
* @throws Error if the operation fails.
|
|
696
|
+
*/
|
|
697
|
+
export async function docxDiffRejectRevisions(redline) {
|
|
698
|
+
const exports = ensureInitialized();
|
|
699
|
+
const bytes = await toBytes(redline);
|
|
700
|
+
await yieldToMain();
|
|
701
|
+
const result = exports.DocxDiffBridge.RejectRevisions(bytes);
|
|
702
|
+
if (result.length === 0) {
|
|
703
|
+
throw new Error("DocxDiff reject-revisions failed - empty result");
|
|
704
|
+
}
|
|
705
|
+
return result;
|
|
706
|
+
}
|
|
707
|
+
// ─── DocxDiff consolidate (composite N-way) ─────────────────────────────────
|
|
708
|
+
//
|
|
709
|
+
// Merge several reviewers' edits against one shared base DOCX. Each reviewer is
|
|
710
|
+
// base64-encoded into the `[{author,docB64}]` wire shape the host base64-DECODES
|
|
711
|
+
// (standard base64, not url-safe). Settings flow as the diff-settings JSON object
|
|
712
|
+
// extended with an optional integer `conflictResolution`.
|
|
713
|
+
/**
|
|
714
|
+
* Encode a Uint8Array to a standard (non-url-safe) base64 string. Uses a chunked
|
|
715
|
+
* binary string so large documents don't blow the call-stack limit of
|
|
716
|
+
* `String.fromCharCode(...bytes)`, and works in both browser (`btoa`) and Node
|
|
717
|
+
* (`Buffer`) hosts.
|
|
718
|
+
*/
|
|
719
|
+
function bytesToBase64(bytes) {
|
|
720
|
+
if (typeof btoa === "function") {
|
|
721
|
+
let binary = "";
|
|
722
|
+
const chunkSize = 0x8000; // 32 KB per chunk keeps the spread small
|
|
723
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
724
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
725
|
+
binary += String.fromCharCode.apply(null, chunk);
|
|
726
|
+
}
|
|
727
|
+
return btoa(binary);
|
|
728
|
+
}
|
|
729
|
+
// Node fallback (e.g. unit tests outside a browser).
|
|
730
|
+
return Buffer.from(bytes).toString("base64");
|
|
731
|
+
}
|
|
732
|
+
/** Serialize reviewers to the `[{author,docB64}]` wire JSON the host expects. */
|
|
733
|
+
async function reviewersJson(reviewers) {
|
|
734
|
+
const arr = await Promise.all(reviewers.map(async (r) => ({
|
|
735
|
+
author: r.author,
|
|
736
|
+
docB64: bytesToBase64(await toBytes(r.document)),
|
|
737
|
+
})));
|
|
738
|
+
return JSON.stringify(arr);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Serialize DocxDiffConsolidateSettings to the wire JSON the bridge parses. Same
|
|
742
|
+
* shape as {@link docxDiffSettingsJson} plus the integer `conflictResolution`
|
|
743
|
+
* when present (empty string when undefined).
|
|
744
|
+
*/
|
|
745
|
+
function docxDiffConsolidateSettingsJson(settings) {
|
|
746
|
+
return settings ? JSON.stringify(settings) : "";
|
|
747
|
+
}
|
|
748
|
+
/** Map a single revision wire object (camelCase or PascalCase) to {@link DocxDiffRevision}. */
|
|
749
|
+
function mapDocxDiffRevision(r) {
|
|
750
|
+
return {
|
|
751
|
+
revisionType: r.revisionType ?? r.RevisionType,
|
|
752
|
+
text: r.text ?? r.Text,
|
|
753
|
+
author: r.author ?? r.Author,
|
|
754
|
+
date: r.date ?? r.Date,
|
|
755
|
+
moveGroupId: r.moveGroupId ?? r.MoveGroupId ?? undefined,
|
|
756
|
+
isMoveSource: r.isMoveSource ?? r.IsMoveSource ?? undefined,
|
|
757
|
+
formatChange: (r.formatChange || r.FormatChange) ? {
|
|
758
|
+
oldProperties: r.formatChange?.oldProperties ?? r.FormatChange?.OldProperties,
|
|
759
|
+
newProperties: r.formatChange?.newProperties ?? r.FormatChange?.NewProperties,
|
|
760
|
+
changedPropertyNames: r.formatChange?.changedPropertyNames ?? r.FormatChange?.ChangedPropertyNames,
|
|
761
|
+
} : undefined,
|
|
762
|
+
leftAnchor: r.leftAnchor ?? r.LeftAnchor ?? undefined,
|
|
763
|
+
rightAnchor: r.rightAnchor ?? r.RightAnchor ?? undefined,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Consolidate several reviewers' edits against a shared base DOCX and return the
|
|
768
|
+
* merged redlined result as a DOCX (native multi-author tracked-changes markup).
|
|
769
|
+
*
|
|
770
|
+
* @param base - The shared base document all reviewers edited from.
|
|
771
|
+
* @param reviewers - The reviewers' edited copies + author names.
|
|
772
|
+
* @param settings - Optional {@link DocxDiffConsolidateSettings}; omit for engine defaults.
|
|
773
|
+
* @returns Consolidated redlined DOCX as Uint8Array.
|
|
774
|
+
* @throws Error if consolidation fails.
|
|
775
|
+
*/
|
|
776
|
+
export async function docxDiffConsolidate(base, reviewers, settings) {
|
|
777
|
+
const exports = ensureInitialized();
|
|
778
|
+
const baseBytes = await toBytes(base);
|
|
779
|
+
const reviewersJsonStr = await reviewersJson(reviewers);
|
|
780
|
+
await yieldToMain();
|
|
781
|
+
const result = exports.DocxDiffBridge.Consolidate(baseBytes, reviewersJsonStr, docxDiffConsolidateSettingsJson(settings));
|
|
782
|
+
if (result.length === 0) {
|
|
783
|
+
throw new Error("DocxDiff consolidation failed - empty result");
|
|
784
|
+
}
|
|
785
|
+
return result;
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Consolidate several reviewers' edits against a shared base DOCX and return the
|
|
789
|
+
* per-token conflict report — every base span two or more reviewers edited
|
|
790
|
+
* incompatibly, with each reviewer's competing variant.
|
|
791
|
+
*
|
|
792
|
+
* @param base - The shared base document all reviewers edited from.
|
|
793
|
+
* @param reviewers - The reviewers' edited copies + author names.
|
|
794
|
+
* @param settings - Optional {@link DocxDiffConsolidateSettings}; omit for engine defaults.
|
|
795
|
+
* @returns Array of {@link DocxDiffConflict}.
|
|
796
|
+
* @throws Error if the operation fails.
|
|
797
|
+
*/
|
|
798
|
+
export async function docxDiffGetConflicts(base, reviewers, settings) {
|
|
799
|
+
const exports = ensureInitialized();
|
|
800
|
+
const baseBytes = await toBytes(base);
|
|
801
|
+
const reviewersJsonStr = await reviewersJson(reviewers);
|
|
802
|
+
await yieldToMain();
|
|
803
|
+
const result = exports.DocxDiffBridge.GetConflictsJson(baseBytes, reviewersJsonStr, docxDiffConsolidateSettingsJson(settings));
|
|
804
|
+
if (isErrorResponse(result)) {
|
|
805
|
+
const error = parseError(result);
|
|
806
|
+
throw new Error(`Failed to get DocxDiff conflicts: ${error.error}`);
|
|
807
|
+
}
|
|
808
|
+
const parsed = JSON.parse(result);
|
|
809
|
+
return (parsed.conflicts || parsed.Conflicts || []).map((c) => ({
|
|
810
|
+
id: c.id ?? c.Id,
|
|
811
|
+
baseAnchor: c.baseAnchor ?? c.BaseAnchor,
|
|
812
|
+
tokenStart: c.tokenStart ?? c.TokenStart,
|
|
813
|
+
tokenEnd: c.tokenEnd ?? c.TokenEnd,
|
|
814
|
+
policy: c.policy ?? c.Policy,
|
|
815
|
+
competitors: (c.competitors || c.Competitors || []).map((comp) => ({
|
|
816
|
+
author: comp.author ?? comp.Author,
|
|
817
|
+
resultText: comp.resultText ?? comp.ResultText,
|
|
818
|
+
})),
|
|
819
|
+
}));
|
|
820
|
+
}
|
|
821
|
+
/**
|
|
822
|
+
* Consolidate several reviewers' edits against a shared base DOCX and return the
|
|
823
|
+
* merged revision list — each revision carrying its author, block anchors, and
|
|
824
|
+
* (when contested) the {@link DocxDiffConsolidatedRevision.conflictId} linking it
|
|
825
|
+
* to a {@link DocxDiffConflict}.
|
|
826
|
+
*
|
|
827
|
+
* @param base - The shared base document all reviewers edited from.
|
|
828
|
+
* @param reviewers - The reviewers' edited copies + author names.
|
|
829
|
+
* @param settings - Optional {@link DocxDiffConsolidateSettings}; omit for engine defaults.
|
|
830
|
+
* @returns Array of {@link DocxDiffConsolidatedRevision}.
|
|
831
|
+
* @throws Error if the operation fails.
|
|
832
|
+
*/
|
|
833
|
+
export async function docxDiffGetConsolidatedRevisions(base, reviewers, settings) {
|
|
834
|
+
const exports = ensureInitialized();
|
|
835
|
+
const baseBytes = await toBytes(base);
|
|
836
|
+
const reviewersJsonStr = await reviewersJson(reviewers);
|
|
837
|
+
await yieldToMain();
|
|
838
|
+
const result = exports.DocxDiffBridge.GetConsolidatedRevisionsJson(baseBytes, reviewersJsonStr, docxDiffConsolidateSettingsJson(settings));
|
|
839
|
+
if (isErrorResponse(result)) {
|
|
840
|
+
const error = parseError(result);
|
|
841
|
+
throw new Error(`Failed to get DocxDiff consolidated revisions: ${error.error}`);
|
|
842
|
+
}
|
|
843
|
+
const parsed = JSON.parse(result);
|
|
844
|
+
return (parsed.revisions || parsed.Revisions || []).map((r) => ({
|
|
845
|
+
...mapDocxDiffRevision(r),
|
|
846
|
+
conflictId: r.conflictId ?? r.ConflictId ?? undefined,
|
|
847
|
+
}));
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Consolidate several reviewers' edits against a shared base DOCX and return the
|
|
851
|
+
* merged edit script as a JSON string — the diff-as-data view of the
|
|
852
|
+
* consolidation (the anchor-addressed list of composite block operations).
|
|
853
|
+
*
|
|
854
|
+
* @param base - The shared base document all reviewers edited from.
|
|
855
|
+
* @param reviewers - The reviewers' edited copies + author names.
|
|
856
|
+
* @param settings - Optional {@link DocxDiffConsolidateSettings}; omit for engine defaults.
|
|
857
|
+
* @returns The consolidated edit script serialized as indented JSON.
|
|
858
|
+
* @throws Error if the operation fails.
|
|
859
|
+
*/
|
|
860
|
+
export async function docxDiffGetConsolidatedEditScript(base, reviewers, settings) {
|
|
861
|
+
const exports = ensureInitialized();
|
|
862
|
+
const baseBytes = await toBytes(base);
|
|
863
|
+
const reviewersJsonStr = await reviewersJson(reviewers);
|
|
864
|
+
await yieldToMain();
|
|
865
|
+
const result = exports.DocxDiffBridge.GetConsolidatedEditScriptJson(baseBytes, reviewersJsonStr, docxDiffConsolidateSettingsJson(settings));
|
|
866
|
+
if (isErrorResponse(result)) {
|
|
867
|
+
const error = parseError(result);
|
|
868
|
+
throw new Error(`Failed to get DocxDiff consolidated edit script: ${error.error}`);
|
|
869
|
+
}
|
|
870
|
+
return result;
|
|
871
|
+
}
|
|
872
|
+
/**
|
|
873
|
+
* Get version information about the library.
|
|
874
|
+
*/
|
|
875
|
+
export function getVersion() {
|
|
876
|
+
const exports = ensureInitialized();
|
|
877
|
+
const result = exports.DocumentConverter.GetVersion();
|
|
878
|
+
const parsed = JSON.parse(result);
|
|
879
|
+
return {
|
|
880
|
+
library: parsed.Library || parsed.library,
|
|
881
|
+
dotnetVersion: parsed.DotnetVersion || parsed.dotnetVersion,
|
|
882
|
+
platform: parsed.Platform || parsed.platform,
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
/**
|
|
886
|
+
* Check if the WASM runtime is initialized.
|
|
887
|
+
*/
|
|
888
|
+
export function isInitialized() {
|
|
889
|
+
return wasmExports !== null;
|
|
890
|
+
}
|
|
891
|
+
/**
|
|
892
|
+
* The raw WASM bridge exports (DocumentConverter, DocxSessionBridge, ...).
|
|
893
|
+
*
|
|
894
|
+
* For consumers that drive a bridge class directly — most notably
|
|
895
|
+
* `DocxEditor.open(container, bytes, exports)`, which needs the exports object
|
|
896
|
+
* rather than the wrapped functions in this module. Requires `initialize()` to
|
|
897
|
+
* have completed; throws otherwise.
|
|
898
|
+
*/
|
|
899
|
+
export function getWasmExports() {
|
|
900
|
+
return ensureInitialized();
|
|
901
|
+
}
|
|
902
|
+
/**
|
|
903
|
+
* Get all annotations from a document.
|
|
904
|
+
*
|
|
905
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
906
|
+
* @returns Array of annotations
|
|
907
|
+
* @throws Error if operation fails
|
|
908
|
+
*
|
|
909
|
+
* @example
|
|
910
|
+
* ```typescript
|
|
911
|
+
* const annotations = await getAnnotations(docxFile);
|
|
912
|
+
* for (const annot of annotations) {
|
|
913
|
+
* console.log(`${annot.label}: "${annot.annotatedText}"`);
|
|
914
|
+
* }
|
|
915
|
+
* ```
|
|
916
|
+
*/
|
|
917
|
+
export async function getAnnotations(document) {
|
|
918
|
+
const exports = ensureInitialized();
|
|
919
|
+
const bytes = await toBytes(document);
|
|
920
|
+
const result = exports.DocumentConverter.GetAnnotations(bytes);
|
|
921
|
+
if (isErrorResponse(result)) {
|
|
922
|
+
const error = parseError(result);
|
|
923
|
+
throw new Error(`Failed to get annotations: ${error.error}`);
|
|
924
|
+
}
|
|
925
|
+
const parsed = JSON.parse(result);
|
|
926
|
+
return (parsed.Annotations || parsed.annotations || []).map((a) => ({
|
|
927
|
+
id: a.Id || a.id,
|
|
928
|
+
labelId: a.LabelId || a.labelId,
|
|
929
|
+
label: a.Label || a.label,
|
|
930
|
+
color: a.Color || a.color,
|
|
931
|
+
author: a.Author || a.author,
|
|
932
|
+
created: a.Created || a.created,
|
|
933
|
+
bookmarkName: a.BookmarkName || a.bookmarkName,
|
|
934
|
+
startPage: a.StartPage ?? a.startPage,
|
|
935
|
+
endPage: a.EndPage ?? a.endPage,
|
|
936
|
+
annotatedText: a.AnnotatedText || a.annotatedText,
|
|
937
|
+
metadata: a.Metadata || a.metadata,
|
|
938
|
+
}));
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* Add an annotation to a document.
|
|
942
|
+
*
|
|
943
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
944
|
+
* @param request - Annotation details including search text or paragraph indices
|
|
945
|
+
* @returns Response with modified document bytes and annotation info
|
|
946
|
+
* @throws Error if operation fails
|
|
947
|
+
*
|
|
948
|
+
* @example
|
|
949
|
+
* ```typescript
|
|
950
|
+
* // Annotate by searching for text
|
|
951
|
+
* const result = await addAnnotation(docxFile, {
|
|
952
|
+
* id: "annot-1",
|
|
953
|
+
* labelId: "CLAUSE_A",
|
|
954
|
+
* label: "Important Clause",
|
|
955
|
+
* color: "#FFEB3B",
|
|
956
|
+
* searchText: "shall not be liable",
|
|
957
|
+
* occurrence: 1
|
|
958
|
+
* });
|
|
959
|
+
*
|
|
960
|
+
* // Annotate by paragraph range
|
|
961
|
+
* const result = await addAnnotation(docxFile, {
|
|
962
|
+
* id: "annot-2",
|
|
963
|
+
* labelId: "SECTION_1",
|
|
964
|
+
* label: "Introduction",
|
|
965
|
+
* color: "#4CAF50",
|
|
966
|
+
* startParagraphIndex: 0,
|
|
967
|
+
* endParagraphIndex: 2
|
|
968
|
+
* });
|
|
969
|
+
*
|
|
970
|
+
* // Get modified document
|
|
971
|
+
* const modifiedDocBytes = base64ToBytes(result.documentBytes);
|
|
972
|
+
* ```
|
|
973
|
+
*/
|
|
974
|
+
export async function addAnnotation(document, request) {
|
|
975
|
+
const exports = ensureInitialized();
|
|
976
|
+
const bytes = await toBytes(document);
|
|
977
|
+
// Yield to browser before WASM work - allows loading states to render
|
|
978
|
+
await yieldToMain();
|
|
979
|
+
const requestJson = JSON.stringify({
|
|
980
|
+
Id: request.id,
|
|
981
|
+
LabelId: request.labelId,
|
|
982
|
+
Label: request.label,
|
|
983
|
+
Color: request.color ?? "#FFEB3B",
|
|
984
|
+
Author: request.author,
|
|
985
|
+
SearchText: request.searchText,
|
|
986
|
+
Occurrence: request.occurrence ?? 1,
|
|
987
|
+
StartParagraphIndex: request.startParagraphIndex,
|
|
988
|
+
EndParagraphIndex: request.endParagraphIndex,
|
|
989
|
+
Metadata: request.metadata,
|
|
990
|
+
});
|
|
991
|
+
const result = exports.DocumentConverter.AddAnnotation(bytes, requestJson);
|
|
992
|
+
if (isErrorResponse(result)) {
|
|
993
|
+
const error = parseError(result);
|
|
994
|
+
throw new Error(`Failed to add annotation: ${error.error}`);
|
|
995
|
+
}
|
|
996
|
+
const parsed = JSON.parse(result);
|
|
997
|
+
const annotation = parsed.Annotation || parsed.annotation;
|
|
998
|
+
return {
|
|
999
|
+
success: parsed.Success ?? parsed.success ?? true,
|
|
1000
|
+
documentBytes: parsed.DocumentBytes || parsed.documentBytes,
|
|
1001
|
+
annotation: annotation ? {
|
|
1002
|
+
id: annotation.Id || annotation.id,
|
|
1003
|
+
labelId: annotation.LabelId || annotation.labelId,
|
|
1004
|
+
label: annotation.Label || annotation.label,
|
|
1005
|
+
color: annotation.Color || annotation.color,
|
|
1006
|
+
author: annotation.Author || annotation.author,
|
|
1007
|
+
created: annotation.Created || annotation.created,
|
|
1008
|
+
bookmarkName: annotation.BookmarkName || annotation.bookmarkName,
|
|
1009
|
+
annotatedText: annotation.AnnotatedText || annotation.annotatedText,
|
|
1010
|
+
} : undefined,
|
|
1011
|
+
};
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Remove an annotation from a document.
|
|
1015
|
+
*
|
|
1016
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1017
|
+
* @param annotationId - The ID of the annotation to remove
|
|
1018
|
+
* @returns Response with modified document bytes
|
|
1019
|
+
* @throws Error if operation fails
|
|
1020
|
+
*
|
|
1021
|
+
* @example
|
|
1022
|
+
* ```typescript
|
|
1023
|
+
* const result = await removeAnnotation(docxFile, "annot-1");
|
|
1024
|
+
* const modifiedDocBytes = base64ToBytes(result.documentBytes);
|
|
1025
|
+
* ```
|
|
1026
|
+
*/
|
|
1027
|
+
export async function removeAnnotation(document, annotationId) {
|
|
1028
|
+
const exports = ensureInitialized();
|
|
1029
|
+
const bytes = await toBytes(document);
|
|
1030
|
+
const result = exports.DocumentConverter.RemoveAnnotation(bytes, annotationId);
|
|
1031
|
+
if (isErrorResponse(result)) {
|
|
1032
|
+
const error = parseError(result);
|
|
1033
|
+
throw new Error(`Failed to remove annotation: ${error.error}`);
|
|
1034
|
+
}
|
|
1035
|
+
const parsed = JSON.parse(result);
|
|
1036
|
+
return {
|
|
1037
|
+
success: parsed.Success ?? parsed.success ?? true,
|
|
1038
|
+
documentBytes: parsed.DocumentBytes || parsed.documentBytes,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* Check if a document has any annotations.
|
|
1043
|
+
*
|
|
1044
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1045
|
+
* @returns true if the document has annotations
|
|
1046
|
+
* @throws Error if operation fails
|
|
1047
|
+
*
|
|
1048
|
+
* @example
|
|
1049
|
+
* ```typescript
|
|
1050
|
+
* if (await hasAnnotations(docxFile)) {
|
|
1051
|
+
* const annotations = await getAnnotations(docxFile);
|
|
1052
|
+
* console.log(`Document has ${annotations.length} annotations`);
|
|
1053
|
+
* }
|
|
1054
|
+
* ```
|
|
1055
|
+
*/
|
|
1056
|
+
export async function hasAnnotations(document) {
|
|
1057
|
+
const exports = ensureInitialized();
|
|
1058
|
+
const bytes = await toBytes(document);
|
|
1059
|
+
const result = exports.DocumentConverter.HasAnnotations(bytes);
|
|
1060
|
+
if (isErrorResponse(result)) {
|
|
1061
|
+
const error = parseError(result);
|
|
1062
|
+
throw new Error(`Failed to check annotations: ${error.error}`);
|
|
1063
|
+
}
|
|
1064
|
+
const parsed = JSON.parse(result);
|
|
1065
|
+
return parsed.HasAnnotations ?? parsed.hasAnnotations ?? false;
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Get the document structure for element-based annotation targeting.
|
|
1069
|
+
*
|
|
1070
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1071
|
+
* @returns Document structure with element tree
|
|
1072
|
+
* @throws Error if operation fails
|
|
1073
|
+
*
|
|
1074
|
+
* @example
|
|
1075
|
+
* ```typescript
|
|
1076
|
+
* const structure = await getDocumentStructure(docxFile);
|
|
1077
|
+
*
|
|
1078
|
+
* // Navigate the structure tree
|
|
1079
|
+
* console.log(`Document has ${structure.root.children.length} top-level elements`);
|
|
1080
|
+
*
|
|
1081
|
+
* // Find all paragraphs
|
|
1082
|
+
* const paragraphs = getParagraphs(structure);
|
|
1083
|
+
* console.log(`Found ${paragraphs.length} paragraphs`);
|
|
1084
|
+
*
|
|
1085
|
+
* // Find all tables
|
|
1086
|
+
* const tables = getTables(structure);
|
|
1087
|
+
* for (const table of tables) {
|
|
1088
|
+
* const columns = getTableColumns(structure, table.id);
|
|
1089
|
+
* console.log(`Table ${table.id} has ${columns.length} columns`);
|
|
1090
|
+
* }
|
|
1091
|
+
*
|
|
1092
|
+
* // Look up element by ID
|
|
1093
|
+
* const element = findElementById(structure, "doc/p-0");
|
|
1094
|
+
* if (element) {
|
|
1095
|
+
* console.log(`First paragraph: "${element.textPreview}"`);
|
|
1096
|
+
* }
|
|
1097
|
+
* ```
|
|
1098
|
+
*/
|
|
1099
|
+
export async function getDocumentStructure(document) {
|
|
1100
|
+
const exports = ensureInitialized();
|
|
1101
|
+
const bytes = await toBytes(document);
|
|
1102
|
+
// Yield to browser before WASM work - allows loading states to render
|
|
1103
|
+
await yieldToMain();
|
|
1104
|
+
const result = exports.DocumentConverter.GetDocumentStructure(bytes);
|
|
1105
|
+
if (isErrorResponse(result)) {
|
|
1106
|
+
const error = parseError(result);
|
|
1107
|
+
throw new Error(`Failed to get document structure: ${error.error}`);
|
|
1108
|
+
}
|
|
1109
|
+
const parsed = JSON.parse(result);
|
|
1110
|
+
// Convert from PascalCase to camelCase
|
|
1111
|
+
const convertElement = (el) => ({
|
|
1112
|
+
id: el.Id || el.id,
|
|
1113
|
+
anchorId: el.AnchorId || el.anchorId,
|
|
1114
|
+
type: el.Type || el.type,
|
|
1115
|
+
textPreview: el.TextPreview || el.textPreview,
|
|
1116
|
+
index: el.Index ?? el.index,
|
|
1117
|
+
rowIndex: el.RowIndex ?? el.rowIndex,
|
|
1118
|
+
columnIndex: el.ColumnIndex ?? el.columnIndex,
|
|
1119
|
+
rowSpan: el.RowSpan ?? el.rowSpan,
|
|
1120
|
+
columnSpan: el.ColumnSpan ?? el.columnSpan,
|
|
1121
|
+
children: (el.Children || el.children || []).map(convertElement),
|
|
1122
|
+
});
|
|
1123
|
+
const convertTableColumn = (col) => ({
|
|
1124
|
+
tableId: col.TableId || col.tableId,
|
|
1125
|
+
anchorId: col.AnchorId || col.anchorId,
|
|
1126
|
+
tableAnchorId: col.TableAnchorId || col.tableAnchorId,
|
|
1127
|
+
isVirtual: col.IsVirtual ?? col.isVirtual ?? false,
|
|
1128
|
+
columnIndex: col.ColumnIndex ?? col.columnIndex,
|
|
1129
|
+
cellIds: col.CellIds || col.cellIds || [],
|
|
1130
|
+
cellAnchorIds: col.CellAnchorIds || col.cellAnchorIds || [],
|
|
1131
|
+
rowCount: col.RowCount ?? col.rowCount,
|
|
1132
|
+
});
|
|
1133
|
+
const root = convertElement(parsed.Root || parsed.root);
|
|
1134
|
+
// Convert elementsById dictionary
|
|
1135
|
+
const elementsById = {};
|
|
1136
|
+
const rawElementsById = parsed.ElementsById || parsed.elementsById || {};
|
|
1137
|
+
for (const [key, el] of Object.entries(rawElementsById)) {
|
|
1138
|
+
elementsById[key] = convertElement(el);
|
|
1139
|
+
}
|
|
1140
|
+
// Convert tableColumns dictionary
|
|
1141
|
+
const tableColumns = {};
|
|
1142
|
+
const rawTableColumns = parsed.TableColumns || parsed.tableColumns || {};
|
|
1143
|
+
for (const [key, col] of Object.entries(rawTableColumns)) {
|
|
1144
|
+
tableColumns[key] = convertTableColumn(col);
|
|
1145
|
+
}
|
|
1146
|
+
return {
|
|
1147
|
+
root,
|
|
1148
|
+
elementsById,
|
|
1149
|
+
tableColumns,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
/**
|
|
1153
|
+
* Get document metadata for lazy loading pagination.
|
|
1154
|
+
* This is a fast operation that extracts structure information without full HTML rendering.
|
|
1155
|
+
*
|
|
1156
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1157
|
+
* @returns Document metadata including sections, dimensions, and content counts
|
|
1158
|
+
* @throws Error if operation fails
|
|
1159
|
+
*
|
|
1160
|
+
* @example
|
|
1161
|
+
* ```typescript
|
|
1162
|
+
* const metadata = await getDocumentMetadata(docxFile);
|
|
1163
|
+
*
|
|
1164
|
+
* // Check document overview
|
|
1165
|
+
* console.log(`Document has ${metadata.totalParagraphs} paragraphs`);
|
|
1166
|
+
* console.log(`Document has ${metadata.sections.length} sections`);
|
|
1167
|
+
* console.log(`Estimated ${metadata.estimatedPageCount} pages`);
|
|
1168
|
+
*
|
|
1169
|
+
* // Check section properties
|
|
1170
|
+
* for (const section of metadata.sections) {
|
|
1171
|
+
* console.log(`Section ${section.sectionIndex}: ${section.pageWidthPt}x${section.pageHeightPt}pt`);
|
|
1172
|
+
* console.log(` Paragraphs: ${section.paragraphCount}, Tables: ${section.tableCount}`);
|
|
1173
|
+
* console.log(` Has header: ${section.hasHeader}, Has footer: ${section.hasFooter}`);
|
|
1174
|
+
* }
|
|
1175
|
+
*
|
|
1176
|
+
* // Check document features
|
|
1177
|
+
* if (metadata.hasTrackedChanges) {
|
|
1178
|
+
* console.log("Document has tracked changes");
|
|
1179
|
+
* }
|
|
1180
|
+
* if (metadata.hasFootnotes) {
|
|
1181
|
+
* console.log("Document has footnotes");
|
|
1182
|
+
* }
|
|
1183
|
+
* ```
|
|
1184
|
+
*/
|
|
1185
|
+
export async function getDocumentMetadata(document) {
|
|
1186
|
+
const exports = ensureInitialized();
|
|
1187
|
+
const bytes = await toBytes(document);
|
|
1188
|
+
// Yield to browser before WASM work - allows loading states to render
|
|
1189
|
+
await yieldToMain();
|
|
1190
|
+
const result = exports.DocumentConverter.GetDocumentMetadata(bytes);
|
|
1191
|
+
if (isErrorResponse(result)) {
|
|
1192
|
+
const error = parseError(result);
|
|
1193
|
+
throw new Error(`Failed to get document metadata: ${error.error}`);
|
|
1194
|
+
}
|
|
1195
|
+
const parsed = JSON.parse(result);
|
|
1196
|
+
// Convert from PascalCase to camelCase
|
|
1197
|
+
const convertSection = (s) => ({
|
|
1198
|
+
sectionIndex: s.SectionIndex ?? s.sectionIndex,
|
|
1199
|
+
pageWidthPt: s.PageWidthPt ?? s.pageWidthPt,
|
|
1200
|
+
pageHeightPt: s.PageHeightPt ?? s.pageHeightPt,
|
|
1201
|
+
marginTopPt: s.MarginTopPt ?? s.marginTopPt,
|
|
1202
|
+
marginRightPt: s.MarginRightPt ?? s.marginRightPt,
|
|
1203
|
+
marginBottomPt: s.MarginBottomPt ?? s.marginBottomPt,
|
|
1204
|
+
marginLeftPt: s.MarginLeftPt ?? s.marginLeftPt,
|
|
1205
|
+
contentWidthPt: s.ContentWidthPt ?? s.contentWidthPt,
|
|
1206
|
+
contentHeightPt: s.ContentHeightPt ?? s.contentHeightPt,
|
|
1207
|
+
headerPt: s.HeaderPt ?? s.headerPt,
|
|
1208
|
+
footerPt: s.FooterPt ?? s.footerPt,
|
|
1209
|
+
paragraphCount: s.ParagraphCount ?? s.paragraphCount,
|
|
1210
|
+
tableCount: s.TableCount ?? s.tableCount,
|
|
1211
|
+
hasHeader: s.HasHeader ?? s.hasHeader,
|
|
1212
|
+
hasFooter: s.HasFooter ?? s.hasFooter,
|
|
1213
|
+
hasFirstPageHeader: s.HasFirstPageHeader ?? s.hasFirstPageHeader,
|
|
1214
|
+
hasFirstPageFooter: s.HasFirstPageFooter ?? s.hasFirstPageFooter,
|
|
1215
|
+
hasEvenPageHeader: s.HasEvenPageHeader ?? s.hasEvenPageHeader,
|
|
1216
|
+
hasEvenPageFooter: s.HasEvenPageFooter ?? s.hasEvenPageFooter,
|
|
1217
|
+
startParagraphIndex: s.StartParagraphIndex ?? s.startParagraphIndex,
|
|
1218
|
+
endParagraphIndex: s.EndParagraphIndex ?? s.endParagraphIndex,
|
|
1219
|
+
startTableIndex: s.StartTableIndex ?? s.startTableIndex,
|
|
1220
|
+
endTableIndex: s.EndTableIndex ?? s.endTableIndex,
|
|
1221
|
+
});
|
|
1222
|
+
return {
|
|
1223
|
+
sections: (parsed.Sections || parsed.sections || []).map(convertSection),
|
|
1224
|
+
totalParagraphs: parsed.TotalParagraphs ?? parsed.totalParagraphs,
|
|
1225
|
+
totalTables: parsed.TotalTables ?? parsed.totalTables,
|
|
1226
|
+
hasFootnotes: parsed.HasFootnotes ?? parsed.hasFootnotes,
|
|
1227
|
+
hasEndnotes: parsed.HasEndnotes ?? parsed.hasEndnotes,
|
|
1228
|
+
hasTrackedChanges: parsed.HasTrackedChanges ?? parsed.hasTrackedChanges,
|
|
1229
|
+
hasComments: parsed.HasComments ?? parsed.hasComments,
|
|
1230
|
+
estimatedPageCount: parsed.EstimatedPageCount ?? parsed.estimatedPageCount,
|
|
1231
|
+
estimatedPageCountSource: parsed.EstimatedPageCountSource ?? parsed.estimatedPageCountSource ?? "heuristic",
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Export document to OpenContracts format.
|
|
1236
|
+
*
|
|
1237
|
+
* This provides complete document text, structure, and layout information
|
|
1238
|
+
* compatible with the OpenContracts ecosystem for document analysis.
|
|
1239
|
+
*
|
|
1240
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1241
|
+
* @returns OpenContractDocExport with complete document data
|
|
1242
|
+
* @throws Error if export fails
|
|
1243
|
+
*
|
|
1244
|
+
* @example
|
|
1245
|
+
* ```typescript
|
|
1246
|
+
* const result = await exportToOpenContract(docxFile);
|
|
1247
|
+
*
|
|
1248
|
+
* // Access complete document text
|
|
1249
|
+
* console.log(`Content length: ${result.content.length} characters`);
|
|
1250
|
+
*
|
|
1251
|
+
* // Get document structure
|
|
1252
|
+
* console.log(`Pages: ${result.pageCount}`);
|
|
1253
|
+
* console.log(`Structural annotations: ${result.labelledText.filter(a => a.structural).length}`);
|
|
1254
|
+
*
|
|
1255
|
+
* // Access PAWLS layout data
|
|
1256
|
+
* for (const page of result.pawlsFileContent) {
|
|
1257
|
+
* console.log(`Page ${page.page.index}: ${page.tokens.length} tokens`);
|
|
1258
|
+
* }
|
|
1259
|
+
* ```
|
|
1260
|
+
*/
|
|
1261
|
+
export async function exportToOpenContract(document) {
|
|
1262
|
+
const exports = ensureInitialized();
|
|
1263
|
+
const bytes = await toBytes(document);
|
|
1264
|
+
// Yield to browser before WASM work - allows loading states to render
|
|
1265
|
+
await yieldToMain();
|
|
1266
|
+
const result = exports.DocumentConverter.ExportToOpenContract(bytes);
|
|
1267
|
+
if (isErrorResponse(result)) {
|
|
1268
|
+
const error = parseError(result);
|
|
1269
|
+
throw new Error(`Failed to export to OpenContract format: ${error.error}`);
|
|
1270
|
+
}
|
|
1271
|
+
const parsed = JSON.parse(result);
|
|
1272
|
+
// Convert from PascalCase to camelCase
|
|
1273
|
+
const convertPawlsPage = (p) => ({
|
|
1274
|
+
page: {
|
|
1275
|
+
width: p.Page?.Width ?? p.page?.width,
|
|
1276
|
+
height: p.Page?.Height ?? p.page?.height,
|
|
1277
|
+
index: p.Page?.Index ?? p.page?.index,
|
|
1278
|
+
},
|
|
1279
|
+
tokens: (p.Tokens || p.tokens || []).map((t) => ({
|
|
1280
|
+
x: t.X ?? t.x,
|
|
1281
|
+
y: t.Y ?? t.y,
|
|
1282
|
+
width: t.Width ?? t.width,
|
|
1283
|
+
height: t.Height ?? t.height,
|
|
1284
|
+
text: t.Text ?? t.text,
|
|
1285
|
+
})),
|
|
1286
|
+
});
|
|
1287
|
+
const convertAnnotation = (a) => ({
|
|
1288
|
+
id: a.Id ?? a.id,
|
|
1289
|
+
annotationLabel: a.AnnotationLabel ?? a.annotationLabel,
|
|
1290
|
+
rawText: a.RawText ?? a.rawText,
|
|
1291
|
+
page: a.Page ?? a.page,
|
|
1292
|
+
annotationJson: convertAnnotationJson(a.AnnotationJson ?? a.annotationJson),
|
|
1293
|
+
parentId: a.ParentId ?? a.parentId,
|
|
1294
|
+
annotationType: a.AnnotationType ?? a.annotationType,
|
|
1295
|
+
structural: a.Structural ?? a.structural,
|
|
1296
|
+
});
|
|
1297
|
+
const convertAnnotationJson = (json) => {
|
|
1298
|
+
if (!json)
|
|
1299
|
+
return undefined;
|
|
1300
|
+
// Check if it's a TextSpan
|
|
1301
|
+
if (json.Start !== undefined || json.start !== undefined) {
|
|
1302
|
+
return {
|
|
1303
|
+
id: json.Id ?? json.id,
|
|
1304
|
+
start: json.Start ?? json.start,
|
|
1305
|
+
end: json.End ?? json.end,
|
|
1306
|
+
text: json.Text ?? json.text,
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
// Otherwise it's a dictionary of single-page annotations
|
|
1310
|
+
const result = {};
|
|
1311
|
+
for (const [key, value] of Object.entries(json)) {
|
|
1312
|
+
const v = value;
|
|
1313
|
+
result[key] = {
|
|
1314
|
+
bounds: {
|
|
1315
|
+
top: v.Bounds?.Top ?? v.bounds?.top,
|
|
1316
|
+
bottom: v.Bounds?.Bottom ?? v.bounds?.bottom,
|
|
1317
|
+
left: v.Bounds?.Left ?? v.bounds?.left,
|
|
1318
|
+
right: v.Bounds?.Right ?? v.bounds?.right,
|
|
1319
|
+
},
|
|
1320
|
+
tokensJsons: (v.TokensJsons || v.tokensJsons || []).map((t) => ({
|
|
1321
|
+
pageIndex: t.PageIndex ?? t.pageIndex,
|
|
1322
|
+
tokenIndex: t.TokenIndex ?? t.tokenIndex,
|
|
1323
|
+
})),
|
|
1324
|
+
rawText: v.RawText ?? v.rawText,
|
|
1325
|
+
};
|
|
1326
|
+
}
|
|
1327
|
+
return result;
|
|
1328
|
+
};
|
|
1329
|
+
const convertRelationship = (r) => ({
|
|
1330
|
+
id: r.Id ?? r.id,
|
|
1331
|
+
relationshipLabel: r.RelationshipLabel ?? r.relationshipLabel,
|
|
1332
|
+
sourceAnnotationIds: r.SourceAnnotationIds ?? r.sourceAnnotationIds ?? [],
|
|
1333
|
+
targetAnnotationIds: r.TargetAnnotationIds ?? r.targetAnnotationIds ?? [],
|
|
1334
|
+
structural: r.Structural ?? r.structural,
|
|
1335
|
+
});
|
|
1336
|
+
return {
|
|
1337
|
+
title: parsed.Title ?? parsed.title,
|
|
1338
|
+
content: parsed.Content ?? parsed.content,
|
|
1339
|
+
description: parsed.Description ?? parsed.description,
|
|
1340
|
+
pageCount: parsed.PageCount ?? parsed.pageCount,
|
|
1341
|
+
pawlsFileContent: (parsed.PawlsFileContent || parsed.pawlsFileContent || []).map(convertPawlsPage),
|
|
1342
|
+
docLabels: parsed.DocLabels ?? parsed.docLabels ?? [],
|
|
1343
|
+
labelledText: (parsed.LabelledText || parsed.labelledText || []).map(convertAnnotation),
|
|
1344
|
+
relationships: (parsed.Relationships || parsed.relationships)?.map(convertRelationship),
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
/**
|
|
1348
|
+
* Convert a DOCX file to an anchor-addressed Markdown projection.
|
|
1349
|
+
*
|
|
1350
|
+
* The projection is a deterministic, anchor-keyed Markdown rendering of the document,
|
|
1351
|
+
* suitable for LLM editing pipelines, structured search indexers, and diff/review UIs.
|
|
1352
|
+
* Every paragraph, heading, list item, table, table cell, footnote, endnote, and
|
|
1353
|
+
* comment is addressable by an `{#kind:scope:unid}` anchor that survives reformatting.
|
|
1354
|
+
*
|
|
1355
|
+
* See `docs/architecture/markdown_projection.md` for the projection spec.
|
|
1356
|
+
*
|
|
1357
|
+
* @param document - DOCX file as `File` or `Uint8Array`
|
|
1358
|
+
* @param settings - Optional projection settings (defaults: all scopes, anchor blocks, accept tracked changes)
|
|
1359
|
+
* @throws Error if conversion fails
|
|
1360
|
+
*
|
|
1361
|
+
* @example
|
|
1362
|
+
* ```typescript
|
|
1363
|
+
* const result = await convertWmlToMarkdown(docxFile);
|
|
1364
|
+
* console.log(result.markdown);
|
|
1365
|
+
* for (const [id, target] of Object.entries(result.anchorIndex)) {
|
|
1366
|
+
* console.log(id, target.partUri);
|
|
1367
|
+
* }
|
|
1368
|
+
* ```
|
|
1369
|
+
*/
|
|
1370
|
+
export async function convertWmlToMarkdown(document, settings = {}) {
|
|
1371
|
+
const exports = ensureInitialized();
|
|
1372
|
+
const bytes = await toBytes(document);
|
|
1373
|
+
await yieldToMain();
|
|
1374
|
+
const settingsJson = JSON.stringify({
|
|
1375
|
+
Scopes: settings.scopes ?? ProjectionScopes.All,
|
|
1376
|
+
HeadingLevelOffset: settings.headingLevelOffset ?? 0,
|
|
1377
|
+
AnchorMode: settings.anchorMode ?? AnchorRenderMode.Block,
|
|
1378
|
+
TableMode: settings.tableMode ?? TableRenderMode.GfmWithOpaqueFallback,
|
|
1379
|
+
TableInlineCellMax: settings.tableInlineCellMax ?? 80,
|
|
1380
|
+
TrackedChanges: settings.trackedChanges ?? TrackedChangeMode.Accept,
|
|
1381
|
+
ResolveNumbering: settings.resolveNumbering ?? true,
|
|
1382
|
+
EmptyParagraphs: settings.emptyParagraphs ?? EmptyParagraphMode.AnchorOnly,
|
|
1383
|
+
});
|
|
1384
|
+
const result = exports.DocumentConverter.ConvertWmlToMarkdown(bytes, settingsJson);
|
|
1385
|
+
if (isErrorResponse(result)) {
|
|
1386
|
+
const error = parseError(result);
|
|
1387
|
+
throw new Error(`Failed to convert document to markdown: ${error.error}`);
|
|
1388
|
+
}
|
|
1389
|
+
const parsed = JSON.parse(result);
|
|
1390
|
+
const rawIndex = parsed.AnchorIndex ?? parsed.anchorIndex ?? {};
|
|
1391
|
+
const anchorIndex = {};
|
|
1392
|
+
for (const [key, value] of Object.entries(rawIndex)) {
|
|
1393
|
+
const v = value;
|
|
1394
|
+
anchorIndex[key] = {
|
|
1395
|
+
id: v.Id ?? v.id,
|
|
1396
|
+
kind: v.Kind ?? v.kind,
|
|
1397
|
+
scope: v.Scope ?? v.scope,
|
|
1398
|
+
unid: v.Unid ?? v.unid,
|
|
1399
|
+
partUri: v.PartUri ?? v.partUri,
|
|
1400
|
+
textPreview: v.TextPreview ?? v.textPreview ?? "",
|
|
1401
|
+
};
|
|
1402
|
+
}
|
|
1403
|
+
return {
|
|
1404
|
+
markdown: parsed.Markdown ?? parsed.markdown ?? "",
|
|
1405
|
+
anchorIndex,
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
/**
|
|
1409
|
+
* Add an annotation using flexible targeting (element ID, indices, or text search).
|
|
1410
|
+
*
|
|
1411
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1412
|
+
* @param request - Annotation details with target specification
|
|
1413
|
+
* @returns Response with modified document bytes and annotation info
|
|
1414
|
+
* @throws Error if operation fails
|
|
1415
|
+
*
|
|
1416
|
+
* @example
|
|
1417
|
+
* ```typescript
|
|
1418
|
+
* // First get the document structure to find target elements
|
|
1419
|
+
* const structure = await getDocumentStructure(docxFile);
|
|
1420
|
+
*
|
|
1421
|
+
* // Annotate a specific paragraph by element ID
|
|
1422
|
+
* const result1 = await addAnnotationWithTarget(docxFile, {
|
|
1423
|
+
* id: "annot-1",
|
|
1424
|
+
* labelId: "INTRO",
|
|
1425
|
+
* label: "Introduction",
|
|
1426
|
+
* color: "#4CAF50",
|
|
1427
|
+
* target: targetElement("doc/p-0")
|
|
1428
|
+
* });
|
|
1429
|
+
*
|
|
1430
|
+
* // Annotate a table cell
|
|
1431
|
+
* const result2 = await addAnnotationWithTarget(docxFile, {
|
|
1432
|
+
* id: "annot-2",
|
|
1433
|
+
* labelId: "CELL_HIGHLIGHT",
|
|
1434
|
+
* label: "Important Cell",
|
|
1435
|
+
* color: "#FFEB3B",
|
|
1436
|
+
* target: targetTableCell(0, 1, 2) // Table 0, Row 1, Cell 2
|
|
1437
|
+
* });
|
|
1438
|
+
*
|
|
1439
|
+
* // Annotate a table column
|
|
1440
|
+
* const result3 = await addAnnotationWithTarget(docxFile, {
|
|
1441
|
+
* id: "annot-3",
|
|
1442
|
+
* labelId: "COLUMN_DATA",
|
|
1443
|
+
* label: "Values Column",
|
|
1444
|
+
* color: "#2196F3",
|
|
1445
|
+
* target: targetTableColumn(0, 1) // Table 0, Column 1
|
|
1446
|
+
* });
|
|
1447
|
+
*
|
|
1448
|
+
* // Search for text within a specific element
|
|
1449
|
+
* const result4 = await addAnnotationWithTarget(docxFile, {
|
|
1450
|
+
* id: "annot-4",
|
|
1451
|
+
* labelId: "KEYWORD",
|
|
1452
|
+
* label: "Keyword",
|
|
1453
|
+
* color: "#FF5722",
|
|
1454
|
+
* target: targetSearchInElement("doc/p-2", "important", 1)
|
|
1455
|
+
* });
|
|
1456
|
+
* ```
|
|
1457
|
+
*/
|
|
1458
|
+
export async function addAnnotationWithTarget(document, request) {
|
|
1459
|
+
const exports = ensureInitialized();
|
|
1460
|
+
const bytes = await toBytes(document);
|
|
1461
|
+
// Yield to browser before WASM work - allows loading states to render
|
|
1462
|
+
await yieldToMain();
|
|
1463
|
+
const requestJson = JSON.stringify({
|
|
1464
|
+
Id: request.id,
|
|
1465
|
+
LabelId: request.labelId,
|
|
1466
|
+
Label: request.label,
|
|
1467
|
+
Color: request.color ?? "#FFEB3B",
|
|
1468
|
+
Author: request.author,
|
|
1469
|
+
Metadata: request.metadata,
|
|
1470
|
+
ElementId: request.target.elementId,
|
|
1471
|
+
ElementType: request.target.elementType,
|
|
1472
|
+
ParagraphIndex: request.target.paragraphIndex,
|
|
1473
|
+
RunIndex: request.target.runIndex,
|
|
1474
|
+
TableIndex: request.target.tableIndex,
|
|
1475
|
+
RowIndex: request.target.rowIndex,
|
|
1476
|
+
CellIndex: request.target.cellIndex,
|
|
1477
|
+
ColumnIndex: request.target.columnIndex,
|
|
1478
|
+
SearchText: request.target.searchText,
|
|
1479
|
+
Occurrence: request.target.occurrence ?? 1,
|
|
1480
|
+
RangeEndParagraphIndex: request.target.rangeEndParagraphIndex,
|
|
1481
|
+
});
|
|
1482
|
+
const result = exports.DocumentConverter.AddAnnotationWithTarget(bytes, requestJson);
|
|
1483
|
+
if (isErrorResponse(result)) {
|
|
1484
|
+
const error = parseError(result);
|
|
1485
|
+
throw new Error(`Failed to add annotation: ${error.error}`);
|
|
1486
|
+
}
|
|
1487
|
+
const parsed = JSON.parse(result);
|
|
1488
|
+
const annotation = parsed.Annotation || parsed.annotation;
|
|
1489
|
+
return {
|
|
1490
|
+
success: parsed.Success ?? parsed.success ?? true,
|
|
1491
|
+
documentBytes: parsed.DocumentBytes || parsed.documentBytes,
|
|
1492
|
+
annotation: annotation ? {
|
|
1493
|
+
id: annotation.Id || annotation.id,
|
|
1494
|
+
labelId: annotation.LabelId || annotation.labelId,
|
|
1495
|
+
label: annotation.Label || annotation.label,
|
|
1496
|
+
color: annotation.Color || annotation.color,
|
|
1497
|
+
author: annotation.Author || annotation.author,
|
|
1498
|
+
created: annotation.Created || annotation.created,
|
|
1499
|
+
bookmarkName: annotation.BookmarkName || annotation.bookmarkName,
|
|
1500
|
+
annotatedText: annotation.AnnotatedText || annotation.annotatedText,
|
|
1501
|
+
metadata: annotation.Metadata || annotation.metadata,
|
|
1502
|
+
} : undefined,
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
// ============================================================================
|
|
1506
|
+
// External Annotation Functions (Issue #57)
|
|
1507
|
+
// ============================================================================
|
|
1508
|
+
/**
|
|
1509
|
+
* Compute the SHA256 hash of a document for integrity validation.
|
|
1510
|
+
*
|
|
1511
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1512
|
+
* @returns SHA256 hash as lowercase hex string
|
|
1513
|
+
* @throws Error if operation fails
|
|
1514
|
+
*
|
|
1515
|
+
* @example
|
|
1516
|
+
* ```typescript
|
|
1517
|
+
* const hash = await computeDocumentHash(docxFile);
|
|
1518
|
+
* console.log(`Document hash: ${hash}`);
|
|
1519
|
+
*
|
|
1520
|
+
* // Later, verify the document hasn't changed
|
|
1521
|
+
* const currentHash = await computeDocumentHash(docxFile);
|
|
1522
|
+
* if (currentHash !== storedHash) {
|
|
1523
|
+
* console.log("Document has been modified");
|
|
1524
|
+
* }
|
|
1525
|
+
* ```
|
|
1526
|
+
*/
|
|
1527
|
+
export async function computeDocumentHash(document) {
|
|
1528
|
+
const exports = ensureInitialized();
|
|
1529
|
+
const bytes = await toBytes(document);
|
|
1530
|
+
const result = exports.DocumentConverter.ComputeDocumentHash(bytes);
|
|
1531
|
+
if (isErrorResponse(result)) {
|
|
1532
|
+
const error = parseError(result);
|
|
1533
|
+
throw new Error(`Failed to compute document hash: ${error.error}`);
|
|
1534
|
+
}
|
|
1535
|
+
const parsed = JSON.parse(result);
|
|
1536
|
+
return parsed.Hash ?? parsed.hash;
|
|
1537
|
+
}
|
|
1538
|
+
/**
|
|
1539
|
+
* Create an ExternalAnnotationSet from a document.
|
|
1540
|
+
* This extracts the document structure and computes the hash for integrity validation.
|
|
1541
|
+
*
|
|
1542
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1543
|
+
* @param documentId - Unique identifier for the document (filename, UUID, etc.)
|
|
1544
|
+
* @returns ExternalAnnotationSet ready for adding annotations
|
|
1545
|
+
* @throws Error if operation fails
|
|
1546
|
+
*
|
|
1547
|
+
* @example
|
|
1548
|
+
* ```typescript
|
|
1549
|
+
* // Create an annotation set
|
|
1550
|
+
* const set = await createExternalAnnotationSet(docxFile, "contract-v1.0");
|
|
1551
|
+
*
|
|
1552
|
+
* // Access document text for searching
|
|
1553
|
+
* console.log(`Document length: ${set.content.length} chars`);
|
|
1554
|
+
*
|
|
1555
|
+
* // Add label definitions
|
|
1556
|
+
* set.textLabels["IMPORTANT"] = {
|
|
1557
|
+
* id: "IMPORTANT",
|
|
1558
|
+
* text: "Important",
|
|
1559
|
+
* color: "#FF0000",
|
|
1560
|
+
* description: "Important text",
|
|
1561
|
+
* icon: "",
|
|
1562
|
+
* labelType: "text"
|
|
1563
|
+
* };
|
|
1564
|
+
*
|
|
1565
|
+
* // Create annotations using the content
|
|
1566
|
+
* const annotation = createAnnotationFromSearch(
|
|
1567
|
+
* "ann-001", "IMPORTANT", set.content, "shall not be liable"
|
|
1568
|
+
* );
|
|
1569
|
+
* if (annotation) {
|
|
1570
|
+
* set.labelledText.push(annotation);
|
|
1571
|
+
* }
|
|
1572
|
+
*
|
|
1573
|
+
* // Serialize for storage
|
|
1574
|
+
* const json = JSON.stringify(set);
|
|
1575
|
+
* ```
|
|
1576
|
+
*/
|
|
1577
|
+
export async function createExternalAnnotationSet(document, documentId) {
|
|
1578
|
+
const exports = ensureInitialized();
|
|
1579
|
+
const bytes = await toBytes(document);
|
|
1580
|
+
// Yield to browser before WASM work - allows loading states to render
|
|
1581
|
+
await yieldToMain();
|
|
1582
|
+
const result = exports.DocumentConverter.CreateExternalAnnotationSet(bytes, documentId);
|
|
1583
|
+
if (isErrorResponse(result)) {
|
|
1584
|
+
const error = parseError(result);
|
|
1585
|
+
throw new Error(`Failed to create external annotation set: ${error.error}`);
|
|
1586
|
+
}
|
|
1587
|
+
const parsed = JSON.parse(result);
|
|
1588
|
+
// Convert from PascalCase to camelCase
|
|
1589
|
+
return convertExternalAnnotationSet(parsed);
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* Validate an external annotation set against a document.
|
|
1593
|
+
* Checks hash match and verifies each annotation's text still matches.
|
|
1594
|
+
*
|
|
1595
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1596
|
+
* @param annotationSet - The annotation set to validate
|
|
1597
|
+
* @returns Validation result with any issues found
|
|
1598
|
+
* @throws Error if operation fails
|
|
1599
|
+
*
|
|
1600
|
+
* @example
|
|
1601
|
+
* ```typescript
|
|
1602
|
+
* const result = await validateExternalAnnotations(docxFile, annotationSet);
|
|
1603
|
+
*
|
|
1604
|
+
* if (!result.isValid) {
|
|
1605
|
+
* if (result.hashMismatch) {
|
|
1606
|
+
* console.log("Document has been modified since annotations were created");
|
|
1607
|
+
* }
|
|
1608
|
+
* for (const issue of result.issues) {
|
|
1609
|
+
* console.log(`${issue.issueType}: ${issue.description}`);
|
|
1610
|
+
* }
|
|
1611
|
+
* }
|
|
1612
|
+
* ```
|
|
1613
|
+
*/
|
|
1614
|
+
export async function validateExternalAnnotations(document, annotationSet) {
|
|
1615
|
+
const exports = ensureInitialized();
|
|
1616
|
+
const bytes = await toBytes(document);
|
|
1617
|
+
// Yield to browser before WASM work - allows loading states to render
|
|
1618
|
+
await yieldToMain();
|
|
1619
|
+
const annotationSetJson = JSON.stringify(annotationSet);
|
|
1620
|
+
const result = exports.DocumentConverter.ValidateExternalAnnotations(bytes, annotationSetJson);
|
|
1621
|
+
if (isErrorResponse(result)) {
|
|
1622
|
+
const error = parseError(result);
|
|
1623
|
+
throw new Error(`Failed to validate external annotations: ${error.error}`);
|
|
1624
|
+
}
|
|
1625
|
+
const parsed = JSON.parse(result);
|
|
1626
|
+
return {
|
|
1627
|
+
isValid: parsed.IsValid ?? parsed.isValid,
|
|
1628
|
+
hashMismatch: parsed.HashMismatch ?? parsed.hashMismatch,
|
|
1629
|
+
issues: (parsed.Issues || parsed.issues || []).map((i) => ({
|
|
1630
|
+
annotationId: i.AnnotationId ?? i.annotationId,
|
|
1631
|
+
issueType: i.IssueType ?? i.issueType,
|
|
1632
|
+
description: i.Description ?? i.description,
|
|
1633
|
+
expectedText: i.ExpectedText ?? i.expectedText,
|
|
1634
|
+
actualText: i.ActualText ?? i.actualText,
|
|
1635
|
+
})),
|
|
1636
|
+
};
|
|
1637
|
+
}
|
|
1638
|
+
/**
|
|
1639
|
+
* Convert a DOCX document to HTML with external annotations projected.
|
|
1640
|
+
*
|
|
1641
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1642
|
+
* @param annotationSet - The external annotation set to project
|
|
1643
|
+
* @param conversionOptions - HTML conversion options
|
|
1644
|
+
* @param projectionOptions - Annotation projection options
|
|
1645
|
+
* @returns HTML string with annotations projected
|
|
1646
|
+
* @throws Error if operation fails
|
|
1647
|
+
*
|
|
1648
|
+
* @example
|
|
1649
|
+
* ```typescript
|
|
1650
|
+
* // Basic usage
|
|
1651
|
+
* const html = await convertDocxToHtmlWithExternalAnnotations(
|
|
1652
|
+
* docxFile,
|
|
1653
|
+
* annotationSet
|
|
1654
|
+
* );
|
|
1655
|
+
*
|
|
1656
|
+
* // With custom options
|
|
1657
|
+
* const html = await convertDocxToHtmlWithExternalAnnotations(
|
|
1658
|
+
* docxFile,
|
|
1659
|
+
* annotationSet,
|
|
1660
|
+
* { pageTitle: "Annotated Document" },
|
|
1661
|
+
* { labelMode: AnnotationLabelMode.Inline, cssClassPrefix: "my-annot-" }
|
|
1662
|
+
* );
|
|
1663
|
+
* ```
|
|
1664
|
+
*/
|
|
1665
|
+
export async function convertDocxToHtmlWithExternalAnnotations(document, annotationSet, conversionOptions, projectionOptions) {
|
|
1666
|
+
const exports = ensureInitialized();
|
|
1667
|
+
const bytes = await toBytes(document);
|
|
1668
|
+
// Yield to browser before WASM work - allows loading states to render
|
|
1669
|
+
await yieldToMain();
|
|
1670
|
+
const annotationSetJson = JSON.stringify(annotationSet);
|
|
1671
|
+
const result = exports.DocumentConverter.ConvertDocxToHtmlWithExternalAnnotations(bytes, annotationSetJson, conversionOptions?.pageTitle ?? "Document", conversionOptions?.cssPrefix ?? "docx-", conversionOptions?.fabricateClasses ?? true, conversionOptions?.additionalCss ?? "", projectionOptions?.cssClassPrefix ?? "ext-annot-", projectionOptions?.labelMode ?? AnnotationLabelMode.Above);
|
|
1672
|
+
if (isErrorResponse(result)) {
|
|
1673
|
+
const error = parseError(result);
|
|
1674
|
+
throw new Error(`Failed to convert with external annotations: ${error.error}`);
|
|
1675
|
+
}
|
|
1676
|
+
const parsed = JSON.parse(result);
|
|
1677
|
+
return parsed.Html ?? parsed.html;
|
|
1678
|
+
}
|
|
1679
|
+
/**
|
|
1680
|
+
* Search for text in a document and return character offsets.
|
|
1681
|
+
* Useful for finding text locations to create annotations.
|
|
1682
|
+
*
|
|
1683
|
+
* @param document - DOCX file as File object or Uint8Array
|
|
1684
|
+
* @param searchText - Text to search for
|
|
1685
|
+
* @param maxResults - Maximum number of results (default: 100)
|
|
1686
|
+
* @returns Array of TextSpan objects with offsets
|
|
1687
|
+
* @throws Error if operation fails
|
|
1688
|
+
*
|
|
1689
|
+
* @example
|
|
1690
|
+
* ```typescript
|
|
1691
|
+
* const occurrences = await searchTextOffsets(docxFile, "liability");
|
|
1692
|
+
* console.log(`Found ${occurrences.length} occurrences`);
|
|
1693
|
+
*
|
|
1694
|
+
* for (const span of occurrences) {
|
|
1695
|
+
* console.log(`"${span.text}" at offset ${span.start}-${span.end}`);
|
|
1696
|
+
* }
|
|
1697
|
+
* ```
|
|
1698
|
+
*/
|
|
1699
|
+
export async function searchTextOffsets(document, searchText, maxResults = 100) {
|
|
1700
|
+
const exports = ensureInitialized();
|
|
1701
|
+
const bytes = await toBytes(document);
|
|
1702
|
+
// Yield to browser before WASM work - allows loading states to render
|
|
1703
|
+
await yieldToMain();
|
|
1704
|
+
const result = exports.DocumentConverter.SearchTextOffsets(bytes, searchText, maxResults);
|
|
1705
|
+
if (isErrorResponse(result)) {
|
|
1706
|
+
const error = parseError(result);
|
|
1707
|
+
throw new Error(`Failed to search text: ${error.error}`);
|
|
1708
|
+
}
|
|
1709
|
+
const parsed = JSON.parse(result);
|
|
1710
|
+
return (parsed.Results || parsed.results || []).map((r) => ({
|
|
1711
|
+
id: r.Id ?? r.id,
|
|
1712
|
+
start: r.Start ?? r.start,
|
|
1713
|
+
end: r.End ?? r.end,
|
|
1714
|
+
text: r.Text ?? r.text,
|
|
1715
|
+
}));
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Create an annotation from character offsets.
|
|
1719
|
+
* This is a client-side helper - no WASM call needed.
|
|
1720
|
+
*
|
|
1721
|
+
* @param id - Unique identifier for the annotation
|
|
1722
|
+
* @param labelId - Label/category ID for the annotation
|
|
1723
|
+
* @param documentText - Full document text (from annotationSet.content)
|
|
1724
|
+
* @param startOffset - Start character offset (0-indexed, inclusive)
|
|
1725
|
+
* @param endOffset - End character offset (exclusive)
|
|
1726
|
+
* @returns OpenContractsAnnotation ready to add to an annotation set
|
|
1727
|
+
* @throws Error if offsets are invalid
|
|
1728
|
+
*
|
|
1729
|
+
* @example
|
|
1730
|
+
* ```typescript
|
|
1731
|
+
* const set = await createExternalAnnotationSet(docxFile, "doc-1");
|
|
1732
|
+
* const annotation = createAnnotation("ann-001", "IMPORTANT", set.content, 100, 150);
|
|
1733
|
+
* set.labelledText.push(annotation);
|
|
1734
|
+
* ```
|
|
1735
|
+
*/
|
|
1736
|
+
export function createAnnotation(id, labelId, documentText, startOffset, endOffset) {
|
|
1737
|
+
if (startOffset < 0) {
|
|
1738
|
+
throw new Error("Start offset must be non-negative");
|
|
1739
|
+
}
|
|
1740
|
+
if (endOffset < startOffset) {
|
|
1741
|
+
throw new Error("End offset must be >= start offset");
|
|
1742
|
+
}
|
|
1743
|
+
if (endOffset > documentText.length) {
|
|
1744
|
+
throw new Error("End offset exceeds document length");
|
|
1745
|
+
}
|
|
1746
|
+
const rawText = documentText.substring(startOffset, endOffset);
|
|
1747
|
+
return {
|
|
1748
|
+
id,
|
|
1749
|
+
annotationLabel: labelId,
|
|
1750
|
+
rawText,
|
|
1751
|
+
page: 0,
|
|
1752
|
+
annotationJson: {
|
|
1753
|
+
id,
|
|
1754
|
+
start: startOffset,
|
|
1755
|
+
end: endOffset,
|
|
1756
|
+
text: rawText,
|
|
1757
|
+
},
|
|
1758
|
+
annotationType: "text",
|
|
1759
|
+
structural: false,
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
/**
|
|
1763
|
+
* Create an annotation by searching for text in the document.
|
|
1764
|
+
* This is a client-side helper - no WASM call needed.
|
|
1765
|
+
*
|
|
1766
|
+
* @param id - Unique identifier for the annotation
|
|
1767
|
+
* @param labelId - Label/category ID for the annotation
|
|
1768
|
+
* @param documentText - Full document text (from annotationSet.content)
|
|
1769
|
+
* @param searchText - Text to search for
|
|
1770
|
+
* @param occurrence - Which occurrence to use (1-based, default: 1)
|
|
1771
|
+
* @returns OpenContractsAnnotation, or null if text not found
|
|
1772
|
+
*
|
|
1773
|
+
* @example
|
|
1774
|
+
* ```typescript
|
|
1775
|
+
* const set = await createExternalAnnotationSet(docxFile, "doc-1");
|
|
1776
|
+
*
|
|
1777
|
+
* // Find first occurrence
|
|
1778
|
+
* const ann1 = createAnnotationFromSearch("ann-001", "LIABILITY", set.content, "shall not be liable");
|
|
1779
|
+
* if (ann1) set.labelledText.push(ann1);
|
|
1780
|
+
*
|
|
1781
|
+
* // Find second occurrence
|
|
1782
|
+
* const ann2 = createAnnotationFromSearch("ann-002", "LIABILITY", set.content, "shall not be liable", 2);
|
|
1783
|
+
* if (ann2) set.labelledText.push(ann2);
|
|
1784
|
+
* ```
|
|
1785
|
+
*/
|
|
1786
|
+
export function createAnnotationFromSearch(id, labelId, documentText, searchText, occurrence = 1) {
|
|
1787
|
+
if (occurrence < 1) {
|
|
1788
|
+
throw new Error("Occurrence must be >= 1");
|
|
1789
|
+
}
|
|
1790
|
+
const offsets = findTextOccurrences(documentText, searchText);
|
|
1791
|
+
if (occurrence > offsets.length) {
|
|
1792
|
+
return null;
|
|
1793
|
+
}
|
|
1794
|
+
const { start, end } = offsets[occurrence - 1];
|
|
1795
|
+
return createAnnotation(id, labelId, documentText, start, end);
|
|
1796
|
+
}
|
|
1797
|
+
/**
|
|
1798
|
+
* Find all occurrences of a text string in the document.
|
|
1799
|
+
* This is a client-side helper - no WASM call needed.
|
|
1800
|
+
*
|
|
1801
|
+
* @param documentText - Full document text
|
|
1802
|
+
* @param searchText - Text to search for
|
|
1803
|
+
* @param maxResults - Maximum number of results (default: 100)
|
|
1804
|
+
* @returns Array of { start, end } offsets
|
|
1805
|
+
*
|
|
1806
|
+
* @example
|
|
1807
|
+
* ```typescript
|
|
1808
|
+
* const occurrences = findTextOccurrences(set.content, "the");
|
|
1809
|
+
* console.log(`Found ${occurrences.length} occurrences of "the"`);
|
|
1810
|
+
* ```
|
|
1811
|
+
*/
|
|
1812
|
+
export function findTextOccurrences(documentText, searchText, maxResults = 100) {
|
|
1813
|
+
if (!searchText)
|
|
1814
|
+
return [];
|
|
1815
|
+
const results = [];
|
|
1816
|
+
let index = 0;
|
|
1817
|
+
while (results.length < maxResults) {
|
|
1818
|
+
index = documentText.indexOf(searchText, index);
|
|
1819
|
+
if (index < 0)
|
|
1820
|
+
break;
|
|
1821
|
+
results.push({ start: index, end: index + searchText.length });
|
|
1822
|
+
index += 1; // Move past start to find overlapping matches
|
|
1823
|
+
}
|
|
1824
|
+
return results;
|
|
1825
|
+
}
|
|
1826
|
+
// Helper function to convert PascalCase response to camelCase ExternalAnnotationSet
|
|
1827
|
+
function convertExternalAnnotationSet(parsed) {
|
|
1828
|
+
const convertLabel = (l) => ({
|
|
1829
|
+
id: l.Id ?? l.id,
|
|
1830
|
+
color: l.Color ?? l.color,
|
|
1831
|
+
description: l.Description ?? l.description ?? "",
|
|
1832
|
+
icon: l.Icon ?? l.icon ?? "",
|
|
1833
|
+
text: l.Text ?? l.text,
|
|
1834
|
+
labelType: l.LabelType ?? l.labelType ?? "text",
|
|
1835
|
+
});
|
|
1836
|
+
const convertPawlsPage = (p) => ({
|
|
1837
|
+
page: {
|
|
1838
|
+
width: p.Page?.Width ?? p.page?.width,
|
|
1839
|
+
height: p.Page?.Height ?? p.page?.height,
|
|
1840
|
+
index: p.Page?.Index ?? p.page?.index,
|
|
1841
|
+
},
|
|
1842
|
+
tokens: (p.Tokens || p.tokens || []).map((t) => ({
|
|
1843
|
+
x: t.X ?? t.x,
|
|
1844
|
+
y: t.Y ?? t.y,
|
|
1845
|
+
width: t.Width ?? t.width,
|
|
1846
|
+
height: t.Height ?? t.height,
|
|
1847
|
+
text: t.Text ?? t.text,
|
|
1848
|
+
})),
|
|
1849
|
+
});
|
|
1850
|
+
const convertAnnotation = (a) => ({
|
|
1851
|
+
id: a.Id ?? a.id,
|
|
1852
|
+
annotationLabel: a.AnnotationLabel ?? a.annotationLabel,
|
|
1853
|
+
rawText: a.RawText ?? a.rawText,
|
|
1854
|
+
page: a.Page ?? a.page,
|
|
1855
|
+
annotationJson: convertAnnotationJson(a.AnnotationJson ?? a.annotationJson),
|
|
1856
|
+
parentId: a.ParentId ?? a.parentId,
|
|
1857
|
+
annotationType: a.AnnotationType ?? a.annotationType,
|
|
1858
|
+
structural: a.Structural ?? a.structural,
|
|
1859
|
+
});
|
|
1860
|
+
const convertAnnotationJson = (json) => {
|
|
1861
|
+
if (!json)
|
|
1862
|
+
return undefined;
|
|
1863
|
+
// Check if it's a TextSpan
|
|
1864
|
+
if (json.Start !== undefined || json.start !== undefined) {
|
|
1865
|
+
return {
|
|
1866
|
+
id: json.Id ?? json.id,
|
|
1867
|
+
start: json.Start ?? json.start,
|
|
1868
|
+
end: json.End ?? json.end,
|
|
1869
|
+
text: json.Text ?? json.text,
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
// Otherwise it's a dictionary of single-page annotations
|
|
1873
|
+
const result = {};
|
|
1874
|
+
for (const [key, value] of Object.entries(json)) {
|
|
1875
|
+
const v = value;
|
|
1876
|
+
result[key] = {
|
|
1877
|
+
bounds: {
|
|
1878
|
+
top: v.Bounds?.Top ?? v.bounds?.top,
|
|
1879
|
+
bottom: v.Bounds?.Bottom ?? v.bounds?.bottom,
|
|
1880
|
+
left: v.Bounds?.Left ?? v.bounds?.left,
|
|
1881
|
+
right: v.Bounds?.Right ?? v.bounds?.right,
|
|
1882
|
+
},
|
|
1883
|
+
tokensJsons: (v.TokensJsons || v.tokensJsons || []).map((t) => ({
|
|
1884
|
+
pageIndex: t.PageIndex ?? t.pageIndex,
|
|
1885
|
+
tokenIndex: t.TokenIndex ?? t.tokenIndex,
|
|
1886
|
+
})),
|
|
1887
|
+
rawText: v.RawText ?? v.rawText,
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
return result;
|
|
1891
|
+
};
|
|
1892
|
+
const convertRelationship = (r) => ({
|
|
1893
|
+
id: r.Id ?? r.id,
|
|
1894
|
+
relationshipLabel: r.RelationshipLabel ?? r.relationshipLabel,
|
|
1895
|
+
sourceAnnotationIds: r.SourceAnnotationIds ?? r.sourceAnnotationIds ?? [],
|
|
1896
|
+
targetAnnotationIds: r.TargetAnnotationIds ?? r.targetAnnotationIds ?? [],
|
|
1897
|
+
structural: r.Structural ?? r.structural,
|
|
1898
|
+
});
|
|
1899
|
+
// Convert label dictionaries
|
|
1900
|
+
const textLabels = {};
|
|
1901
|
+
const rawTextLabels = parsed.TextLabels || parsed.textLabels || {};
|
|
1902
|
+
for (const [key, value] of Object.entries(rawTextLabels)) {
|
|
1903
|
+
textLabels[key] = convertLabel(value);
|
|
1904
|
+
}
|
|
1905
|
+
const docLabelDefinitions = {};
|
|
1906
|
+
const rawDocLabelDefs = parsed.DocLabelDefinitions || parsed.docLabelDefinitions || {};
|
|
1907
|
+
for (const [key, value] of Object.entries(rawDocLabelDefs)) {
|
|
1908
|
+
docLabelDefinitions[key] = convertLabel(value);
|
|
1909
|
+
}
|
|
1910
|
+
return {
|
|
1911
|
+
documentId: parsed.DocumentId ?? parsed.documentId,
|
|
1912
|
+
documentHash: parsed.DocumentHash ?? parsed.documentHash,
|
|
1913
|
+
createdAt: parsed.CreatedAt ?? parsed.createdAt,
|
|
1914
|
+
updatedAt: parsed.UpdatedAt ?? parsed.updatedAt,
|
|
1915
|
+
version: parsed.Version ?? parsed.version,
|
|
1916
|
+
title: parsed.Title ?? parsed.title,
|
|
1917
|
+
content: parsed.Content ?? parsed.content,
|
|
1918
|
+
description: parsed.Description ?? parsed.description,
|
|
1919
|
+
pageCount: parsed.PageCount ?? parsed.pageCount,
|
|
1920
|
+
pawlsFileContent: (parsed.PawlsFileContent || parsed.pawlsFileContent || []).map(convertPawlsPage),
|
|
1921
|
+
docLabels: parsed.DocLabels ?? parsed.docLabels ?? [],
|
|
1922
|
+
labelledText: (parsed.LabelledText || parsed.labelledText || []).map(convertAnnotation),
|
|
1923
|
+
relationships: (parsed.Relationships || parsed.relationships)?.map(convertRelationship),
|
|
1924
|
+
textLabels,
|
|
1925
|
+
docLabelDefinitions,
|
|
1926
|
+
};
|
|
1927
|
+
}
|
|
1928
|
+
// ============================================================================
|
|
1929
|
+
// Incremental Annotation Overlay API (Issue #106)
|
|
1930
|
+
// ============================================================================
|
|
1931
|
+
/**
|
|
1932
|
+
* Project external annotations onto already-converted HTML.
|
|
1933
|
+
* This avoids full DOCX re-conversion when only annotations change.
|
|
1934
|
+
*
|
|
1935
|
+
* Workflow:
|
|
1936
|
+
* 1. Convert DOCX to HTML once using `convertDocxToHtml()`
|
|
1937
|
+
* 2. Use this function to overlay annotations on the cached HTML
|
|
1938
|
+
* 3. When annotations change, call this again with the same base HTML
|
|
1939
|
+
*
|
|
1940
|
+
* @param html - HTML string (previously converted via convertDocxToHtml)
|
|
1941
|
+
* @param annotationSet - The external annotation set to project
|
|
1942
|
+
* @param projectionOptions - Projection settings (CSS prefix, label mode, etc.)
|
|
1943
|
+
* @returns HTML string with annotations projected
|
|
1944
|
+
* @throws Error if projection fails
|
|
1945
|
+
*
|
|
1946
|
+
* @example
|
|
1947
|
+
* ```typescript
|
|
1948
|
+
* // Step 1: Convert once
|
|
1949
|
+
* const baseHtml = await convertDocxToHtml(docxFile);
|
|
1950
|
+
*
|
|
1951
|
+
* // Step 2: Project annotations (fast, no DOCX re-conversion)
|
|
1952
|
+
* const annotatedHtml = await projectAnnotationsOntoHtml(baseHtml, annotationSet);
|
|
1953
|
+
*
|
|
1954
|
+
* // Step 3: When annotations change, project again on the same base HTML
|
|
1955
|
+
* annotationSet.labelledText.push(newAnnotation);
|
|
1956
|
+
* const updatedHtml = await projectAnnotationsOntoHtml(baseHtml, annotationSet);
|
|
1957
|
+
* ```
|
|
1958
|
+
*/
|
|
1959
|
+
export async function projectAnnotationsOntoHtml(html, annotationSet, projectionOptions) {
|
|
1960
|
+
const exports = ensureInitialized();
|
|
1961
|
+
await yieldToMain();
|
|
1962
|
+
const annotationSetJson = JSON.stringify(annotationSet);
|
|
1963
|
+
const result = exports.DocumentConverter.ProjectAnnotationsOntoHtml(html, annotationSetJson, projectionOptions?.cssClassPrefix ?? "ext-annot-", projectionOptions?.labelMode ?? AnnotationLabelMode.Above);
|
|
1964
|
+
if (isErrorResponse(result)) {
|
|
1965
|
+
const error = parseError(result);
|
|
1966
|
+
throw new Error(`Failed to project annotations: ${error.error}`);
|
|
1967
|
+
}
|
|
1968
|
+
const parsed = JSON.parse(result);
|
|
1969
|
+
return parsed.Html ?? parsed.html;
|
|
1970
|
+
}
|
|
1971
|
+
/**
|
|
1972
|
+
* Add a single annotation to existing HTML without re-converting the document.
|
|
1973
|
+
* This is the fastest way to add one annotation to already-rendered HTML.
|
|
1974
|
+
*
|
|
1975
|
+
* @param html - HTML string (with or without existing annotations)
|
|
1976
|
+
* @param annotation - The annotation to add
|
|
1977
|
+
* @param label - Label definition for the annotation (optional, for color/text)
|
|
1978
|
+
* @param projectionOptions - Projection settings
|
|
1979
|
+
* @returns HTML string with the annotation added
|
|
1980
|
+
* @throws Error if operation fails
|
|
1981
|
+
*
|
|
1982
|
+
* @example
|
|
1983
|
+
* ```typescript
|
|
1984
|
+
* const annotation = createAnnotation("ann-new", "CLAUSE", set.content, 100, 150);
|
|
1985
|
+
* const label = { id: "CLAUSE", text: "Clause", color: "#FF5722" };
|
|
1986
|
+
* const updatedHtml = await addAnnotationToHtml(currentHtml, annotation, label);
|
|
1987
|
+
* ```
|
|
1988
|
+
*/
|
|
1989
|
+
export async function addAnnotationToHtml(html, annotation, label, projectionOptions) {
|
|
1990
|
+
const exports = ensureInitialized();
|
|
1991
|
+
await yieldToMain();
|
|
1992
|
+
const annotationJson = JSON.stringify(annotation);
|
|
1993
|
+
const labelJson = label ? JSON.stringify(label) : "";
|
|
1994
|
+
const result = exports.DocumentConverter.AddAnnotationToHtml(html, annotationJson, labelJson, projectionOptions?.cssClassPrefix ?? "ext-annot-", projectionOptions?.labelMode ?? AnnotationLabelMode.Above);
|
|
1995
|
+
if (isErrorResponse(result)) {
|
|
1996
|
+
const error = parseError(result);
|
|
1997
|
+
throw new Error(`Failed to add annotation to HTML: ${error.error}`);
|
|
1998
|
+
}
|
|
1999
|
+
const parsed = JSON.parse(result);
|
|
2000
|
+
return parsed.Html ?? parsed.html;
|
|
2001
|
+
}
|
|
2002
|
+
/**
|
|
2003
|
+
* Remove a single annotation from HTML by annotation ID.
|
|
2004
|
+
* Unwraps annotation spans back to plain text.
|
|
2005
|
+
*
|
|
2006
|
+
* @param html - HTML string with annotations
|
|
2007
|
+
* @param annotationId - ID of the annotation to remove
|
|
2008
|
+
* @param cssClassPrefix - CSS class prefix used for annotations (default: "ext-annot-")
|
|
2009
|
+
* @returns HTML string with the annotation removed
|
|
2010
|
+
* @throws Error if operation fails
|
|
2011
|
+
*
|
|
2012
|
+
* @example
|
|
2013
|
+
* ```typescript
|
|
2014
|
+
* const updatedHtml = await removeAnnotationFromHtml(currentHtml, "ann-001");
|
|
2015
|
+
* ```
|
|
2016
|
+
*/
|
|
2017
|
+
export async function removeAnnotationFromHtml(html, annotationId, cssClassPrefix) {
|
|
2018
|
+
const exports = ensureInitialized();
|
|
2019
|
+
await yieldToMain();
|
|
2020
|
+
const result = exports.DocumentConverter.RemoveAnnotationFromHtml(html, annotationId, cssClassPrefix ?? "ext-annot-");
|
|
2021
|
+
if (isErrorResponse(result)) {
|
|
2022
|
+
const error = parseError(result);
|
|
2023
|
+
throw new Error(`Failed to remove annotation from HTML: ${error.error}`);
|
|
2024
|
+
}
|
|
2025
|
+
const parsed = JSON.parse(result);
|
|
2026
|
+
return parsed.Html ?? parsed.html;
|
|
2027
|
+
}
|
|
2028
|
+
/**
|
|
2029
|
+
* Generate CSS to hide annotations with specific label IDs.
|
|
2030
|
+
* Enables CSS-based label filtering without re-rendering HTML.
|
|
2031
|
+
*
|
|
2032
|
+
* Apply the returned CSS to your document (e.g., via a `<style>` element)
|
|
2033
|
+
* to hide/show annotations by label. This is much faster than re-projecting
|
|
2034
|
+
* all annotations.
|
|
2035
|
+
*
|
|
2036
|
+
* @param hiddenLabelIds - Array of label IDs to hide
|
|
2037
|
+
* @param cssClassPrefix - CSS class prefix (default: "ext-annot-")
|
|
2038
|
+
* @returns CSS string that hides the specified labels
|
|
2039
|
+
* @throws Error if operation fails
|
|
2040
|
+
*
|
|
2041
|
+
* @example
|
|
2042
|
+
* ```typescript
|
|
2043
|
+
* // Hide annotations with label "DRAFT" and "INTERNAL"
|
|
2044
|
+
* const css = await generateAnnotationVisibilityCss(["DRAFT", "INTERNAL"]);
|
|
2045
|
+
*
|
|
2046
|
+
* // Apply to a <style> element in the DOM
|
|
2047
|
+
* const styleEl = document.getElementById("visibility-overrides");
|
|
2048
|
+
* styleEl.textContent = css;
|
|
2049
|
+
*
|
|
2050
|
+
* // To show all annotations again, clear the style:
|
|
2051
|
+
* styleEl.textContent = "";
|
|
2052
|
+
* ```
|
|
2053
|
+
*/
|
|
2054
|
+
export async function generateAnnotationVisibilityCss(hiddenLabelIds, cssClassPrefix) {
|
|
2055
|
+
const exports = ensureInitialized();
|
|
2056
|
+
await yieldToMain();
|
|
2057
|
+
const result = exports.DocumentConverter.GenerateAnnotationVisibilityCss(JSON.stringify(hiddenLabelIds), cssClassPrefix ?? "ext-annot-");
|
|
2058
|
+
if (isErrorResponse(result)) {
|
|
2059
|
+
const error = parseError(result);
|
|
2060
|
+
throw new Error(`Failed to generate visibility CSS: ${error.error}`);
|
|
2061
|
+
}
|
|
2062
|
+
const parsed = JSON.parse(result);
|
|
2063
|
+
return parsed.Css ?? parsed.css;
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Generate annotation CSS for a set of labels.
|
|
2067
|
+
* Useful when managing CSS separately from HTML content.
|
|
2068
|
+
*
|
|
2069
|
+
* @param labels - Label definitions (keyed by label ID)
|
|
2070
|
+
* @param projectionOptions - Projection settings
|
|
2071
|
+
* @returns CSS string for the annotation styles
|
|
2072
|
+
* @throws Error if operation fails
|
|
2073
|
+
*
|
|
2074
|
+
* @example
|
|
2075
|
+
* ```typescript
|
|
2076
|
+
* const labels = {
|
|
2077
|
+
* "CLAUSE": { id: "CLAUSE", text: "Clause", color: "#FF5722" },
|
|
2078
|
+
* "TERM": { id: "TERM", text: "Term", color: "#2196F3" },
|
|
2079
|
+
* };
|
|
2080
|
+
* const css = await generateAnnotationCss(labels);
|
|
2081
|
+
* ```
|
|
2082
|
+
*/
|
|
2083
|
+
export async function generateAnnotationCss(labels, projectionOptions) {
|
|
2084
|
+
const exports = ensureInitialized();
|
|
2085
|
+
await yieldToMain();
|
|
2086
|
+
const result = exports.DocumentConverter.GenerateAnnotationCss(JSON.stringify(labels), projectionOptions?.cssClassPrefix ?? "ext-annot-", projectionOptions?.labelMode ?? AnnotationLabelMode.Above);
|
|
2087
|
+
if (isErrorResponse(result)) {
|
|
2088
|
+
const error = parseError(result);
|
|
2089
|
+
throw new Error(`Failed to generate annotation CSS: ${error.error}`);
|
|
2090
|
+
}
|
|
2091
|
+
const parsed = JSON.parse(result);
|
|
2092
|
+
return parsed.Css ?? parsed.css;
|
|
2093
|
+
}
|
|
2094
|
+
//# sourceMappingURL=core.js.map
|