@beyondwork/docx-react-component 1.0.57 → 1.0.59
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 +1 -1
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +1149 -8
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +2 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +120 -39
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +165 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +3 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
- package/src/runtime/document-runtime.ts +544 -35
- package/src/runtime/document-search.ts +176 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +183 -0
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/scope-resolver.ts +60 -0
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +293 -18
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +258 -44
- package/src/ui/editor-runtime-boundary.ts +13 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +23 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +158 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +105 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +52 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -97,13 +97,27 @@ function serializeMarkup(
|
|
|
97
97
|
|
|
98
98
|
switch (revision.kind) {
|
|
99
99
|
case "move":
|
|
100
|
+
return serializeMoveMarkup(markup, revision.status);
|
|
100
101
|
case "formatting":
|
|
102
|
+
if (
|
|
103
|
+
markup.originalRevisionType === "cellIns" ||
|
|
104
|
+
markup.originalRevisionType === "cellDel" ||
|
|
105
|
+
markup.originalRevisionType === "cellMerge"
|
|
106
|
+
) {
|
|
107
|
+
return serializeStructuralTableMarkup(markup.rawXml, revision.status);
|
|
108
|
+
}
|
|
101
109
|
return markup.rawXml;
|
|
102
110
|
case "property-change":
|
|
103
111
|
return serializePropertyChangeMarkup(markup.rawXml, revision.status);
|
|
104
112
|
case "insertion":
|
|
113
|
+
if (markup.originalRevisionType === "row-ins") {
|
|
114
|
+
return serializeStructuralRowMarkup(markup.rawXml, revision.status);
|
|
115
|
+
}
|
|
105
116
|
return serializeInsertionMarkup(markup.rawXml, revision.status);
|
|
106
117
|
case "deletion":
|
|
118
|
+
if (markup.originalRevisionType === "row-del") {
|
|
119
|
+
return serializeStructuralRowMarkup(markup.rawXml, revision.status);
|
|
120
|
+
}
|
|
107
121
|
return serializeDeletionMarkup(markup.rawXml, revision.status);
|
|
108
122
|
}
|
|
109
123
|
}
|
|
@@ -346,6 +360,91 @@ function serializePropertyChangeMarkup(
|
|
|
346
360
|
}
|
|
347
361
|
}
|
|
348
362
|
|
|
363
|
+
function serializeMoveMarkup(
|
|
364
|
+
markup: PreservedRevisionMarkup,
|
|
365
|
+
status: RevisionRecord["status"],
|
|
366
|
+
): string {
|
|
367
|
+
if (status === "active" || status === "detached") {
|
|
368
|
+
return markup.rawXml;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const direction: "from" | "to" | undefined =
|
|
372
|
+
markup.originalRevisionType === "moveFrom"
|
|
373
|
+
? "from"
|
|
374
|
+
: markup.originalRevisionType === "moveTo"
|
|
375
|
+
? "to"
|
|
376
|
+
: undefined;
|
|
377
|
+
|
|
378
|
+
if (!direction) {
|
|
379
|
+
return markup.rawXml;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Accept = commit the move: source removed, destination unwrapped.
|
|
383
|
+
// Reject = cancel the move: source unwrapped, destination removed.
|
|
384
|
+
const shouldDropWrapperAndContent =
|
|
385
|
+
(status === "accepted" && direction === "from") ||
|
|
386
|
+
(status === "rejected" && direction === "to");
|
|
387
|
+
|
|
388
|
+
if (shouldDropWrapperAndContent) {
|
|
389
|
+
return "";
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return unwrapMoveWrapper(markup.rawXml, direction);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function unwrapMoveWrapper(rawXml: string, direction: "from" | "to"): string {
|
|
396
|
+
const tag = direction === "from" ? "moveFrom" : "moveTo";
|
|
397
|
+
const selfClosingPattern = new RegExp(`^\\s*<w:${tag}\\b[^>]*/>\\s*$`);
|
|
398
|
+
if (selfClosingPattern.test(rawXml)) {
|
|
399
|
+
return "";
|
|
400
|
+
}
|
|
401
|
+
const openPattern = new RegExp(`^\\s*<w:${tag}\\b[^>]*>`);
|
|
402
|
+
const closePattern = new RegExp(`</w:${tag}>\\s*$`);
|
|
403
|
+
return rawXml.replace(openPattern, "").replace(closePattern, "");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function serializeStructuralRowMarkup(
|
|
407
|
+
rawXml: string,
|
|
408
|
+
status: RevisionRecord["status"],
|
|
409
|
+
): string {
|
|
410
|
+
// accept(row-ins) and reject(row-del): drop the marker from trPr while
|
|
411
|
+
// leaving the enclosing row intact. The canonical-document row is
|
|
412
|
+
// unchanged in these cases.
|
|
413
|
+
// accept(row-del) and reject(row-ins): the canonical model layer already
|
|
414
|
+
// removed the entire row via `removeTableRowPure`, so the <w:tr> itself
|
|
415
|
+
// won't exist in the serialized output — this helper only runs on the
|
|
416
|
+
// surviving-row branch.
|
|
417
|
+
switch (status) {
|
|
418
|
+
case "accepted":
|
|
419
|
+
case "rejected":
|
|
420
|
+
return "";
|
|
421
|
+
case "active":
|
|
422
|
+
case "detached":
|
|
423
|
+
return rawXml;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function serializeStructuralTableMarkup(
|
|
428
|
+
rawXml: string,
|
|
429
|
+
status: RevisionRecord["status"],
|
|
430
|
+
): string {
|
|
431
|
+
// Lane 7b/L: cellIns on rejected status means the canonical-model layer
|
|
432
|
+
// already removed the enclosing <w:tc>, so the marker never reaches the
|
|
433
|
+
// exported table. When it does fire, the marker should be dropped just
|
|
434
|
+
// like on accept (the alternative — leaving the marker in place — would
|
|
435
|
+
// round-trip back as a still-pending revision, contradicting the store
|
|
436
|
+
// status). Accept = commit the cell (drop marker). Reject = cell removed
|
|
437
|
+
// (marker also drops).
|
|
438
|
+
switch (status) {
|
|
439
|
+
case "accepted":
|
|
440
|
+
case "rejected":
|
|
441
|
+
return "";
|
|
442
|
+
case "active":
|
|
443
|
+
case "detached":
|
|
444
|
+
return rawXml;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
349
448
|
function serializeInsertionMarkup(
|
|
350
449
|
rawXml: string,
|
|
351
450
|
status: RevisionRecord["status"],
|
|
@@ -229,6 +229,15 @@ function serializeCellShading(shading: ParsedCellShading): string {
|
|
|
229
229
|
if (shading.val) attrs.push(`w:val="${shading.val}"`);
|
|
230
230
|
if (shading.color) attrs.push(`w:color="${shading.color}"`);
|
|
231
231
|
if (shading.fill) attrs.push(`w:fill="${shading.fill}"`);
|
|
232
|
+
// SOW gap G3 — round-trip theme-shading attributes so a doc imported with
|
|
233
|
+
// `<w:shd w:themeFill="accent5" w:themeFillTint="33"/>` serializes back to
|
|
234
|
+
// the same form instead of losing the theme reference after edit.
|
|
235
|
+
if (shading.themeFill) attrs.push(`w:themeFill="${shading.themeFill}"`);
|
|
236
|
+
if (shading.themeFillTint) attrs.push(`w:themeFillTint="${shading.themeFillTint}"`);
|
|
237
|
+
if (shading.themeFillShade) attrs.push(`w:themeFillShade="${shading.themeFillShade}"`);
|
|
238
|
+
if (shading.themeColor) attrs.push(`w:themeColor="${shading.themeColor}"`);
|
|
239
|
+
if (shading.themeColorTint) attrs.push(`w:themeColorTint="${shading.themeColorTint}"`);
|
|
240
|
+
if (shading.themeColorShade) attrs.push(`w:themeColorShade="${shading.themeColorShade}"`);
|
|
232
241
|
const attrsStr = attrs.length > 0 ? ` ${attrs.join(" ")}` : "";
|
|
233
242
|
return `<w:shd${attrsStr}/>`;
|
|
234
243
|
}
|
|
@@ -45,6 +45,13 @@ interface CellShadingLike {
|
|
|
45
45
|
val?: string;
|
|
46
46
|
color?: string;
|
|
47
47
|
fill?: string;
|
|
48
|
+
/** SOW gap G3 — round-trip theme-shading references. */
|
|
49
|
+
themeFill?: string;
|
|
50
|
+
themeFillTint?: string;
|
|
51
|
+
themeFillShade?: string;
|
|
52
|
+
themeColor?: string;
|
|
53
|
+
themeColorTint?: string;
|
|
54
|
+
themeColorShade?: string;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
interface TableIndentLike {
|
|
@@ -476,5 +483,12 @@ function serializeCellShading(shading: CellShadingLike): string {
|
|
|
476
483
|
if (shading.val) attrs.push(`w:val="${escapeXmlAttribute(shading.val)}"`);
|
|
477
484
|
if (shading.color) attrs.push(`w:color="${escapeXmlAttribute(shading.color)}"`);
|
|
478
485
|
if (shading.fill) attrs.push(`w:fill="${escapeXmlAttribute(shading.fill)}"`);
|
|
486
|
+
// SOW gap G3 — theme-shading round-trip.
|
|
487
|
+
if (shading.themeFill) attrs.push(`w:themeFill="${escapeXmlAttribute(shading.themeFill)}"`);
|
|
488
|
+
if (shading.themeFillTint) attrs.push(`w:themeFillTint="${escapeXmlAttribute(shading.themeFillTint)}"`);
|
|
489
|
+
if (shading.themeFillShade) attrs.push(`w:themeFillShade="${escapeXmlAttribute(shading.themeFillShade)}"`);
|
|
490
|
+
if (shading.themeColor) attrs.push(`w:themeColor="${escapeXmlAttribute(shading.themeColor)}"`);
|
|
491
|
+
if (shading.themeColorTint) attrs.push(`w:themeColorTint="${escapeXmlAttribute(shading.themeColorTint)}"`);
|
|
492
|
+
if (shading.themeColorShade) attrs.push(`w:themeColorShade="${escapeXmlAttribute(shading.themeColorShade)}"`);
|
|
479
493
|
return attrs.length > 0 ? `<w:shd ${attrs.join(" ")}/>` : "";
|
|
480
494
|
}
|
package/src/io/load-scheduler.ts
CHANGED
|
@@ -25,12 +25,31 @@ export type LoadSchedulerBackend =
|
|
|
25
25
|
| "timeout"
|
|
26
26
|
| "sync";
|
|
27
27
|
|
|
28
|
+
export interface ScheduleIdleOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Abort the pending idle callback before it runs. If the signal is already
|
|
31
|
+
* aborted at call time, the task is not scheduled. Post-run aborts are
|
|
32
|
+
* no-ops. The idle handle is cancelled and the abort listener removed in
|
|
33
|
+
* both the run and abort paths.
|
|
34
|
+
*
|
|
35
|
+
* Typical consumer: `RuntimeCollabSyncHandle.getRemoteActivitySignal()`
|
|
36
|
+
* (Lane 4 R3). Idle prep that races with remote commit work passes the
|
|
37
|
+
* signal here so remote arrivals cancel stale prep before the document
|
|
38
|
+
* moves. See `docs/wiki/collaboration.md` §"Remote-activity abort
|
|
39
|
+
* signal (R3)" for the end-to-end narrative.
|
|
40
|
+
*
|
|
41
|
+
* Stability: advanced-supported (v2.0.0, shipped 2026-04-20 in
|
|
42
|
+
* commit c2aeaf8d).
|
|
43
|
+
*/
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
export interface LoadScheduler {
|
|
29
48
|
readonly backend: LoadSchedulerBackend;
|
|
30
49
|
/** Yield to the browser. Resolves on next scheduled task / microtask. */
|
|
31
50
|
yield(): Promise<void>;
|
|
32
51
|
/** Schedule low-priority work for post-skeleton execution. */
|
|
33
|
-
scheduleIdle(task: () => void): void;
|
|
52
|
+
scheduleIdle(task: () => void, options?: ScheduleIdleOptions): void;
|
|
34
53
|
/** Cancel pending idle tasks. Must be called on unmount / dispose. */
|
|
35
54
|
dispose(): void;
|
|
36
55
|
}
|
|
@@ -115,9 +134,8 @@ function createSchedulerApiBackend(frameDeadlineMs: number): InternalScheduler {
|
|
|
115
134
|
backend: "scheduler-api",
|
|
116
135
|
frameDeadlineMs,
|
|
117
136
|
yield: () => g.scheduler.yield(),
|
|
118
|
-
scheduleIdle(task) {
|
|
119
|
-
|
|
120
|
-
pendingIdleHandles.add(handle);
|
|
137
|
+
scheduleIdle(task, options) {
|
|
138
|
+
scheduleIdleCallback(task, pendingIdleHandles, options);
|
|
121
139
|
},
|
|
122
140
|
dispose() {
|
|
123
141
|
disposeIdleHandles(pendingIdleHandles);
|
|
@@ -141,9 +159,8 @@ function createMessageChannelBackend(frameDeadlineMs: number): InternalScheduler
|
|
|
141
159
|
channel.port2.postMessage(null);
|
|
142
160
|
});
|
|
143
161
|
},
|
|
144
|
-
scheduleIdle(task) {
|
|
145
|
-
|
|
146
|
-
pendingIdleHandles.add(handle);
|
|
162
|
+
scheduleIdle(task, options) {
|
|
163
|
+
scheduleIdleCallback(task, pendingIdleHandles, options);
|
|
147
164
|
},
|
|
148
165
|
dispose() {
|
|
149
166
|
disposeIdleHandles(pendingIdleHandles);
|
|
@@ -161,15 +178,11 @@ function createTimeoutBackend(frameDeadlineMs: number): InternalScheduler {
|
|
|
161
178
|
setTimeout(resolve, 0);
|
|
162
179
|
});
|
|
163
180
|
},
|
|
164
|
-
scheduleIdle(task) {
|
|
165
|
-
|
|
166
|
-
pendingIdleHandles.add(handle);
|
|
181
|
+
scheduleIdle(task, options) {
|
|
182
|
+
scheduleIdleCallback(task, pendingIdleHandles, options);
|
|
167
183
|
},
|
|
168
184
|
dispose() {
|
|
169
|
-
|
|
170
|
-
clearTimeout(handle as unknown as ReturnType<typeof setTimeout>);
|
|
171
|
-
}
|
|
172
|
-
pendingIdleHandles.clear();
|
|
185
|
+
disposeIdleHandles(pendingIdleHandles);
|
|
173
186
|
},
|
|
174
187
|
};
|
|
175
188
|
}
|
|
@@ -179,7 +192,8 @@ function createSyncBackend(frameDeadlineMs: number): InternalScheduler {
|
|
|
179
192
|
backend: "sync",
|
|
180
193
|
frameDeadlineMs,
|
|
181
194
|
yield: () => Promise.resolve(),
|
|
182
|
-
scheduleIdle(task) {
|
|
195
|
+
scheduleIdle(task, options) {
|
|
196
|
+
if (options?.signal?.aborted) return;
|
|
183
197
|
task();
|
|
184
198
|
},
|
|
185
199
|
dispose() {
|
|
@@ -193,26 +207,54 @@ type IdleHandle = number;
|
|
|
193
207
|
function scheduleIdleCallback(
|
|
194
208
|
task: () => void,
|
|
195
209
|
store: Set<IdleHandle>,
|
|
196
|
-
|
|
210
|
+
options?: ScheduleIdleOptions,
|
|
211
|
+
): void {
|
|
212
|
+
const signal = options?.signal;
|
|
213
|
+
if (signal?.aborted) return;
|
|
214
|
+
|
|
197
215
|
const g = globalThis as unknown as {
|
|
198
216
|
requestIdleCallback?: (cb: () => void, options?: { timeout: number }) => number;
|
|
199
217
|
cancelIdleCallback?: (handle: number) => void;
|
|
200
218
|
};
|
|
219
|
+
|
|
220
|
+
// Forward-declared handle ref so onAbort can cancel before it's assigned.
|
|
221
|
+
let handle: IdleHandle = -1;
|
|
222
|
+
let onAbort: (() => void) | null = null;
|
|
223
|
+
|
|
224
|
+
const cleanupAbortListener = () => {
|
|
225
|
+
if (signal && onAbort) {
|
|
226
|
+
signal.removeEventListener("abort", onAbort);
|
|
227
|
+
onAbort = null;
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const run = () => {
|
|
232
|
+
store.delete(handle);
|
|
233
|
+
cleanupAbortListener();
|
|
234
|
+
task();
|
|
235
|
+
};
|
|
236
|
+
|
|
201
237
|
if (typeof g.requestIdleCallback === "function") {
|
|
202
|
-
|
|
203
|
-
|
|
238
|
+
handle = g.requestIdleCallback(run, { timeout: 50 });
|
|
239
|
+
} else {
|
|
240
|
+
handle = setTimeout(run, 0) as unknown as number;
|
|
241
|
+
}
|
|
242
|
+
store.add(handle);
|
|
243
|
+
|
|
244
|
+
if (signal) {
|
|
245
|
+
onAbort = () => {
|
|
246
|
+
if (store.has(handle)) {
|
|
247
|
+
if (typeof g.cancelIdleCallback === "function" && typeof g.requestIdleCallback === "function") {
|
|
248
|
+
g.cancelIdleCallback(handle);
|
|
249
|
+
} else {
|
|
250
|
+
clearTimeout(handle as unknown as ReturnType<typeof setTimeout>);
|
|
251
|
+
}
|
|
204
252
|
store.delete(handle);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
);
|
|
209
|
-
return handle;
|
|
253
|
+
}
|
|
254
|
+
onAbort = null;
|
|
255
|
+
};
|
|
256
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
210
257
|
}
|
|
211
|
-
const handle = setTimeout(() => {
|
|
212
|
-
store.delete(handle as unknown as number);
|
|
213
|
-
task();
|
|
214
|
-
}, 0) as unknown as number;
|
|
215
|
-
return handle;
|
|
216
258
|
}
|
|
217
259
|
|
|
218
260
|
function disposeIdleHandles(store: Set<IdleHandle>): void {
|
|
@@ -531,6 +531,18 @@ function normalizeInlineChildren(
|
|
|
531
531
|
});
|
|
532
532
|
state.cursor += 1;
|
|
533
533
|
break;
|
|
534
|
+
case "ole_embed":
|
|
535
|
+
normalized.push({
|
|
536
|
+
type: "ole_embed",
|
|
537
|
+
id: node.id,
|
|
538
|
+
...(node.progId ? { progId: node.progId } : {}),
|
|
539
|
+
embedType: node.embedType,
|
|
540
|
+
relationshipId: node.relationshipId,
|
|
541
|
+
metadata: { ...node.metadata },
|
|
542
|
+
rawXml: node.rawXml,
|
|
543
|
+
});
|
|
544
|
+
state.cursor += 1;
|
|
545
|
+
break;
|
|
534
546
|
case "bookmark_start":
|
|
535
547
|
normalized.push({
|
|
536
548
|
type: "bookmark_start",
|
|
@@ -569,6 +581,7 @@ function normalizeInlineChildren(
|
|
|
569
581
|
...(classification.target ? { fieldTarget: classification.target } : {}),
|
|
570
582
|
...(classification.switches ? { switches: classification.switches } : {}),
|
|
571
583
|
refreshStatus: classification.supported ? "stale" : "preserve-only",
|
|
584
|
+
...(node.legacyFormField ? { legacyFormField: node.legacyFormField } : {}),
|
|
572
585
|
});
|
|
573
586
|
state.cursor += renderedLength > 0 ? renderedLength : 1;
|
|
574
587
|
break;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _mini-xml.ts — Shared minimal XML parser for CO4 DrawingML modules.
|
|
3
|
+
*
|
|
4
|
+
* Consolidates the four identical copies previously duplicated across
|
|
5
|
+
* parse-anchor.ts, parse-drawing.ts, parse-picture.ts, and parse-shapes.ts.
|
|
6
|
+
*
|
|
7
|
+
* Scope: OOXML attribute-preserving walker used for decoding DrawingML
|
|
8
|
+
* fragments. Self-contained (no fast-xml-parser dependency), low allocation,
|
|
9
|
+
* preserves attribute order.
|
|
10
|
+
*
|
|
11
|
+
* Throws on unterminated tags (CO4 B4 fix). Callers in parseDrawingFrame +
|
|
12
|
+
* parseShapeContent are already try/catch-wrapped at parse-main-document.ts.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export interface XmlElementNode {
|
|
16
|
+
type: "element";
|
|
17
|
+
name: string;
|
|
18
|
+
attributes: Record<string, string>;
|
|
19
|
+
children: XmlNode[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface XmlTextNode {
|
|
23
|
+
type: "text";
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type XmlNode = XmlElementNode | XmlTextNode;
|
|
28
|
+
|
|
29
|
+
export function findFirstChild(
|
|
30
|
+
node: XmlElementNode,
|
|
31
|
+
local: string,
|
|
32
|
+
): XmlElementNode | undefined {
|
|
33
|
+
for (const child of node.children) {
|
|
34
|
+
if (child.type === "element" && localName(child.name) === local) return child;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function findFirstDescendant(
|
|
40
|
+
node: XmlElementNode,
|
|
41
|
+
local: string,
|
|
42
|
+
): XmlElementNode | undefined {
|
|
43
|
+
for (const child of node.children) {
|
|
44
|
+
if (child.type !== "element") continue;
|
|
45
|
+
if (localName(child.name) === local) return child;
|
|
46
|
+
const found = findFirstDescendant(child, local);
|
|
47
|
+
if (found) return found;
|
|
48
|
+
}
|
|
49
|
+
return undefined;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function findDescendants(
|
|
53
|
+
node: XmlElementNode,
|
|
54
|
+
local: string,
|
|
55
|
+
): XmlElementNode[] {
|
|
56
|
+
const out: XmlElementNode[] = [];
|
|
57
|
+
for (const child of node.children) {
|
|
58
|
+
if (child.type !== "element") continue;
|
|
59
|
+
if (localName(child.name) === local) out.push(child);
|
|
60
|
+
out.push(...findDescendants(child, local));
|
|
61
|
+
}
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function localName(name: string): string {
|
|
66
|
+
const i = name.indexOf(":");
|
|
67
|
+
return i >= 0 ? name.slice(i + 1) : name;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function extractText(node: XmlElementNode): string {
|
|
71
|
+
return node.children
|
|
72
|
+
.map((c) => (c.type === "text" ? c.text : extractText(c as XmlElementNode)))
|
|
73
|
+
.join("");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function parseXml(xml: string): XmlElementNode {
|
|
77
|
+
const root: XmlElementNode = {
|
|
78
|
+
type: "element",
|
|
79
|
+
name: "__root__",
|
|
80
|
+
attributes: {},
|
|
81
|
+
children: [],
|
|
82
|
+
};
|
|
83
|
+
const stack: XmlElementNode[] = [root];
|
|
84
|
+
let cursor = 0;
|
|
85
|
+
|
|
86
|
+
while (cursor < xml.length) {
|
|
87
|
+
if (xml.startsWith("<!--", cursor)) {
|
|
88
|
+
const end = xml.indexOf("-->", cursor);
|
|
89
|
+
cursor = end >= 0 ? end + 3 : xml.length;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (xml.startsWith("<?", cursor)) {
|
|
93
|
+
const end = xml.indexOf("?>", cursor);
|
|
94
|
+
cursor = end >= 0 ? end + 2 : xml.length;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (xml[cursor] !== "<") {
|
|
98
|
+
const nextTag = xml.indexOf("<", cursor);
|
|
99
|
+
const end = nextTag >= 0 ? nextTag : xml.length;
|
|
100
|
+
const text = decodeEntities(xml.slice(cursor, end));
|
|
101
|
+
if (text) stack[stack.length - 1]?.children.push({ type: "text", text });
|
|
102
|
+
cursor = end;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (xml[cursor + 1] === "/") {
|
|
106
|
+
const end = xml.indexOf(">", cursor);
|
|
107
|
+
stack.pop();
|
|
108
|
+
cursor = end + 1;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const tagEnd = findTagEnd(xml, cursor);
|
|
112
|
+
const tagBody = xml.slice(cursor + 1, tagEnd);
|
|
113
|
+
const selfClosing = /\/\s*$/.test(tagBody);
|
|
114
|
+
const { name, attributes } = parseTag(tagBody.replace(/\/\s*$/, "").trim());
|
|
115
|
+
const el: XmlElementNode = { type: "element", name, attributes, children: [] };
|
|
116
|
+
stack[stack.length - 1]?.children.push(el);
|
|
117
|
+
if (!selfClosing) stack.push(el);
|
|
118
|
+
cursor = tagEnd + 1;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return root;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function findTagEnd(xml: string, start: number): number {
|
|
125
|
+
let cursor = start + 1;
|
|
126
|
+
let quote: string | null = null;
|
|
127
|
+
while (cursor < xml.length) {
|
|
128
|
+
const ch = xml[cursor];
|
|
129
|
+
if (quote) {
|
|
130
|
+
if (ch === quote) quote = null;
|
|
131
|
+
} else if (ch === `"` || ch === `'`) {
|
|
132
|
+
quote = ch;
|
|
133
|
+
} else if (ch === ">") {
|
|
134
|
+
return cursor;
|
|
135
|
+
}
|
|
136
|
+
cursor++;
|
|
137
|
+
}
|
|
138
|
+
// B4 — throw on unterminated tag; callers are try/catch-wrapped at the
|
|
139
|
+
// parse-main-document.ts case "drawing" dispatch so the legacy chain +
|
|
140
|
+
// opaque_inline fallback take over.
|
|
141
|
+
throw new Error(`Malformed XML: unterminated tag starting at offset ${start}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function parseTag(body: string): { name: string; attributes: Record<string, string> } {
|
|
145
|
+
let i = 0;
|
|
146
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i++;
|
|
147
|
+
const nameStart = i;
|
|
148
|
+
while (i < body.length && !/\s/.test(body[i] ?? "")) i++;
|
|
149
|
+
const name = body.slice(nameStart, i);
|
|
150
|
+
const attributes: Record<string, string> = {};
|
|
151
|
+
while (i < body.length) {
|
|
152
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i++;
|
|
153
|
+
if (i >= body.length) break;
|
|
154
|
+
const kStart = i;
|
|
155
|
+
while (i < body.length && !/[\s=]/.test(body[i] ?? "")) i++;
|
|
156
|
+
const key = body.slice(kStart, i);
|
|
157
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i++;
|
|
158
|
+
if (body[i] !== "=") { attributes[key] = ""; continue; }
|
|
159
|
+
i++;
|
|
160
|
+
while (i < body.length && /\s/.test(body[i] ?? "")) i++;
|
|
161
|
+
const q = body[i];
|
|
162
|
+
if (q !== `"` && q !== `'`) throw new Error(`Bad attr ${key}`);
|
|
163
|
+
i++;
|
|
164
|
+
const vStart = i;
|
|
165
|
+
while (i < body.length && body[i] !== q) i++;
|
|
166
|
+
attributes[key] = decodeEntities(body.slice(vStart, i));
|
|
167
|
+
i++;
|
|
168
|
+
}
|
|
169
|
+
return { name, attributes };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function decodeEntities(s: string): string {
|
|
173
|
+
return s.replace(/&(#x[0-9a-fA-F]+|#\d+|amp|lt|gt|quot|apos);/g, (_, e) => {
|
|
174
|
+
if (e === "amp") return "&";
|
|
175
|
+
if (e === "lt") return "<";
|
|
176
|
+
if (e === "gt") return ">";
|
|
177
|
+
if (e === "quot") return `"`;
|
|
178
|
+
if (e === "apos") return "'";
|
|
179
|
+
if (e.startsWith("#x")) return String.fromCodePoint(parseInt(e.slice(2), 16));
|
|
180
|
+
if (e.startsWith("#")) return String.fromCodePoint(parseInt(e.slice(1), 10));
|
|
181
|
+
return `&${e};`;
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function encodeEntities(s: string): string {
|
|
186
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function serializeXmlNode(node: XmlElementNode): string {
|
|
190
|
+
const attrs = Object.entries(node.attributes)
|
|
191
|
+
.map(([k, v]) => ` ${k}="${encodeEntities(v)}"`)
|
|
192
|
+
.join("");
|
|
193
|
+
if (node.children.length === 0) return `<${node.name}${attrs}/>`;
|
|
194
|
+
const inner = node.children
|
|
195
|
+
.map((c) => (c.type === "text" ? encodeEntities(c.text) : serializeXmlNode(c)))
|
|
196
|
+
.join("");
|
|
197
|
+
return `<${node.name}${attrs}>${inner}</${node.name}>`;
|
|
198
|
+
}
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* reader use this same routine) and self-referential (the signature
|
|
22
22
|
* element is excluded from its own input).
|
|
23
23
|
*/
|
|
24
|
+
import { localName } from "./xml-attr-helpers.ts";
|
|
24
25
|
export function canonicalizePayload(xml: string): Uint8Array {
|
|
25
26
|
const tree = parseXml(xml);
|
|
26
27
|
const filtered = stripSignature(tree);
|
|
@@ -273,10 +274,6 @@ function preservesSpace(): boolean {
|
|
|
273
274
|
|
|
274
275
|
// ----- Helpers --------------------------------------------------------------
|
|
275
276
|
|
|
276
|
-
function localName(qname: string): string {
|
|
277
|
-
const colon = qname.indexOf(":");
|
|
278
|
-
return colon < 0 ? qname : qname.slice(colon + 1);
|
|
279
|
-
}
|
|
280
277
|
|
|
281
278
|
function normalizeWhitespace(value: string): string {
|
|
282
279
|
return value.replace(/\s+/g, " ").trim();
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
29
|
import type { FillSpec, StrokeSpec, TextProperties } from "./types.ts";
|
|
30
|
+
import { EMU_PER_PX } from "../../../runtime/units.ts";
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* How each series gets a color.
|
|
@@ -100,7 +101,7 @@ const GRIDLINE_STROKE_SUBTLE: StrokeSpec = {
|
|
|
100
101
|
{ kind: "lumMod", value: 15000 },
|
|
101
102
|
{ kind: "lumOff", value: 85000 },
|
|
102
103
|
] },
|
|
103
|
-
widthEmu:
|
|
104
|
+
widthEmu: EMU_PER_PX, // 0.75 pt
|
|
104
105
|
dash: "solid",
|
|
105
106
|
};
|
|
106
107
|
|
|
@@ -110,7 +111,7 @@ const SERIES_OUTLINE_NONE: StrokeSpec = {
|
|
|
110
111
|
|
|
111
112
|
const SERIES_OUTLINE_LIGHT: StrokeSpec = {
|
|
112
113
|
color: { kind: "scheme", value: "lt1" },
|
|
113
|
-
widthEmu:
|
|
114
|
+
widthEmu: EMU_PER_PX,
|
|
114
115
|
dash: "solid",
|
|
115
116
|
};
|
|
116
117
|
|
|
@@ -305,7 +306,7 @@ const GRIDLINE_STROKE_LIGHT: StrokeSpec = {
|
|
|
305
306
|
color: { kind: "scheme", value: "lt1", mods: [
|
|
306
307
|
{ kind: "lumMod", value: 80000 },
|
|
307
308
|
] },
|
|
308
|
-
widthEmu:
|
|
309
|
+
widthEmu: EMU_PER_PX,
|
|
309
310
|
dash: "solid",
|
|
310
311
|
};
|
|
311
312
|
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from "../xml-attr-helpers.ts";
|
|
31
31
|
import type { XmlElementNode } from "../xml-element.ts";
|
|
32
32
|
import { parseXml } from "../xml-parser.ts";
|
|
33
|
+
import { ROTATION_UNITS_PER_DEGREE } from "../../../runtime/units.ts";
|
|
33
34
|
|
|
34
35
|
import { parseAxisNode } from "./parse-axis.ts";
|
|
35
36
|
import {
|
|
@@ -530,9 +531,6 @@ function readGroupBoolean(groupNode: XmlElementNode, local: string): boolean {
|
|
|
530
531
|
// Pie / Doughnut
|
|
531
532
|
// ---------------------------------------------------------------------------
|
|
532
533
|
|
|
533
|
-
/** OOXML's angle unit is 1/60000 of a degree (sixtyThousandths). */
|
|
534
|
-
const OOXML_ANGLE_UNIT = 60000;
|
|
535
|
-
|
|
536
534
|
function parsePieGroup(
|
|
537
535
|
groupNode: XmlElementNode,
|
|
538
536
|
common: ChartCommon,
|
|
@@ -553,7 +551,7 @@ function parsePieGroup(
|
|
|
553
551
|
const firstSliceAngleRaw = readIntVal(findChildOptional(groupNode, "firstSliceAngle"));
|
|
554
552
|
const firstSliceAngle =
|
|
555
553
|
firstSliceAngleRaw !== undefined
|
|
556
|
-
? (((firstSliceAngleRaw /
|
|
554
|
+
? (((firstSliceAngleRaw / ROTATION_UNITS_PER_DEGREE) % 360) + 360) % 360
|
|
557
555
|
: 0;
|
|
558
556
|
|
|
559
557
|
// Doughnut hole size is a percent (0–99 typical). Missing on pieChart.
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
} from "../xml-attr-helpers.ts";
|
|
23
23
|
import type { XmlElementNode } from "../xml-element.ts";
|
|
24
24
|
import { parseXml } from "../xml-parser.ts";
|
|
25
|
+
import { GRADIENT_STOP_UNITS } from "../../../runtime/units.ts";
|
|
25
26
|
|
|
26
27
|
import type {
|
|
27
28
|
BubbleSeries,
|
|
@@ -513,7 +514,7 @@ function parseGradientFill(gradNode: XmlElementNode): FillSpec | undefined {
|
|
|
513
514
|
const color = parseColorRef(child);
|
|
514
515
|
if (!color) continue;
|
|
515
516
|
const pos = Number.isFinite(posPpm)
|
|
516
|
-
? Math.max(0, Math.min(1, posPpm /
|
|
517
|
+
? Math.max(0, Math.min(1, posPpm / GRADIENT_STOP_UNITS))
|
|
517
518
|
: stops.length / 2;
|
|
518
519
|
stops.push({ pos, color });
|
|
519
520
|
}
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
|
|
20
20
|
import type { ColorMod, ColorRef } from "./types.ts";
|
|
21
21
|
import type { ResolvedTheme } from "../../../model/canonical-document.ts";
|
|
22
|
+
import { GRADIENT_STOP_UNITS } from "../../../runtime/units.ts";
|
|
22
23
|
|
|
23
24
|
const FALLBACK_COLOR = "#808080";
|
|
24
|
-
const OOXML_UNIT = 100_000; // modifier val is in parts per 100,000
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Scheme-alias map. Word often emits aliases that refer to the primary
|
|
@@ -100,7 +100,7 @@ function applyMods(rgb: Rgb, mods: readonly ColorMod[] | undefined): Rgb {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function applyMod(rgb: Rgb, mod: ColorMod): Rgb {
|
|
103
|
-
const frac = mod.value /
|
|
103
|
+
const frac = mod.value / GRADIENT_STOP_UNITS;
|
|
104
104
|
switch (mod.kind) {
|
|
105
105
|
case "lumMod": {
|
|
106
106
|
// Scale luminance.
|