@beyondwork/docx-react-component 1.0.18 → 1.0.19

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 (74) hide show
  1. package/README.md +8 -2
  2. package/package.json +24 -34
  3. package/src/api/README.md +5 -1
  4. package/src/api/public-types.ts +374 -4
  5. package/src/api/session-state.ts +58 -0
  6. package/src/core/commands/formatting-commands.ts +1 -0
  7. package/src/core/commands/image-commands.ts +147 -0
  8. package/src/core/commands/index.ts +5 -1
  9. package/src/core/commands/list-commands.ts +231 -36
  10. package/src/core/commands/paragraph-layout-commands.ts +339 -0
  11. package/src/core/commands/section-layout-commands.ts +680 -0
  12. package/src/core/commands/style-commands.ts +262 -0
  13. package/src/core/search/search-text.ts +329 -0
  14. package/src/core/selection/mapping.ts +41 -0
  15. package/src/core/state/editor-state.ts +1 -1
  16. package/src/index.ts +30 -0
  17. package/src/io/docx-session.ts +260 -39
  18. package/src/io/export/serialize-main-document.ts +202 -5
  19. package/src/io/export/serialize-numbering.ts +28 -7
  20. package/src/io/normalize/normalize-text.ts +63 -25
  21. package/src/io/ooxml/numbering-sentinels.ts +44 -0
  22. package/src/io/ooxml/parse-footnotes.ts +212 -20
  23. package/src/io/ooxml/parse-headers-footers.ts +229 -25
  24. package/src/io/ooxml/parse-inline-media.ts +16 -0
  25. package/src/io/ooxml/parse-main-document.ts +411 -6
  26. package/src/io/ooxml/parse-numbering.ts +7 -0
  27. package/src/io/ooxml/parse-settings.ts +184 -0
  28. package/src/io/ooxml/parse-shapes.ts +25 -0
  29. package/src/io/ooxml/parse-styles.ts +463 -0
  30. package/src/io/ooxml/parse-theme.ts +32 -0
  31. package/src/model/canonical-document.ts +133 -3
  32. package/src/model/cds-1.0.0.ts +13 -0
  33. package/src/model/snapshot.ts +2 -1
  34. package/src/runtime/document-layout.ts +332 -0
  35. package/src/runtime/document-navigation.ts +564 -0
  36. package/src/runtime/document-runtime.ts +265 -35
  37. package/src/runtime/document-search.ts +145 -0
  38. package/src/runtime/numbering-prefix.ts +47 -26
  39. package/src/runtime/page-layout-estimation.ts +212 -0
  40. package/src/runtime/read-only-diagnostics-runtime.ts +1 -0
  41. package/src/runtime/session-capabilities.ts +2 -0
  42. package/src/runtime/story-context.ts +164 -0
  43. package/src/runtime/story-targeting.ts +162 -0
  44. package/src/runtime/surface-projection.ts +239 -12
  45. package/src/runtime/table-schema.ts +87 -5
  46. package/src/runtime/view-state.ts +459 -0
  47. package/src/ui/WordReviewEditor.tsx +1902 -312
  48. package/src/ui/browser-export.ts +52 -0
  49. package/src/ui/headless/preserve-editor-selection.ts +5 -0
  50. package/src/ui/headless/selection-helpers.ts +20 -0
  51. package/src/ui/headless/selection-toolbar-model.ts +22 -0
  52. package/src/ui/headless/use-editor-keyboard.ts +6 -1
  53. package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
  54. package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +125 -14
  55. package/src/ui-tailwind/editor-surface/perf-probe.ts +107 -0
  56. package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +45 -6
  57. package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
  58. package/src/ui-tailwind/editor-surface/pm-position-map.ts +2 -2
  59. package/src/ui-tailwind/editor-surface/pm-schema.ts +47 -5
  60. package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +95 -22
  61. package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
  62. package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
  63. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +394 -77
  64. package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
  65. package/src/ui-tailwind/index.ts +2 -1
  66. package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
  67. package/src/ui-tailwind/review/tw-review-rail.tsx +6 -6
  68. package/src/ui-tailwind/theme/editor-theme.css +123 -0
  69. package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
  70. package/src/ui-tailwind/toolbar/tw-toolbar.tsx +291 -12
  71. package/src/ui-tailwind/tw-review-workspace.tsx +926 -27
  72. package/src/validation/compatibility-engine.ts +92 -20
  73. package/src/validation/diagnostics.ts +1 -0
  74. package/src/validation/docx-comment-proof.ts +487 -0
