@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.
Files changed (82) hide show
  1. package/README.md +17 -0
  2. package/package.json +5 -4
  3. package/src/api/editor-state-types.ts +110 -0
  4. package/src/api/public-types.ts +333 -4
  5. package/src/core/commands/formatting-commands.ts +7 -1
  6. package/src/core/commands/index.ts +60 -10
  7. package/src/core/commands/text-commands.ts +59 -0
  8. package/src/core/search/search-text.ts +15 -2
  9. package/src/core/selection/review-anchors.ts +131 -21
  10. package/src/index.ts +29 -1
  11. package/src/io/chart-preview-resolver.ts +281 -0
  12. package/src/io/docx-session.ts +692 -2
  13. package/src/io/export/build-app-properties-xml.ts +1 -1
  14. package/src/io/export/serialize-comments.ts +38 -9
  15. package/src/io/export/twip.ts +1 -1
  16. package/src/io/load-scheduler.ts +230 -0
  17. package/src/io/normalize/normalize-text.ts +116 -0
  18. package/src/io/ooxml/parse-comments.ts +0 -33
  19. package/src/io/ooxml/parse-complex-content.ts +14 -0
  20. package/src/io/ooxml/parse-main-document.ts +4 -0
  21. package/src/io/ooxml/workflow-payload-validator.ts +97 -1
  22. package/src/io/ooxml/workflow-payload.ts +172 -1
  23. package/src/preservation/opaque-region.ts +5 -0
  24. package/src/review/store/comment-remapping.ts +2 -2
  25. package/src/runtime/collab-session.ts +1 -1
  26. package/src/runtime/document-runtime.ts +661 -42
  27. package/src/runtime/edit-dispatch/dispatch-text-command.ts +98 -0
  28. package/src/runtime/edit-dispatch/index.ts +2 -0
  29. package/src/runtime/edit-dispatch/list-aware-dispatch.ts +125 -0
  30. package/src/runtime/editor-state-channel.ts +544 -0
  31. package/src/runtime/editor-state-integration.ts +217 -0
  32. package/src/runtime/editor-surface/capabilities.ts +411 -0
  33. package/src/runtime/layout/index.ts +2 -0
  34. package/src/runtime/layout/inert-layout-facet.ts +4 -0
  35. package/src/runtime/layout/layout-engine-instance.ts +63 -2
  36. package/src/runtime/layout/layout-engine-version.ts +41 -0
  37. package/src/runtime/layout/paginated-layout-engine.ts +211 -14
  38. package/src/runtime/layout/public-facet.ts +430 -1
  39. package/src/runtime/perf-counters.ts +28 -0
  40. package/src/runtime/prerender/cache-envelope.ts +29 -0
  41. package/src/runtime/prerender/cache-key.ts +66 -0
  42. package/src/runtime/prerender/font-fingerprint.ts +17 -0
  43. package/src/runtime/prerender/graph-canonicalize.ts +121 -0
  44. package/src/runtime/prerender/indexeddb-cache.ts +184 -0
  45. package/src/runtime/prerender/prerender-document.ts +145 -0
  46. package/src/runtime/render/block-fragment-projection.ts +2 -0
  47. package/src/runtime/render/render-frame-types.ts +17 -0
  48. package/src/runtime/render/render-kernel.ts +172 -29
  49. package/src/runtime/selection/post-edit-validator.ts +77 -0
  50. package/src/runtime/surface-projection.ts +45 -7
  51. package/src/runtime/workflow-markup.ts +71 -16
  52. package/src/ui/WordReviewEditor.tsx +142 -237
  53. package/src/ui/editor-command-bag.ts +14 -0
  54. package/src/ui/editor-runtime-boundary.ts +115 -12
  55. package/src/ui/editor-shell-view.tsx +10 -0
  56. package/src/ui/editor-surface-controller.tsx +5 -0
  57. package/src/ui/headless/selection-helpers.ts +10 -0
  58. package/src/ui/runtime-shortcut-dispatch.ts +28 -68
  59. package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +62 -2
  60. package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +281 -0
  61. package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +48 -0
  62. package/src/ui-tailwind/editor-surface/paste-plain-text.ts +72 -0
  63. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +118 -8
  64. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +76 -165
  65. package/src/ui-tailwind/editor-surface/pm-schema.ts +170 -4
  66. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +58 -7
  67. package/src/ui-tailwind/editor-surface/surface-build-keys.ts +2 -0
  68. package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +265 -0
  69. package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +8 -255
  70. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +47 -0
  71. package/src/ui-tailwind/index.ts +5 -1
  72. package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +57 -0
  73. package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +71 -0
  74. package/src/ui-tailwind/page-stack/tw-page-footer-band.tsx +73 -0
  75. package/src/ui-tailwind/page-stack/tw-page-header-band.tsx +74 -0
  76. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +477 -0
  77. package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +374 -0
  78. package/src/ui-tailwind/page-stack/use-visible-block-range.ts +157 -0
  79. package/src/ui-tailwind/review/comment-markdown-renderer.tsx +155 -0
  80. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +77 -16
  81. package/src/ui-tailwind/theme/editor-theme.css +47 -14
  82. package/src/ui-tailwind/tw-review-workspace.tsx +303 -123
