@beyondwork/docx-react-component 1.0.41 → 1.0.43
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/package.json +38 -37
- package/src/api/awareness-identity-types.ts +35 -0
- package/src/api/comment-negotiation-types.ts +130 -0
- package/src/api/comment-presentation-types.ts +106 -0
- package/src/api/editor-state-types.ts +110 -0
- package/src/api/external-custody-types.ts +74 -0
- package/src/api/participants-types.ts +18 -0
- package/src/api/public-types.ts +541 -5
- package/src/api/scope-metadata-resolver-types.ts +88 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +601 -9
- package/src/core/search/search-text.ts +15 -2
- package/src/index.ts +131 -1
- package/src/io/docx-session.ts +672 -2
- package/src/io/export/escape-xml-attribute.ts +26 -0
- package/src/io/export/external-send.ts +188 -0
- package/src/io/export/serialize-comments.ts +13 -16
- package/src/io/export/serialize-footnotes.ts +17 -24
- package/src/io/export/serialize-headers-footers.ts +17 -24
- package/src/io/export/serialize-main-document.ts +59 -62
- package/src/io/export/serialize-numbering.ts +20 -27
- package/src/io/export/serialize-runtime-revisions.ts +2 -9
- package/src/io/export/serialize-tables.ts +8 -15
- package/src/io/export/table-properties-xml.ts +25 -32
- package/src/io/import/external-reimport.ts +40 -0
- package/src/io/load-scheduler.ts +230 -0
- package/src/io/normalize/normalize-text.ts +83 -0
- package/src/io/ooxml/bw-xml.ts +244 -0
- package/src/io/ooxml/canonicalize-payload.ts +301 -0
- package/src/io/ooxml/comment-negotiation-payload.ts +288 -0
- package/src/io/ooxml/comment-presentation-payload.ts +311 -0
- package/src/io/ooxml/external-custody-payload.ts +102 -0
- package/src/io/ooxml/participants-payload.ts +97 -0
- package/src/io/ooxml/payload-signature.ts +112 -0
- package/src/io/ooxml/workflow-payload-validator.ts +367 -0
- package/src/io/ooxml/workflow-payload.ts +317 -7
- package/src/runtime/awareness-identity.ts +173 -0
- package/src/runtime/collab/event-types.ts +27 -0
- package/src/runtime/collab-session-bridge.ts +157 -0
- package/src/runtime/collab-session-facet.ts +193 -0
- package/src/runtime/collab-session.ts +273 -0
- package/src/runtime/comment-negotiation-sync.ts +91 -0
- package/src/runtime/comment-negotiation.ts +158 -0
- package/src/runtime/comment-presentation.ts +223 -0
- package/src/runtime/document-runtime.ts +639 -124
- package/src/runtime/editor-state-channel.ts +544 -0
- package/src/runtime/editor-state-integration.ts +217 -0
- package/src/runtime/external-send-runtime.ts +117 -0
- package/src/runtime/layout/docx-font-loader.ts +11 -30
- 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 +139 -14
- package/src/runtime/layout/page-graph.ts +79 -7
- package/src/runtime/layout/paginated-layout-engine.ts +441 -48
- package/src/runtime/layout/public-facet.ts +585 -14
- package/src/runtime/layout/table-row-split.ts +316 -0
- package/src/runtime/markdown-sanitizer.ts +132 -0
- package/src/runtime/participants.ts +134 -0
- package/src/runtime/perf-counters.ts +28 -0
- package/src/runtime/render/render-frame-types.ts +17 -0
- package/src/runtime/render/render-kernel.ts +172 -29
- package/src/runtime/resign-payload.ts +120 -0
- package/src/runtime/surface-projection.ts +10 -5
- package/src/runtime/tamper-gate.ts +157 -0
- package/src/runtime/workflow-markup.ts +80 -16
- package/src/runtime/workflow-rail-segments.ts +244 -5
- package/src/ui/WordReviewEditor.tsx +654 -45
- package/src/ui/editor-command-bag.ts +14 -0
- package/src/ui/editor-runtime-boundary.ts +111 -11
- package/src/ui/editor-shell-view.tsx +21 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-helpers.ts +10 -0
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +28 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
- package/src/ui-tailwind/chrome/collab-audience-chip.tsx +73 -0
- package/src/ui-tailwind/chrome/collab-negotiation-action-bar.tsx +244 -0
- package/src/ui-tailwind/chrome/collab-presence-strip.tsx +150 -0
- package/src/ui-tailwind/chrome/collab-role-chip.tsx +62 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-send-to-supplier-modal.tsx +149 -0
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +68 -0
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
- package/src/ui-tailwind/chrome/forward-non-drag-click.ts +104 -0
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +1 -0
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +1 -38
- package/src/ui-tailwind/chrome-overlay/index.ts +6 -0
- package/src/ui-tailwind/chrome-overlay/scope-card-role-model.ts +78 -0
- package/src/ui-tailwind/chrome-overlay/scope-keyboard-cycle.ts +49 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +106 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +527 -0
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +120 -22
- package/src/ui-tailwind/chrome-overlay/tw-scope-card.tsx +310 -32
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +93 -14
- 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-decorations.ts +35 -1
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +86 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +167 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +35 -7
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +20 -3
- 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 +10 -256
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +9 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +66 -0
- package/src/ui-tailwind/index.ts +37 -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/review/comment-markdown-renderer.tsx +155 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +29 -3
- package/src/ui-tailwind/status/tw-status-bar.tsx +52 -1
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +455 -118
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { twip } from "./twip.ts";
|
|
2
|
+
import { escapeXmlAttribute } from "./escape-xml-attribute.ts";
|
|
2
3
|
|
|
3
4
|
interface TableWidthLike {
|
|
4
5
|
value: number;
|
|
@@ -233,31 +234,31 @@ function stripKnownProperties(xml: string, stripSpec: PropertyStripSpec): string
|
|
|
233
234
|
function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
|
|
234
235
|
const children: string[] = [];
|
|
235
236
|
if (table.styleId) {
|
|
236
|
-
children.push(`<w:tblStyle w:val="${
|
|
237
|
+
children.push(`<w:tblStyle w:val="${escapeXmlAttribute(table.styleId)}"/>`);
|
|
237
238
|
}
|
|
238
239
|
if (table.width) {
|
|
239
240
|
children.push(serializeWidth("tblW", table.width));
|
|
240
241
|
}
|
|
241
242
|
if (table.alignment) {
|
|
242
|
-
children.push(`<w:jc w:val="${
|
|
243
|
+
children.push(`<w:jc w:val="${escapeXmlAttribute(table.alignment)}"/>`);
|
|
243
244
|
}
|
|
244
245
|
if (table.indent) {
|
|
245
|
-
children.push(`<w:tblInd w:w="${table.indent.value}" w:type="${
|
|
246
|
+
children.push(`<w:tblInd w:w="${table.indent.value}" w:type="${escapeXmlAttribute(table.indent.type)}"/>`);
|
|
246
247
|
}
|
|
247
248
|
if (table.layoutMode) {
|
|
248
|
-
children.push(`<w:tblLayout w:type="${
|
|
249
|
+
children.push(`<w:tblLayout w:type="${escapeXmlAttribute(table.layoutMode)}"/>`);
|
|
249
250
|
}
|
|
250
251
|
if (table.cellSpacing) {
|
|
251
|
-
children.push(`<w:tblCellSpacing w:w="${table.cellSpacing.value}" w:type="${
|
|
252
|
+
children.push(`<w:tblCellSpacing w:w="${table.cellSpacing.value}" w:type="${escapeXmlAttribute(table.cellSpacing.type)}"/>`);
|
|
252
253
|
}
|
|
253
254
|
if (table.bidiVisual !== undefined) {
|
|
254
255
|
children.push(table.bidiVisual ? `<w:bidiVisual/>` : `<w:bidiVisual w:val="0"/>`);
|
|
255
256
|
}
|
|
256
257
|
if (table.caption !== undefined) {
|
|
257
|
-
children.push(`<w:tblCaption w:val="${
|
|
258
|
+
children.push(`<w:tblCaption w:val="${escapeXmlAttribute(table.caption)}"/>`);
|
|
258
259
|
}
|
|
259
260
|
if (table.description !== undefined) {
|
|
260
|
-
children.push(`<w:tblDescription w:val="${
|
|
261
|
+
children.push(`<w:tblDescription w:val="${escapeXmlAttribute(table.description)}"/>`);
|
|
261
262
|
}
|
|
262
263
|
if (table.floating) {
|
|
263
264
|
const floatingXml = serializeTableFloating(table.floating);
|
|
@@ -286,11 +287,11 @@ function buildTablePropertiesInnerXml(table: TablePropertiesLike): string {
|
|
|
286
287
|
|
|
287
288
|
function serializeTableFloating(floating: TableFloatingPropertiesLike): string {
|
|
288
289
|
const attrs: string[] = [];
|
|
289
|
-
if (floating.horizontalAnchor) attrs.push(`w:horzAnchor="${
|
|
290
|
-
if (floating.verticalAnchor) attrs.push(`w:vertAnchor="${
|
|
291
|
-
if (floating.horizontalAlign) attrs.push(`w:tblpXSpec="${
|
|
290
|
+
if (floating.horizontalAnchor) attrs.push(`w:horzAnchor="${escapeXmlAttribute(floating.horizontalAnchor)}"`);
|
|
291
|
+
if (floating.verticalAnchor) attrs.push(`w:vertAnchor="${escapeXmlAttribute(floating.verticalAnchor)}"`);
|
|
292
|
+
if (floating.horizontalAlign) attrs.push(`w:tblpXSpec="${escapeXmlAttribute(floating.horizontalAlign)}"`);
|
|
292
293
|
if (floating.horizontalOffset !== undefined) attrs.push(`w:tblpX="${floating.horizontalOffset}"`);
|
|
293
|
-
if (floating.verticalAlign) attrs.push(`w:tblpYSpec="${
|
|
294
|
+
if (floating.verticalAlign) attrs.push(`w:tblpYSpec="${escapeXmlAttribute(floating.verticalAlign)}"`);
|
|
294
295
|
if (floating.verticalOffset !== undefined) attrs.push(`w:tblpY="${floating.verticalOffset}"`);
|
|
295
296
|
if (floating.leftFromText !== undefined) attrs.push(`w:leftFromText="${floating.leftFromText}"`);
|
|
296
297
|
if (floating.rightFromText !== undefined) attrs.push(`w:rightFromText="${floating.rightFromText}"`);
|
|
@@ -306,7 +307,7 @@ function serializeTableFloating(floating: TableFloatingPropertiesLike): string {
|
|
|
306
307
|
function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
|
|
307
308
|
const children: string[] = [];
|
|
308
309
|
if (row.cnfStyle) {
|
|
309
|
-
children.push(`<w:cnfStyle w:val="${
|
|
310
|
+
children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(row.cnfStyle)}"/>`);
|
|
310
311
|
}
|
|
311
312
|
if (row.gridBefore !== undefined) {
|
|
312
313
|
children.push(`<w:gridBefore w:val="${twip(row.gridBefore)}"/>`);
|
|
@@ -328,7 +329,7 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
|
|
|
328
329
|
children.push(row.cantSplit ? `<w:cantSplit/>` : `<w:cantSplit w:val="0"/>`);
|
|
329
330
|
}
|
|
330
331
|
if (row.height !== undefined) {
|
|
331
|
-
const hRuleAttr = row.heightRule ? ` w:hRule="${
|
|
332
|
+
const hRuleAttr = row.heightRule ? ` w:hRule="${escapeXmlAttribute(row.heightRule)}"` : "";
|
|
332
333
|
children.push(`<w:trHeight w:val="${twip(row.height)}"${hRuleAttr}/>`);
|
|
333
334
|
}
|
|
334
335
|
// ST_OnOff element (A.3):
|
|
@@ -343,7 +344,7 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
|
|
|
343
344
|
children.push(`<w:tblHeader w:val="false"/>`);
|
|
344
345
|
}
|
|
345
346
|
if (row.horizontalAlignment) {
|
|
346
|
-
children.push(`<w:jc w:val="${
|
|
347
|
+
children.push(`<w:jc w:val="${escapeXmlAttribute(row.horizontalAlignment)}"/>`);
|
|
347
348
|
}
|
|
348
349
|
return children.join("");
|
|
349
350
|
}
|
|
@@ -351,7 +352,7 @@ function buildTableRowPropertiesInnerXml(row: TableRowPropertiesLike): string {
|
|
|
351
352
|
function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string {
|
|
352
353
|
const children: string[] = [];
|
|
353
354
|
if (cell.cnfStyle) {
|
|
354
|
-
children.push(`<w:cnfStyle w:val="${
|
|
355
|
+
children.push(`<w:cnfStyle w:val="${escapeXmlAttribute(cell.cnfStyle)}"/>`);
|
|
355
356
|
}
|
|
356
357
|
if (cell.width) {
|
|
357
358
|
children.push(serializeWidth("tcW", cell.width));
|
|
@@ -391,10 +392,10 @@ function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string
|
|
|
391
392
|
children.push(cell.fitText ? `<w:tcFitText/>` : `<w:tcFitText w:val="0"/>`);
|
|
392
393
|
}
|
|
393
394
|
if (cell.textDirection) {
|
|
394
|
-
children.push(`<w:textDirection w:val="${
|
|
395
|
+
children.push(`<w:textDirection w:val="${escapeXmlAttribute(cell.textDirection)}"/>`);
|
|
395
396
|
}
|
|
396
397
|
if (cell.verticalAlign) {
|
|
397
|
-
children.push(`<w:vAlign w:val="${
|
|
398
|
+
children.push(`<w:vAlign w:val="${escapeXmlAttribute(cell.verticalAlign)}"/>`);
|
|
398
399
|
}
|
|
399
400
|
return children.join("");
|
|
400
401
|
}
|
|
@@ -402,7 +403,7 @@ function buildTableCellPropertiesInnerXml(cell: TableCellPropertiesLike): string
|
|
|
402
403
|
function serializeWidth(elementName: "tblW" | "tcW", width: TableWidthLike): string {
|
|
403
404
|
// OOXML allows w:w to be percentage (pct) or twentieths-of-a-percent too, but
|
|
404
405
|
// both are integer-typed in the schema. Always round at the authoring edge.
|
|
405
|
-
return `<w:${elementName} w:w="${twip(width.value)}" w:type="${
|
|
406
|
+
return `<w:${elementName} w:w="${twip(width.value)}" w:type="${escapeXmlAttribute(width.type)}"/>`;
|
|
406
407
|
}
|
|
407
408
|
|
|
408
409
|
function serializeBorders(borders: TableBordersLike): string {
|
|
@@ -415,10 +416,10 @@ function serializeBorders(borders: TableBordersLike): string {
|
|
|
415
416
|
|
|
416
417
|
function serializeBorderSpec(elementName: string, border: BorderSpecLike): string {
|
|
417
418
|
const attrs: string[] = [];
|
|
418
|
-
if (border.value) attrs.push(`w:val="${
|
|
419
|
+
if (border.value) attrs.push(`w:val="${escapeXmlAttribute(border.value)}"`);
|
|
419
420
|
if (border.size !== undefined) attrs.push(`w:sz="${twip(border.size)}"`);
|
|
420
421
|
if (border.space !== undefined) attrs.push(`w:space="${twip(border.space)}"`);
|
|
421
|
-
if (border.color) attrs.push(`w:color="${
|
|
422
|
+
if (border.color) attrs.push(`w:color="${escapeXmlAttribute(border.color)}"`);
|
|
422
423
|
return attrs.length > 0 ? `<w:${elementName} ${attrs.join(" ")}/>` : "";
|
|
423
424
|
}
|
|
424
425
|
|
|
@@ -434,7 +435,7 @@ function serializeTableCellMargins(margins: TableCellMarginsLike): string {
|
|
|
434
435
|
function serializeTableLook(tblLook: TableLookLike): string {
|
|
435
436
|
const attrs: string[] = [];
|
|
436
437
|
if (tblLook.val) {
|
|
437
|
-
attrs.push(`w:val="${
|
|
438
|
+
attrs.push(`w:val="${escapeXmlAttribute(tblLook.val)}"`);
|
|
438
439
|
}
|
|
439
440
|
for (const [key, attr] of [
|
|
440
441
|
["firstRow", "w:firstRow"],
|
|
@@ -457,16 +458,8 @@ function serializeTableLook(tblLook: TableLookLike): string {
|
|
|
457
458
|
|
|
458
459
|
function serializeCellShading(shading: CellShadingLike): string {
|
|
459
460
|
const attrs: string[] = [];
|
|
460
|
-
if (shading.val) attrs.push(`w:val="${
|
|
461
|
-
if (shading.color) attrs.push(`w:color="${
|
|
462
|
-
if (shading.fill) attrs.push(`w:fill="${
|
|
461
|
+
if (shading.val) attrs.push(`w:val="${escapeXmlAttribute(shading.val)}"`);
|
|
462
|
+
if (shading.color) attrs.push(`w:color="${escapeXmlAttribute(shading.color)}"`);
|
|
463
|
+
if (shading.fill) attrs.push(`w:fill="${escapeXmlAttribute(shading.fill)}"`);
|
|
463
464
|
return attrs.length > 0 ? `<w:shd ${attrs.join(" ")}/>` : "";
|
|
464
465
|
}
|
|
465
|
-
|
|
466
|
-
function escapeAttribute(value: string): string {
|
|
467
|
-
return value
|
|
468
|
-
.replace(/&/gu, "&")
|
|
469
|
-
.replace(/"/gu, """)
|
|
470
|
-
.replace(/</gu, "<")
|
|
471
|
-
.replace(/>/gu, ">");
|
|
472
|
-
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExternalCustody,
|
|
3
|
+
ExternalCustodyResolver,
|
|
4
|
+
ExternalCustodyRestoredContent,
|
|
5
|
+
} from "../../api/external-custody-types.ts";
|
|
6
|
+
|
|
7
|
+
export type ReimportResult =
|
|
8
|
+
| { outcome: "skipped" /* no custody attached */ }
|
|
9
|
+
| { outcome: "resolver_missing" /* custody present, no host resolver */ }
|
|
10
|
+
| { outcome: "tampered" /* incoming doc body hash ≠ originContentHash */ }
|
|
11
|
+
| { outcome: "deferred" /* resolver returned undefined; keep custody for retry */ }
|
|
12
|
+
| { outcome: "restored"; content: ExternalCustodyRestoredContent };
|
|
13
|
+
|
|
14
|
+
export interface MaybeRestoreArgs {
|
|
15
|
+
custody: ExternalCustody | undefined;
|
|
16
|
+
resolver: ExternalCustodyResolver | undefined;
|
|
17
|
+
/** sha256:{hex} of canonicalized word/document.xml of the incoming docx. */
|
|
18
|
+
incomingDocxHash: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Re-import pipeline. Classifies the call into a closed outcome set so
|
|
23
|
+
* the caller can decide how to merge the restored content back into
|
|
24
|
+
* the runtime state.
|
|
25
|
+
*/
|
|
26
|
+
export async function maybeRestoreFromExternalCustody(
|
|
27
|
+
args: MaybeRestoreArgs,
|
|
28
|
+
): Promise<ReimportResult> {
|
|
29
|
+
if (!args.custody) return { outcome: "skipped" };
|
|
30
|
+
if (!args.resolver) return { outcome: "resolver_missing" };
|
|
31
|
+
if (args.custody.originContentHash !== args.incomingDocxHash) {
|
|
32
|
+
return { outcome: "tampered" };
|
|
33
|
+
}
|
|
34
|
+
const restored = await args.resolver.restore({
|
|
35
|
+
custodyId: args.custody.custodyId,
|
|
36
|
+
originContentHash: args.custody.originContentHash,
|
|
37
|
+
});
|
|
38
|
+
if (!restored) return { outcome: "deferred" };
|
|
39
|
+
return { outcome: "restored", content: restored };
|
|
40
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -36,6 +36,11 @@ import type {
|
|
|
36
36
|
ParsedTableRowNode,
|
|
37
37
|
} from "../ooxml/parse-main-document.ts";
|
|
38
38
|
import { classifyFieldInstruction, buildFieldRegistry } from "../ooxml/parse-fields.ts";
|
|
39
|
+
import {
|
|
40
|
+
type LoadScheduler,
|
|
41
|
+
nowMs,
|
|
42
|
+
shouldYield,
|
|
43
|
+
} from "../load-scheduler.ts";
|
|
39
44
|
|
|
40
45
|
export interface NormalizedTextDocument {
|
|
41
46
|
content: DocumentRootNode;
|
|
@@ -115,6 +120,84 @@ export function normalizeParsedTextDocument(
|
|
|
115
120
|
};
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Fastload P6: async sibling of `normalizeParsedTextDocument` that yields to
|
|
125
|
+
* the browser every {@link NORMALIZE_YIELD_STRIDE} top-level blocks when
|
|
126
|
+
* {@link shouldYield} fires against the scheduler's frame deadline. Shares
|
|
127
|
+
* the private normalizeBlocks / normalizeParagraph / normalizeInlineChildren
|
|
128
|
+
* helpers with the sync export — only the outermost block walk is duplicated.
|
|
129
|
+
*
|
|
130
|
+
* Byte-equivalent to the sync export on any corpus (fixture parity is asserted
|
|
131
|
+
* in `test/io/normalize-text-async.test.ts` across every F*.docx fixture).
|
|
132
|
+
*/
|
|
133
|
+
const NORMALIZE_YIELD_STRIDE = 256;
|
|
134
|
+
|
|
135
|
+
export async function normalizeParsedTextDocumentAsync(
|
|
136
|
+
document: ParsedMainDocument,
|
|
137
|
+
packagePartName = "/word/document.xml",
|
|
138
|
+
scheduler: LoadScheduler,
|
|
139
|
+
options?: { styles?: import("../../model/canonical-document.ts").StylesCatalog },
|
|
140
|
+
): Promise<NormalizedTextDocument> {
|
|
141
|
+
const state: NormalizationState = {
|
|
142
|
+
nextFragmentIndex: 1,
|
|
143
|
+
nextWarningIndex: 1,
|
|
144
|
+
nextDiagnosticIndex: 1,
|
|
145
|
+
cursor: 0,
|
|
146
|
+
media: {
|
|
147
|
+
items: {},
|
|
148
|
+
},
|
|
149
|
+
preservation: {
|
|
150
|
+
opaqueFragments: {},
|
|
151
|
+
packageParts: {},
|
|
152
|
+
},
|
|
153
|
+
diagnostics: {
|
|
154
|
+
warnings: [],
|
|
155
|
+
errors: [],
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const children: BlockNode[] = [];
|
|
160
|
+
let previousParagraph = false;
|
|
161
|
+
let lastYieldAt = nowMs();
|
|
162
|
+
|
|
163
|
+
for (let i = 0; i < document.blocks.length; i += 1) {
|
|
164
|
+
const block = document.blocks[i];
|
|
165
|
+
const normalizedBlocks = normalizeBlocks(block, state, packagePartName);
|
|
166
|
+
for (const normalizedBlock of normalizedBlocks) {
|
|
167
|
+
if (previousParagraph && normalizedBlock.type === "paragraph") {
|
|
168
|
+
state.cursor += 1;
|
|
169
|
+
}
|
|
170
|
+
children.push(normalizedBlock);
|
|
171
|
+
previousParagraph = normalizedBlock.type === "paragraph";
|
|
172
|
+
}
|
|
173
|
+
if (
|
|
174
|
+
i > 0 &&
|
|
175
|
+
i % NORMALIZE_YIELD_STRIDE === 0 &&
|
|
176
|
+
shouldYield(scheduler, lastYieldAt)
|
|
177
|
+
) {
|
|
178
|
+
await scheduler.yield();
|
|
179
|
+
lastYieldAt = nowMs();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const content: DocumentRootNode = { type: "doc", children };
|
|
184
|
+
|
|
185
|
+
const styles = options?.styles ?? { paragraphs: {}, characters: {}, tables: {} };
|
|
186
|
+
const fieldRegistry = buildFieldRegistry({ content, styles });
|
|
187
|
+
const hasFields = fieldRegistry.supported.length > 0 || fieldRegistry.preserveOnly.length > 0;
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
content,
|
|
191
|
+
media: state.media,
|
|
192
|
+
preservation: state.preservation,
|
|
193
|
+
diagnostics: state.diagnostics,
|
|
194
|
+
...(document.finalSectionProperties !== undefined
|
|
195
|
+
? { finalSectionProperties: document.finalSectionProperties }
|
|
196
|
+
: {}),
|
|
197
|
+
...(hasFields ? { fieldRegistry } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
118
201
|
function normalizeBlocks(
|
|
119
202
|
block: ParsedBlockNode,
|
|
120
203
|
state: NormalizationState,
|