@@ -1,12 +1,14 @@
1
1
  import type {
2
2
  CompatibilityReport as PublicCompatibilityReport,
3
3
  EditorError,
4
+ EditorSessionState,
4
5
  EditorWarning as PublicEditorWarning,
5
6
  EditorAnchorProjection as PublicEditorAnchorProjection,
6
7
  ExportDocxOptions,
7
8
  ExportResult,
8
9
  PersistedEditorSnapshot,
9
10
  } from "../api/public-types.ts";
11
+ import { editorSessionStateFromPersistedSnapshot } from "../api/session-state.ts";
10
12
  import type {
11
13
  CanonicalDocumentEnvelope,
12
14
  CompatibilityFeatureEntry as InternalCompatibilityFeatureEntry,
@@ -80,6 +82,7 @@ import type {
80
82
  import { createReadOnlyDiagnosticsRuntime } from "../runtime/read-only-diagnostics-runtime.ts";
81
83
  import {
82
84
  WORD_NUMBERING_CONTENT_TYPE,
85
+ hasSerializableNumberingEntries,
83
86
  serializeNumberingXml,
84
87
  } from "./export/serialize-numbering.ts";
85
88
  import {
@@ -89,6 +92,9 @@ import {
89
92
  } from "./ooxml/parse-headers-footers.ts";
90
93
  import { parseFootnotesXml, parseEndnotesXml } from "./ooxml/parse-footnotes.ts";
91
94
  import { parseThemeXml } from "./ooxml/parse-theme.ts";
95
+ import { resolveTheme } from "./ooxml/parse-theme.ts";
96
+ import { parseSettingsXml } from "./ooxml/parse-settings.ts";
97
+ import { parseStylesXml, type ParseStylesResult } from "./ooxml/parse-styles.ts";
92
98
  import {
93
99
  serializeHeaderXml,
94
100
  serializeFooterXml,
@@ -102,6 +108,11 @@ import {
102
108
  WORD_ENDNOTES_CONTENT_TYPE,
103
109
  } from "./export/serialize-footnotes.ts";
104
110
  import { createPersistedSourcePackage } from "./source-package-provenance.ts";
111
+ import { validatePersistedEditorSnapshot } from "../model/snapshot.ts";
112
+ import {
113
+ createSyntheticDocxNullNumberingCatalog,
114
+ DOCX_NULL_NUMBERING_INSTANCE_ID,
115
+ } from "./ooxml/numbering-sentinels.ts";
105
116
 
106
117
  const MAIN_DOCUMENT_PATH = "/word/document.xml";
107
118
  const NUMBERING_PART_PATH = "/word/numbering.xml";
@@ -149,22 +160,29 @@ const FOOTNOTES_RELATIONSHIP_TYPE =
149
160
  "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
150
161
  const ENDNOTES_RELATIONSHIP_TYPE =
151
162
  "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
163
+ const SETTINGS_RELATIONSHIP_TYPE =
164
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
165
+ const STYLES_RELATIONSHIP_TYPE =
166
+ "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
167
+ const STYLES_PART_PATH = "/word/styles.xml";
152
168
  const FOOTNOTES_PART_PATH = "/word/footnotes.xml";
153
169
  const ENDNOTES_PART_PATH = "/word/endnotes.xml";
170
+ const SETTINGS_PART_PATH = "/word/settings.xml";
154
171
 
155
172
  interface LoadDocxEditorSessionOptions {
156
173
  documentId: string;
157
174
  sourceLabel?: string;
158
175
  bytes: Uint8Array | ArrayBuffer;
159
- editorBuild: string;
176
+ editorBuild?: string;
160
177
  }
161
178
 
162
179
  export interface LoadedDocxEditorSession {
180
+ initialSessionState: EditorSessionState;
163
181
  initialSnapshot: PersistedEditorSnapshot;
164
182
  fatalError?: EditorError;
165
183
  readOnly: boolean;
166
184
  exportDocx: (
167
- snapshot: PersistedEditorSnapshot,
185
+ sessionState: EditorSessionState | PersistedEditorSnapshot,
168
186
  options?: ExportDocxOptions,
169
187
  ) => Promise<ExportResult>;
170
188
  }
@@ -220,6 +238,10 @@ const BLOCKING_COMMENT_DIAGNOSTIC_CODES = new Set<CommentImportDiagnostic["code"
220
238
  export function loadDocxEditorSession(
221
239
  options: LoadDocxEditorSessionOptions,
222
240
  ): LoadedDocxEditorSession {
241
+ const editorBuild =
242
+ typeof options.editorBuild === "string" && options.editorBuild.length > 0
243
+ ? options.editorBuild
244
+ : "dev";
223
245
  const sourceBytes = toUint8Array(options.bytes);
224
246
  let sourcePackage: OpcPackage;
225
247
 
@@ -404,6 +426,7 @@ export function loadDocxEditorSession(
404
426
  variant: ref.variant,
405
427
  partPath,
406
428
  relationshipId: ref.relationshipId,
429
+ ...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
407
430
  blocks: parsed.blocks,
408
431
  });
409
432
  sourceHeaderPaths.push({ partPath, relationshipId: ref.relationshipId });
@@ -413,6 +436,7 @@ export function loadDocxEditorSession(
413
436
  variant: ref.variant,
414
437
  partPath,
415
438
  relationshipId: ref.relationshipId,
439
+ ...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
416
440
  blocks: parsed.blocks,
417
441
  });
418
442
  sourceFooterPaths.push({ partPath, relationshipId: ref.relationshipId });
@@ -466,17 +490,54 @@ export function loadDocxEditorSession(
466
490
  decodeUtf8(sourcePackage.parts.get(themePartPath)?.bytes ?? new Uint8Array()),
467
491
  )
468
492
  : undefined;
493
+ const resolvedTheme = parsedTheme ? resolveTheme(parsedTheme) : undefined;
494
+ const settingsPartPath = resolveDocumentRelatedPartPath(
495
+ sourcePackage,
496
+ mainDocumentPath,
497
+ documentPart.relationships,
498
+ SETTINGS_RELATIONSHIP_TYPE,
499
+ SETTINGS_PART_PATH,
500
+ );
501
+ const parsedSettings =
502
+ settingsPartPath && sourcePackage.parts.has(settingsPartPath)
503
+ ? parseSettingsXml(
504
+ decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array()),
505
+ )
506
+ : undefined;
507
+
508
+ // ---- Parse styles.xml for canonical style catalog ----
509
+ const stylesPartPath = resolveDocumentRelatedPartPath(
510
+ sourcePackage,
511
+ mainDocumentPath,
512
+ documentPart.relationships,
513
+ STYLES_RELATIONSHIP_TYPE,
514
+ STYLES_PART_PATH,
515
+ );
516
+ const parsedStyles =
517
+ stylesPartPath && sourcePackage.parts.has(stylesPartPath)
518
+ ? parseStylesXml(
519
+ decodeUtf8(sourcePackage.parts.get(stylesPartPath)?.bytes ?? new Uint8Array()),
520
+ )
521
+ : parseStylesXml("");
469
522
 
470
523
  const subParts: SubPartsCatalog | undefined =
471
524
  parsedHeaders.length > 0 ||
472
525
  parsedFooters.length > 0 ||
473
526
  footnoteCollection !== undefined ||
474
- parsedTheme !== undefined
527
+ parsedTheme !== undefined ||
528
+ normalizedDocument.finalSectionProperties !== undefined ||
529
+ resolvedTheme !== undefined ||
530
+ parsedSettings !== undefined
475
531
  ? {
476
532
  headers: parsedHeaders,
477
533
  footers: parsedFooters,
478
534
  ...(footnoteCollection !== undefined ? { footnoteCollection } : {}),
479
535
  ...(parsedTheme !== undefined ? { theme: parsedTheme } : {}),
536
+ ...(normalizedDocument.finalSectionProperties !== undefined
537
+ ? { finalSectionProperties: normalizedDocument.finalSectionProperties }
538
+ : {}),
539
+ ...(resolvedTheme !== undefined ? { resolvedTheme } : {}),
540
+ ...(parsedSettings !== undefined ? { settings: parsedSettings } : {}),
480
541
  }
481
542
  : undefined;
482
543
 
@@ -488,6 +549,7 @@ export function loadDocxEditorSession(
488
549
  media: normalizedDocument.media,
489
550
  content: normalizedDocument.content,
490
551
  subParts,
552
+ parsedStyles,
491
553
  preservation: {
492
554
  ...normalizedDocument.preservation,
493
555
  packageParts: {
@@ -532,12 +594,28 @@ export function loadDocxEditorSession(
532
594
  });
533
595
  const snapshot = createImportedSnapshot({
534
596
  documentId: options.documentId,
535
- editorBuild: options.editorBuild,
597
+ editorBuild,
536
598
  timestamp,
537
599
  document,
538
600
  compatibility: toPublicCompatibilityReport(compatibility),
539
601
  sourcePackage: createPersistedSourcePackage(sourceBytes, options.sourceLabel),
540
602
  });
603
+ const snapshotIssues = validatePersistedEditorSnapshot(snapshot);
604
+ if (snapshotIssues.length > 0) {
605
+ const firstIssue = snapshotIssues[0];
606
+ return createDiagnosticsSession(
607
+ options,
608
+ createValidationImportDiagnostics({
609
+ message: `DOCX import produced an invalid editor state during validation${firstIssue ? ` (${firstIssue.path}: ${firstIssue.message})` : "."}`,
610
+ source: "import",
611
+ details: {
612
+ issueCount: snapshotIssues.length,
613
+ firstIssuePath: firstIssue?.path,
614
+ },
615
+ }),
616
+ );
617
+ }
618
+ const initialSessionState = editorSessionStateFromPersistedSnapshot(snapshot);
541
619
  const importedState: ImportedDocxState = {
542
620
  sourceBytes: new Uint8Array(sourceBytes),
543
621
  sourcePackage,
@@ -597,10 +675,11 @@ export function loadDocxEditorSession(
597
675
  };
598
676
 
599
677
  return {
678
+ initialSessionState,
600
679
  initialSnapshot: snapshot,
601
680
  readOnly: false,
602
- exportDocx: async (nextSnapshot, exportOptions) =>
603
- exportDocxEditorSession(importedState, nextSnapshot, exportOptions),
681
+ exportDocx: async (nextSessionState, exportOptions) =>
682
+ exportDocxEditorSession(importedState, nextSessionState, exportOptions),
604
683
  };
605
684
  } catch (error) {
606
685
  return createDiagnosticsSession(
@@ -612,14 +691,16 @@ export function loadDocxEditorSession(
612
691
 
613
692
  function exportDocxEditorSession(
614
693
  state: ImportedDocxState,
615
- snapshot: PersistedEditorSnapshot,
694
+ sessionStateOrSnapshot: EditorSessionState | PersistedEditorSnapshot,
616
695
  options?: ExportDocxOptions,
617
696
  ): ExportResult {
618
- if (snapshot.compatibility.blockExport) {
697
+ const sessionState = toEditorSessionState(sessionStateOrSnapshot);
698
+
699
+ if (sessionState.compatibility.blockExport) {
619
700
  throw new Error("DOCX export is blocked by the current compatibility report.");
620
701
  }
621
702
 
622
- const currentDocument = snapshot.canonicalDocument as CanonicalDocumentEnvelope;
703
+ const currentDocument = sessionState.canonicalDocument as CanonicalDocumentEnvelope;
623
704
  if (
624
705
  serializeCanonicalDocumentForExport(currentDocument) ===
625
706
  state.initialCanonicalSignature &&
@@ -628,7 +709,10 @@ function exportDocxEditorSession(
628
709
  return {
629
710
  bytes: new Uint8Array(state.sourceBytes),
630
711
  mimeType: DOCX_MIME_TYPE,
631
- fileName: options?.fileName ?? `${snapshot.documentId}.docx`,
712
+ fileName: options?.fileName ?? `${sessionState.documentId}.docx`,
713
+ delivery: {
714
+ mode: "exported-bytes-only",
715
+ },
632
716
  };
633
717
  }
634
718
  if (state.blockingCommentDiagnostics.length > 0) {
@@ -660,6 +744,7 @@ function exportDocxEditorSession(
660
744
  {
661
745
  documentAttributes: state.sourceDocumentAttributes,
662
746
  media: currentDocument.media as MediaCatalog,
747
+ finalSectionProperties: currentDocument.subParts?.finalSectionProperties,
663
748
  },
664
749
  );
665
750
  const revisionDocument = serializeRuntimeRevisionsIntoDocumentXml(
@@ -717,7 +802,9 @@ function exportDocxEditorSession(
717
802
  state.sourcePeoplePartPath ?? PEOPLE_PART_PATH;
718
803
  const numberingPartPath =
719
804
  state.sourceNumberingPartPath ?? NUMBERING_PART_PATH;
720
- const serializedNumberingXml = hasNumberingEntries(currentDocument.numbering as NumberingCatalog)
805
+ const serializedNumberingXml = hasSerializableNumberingEntries(
806
+ currentDocument.numbering as NumberingCatalog,
807
+ )
721
808
  ? serializeNumberingXml(currentDocument.numbering as NumberingCatalog)
722
809
  : undefined;
723
810
  const nextRelationships = withDocumentRelatedParts(
@@ -923,10 +1010,21 @@ function exportDocxEditorSession(
923
1010
  return {
924
1011
  bytes: exportSession.serialize(),
925
1012
  mimeType: DOCX_MIME_TYPE,
926
- fileName: options?.fileName ?? `${snapshot.documentId}.docx`,
1013
+ fileName: options?.fileName ?? `${sessionState.documentId}.docx`,
1014
+ delivery: {
1015
+ mode: "exported-bytes-only",
1016
+ },
927
1017
  };
928
1018
  }
929
1019
 
1020
+ function toEditorSessionState(
1021
+ sessionStateOrSnapshot: EditorSessionState | PersistedEditorSnapshot,
1022
+ ): EditorSessionState {
1023
+ return "sessionVersion" in sessionStateOrSnapshot
1024
+ ? sessionStateOrSnapshot
1025
+ : editorSessionStateFromPersistedSnapshot(sessionStateOrSnapshot);
1026
+ }
1027
+
930
1028
  function createImportedCanonicalDocument(input: {
931
1029
  documentId: string;
932
1030
  timestamp: string;
@@ -934,23 +1032,21 @@ function createImportedCanonicalDocument(input: {
934
1032
  media: CanonicalDocumentEnvelope["media"];
935
1033
  content: CanonicalDocumentEnvelope["content"];
936
1034
  subParts?: SubPartsCatalog;
1035
+ parsedStyles?: ParseStylesResult;
937
1036
  preservation: CanonicalDocumentEnvelope["preservation"];
938
1037
  diagnostics: CanonicalDocumentEnvelope["diagnostics"];
939
1038
  review: CanonicalDocumentEnvelope["review"];
940
1039
  }): CanonicalDocumentEnvelope {
941
- const paragraphStyles = Object.fromEntries(
942
- [...collectReferencedParagraphStyleIds(input.content, input.subParts)]
943
- .sort((left, right) => left.localeCompare(right))
944
- .map((styleId) => [
945
- styleId,
946
- {
947
- styleId,
948
- displayName: styleId,
949
- kind: "paragraph" as const,
950
- isDefault: styleId === "Normal",
951
- },
952
- ]),
1040
+ const numbering = ensureImportedNumberingCatalogSupportsContent(
1041
+ input.numbering,
1042
+ input.content,
953
1043
  );
1044
+
1045
+ // Use package-backed style catalog when available; fall back to synthetic
1046
+ // styles derived from referenced styleId values when styles.xml is missing
1047
+ // or could not be parsed.
1048
+ const styles = buildStylesCatalog(input.parsedStyles, input.content, input.subParts);
1049
+
954
1050
  return {
955
1051
  schemaVersion: "cds/1.0.0",
956
1052
  docId: createCanonicalDocumentId(input.documentId),
@@ -959,12 +1055,8 @@ function createImportedCanonicalDocument(input: {
959
1055
  metadata: {
960
1056
  customProperties: {},
961
1057
  },
962
- styles: {
963
- paragraphs: paragraphStyles,
964
- characters: {},
965
- tables: {},
966
- },
967
- numbering: input.numbering,
1058
+ styles,
1059
+ numbering,
968
1060
  media: input.media,
969
1061
  content: input.content,
970
1062
  review: input.review,
@@ -974,6 +1066,136 @@ function createImportedCanonicalDocument(input: {
974
1066
  };
975
1067
  }
976
1068
 
1069
+ // Canonical model styleId validation pattern — styleIds that don't match
1070
+ // are excluded from the catalog to avoid snapshot validation failures.
1071
+ const VALID_STYLE_ID = /^[A-Za-z_][A-Za-z0-9._-]{0,127}$/;
1072
+
1073
+ function buildStylesCatalog(
1074
+ parsedStyles: ParseStylesResult | undefined,
1075
+ content: CanonicalDocumentEnvelope["content"],
1076
+ subParts?: SubPartsCatalog,
1077
+ ): CanonicalDocumentEnvelope["styles"] {
1078
+ if (parsedStyles?.fromPackage) {
1079
+ // Package-backed catalog: filter entries whose styleId does not satisfy
1080
+ // the canonical model pattern (e.g. numeric-only ids from Word).
1081
+ const catalog = filterValidStyleIds(parsedStyles.catalog);
1082
+
1083
+ // Merge in any referenced styleIds that the package styles.xml did not
1084
+ // define (rare but defensive).
1085
+ const referencedIds = collectReferencedParagraphStyleIds(content, subParts);
1086
+ for (const styleId of referencedIds) {
1087
+ if (!catalog.paragraphs[styleId] && VALID_STYLE_ID.test(styleId)) {
1088
+ catalog.paragraphs[styleId] = {
1089
+ styleId,
1090
+ displayName: styleId,
1091
+ kind: "paragraph",
1092
+ isDefault: styleId === "Normal",
1093
+ };
1094
+ }
1095
+ }
1096
+ return {
1097
+ ...catalog,
1098
+ fromPackage: true,
1099
+ };
1100
+ }
1101
+
1102
+ // Synthetic fallback: no styles.xml available
1103
+ const paragraphStyles = Object.fromEntries(
1104
+ [...collectReferencedParagraphStyleIds(content, subParts)]
1105
+ .sort((left, right) => left.localeCompare(right))
1106
+ .filter((styleId) => VALID_STYLE_ID.test(styleId))
1107
+ .map((styleId) => [
1108
+ styleId,
1109
+ {
1110
+ styleId,
1111
+ displayName: styleId,
1112
+ kind: "paragraph" as const,
1113
+ isDefault: styleId === "Normal",
1114
+ },
1115
+ ]),
1116
+ );
1117
+ return {
1118
+ paragraphs: paragraphStyles,
1119
+ characters: {},
1120
+ tables: {},
1121
+ fromPackage: false,
1122
+ };
1123
+ }
1124
+
1125
+ function filterValidStyleIds(
1126
+ catalog: CanonicalDocumentEnvelope["styles"],
1127
+ ): CanonicalDocumentEnvelope["styles"] {
1128
+ const filterRecord = <T extends { styleId: string }>(
1129
+ record: Record<string, T>,
1130
+ ): Record<string, T> => {
1131
+ const result: Record<string, T> = {};
1132
+ for (const [key, value] of Object.entries(record)) {
1133
+ if (VALID_STYLE_ID.test(key)) {
1134
+ result[key] = value;
1135
+ }
1136
+ }
1137
+ return result;
1138
+ };
1139
+
1140
+ return {
1141
+ paragraphs: filterRecord(catalog.paragraphs),
1142
+ characters: filterRecord(catalog.characters),
1143
+ tables: filterRecord(catalog.tables),
1144
+ ...(catalog.latentStyles ? { latentStyles: catalog.latentStyles } : {}),
1145
+ ...(catalog.fromPackage !== undefined ? { fromPackage: catalog.fromPackage } : {}),
1146
+ };
1147
+ }
1148
+
1149
+ function ensureImportedNumberingCatalogSupportsContent(
1150
+ catalog: NumberingCatalog,
1151
+ content: CanonicalDocumentEnvelope["content"],
1152
+ ): NumberingCatalog {
1153
+ if (
1154
+ catalog.instances[DOCX_NULL_NUMBERING_INSTANCE_ID] ||
1155
+ !collectReferencedNumberingInstanceIds(content).has(DOCX_NULL_NUMBERING_INSTANCE_ID)
1156
+ ) {
1157
+ return catalog;
1158
+ }
1159
+
1160
+ const syntheticNullCatalog = createSyntheticDocxNullNumberingCatalog();
1161
+ return {
1162
+ abstractDefinitions: {
1163
+ ...catalog.abstractDefinitions,
1164
+ ...syntheticNullCatalog.abstractDefinitions,
1165
+ },
1166
+ instances: {
1167
+ ...catalog.instances,
1168
+ ...syntheticNullCatalog.instances,
1169
+ },
1170
+ };
1171
+ }
1172
+
1173
+ function collectReferencedNumberingInstanceIds(
1174
+ content: CanonicalDocumentEnvelope["content"],
1175
+ ): Set<string> {
1176
+ const numberingInstanceIds = new Set<string>();
1177
+
1178
+ const visitBlocks = (blocks: ReadonlyArray<BlockNode>) => {
1179
+ for (const block of blocks) {
1180
+ if (block.type === "paragraph" && block.numbering?.numberingInstanceId) {
1181
+ numberingInstanceIds.add(block.numbering.numberingInstanceId);
1182
+ }
1183
+ if (block.type === "table") {
1184
+ for (const row of block.rows) {
1185
+ for (const cell of row.cells) {
1186
+ visitBlocks(cell.children);
1187
+ }
1188
+ }
1189
+ } else if (block.type === "sdt" || block.type === "custom_xml") {
1190
+ visitBlocks(block.children);
1191
+ }
1192
+ }
1193
+ };
1194
+
1195
+ visitBlocks(content.children);
1196
+ return numberingInstanceIds;
1197
+ }
1198
+
977
1199
  function collectReferencedParagraphStyleIds(
978
1200
  content: CanonicalDocumentEnvelope["content"],
979
1201
  subParts?: SubPartsCatalog,
@@ -1110,20 +1332,26 @@ function createDiagnosticsSession(
1110
1332
  diagnostics: ImportDiagnosticsResult,
1111
1333
  ): LoadedDocxEditorSession {
1112
1334
  const timestamp = new Date().toISOString();
1335
+ const editorBuild =
1336
+ typeof options.editorBuild === "string" && options.editorBuild.length > 0
1337
+ ? options.editorBuild
1338
+ : "dev";
1113
1339
  const runtime = createReadOnlyDiagnosticsRuntime({
1114
1340
  documentId: options.documentId,
1115
1341
  sourceLabel: options.sourceLabel,
1116
- editorBuild: options.editorBuild,
1342
+ editorBuild,
1117
1343
  generatedAt: timestamp,
1118
1344
  diagnostics,
1119
1345
  });
1120
1346
  const initialSnapshot = runtime.getPersistedSnapshot();
1347
+ const initialSessionState = editorSessionStateFromPersistedSnapshot(initialSnapshot);
1121
1348
 
1122
1349
  return {
1350
+ initialSessionState,
1123
1351
  initialSnapshot,
1124
1352
  fatalError: diagnostics.fatalError,
1125
1353
  readOnly: true,
1126
- exportDocx: async (_snapshot, exportOptions) => runtime.exportDocx(exportOptions),
1354
+ exportDocx: async (_sessionState, exportOptions) => runtime.exportDocx(exportOptions),
1127
1355
  };
1128
1356
  }
1129
1357
 
@@ -1670,13 +1898,6 @@ function createEmptyNumberingCatalog(): NumberingCatalog {
1670
1898
  };
1671
1899
  }
1672
1900
 
1673
- function hasNumberingEntries(catalog: NumberingCatalog): boolean {
1674
- return (
1675
- Object.keys(catalog.abstractDefinitions ?? {}).length > 0 ||
1676
- Object.keys(catalog.instances ?? {}).length > 0
1677
- );
1678
- }
1679
-
1680
1901
  function collectBrokenInternalRelationshipIssues(
1681
1902
  sourcePackage: OpcPackage,
1682
1903
  mainDocumentPath?: string,