@@ -1,6 +1,7 @@
1
1
  import type {
2
2
  CompatibilityReport as PublicCompatibilityReport,
3
3
  EditorError,
4
+ EditorHostAdapter,
4
5
  EditorSessionState,
5
6
  EditorWarning as PublicEditorWarning,
6
7
  EditorAnchorProjection as PublicEditorAnchorProjection,
@@ -39,7 +40,12 @@ import {
39
40
  type ParsedInlineNode,
40
41
  type ParsedPermStartInlineNode,
41
42
  } from "./ooxml/parse-main-document.ts";
42
- import { normalizeParsedTextDocument } from "./normalize/normalize-text.ts";
43
+ import {
44
+ normalizeParsedTextDocument,
45
+ normalizeParsedTextDocumentAsync,
46
+ } from "./normalize/normalize-text.ts";
47
+ import { resolveChartPreviewsForDocument } from "./chart-preview-resolver.ts";
48
+ import { type LoadScheduler } from "./load-scheduler.ts";
43
49
  import {
44
50
  CONTENT_TYPES_PATH,
45
51
  PACKAGE_RELATIONSHIPS_PATH,
@@ -213,6 +219,26 @@ interface LoadDocxEditorSessionOptions {
213
219
  sourceLabel?: string;
214
220
  bytes: Uint8Array | ArrayBuffer;
215
221
  editorBuild?: string;
222
+ /**
223
+ * Fastload P2: optional instrumentation callback invoked once per
224
+ * completed load stage. Stage ordering: opc → body →
225
+ * styles-numbering-comments → skeleton-ready. `durationMs` is
226
+ * measured against `performance.now()` boundaries inside the loader.
227
+ *
228
+ * Pure instrumentation — has no effect on returned session shape.
229
+ * Safe to leave undefined; existing callers observe no behavior
230
+ * change.
231
+ */
232
+ onLoadStage?: (stage: import("../api/public-types.ts").LoadStage, durationMs: number) => void;
233
+ /**
234
+ * Stage 0B.1: host-adapter surface. The sync loader accepts the option for
235
+ * API symmetry with {@link LoadDocxEditorSessionAsyncOptions} but does not
236
+ * invoke `renderChartPreview` — chart-preview synthesis is asynchronous, so
237
+ * hosts that want preview bitmaps must call {@link loadDocxEditorSessionAsync}.
238
+ * Other adapter methods (load/saveSession/logEvent) are not consumed by the
239
+ * loader itself and are carried through unchanged.
240
+ */
241
+ hostAdapter?: EditorHostAdapter;
216
242
  }
217
243
 
218
244
  export interface LoadedDocxEditorSession {
@@ -225,6 +251,8 @@ export interface LoadedDocxEditorSession {
225
251
  sessionState: EditorSessionState | PersistedEditorSnapshot,
226
252
  options?: ExportDocxOptions,
227
253
  ) => Promise<ExportResult>;
254
+ /** Schema 1.2 — editorState block parsed from the 1.2 payload, if present. */
255
+ initialEditorStatePayload?: import("./ooxml/workflow-payload.ts").EditorStatePayload;
228
256
  }
229
257
 
230
258
  interface ImportedDocxState {
@@ -277,6 +305,37 @@ const BLOCKING_COMMENT_DIAGNOSTIC_CODES = new Set<CommentImportDiagnostic["code"
277
305
  "preserve_only_revision_overlap",
278
306
  ]);
279
307
 
308
+ interface StageEmitter {
309
+ emit(stage: import("../api/public-types.ts").LoadStage): void;
310
+ }
311
+
312
+ /**
313
+ * Fastload P2 (hoisted in P6): build a stage-event emitter. When no callback
314
+ * is supplied the emitter is a zero-cost no-op. Shared between the sync and
315
+ * async load orchestrators so both paths emit `load-stage` events at the
316
+ * same four boundaries: `opc` → `body` → `styles-numbering-comments` →
317
+ * `skeleton-ready`.
318
+ */
319
+ function createStageEmitter(
320
+ onStage: LoadDocxEditorSessionOptions["onLoadStage"],
321
+ ): StageEmitter {
322
+ if (!onStage) {
323
+ return {
324
+ emit() {
325
+ /* no-op */
326
+ },
327
+ };
328
+ }
329
+ let cursor = loadStageNow();
330
+ return {
331
+ emit(stage) {
332
+ const nextMark = loadStageNow();
333
+ onStage(stage, nextMark - cursor);
334
+ cursor = nextMark;
335
+ },
336
+ };
337
+ }
338
+
280
339
  export function loadDocxEditorSession(
281
340
  options: LoadDocxEditorSessionOptions,
282
341
  ): LoadedDocxEditorSession {
@@ -285,6 +344,9 @@ export function loadDocxEditorSession(
285
344
  ? options.editorBuild
286
345
  : "dev";
287
346
  const sourceBytes = toUint8Array(options.bytes);
347
+
348
+ const stages = createStageEmitter(options.onLoadStage);
349
+
288
350
  let sourcePackage: OpcPackage;
289
351
 
290
352
  try {
@@ -297,6 +359,7 @@ export function loadDocxEditorSession(
297
359
  }),
298
360
  );
299
361
  }
362
+ stages.emit("opc");
300
363
  const embeddedWorkflowPayload = parseWorkflowPayloadEnvelopeFromPackage(sourcePackage);
301
364
  const embeddedWorkflowMetadata = embeddedWorkflowPayload?.workflowMetadata;
302
365
  const embeddedWorkflowOverlay = embeddedWorkflowPayload?.workflowOverlay;
@@ -376,6 +439,7 @@ export function loadDocxEditorSession(
376
439
  parsedDocument,
377
440
  mainDocumentPath,
378
441
  );
442
+ stages.emit("body");
379
443
  const commentsPartPath = resolveCommentsPartPath(
380
444
  sourcePackage,
381
445
  mainDocumentPath,
@@ -438,6 +502,7 @@ export function loadDocxEditorSession(
438
502
  normalizedDocument.preservation.opaqueFragments,
439
503
  normalizedRevisions.revisions,
440
504
  );
505
+ stages.emit("styles-numbering-comments");
441
506
  const importedStoryRevisions: ReviewRevisionRecord[] = [];
442
507
  const importedStoryRevisionDiagnostics: ParsedRevisionsResult["diagnostics"] = [];
443
508
  const subPartOpaqueState = createSubPartOpaqueImportState(
@@ -813,6 +878,7 @@ export function loadDocxEditorSession(
813
878
  },
814
879
  };
815
880
 
881
+ stages.emit("skeleton-ready");
816
882
  return {
817
883
  initialSessionState,
818
884
  initialSnapshot: snapshot,
@@ -820,6 +886,9 @@ export function loadDocxEditorSession(
820
886
  protectionSnapshot: importedProtectionSnapshot,
821
887
  exportDocx: async (nextSessionState, exportOptions) =>
822
888
  exportDocxEditorSession(importedState, nextSessionState, exportOptions),
889
+ ...(embeddedWorkflowPayload?.editorState
890
+ ? { initialEditorStatePayload: embeddedWorkflowPayload.editorState }
891
+ : {}),
823
892
  };
824
893
  } catch (error) {
825
894
  return createDiagnosticsSession(
@@ -829,6 +898,623 @@ export function loadDocxEditorSession(
829
898
  }
830
899
  }
831
900
 
901
+ interface LoadDocxEditorSessionAsyncOptions extends LoadDocxEditorSessionOptions {
902
+ /**
903
+ * Scheduler that the async loader awaits between parse stages. Callers
904
+ * in DOM environments should construct this with `createLoadScheduler()`
905
+ * (auto-detects scheduler.yield → MessageChannel → setTimeout backend).
906
+ * Callers in Node / SSR should pass
907
+ * `createLoadScheduler({ backendOverride: "sync" })` — sync backend
908
+ * yields resolve without task-boundary latency, so the async path
909
+ * behaves like the sync path from the test harness POV.
910
+ */
911
+ scheduler: LoadScheduler;
912
+ }
913
+
914
+ /**
915
+ * Fastload P6: async sibling of {@link loadDocxEditorSession} that yields to
916
+ * the browser between parse stages. Parse sequence is byte-equivalent to
917
+ * the sync path (asserted on every F*.docx fixture in
918
+ * `test/io/fastload-parity.test.ts`). Yields fire at:
919
+ * 1. after OPC read,
920
+ * 2. after `parseMainDocumentXml`,
921
+ * 3. between every header/footer sub-part parse,
922
+ * 4. after footnotes/endnotes parse,
923
+ * 5. after theme + settings + styles parse,
924
+ * 6. after `buildCompatibilityReport`,
925
+ * plus mid-walk yields every 256 blocks inside
926
+ * `normalizeParsedTextDocumentAsync` (stage 3 — body normalize).
927
+ *
928
+ * Sync `loadDocxEditorSession` remains the only entry point for Node tests
929
+ * and SSR. The DOM boundary in `editor-runtime-boundary.ts` calls this
930
+ * async path so the browser can paint the skeleton mid-parse.
931
+ */
932
+ export async function loadDocxEditorSessionAsync(
933
+ options: LoadDocxEditorSessionAsyncOptions,
934
+ ): Promise<LoadedDocxEditorSession> {
935
+ const { scheduler } = options;
936
+ const editorBuild =
937
+ typeof options.editorBuild === "string" && options.editorBuild.length > 0
938
+ ? options.editorBuild
939
+ : "dev";
940
+ const sourceBytes = toUint8Array(options.bytes);
941
+
942
+ const stages = createStageEmitter(options.onLoadStage);
943
+
944
+ let sourcePackage: OpcPackage;
945
+
946
+ try {
947
+ sourcePackage = readOpcPackage(sourceBytes);
948
+ } catch (error) {
949
+ return createDiagnosticsSession(
950
+ options,
951
+ createPackageImportDiagnostics({
952
+ issue: classifyCorruptPackageError(error),
953
+ }),
954
+ );
955
+ }
956
+ stages.emit("opc");
957
+ await scheduler.yield();
958
+ const embeddedWorkflowPayload = parseWorkflowPayloadEnvelopeFromPackage(sourcePackage);
959
+ const embeddedWorkflowMetadata = embeddedWorkflowPayload?.workflowMetadata;
960
+ const embeddedWorkflowOverlay = embeddedWorkflowPayload?.workflowOverlay;
961
+
962
+ const mainDocumentPath = resolveMainDocumentPartPath(sourcePackage);
963
+ const brokenRelationshipIssues = collectBrokenInternalRelationshipIssues(
964
+ sourcePackage,
965
+ mainDocumentPath,
966
+ );
967
+ if (brokenRelationshipIssues.length > 0) {
968
+ return createDiagnosticsSession(
969
+ options,
970
+ createPackageImportDiagnostics({
971
+ issue: {
972
+ ...brokenRelationshipIssues[0],
973
+ message: summarizeBrokenRelationshipIssues(brokenRelationshipIssues),
974
+ details: {
975
+ issueCount: brokenRelationshipIssues.length,
976
+ targets: brokenRelationshipIssues.map((issue) => issue.targetPartPath).filter(Boolean),
977
+ },
978
+ },
979
+ }),
980
+ );
981
+ }
982
+
983
+ if (!mainDocumentPath) {
984
+ return createDiagnosticsSession(
985
+ options,
986
+ createPackageImportDiagnostics({
987
+ issue: createMissingPartIssue(MAIN_DOCUMENT_PATH),
988
+ }),
989
+ );
990
+ }
991
+
992
+ const documentPart = sourcePackage.parts.get(mainDocumentPath);
993
+ if (!documentPart) {
994
+ return createDiagnosticsSession(
995
+ options,
996
+ createPackageImportDiagnostics({
997
+ issue: createMissingPartIssue(mainDocumentPath),
998
+ }),
999
+ );
1000
+ }
1001
+ if (documentPart.contentType !== MAIN_DOCUMENT_CONTENT_TYPE) {
1002
+ return createDiagnosticsSession(
1003
+ options,
1004
+ createValidationImportDiagnostics({
1005
+ message: `DOCX main document part ${mainDocumentPath} must use content type ${MAIN_DOCUMENT_CONTENT_TYPE}.`,
1006
+ }),
1007
+ );
1008
+ }
1009
+
1010
+ try {
1011
+ const sourceDocumentXml = decodeUtf8(documentPart.bytes);
1012
+ const importedRevisions = parseRevisionsFromDocumentXml(sourceDocumentXml);
1013
+ const numberingPartPath = resolveDocumentRelatedPartPath(
1014
+ sourcePackage,
1015
+ mainDocumentPath,
1016
+ documentPart.relationships,
1017
+ NUMBERING_RELATIONSHIP_TYPE,
1018
+ NUMBERING_PART_PATH,
1019
+ );
1020
+ const parsedNumbering = numberingPartPath
1021
+ ? parseNumberingXml(
1022
+ decodeUtf8(sourcePackage.parts.get(numberingPartPath)?.bytes ?? new Uint8Array()),
1023
+ )
1024
+ : createEmptyNumberingCatalog();
1025
+ const mediaParts = collectInlineMediaParts(sourcePackage);
1026
+ const parsedDocument = parseMainDocumentXml(
1027
+ sourceDocumentXml,
1028
+ documentPart.relationships,
1029
+ mediaParts,
1030
+ mainDocumentPath,
1031
+ );
1032
+ await scheduler.yield();
1033
+ const protectionRanges = extractProtectionRanges(parsedDocument.blocks);
1034
+ const normalizedDocument = await normalizeParsedTextDocumentAsync(
1035
+ parsedDocument,
1036
+ mainDocumentPath,
1037
+ scheduler,
1038
+ );
1039
+ stages.emit("body");
1040
+ await scheduler.yield();
1041
+ const commentsPartPath = resolveCommentsPartPath(
1042
+ sourcePackage,
1043
+ mainDocumentPath,
1044
+ documentPart.relationships,
1045
+ );
1046
+ const commentsExtendedPartPath = resolveDocumentRelatedPartPath(
1047
+ sourcePackage,
1048
+ mainDocumentPath,
1049
+ documentPart.relationships,
1050
+ COMMENTS_EXTENDED_RELATIONSHIP_TYPE,
1051
+ COMMENTS_EXTENDED_PART_PATH,
1052
+ );
1053
+ const commentsIdsPartPath = resolveDocumentRelatedPartPath(
1054
+ sourcePackage,
1055
+ mainDocumentPath,
1056
+ documentPart.relationships,
1057
+ COMMENTS_IDS_RELATIONSHIP_TYPE,
1058
+ COMMENTS_IDS_PART_PATH,
1059
+ );
1060
+ const peoplePartPath = resolveDocumentRelatedPartPath(
1061
+ sourcePackage,
1062
+ mainDocumentPath,
1063
+ documentPart.relationships,
1064
+ PEOPLE_RELATIONSHIP_TYPE,
1065
+ PEOPLE_PART_PATH,
1066
+ );
1067
+ const parsedComments = commentsPartPath
1068
+ ? parseCommentsFromOoxml(
1069
+ sourceDocumentXml,
1070
+ {
1071
+ commentsXml: decodeUtf8(sourcePackage.parts.get(commentsPartPath)?.bytes ?? new Uint8Array()),
1072
+ commentsExtendedXml: decodeUtf8(
1073
+ sourcePackage.parts.get(commentsExtendedPartPath ?? "")?.bytes ?? new Uint8Array(),
1074
+ ),
1075
+ commentsIdsXml: decodeUtf8(
1076
+ sourcePackage.parts.get(commentsIdsPartPath ?? "")?.bytes ?? new Uint8Array(),
1077
+ ),
1078
+ peopleXml: decodeUtf8(
1079
+ sourcePackage.parts.get(peoplePartPath ?? "")?.bytes ?? new Uint8Array(),
1080
+ ),
1081
+ },
1082
+ )
1083
+ : {
1084
+ threads: [] as CommentThread[],
1085
+ diagnostics: [] as CommentImportDiagnostic[],
1086
+ definitions: [] as ImportedCommentDefinition[],
1087
+ sourceRootTag: undefined,
1088
+ sourceExtendedRootTag: undefined,
1089
+ sourceIdsRootTag: undefined,
1090
+ sourcePeopleRootTag: undefined,
1091
+ peopleAuthors: [] as string[],
1092
+ };
1093
+ const normalizedRevisions = normalizeImportedRevisionRecords(
1094
+ importedRevisions,
1095
+ normalizedDocument.content,
1096
+ normalizedDocument.preservation.opaqueFragments,
1097
+ );
1098
+ const normalizedComments = normalizeImportedCommentThreads(
1099
+ parsedComments,
1100
+ normalizedDocument.preservation.opaqueFragments,
1101
+ normalizedRevisions.revisions,
1102
+ );
1103
+ stages.emit("styles-numbering-comments");
1104
+ const importedStoryRevisions: ReviewRevisionRecord[] = [];
1105
+ const importedStoryRevisionDiagnostics: ParsedRevisionsResult["diagnostics"] = [];
1106
+ const subPartOpaqueState = createSubPartOpaqueImportState(
1107
+ normalizedDocument.preservation.opaqueFragments,
1108
+ normalizedDocument.diagnostics.warnings,
1109
+ );
1110
+ // ---- Parse sub-parts: headers, footers, footnotes, endnotes, theme ----
1111
+ const headerFooterRefs = parseHeaderFooterReferences(sourceDocumentXml);
1112
+ const parsedHeaders: HeaderDocument[] = [];
1113
+ const parsedFooters: FooterDocument[] = [];
1114
+ const sourceHeaderPaths: Array<{ partPath: string; relationshipId: string }> = [];
1115
+ const sourceFooterPaths: Array<{ partPath: string; relationshipId: string }> = [];
1116
+ const seenSubPartKeys = new Set<string>();
1117
+
1118
+ for (const ref of headerFooterRefs) {
1119
+ const dedupeKey = `${ref.kind}:${ref.variant}:${ref.relationshipId}`;
1120
+ if (seenSubPartKeys.has(dedupeKey)) {
1121
+ continue;
1122
+ }
1123
+ seenSubPartKeys.add(dedupeKey);
1124
+
1125
+ const relationship = documentPart.relationships.find(
1126
+ (r) => r.id === ref.relationshipId && r.targetMode === "internal",
1127
+ );
1128
+ if (!relationship) {
1129
+ continue;
1130
+ }
1131
+
1132
+ const partPath = resolveRelationshipTarget(mainDocumentPath, relationship);
1133
+ const partBytes = sourcePackage.parts.get(partPath)?.bytes;
1134
+ if (!partBytes) {
1135
+ continue;
1136
+ }
1137
+
1138
+ await scheduler.yield();
1139
+ const xml = decodeUtf8(partBytes);
1140
+ if (ref.kind === "header") {
1141
+ const parsedHeaderRevisions = parseRevisionsFromStoryXml(xml);
1142
+ const parsed = parseHeaderXml(xml);
1143
+ parsedHeaders.push({
1144
+ variant: ref.variant,
1145
+ partPath,
1146
+ relationshipId: ref.relationshipId,
1147
+ ...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
1148
+ blocks: normalizeSubPartOpaqueBlocks(
1149
+ parsed.blocks,
1150
+ normalizedDocument.preservation.opaqueFragments,
1151
+ normalizedDocument.diagnostics.warnings,
1152
+ partPath,
1153
+ subPartOpaqueState,
1154
+ ),
1155
+ });
1156
+ importedStoryRevisions.push(
1157
+ ...parsedHeaderRevisions.revisions.map((revision): ReviewRevisionRecord => ({
1158
+ ...revision,
1159
+ metadata: {
1160
+ ...revision.metadata,
1161
+ storyTarget: {
1162
+ kind: "header" as const,
1163
+ relationshipId: ref.relationshipId,
1164
+ variant: ref.variant,
1165
+ ...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
1166
+ },
1167
+ },
1168
+ })),
1169
+ );
1170
+ importedStoryRevisionDiagnostics.push(...parsedHeaderRevisions.diagnostics);
1171
+ sourceHeaderPaths.push({ partPath, relationshipId: ref.relationshipId });
1172
+ } else {
1173
+ const parsedFooterRevisions = parseRevisionsFromStoryXml(xml);
1174
+ const parsed = parseFooterXml(xml);
1175
+ parsedFooters.push({
1176
+ variant: ref.variant,
1177
+ partPath,
1178
+ relationshipId: ref.relationshipId,
1179
+ ...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
1180
+ blocks: normalizeSubPartOpaqueBlocks(
1181
+ parsed.blocks,
1182
+ normalizedDocument.preservation.opaqueFragments,
1183
+ normalizedDocument.diagnostics.warnings,
1184
+ partPath,
1185
+ subPartOpaqueState,
1186
+ ),
1187
+ });
1188
+ importedStoryRevisions.push(
1189
+ ...parsedFooterRevisions.revisions.map((revision): ReviewRevisionRecord => ({
1190
+ ...revision,
1191
+ metadata: {
1192
+ ...revision.metadata,
1193
+ storyTarget: {
1194
+ kind: "footer" as const,
1195
+ relationshipId: ref.relationshipId,
1196
+ variant: ref.variant,
1197
+ ...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
1198
+ },
1199
+ },
1200
+ })),
1201
+ );
1202
+ importedStoryRevisionDiagnostics.push(...parsedFooterRevisions.diagnostics);
1203
+ sourceFooterPaths.push({ partPath, relationshipId: ref.relationshipId });
1204
+ }
1205
+ }
1206
+
1207
+ const footnotesPartPath = resolveDocumentRelatedPartPath(
1208
+ sourcePackage,
1209
+ mainDocumentPath,
1210
+ documentPart.relationships,
1211
+ FOOTNOTES_RELATIONSHIP_TYPE,
1212
+ FOOTNOTES_PART_PATH,
1213
+ );
1214
+ const footnotesRelationshipId = documentPart.relationships.find(
1215
+ (r) => r.type === FOOTNOTES_RELATIONSHIP_TYPE && r.targetMode === "internal",
1216
+ )?.id;
1217
+ const endnotesPartPath = resolveDocumentRelatedPartPath(
1218
+ sourcePackage,
1219
+ mainDocumentPath,
1220
+ documentPart.relationships,
1221
+ ENDNOTES_RELATIONSHIP_TYPE,
1222
+ ENDNOTES_PART_PATH,
1223
+ );
1224
+ const endnotesRelationshipId = documentPart.relationships.find(
1225
+ (r) => r.type === ENDNOTES_RELATIONSHIP_TYPE && r.targetMode === "internal",
1226
+ )?.id;
1227
+
1228
+ let footnoteCollection: FootnoteCollection | undefined;
1229
+ if (footnotesPartPath) {
1230
+ footnoteCollection = parseFootnotesXml(
1231
+ decodeUtf8(sourcePackage.parts.get(footnotesPartPath)?.bytes ?? new Uint8Array()),
1232
+ );
1233
+ normalizeFootnoteCollectionOpaqueBlocks(
1234
+ footnoteCollection,
1235
+ "footnote",
1236
+ normalizedDocument.preservation.opaqueFragments,
1237
+ normalizedDocument.diagnostics.warnings,
1238
+ footnotesPartPath,
1239
+ subPartOpaqueState,
1240
+ );
1241
+ }
1242
+ if (endnotesPartPath) {
1243
+ footnoteCollection = parseEndnotesXml(
1244
+ decodeUtf8(sourcePackage.parts.get(endnotesPartPath)?.bytes ?? new Uint8Array()),
1245
+ footnoteCollection,
1246
+ );
1247
+ normalizeFootnoteCollectionOpaqueBlocks(
1248
+ footnoteCollection,
1249
+ "endnote",
1250
+ normalizedDocument.preservation.opaqueFragments,
1251
+ normalizedDocument.diagnostics.warnings,
1252
+ endnotesPartPath,
1253
+ subPartOpaqueState,
1254
+ );
1255
+ }
1256
+ await scheduler.yield();
1257
+
1258
+ const themeRelationship = documentPart.relationships.find(
1259
+ (r) => r.type === "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" &&
1260
+ r.targetMode === "internal",
1261
+ );
1262
+ const themePartPath = themeRelationship
1263
+ ? resolveRelationshipTarget(mainDocumentPath, themeRelationship)
1264
+ : undefined;
1265
+ const parsedTheme =
1266
+ themePartPath && sourcePackage.parts.has(themePartPath)
1267
+ ? parseThemeXml(
1268
+ decodeUtf8(sourcePackage.parts.get(themePartPath)?.bytes ?? new Uint8Array()),
1269
+ )
1270
+ : undefined;
1271
+ const resolvedTheme = parsedTheme ? resolveTheme(parsedTheme) : undefined;
1272
+ const settingsPartPath = resolveDocumentRelatedPartPath(
1273
+ sourcePackage,
1274
+ mainDocumentPath,
1275
+ documentPart.relationships,
1276
+ SETTINGS_RELATIONSHIP_TYPE,
1277
+ SETTINGS_PART_PATH,
1278
+ );
1279
+ const parsedSettings =
1280
+ settingsPartPath && sourcePackage.parts.has(settingsPartPath)
1281
+ ? parseSettingsXml(
1282
+ decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array()),
1283
+ )
1284
+ : undefined;
1285
+ const settingsXmlForProtection =
1286
+ settingsPartPath && sourcePackage.parts.has(settingsPartPath)
1287
+ ? decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array())
1288
+ : "";
1289
+ const documentProtection = extractDocumentProtection(settingsXmlForProtection);
1290
+ const importedProtectionSnapshot = buildProtectionSnapshot(documentProtection, protectionRanges);
1291
+
1292
+ // ---- Parse styles.xml for canonical style catalog ----
1293
+ const stylesPartPath = resolveDocumentRelatedPartPath(
1294
+ sourcePackage,
1295
+ mainDocumentPath,
1296
+ documentPart.relationships,
1297
+ STYLES_RELATIONSHIP_TYPE,
1298
+ STYLES_PART_PATH,
1299
+ );
1300
+ const parsedStyles =
1301
+ stylesPartPath && sourcePackage.parts.has(stylesPartPath)
1302
+ ? parseStylesXml(
1303
+ decodeUtf8(sourcePackage.parts.get(stylesPartPath)?.bytes ?? new Uint8Array()),
1304
+ )
1305
+ : parseStylesXml("");
1306
+ await scheduler.yield();
1307
+
1308
+ const subParts: SubPartsCatalog | undefined =
1309
+ parsedHeaders.length > 0 ||
1310
+ parsedFooters.length > 0 ||
1311
+ footnoteCollection !== undefined ||
1312
+ parsedTheme !== undefined ||
1313
+ normalizedDocument.finalSectionProperties !== undefined ||
1314
+ resolvedTheme !== undefined ||
1315
+ parsedSettings !== undefined
1316
+ ? {
1317
+ headers: parsedHeaders,
1318
+ footers: parsedFooters,
1319
+ ...(footnoteCollection !== undefined ? { footnoteCollection } : {}),
1320
+ ...(parsedTheme !== undefined ? { theme: parsedTheme } : {}),
1321
+ ...(normalizedDocument.finalSectionProperties !== undefined
1322
+ ? { finalSectionProperties: normalizedDocument.finalSectionProperties }
1323
+ : {}),
1324
+ ...(resolvedTheme !== undefined ? { resolvedTheme } : {}),
1325
+ ...(parsedSettings !== undefined ? { settings: parsedSettings } : {}),
1326
+ }
1327
+ : undefined;
1328
+
1329
+ const timestamp = new Date().toISOString();
1330
+ const translatedWorkflowState = translateClmCommentsToWorkflow({
1331
+ comments: normalizedComments.threads,
1332
+ workflowOverlay: embeddedWorkflowOverlay,
1333
+ workflowMetadata: embeddedWorkflowMetadata,
1334
+ timestamp,
1335
+ });
1336
+ const importedDocument = createImportedCanonicalDocument({
1337
+ documentId: options.documentId,
1338
+ timestamp,
1339
+ numbering: parsedNumbering,
1340
+ media: normalizedDocument.media,
1341
+ content: normalizedDocument.content,
1342
+ subParts,
1343
+ parsedStyles,
1344
+ preservation: {
1345
+ ...normalizedDocument.preservation,
1346
+ packageParts: {
1347
+ ...normalizedDocument.preservation.packageParts,
1348
+ ...collectPreservedPackageParts(sourcePackage, [
1349
+ mainDocumentPath,
1350
+ numberingPartPath,
1351
+ commentsPartPath,
1352
+ commentsExtendedPartPath,
1353
+ commentsIdsPartPath,
1354
+ peoplePartPath,
1355
+ ]),
1356
+ },
1357
+ },
1358
+ diagnostics: {
1359
+ warnings: [
1360
+ ...createBrokenRelationshipWarnings(sourcePackage, mainDocumentPath),
1361
+ ...normalizedDocument.diagnostics.warnings,
1362
+ ...normalizedRevisions.diagnostics.map((diagnostic, index) => ({
1363
+ diagnosticId: `diagnostic:revision-import-${index + 1}`,
1364
+ warningId: `warning:revision-import-${diagnostic.revisionId}`,
1365
+ source: "review" as const,
1366
+ message: diagnostic.message,
1367
+ })),
1368
+ ...importedStoryRevisionDiagnostics.map((diagnostic, index) => ({
1369
+ diagnosticId: `diagnostic:story-revision-import-${index + 1}`,
1370
+ warningId: `warning:story-revision-import-${diagnostic.revisionId}`,
1371
+ source: "review" as const,
1372
+ message: diagnostic.message,
1373
+ })),
1374
+ ...normalizedComments.diagnostics.map((diagnostic, index) => ({
1375
+ diagnosticId: `diagnostic:comment-import-${index + 1}`,
1376
+ warningId: `warning:comment-import-${diagnostic.commentId}`,
1377
+ source: "review" as const,
1378
+ message: diagnostic.message,
1379
+ })),
1380
+ ],
1381
+ errors: [],
1382
+ },
1383
+ review: {
1384
+ comments: toRuntimeCommentRecords(translatedWorkflowState.comments),
1385
+ revisions: toRuntimeRevisionRecords([
1386
+ ...normalizedRevisions.revisions,
1387
+ ...importedStoryRevisions,
1388
+ ]),
1389
+ },
1390
+ });
1391
+ // Stage 0B.1: if the host implements `renderChartPreview`, resolve
1392
+ // chart_preview nodes inline so the first snapshot already carries the
1393
+ // synthesized `previewMediaId`. Fallback-safe: returning null or throwing
1394
+ // is per-chart — the typed badge renders as if the adapter weren't set.
1395
+ const document = (await resolveChartPreviewsForDocument(
1396
+ importedDocument,
1397
+ sourcePackage,
1398
+ options.hostAdapter,
1399
+ )) as CanonicalDocumentEnvelope;
1400
+ const compatibility = buildCompatibilityReport({
1401
+ document,
1402
+ generatedAt: timestamp,
1403
+ });
1404
+ await scheduler.yield();
1405
+ const snapshot = createImportedSnapshot({
1406
+ documentId: options.documentId,
1407
+ editorBuild,
1408
+ timestamp,
1409
+ document,
1410
+ compatibility: toPublicCompatibilityReport(compatibility),
1411
+ protectionSnapshot: importedProtectionSnapshot,
1412
+ sourcePackage: createPersistedSourcePackage(sourceBytes, options.sourceLabel),
1413
+ workflowOverlay: translatedWorkflowState.workflowOverlay,
1414
+ workflowMetadata: translatedWorkflowState.workflowMetadata,
1415
+ });
1416
+ const snapshotIssues = validatePersistedEditorSnapshot(snapshot);
1417
+ if (snapshotIssues.length > 0) {
1418
+ const firstIssue = snapshotIssues[0];
1419
+ return createDiagnosticsSession(
1420
+ options,
1421
+ createValidationImportDiagnostics({
1422
+ message: `DOCX import produced an invalid editor state during validation${firstIssue ? ` (${firstIssue.path}: ${firstIssue.message})` : "."}`,
1423
+ source: "import",
1424
+ details: {
1425
+ issueCount: snapshotIssues.length,
1426
+ firstIssuePath: firstIssue?.path,
1427
+ },
1428
+ }),
1429
+ );
1430
+ }
1431
+ const initialSessionState = editorSessionStateFromPersistedSnapshot(snapshot);
1432
+ const importedState: ImportedDocxState = {
1433
+ sourceBytes: new Uint8Array(sourceBytes),
1434
+ sourcePackage,
1435
+ sourceDocumentXml,
1436
+ sourceDocumentPartPath: mainDocumentPath,
1437
+ sourceDocumentRelationships: documentPart.relationships,
1438
+ sourceDocumentAttributes: extractDocumentRootAttributes(sourceDocumentXml),
1439
+ sourceNumberingPartPath: numberingPartPath,
1440
+ sourceNumberingRelationshipId: documentPart.relationships.find(
1441
+ (relationship) =>
1442
+ relationship.type === NUMBERING_RELATIONSHIP_TYPE &&
1443
+ relationship.targetMode === "internal",
1444
+ )?.id,
1445
+ sourceCommentsPartPath: commentsPartPath,
1446
+ sourceCommentsRelationshipId: documentPart.relationships.find(
1447
+ (relationship) =>
1448
+ relationship.type === COMMENTS_RELATIONSHIP_TYPE &&
1449
+ relationship.targetMode === "internal",
1450
+ )?.id,
1451
+ sourceCommentsRootTag: normalizedComments.sourceRootTag,
1452
+ sourceCommentsExtendedPartPath: commentsExtendedPartPath,
1453
+ sourceCommentsExtendedRelationshipId: documentPart.relationships.find(
1454
+ (relationship) =>
1455
+ relationship.type === COMMENTS_EXTENDED_RELATIONSHIP_TYPE &&
1456
+ relationship.targetMode === "internal",
1457
+ )?.id,
1458
+ sourceCommentsExtendedRootTag: normalizedComments.sourceExtendedRootTag,
1459
+ sourceCommentsIdsPartPath: commentsIdsPartPath,
1460
+ sourceCommentsIdsRelationshipId: documentPart.relationships.find(
1461
+ (relationship) =>
1462
+ relationship.type === COMMENTS_IDS_RELATIONSHIP_TYPE &&
1463
+ relationship.targetMode === "internal",
1464
+ )?.id,
1465
+ sourceCommentsIdsRootTag: normalizedComments.sourceIdsRootTag,
1466
+ sourcePeoplePartPath: peoplePartPath,
1467
+ sourcePeopleRelationshipId: documentPart.relationships.find(
1468
+ (relationship) =>
1469
+ relationship.type === PEOPLE_RELATIONSHIP_TYPE &&
1470
+ relationship.targetMode === "internal",
1471
+ )?.id,
1472
+ sourcePeopleRootTag: normalizedComments.sourcePeopleRootTag,
1473
+ sourcePeopleAuthors: normalizedComments.peopleAuthors,
1474
+ protectionSnapshot: buildProtectionSnapshot(documentProtection, protectionRanges),
1475
+ preservedCommentDefinitions: normalizedComments.preservedDefinitions,
1476
+ blockingCommentDiagnostics: normalizedComments.diagnostics.filter((diagnostic) =>
1477
+ BLOCKING_COMMENT_DIAGNOSTIC_CODES.has(diagnostic.code),
1478
+ ),
1479
+ initialCanonicalSignature: serializeCanonicalDocumentForExport(document),
1480
+ sourceSubPartPaths: {
1481
+ headers: sourceHeaderPaths,
1482
+ footers: sourceFooterPaths,
1483
+ footnotesPartPath,
1484
+ footnotesRelationshipId,
1485
+ endnotesPartPath,
1486
+ endnotesRelationshipId,
1487
+ themePartPath,
1488
+ themeRelationshipId: themeRelationship?.id,
1489
+ },
1490
+ };
1491
+
1492
+ stages.emit("skeleton-ready");
1493
+ return {
1494
+ initialSessionState,
1495
+ initialSnapshot: snapshot,
1496
+ readOnly: false,
1497
+ protectionSnapshot: importedProtectionSnapshot,
1498
+ exportDocx: async (nextSessionState, exportOptions) =>
1499
+ exportDocxEditorSession(importedState, nextSessionState, exportOptions),
1500
+ ...(embeddedWorkflowPayload?.editorState
1501
+ ? { initialEditorStatePayload: embeddedWorkflowPayload.editorState }
1502
+ : {}),
1503
+ };
1504
+ } catch (error) {
1505
+ return createDiagnosticsSession(
1506
+ options,
1507
+ createImportDiagnosticsFromError(error),
1508
+ );
1509
+ }
1510
+ }
1511
+
1512
+ function loadStageNow(): number {
1513
+ return typeof performance !== "undefined" && typeof performance.now === "function"
1514
+ ? performance.now()
1515
+ : Date.now();
1516
+ }
1517
+
832
1518
  function exportDocxEditorSession(
833
1519
  state: ImportedDocxState,
834
1520
  sessionStateOrSnapshot: EditorSessionState | PersistedEditorSnapshot,
@@ -1286,7 +1972,9 @@ function exportDocxEditorSession(
1286
1972
  }
1287
1973
 
1288
1974
  ensureHostMetadataParts(exportSession, state.sourcePackage, currentDocument);
1289
- ensureWorkflowPayloadParts(exportSession, sessionState, currentDocument, state.sourcePackage);
1975
+ // Schema 1.2: pass through editorState payload collected by the runtime channel.
1976
+ const internalEditorState = (options as { _editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload } | undefined)?._editorState;
1977
+ ensureWorkflowPayloadParts(exportSession, sessionState, currentDocument, state.sourcePackage, internalEditorState);
1290
1978
 
1291
1979
  return {
1292
1980
  bytes: exportSession.serialize(),
@@ -3012,11 +3700,13 @@ function ensureWorkflowPayloadParts(
3012
3700
  sessionState: EditorSessionState,
3013
3701
  document: CanonicalDocumentEnvelope,
3014
3702
  sourcePackage: OpcPackage,
3703
+ editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload,
3015
3704
  ): void {
3016
3705
  const payloadParts = buildWorkflowPayloadParts({
3017
3706
  sourcePackage,
3018
3707
  workflowMetadata: sessionState.workflowMetadata,
3019
3708
  workflowOverlay: sessionState.workflowOverlay,
3709
+ editorState,
3020
3710
  documentId: sessionState.documentId,
3021
3711
  createdAt: document.createdAt,
3022
3712
  updatedAt: document.updatedAt,