@beyondwork/docx-react-component 1.0.42 → 1.0.45
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 +17 -0
- package/package.json +5 -4
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/public-types.ts +333 -4
- package/src/core/commands/formatting-commands.ts +7 -1
- package/src/core/commands/index.ts +60 -10
- package/src/core/commands/text-commands.ts +59 -0
- package/src/core/search/search-text.ts +15 -2
- package/src/core/selection/review-anchors.ts +131 -21
- package/src/index.ts +29 -1
- package/src/io/chart-preview-resolver.ts +281 -0
- package/src/io/docx-session.ts +692 -2
- package/src/io/export/build-app-properties-xml.ts +1 -1
- package/src/io/export/serialize-comments.ts +38 -9
- package/src/io/export/twip.ts +1 -1
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +116 -0
- package/src/io/ooxml/parse-comments.ts +0 -33
- package/src/io/ooxml/parse-complex-content.ts +14 -0
- package/src/io/ooxml/parse-main-document.ts +4 -0
- package/src/io/ooxml/workflow-payload-validator.ts +97 -1
- package/src/io/ooxml/workflow-payload.ts +172 -1
- package/src/preservation/opaque-region.ts +5 -0
- package/src/review/store/comment-remapping.ts +2 -2
- package/src/runtime/collab-session.ts +1 -1
- package/src/runtime/document-runtime.ts +661 -42
- package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
- package/src/runtime/edit-dispatch/index.ts +2 -0
- package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/editor-surface/capabilities.ts +411 -0
- package/src/runtime/layout/index.ts +2 -0
- package/src/runtime/layout/inert-layout-facet.ts +4 -0
- package/src/runtime/layout/layout-engine-instance.ts +63 -2
- package/src/runtime/layout/layout-engine-version.ts +41 -0
- package/src/runtime/layout/paginated-layout-engine.ts +211 -14
- package/src/runtime/layout/public-facet.ts +430 -1
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/prerender/cache-envelope.ts +29 -0
- package/src/runtime/prerender/cache-key.ts +66 -0
- package/src/runtime/prerender/font-fingerprint.ts +17 -0
- package/src/runtime/prerender/graph-canonicalize.ts +121 -0
- package/src/runtime/prerender/indexeddb-cache.ts +184 -0
- package/src/runtime/prerender/prerender-document.ts +145 -0
- package/src/runtime/render/block-fragment-projection.ts +2 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/selection/post-edit-validator.ts +77 -0
- package/src/runtime/surface-projection.ts +45 -7
- package/src/runtime/workflow-markup.ts +71 -16
- package/src/ui/WordReviewEditor.tsx +142 -237
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +115 -12
- package/src/ui/editor-shell-view.tsx +10 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui/runtime-shortcut-dispatch.ts +28 -68
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
- package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
- package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
- package/src/ui-tailwind/index.ts +5 -1
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
- package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
- package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/theme/editor-theme.css +47 -14
- package/src/ui-tailwind/tw-review-workspace.tsx +303 -123
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* content type.
|
|
8
8
|
*
|
|
9
9
|
* Required by:
|
|
10
|
-
* docs/plans/
|
|
10
|
+
* docs/plans/lane-3-layout-engine-ooxml-fidelity.md §2 A.5
|
|
11
11
|
*
|
|
12
12
|
* Schema shape (ECMA-376 Part 1, Office Extended properties):
|
|
13
13
|
* <Properties xmlns="…/extended-properties" xmlns:vt="…/docPropsVTypes">
|
|
@@ -267,19 +267,16 @@ export function serializeCommentAnchorsIntoDocumentXml(
|
|
|
267
267
|
continue;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
anchor.range.from >= candidate.start &&
|
|
273
|
-
anchor.range.to <= candidate.end,
|
|
274
|
-
);
|
|
270
|
+
const startParagraph = findParagraphForEndpoint(paragraphs, anchor.range.from, "start");
|
|
271
|
+
const endParagraph = findParagraphForEndpoint(paragraphs, anchor.range.to, "end");
|
|
275
272
|
|
|
276
|
-
if (!
|
|
273
|
+
if (!startParagraph || !endParagraph) {
|
|
277
274
|
skippedCommentIds.push(thread.commentId);
|
|
278
275
|
continue;
|
|
279
276
|
}
|
|
280
277
|
|
|
281
|
-
const startIndex =
|
|
282
|
-
const endIndex =
|
|
278
|
+
const startIndex = startParagraph.boundaries.get(anchor.range.from);
|
|
279
|
+
const endIndex = endParagraph.boundaries.get(anchor.range.to);
|
|
283
280
|
|
|
284
281
|
if (startIndex === undefined || endIndex === undefined) {
|
|
285
282
|
skippedCommentIds.push(thread.commentId);
|
|
@@ -324,6 +321,25 @@ export function serializeCommentAnchorsIntoDocumentXml(
|
|
|
324
321
|
};
|
|
325
322
|
}
|
|
326
323
|
|
|
324
|
+
function findParagraphForEndpoint(
|
|
325
|
+
paragraphs: readonly Pick<
|
|
326
|
+
RevisionParagraphBoundary,
|
|
327
|
+
"paragraphIndex" | "start" | "end" | "boundaries"
|
|
328
|
+
>[],
|
|
329
|
+
offset: number,
|
|
330
|
+
kind: "start" | "end",
|
|
331
|
+
): Pick<RevisionParagraphBoundary, "paragraphIndex" | "start" | "end" | "boundaries"> | undefined {
|
|
332
|
+
const matches = paragraphs.filter(
|
|
333
|
+
(p) => offset >= p.start && offset <= p.end,
|
|
334
|
+
);
|
|
335
|
+
if (matches.length === 0) return undefined;
|
|
336
|
+
if (matches.length === 1) return matches[0];
|
|
337
|
+
if (kind === "start") {
|
|
338
|
+
return matches.reduce((a, b) => (b.start >= a.start ? b : a));
|
|
339
|
+
}
|
|
340
|
+
return matches.reduce((a, b) => (b.end <= a.end ? b : a));
|
|
341
|
+
}
|
|
342
|
+
|
|
327
343
|
export function createCommentExportIdMap(
|
|
328
344
|
threads: readonly CommentThread[],
|
|
329
345
|
preservedDefinitions: readonly ImportedCommentDefinition[] = [],
|
|
@@ -729,6 +745,8 @@ function walkInlineNodeForBoundaries(
|
|
|
729
745
|
}
|
|
730
746
|
|
|
731
747
|
switch (localName(node.name)) {
|
|
748
|
+
case "pPr":
|
|
749
|
+
return;
|
|
732
750
|
case "r": {
|
|
733
751
|
if (!boundaries.has(getCursor())) {
|
|
734
752
|
boundaries.set(getCursor(), node.start);
|
|
@@ -750,7 +768,18 @@ function walkInlineNodeForBoundaries(
|
|
|
750
768
|
return;
|
|
751
769
|
}
|
|
752
770
|
case "tab":
|
|
753
|
-
case "br":
|
|
771
|
+
case "br":
|
|
772
|
+
case "footnoteReference":
|
|
773
|
+
case "endnoteReference":
|
|
774
|
+
case "drawing":
|
|
775
|
+
case "pict":
|
|
776
|
+
case "object":
|
|
777
|
+
case "sym":
|
|
778
|
+
case "ptab":
|
|
779
|
+
case "separator":
|
|
780
|
+
case "continuationSeparator":
|
|
781
|
+
case "noBreakHyphen":
|
|
782
|
+
case "softHyphen": {
|
|
754
783
|
const startCursor = getCursor();
|
|
755
784
|
boundaries.set(startCursor, node.start);
|
|
756
785
|
const nextCursor = startCursor + 1;
|
package/src/io/export/twip.ts
CHANGED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Load scheduler — main-thread time-slicing primitive for the staged
|
|
3
|
+
* document-load pipeline.
|
|
4
|
+
*
|
|
5
|
+
* The loader calls `scheduler.yield()` between parse stages so the browser
|
|
6
|
+
* can paint, service input, and run React commits. `scheduleIdle(task)`
|
|
7
|
+
* queues low-priority work (e.g., sub-part hydration, compatibility report)
|
|
8
|
+
* for post-skeleton execution.
|
|
9
|
+
*
|
|
10
|
+
* Backend cascade (first available wins):
|
|
11
|
+
* 1. `globalThis.scheduler.yield()` — native browser API (Chrome 129+, Edge).
|
|
12
|
+
* 2. `MessageChannel.postMessage` — universal DOM fallback, ~0.1ms per yield.
|
|
13
|
+
* 3. `setTimeout(0)` — last-resort fallback.
|
|
14
|
+
* 4. `sync` — SSR / Node test harness. `yield()` resolves immediately;
|
|
15
|
+
* `scheduleIdle` runs inline.
|
|
16
|
+
*
|
|
17
|
+
* The `sync` backend is selected when `typeof document === "undefined"` so
|
|
18
|
+
* existing Node-side tests drive the staged pipeline with byte-identical
|
|
19
|
+
* behavior to the eager pipeline (no real yielding, no idle deferral).
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export type LoadSchedulerBackend =
|
|
23
|
+
| "scheduler-api"
|
|
24
|
+
| "message-channel"
|
|
25
|
+
| "timeout"
|
|
26
|
+
| "sync";
|
|
27
|
+
|
|
28
|
+
export interface LoadScheduler {
|
|
29
|
+
readonly backend: LoadSchedulerBackend;
|
|
30
|
+
/** Yield to the browser. Resolves on next scheduled task / microtask. */
|
|
31
|
+
yield(): Promise<void>;
|
|
32
|
+
/** Schedule low-priority work for post-skeleton execution. */
|
|
33
|
+
scheduleIdle(task: () => void): void;
|
|
34
|
+
/** Cancel pending idle tasks. Must be called on unmount / dispose. */
|
|
35
|
+
dispose(): void;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface CreateLoadSchedulerOptions {
|
|
39
|
+
/** Frame deadline in ms. Default 4ms (keeps browser at 60fps). */
|
|
40
|
+
frameDeadlineMs?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Force a specific backend (test-only). When omitted, the scheduler
|
|
43
|
+
* detects the best available backend at construction time.
|
|
44
|
+
*/
|
|
45
|
+
backendOverride?: LoadSchedulerBackend;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const DEFAULT_FRAME_DEADLINE_MS = 4;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns true when the elapsed time since `lastYieldAt` exceeds the
|
|
52
|
+
* scheduler's frame deadline. Callers use this inside tight loops to decide
|
|
53
|
+
* when to `await scheduler.yield()`.
|
|
54
|
+
*/
|
|
55
|
+
export function shouldYield(
|
|
56
|
+
scheduler: LoadScheduler & { readonly frameDeadlineMs?: number },
|
|
57
|
+
lastYieldAt: number,
|
|
58
|
+
): boolean {
|
|
59
|
+
const now = typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
60
|
+
const deadline = scheduler.frameDeadlineMs ?? DEFAULT_FRAME_DEADLINE_MS;
|
|
61
|
+
return now - lastYieldAt >= deadline;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns a monotonic timestamp suitable for `shouldYield` comparisons.
|
|
66
|
+
*/
|
|
67
|
+
export function nowMs(): number {
|
|
68
|
+
return typeof performance !== "undefined" ? performance.now() : Date.now();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface InternalScheduler extends LoadScheduler {
|
|
72
|
+
readonly frameDeadlineMs: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function createLoadScheduler(
|
|
76
|
+
options: CreateLoadSchedulerOptions = {},
|
|
77
|
+
): LoadScheduler {
|
|
78
|
+
const frameDeadlineMs = options.frameDeadlineMs ?? DEFAULT_FRAME_DEADLINE_MS;
|
|
79
|
+
const backend = options.backendOverride ?? detectBackend();
|
|
80
|
+
|
|
81
|
+
switch (backend) {
|
|
82
|
+
case "scheduler-api":
|
|
83
|
+
return createSchedulerApiBackend(frameDeadlineMs);
|
|
84
|
+
case "message-channel":
|
|
85
|
+
return createMessageChannelBackend(frameDeadlineMs);
|
|
86
|
+
case "timeout":
|
|
87
|
+
return createTimeoutBackend(frameDeadlineMs);
|
|
88
|
+
case "sync":
|
|
89
|
+
return createSyncBackend(frameDeadlineMs);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function detectBackend(): LoadSchedulerBackend {
|
|
94
|
+
if (typeof document === "undefined") {
|
|
95
|
+
return "sync";
|
|
96
|
+
}
|
|
97
|
+
const g = globalThis as unknown as {
|
|
98
|
+
scheduler?: { yield?: () => Promise<void> };
|
|
99
|
+
};
|
|
100
|
+
if (typeof g.scheduler?.yield === "function") {
|
|
101
|
+
return "scheduler-api";
|
|
102
|
+
}
|
|
103
|
+
if (typeof MessageChannel !== "undefined") {
|
|
104
|
+
return "message-channel";
|
|
105
|
+
}
|
|
106
|
+
return "timeout";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function createSchedulerApiBackend(frameDeadlineMs: number): InternalScheduler {
|
|
110
|
+
const g = globalThis as unknown as {
|
|
111
|
+
scheduler: { yield: () => Promise<void> };
|
|
112
|
+
};
|
|
113
|
+
const pendingIdleHandles = new Set<number>();
|
|
114
|
+
return {
|
|
115
|
+
backend: "scheduler-api",
|
|
116
|
+
frameDeadlineMs,
|
|
117
|
+
yield: () => g.scheduler.yield(),
|
|
118
|
+
scheduleIdle(task) {
|
|
119
|
+
const handle = scheduleIdleCallback(task, pendingIdleHandles);
|
|
120
|
+
pendingIdleHandles.add(handle);
|
|
121
|
+
},
|
|
122
|
+
dispose() {
|
|
123
|
+
disposeIdleHandles(pendingIdleHandles);
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createMessageChannelBackend(frameDeadlineMs: number): InternalScheduler {
|
|
129
|
+
const pendingIdleHandles = new Set<number>();
|
|
130
|
+
return {
|
|
131
|
+
backend: "message-channel",
|
|
132
|
+
frameDeadlineMs,
|
|
133
|
+
yield() {
|
|
134
|
+
return new Promise<void>((resolve) => {
|
|
135
|
+
const channel = new MessageChannel();
|
|
136
|
+
channel.port1.onmessage = () => {
|
|
137
|
+
channel.port1.close();
|
|
138
|
+
channel.port2.close();
|
|
139
|
+
resolve();
|
|
140
|
+
};
|
|
141
|
+
channel.port2.postMessage(null);
|
|
142
|
+
});
|
|
143
|
+
},
|
|
144
|
+
scheduleIdle(task) {
|
|
145
|
+
const handle = scheduleIdleCallback(task, pendingIdleHandles);
|
|
146
|
+
pendingIdleHandles.add(handle);
|
|
147
|
+
},
|
|
148
|
+
dispose() {
|
|
149
|
+
disposeIdleHandles(pendingIdleHandles);
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function createTimeoutBackend(frameDeadlineMs: number): InternalScheduler {
|
|
155
|
+
const pendingIdleHandles = new Set<number>();
|
|
156
|
+
return {
|
|
157
|
+
backend: "timeout",
|
|
158
|
+
frameDeadlineMs,
|
|
159
|
+
yield() {
|
|
160
|
+
return new Promise<void>((resolve) => {
|
|
161
|
+
setTimeout(resolve, 0);
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
scheduleIdle(task) {
|
|
165
|
+
const handle = setTimeout(task, 0) as unknown as number;
|
|
166
|
+
pendingIdleHandles.add(handle);
|
|
167
|
+
},
|
|
168
|
+
dispose() {
|
|
169
|
+
for (const handle of pendingIdleHandles) {
|
|
170
|
+
clearTimeout(handle as unknown as ReturnType<typeof setTimeout>);
|
|
171
|
+
}
|
|
172
|
+
pendingIdleHandles.clear();
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function createSyncBackend(frameDeadlineMs: number): InternalScheduler {
|
|
178
|
+
return {
|
|
179
|
+
backend: "sync",
|
|
180
|
+
frameDeadlineMs,
|
|
181
|
+
yield: () => Promise.resolve(),
|
|
182
|
+
scheduleIdle(task) {
|
|
183
|
+
task();
|
|
184
|
+
},
|
|
185
|
+
dispose() {
|
|
186
|
+
/* no-op */
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
type IdleHandle = number;
|
|
192
|
+
|
|
193
|
+
function scheduleIdleCallback(
|
|
194
|
+
task: () => void,
|
|
195
|
+
store: Set<IdleHandle>,
|
|
196
|
+
): IdleHandle {
|
|
197
|
+
const g = globalThis as unknown as {
|
|
198
|
+
requestIdleCallback?: (cb: () => void, options?: { timeout: number }) => number;
|
|
199
|
+
cancelIdleCallback?: (handle: number) => void;
|
|
200
|
+
};
|
|
201
|
+
if (typeof g.requestIdleCallback === "function") {
|
|
202
|
+
const handle = g.requestIdleCallback(
|
|
203
|
+
() => {
|
|
204
|
+
store.delete(handle);
|
|
205
|
+
task();
|
|
206
|
+
},
|
|
207
|
+
{ timeout: 50 },
|
|
208
|
+
);
|
|
209
|
+
return handle;
|
|
210
|
+
}
|
|
211
|
+
const handle = setTimeout(() => {
|
|
212
|
+
store.delete(handle as unknown as number);
|
|
213
|
+
task();
|
|
214
|
+
}, 0) as unknown as number;
|
|
215
|
+
return handle;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function disposeIdleHandles(store: Set<IdleHandle>): void {
|
|
219
|
+
const g = globalThis as unknown as {
|
|
220
|
+
cancelIdleCallback?: (handle: number) => void;
|
|
221
|
+
};
|
|
222
|
+
for (const handle of store) {
|
|
223
|
+
if (typeof g.cancelIdleCallback === "function") {
|
|
224
|
+
g.cancelIdleCallback(handle);
|
|
225
|
+
} else {
|
|
226
|
+
clearTimeout(handle as unknown as ReturnType<typeof setTimeout>);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
store.clear();
|
|
230
|
+
}
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
import type {
|
|
24
24
|
ParsedAltChunkNode,
|
|
25
25
|
ParsedBlockNode,
|
|
26
|
+
ParsedChartPreviewNode,
|
|
26
27
|
ParsedCustomXmlNode,
|
|
27
28
|
ParsedHyperlinkNode,
|
|
28
29
|
ParsedInlineNode,
|
|
@@ -31,11 +32,17 @@ import type {
|
|
|
31
32
|
ParsedParagraphNode,
|
|
32
33
|
ParsedSectionBreakNode,
|
|
33
34
|
ParsedSdtNode,
|
|
35
|
+
ParsedSmartArtPreviewNode,
|
|
34
36
|
ParsedTableBlockNode,
|
|
35
37
|
ParsedTableCellNode,
|
|
36
38
|
ParsedTableRowNode,
|
|
37
39
|
} from "../ooxml/parse-main-document.ts";
|
|
38
40
|
import { classifyFieldInstruction, buildFieldRegistry } from "../ooxml/parse-fields.ts";
|
|
41
|
+
import {
|
|
42
|
+
type LoadScheduler,
|
|
43
|
+
nowMs,
|
|
44
|
+
shouldYield,
|
|
45
|
+
} from "../load-scheduler.ts";
|
|
39
46
|
|
|
40
47
|
export interface NormalizedTextDocument {
|
|
41
48
|
content: DocumentRootNode;
|
|
@@ -115,6 +122,84 @@ export function normalizeParsedTextDocument(
|
|
|
115
122
|
};
|
|
116
123
|
}
|
|
117
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Fastload P6: async sibling of `normalizeParsedTextDocument` that yields to
|
|
127
|
+
* the browser every {@link NORMALIZE_YIELD_STRIDE} top-level blocks when
|
|
128
|
+
* {@link shouldYield} fires against the scheduler's frame deadline. Shares
|
|
129
|
+
* the private normalizeBlocks / normalizeParagraph / normalizeInlineChildren
|
|
130
|
+
* helpers with the sync export — only the outermost block walk is duplicated.
|
|
131
|
+
*
|
|
132
|
+
* Byte-equivalent to the sync export on any corpus (fixture parity is asserted
|
|
133
|
+
* in `test/io/normalize-text-async.test.ts` across every F*.docx fixture).
|
|
134
|
+
*/
|
|
135
|
+
const NORMALIZE_YIELD_STRIDE = 256;
|
|
136
|
+
|
|
137
|
+
export async function normalizeParsedTextDocumentAsync(
|
|
138
|
+
document: ParsedMainDocument,
|
|
139
|
+
packagePartName = "/word/document.xml",
|
|
140
|
+
scheduler: LoadScheduler,
|
|
141
|
+
options?: { styles?: import("../../model/canonical-document.ts").StylesCatalog },
|
|
142
|
+
): Promise<NormalizedTextDocument> {
|
|
143
|
+
const state: NormalizationState = {
|
|
144
|
+
nextFragmentIndex: 1,
|
|
145
|
+
nextWarningIndex: 1,
|
|
146
|
+
nextDiagnosticIndex: 1,
|
|
147
|
+
cursor: 0,
|
|
148
|
+
media: {
|
|
149
|
+
items: {},
|
|
150
|
+
},
|
|
151
|
+
preservation: {
|
|
152
|
+
opaqueFragments: {},
|
|
153
|
+
packageParts: {},
|
|
154
|
+
},
|
|
155
|
+
diagnostics: {
|
|
156
|
+
warnings: [],
|
|
157
|
+
errors: [],
|
|
158
|
+
},
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const children: BlockNode[] = [];
|
|
162
|
+
let previousParagraph = false;
|
|
163
|
+
let lastYieldAt = nowMs();
|
|
164
|
+
|
|
165
|
+
for (let i = 0; i < document.blocks.length; i += 1) {
|
|
166
|
+
const block = document.blocks[i];
|
|
167
|
+
const normalizedBlocks = normalizeBlocks(block, state, packagePartName);
|
|
168
|
+
for (const normalizedBlock of normalizedBlocks) {
|
|
169
|
+
if (previousParagraph && normalizedBlock.type === "paragraph") {
|
|
170
|
+
state.cursor += 1;
|
|
171
|
+
}
|
|
172
|
+
children.push(normalizedBlock);
|
|
173
|
+
previousParagraph = normalizedBlock.type === "paragraph";
|
|
174
|
+
}
|
|
175
|
+
if (
|
|
176
|
+
i > 0 &&
|
|
177
|
+
i % NORMALIZE_YIELD_STRIDE === 0 &&
|
|
178
|
+
shouldYield(scheduler, lastYieldAt)
|
|
179
|
+
) {
|
|
180
|
+
await scheduler.yield();
|
|
181
|
+
lastYieldAt = nowMs();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const content: DocumentRootNode = { type: "doc", children };
|
|
186
|
+
|
|
187
|
+
const styles = options?.styles ?? { paragraphs: {}, characters: {}, tables: {} };
|
|
188
|
+
const fieldRegistry = buildFieldRegistry({ content, styles });
|
|
189
|
+
const hasFields = fieldRegistry.supported.length > 0 || fieldRegistry.preserveOnly.length > 0;
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
content,
|
|
193
|
+
media: state.media,
|
|
194
|
+
preservation: state.preservation,
|
|
195
|
+
diagnostics: state.diagnostics,
|
|
196
|
+
...(document.finalSectionProperties !== undefined
|
|
197
|
+
? { finalSectionProperties: document.finalSectionProperties }
|
|
198
|
+
: {}),
|
|
199
|
+
...(hasFields ? { fieldRegistry } : {}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
118
203
|
function normalizeBlocks(
|
|
119
204
|
block: ParsedBlockNode,
|
|
120
205
|
state: NormalizationState,
|
|
@@ -393,6 +478,7 @@ function normalizeInlineChildren(
|
|
|
393
478
|
state.cursor += 1;
|
|
394
479
|
break;
|
|
395
480
|
case "chart_preview":
|
|
481
|
+
registerComplexPreviewMedia(state, node);
|
|
396
482
|
normalized.push({
|
|
397
483
|
type: "chart_preview",
|
|
398
484
|
...(node.previewMediaId ? { previewMediaId: node.previewMediaId } : {}),
|
|
@@ -401,6 +487,7 @@ function normalizeInlineChildren(
|
|
|
401
487
|
state.cursor += 1;
|
|
402
488
|
break;
|
|
403
489
|
case "smartart_preview":
|
|
490
|
+
registerComplexPreviewMedia(state, node);
|
|
404
491
|
normalized.push({
|
|
405
492
|
type: "smartart_preview",
|
|
406
493
|
...(node.previewMediaId ? { previewMediaId: node.previewMediaId } : {}),
|
|
@@ -528,6 +615,35 @@ function normalizeImageNode(
|
|
|
528
615
|
};
|
|
529
616
|
}
|
|
530
617
|
|
|
618
|
+
/**
|
|
619
|
+
* Register a chart/SmartArt preview bitmap in the media catalog so the
|
|
620
|
+
* surface renderer can resolve `previewMediaId` → `previewSrc` the same
|
|
621
|
+
* way it does for inline images. Chart/SmartArt nodes weren't previously
|
|
622
|
+
* registered because only image nodes walked through `normalizeImageNode`.
|
|
623
|
+
* Needed by docs/plans/lane-5-charts.md Stage 0 (real mc:Fallback bitmaps) and
|
|
624
|
+
* Stage 0B (synthesized previews from the demo harness decorator) —
|
|
625
|
+
* without this, previewMediaId sits in the canonical model but
|
|
626
|
+
* `canonicalDocument.media.items` has no corresponding MediaItem and
|
|
627
|
+
* the editor's mediaPreview resolver skips the bitmap.
|
|
628
|
+
*/
|
|
629
|
+
function registerComplexPreviewMedia(
|
|
630
|
+
state: NormalizationState,
|
|
631
|
+
node: ParsedChartPreviewNode | ParsedSmartArtPreviewNode,
|
|
632
|
+
): void {
|
|
633
|
+
if (!node.previewMediaId) return;
|
|
634
|
+
if (state.media.items[node.previewMediaId]) return; // already registered (e.g. via another reference)
|
|
635
|
+
const packagePartName =
|
|
636
|
+
node.previewPackagePartName ?? `/${node.previewMediaId.slice("media:".length)}`;
|
|
637
|
+
const filename =
|
|
638
|
+
packagePartName.slice(packagePartName.lastIndexOf("/") + 1) || "chart-preview.bin";
|
|
639
|
+
state.media.items[node.previewMediaId] = {
|
|
640
|
+
mediaId: node.previewMediaId,
|
|
641
|
+
contentType: node.previewContentType ?? "application/octet-stream",
|
|
642
|
+
filename,
|
|
643
|
+
packagePartName,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
|
|
531
647
|
function normalizeHyperlink(node: ParsedHyperlinkNode): {
|
|
532
648
|
type: "hyperlink";
|
|
533
649
|
href: string;
|
|
@@ -173,39 +173,6 @@ export function parseCommentsFromOoxml(
|
|
|
173
173
|
continue;
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
-
if (startParagraphIndex !== endParagraphIndex) {
|
|
177
|
-
diagnostics.push({
|
|
178
|
-
commentId: rootCommentId,
|
|
179
|
-
code: "multi_paragraph_anchor_preserve_only",
|
|
180
|
-
message:
|
|
181
|
-
"Comment anchor spans multiple paragraphs. Thread is visible but detached; cross-paragraph anchoring is not yet supported for live editing.",
|
|
182
|
-
featureClass: "preserve-only",
|
|
183
|
-
detachedReason: "multi-paragraph",
|
|
184
|
-
actionabilityNote: "The comment thread and body are preserved. Operators see the thread in the sidebar but cannot navigate to an inline highlight.",
|
|
185
|
-
});
|
|
186
|
-
threads.push(
|
|
187
|
-
createImportedCommentThread({
|
|
188
|
-
commentId: rootCommentId,
|
|
189
|
-
body: rootDefinition.body,
|
|
190
|
-
createdBy,
|
|
191
|
-
createdAt,
|
|
192
|
-
range: detachedRange,
|
|
193
|
-
entries,
|
|
194
|
-
status: "detached",
|
|
195
|
-
resolution,
|
|
196
|
-
metadata: {
|
|
197
|
-
source: "import",
|
|
198
|
-
rootOoxmlCommentId: rootDefinition.commentId,
|
|
199
|
-
rootParaId: rootDefinition.paraId,
|
|
200
|
-
detachedReason: "multi-paragraph",
|
|
201
|
-
actionabilityNote:
|
|
202
|
-
"The comment thread and body are preserved. Operators see the thread in the sidebar but cannot navigate to an inline highlight.",
|
|
203
|
-
},
|
|
204
|
-
}),
|
|
205
|
-
);
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
176
|
threads.push(
|
|
210
177
|
createImportedCommentThread({
|
|
211
178
|
commentId: rootCommentId,
|
|
@@ -21,6 +21,10 @@ export interface ParsedChartContent {
|
|
|
21
21
|
type: "chart_preview";
|
|
22
22
|
/** Media ID of the fallback preview image, if one is present in mc:Fallback. */
|
|
23
23
|
previewMediaId?: string;
|
|
24
|
+
/** Absolute package path of the preview media (e.g. `/word/media/chartN.png`); required by the media catalog when the preview media needs an entry. */
|
|
25
|
+
previewPackagePartName?: string;
|
|
26
|
+
/** MIME type of the preview media (e.g. `image/png`, `image/svg+xml`). */
|
|
27
|
+
previewContentType?: string;
|
|
24
28
|
/** Original drawing XML slice for lossless round-trip export. */
|
|
25
29
|
rawXml: string;
|
|
26
30
|
}
|
|
@@ -29,6 +33,10 @@ export interface ParsedSmartArtContent {
|
|
|
29
33
|
type: "smartart_preview";
|
|
30
34
|
/** Media ID of the fallback preview image, if one is present in mc:Fallback. */
|
|
31
35
|
previewMediaId?: string;
|
|
36
|
+
/** Absolute package path of the preview media. */
|
|
37
|
+
previewPackagePartName?: string;
|
|
38
|
+
/** MIME type of the preview media. */
|
|
39
|
+
previewContentType?: string;
|
|
32
40
|
/** Original drawing XML slice for lossless round-trip export. */
|
|
33
41
|
rawXml: string;
|
|
34
42
|
}
|
|
@@ -116,6 +124,8 @@ function parseAlternateContent(
|
|
|
116
124
|
|
|
117
125
|
// Extract fallback preview image if present
|
|
118
126
|
let previewMediaId: string | undefined;
|
|
127
|
+
let previewPackagePartName: string | undefined;
|
|
128
|
+
let previewContentType: string | undefined;
|
|
119
129
|
if (fallback) {
|
|
120
130
|
const fallbackBlip = findFirstDescendant(fallback, "blip");
|
|
121
131
|
if (fallbackBlip) {
|
|
@@ -132,6 +142,8 @@ function parseAlternateContent(
|
|
|
132
142
|
const mediaPart = mediaParts.get(packagePartName);
|
|
133
143
|
if (mediaPart) {
|
|
134
144
|
previewMediaId = `media:${packagePartName.slice(1)}`;
|
|
145
|
+
previewPackagePartName = packagePartName;
|
|
146
|
+
previewContentType = mediaPart.contentType;
|
|
135
147
|
}
|
|
136
148
|
}
|
|
137
149
|
}
|
|
@@ -141,6 +153,8 @@ function parseAlternateContent(
|
|
|
141
153
|
return {
|
|
142
154
|
type: contentType,
|
|
143
155
|
...(previewMediaId ? { previewMediaId } : {}),
|
|
156
|
+
...(previewPackagePartName ? { previewPackagePartName } : {}),
|
|
157
|
+
...(previewContentType ? { previewContentType } : {}),
|
|
144
158
|
rawXml: fullDrawingXml,
|
|
145
159
|
};
|
|
146
160
|
}
|
|
@@ -211,12 +211,16 @@ export interface ParsedOpaqueInlineNode {
|
|
|
211
211
|
export interface ParsedChartPreviewNode {
|
|
212
212
|
type: "chart_preview";
|
|
213
213
|
previewMediaId?: string;
|
|
214
|
+
previewPackagePartName?: string;
|
|
215
|
+
previewContentType?: string;
|
|
214
216
|
rawXml: string;
|
|
215
217
|
}
|
|
216
218
|
|
|
217
219
|
export interface ParsedSmartArtPreviewNode {
|
|
218
220
|
type: "smartart_preview";
|
|
219
221
|
previewMediaId?: string;
|
|
222
|
+
previewPackagePartName?: string;
|
|
223
|
+
previewContentType?: string;
|
|
220
224
|
rawXml: string;
|
|
221
225
|
}
|
|
222
226
|
|