@beyondwork/docx-react-component 1.0.67 → 1.0.70
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -932
- package/package.json +26 -27
- package/src/api/anchor-conversion.ts +43 -0
- package/src/api/editor-state-types.ts +2 -1
- package/src/api/public-types.ts +504 -101
- package/src/api/session-state.ts +4 -0
- package/src/api/v3/README.md +91 -0
- package/src/api/v3/_create.ts +146 -0
- package/src/api/v3/_layer-metadata.ts +362 -0
- package/src/api/v3/_mocks.ts +84 -0
- package/src/api/v3/_runtime-handle.ts +162 -0
- package/src/api/v3/_ux-response.ts +73 -0
- package/src/api/v3/ai/_metadata-audit.ts +225 -0
- package/src/api/v3/ai/attach.ts +235 -0
- package/src/api/v3/ai/bundle.ts +132 -0
- package/src/api/v3/ai/explain.ts +144 -0
- package/src/api/v3/ai/export.ts +54 -0
- package/src/api/v3/ai/inspect.ts +118 -0
- package/src/api/v3/ai/policy.ts +77 -0
- package/src/api/v3/ai/replacement.ts +341 -0
- package/src/api/v3/ai/resolve.ts +133 -0
- package/src/api/v3/index.ts +79 -0
- package/src/api/v3/runtime/chart.ts +310 -0
- package/src/api/v3/runtime/clipboard.ts +81 -0
- package/src/api/v3/runtime/collab.ts +331 -0
- package/src/api/v3/runtime/content.ts +236 -0
- package/src/api/v3/runtime/document.ts +282 -0
- package/src/api/v3/runtime/formatting.ts +186 -0
- package/src/api/v3/runtime/geometry.ts +349 -0
- package/src/api/v3/runtime/layout.ts +108 -0
- package/src/api/v3/runtime/review.ts +129 -0
- package/src/api/v3/runtime/search.ts +74 -0
- package/src/api/v3/runtime/table.ts +63 -0
- package/src/api/v3/runtime/workflow.ts +434 -0
- package/src/api/v3/ui/_context.ts +86 -0
- package/src/api/v3/ui/_create.ts +65 -0
- package/src/api/v3/ui/_types.ts +520 -0
- package/src/api/v3/ui/chrome-composition.ts +342 -0
- package/src/{ui-tailwind/chrome → api/v3/ui}/chrome-preset-model.ts +11 -1
- package/src/api/v3/ui/chrome.ts +476 -0
- package/src/api/v3/ui/debug.ts +124 -0
- package/src/api/v3/ui/index.ts +64 -0
- package/src/api/v3/ui/overlays-visibility.ts +170 -0
- package/src/api/v3/ui/overlays.ts +427 -0
- package/src/api/v3/ui/scope.ts +71 -0
- package/src/api/v3/ui/session.ts +100 -0
- package/src/api/v3/ui/surface.ts +170 -0
- package/src/api/v3/ui/viewport.ts +303 -0
- package/src/core/commands/index.ts +28 -6
- package/src/core/commands/list-commands.ts +3 -2
- package/src/core/commands/section-layout-commands.ts +9 -8
- package/src/core/schema/text-schema.ts +16 -0
- package/src/core/selection/mapping.ts +33 -72
- package/src/core/state/editor-state.ts +96 -189
- package/src/index.ts +23 -4
- package/src/io/chart-preview-resolver.ts +1 -1
- package/src/io/docx-session.ts +36 -4797
- package/src/io/export/build-app-properties-xml.ts +1 -1
- package/src/io/export/serialize-comments.ts +1 -1
- package/src/io/export/serialize-headers-footers.ts +6 -1
- package/src/io/export/serialize-main-document.ts +45 -0
- package/src/io/export/serialize-run-formatting.ts +17 -2
- package/src/io/export/twip.ts +1 -1
- package/src/io/normalize/normalize-text.ts +27 -20
- package/src/io/ooxml/chart/parse-series.ts +1 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +1 -1
- package/src/io/ooxml/classify-embedding.ts +83 -33
- package/src/io/ooxml/parse-fill.ts +1 -1
- package/src/io/ooxml/parse-main-document.ts +71 -1
- package/src/io/ooxml/parse-object.ts +14 -10
- package/src/io/ooxml/parse-run-formatting.ts +47 -1
- package/src/io/ooxml/property-grab-bag.ts +2 -2
- package/src/io/ooxml/units.ts +11 -0
- package/src/io/ooxml/workflow-payload.ts +282 -7
- package/src/model/anchor.ts +85 -0
- package/src/model/canonical-document.ts +351 -15
- package/src/model/chart-types.ts +1 -1
- package/src/model/layout/index.ts +83 -0
- package/src/model/layout/page-graph-types.ts +181 -0
- package/src/model/layout/page-layout-snapshot.ts +105 -0
- package/src/model/layout/resolved-layout-types.ts +47 -0
- package/src/model/layout/runtime-page-graph-types.ts +102 -0
- package/src/model/paragraph-scope-ids.ts +72 -0
- package/src/model/review/comment-types.ts +112 -0
- package/src/model/review/index.ts +2 -0
- package/src/model/review/revision-types.ts +215 -0
- package/src/model/snapshot.ts +32 -0
- package/src/review/store/comment-store.ts +21 -47
- package/src/review/store/revision-types.ts +40 -198
- package/src/runtime/collab/base-doc-fingerprint.ts +6 -1
- package/src/runtime/collab/runtime-collab-sync.ts +13 -3
- package/src/runtime/collab-session.ts +1 -1
- package/src/runtime/debug/build-debug-inspector-snapshot.ts +686 -0
- package/src/runtime/debug/event-ring-buffer.ts +64 -0
- package/src/runtime/debug/probability-sampler.ts +18 -0
- package/src/runtime/debug/runtime-debug-facet.ts +67 -0
- package/src/runtime/debug/stage-tokens.ts +31 -0
- package/src/runtime/debug/telemetry-bus.ts +271 -0
- package/src/runtime/debug/types.ts +275 -0
- package/src/runtime/debug/wrap-ref-for-telemetry.ts +118 -0
- package/src/runtime/document-layout.ts +8 -6
- package/src/runtime/document-runtime.ts +843 -1141
- package/src/runtime/document-search.ts +1 -1
- package/src/runtime/edit-ops/index.ts +1 -1
- package/src/runtime/external-send-runtime.ts +1 -1
- package/src/runtime/formatting/document-lookup.ts +235 -0
- package/src/runtime/formatting/field/registry.ts +41 -0
- package/src/runtime/{field-resolver.ts → formatting/field/resolver.ts} +27 -2
- package/src/runtime/formatting/font-resolution.ts +83 -0
- package/src/runtime/formatting/formatting-context.ts +903 -0
- package/src/runtime/formatting/formatting-types.ts +157 -0
- package/src/runtime/{hyperlink-color-resolver.ts → formatting/hyperlink-color.ts} +2 -2
- package/src/runtime/formatting/index.ts +125 -0
- package/src/runtime/{resolved-numbering-geometry.ts → formatting/numbering/geometry.ts} +1 -1
- package/src/runtime/{numbering-prefix.ts → formatting/numbering/prefix.ts} +170 -3
- package/src/runtime/formatting/paragraph-style-resolver.ts +92 -0
- package/src/runtime/formatting/projector.ts +75 -0
- package/src/runtime/formatting/resolve-effective.ts +407 -0
- package/src/runtime/formatting/revision-display.ts +105 -0
- package/src/runtime/{paragraph-style-resolver.ts → formatting/style-cascade.ts} +84 -141
- package/src/runtime/{table-style-resolver.ts → formatting/table-style-resolver.ts} +1 -1
- package/src/runtime/formatting/telemetry-bridge.ts +106 -0
- package/src/runtime/{theme-color-resolver.ts → formatting/theme-color.ts} +2 -30
- package/src/runtime/geometry/caret-geometry.ts +164 -0
- package/src/runtime/geometry/geometry-facet.ts +364 -0
- package/src/runtime/geometry/geometry-types.ts +256 -0
- package/src/runtime/geometry/hit-test.ts +125 -0
- package/src/runtime/geometry/index.ts +71 -0
- package/src/runtime/geometry/inert-geometry-facet.ts +43 -0
- package/src/runtime/geometry/invalidation.ts +35 -0
- package/src/runtime/geometry/object-handles.ts +77 -0
- package/src/runtime/geometry/overlay-rects.ts +85 -0
- package/src/runtime/geometry/project-anchors.ts +100 -0
- package/src/runtime/geometry/project-fragments.ts +216 -0
- package/src/runtime/geometry/projector.ts +129 -0
- package/src/runtime/geometry/replacement-envelope.ts +130 -0
- package/src/runtime/geometry/viewport.ts +218 -0
- package/src/runtime/layout/compat-input-ledger.ts +211 -0
- package/src/runtime/layout/index.ts +6 -1
- package/src/runtime/layout/inert-layout-facet.ts +12 -7
- package/src/runtime/layout/layout-engine-instance.ts +189 -11
- package/src/runtime/layout/layout-engine-version.ts +450 -1
- package/src/runtime/layout/layout-facet-types.ts +60 -0
- package/src/runtime/layout/layout-measurement-provider.ts +13 -0
- package/src/runtime/layout/measurement-backend-canvas.ts +14 -2
- package/src/runtime/layout/measurement-backend-empirical.ts +23 -4
- package/src/runtime/layout/page-graph.ts +62 -209
- package/src/runtime/layout/page-story-resolver.ts +7 -12
- package/src/runtime/layout/paginated-layout-engine.ts +186 -11
- package/src/runtime/layout/project-block-fragments.ts +11 -0
- package/src/runtime/layout/projector.ts +90 -0
- package/src/runtime/layout/public-facet.ts +187 -442
- package/src/runtime/layout/resolved-formatting-state.ts +158 -26
- package/src/runtime/layout/table-render-plan.ts +1 -1
- package/src/runtime/prerender/cache-envelope.ts +6 -1
- package/src/runtime/prerender/prerender-document.ts +18 -23
- package/src/runtime/render/decoration-resolver.ts +1 -1
- package/src/runtime/render/render-frame-types.ts +20 -0
- package/src/runtime/render/render-kernel.ts +94 -25
- package/src/runtime/scopes/_formatting-seam.ts +262 -0
- package/src/runtime/scopes/_scope-dependencies.ts +49 -0
- package/src/runtime/scopes/action-validation.ts +356 -0
- package/src/runtime/scopes/attach-explanation.ts +102 -0
- package/src/runtime/scopes/audit-bundle.ts +71 -0
- package/src/runtime/scopes/compile-scope-bundle.ts +163 -0
- package/src/runtime/scopes/compile-scope.ts +262 -0
- package/src/runtime/scopes/compiler-service.ts +431 -0
- package/src/runtime/scopes/create-issue.ts +107 -0
- package/src/runtime/scopes/enumerate-scopes.ts +543 -0
- package/src/runtime/scopes/evidence.ts +233 -0
- package/src/runtime/scopes/index.ts +150 -0
- package/src/runtime/scopes/position-map.ts +214 -0
- package/src/runtime/scopes/preservation-boundary.ts +91 -0
- package/src/runtime/scopes/projector.ts +49 -0
- package/src/runtime/scopes/replaceability.ts +87 -0
- package/src/runtime/scopes/replacement/apply.ts +228 -0
- package/src/runtime/scopes/replacement/compile.ts +59 -0
- package/src/runtime/scopes/replacement/propose.ts +42 -0
- package/src/runtime/scopes/resolve-reference.ts +347 -0
- package/src/runtime/scopes/review-bundle.ts +141 -0
- package/src/runtime/scopes/scope-kinds/_paragraph-text.ts +57 -0
- package/src/runtime/scopes/scope-kinds/_table-text.ts +42 -0
- package/src/runtime/scopes/scope-kinds/comment-thread.ts +59 -0
- package/src/runtime/scopes/scope-kinds/field.ts +65 -0
- package/src/runtime/scopes/scope-kinds/heading.ts +84 -0
- package/src/runtime/scopes/scope-kinds/list-item.ts +77 -0
- package/src/runtime/scopes/scope-kinds/paragraph.ts +182 -0
- package/src/runtime/scopes/scope-kinds/revision.ts +62 -0
- package/src/runtime/scopes/scope-kinds/table-cell.ts +57 -0
- package/src/runtime/scopes/scope-kinds/table-row.ts +61 -0
- package/src/runtime/scopes/scope-kinds/table.ts +55 -0
- package/src/runtime/scopes/scope-range.ts +208 -0
- package/src/runtime/scopes/semantic-scope-types.ts +454 -0
- package/src/runtime/scopes/workflow-overlap.ts +92 -0
- package/src/runtime/selection/index.ts +1 -1
- package/src/runtime/structure-ops/fragment-insert.ts +1 -1
- package/src/runtime/structure-ops/index.ts +1 -1
- package/src/runtime/surface-projection.ts +232 -262
- package/src/runtime/units.ts +4 -2
- package/src/runtime/workflow/coordinator.ts +1348 -0
- package/src/runtime/workflow/derived-scope-resolver.ts +125 -0
- package/src/runtime/workflow/index.ts +25 -0
- package/src/runtime/workflow/markup-mode-policy.ts +98 -0
- package/src/runtime/{workflow-markup.ts → workflow/markup.ts} +6 -6
- package/src/runtime/workflow/metadata-persistence.ts +306 -0
- package/src/runtime/workflow/metadata-writer.ts +123 -0
- package/src/runtime/workflow/overlay-store.ts +690 -0
- package/src/runtime/workflow/projector.ts +127 -0
- package/src/runtime/{query-scopes.ts → workflow/query-scopes.ts} +3 -3
- package/src/runtime/{workflow-rail-segments.ts → workflow/rail/compose.ts} +60 -165
- package/src/runtime/workflow/rail/types.ts +198 -0
- package/src/runtime/workflow/scope-rail-composer.ts +39 -0
- package/src/runtime/{scope-resolver.ts → workflow/scope-resolver.ts} +3 -3
- package/src/runtime/workflow/scope-writer.ts +188 -0
- package/src/runtime/{tamper-gate.ts → workflow/tamper-gate.ts} +1 -1
- package/src/runtime/workflow/visibility-policy.ts +129 -0
- package/src/session/_sync-legacy.ts +66 -0
- package/src/session/export/embedded-reconstitute.ts +104 -0
- package/src/session/export/export-diagnostics.ts +85 -0
- package/src/session/export/export-validation.ts +110 -0
- package/src/session/export/index.ts +34 -0
- package/src/session/export/preservation-reattach.ts +30 -0
- package/src/session/export/serialize-dispatch.ts +165 -0
- package/src/session/export/stateful-export-pipeline.ts +432 -0
- package/src/session/export/stateful-export.ts +684 -0
- package/src/session/import/canonical-assembly.ts +227 -0
- package/src/session/import/diagnostics-session.ts +54 -0
- package/src/session/import/embedded-discovery.ts +225 -0
- package/src/session/import/embedded-offload.ts +337 -0
- package/src/session/import/import-diagnostics.ts +69 -0
- package/src/session/import/loader-types.ts +313 -0
- package/src/session/import/loader.ts +1834 -0
- package/src/session/import/normalize.ts +195 -0
- package/src/session/import/package-parts.ts +217 -0
- package/src/session/import/package-read.ts +195 -0
- package/src/session/import/parse-orchestration.ts +105 -0
- package/src/session/import/part-constants.ts +70 -0
- package/src/session/import/part-discovery.ts +94 -0
- package/src/session/import/preservation-index.ts +46 -0
- package/src/{runtime/read-only-diagnostics-runtime.ts → session/import/read-only-diagnostics.ts} +24 -3
- package/src/session/import/review-import.ts +508 -0
- package/src/session/import/styles-consolidation.ts +281 -0
- package/src/session/import/workflow-scope-import.ts +256 -0
- package/src/session/index.ts +37 -0
- package/src/session/session-state.ts +69 -0
- package/src/session/session.ts +532 -0
- package/src/session/shared/protection.ts +228 -0
- package/src/session/shared/session-utils.ts +82 -0
- package/src/session/types.ts +499 -0
- package/src/shell/chart-snapshots.ts +96 -0
- package/src/shell/media-previews.ts +85 -0
- package/src/shell/overlay-anchor-bridge.ts +53 -0
- package/src/shell/paste-adapter.ts +23 -0
- package/src/shell/ref-commands.ts +1697 -0
- package/src/shell/ref-utilities.ts +48 -0
- package/src/shell/search.ts +51 -0
- package/src/{ui/editor-runtime-boundary.ts → shell/session-bootstrap.ts} +243 -67
- package/src/shell/ui-subscriber-channels.ts +81 -0
- package/src/shell/use-collab-sync.ts +116 -0
- package/src/ui/WordReviewEditor.tsx +496 -2051
- package/src/ui/editor-shell-view.tsx +30 -1
- package/src/ui/editor-surface-controller.tsx +49 -1
- package/src/ui/headless/revision-decoration-model.ts +83 -0
- package/src/{ui-tailwind/chrome → ui/headless}/role-action-sets.ts +1 -1
- package/src/ui/headless/scoped-chrome-policy.ts +2 -2
- package/src/ui/headless/selection-tool-context.ts +1 -1
- package/src/ui/headless/selection-tool-resolver.ts +1 -1
- package/src/ui/runtime-shortcut-dispatch.ts +46 -1
- package/src/ui/ui-controller-factory.ts +221 -0
- package/src/ui-tailwind/chart/ChartSurface.tsx +2 -2
- package/src/ui-tailwind/chart/layout/legend-layout.ts +1 -1
- package/src/ui-tailwind/chart/layout/plot-area.ts +2 -2
- package/src/ui-tailwind/chart/layout/title-layout.ts +1 -1
- package/src/ui-tailwind/chart/render/area.tsx +3 -3
- package/src/ui-tailwind/chart/render/bar-column.tsx +3 -3
- package/src/ui-tailwind/chart/render/bubble.tsx +3 -3
- package/src/ui-tailwind/chart/render/combo.tsx +2 -2
- package/src/ui-tailwind/chart/render/data-labels.tsx +2 -2
- package/src/ui-tailwind/chart/render/font-metrics.ts +2 -2
- package/src/ui-tailwind/chart/render/line.tsx +3 -3
- package/src/ui-tailwind/chart/render/pie.tsx +6 -6
- package/src/ui-tailwind/chart/render/scatter.tsx +3 -3
- package/src/ui-tailwind/chart/render/svg-primitives.ts +3 -3
- package/src/ui-tailwind/chart/render/unsupported.tsx +2 -2
- package/src/ui-tailwind/chrome/build-context-menu-entries.ts +88 -0
- package/src/ui-tailwind/chrome/chrome-preset-toolbar.tsx +1 -1
- package/src/ui-tailwind/chrome/collab-send-to-supplier-button.tsx +1 -1
- package/src/ui-tailwind/chrome/collab-tamper-banner.tsx +1 -1
- package/src/ui-tailwind/chrome/collab-top-nav-container.tsx +1 -1
- package/src/ui-tailwind/chrome/editor-action-registry.ts +553 -0
- package/src/ui-tailwind/chrome/editor-actions-to-palette.ts +182 -0
- package/src/ui-tailwind/chrome/local-surface-arbiter.ts +534 -0
- package/src/ui-tailwind/chrome/resolve-target-kind.ts +226 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +38 -4
- package/src/ui-tailwind/chrome/tw-context-band.tsx +125 -0
- package/src/ui-tailwind/chrome/tw-context-menu-portal.tsx +248 -0
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +42 -1
- package/src/ui-tailwind/chrome/tw-selection-anchor-resolver.ts +8 -7
- package/src/ui-tailwind/chrome/tw-selection-tool-blocked.tsx +38 -4
- package/src/ui-tailwind/chrome/tw-selection-tool-comment.tsx +104 -6
- package/src/ui-tailwind/chrome/tw-selection-tool-host.tsx +66 -7
- package/src/ui-tailwind/chrome/tw-selection-tool-workflow.tsx +54 -8
- package/src/ui-tailwind/chrome/tw-shortcut-hint.tsx +7 -1
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +33 -0
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +78 -1
- package/src/ui-tailwind/chrome/tw-table-grip-layer.tsx +16 -8
- package/src/ui-tailwind/chrome/tw-workspace-chrome-host.tsx +276 -0
- package/src/ui-tailwind/chrome/use-context-menu-controller.ts +201 -0
- package/src/ui-tailwind/chrome-overlay/chrome-overlay-projector.ts +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +22 -4
- package/src/ui-tailwind/chrome-overlay/tw-comment-balloon-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-locked-block-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +11 -5
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +197 -3
- package/src/ui-tailwind/chrome-overlay/tw-revision-margin-bar-layer.tsx +1 -1
- package/src/ui-tailwind/chrome-overlay/tw-scope-card-layer.tsx +35 -6
- package/src/ui-tailwind/chrome-overlay/tw-scope-rail-layer.tsx +24 -16
- package/src/ui-tailwind/chrome-overlay/tw-table-continuation-header.tsx +1 -1
- package/src/ui-tailwind/debug/README.md +57 -0
- package/src/ui-tailwind/debug/index.ts +3 -0
- package/src/ui-tailwind/debug/tw-debug-overlay.tsx +186 -0
- package/src/ui-tailwind/debug/tw-debug-presentation.tsx +80 -0
- package/src/ui-tailwind/debug/tw-debug-top-bar.tsx +83 -0
- package/src/ui-tailwind/editor-surface/chart-node-view.tsx +2 -2
- package/src/ui-tailwind/editor-surface/float-wrap-resolver.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +135 -10
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +40 -13
- package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-schema.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +3 -3
- package/src/ui-tailwind/editor-surface/predicted-tag-preflight.ts +1 -1
- package/src/ui-tailwind/editor-surface/remote-cursor-plugin.ts +2 -2
- package/src/ui-tailwind/editor-surface/scroll-anchor.ts +91 -9
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +1 -1
- package/src/ui-tailwind/editor-surface/surface-layer.ts +1 -1
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +1 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +23 -6
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +132 -22
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +1 -1
- package/src/ui-tailwind/index.ts +0 -5
- package/src/ui-tailwind/overlay-anchor-bridge-context.tsx +33 -0
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +66 -29
- package/src/ui-tailwind/page-stack/tw-floating-image-layer.tsx +25 -2
- package/src/ui-tailwind/review/comment-markdown-renderer.tsx +15 -0
- package/src/ui-tailwind/review/tw-review-rail.tsx +92 -4
- package/src/ui-tailwind/review/tw-workflow-tab.tsx +1 -1
- package/src/ui-tailwind/review-workspace/page-chrome.ts +210 -0
- package/src/ui-tailwind/review-workspace/page-shell-metrics.ts +101 -0
- package/src/ui-tailwind/review-workspace/paragraph-layout.ts +115 -0
- package/src/ui-tailwind/review-workspace/selection-toolbar-placement.ts +97 -0
- package/src/ui-tailwind/review-workspace/tw-review-workspace-navigator.tsx +130 -0
- package/src/ui-tailwind/review-workspace/tw-review-workspace-page-toolbar.tsx +240 -0
- package/src/ui-tailwind/review-workspace/tw-review-workspace-rail.tsx +59 -0
- package/src/ui-tailwind/review-workspace/types.ts +408 -0
- package/src/ui-tailwind/review-workspace/use-chrome-policy.ts +104 -0
- package/src/ui-tailwind/review-workspace/use-derived-view-state.ts +151 -0
- package/src/ui-tailwind/review-workspace/use-diagnostics-signal.ts +70 -0
- package/src/ui-tailwind/review-workspace/use-grabbed-segment-offsets.ts +40 -0
- package/src/ui-tailwind/review-workspace/use-layout-facet-render-signal.ts +55 -0
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +130 -0
- package/src/ui-tailwind/review-workspace/use-pm-surface-capture.ts +60 -0
- package/src/ui-tailwind/review-workspace/use-review-rail-state.ts +63 -0
- package/src/ui-tailwind/review-workspace/use-scope-card-state.ts +170 -0
- package/src/ui-tailwind/review-workspace/use-scroll-root-capture.ts +28 -0
- package/src/ui-tailwind/review-workspace/use-selection-toolbar-placement.ts +113 -0
- package/src/ui-tailwind/review-workspace/use-shell-selection-anchor-bridge.ts +120 -0
- package/src/ui-tailwind/review-workspace/use-status-bar-page-facts.ts +55 -0
- package/src/ui-tailwind/review-workspace/use-viewport-dimensions.ts +43 -0
- package/src/ui-tailwind/review-workspace/use-workspace-arbiter.ts +25 -0
- package/src/ui-tailwind/review-workspace/use-workspace-composition.ts +86 -0
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +150 -0
- package/src/ui-tailwind/theme/editor-theme.css +25 -0
- package/src/ui-tailwind/toolbar/tw-role-action-region.tsx +2 -2
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +61 -98
- package/src/ui-tailwind/tw-review-workspace.tsx +521 -1802
- package/src/ui-tailwind/ui-api-context.tsx +43 -0
- package/src/ui-tailwind/ui-shell-channels-context.tsx +49 -0
- package/src/validation/compatibility-engine.ts +6 -6
- package/src/runtime/styles-cascade.ts +0 -33
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +0 -85
- /package/src/runtime/{page-number-format.ts → formatting/field/page-number-format.ts} +0 -0
- /package/src/runtime/{ai-action-policy.ts → workflow/ai-action-policy.ts} +0 -0
- /package/src/runtime/{scope-tag-registry.ts → workflow/scope-tag-registry.ts} +0 -0
package/src/io/docx-session.ts
CHANGED
|
@@ -1,4797 +1,36 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// (surface projection, cache envelopes, read-only diagnostics) and
|
|
38
|
-
// presentation. The runtime imports below (surface-projection,
|
|
39
|
-
// prerender/cache-envelope, read-only-diagnostics-runtime) are load-bearing
|
|
40
|
-
// and violate the nominal io→runtime boundary on purpose. The proper
|
|
41
|
-
// long-term fix is to relocate this file to src/session/ (see
|
|
42
|
-
// docs/plans/architecture-lane.md §F2). Do NOT add more runtime imports
|
|
43
|
-
// here without first reading that deferral rationale.
|
|
44
|
-
import { createEditorSurfaceSnapshot } from "../runtime/surface-projection.ts";
|
|
45
|
-
import { MAIN_STORY_TARGET } from "../core/selection/mapping.ts";
|
|
46
|
-
import {
|
|
47
|
-
createDetachedAnchor,
|
|
48
|
-
storyTargetsEqual,
|
|
49
|
-
type EditorAnchorProjection as InternalEditorAnchorProjection,
|
|
50
|
-
} from "../core/selection/mapping.ts";
|
|
51
|
-
import { DOCX_MIME_TYPE } from "./opc/docx-package.ts";
|
|
52
|
-
import { readOpcPackage, type OpcPackage } from "./opc/package-reader.ts";
|
|
53
|
-
import {
|
|
54
|
-
parseMainDocumentXml,
|
|
55
|
-
type ParsedBlockNode,
|
|
56
|
-
type ParsedInlineNode,
|
|
57
|
-
type ParsedPermStartInlineNode,
|
|
58
|
-
} from "./ooxml/parse-main-document.ts";
|
|
59
|
-
import {
|
|
60
|
-
normalizeParsedTextDocument,
|
|
61
|
-
normalizeParsedTextDocumentAsync,
|
|
62
|
-
} from "./normalize/normalize-text.ts";
|
|
63
|
-
import { createChartPartLookup, resolveChartPreviewsForDocument, scheduleChartPreviewResolution } from "./chart-preview-resolver.ts";
|
|
64
|
-
import { type LoadScheduler } from "./load-scheduler.ts";
|
|
65
|
-
import type { CacheEnvelope } from "../runtime/prerender/cache-envelope.ts";
|
|
66
|
-
import {
|
|
67
|
-
CONTENT_TYPES_PATH,
|
|
68
|
-
PACKAGE_RELATIONSHIPS_PATH,
|
|
69
|
-
getRelationshipsPartPath,
|
|
70
|
-
normalizePartPath,
|
|
71
|
-
resolveRelationshipTarget,
|
|
72
|
-
type OpcRelationship,
|
|
73
|
-
} from "./ooxml/part-manifest.ts";
|
|
74
|
-
import {
|
|
75
|
-
buildWorkflowPayloadParts,
|
|
76
|
-
getDocumentBackedWorkflowMetadata,
|
|
77
|
-
parseWorkflowPayloadEnvelopeFromPackage,
|
|
78
|
-
resolvePayloadPartPath,
|
|
79
|
-
resolveWorkflowPayloadPartPaths,
|
|
80
|
-
WORKFLOW_PAYLOAD_CONTENT_TYPE,
|
|
81
|
-
WORKFLOW_PAYLOAD_CUSTOM_PROPS_CONTENT_TYPE,
|
|
82
|
-
WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
|
|
83
|
-
WORKFLOW_PAYLOAD_CUSTOM_PROPS_RELATIONSHIP_TYPE,
|
|
84
|
-
WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE,
|
|
85
|
-
WORKFLOW_PAYLOAD_RELATIONSHIP_TYPE,
|
|
86
|
-
} from "./ooxml/workflow-payload.ts";
|
|
87
|
-
import {
|
|
88
|
-
classifyCorruptPackageError,
|
|
89
|
-
createBrokenRelationshipIssue,
|
|
90
|
-
createMissingPartIssue,
|
|
91
|
-
} from "./opc/corrupt-package.ts";
|
|
92
|
-
import { buildAppPropertiesXml } from "./export/build-app-properties-xml.ts";
|
|
93
|
-
import { createExportSession } from "./export/export-session.ts";
|
|
94
|
-
import { serializeMainDocument } from "./export/serialize-main-document.ts";
|
|
95
|
-
import {
|
|
96
|
-
serializeSettingsXml,
|
|
97
|
-
WORD_SETTINGS_CONTENT_TYPE,
|
|
98
|
-
} from "./export/serialize-settings.ts";
|
|
99
|
-
import {
|
|
100
|
-
parseRevisionsFromDocumentXml,
|
|
101
|
-
parseRevisionsFromStoryXml,
|
|
102
|
-
type ParsedRevisionsResult,
|
|
103
|
-
} from "./ooxml/parse-revisions.ts";
|
|
104
|
-
import { parseCommentsFromOoxml } from "./ooxml/parse-comments.ts";
|
|
105
|
-
import { parseNumberingXml } from "./ooxml/parse-numbering.ts";
|
|
106
|
-
import {
|
|
107
|
-
createCommentExportIdMap,
|
|
108
|
-
mapParagraphBoundaries,
|
|
109
|
-
serializeCommentAnchorsIntoDocumentXml,
|
|
110
|
-
serializeMergedCommentsXml,
|
|
111
|
-
} from "./export/serialize-comments.ts";
|
|
112
|
-
import { splitDocumentAtReviewBoundaries } from "./export/split-review-boundaries.ts";
|
|
113
|
-
import { splitStoryBlocksForRuntimeRevisions } from "./export/split-story-blocks-for-runtime-revisions.ts";
|
|
114
|
-
import {
|
|
115
|
-
serializeRuntimeRevisionsIntoDocumentXml,
|
|
116
|
-
serializeRuntimeRevisionsIntoStoryXml,
|
|
117
|
-
} from "./export/serialize-runtime-revisions.ts";
|
|
118
|
-
import { createCommentStoreFromRuntimeComments } from "../review/store/runtime-comment-store.ts";
|
|
119
|
-
import type { CommentThread } from "../review/store/comment-store.ts";
|
|
120
|
-
import type { RevisionRecord as ReviewRevisionRecord } from "../review/store/revision-types.ts";
|
|
121
|
-
import { getRevisionActionability } from "../review/store/revision-types.ts";
|
|
122
|
-
import { buildCompatibilityReport } from "../validation/compatibility-engine.ts";
|
|
123
|
-
import {
|
|
124
|
-
createPackageImportDiagnostics,
|
|
125
|
-
createValidationImportDiagnostics,
|
|
126
|
-
type ImportDiagnosticsResult,
|
|
127
|
-
} from "../validation/import-diagnostics.ts";
|
|
128
|
-
import type {
|
|
129
|
-
BlockNode,
|
|
130
|
-
FootnoteCollection,
|
|
131
|
-
HeaderDocument,
|
|
132
|
-
FooterDocument,
|
|
133
|
-
InlineNode,
|
|
134
|
-
MediaCatalog,
|
|
135
|
-
NumberingCatalog,
|
|
136
|
-
OpaqueFragmentRecord,
|
|
137
|
-
PreservedPackagePart,
|
|
138
|
-
SubPartsCatalog,
|
|
139
|
-
} from "../model/canonical-document.ts";
|
|
140
|
-
import { createCanonicalDocumentSignature } from "../model/canonical-document.ts";
|
|
141
|
-
import type { CanonicalDocument } from "../model/canonical-document.ts";
|
|
142
|
-
import type {
|
|
143
|
-
CommentImportDiagnostic,
|
|
144
|
-
ImportedCommentDefinition,
|
|
145
|
-
ParsedCommentsResult,
|
|
146
|
-
} from "./ooxml/parse-comments.ts";
|
|
147
|
-
import { createReadOnlyDiagnosticsRuntime } from "../runtime/read-only-diagnostics-runtime.ts";
|
|
148
|
-
import {
|
|
149
|
-
WORD_NUMBERING_CONTENT_TYPE,
|
|
150
|
-
hasSerializableNumberingEntries,
|
|
151
|
-
serializeNumberingXml,
|
|
152
|
-
} from "./export/serialize-numbering.ts";
|
|
153
|
-
import {
|
|
154
|
-
parseHeaderFooterReferences,
|
|
155
|
-
parseHeaderXml,
|
|
156
|
-
parseFooterXml,
|
|
157
|
-
} from "./ooxml/parse-headers-footers.ts";
|
|
158
|
-
import { parseFootnotesXml, parseEndnotesXml } from "./ooxml/parse-footnotes.ts";
|
|
159
|
-
import { materializeCanonicalTheme, parseThemeXml, resolveTheme } from "./ooxml/parse-theme.ts";
|
|
160
|
-
import { parseSettingsXml } from "./ooxml/parse-settings.ts";
|
|
161
|
-
import { parseStylesXml, type ParseStylesResult } from "./ooxml/parse-styles.ts";
|
|
162
|
-
import { parseFontTable } from "./ooxml/parse-font-table.ts";
|
|
163
|
-
import {
|
|
164
|
-
serializeHeaderXml,
|
|
165
|
-
serializeHeaderXmlWithRevisions,
|
|
166
|
-
serializeFooterXml,
|
|
167
|
-
serializeFooterXmlWithRevisions,
|
|
168
|
-
WORD_HEADER_CONTENT_TYPE,
|
|
169
|
-
WORD_FOOTER_CONTENT_TYPE,
|
|
170
|
-
} from "./export/serialize-headers-footers.ts";
|
|
171
|
-
import {
|
|
172
|
-
serializeFootnotesXml,
|
|
173
|
-
serializeEndnotesXml,
|
|
174
|
-
WORD_FOOTNOTES_CONTENT_TYPE,
|
|
175
|
-
WORD_ENDNOTES_CONTENT_TYPE,
|
|
176
|
-
} from "./export/serialize-footnotes.ts";
|
|
177
|
-
import { createPersistedSourcePackage } from "./source-package-provenance.ts";
|
|
178
|
-
import { validatePersistedEditorSnapshot } from "../model/snapshot.ts";
|
|
179
|
-
import {
|
|
180
|
-
createSyntheticDocxNullNumberingCatalog,
|
|
181
|
-
DOCX_NULL_NUMBERING_INSTANCE_ID,
|
|
182
|
-
} from "./ooxml/numbering-sentinels.ts";
|
|
183
|
-
|
|
184
|
-
const MAIN_DOCUMENT_PATH = "/word/document.xml";
|
|
185
|
-
const NUMBERING_PART_PATH = "/word/numbering.xml";
|
|
186
|
-
const COMMENTS_PART_PATH = "/word/comments.xml";
|
|
187
|
-
const COMMENTS_EXTENDED_PART_PATH = "/word/commentsExtended.xml";
|
|
188
|
-
const COMMENTS_IDS_PART_PATH = "/word/commentsIds.xml";
|
|
189
|
-
const PEOPLE_PART_PATH = "/word/people.xml";
|
|
190
|
-
const APP_PROPERTIES_PART_PATH = "/docProps/app.xml";
|
|
191
|
-
const CORE_PROPERTIES_PART_PATH = "/docProps/core.xml";
|
|
192
|
-
const MAIN_DOCUMENT_CONTENT_TYPE =
|
|
193
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml";
|
|
194
|
-
const APP_PROPERTIES_CONTENT_TYPE =
|
|
195
|
-
"application/vnd.openxmlformats-officedocument.extended-properties+xml";
|
|
196
|
-
const CORE_PROPERTIES_CONTENT_TYPE =
|
|
197
|
-
"application/vnd.openxmlformats-package.core-properties+xml";
|
|
198
|
-
const NUMBERING_RELATIONSHIP_TYPE =
|
|
199
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
|
|
200
|
-
const COMMENTS_CONTENT_TYPE =
|
|
201
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
|
|
202
|
-
const COMMENTS_EXTENDED_CONTENT_TYPE =
|
|
203
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.commentsExtended+xml";
|
|
204
|
-
const COMMENTS_IDS_CONTENT_TYPE =
|
|
205
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.commentsIds+xml";
|
|
206
|
-
const PEOPLE_CONTENT_TYPE =
|
|
207
|
-
"application/vnd.openxmlformats-officedocument.wordprocessingml.people+xml";
|
|
208
|
-
const COMMENTS_RELATIONSHIP_TYPE =
|
|
209
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
|
|
210
|
-
const COMMENTS_EXTENDED_RELATIONSHIP_TYPE =
|
|
211
|
-
"http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
|
|
212
|
-
const COMMENTS_IDS_RELATIONSHIP_TYPE =
|
|
213
|
-
"http://schemas.microsoft.com/office/2016/09/relationships/commentsIds";
|
|
214
|
-
const PEOPLE_RELATIONSHIP_TYPE =
|
|
215
|
-
"http://schemas.microsoft.com/office/2011/relationships/people";
|
|
216
|
-
const APP_PROPERTIES_RELATIONSHIP_TYPE =
|
|
217
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties";
|
|
218
|
-
const CORE_PROPERTIES_RELATIONSHIP_TYPE =
|
|
219
|
-
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties";
|
|
220
|
-
const OFFICE_DOCUMENT_RELATIONSHIP_TYPE =
|
|
221
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument";
|
|
222
|
-
const HEADER_RELATIONSHIP_TYPE =
|
|
223
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/header";
|
|
224
|
-
const FOOTER_RELATIONSHIP_TYPE =
|
|
225
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer";
|
|
226
|
-
const FOOTNOTES_RELATIONSHIP_TYPE =
|
|
227
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
|
|
228
|
-
const ENDNOTES_RELATIONSHIP_TYPE =
|
|
229
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
|
|
230
|
-
const SETTINGS_RELATIONSHIP_TYPE =
|
|
231
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
|
|
232
|
-
const STYLES_RELATIONSHIP_TYPE =
|
|
233
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
|
|
234
|
-
const STYLES_PART_PATH = "/word/styles.xml";
|
|
235
|
-
const FONT_TABLE_RELATIONSHIP_TYPE =
|
|
236
|
-
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable";
|
|
237
|
-
const FONT_TABLE_PART_PATH = "/word/fontTable.xml";
|
|
238
|
-
const FOOTNOTES_PART_PATH = "/word/footnotes.xml";
|
|
239
|
-
const ENDNOTES_PART_PATH = "/word/endnotes.xml";
|
|
240
|
-
const SETTINGS_PART_PATH = "/word/settings.xml";
|
|
241
|
-
|
|
242
|
-
interface LoadDocxEditorSessionOptions {
|
|
243
|
-
documentId: string;
|
|
244
|
-
sourceLabel?: string;
|
|
245
|
-
bytes: Uint8Array | ArrayBuffer;
|
|
246
|
-
editorBuild?: string;
|
|
247
|
-
/**
|
|
248
|
-
* Fastload P2: optional instrumentation callback invoked once per
|
|
249
|
-
* completed load stage. Stage ordering: opc → body →
|
|
250
|
-
* styles-numbering-comments → skeleton-ready. `durationMs` is
|
|
251
|
-
* measured against `performance.now()` boundaries inside the loader.
|
|
252
|
-
*
|
|
253
|
-
* Pure instrumentation — has no effect on returned session shape.
|
|
254
|
-
* Safe to leave undefined; existing callers observe no behavior
|
|
255
|
-
* change.
|
|
256
|
-
*/
|
|
257
|
-
onLoadStage?: (stage: import("../api/public-types.ts").LoadStage, durationMs: number) => void;
|
|
258
|
-
/**
|
|
259
|
-
* Stage 0B.1: host-adapter surface. The sync loader accepts the option for
|
|
260
|
-
* API symmetry with {@link LoadDocxEditorSessionAsyncOptions} but does not
|
|
261
|
-
* invoke `renderChartPreview` — chart-preview synthesis is asynchronous, so
|
|
262
|
-
* hosts that want preview bitmaps must call {@link loadDocxEditorSessionAsync}.
|
|
263
|
-
* Other adapter methods (load/saveSession/logEvent) are not consumed by the
|
|
264
|
-
* loader itself and are carried through unchanged.
|
|
265
|
-
*/
|
|
266
|
-
hostAdapter?: EditorHostAdapter;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
export interface LoadedDocxEditorSession {
|
|
270
|
-
initialSessionState: EditorSessionState;
|
|
271
|
-
initialSnapshot: PersistedEditorSnapshot;
|
|
272
|
-
fatalError?: EditorError;
|
|
273
|
-
readOnly: boolean;
|
|
274
|
-
protectionSnapshot: ProtectionSnapshot;
|
|
275
|
-
exportDocx: (
|
|
276
|
-
sessionState: EditorSessionState | PersistedEditorSnapshot,
|
|
277
|
-
options?: ExportDocxOptions,
|
|
278
|
-
) => Promise<ExportResult>;
|
|
279
|
-
/** Schema 1.2 — editorState block parsed from the 1.2 payload, if present. */
|
|
280
|
-
initialEditorStatePayload?: import("./ooxml/workflow-payload.ts").EditorStatePayload;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
interface ImportedDocxState {
|
|
284
|
-
sourceBytes: Uint8Array;
|
|
285
|
-
sourcePackage: OpcPackage;
|
|
286
|
-
sourceDocumentXml: string;
|
|
287
|
-
sourceDocumentPartPath: string;
|
|
288
|
-
sourceDocumentRelationships: readonly OpcRelationship[];
|
|
289
|
-
sourceDocumentAttributes: Record<string, string>;
|
|
290
|
-
sourceNumberingPartPath?: string;
|
|
291
|
-
sourceNumberingRelationshipId?: string;
|
|
292
|
-
/**
|
|
293
|
-
* Resolved `/word/settings.xml` part path when the source package carried
|
|
294
|
-
* one. Threaded through to the export path so the settings serializer can
|
|
295
|
-
* call `replaceOwnedPart` with the right relationship target.
|
|
296
|
-
*/
|
|
297
|
-
sourceSettingsPartPath?: string;
|
|
298
|
-
/**
|
|
299
|
-
* Original settings.xml bytes decoded as UTF-8. Passed to
|
|
300
|
-
* `serializeSettingsXml(settings, sourceXml)` as the graft source so
|
|
301
|
-
* unmodelled top-level children (`<w:defaultTabStop>`,
|
|
302
|
-
* `<w:documentProtection>`, mail-merge state, etc.) survive verbatim
|
|
303
|
-
* through round-trip. Undefined when the source package lacked a
|
|
304
|
-
* settings part.
|
|
305
|
-
*/
|
|
306
|
-
sourceSettingsXml?: string;
|
|
307
|
-
sourceCommentsPartPath?: string;
|
|
308
|
-
sourceCommentsRelationshipId?: string;
|
|
309
|
-
sourceCommentsRootTag?: string;
|
|
310
|
-
sourceCommentsExtendedPartPath?: string;
|
|
311
|
-
sourceCommentsExtendedRelationshipId?: string;
|
|
312
|
-
sourceCommentsExtendedRootTag?: string;
|
|
313
|
-
sourceCommentsIdsPartPath?: string;
|
|
314
|
-
sourceCommentsIdsRelationshipId?: string;
|
|
315
|
-
sourceCommentsIdsRootTag?: string;
|
|
316
|
-
sourcePeoplePartPath?: string;
|
|
317
|
-
sourcePeopleRelationshipId?: string;
|
|
318
|
-
sourcePeopleRootTag?: string;
|
|
319
|
-
sourcePeopleAuthors: readonly string[];
|
|
320
|
-
protectionSnapshot: ProtectionSnapshot;
|
|
321
|
-
preservedCommentDefinitions: readonly ImportedCommentDefinition[];
|
|
322
|
-
blockingCommentDiagnostics: readonly CommentImportDiagnostic[];
|
|
323
|
-
initialCanonicalSignature: string;
|
|
324
|
-
sourceSubPartPaths: {
|
|
325
|
-
headers: Array<{ partPath: string; relationshipId: string }>;
|
|
326
|
-
footers: Array<{ partPath: string; relationshipId: string }>;
|
|
327
|
-
footnotesPartPath?: string;
|
|
328
|
-
footnotesRelationshipId?: string;
|
|
329
|
-
endnotesPartPath?: string;
|
|
330
|
-
endnotesRelationshipId?: string;
|
|
331
|
-
themePartPath?: string;
|
|
332
|
-
themeRelationshipId?: string;
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
export interface NormalizedImportedCommentsResult extends ParsedCommentsResult {
|
|
337
|
-
preservedDefinitions: readonly ImportedCommentDefinition[];
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
const BLOCKING_COMMENT_DIAGNOSTIC_CODES = new Set<CommentImportDiagnostic["code"]>([
|
|
341
|
-
"missing_comment_definition",
|
|
342
|
-
"missing_anchor_reference",
|
|
343
|
-
"multi_paragraph_anchor_preserve_only",
|
|
344
|
-
"opaque_anchor_preserve_only",
|
|
345
|
-
"preserve_only_revision_overlap",
|
|
346
|
-
]);
|
|
347
|
-
|
|
348
|
-
interface StageEmitter {
|
|
349
|
-
emit(stage: import("../api/public-types.ts").LoadStage): void;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Fastload P2 (hoisted in P6): build a stage-event emitter. When no callback
|
|
354
|
-
* is supplied the emitter is a zero-cost no-op. Shared between the sync and
|
|
355
|
-
* async load orchestrators so both paths emit `load-stage` events at the
|
|
356
|
-
* same four boundaries: `opc` → `body` → `styles-numbering-comments` →
|
|
357
|
-
* `skeleton-ready`.
|
|
358
|
-
*/
|
|
359
|
-
function createStageEmitter(
|
|
360
|
-
onStage: LoadDocxEditorSessionOptions["onLoadStage"],
|
|
361
|
-
): StageEmitter {
|
|
362
|
-
if (!onStage) {
|
|
363
|
-
return {
|
|
364
|
-
emit() {
|
|
365
|
-
/* no-op */
|
|
366
|
-
},
|
|
367
|
-
};
|
|
368
|
-
}
|
|
369
|
-
let cursor = loadStageNow();
|
|
370
|
-
return {
|
|
371
|
-
emit(stage) {
|
|
372
|
-
const nextMark = loadStageNow();
|
|
373
|
-
onStage(stage, nextMark - cursor);
|
|
374
|
-
cursor = nextMark;
|
|
375
|
-
},
|
|
376
|
-
};
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
export function loadDocxEditorSession(
|
|
380
|
-
options: LoadDocxEditorSessionOptions,
|
|
381
|
-
): LoadedDocxEditorSession {
|
|
382
|
-
const editorBuild =
|
|
383
|
-
typeof options.editorBuild === "string" && options.editorBuild.length > 0
|
|
384
|
-
? options.editorBuild
|
|
385
|
-
: "dev";
|
|
386
|
-
const sourceBytes = toUint8Array(options.bytes);
|
|
387
|
-
|
|
388
|
-
const stages = createStageEmitter(options.onLoadStage);
|
|
389
|
-
|
|
390
|
-
let sourcePackage: OpcPackage;
|
|
391
|
-
|
|
392
|
-
try {
|
|
393
|
-
sourcePackage = readOpcPackage(sourceBytes);
|
|
394
|
-
} catch (error) {
|
|
395
|
-
return createDiagnosticsSession(
|
|
396
|
-
options,
|
|
397
|
-
createPackageImportDiagnostics({
|
|
398
|
-
issue: classifyCorruptPackageError(error),
|
|
399
|
-
}),
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
stages.emit("opc");
|
|
403
|
-
const embeddedWorkflowPayload = parseWorkflowPayloadEnvelopeFromPackage(sourcePackage);
|
|
404
|
-
const embeddedWorkflowMetadata = embeddedWorkflowPayload?.workflowMetadata;
|
|
405
|
-
const embeddedWorkflowOverlay = embeddedWorkflowPayload?.workflowOverlay;
|
|
406
|
-
|
|
407
|
-
const mainDocumentPath = resolveMainDocumentPartPath(sourcePackage);
|
|
408
|
-
const brokenRelationshipIssues = collectBrokenInternalRelationshipIssues(
|
|
409
|
-
sourcePackage,
|
|
410
|
-
mainDocumentPath,
|
|
411
|
-
);
|
|
412
|
-
if (brokenRelationshipIssues.length > 0) {
|
|
413
|
-
return createDiagnosticsSession(
|
|
414
|
-
options,
|
|
415
|
-
createPackageImportDiagnostics({
|
|
416
|
-
issue: {
|
|
417
|
-
...brokenRelationshipIssues[0],
|
|
418
|
-
message: summarizeBrokenRelationshipIssues(brokenRelationshipIssues),
|
|
419
|
-
details: {
|
|
420
|
-
issueCount: brokenRelationshipIssues.length,
|
|
421
|
-
targets: brokenRelationshipIssues.map((issue) => issue.targetPartPath).filter(Boolean),
|
|
422
|
-
},
|
|
423
|
-
},
|
|
424
|
-
}),
|
|
425
|
-
);
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
if (!mainDocumentPath) {
|
|
429
|
-
return createDiagnosticsSession(
|
|
430
|
-
options,
|
|
431
|
-
createPackageImportDiagnostics({
|
|
432
|
-
issue: createMissingPartIssue(MAIN_DOCUMENT_PATH),
|
|
433
|
-
}),
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
const documentPart = sourcePackage.parts.get(mainDocumentPath);
|
|
438
|
-
if (!documentPart) {
|
|
439
|
-
return createDiagnosticsSession(
|
|
440
|
-
options,
|
|
441
|
-
createPackageImportDiagnostics({
|
|
442
|
-
issue: createMissingPartIssue(mainDocumentPath),
|
|
443
|
-
}),
|
|
444
|
-
);
|
|
445
|
-
}
|
|
446
|
-
if (documentPart.contentType !== MAIN_DOCUMENT_CONTENT_TYPE) {
|
|
447
|
-
return createDiagnosticsSession(
|
|
448
|
-
options,
|
|
449
|
-
createValidationImportDiagnostics({
|
|
450
|
-
message: `DOCX main document part ${mainDocumentPath} must use content type ${MAIN_DOCUMENT_CONTENT_TYPE}.`,
|
|
451
|
-
}),
|
|
452
|
-
);
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
try {
|
|
456
|
-
const sourceDocumentXml = decodeUtf8(documentPart.bytes);
|
|
457
|
-
const importedRevisions = parseRevisionsFromDocumentXml(sourceDocumentXml);
|
|
458
|
-
const numberingPartPath = resolveDocumentRelatedPartPath(
|
|
459
|
-
sourcePackage,
|
|
460
|
-
mainDocumentPath,
|
|
461
|
-
documentPart.relationships,
|
|
462
|
-
NUMBERING_RELATIONSHIP_TYPE,
|
|
463
|
-
NUMBERING_PART_PATH,
|
|
464
|
-
);
|
|
465
|
-
const parsedNumbering = numberingPartPath
|
|
466
|
-
? parseNumberingXml(
|
|
467
|
-
decodeUtf8(sourcePackage.parts.get(numberingPartPath)?.bytes ?? new Uint8Array()),
|
|
468
|
-
{
|
|
469
|
-
relationships: sourcePackage.parts.get(numberingPartPath)?.relationships,
|
|
470
|
-
partPath: numberingPartPath,
|
|
471
|
-
},
|
|
472
|
-
)
|
|
473
|
-
: createEmptyNumberingCatalog();
|
|
474
|
-
const mediaParts = collectInlineMediaParts(sourcePackage);
|
|
475
|
-
const chartPartLookup = createChartPartLookup(
|
|
476
|
-
sourcePackage,
|
|
477
|
-
mainDocumentPath,
|
|
478
|
-
documentPart.relationships,
|
|
479
|
-
);
|
|
480
|
-
const parsedDocument = parseMainDocumentXml(
|
|
481
|
-
sourceDocumentXml,
|
|
482
|
-
documentPart.relationships,
|
|
483
|
-
mediaParts,
|
|
484
|
-
mainDocumentPath,
|
|
485
|
-
chartPartLookup,
|
|
486
|
-
);
|
|
487
|
-
const protectionRanges = extractProtectionRanges(parsedDocument.blocks);
|
|
488
|
-
const normalizedDocument = normalizeParsedTextDocument(
|
|
489
|
-
parsedDocument,
|
|
490
|
-
mainDocumentPath,
|
|
491
|
-
);
|
|
492
|
-
stages.emit("body");
|
|
493
|
-
const commentsPartPath = resolveCommentsPartPath(
|
|
494
|
-
sourcePackage,
|
|
495
|
-
mainDocumentPath,
|
|
496
|
-
documentPart.relationships,
|
|
497
|
-
);
|
|
498
|
-
const commentsExtendedPartPath = resolveDocumentRelatedPartPath(
|
|
499
|
-
sourcePackage,
|
|
500
|
-
mainDocumentPath,
|
|
501
|
-
documentPart.relationships,
|
|
502
|
-
COMMENTS_EXTENDED_RELATIONSHIP_TYPE,
|
|
503
|
-
COMMENTS_EXTENDED_PART_PATH,
|
|
504
|
-
);
|
|
505
|
-
const commentsIdsPartPath = resolveDocumentRelatedPartPath(
|
|
506
|
-
sourcePackage,
|
|
507
|
-
mainDocumentPath,
|
|
508
|
-
documentPart.relationships,
|
|
509
|
-
COMMENTS_IDS_RELATIONSHIP_TYPE,
|
|
510
|
-
COMMENTS_IDS_PART_PATH,
|
|
511
|
-
);
|
|
512
|
-
const peoplePartPath = resolveDocumentRelatedPartPath(
|
|
513
|
-
sourcePackage,
|
|
514
|
-
mainDocumentPath,
|
|
515
|
-
documentPart.relationships,
|
|
516
|
-
PEOPLE_RELATIONSHIP_TYPE,
|
|
517
|
-
PEOPLE_PART_PATH,
|
|
518
|
-
);
|
|
519
|
-
const parsedComments = commentsPartPath
|
|
520
|
-
? parseCommentsFromOoxml(
|
|
521
|
-
sourceDocumentXml,
|
|
522
|
-
{
|
|
523
|
-
commentsXml: decodeUtf8(sourcePackage.parts.get(commentsPartPath)?.bytes ?? new Uint8Array()),
|
|
524
|
-
commentsExtendedXml: decodeUtf8(
|
|
525
|
-
sourcePackage.parts.get(commentsExtendedPartPath ?? "")?.bytes ?? new Uint8Array(),
|
|
526
|
-
),
|
|
527
|
-
commentsIdsXml: decodeUtf8(
|
|
528
|
-
sourcePackage.parts.get(commentsIdsPartPath ?? "")?.bytes ?? new Uint8Array(),
|
|
529
|
-
),
|
|
530
|
-
peopleXml: decodeUtf8(
|
|
531
|
-
sourcePackage.parts.get(peoplePartPath ?? "")?.bytes ?? new Uint8Array(),
|
|
532
|
-
),
|
|
533
|
-
},
|
|
534
|
-
)
|
|
535
|
-
: {
|
|
536
|
-
threads: [] as CommentThread[],
|
|
537
|
-
diagnostics: [] as CommentImportDiagnostic[],
|
|
538
|
-
definitions: [] as ImportedCommentDefinition[],
|
|
539
|
-
sourceRootTag: undefined,
|
|
540
|
-
sourceExtendedRootTag: undefined,
|
|
541
|
-
sourceIdsRootTag: undefined,
|
|
542
|
-
sourcePeopleRootTag: undefined,
|
|
543
|
-
peopleAuthors: [] as string[],
|
|
544
|
-
};
|
|
545
|
-
const normalizedRevisions = normalizeImportedRevisionRecords(
|
|
546
|
-
importedRevisions,
|
|
547
|
-
normalizedDocument.content,
|
|
548
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
549
|
-
);
|
|
550
|
-
const normalizedComments = normalizeImportedCommentThreads(
|
|
551
|
-
parsedComments,
|
|
552
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
553
|
-
normalizedRevisions.revisions,
|
|
554
|
-
);
|
|
555
|
-
stages.emit("styles-numbering-comments");
|
|
556
|
-
const importedStoryRevisions: ReviewRevisionRecord[] = [];
|
|
557
|
-
const importedStoryRevisionDiagnostics: ParsedRevisionsResult["diagnostics"] = [];
|
|
558
|
-
const subPartOpaqueState = createSubPartOpaqueImportState(
|
|
559
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
560
|
-
normalizedDocument.diagnostics.warnings,
|
|
561
|
-
);
|
|
562
|
-
// ---- Parse sub-parts: headers, footers, footnotes, endnotes, theme ----
|
|
563
|
-
const headerFooterRefs = parseHeaderFooterReferences(sourceDocumentXml);
|
|
564
|
-
const parsedHeaders: HeaderDocument[] = [];
|
|
565
|
-
const parsedFooters: FooterDocument[] = [];
|
|
566
|
-
const sourceHeaderPaths: Array<{ partPath: string; relationshipId: string }> = [];
|
|
567
|
-
const sourceFooterPaths: Array<{ partPath: string; relationshipId: string }> = [];
|
|
568
|
-
const seenSubPartKeys = new Set<string>();
|
|
569
|
-
|
|
570
|
-
for (const ref of headerFooterRefs) {
|
|
571
|
-
const dedupeKey = `${ref.kind}:${ref.variant}:${ref.relationshipId}`;
|
|
572
|
-
if (seenSubPartKeys.has(dedupeKey)) {
|
|
573
|
-
continue;
|
|
574
|
-
}
|
|
575
|
-
seenSubPartKeys.add(dedupeKey);
|
|
576
|
-
|
|
577
|
-
const relationship = documentPart.relationships.find(
|
|
578
|
-
(r) => r.id === ref.relationshipId && r.targetMode === "internal",
|
|
579
|
-
);
|
|
580
|
-
if (!relationship) {
|
|
581
|
-
continue;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
const partPath = resolveRelationshipTarget(mainDocumentPath, relationship);
|
|
585
|
-
const part = sourcePackage.parts.get(partPath);
|
|
586
|
-
const partBytes = part?.bytes;
|
|
587
|
-
if (!partBytes) {
|
|
588
|
-
continue;
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
const xml = decodeUtf8(partBytes);
|
|
592
|
-
const subPartRelationships = part?.relationships ?? [];
|
|
593
|
-
const subPartChartPartLookup = createChartPartLookup(
|
|
594
|
-
sourcePackage,
|
|
595
|
-
partPath,
|
|
596
|
-
subPartRelationships,
|
|
597
|
-
);
|
|
598
|
-
if (ref.kind === "header") {
|
|
599
|
-
const parsedHeaderRevisions = parseRevisionsFromStoryXml(xml);
|
|
600
|
-
const parsed = parseHeaderXml(xml, {
|
|
601
|
-
relationships: subPartRelationships,
|
|
602
|
-
mediaParts,
|
|
603
|
-
sourcePartPath: partPath,
|
|
604
|
-
chartPartLookup: subPartChartPartLookup,
|
|
605
|
-
});
|
|
606
|
-
parsedHeaders.push({
|
|
607
|
-
variant: ref.variant,
|
|
608
|
-
partPath,
|
|
609
|
-
relationshipId: ref.relationshipId,
|
|
610
|
-
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
611
|
-
blocks: normalizeSubPartOpaqueBlocks(
|
|
612
|
-
parsed.blocks,
|
|
613
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
614
|
-
normalizedDocument.diagnostics.warnings,
|
|
615
|
-
partPath,
|
|
616
|
-
subPartOpaqueState,
|
|
617
|
-
),
|
|
618
|
-
});
|
|
619
|
-
importedStoryRevisions.push(
|
|
620
|
-
...parsedHeaderRevisions.revisions.map((revision): ReviewRevisionRecord => ({
|
|
621
|
-
...revision,
|
|
622
|
-
metadata: {
|
|
623
|
-
...revision.metadata,
|
|
624
|
-
storyTarget: {
|
|
625
|
-
kind: "header" as const,
|
|
626
|
-
relationshipId: ref.relationshipId,
|
|
627
|
-
variant: ref.variant,
|
|
628
|
-
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
629
|
-
},
|
|
630
|
-
},
|
|
631
|
-
})),
|
|
632
|
-
);
|
|
633
|
-
importedStoryRevisionDiagnostics.push(...parsedHeaderRevisions.diagnostics);
|
|
634
|
-
sourceHeaderPaths.push({ partPath, relationshipId: ref.relationshipId });
|
|
635
|
-
} else {
|
|
636
|
-
const parsedFooterRevisions = parseRevisionsFromStoryXml(xml);
|
|
637
|
-
const parsed = parseFooterXml(xml, {
|
|
638
|
-
relationships: subPartRelationships,
|
|
639
|
-
mediaParts,
|
|
640
|
-
sourcePartPath: partPath,
|
|
641
|
-
chartPartLookup: subPartChartPartLookup,
|
|
642
|
-
});
|
|
643
|
-
parsedFooters.push({
|
|
644
|
-
variant: ref.variant,
|
|
645
|
-
partPath,
|
|
646
|
-
relationshipId: ref.relationshipId,
|
|
647
|
-
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
648
|
-
blocks: normalizeSubPartOpaqueBlocks(
|
|
649
|
-
parsed.blocks,
|
|
650
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
651
|
-
normalizedDocument.diagnostics.warnings,
|
|
652
|
-
partPath,
|
|
653
|
-
subPartOpaqueState,
|
|
654
|
-
),
|
|
655
|
-
});
|
|
656
|
-
importedStoryRevisions.push(
|
|
657
|
-
...parsedFooterRevisions.revisions.map((revision): ReviewRevisionRecord => ({
|
|
658
|
-
...revision,
|
|
659
|
-
metadata: {
|
|
660
|
-
...revision.metadata,
|
|
661
|
-
storyTarget: {
|
|
662
|
-
kind: "footer" as const,
|
|
663
|
-
relationshipId: ref.relationshipId,
|
|
664
|
-
variant: ref.variant,
|
|
665
|
-
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
666
|
-
},
|
|
667
|
-
},
|
|
668
|
-
})),
|
|
669
|
-
);
|
|
670
|
-
importedStoryRevisionDiagnostics.push(...parsedFooterRevisions.diagnostics);
|
|
671
|
-
sourceFooterPaths.push({ partPath, relationshipId: ref.relationshipId });
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
const footnotesPartPath = resolveDocumentRelatedPartPath(
|
|
676
|
-
sourcePackage,
|
|
677
|
-
mainDocumentPath,
|
|
678
|
-
documentPart.relationships,
|
|
679
|
-
FOOTNOTES_RELATIONSHIP_TYPE,
|
|
680
|
-
FOOTNOTES_PART_PATH,
|
|
681
|
-
);
|
|
682
|
-
const footnotesRelationshipId = documentPart.relationships.find(
|
|
683
|
-
(r) => r.type === FOOTNOTES_RELATIONSHIP_TYPE && r.targetMode === "internal",
|
|
684
|
-
)?.id;
|
|
685
|
-
const endnotesPartPath = resolveDocumentRelatedPartPath(
|
|
686
|
-
sourcePackage,
|
|
687
|
-
mainDocumentPath,
|
|
688
|
-
documentPart.relationships,
|
|
689
|
-
ENDNOTES_RELATIONSHIP_TYPE,
|
|
690
|
-
ENDNOTES_PART_PATH,
|
|
691
|
-
);
|
|
692
|
-
const endnotesRelationshipId = documentPart.relationships.find(
|
|
693
|
-
(r) => r.type === ENDNOTES_RELATIONSHIP_TYPE && r.targetMode === "internal",
|
|
694
|
-
)?.id;
|
|
695
|
-
|
|
696
|
-
let footnoteCollection: FootnoteCollection | undefined;
|
|
697
|
-
if (footnotesPartPath) {
|
|
698
|
-
footnoteCollection = parseFootnotesXml(
|
|
699
|
-
decodeUtf8(sourcePackage.parts.get(footnotesPartPath)?.bytes ?? new Uint8Array()),
|
|
700
|
-
);
|
|
701
|
-
normalizeFootnoteCollectionOpaqueBlocks(
|
|
702
|
-
footnoteCollection,
|
|
703
|
-
"footnote",
|
|
704
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
705
|
-
normalizedDocument.diagnostics.warnings,
|
|
706
|
-
footnotesPartPath,
|
|
707
|
-
subPartOpaqueState,
|
|
708
|
-
);
|
|
709
|
-
}
|
|
710
|
-
if (endnotesPartPath) {
|
|
711
|
-
footnoteCollection = parseEndnotesXml(
|
|
712
|
-
decodeUtf8(sourcePackage.parts.get(endnotesPartPath)?.bytes ?? new Uint8Array()),
|
|
713
|
-
footnoteCollection,
|
|
714
|
-
);
|
|
715
|
-
normalizeFootnoteCollectionOpaqueBlocks(
|
|
716
|
-
footnoteCollection,
|
|
717
|
-
"endnote",
|
|
718
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
719
|
-
normalizedDocument.diagnostics.warnings,
|
|
720
|
-
endnotesPartPath,
|
|
721
|
-
subPartOpaqueState,
|
|
722
|
-
);
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
const themeRelationship = documentPart.relationships.find(
|
|
726
|
-
(r) => r.type === "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" &&
|
|
727
|
-
r.targetMode === "internal",
|
|
728
|
-
);
|
|
729
|
-
const themePartPath = themeRelationship
|
|
730
|
-
? resolveRelationshipTarget(mainDocumentPath, themeRelationship)
|
|
731
|
-
: undefined;
|
|
732
|
-
const parsedTheme =
|
|
733
|
-
themePartPath && sourcePackage.parts.has(themePartPath)
|
|
734
|
-
? parseThemeXml(
|
|
735
|
-
decodeUtf8(sourcePackage.parts.get(themePartPath)?.bytes ?? new Uint8Array()),
|
|
736
|
-
)
|
|
737
|
-
: undefined;
|
|
738
|
-
const resolvedTheme = parsedTheme ? resolveTheme(parsedTheme) : undefined;
|
|
739
|
-
const settingsPartPath = resolveDocumentRelatedPartPath(
|
|
740
|
-
sourcePackage,
|
|
741
|
-
mainDocumentPath,
|
|
742
|
-
documentPart.relationships,
|
|
743
|
-
SETTINGS_RELATIONSHIP_TYPE,
|
|
744
|
-
SETTINGS_PART_PATH,
|
|
745
|
-
);
|
|
746
|
-
const parsedSettings =
|
|
747
|
-
settingsPartPath && sourcePackage.parts.has(settingsPartPath)
|
|
748
|
-
? parseSettingsXml(
|
|
749
|
-
decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array()),
|
|
750
|
-
)
|
|
751
|
-
: undefined;
|
|
752
|
-
const canonicalTheme =
|
|
753
|
-
parsedTheme !== undefined
|
|
754
|
-
? materializeCanonicalTheme(
|
|
755
|
-
parsedTheme,
|
|
756
|
-
parsedSettings?.clrSchemeMapping ?? {},
|
|
757
|
-
)
|
|
758
|
-
: undefined;
|
|
759
|
-
const settingsXmlForProtection =
|
|
760
|
-
settingsPartPath && sourcePackage.parts.has(settingsPartPath)
|
|
761
|
-
? decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array())
|
|
762
|
-
: "";
|
|
763
|
-
const documentProtection = extractDocumentProtection(settingsXmlForProtection);
|
|
764
|
-
const importedProtectionSnapshot = buildProtectionSnapshot(documentProtection, protectionRanges);
|
|
765
|
-
|
|
766
|
-
// ---- Parse styles.xml for canonical style catalog ----
|
|
767
|
-
const stylesPartPath = resolveDocumentRelatedPartPath(
|
|
768
|
-
sourcePackage,
|
|
769
|
-
mainDocumentPath,
|
|
770
|
-
documentPart.relationships,
|
|
771
|
-
STYLES_RELATIONSHIP_TYPE,
|
|
772
|
-
STYLES_PART_PATH,
|
|
773
|
-
);
|
|
774
|
-
const parsedStyles =
|
|
775
|
-
stylesPartPath && sourcePackage.parts.has(stylesPartPath)
|
|
776
|
-
? parseStylesXml(
|
|
777
|
-
decodeUtf8(sourcePackage.parts.get(stylesPartPath)?.bytes ?? new Uint8Array()),
|
|
778
|
-
)
|
|
779
|
-
: parseStylesXml("");
|
|
780
|
-
|
|
781
|
-
// ---- Parse fontTable.xml for canonical font catalog ----
|
|
782
|
-
const fontTablePartPath = resolveDocumentRelatedPartPath(
|
|
783
|
-
sourcePackage,
|
|
784
|
-
mainDocumentPath,
|
|
785
|
-
documentPart.relationships,
|
|
786
|
-
FONT_TABLE_RELATIONSHIP_TYPE,
|
|
787
|
-
FONT_TABLE_PART_PATH,
|
|
788
|
-
);
|
|
789
|
-
const parsedFontTable =
|
|
790
|
-
fontTablePartPath && sourcePackage.parts.has(fontTablePartPath)
|
|
791
|
-
? parseFontTable(
|
|
792
|
-
decodeUtf8(sourcePackage.parts.get(fontTablePartPath)?.bytes ?? new Uint8Array()),
|
|
793
|
-
)
|
|
794
|
-
: undefined;
|
|
795
|
-
|
|
796
|
-
const mergedMedia = mergeSecondaryStoryMediaCatalog(normalizedDocument.media, {
|
|
797
|
-
headers: parsedHeaders,
|
|
798
|
-
footers: parsedFooters,
|
|
799
|
-
footnoteCollection,
|
|
800
|
-
mediaParts,
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
const subParts: SubPartsCatalog | undefined =
|
|
804
|
-
parsedHeaders.length > 0 ||
|
|
805
|
-
parsedFooters.length > 0 ||
|
|
806
|
-
footnoteCollection !== undefined ||
|
|
807
|
-
parsedTheme !== undefined ||
|
|
808
|
-
normalizedDocument.finalSectionProperties !== undefined ||
|
|
809
|
-
resolvedTheme !== undefined ||
|
|
810
|
-
canonicalTheme !== undefined ||
|
|
811
|
-
parsedSettings !== undefined
|
|
812
|
-
? {
|
|
813
|
-
headers: parsedHeaders,
|
|
814
|
-
footers: parsedFooters,
|
|
815
|
-
...(footnoteCollection !== undefined ? { footnoteCollection } : {}),
|
|
816
|
-
...(parsedTheme !== undefined ? { theme: parsedTheme } : {}),
|
|
817
|
-
...(normalizedDocument.finalSectionProperties !== undefined
|
|
818
|
-
? { finalSectionProperties: normalizedDocument.finalSectionProperties }
|
|
819
|
-
: {}),
|
|
820
|
-
...(resolvedTheme !== undefined ? { resolvedTheme } : {}),
|
|
821
|
-
...(canonicalTheme !== undefined ? { canonicalTheme } : {}),
|
|
822
|
-
...(parsedSettings !== undefined ? { settings: parsedSettings } : {}),
|
|
823
|
-
}
|
|
824
|
-
: undefined;
|
|
825
|
-
|
|
826
|
-
const timestamp = new Date().toISOString();
|
|
827
|
-
const translatedWorkflowState = translateClmCommentsToWorkflow({
|
|
828
|
-
comments: normalizedComments.threads,
|
|
829
|
-
workflowOverlay: embeddedWorkflowOverlay,
|
|
830
|
-
workflowMetadata: embeddedWorkflowMetadata,
|
|
831
|
-
timestamp,
|
|
832
|
-
});
|
|
833
|
-
const document = createImportedCanonicalDocument({
|
|
834
|
-
documentId: options.documentId,
|
|
835
|
-
timestamp,
|
|
836
|
-
numbering: parsedNumbering,
|
|
837
|
-
media: mergedMedia,
|
|
838
|
-
content: normalizedDocument.content,
|
|
839
|
-
subParts,
|
|
840
|
-
parsedStyles,
|
|
841
|
-
fontTable: parsedFontTable,
|
|
842
|
-
preservation: {
|
|
843
|
-
...normalizedDocument.preservation,
|
|
844
|
-
packageParts: {
|
|
845
|
-
...normalizedDocument.preservation.packageParts,
|
|
846
|
-
...collectPreservedPackageParts(sourcePackage, [
|
|
847
|
-
mainDocumentPath,
|
|
848
|
-
numberingPartPath,
|
|
849
|
-
commentsPartPath,
|
|
850
|
-
commentsExtendedPartPath,
|
|
851
|
-
commentsIdsPartPath,
|
|
852
|
-
peoplePartPath,
|
|
853
|
-
]),
|
|
854
|
-
},
|
|
855
|
-
},
|
|
856
|
-
diagnostics: {
|
|
857
|
-
warnings: [
|
|
858
|
-
...createBrokenRelationshipWarnings(sourcePackage, mainDocumentPath),
|
|
859
|
-
...normalizedDocument.diagnostics.warnings,
|
|
860
|
-
...normalizedRevisions.diagnostics.map((diagnostic, index) => ({
|
|
861
|
-
diagnosticId: `diagnostic:revision-import-${index + 1}`,
|
|
862
|
-
warningId: `warning:revision-import-${diagnostic.revisionId}`,
|
|
863
|
-
source: "review" as const,
|
|
864
|
-
message: diagnostic.message,
|
|
865
|
-
})),
|
|
866
|
-
...importedStoryRevisionDiagnostics.map((diagnostic, index) => ({
|
|
867
|
-
diagnosticId: `diagnostic:story-revision-import-${index + 1}`,
|
|
868
|
-
warningId: `warning:story-revision-import-${diagnostic.revisionId}`,
|
|
869
|
-
source: "review" as const,
|
|
870
|
-
message: diagnostic.message,
|
|
871
|
-
})),
|
|
872
|
-
...normalizedComments.diagnostics.map((diagnostic, index) => ({
|
|
873
|
-
diagnosticId: `diagnostic:comment-import-${index + 1}`,
|
|
874
|
-
warningId: `warning:comment-import-${diagnostic.commentId}`,
|
|
875
|
-
source: "review" as const,
|
|
876
|
-
message: diagnostic.message,
|
|
877
|
-
})),
|
|
878
|
-
],
|
|
879
|
-
errors: [],
|
|
880
|
-
},
|
|
881
|
-
review: {
|
|
882
|
-
comments: toRuntimeCommentRecords(translatedWorkflowState.comments),
|
|
883
|
-
revisions: toRuntimeRevisionRecords([
|
|
884
|
-
...normalizedRevisions.revisions,
|
|
885
|
-
...importedStoryRevisions,
|
|
886
|
-
]),
|
|
887
|
-
},
|
|
888
|
-
});
|
|
889
|
-
const compatibility = buildCompatibilityReport({
|
|
890
|
-
document,
|
|
891
|
-
generatedAt: timestamp,
|
|
892
|
-
});
|
|
893
|
-
const snapshot = createImportedSnapshot({
|
|
894
|
-
documentId: options.documentId,
|
|
895
|
-
editorBuild,
|
|
896
|
-
timestamp,
|
|
897
|
-
document,
|
|
898
|
-
compatibility: toPublicCompatibilityReport(compatibility),
|
|
899
|
-
protectionSnapshot: importedProtectionSnapshot,
|
|
900
|
-
sourcePackage: createPersistedSourcePackage(sourceBytes, options.sourceLabel),
|
|
901
|
-
workflowOverlay: translatedWorkflowState.workflowOverlay,
|
|
902
|
-
workflowMetadata: translatedWorkflowState.workflowMetadata,
|
|
903
|
-
});
|
|
904
|
-
const snapshotIssues = validatePersistedEditorSnapshot(snapshot);
|
|
905
|
-
if (snapshotIssues.length > 0) {
|
|
906
|
-
const firstIssue = snapshotIssues[0];
|
|
907
|
-
return createDiagnosticsSession(
|
|
908
|
-
options,
|
|
909
|
-
createValidationImportDiagnostics({
|
|
910
|
-
message: `DOCX import produced an invalid editor state during validation${firstIssue ? ` (${firstIssue.path}: ${firstIssue.message})` : "."}`,
|
|
911
|
-
source: "import",
|
|
912
|
-
details: {
|
|
913
|
-
issueCount: snapshotIssues.length,
|
|
914
|
-
firstIssuePath: firstIssue?.path,
|
|
915
|
-
},
|
|
916
|
-
}),
|
|
917
|
-
);
|
|
918
|
-
}
|
|
919
|
-
const initialSessionState = editorSessionStateFromPersistedSnapshot(snapshot);
|
|
920
|
-
const importedState: ImportedDocxState = {
|
|
921
|
-
sourceBytes: new Uint8Array(sourceBytes),
|
|
922
|
-
sourcePackage,
|
|
923
|
-
sourceDocumentXml,
|
|
924
|
-
sourceDocumentPartPath: mainDocumentPath,
|
|
925
|
-
sourceDocumentRelationships: documentPart.relationships,
|
|
926
|
-
sourceDocumentAttributes: extractDocumentRootAttributes(sourceDocumentXml),
|
|
927
|
-
sourceNumberingPartPath: numberingPartPath,
|
|
928
|
-
sourceNumberingRelationshipId: documentPart.relationships.find(
|
|
929
|
-
(relationship) =>
|
|
930
|
-
relationship.type === NUMBERING_RELATIONSHIP_TYPE &&
|
|
931
|
-
relationship.targetMode === "internal",
|
|
932
|
-
)?.id,
|
|
933
|
-
sourceSettingsPartPath: settingsPartPath,
|
|
934
|
-
sourceSettingsXml:
|
|
935
|
-
settingsXmlForProtection.length > 0 ? settingsXmlForProtection : undefined,
|
|
936
|
-
sourceCommentsPartPath: commentsPartPath,
|
|
937
|
-
sourceCommentsRelationshipId: documentPart.relationships.find(
|
|
938
|
-
(relationship) =>
|
|
939
|
-
relationship.type === COMMENTS_RELATIONSHIP_TYPE &&
|
|
940
|
-
relationship.targetMode === "internal",
|
|
941
|
-
)?.id,
|
|
942
|
-
sourceCommentsRootTag: normalizedComments.sourceRootTag,
|
|
943
|
-
sourceCommentsExtendedPartPath: commentsExtendedPartPath,
|
|
944
|
-
sourceCommentsExtendedRelationshipId: documentPart.relationships.find(
|
|
945
|
-
(relationship) =>
|
|
946
|
-
relationship.type === COMMENTS_EXTENDED_RELATIONSHIP_TYPE &&
|
|
947
|
-
relationship.targetMode === "internal",
|
|
948
|
-
)?.id,
|
|
949
|
-
sourceCommentsExtendedRootTag: normalizedComments.sourceExtendedRootTag,
|
|
950
|
-
sourceCommentsIdsPartPath: commentsIdsPartPath,
|
|
951
|
-
sourceCommentsIdsRelationshipId: documentPart.relationships.find(
|
|
952
|
-
(relationship) =>
|
|
953
|
-
relationship.type === COMMENTS_IDS_RELATIONSHIP_TYPE &&
|
|
954
|
-
relationship.targetMode === "internal",
|
|
955
|
-
)?.id,
|
|
956
|
-
sourceCommentsIdsRootTag: normalizedComments.sourceIdsRootTag,
|
|
957
|
-
sourcePeoplePartPath: peoplePartPath,
|
|
958
|
-
sourcePeopleRelationshipId: documentPart.relationships.find(
|
|
959
|
-
(relationship) =>
|
|
960
|
-
relationship.type === PEOPLE_RELATIONSHIP_TYPE &&
|
|
961
|
-
relationship.targetMode === "internal",
|
|
962
|
-
)?.id,
|
|
963
|
-
sourcePeopleRootTag: normalizedComments.sourcePeopleRootTag,
|
|
964
|
-
sourcePeopleAuthors: normalizedComments.peopleAuthors,
|
|
965
|
-
protectionSnapshot: buildProtectionSnapshot(documentProtection, protectionRanges),
|
|
966
|
-
preservedCommentDefinitions: normalizedComments.preservedDefinitions,
|
|
967
|
-
blockingCommentDiagnostics: normalizedComments.diagnostics.filter((diagnostic) =>
|
|
968
|
-
BLOCKING_COMMENT_DIAGNOSTIC_CODES.has(diagnostic.code),
|
|
969
|
-
),
|
|
970
|
-
initialCanonicalSignature: serializeCanonicalDocumentForExport(document),
|
|
971
|
-
sourceSubPartPaths: {
|
|
972
|
-
headers: sourceHeaderPaths,
|
|
973
|
-
footers: sourceFooterPaths,
|
|
974
|
-
footnotesPartPath,
|
|
975
|
-
footnotesRelationshipId,
|
|
976
|
-
endnotesPartPath,
|
|
977
|
-
endnotesRelationshipId,
|
|
978
|
-
themePartPath,
|
|
979
|
-
themeRelationshipId: themeRelationship?.id,
|
|
980
|
-
},
|
|
981
|
-
};
|
|
982
|
-
|
|
983
|
-
stages.emit("skeleton-ready");
|
|
984
|
-
return {
|
|
985
|
-
initialSessionState,
|
|
986
|
-
initialSnapshot: snapshot,
|
|
987
|
-
readOnly: false,
|
|
988
|
-
protectionSnapshot: importedProtectionSnapshot,
|
|
989
|
-
exportDocx: async (nextSessionState, exportOptions) =>
|
|
990
|
-
exportDocxEditorSession(importedState, nextSessionState, exportOptions),
|
|
991
|
-
...(embeddedWorkflowPayload?.editorState
|
|
992
|
-
? { initialEditorStatePayload: embeddedWorkflowPayload.editorState }
|
|
993
|
-
: {}),
|
|
994
|
-
};
|
|
995
|
-
} catch (error) {
|
|
996
|
-
return createDiagnosticsSession(
|
|
997
|
-
options,
|
|
998
|
-
createImportDiagnosticsFromError(error),
|
|
999
|
-
);
|
|
1000
|
-
}
|
|
1001
|
-
}
|
|
1002
|
-
|
|
1003
|
-
export interface LoadDocxEditorSessionAsyncOptions extends LoadDocxEditorSessionOptions {
|
|
1004
|
-
/**
|
|
1005
|
-
* Scheduler that the async loader awaits between parse stages. Callers
|
|
1006
|
-
* in DOM environments should construct this with `createLoadScheduler()`
|
|
1007
|
-
* (auto-detects scheduler.yield → MessageChannel → setTimeout backend).
|
|
1008
|
-
* Callers in Node / SSR should pass
|
|
1009
|
-
* `createLoadScheduler({ backendOverride: "sync" })` — sync backend
|
|
1010
|
-
* yields resolve without task-boundary latency, so the async path
|
|
1011
|
-
* behaves like the sync path from the test harness POV.
|
|
1012
|
-
*/
|
|
1013
|
-
scheduler: LoadScheduler;
|
|
1014
|
-
/**
|
|
1015
|
-
* L7 Phase 2.5 Plan B B.6b — optional laycache envelope. When supplied,
|
|
1016
|
-
* `loadDocxEditorSessionAsync` still performs the cheap OPC read +
|
|
1017
|
-
* workflow-payload parse (needed for `initialEditorStatePayload`,
|
|
1018
|
-
* `workflowOverlay`, and `workflowMetadata`), then skips the five
|
|
1019
|
-
* expensive stages — `parseMainDocumentXml`,
|
|
1020
|
-
* `normalizeParsedTextDocumentAsync`, `parseCommentsFromOoxml`,
|
|
1021
|
-
* `parseStylesXml`, and `createImportedCanonicalDocument` — and uses
|
|
1022
|
-
* `envelope.canonicalDocument` directly (reference-equal, no clone).
|
|
1023
|
-
*
|
|
1024
|
-
* Callers obtain a validated envelope by calling
|
|
1025
|
-
* `tryReadLaycacheEnvelope(bytes)` before invoking this function; when
|
|
1026
|
-
* the probe returns `null`, omit this field and the loader runs the
|
|
1027
|
-
* full parse.
|
|
1028
|
-
*
|
|
1029
|
-
* Async-only — the sync `loadDocxEditorSession` does not honor this
|
|
1030
|
-
* option. `buildCompatibilityReport` and
|
|
1031
|
-
* `resolveChartPreviewsForDocument` still run on the short-circuit
|
|
1032
|
-
* path because their outputs are required by downstream consumers.
|
|
1033
|
-
*/
|
|
1034
|
-
laycacheEnvelope?: CacheEnvelope;
|
|
1035
|
-
/**
|
|
1036
|
-
* L7 Phase 2 Finale C2 — progressive initial mount. When supplied, the
|
|
1037
|
-
* async loader fires this callback exactly once, after the body stage
|
|
1038
|
-
* completes but before styles / sub-parts / compatibility / snapshot
|
|
1039
|
-
* assembly, with a viewport-windowed `EditorSurfaceSnapshot`. The
|
|
1040
|
-
* callback lets UI consumers show the first page's text before the
|
|
1041
|
-
* rest of the load finishes, measurably shrinking perceived cold-open.
|
|
1042
|
-
*
|
|
1043
|
-
* The progressive surface is synthesized from a provisional
|
|
1044
|
-
* `CanonicalDocumentEnvelope`: `content` is the normalized body; all
|
|
1045
|
-
* other catalogs (styles, numbering, media, review, preservation) are
|
|
1046
|
-
* empty. Viewport blocks render live; blocks beyond the viewport
|
|
1047
|
-
* render as placeholders via the existing `cullBuild` flag. Consumers
|
|
1048
|
-
* must treat the progressive surface as provisional — the final
|
|
1049
|
-
* `LoadedDocxEditorSession` (returned from the same `await`) carries
|
|
1050
|
-
* the real styled envelope.
|
|
1051
|
-
*
|
|
1052
|
-
* The callback receives `blocksRealized` (the viewport window size)
|
|
1053
|
-
* and `blocksTotal` (total block count at body-stage time) so the
|
|
1054
|
-
* consumer can size its viewport-commit telemetry.
|
|
1055
|
-
*
|
|
1056
|
-
* Optional. When absent, the async loader does not perform the
|
|
1057
|
-
* provisional-envelope synthesis — no cold-path regression for
|
|
1058
|
-
* consumers that do not opt in.
|
|
1059
|
-
*
|
|
1060
|
-
* Omitted on the Plan B short-circuit path (laycacheEnvelope !== undefined):
|
|
1061
|
-
* the short-circuit is already fast enough that a progressive pre-commit
|
|
1062
|
-
* adds more overhead than it saves.
|
|
1063
|
-
*/
|
|
1064
|
-
onProgressiveSnapshot?: (partial: {
|
|
1065
|
-
surface: EditorSurfaceSnapshot;
|
|
1066
|
-
phase: "viewport";
|
|
1067
|
-
blocksRealized: number;
|
|
1068
|
-
blocksTotal: number;
|
|
1069
|
-
}) => void;
|
|
1070
|
-
/**
|
|
1071
|
-
* C3b — deferred chart preview resolution. When supplied, chart preview
|
|
1072
|
-
* rendering (`renderChartPreview()` calls) is removed from the critical
|
|
1073
|
-
* load path: the session returns without chart previews, and this
|
|
1074
|
-
* callback fires later (via requestIdleCallback / setTimeout) with the
|
|
1075
|
-
* CanonicalDocument that has all chart previews resolved. Expected win:
|
|
1076
|
-
* 30–80 ms on extra-large warm-path docs with ~12 charts.
|
|
1077
|
-
*
|
|
1078
|
-
* Consumers that need chart previews on first render should not supply
|
|
1079
|
-
* this callback — the blocking path remains available by omitting it.
|
|
1080
|
-
*/
|
|
1081
|
-
onChartPreviewsReady?: (resolvedDoc: CanonicalDocument) => void;
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
/**
|
|
1085
|
-
* Fastload P6: async sibling of {@link loadDocxEditorSession} that yields to
|
|
1086
|
-
* the browser between parse stages. Parse sequence is byte-equivalent to
|
|
1087
|
-
* the sync path (asserted on every F*.docx fixture in
|
|
1088
|
-
* `test/io/fastload-parity.test.ts`). Yields fire at:
|
|
1089
|
-
* 1. after OPC read,
|
|
1090
|
-
* 2. after `parseMainDocumentXml`,
|
|
1091
|
-
* 3. between every header/footer sub-part parse,
|
|
1092
|
-
* 4. after footnotes/endnotes parse,
|
|
1093
|
-
* 5. after theme + settings + styles parse,
|
|
1094
|
-
* 6. after `buildCompatibilityReport`,
|
|
1095
|
-
* plus mid-walk yields every 256 blocks inside
|
|
1096
|
-
* `normalizeParsedTextDocumentAsync` (stage 3 — body normalize).
|
|
1097
|
-
*
|
|
1098
|
-
* Sync `loadDocxEditorSession` remains the only entry point for Node tests
|
|
1099
|
-
* and SSR. The DOM boundary in `editor-runtime-boundary.ts` calls this
|
|
1100
|
-
* async path so the browser can paint the skeleton mid-parse.
|
|
1101
|
-
*/
|
|
1102
|
-
|
|
1103
|
-
/**
|
|
1104
|
-
* L7 Phase 2 Finale C2 — progressive initial mount viewport window size.
|
|
1105
|
-
*
|
|
1106
|
-
* Sized to cover the first page of a typical paginated document (~20
|
|
1107
|
-
* body blocks = ~1 page on CCEP-scale templates at default margins). The
|
|
1108
|
-
* window is intentionally small so the provisional-envelope synthesis +
|
|
1109
|
-
* surface projection stays under ~30 ms — making `firstViewportCommit`
|
|
1110
|
-
* significantly faster than the full load.
|
|
1111
|
-
*
|
|
1112
|
-
* Blocks beyond this window render as `placeholder-culled` entries via
|
|
1113
|
-
* Phase 2.9's cullBuild flag (auto-derived from `!isInViewport`) — they
|
|
1114
|
-
* preserve `from`/`to` offsets for selection stability while costing ~0
|
|
1115
|
-
* style-cascade work.
|
|
1116
|
-
*/
|
|
1117
|
-
const PROGRESSIVE_VIEWPORT_BLOCKS = 20;
|
|
1118
|
-
|
|
1119
|
-
export async function loadDocxEditorSessionAsync(
|
|
1120
|
-
options: LoadDocxEditorSessionAsyncOptions,
|
|
1121
|
-
): Promise<LoadedDocxEditorSession> {
|
|
1122
|
-
const { scheduler } = options;
|
|
1123
|
-
const editorBuild =
|
|
1124
|
-
typeof options.editorBuild === "string" && options.editorBuild.length > 0
|
|
1125
|
-
? options.editorBuild
|
|
1126
|
-
: "dev";
|
|
1127
|
-
const sourceBytes = toUint8Array(options.bytes);
|
|
1128
|
-
|
|
1129
|
-
const stages = createStageEmitter(options.onLoadStage);
|
|
1130
|
-
|
|
1131
|
-
let sourcePackage: OpcPackage;
|
|
1132
|
-
|
|
1133
|
-
try {
|
|
1134
|
-
sourcePackage = readOpcPackage(sourceBytes);
|
|
1135
|
-
} catch (error) {
|
|
1136
|
-
return createDiagnosticsSession(
|
|
1137
|
-
options,
|
|
1138
|
-
createPackageImportDiagnostics({
|
|
1139
|
-
issue: classifyCorruptPackageError(error),
|
|
1140
|
-
}),
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
stages.emit("opc");
|
|
1144
|
-
await scheduler.yield();
|
|
1145
|
-
const embeddedWorkflowPayload = parseWorkflowPayloadEnvelopeFromPackage(sourcePackage);
|
|
1146
|
-
const embeddedWorkflowMetadata = embeddedWorkflowPayload?.workflowMetadata;
|
|
1147
|
-
const embeddedWorkflowOverlay = embeddedWorkflowPayload?.workflowOverlay;
|
|
1148
|
-
|
|
1149
|
-
const mainDocumentPath = resolveMainDocumentPartPath(sourcePackage);
|
|
1150
|
-
const brokenRelationshipIssues = collectBrokenInternalRelationshipIssues(
|
|
1151
|
-
sourcePackage,
|
|
1152
|
-
mainDocumentPath,
|
|
1153
|
-
);
|
|
1154
|
-
if (brokenRelationshipIssues.length > 0) {
|
|
1155
|
-
return createDiagnosticsSession(
|
|
1156
|
-
options,
|
|
1157
|
-
createPackageImportDiagnostics({
|
|
1158
|
-
issue: {
|
|
1159
|
-
...brokenRelationshipIssues[0],
|
|
1160
|
-
message: summarizeBrokenRelationshipIssues(brokenRelationshipIssues),
|
|
1161
|
-
details: {
|
|
1162
|
-
issueCount: brokenRelationshipIssues.length,
|
|
1163
|
-
targets: brokenRelationshipIssues.map((issue) => issue.targetPartPath).filter(Boolean),
|
|
1164
|
-
},
|
|
1165
|
-
},
|
|
1166
|
-
}),
|
|
1167
|
-
);
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
if (!mainDocumentPath) {
|
|
1171
|
-
return createDiagnosticsSession(
|
|
1172
|
-
options,
|
|
1173
|
-
createPackageImportDiagnostics({
|
|
1174
|
-
issue: createMissingPartIssue(MAIN_DOCUMENT_PATH),
|
|
1175
|
-
}),
|
|
1176
|
-
);
|
|
1177
|
-
}
|
|
1178
|
-
|
|
1179
|
-
const documentPart = sourcePackage.parts.get(mainDocumentPath);
|
|
1180
|
-
if (!documentPart) {
|
|
1181
|
-
return createDiagnosticsSession(
|
|
1182
|
-
options,
|
|
1183
|
-
createPackageImportDiagnostics({
|
|
1184
|
-
issue: createMissingPartIssue(mainDocumentPath),
|
|
1185
|
-
}),
|
|
1186
|
-
);
|
|
1187
|
-
}
|
|
1188
|
-
if (documentPart.contentType !== MAIN_DOCUMENT_CONTENT_TYPE) {
|
|
1189
|
-
return createDiagnosticsSession(
|
|
1190
|
-
options,
|
|
1191
|
-
createValidationImportDiagnostics({
|
|
1192
|
-
message: `DOCX main document part ${mainDocumentPath} must use content type ${MAIN_DOCUMENT_CONTENT_TYPE}.`,
|
|
1193
|
-
}),
|
|
1194
|
-
);
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
try {
|
|
1198
|
-
// L7 Phase 2.5 Plan B B.6b — loader short-circuit. Hand
|
|
1199
|
-
// `envelope.canonicalDocument` through reference-equal and skip the
|
|
1200
|
-
// five expensive parse stages. The four `onLoadStage` callbacks still
|
|
1201
|
-
// fire in order — `body` and `styles-numbering-comments` emit with
|
|
1202
|
-
// near-zero duration — so host progress bars are unaffected.
|
|
1203
|
-
if (options.laycacheEnvelope) {
|
|
1204
|
-
stages.emit("body");
|
|
1205
|
-
stages.emit("styles-numbering-comments");
|
|
1206
|
-
|
|
1207
|
-
const canonicalDocument = options.laycacheEnvelope.canonicalDocument;
|
|
1208
|
-
|
|
1209
|
-
// `extractProtectionRanges` needs `parsedDocument.blocks` (which we
|
|
1210
|
-
// are skipping), so the short-circuit uses an empty ranges list;
|
|
1211
|
-
// document-level `editType` / `enforcement` still come from
|
|
1212
|
-
// settings.xml so read-only docs stay read-only.
|
|
1213
|
-
const settingsPartPath = resolveDocumentRelatedPartPath(
|
|
1214
|
-
sourcePackage,
|
|
1215
|
-
mainDocumentPath,
|
|
1216
|
-
documentPart.relationships,
|
|
1217
|
-
SETTINGS_RELATIONSHIP_TYPE,
|
|
1218
|
-
SETTINGS_PART_PATH,
|
|
1219
|
-
);
|
|
1220
|
-
const settingsXml =
|
|
1221
|
-
settingsPartPath && sourcePackage.parts.has(settingsPartPath)
|
|
1222
|
-
? decodeUtf8(
|
|
1223
|
-
sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array(),
|
|
1224
|
-
)
|
|
1225
|
-
: "";
|
|
1226
|
-
const documentProtection = extractDocumentProtection(settingsXml);
|
|
1227
|
-
const protectionSnapshot = buildProtectionSnapshot(documentProtection, []);
|
|
1228
|
-
|
|
1229
|
-
// Chart previews (`previewMediaId` is host-dependent) aren't cached
|
|
1230
|
-
// in the envelope. C3b: when onChartPreviewsReady is provided, defer
|
|
1231
|
-
// resolution out of the critical path. Otherwise block (legacy behavior).
|
|
1232
|
-
let documentWithChartPreviews: CanonicalDocumentEnvelope;
|
|
1233
|
-
if (options.onChartPreviewsReady) {
|
|
1234
|
-
scheduleChartPreviewResolution(
|
|
1235
|
-
canonicalDocument,
|
|
1236
|
-
sourcePackage,
|
|
1237
|
-
options.hostAdapter,
|
|
1238
|
-
options.onChartPreviewsReady,
|
|
1239
|
-
);
|
|
1240
|
-
documentWithChartPreviews = canonicalDocument as CanonicalDocumentEnvelope;
|
|
1241
|
-
} else {
|
|
1242
|
-
documentWithChartPreviews = (await resolveChartPreviewsForDocument(
|
|
1243
|
-
canonicalDocument,
|
|
1244
|
-
sourcePackage,
|
|
1245
|
-
options.hostAdapter,
|
|
1246
|
-
)) as CanonicalDocumentEnvelope;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
const timestamp = new Date().toISOString();
|
|
1250
|
-
// Phase 2 Finale C3: skip `buildCompatibilityReport` (60–100 ms on
|
|
1251
|
-
// extra-large) when the envelope carries a pre-computed report.
|
|
1252
|
-
// Pure-function determinism of the report is enforced by
|
|
1253
|
-
// `canonicalDocumentHash` (5th input to `deriveCacheKey`): any
|
|
1254
|
-
// change to the canonical doc flips the hash and rejects the
|
|
1255
|
-
// envelope on load.
|
|
1256
|
-
//
|
|
1257
|
-
// The cached report's `generatedAt` is a fixed sentinel
|
|
1258
|
-
// (`CACHE_NORMALIZED_GENERATED_AT`) for envelope byte-identity.
|
|
1259
|
-
// Swap it for the live ISO8601 timestamp here because downstream
|
|
1260
|
-
// `validatePersistedEditorSnapshot` requires
|
|
1261
|
-
// `$.compatibility.generatedAt` to be ISO 8601.
|
|
1262
|
-
const cachedReport = options.laycacheEnvelope?.compatibilityReport;
|
|
1263
|
-
const compatibility = cachedReport
|
|
1264
|
-
? { ...cachedReport, generatedAt: timestamp }
|
|
1265
|
-
: buildCompatibilityReport({
|
|
1266
|
-
document: documentWithChartPreviews,
|
|
1267
|
-
generatedAt: timestamp,
|
|
1268
|
-
});
|
|
1269
|
-
await scheduler.yield();
|
|
1270
|
-
|
|
1271
|
-
const snapshot = createImportedSnapshot({
|
|
1272
|
-
documentId: options.documentId,
|
|
1273
|
-
editorBuild,
|
|
1274
|
-
timestamp,
|
|
1275
|
-
document: documentWithChartPreviews,
|
|
1276
|
-
compatibility: toPublicCompatibilityReport(compatibility),
|
|
1277
|
-
protectionSnapshot,
|
|
1278
|
-
sourcePackage: createPersistedSourcePackage(sourceBytes, options.sourceLabel),
|
|
1279
|
-
workflowOverlay: embeddedWorkflowOverlay,
|
|
1280
|
-
workflowMetadata: embeddedWorkflowMetadata,
|
|
1281
|
-
});
|
|
1282
|
-
const snapshotIssues = validatePersistedEditorSnapshot(snapshot);
|
|
1283
|
-
if (snapshotIssues.length > 0) {
|
|
1284
|
-
const firstIssue = snapshotIssues[0];
|
|
1285
|
-
return createDiagnosticsSession(
|
|
1286
|
-
options,
|
|
1287
|
-
createValidationImportDiagnostics({
|
|
1288
|
-
message: `DOCX import produced an invalid editor state during validation${firstIssue ? ` (${firstIssue.path}: ${firstIssue.message})` : "."}`,
|
|
1289
|
-
source: "import",
|
|
1290
|
-
details: {
|
|
1291
|
-
issueCount: snapshotIssues.length,
|
|
1292
|
-
firstIssuePath: firstIssue?.path,
|
|
1293
|
-
},
|
|
1294
|
-
}),
|
|
1295
|
-
);
|
|
1296
|
-
}
|
|
1297
|
-
|
|
1298
|
-
// Build `initialSessionState` inline — bypassing
|
|
1299
|
-
// `editorSessionStateFromPersistedSnapshot`'s structuredClone so
|
|
1300
|
-
// `session.initialSessionState.canonicalDocument` is reference-equal
|
|
1301
|
-
// to `envelope.canonicalDocument` (cloning a large canonical document
|
|
1302
|
-
// defeats part of the cache gain).
|
|
1303
|
-
const sessionState: EditorSessionState = {
|
|
1304
|
-
sessionVersion: "editor-session-state/1",
|
|
1305
|
-
schemaVersion: snapshot.schemaVersion,
|
|
1306
|
-
documentId: snapshot.documentId,
|
|
1307
|
-
docId: snapshot.docId,
|
|
1308
|
-
createdAt: snapshot.createdAt,
|
|
1309
|
-
updatedAt: snapshot.updatedAt,
|
|
1310
|
-
editorBuild: snapshot.editorBuild,
|
|
1311
|
-
canonicalDocument: snapshot.canonicalDocument,
|
|
1312
|
-
compatibility: snapshot.compatibility,
|
|
1313
|
-
warningLog: snapshot.warningLog,
|
|
1314
|
-
protectionSnapshot: snapshot.protectionSnapshot,
|
|
1315
|
-
sourcePackage: snapshot.sourcePackage,
|
|
1316
|
-
workflowOverlay: snapshot.workflowOverlay,
|
|
1317
|
-
workflowMetadata: snapshot.workflowMetadata,
|
|
1318
|
-
};
|
|
1319
|
-
|
|
1320
|
-
// The short-circuit path does not carry an `ImportedDocxState`, so
|
|
1321
|
-
// `exportDocx` lazily re-runs the cold path on first invocation and
|
|
1322
|
-
// memoizes. Keeps the warm-load fast while preserving byte-exact
|
|
1323
|
-
// export correctness.
|
|
1324
|
-
let lazyColdExport: LoadedDocxEditorSession["exportDocx"] | undefined;
|
|
1325
|
-
const exportDocx: LoadedDocxEditorSession["exportDocx"] = async (
|
|
1326
|
-
nextSessionStateOrSnapshot,
|
|
1327
|
-
exportOptions,
|
|
1328
|
-
) => {
|
|
1329
|
-
if (!lazyColdExport) {
|
|
1330
|
-
const { laycacheEnvelope: _unused, ...coldOptions } = options;
|
|
1331
|
-
void _unused;
|
|
1332
|
-
const coldSession = await loadDocxEditorSessionAsync(coldOptions);
|
|
1333
|
-
if (coldSession.fatalError) {
|
|
1334
|
-
throw new Error(
|
|
1335
|
-
`DOCX export via short-circuit fallback failed cold load: ${coldSession.fatalError.message ?? "fatal error"}`,
|
|
1336
|
-
);
|
|
1337
|
-
}
|
|
1338
|
-
lazyColdExport = coldSession.exportDocx;
|
|
1339
|
-
}
|
|
1340
|
-
return lazyColdExport(nextSessionStateOrSnapshot, exportOptions);
|
|
1341
|
-
};
|
|
1342
|
-
|
|
1343
|
-
stages.emit("skeleton-ready");
|
|
1344
|
-
return {
|
|
1345
|
-
initialSessionState: sessionState,
|
|
1346
|
-
initialSnapshot: snapshot,
|
|
1347
|
-
readOnly: false,
|
|
1348
|
-
protectionSnapshot,
|
|
1349
|
-
exportDocx,
|
|
1350
|
-
...(embeddedWorkflowPayload?.editorState
|
|
1351
|
-
? { initialEditorStatePayload: embeddedWorkflowPayload.editorState }
|
|
1352
|
-
: {}),
|
|
1353
|
-
};
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
const sourceDocumentXml = decodeUtf8(documentPart.bytes);
|
|
1357
|
-
const importedRevisions = parseRevisionsFromDocumentXml(sourceDocumentXml);
|
|
1358
|
-
const numberingPartPath = resolveDocumentRelatedPartPath(
|
|
1359
|
-
sourcePackage,
|
|
1360
|
-
mainDocumentPath,
|
|
1361
|
-
documentPart.relationships,
|
|
1362
|
-
NUMBERING_RELATIONSHIP_TYPE,
|
|
1363
|
-
NUMBERING_PART_PATH,
|
|
1364
|
-
);
|
|
1365
|
-
const parsedNumbering = numberingPartPath
|
|
1366
|
-
? parseNumberingXml(
|
|
1367
|
-
decodeUtf8(sourcePackage.parts.get(numberingPartPath)?.bytes ?? new Uint8Array()),
|
|
1368
|
-
{
|
|
1369
|
-
relationships: sourcePackage.parts.get(numberingPartPath)?.relationships,
|
|
1370
|
-
partPath: numberingPartPath,
|
|
1371
|
-
},
|
|
1372
|
-
)
|
|
1373
|
-
: createEmptyNumberingCatalog();
|
|
1374
|
-
const mediaParts = collectInlineMediaParts(sourcePackage);
|
|
1375
|
-
const chartPartLookup = createChartPartLookup(
|
|
1376
|
-
sourcePackage,
|
|
1377
|
-
mainDocumentPath,
|
|
1378
|
-
documentPart.relationships,
|
|
1379
|
-
);
|
|
1380
|
-
const parsedDocument = parseMainDocumentXml(
|
|
1381
|
-
sourceDocumentXml,
|
|
1382
|
-
documentPart.relationships,
|
|
1383
|
-
mediaParts,
|
|
1384
|
-
mainDocumentPath,
|
|
1385
|
-
chartPartLookup,
|
|
1386
|
-
);
|
|
1387
|
-
await scheduler.yield();
|
|
1388
|
-
const protectionRanges = extractProtectionRanges(parsedDocument.blocks);
|
|
1389
|
-
const normalizedDocument = await normalizeParsedTextDocumentAsync(
|
|
1390
|
-
parsedDocument,
|
|
1391
|
-
mainDocumentPath,
|
|
1392
|
-
scheduler,
|
|
1393
|
-
);
|
|
1394
|
-
stages.emit("body");
|
|
1395
|
-
await scheduler.yield();
|
|
1396
|
-
|
|
1397
|
-
// L7 Phase 2 Finale C2 — progressive initial mount.
|
|
1398
|
-
//
|
|
1399
|
-
// Fire `onProgressiveSnapshot` exactly once, after the body stage and
|
|
1400
|
-
// its post-yield. At this point `normalizedDocument.content` carries
|
|
1401
|
-
// the full block tree with per-block runProperties already resolved
|
|
1402
|
-
// during `normalizeParsedTextDocumentAsync`. We synthesize a
|
|
1403
|
-
// throw-away `CanonicalDocumentEnvelope` using the normalized content
|
|
1404
|
-
// + empty style/review/preservation catalogs, then project a
|
|
1405
|
-
// viewport-windowed `EditorSurfaceSnapshot` (first `PROGRESSIVE_VIEWPORT_BLOCKS`
|
|
1406
|
-
// blocks real, rest as culled placeholders via the Phase 2.9 flag).
|
|
1407
|
-
//
|
|
1408
|
-
// The bench's measured signal: time from `loadDocxEditorSessionAsync`
|
|
1409
|
-
// entry to this callback's fire is `firstViewportCommitMs` — the
|
|
1410
|
-
// metric C2 gates on.
|
|
1411
|
-
//
|
|
1412
|
-
// Skipped on the Plan B short-circuit: `laycacheEnvelope !== undefined`
|
|
1413
|
-
// already completes ~376 ms faster than cold — adding a progressive
|
|
1414
|
-
// synthesis on top costs more than it saves. The short-circuit path
|
|
1415
|
-
// returns the real snapshot fast enough.
|
|
1416
|
-
if (
|
|
1417
|
-
options.onProgressiveSnapshot !== undefined &&
|
|
1418
|
-
options.laycacheEnvelope === undefined
|
|
1419
|
-
) {
|
|
1420
|
-
const provisionalDoc: CanonicalDocumentEnvelope = {
|
|
1421
|
-
...createDefaultCanonicalDocument(
|
|
1422
|
-
options.documentId,
|
|
1423
|
-
new Date().toISOString(),
|
|
1424
|
-
),
|
|
1425
|
-
content: normalizedDocument.content,
|
|
1426
|
-
};
|
|
1427
|
-
const blocksTotal = normalizedDocument.content.children.length;
|
|
1428
|
-
const blocksRealized = Math.min(
|
|
1429
|
-
PROGRESSIVE_VIEWPORT_BLOCKS,
|
|
1430
|
-
blocksTotal,
|
|
1431
|
-
);
|
|
1432
|
-
const progressiveSurface = createEditorSurfaceSnapshot(
|
|
1433
|
-
provisionalDoc,
|
|
1434
|
-
createSelectionSnapshot(0, 0),
|
|
1435
|
-
MAIN_STORY_TARGET,
|
|
1436
|
-
blocksRealized < blocksTotal
|
|
1437
|
-
? { viewportBlockRange: { start: 0, end: blocksRealized } }
|
|
1438
|
-
: undefined,
|
|
1439
|
-
);
|
|
1440
|
-
try {
|
|
1441
|
-
options.onProgressiveSnapshot({
|
|
1442
|
-
surface: progressiveSurface,
|
|
1443
|
-
phase: "viewport",
|
|
1444
|
-
blocksRealized,
|
|
1445
|
-
blocksTotal,
|
|
1446
|
-
});
|
|
1447
|
-
} catch {
|
|
1448
|
-
// A throwing consumer must not abort the load. Progressive is
|
|
1449
|
-
// a best-effort optimization; errors on the callback side
|
|
1450
|
-
// silently fall through to the normal full-commit path.
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
|
-
|
|
1454
|
-
const commentsPartPath = resolveCommentsPartPath(
|
|
1455
|
-
sourcePackage,
|
|
1456
|
-
mainDocumentPath,
|
|
1457
|
-
documentPart.relationships,
|
|
1458
|
-
);
|
|
1459
|
-
const commentsExtendedPartPath = resolveDocumentRelatedPartPath(
|
|
1460
|
-
sourcePackage,
|
|
1461
|
-
mainDocumentPath,
|
|
1462
|
-
documentPart.relationships,
|
|
1463
|
-
COMMENTS_EXTENDED_RELATIONSHIP_TYPE,
|
|
1464
|
-
COMMENTS_EXTENDED_PART_PATH,
|
|
1465
|
-
);
|
|
1466
|
-
const commentsIdsPartPath = resolveDocumentRelatedPartPath(
|
|
1467
|
-
sourcePackage,
|
|
1468
|
-
mainDocumentPath,
|
|
1469
|
-
documentPart.relationships,
|
|
1470
|
-
COMMENTS_IDS_RELATIONSHIP_TYPE,
|
|
1471
|
-
COMMENTS_IDS_PART_PATH,
|
|
1472
|
-
);
|
|
1473
|
-
const peoplePartPath = resolveDocumentRelatedPartPath(
|
|
1474
|
-
sourcePackage,
|
|
1475
|
-
mainDocumentPath,
|
|
1476
|
-
documentPart.relationships,
|
|
1477
|
-
PEOPLE_RELATIONSHIP_TYPE,
|
|
1478
|
-
PEOPLE_PART_PATH,
|
|
1479
|
-
);
|
|
1480
|
-
const parsedComments = commentsPartPath
|
|
1481
|
-
? parseCommentsFromOoxml(
|
|
1482
|
-
sourceDocumentXml,
|
|
1483
|
-
{
|
|
1484
|
-
commentsXml: decodeUtf8(sourcePackage.parts.get(commentsPartPath)?.bytes ?? new Uint8Array()),
|
|
1485
|
-
commentsExtendedXml: decodeUtf8(
|
|
1486
|
-
sourcePackage.parts.get(commentsExtendedPartPath ?? "")?.bytes ?? new Uint8Array(),
|
|
1487
|
-
),
|
|
1488
|
-
commentsIdsXml: decodeUtf8(
|
|
1489
|
-
sourcePackage.parts.get(commentsIdsPartPath ?? "")?.bytes ?? new Uint8Array(),
|
|
1490
|
-
),
|
|
1491
|
-
peopleXml: decodeUtf8(
|
|
1492
|
-
sourcePackage.parts.get(peoplePartPath ?? "")?.bytes ?? new Uint8Array(),
|
|
1493
|
-
),
|
|
1494
|
-
},
|
|
1495
|
-
)
|
|
1496
|
-
: {
|
|
1497
|
-
threads: [] as CommentThread[],
|
|
1498
|
-
diagnostics: [] as CommentImportDiagnostic[],
|
|
1499
|
-
definitions: [] as ImportedCommentDefinition[],
|
|
1500
|
-
sourceRootTag: undefined,
|
|
1501
|
-
sourceExtendedRootTag: undefined,
|
|
1502
|
-
sourceIdsRootTag: undefined,
|
|
1503
|
-
sourcePeopleRootTag: undefined,
|
|
1504
|
-
peopleAuthors: [] as string[],
|
|
1505
|
-
};
|
|
1506
|
-
const normalizedRevisions = normalizeImportedRevisionRecords(
|
|
1507
|
-
importedRevisions,
|
|
1508
|
-
normalizedDocument.content,
|
|
1509
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
1510
|
-
);
|
|
1511
|
-
const normalizedComments = normalizeImportedCommentThreads(
|
|
1512
|
-
parsedComments,
|
|
1513
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
1514
|
-
normalizedRevisions.revisions,
|
|
1515
|
-
);
|
|
1516
|
-
stages.emit("styles-numbering-comments");
|
|
1517
|
-
const importedStoryRevisions: ReviewRevisionRecord[] = [];
|
|
1518
|
-
const importedStoryRevisionDiagnostics: ParsedRevisionsResult["diagnostics"] = [];
|
|
1519
|
-
const subPartOpaqueState = createSubPartOpaqueImportState(
|
|
1520
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
1521
|
-
normalizedDocument.diagnostics.warnings,
|
|
1522
|
-
);
|
|
1523
|
-
// ---- Parse sub-parts: headers, footers, footnotes, endnotes, theme ----
|
|
1524
|
-
const headerFooterRefs = parseHeaderFooterReferences(sourceDocumentXml);
|
|
1525
|
-
const parsedHeaders: HeaderDocument[] = [];
|
|
1526
|
-
const parsedFooters: FooterDocument[] = [];
|
|
1527
|
-
const sourceHeaderPaths: Array<{ partPath: string; relationshipId: string }> = [];
|
|
1528
|
-
const sourceFooterPaths: Array<{ partPath: string; relationshipId: string }> = [];
|
|
1529
|
-
const seenSubPartKeys = new Set<string>();
|
|
1530
|
-
|
|
1531
|
-
for (const ref of headerFooterRefs) {
|
|
1532
|
-
const dedupeKey = `${ref.kind}:${ref.variant}:${ref.relationshipId}`;
|
|
1533
|
-
if (seenSubPartKeys.has(dedupeKey)) {
|
|
1534
|
-
continue;
|
|
1535
|
-
}
|
|
1536
|
-
seenSubPartKeys.add(dedupeKey);
|
|
1537
|
-
|
|
1538
|
-
const relationship = documentPart.relationships.find(
|
|
1539
|
-
(r) => r.id === ref.relationshipId && r.targetMode === "internal",
|
|
1540
|
-
);
|
|
1541
|
-
if (!relationship) {
|
|
1542
|
-
continue;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
const partPath = resolveRelationshipTarget(mainDocumentPath, relationship);
|
|
1546
|
-
const part = sourcePackage.parts.get(partPath);
|
|
1547
|
-
const partBytes = part?.bytes;
|
|
1548
|
-
if (!partBytes) {
|
|
1549
|
-
continue;
|
|
1550
|
-
}
|
|
1551
|
-
|
|
1552
|
-
await scheduler.yield();
|
|
1553
|
-
const xml = decodeUtf8(partBytes);
|
|
1554
|
-
const subPartRelationships = part?.relationships ?? [];
|
|
1555
|
-
const subPartChartPartLookup = createChartPartLookup(
|
|
1556
|
-
sourcePackage,
|
|
1557
|
-
partPath,
|
|
1558
|
-
subPartRelationships,
|
|
1559
|
-
);
|
|
1560
|
-
if (ref.kind === "header") {
|
|
1561
|
-
const parsedHeaderRevisions = parseRevisionsFromStoryXml(xml);
|
|
1562
|
-
const parsed = parseHeaderXml(xml, {
|
|
1563
|
-
relationships: subPartRelationships,
|
|
1564
|
-
mediaParts,
|
|
1565
|
-
sourcePartPath: partPath,
|
|
1566
|
-
chartPartLookup: subPartChartPartLookup,
|
|
1567
|
-
});
|
|
1568
|
-
parsedHeaders.push({
|
|
1569
|
-
variant: ref.variant,
|
|
1570
|
-
partPath,
|
|
1571
|
-
relationshipId: ref.relationshipId,
|
|
1572
|
-
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
1573
|
-
blocks: normalizeSubPartOpaqueBlocks(
|
|
1574
|
-
parsed.blocks,
|
|
1575
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
1576
|
-
normalizedDocument.diagnostics.warnings,
|
|
1577
|
-
partPath,
|
|
1578
|
-
subPartOpaqueState,
|
|
1579
|
-
),
|
|
1580
|
-
});
|
|
1581
|
-
importedStoryRevisions.push(
|
|
1582
|
-
...parsedHeaderRevisions.revisions.map((revision): ReviewRevisionRecord => ({
|
|
1583
|
-
...revision,
|
|
1584
|
-
metadata: {
|
|
1585
|
-
...revision.metadata,
|
|
1586
|
-
storyTarget: {
|
|
1587
|
-
kind: "header" as const,
|
|
1588
|
-
relationshipId: ref.relationshipId,
|
|
1589
|
-
variant: ref.variant,
|
|
1590
|
-
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
1591
|
-
},
|
|
1592
|
-
},
|
|
1593
|
-
})),
|
|
1594
|
-
);
|
|
1595
|
-
importedStoryRevisionDiagnostics.push(...parsedHeaderRevisions.diagnostics);
|
|
1596
|
-
sourceHeaderPaths.push({ partPath, relationshipId: ref.relationshipId });
|
|
1597
|
-
} else {
|
|
1598
|
-
const parsedFooterRevisions = parseRevisionsFromStoryXml(xml);
|
|
1599
|
-
const parsed = parseFooterXml(xml, {
|
|
1600
|
-
relationships: subPartRelationships,
|
|
1601
|
-
mediaParts,
|
|
1602
|
-
sourcePartPath: partPath,
|
|
1603
|
-
chartPartLookup: subPartChartPartLookup,
|
|
1604
|
-
});
|
|
1605
|
-
parsedFooters.push({
|
|
1606
|
-
variant: ref.variant,
|
|
1607
|
-
partPath,
|
|
1608
|
-
relationshipId: ref.relationshipId,
|
|
1609
|
-
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
1610
|
-
blocks: normalizeSubPartOpaqueBlocks(
|
|
1611
|
-
parsed.blocks,
|
|
1612
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
1613
|
-
normalizedDocument.diagnostics.warnings,
|
|
1614
|
-
partPath,
|
|
1615
|
-
subPartOpaqueState,
|
|
1616
|
-
),
|
|
1617
|
-
});
|
|
1618
|
-
importedStoryRevisions.push(
|
|
1619
|
-
...parsedFooterRevisions.revisions.map((revision): ReviewRevisionRecord => ({
|
|
1620
|
-
...revision,
|
|
1621
|
-
metadata: {
|
|
1622
|
-
...revision.metadata,
|
|
1623
|
-
storyTarget: {
|
|
1624
|
-
kind: "footer" as const,
|
|
1625
|
-
relationshipId: ref.relationshipId,
|
|
1626
|
-
variant: ref.variant,
|
|
1627
|
-
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
1628
|
-
},
|
|
1629
|
-
},
|
|
1630
|
-
})),
|
|
1631
|
-
);
|
|
1632
|
-
importedStoryRevisionDiagnostics.push(...parsedFooterRevisions.diagnostics);
|
|
1633
|
-
sourceFooterPaths.push({ partPath, relationshipId: ref.relationshipId });
|
|
1634
|
-
}
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
const footnotesPartPath = resolveDocumentRelatedPartPath(
|
|
1638
|
-
sourcePackage,
|
|
1639
|
-
mainDocumentPath,
|
|
1640
|
-
documentPart.relationships,
|
|
1641
|
-
FOOTNOTES_RELATIONSHIP_TYPE,
|
|
1642
|
-
FOOTNOTES_PART_PATH,
|
|
1643
|
-
);
|
|
1644
|
-
const footnotesRelationshipId = documentPart.relationships.find(
|
|
1645
|
-
(r) => r.type === FOOTNOTES_RELATIONSHIP_TYPE && r.targetMode === "internal",
|
|
1646
|
-
)?.id;
|
|
1647
|
-
const endnotesPartPath = resolveDocumentRelatedPartPath(
|
|
1648
|
-
sourcePackage,
|
|
1649
|
-
mainDocumentPath,
|
|
1650
|
-
documentPart.relationships,
|
|
1651
|
-
ENDNOTES_RELATIONSHIP_TYPE,
|
|
1652
|
-
ENDNOTES_PART_PATH,
|
|
1653
|
-
);
|
|
1654
|
-
const endnotesRelationshipId = documentPart.relationships.find(
|
|
1655
|
-
(r) => r.type === ENDNOTES_RELATIONSHIP_TYPE && r.targetMode === "internal",
|
|
1656
|
-
)?.id;
|
|
1657
|
-
|
|
1658
|
-
let footnoteCollection: FootnoteCollection | undefined;
|
|
1659
|
-
if (footnotesPartPath) {
|
|
1660
|
-
footnoteCollection = parseFootnotesXml(
|
|
1661
|
-
decodeUtf8(sourcePackage.parts.get(footnotesPartPath)?.bytes ?? new Uint8Array()),
|
|
1662
|
-
);
|
|
1663
|
-
normalizeFootnoteCollectionOpaqueBlocks(
|
|
1664
|
-
footnoteCollection,
|
|
1665
|
-
"footnote",
|
|
1666
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
1667
|
-
normalizedDocument.diagnostics.warnings,
|
|
1668
|
-
footnotesPartPath,
|
|
1669
|
-
subPartOpaqueState,
|
|
1670
|
-
);
|
|
1671
|
-
}
|
|
1672
|
-
if (endnotesPartPath) {
|
|
1673
|
-
footnoteCollection = parseEndnotesXml(
|
|
1674
|
-
decodeUtf8(sourcePackage.parts.get(endnotesPartPath)?.bytes ?? new Uint8Array()),
|
|
1675
|
-
footnoteCollection,
|
|
1676
|
-
);
|
|
1677
|
-
normalizeFootnoteCollectionOpaqueBlocks(
|
|
1678
|
-
footnoteCollection,
|
|
1679
|
-
"endnote",
|
|
1680
|
-
normalizedDocument.preservation.opaqueFragments,
|
|
1681
|
-
normalizedDocument.diagnostics.warnings,
|
|
1682
|
-
endnotesPartPath,
|
|
1683
|
-
subPartOpaqueState,
|
|
1684
|
-
);
|
|
1685
|
-
}
|
|
1686
|
-
await scheduler.yield();
|
|
1687
|
-
|
|
1688
|
-
const themeRelationship = documentPart.relationships.find(
|
|
1689
|
-
(r) => r.type === "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" &&
|
|
1690
|
-
r.targetMode === "internal",
|
|
1691
|
-
);
|
|
1692
|
-
const themePartPath = themeRelationship
|
|
1693
|
-
? resolveRelationshipTarget(mainDocumentPath, themeRelationship)
|
|
1694
|
-
: undefined;
|
|
1695
|
-
const parsedTheme =
|
|
1696
|
-
themePartPath && sourcePackage.parts.has(themePartPath)
|
|
1697
|
-
? parseThemeXml(
|
|
1698
|
-
decodeUtf8(sourcePackage.parts.get(themePartPath)?.bytes ?? new Uint8Array()),
|
|
1699
|
-
)
|
|
1700
|
-
: undefined;
|
|
1701
|
-
const resolvedTheme = parsedTheme ? resolveTheme(parsedTheme) : undefined;
|
|
1702
|
-
const settingsPartPath = resolveDocumentRelatedPartPath(
|
|
1703
|
-
sourcePackage,
|
|
1704
|
-
mainDocumentPath,
|
|
1705
|
-
documentPart.relationships,
|
|
1706
|
-
SETTINGS_RELATIONSHIP_TYPE,
|
|
1707
|
-
SETTINGS_PART_PATH,
|
|
1708
|
-
);
|
|
1709
|
-
const parsedSettings =
|
|
1710
|
-
settingsPartPath && sourcePackage.parts.has(settingsPartPath)
|
|
1711
|
-
? parseSettingsXml(
|
|
1712
|
-
decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array()),
|
|
1713
|
-
)
|
|
1714
|
-
: undefined;
|
|
1715
|
-
const canonicalTheme =
|
|
1716
|
-
parsedTheme !== undefined
|
|
1717
|
-
? materializeCanonicalTheme(
|
|
1718
|
-
parsedTheme,
|
|
1719
|
-
parsedSettings?.clrSchemeMapping ?? {},
|
|
1720
|
-
)
|
|
1721
|
-
: undefined;
|
|
1722
|
-
const settingsXmlForProtection =
|
|
1723
|
-
settingsPartPath && sourcePackage.parts.has(settingsPartPath)
|
|
1724
|
-
? decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array())
|
|
1725
|
-
: "";
|
|
1726
|
-
const documentProtection = extractDocumentProtection(settingsXmlForProtection);
|
|
1727
|
-
const importedProtectionSnapshot = buildProtectionSnapshot(documentProtection, protectionRanges);
|
|
1728
|
-
|
|
1729
|
-
// ---- Parse styles.xml for canonical style catalog ----
|
|
1730
|
-
const stylesPartPath = resolveDocumentRelatedPartPath(
|
|
1731
|
-
sourcePackage,
|
|
1732
|
-
mainDocumentPath,
|
|
1733
|
-
documentPart.relationships,
|
|
1734
|
-
STYLES_RELATIONSHIP_TYPE,
|
|
1735
|
-
STYLES_PART_PATH,
|
|
1736
|
-
);
|
|
1737
|
-
const parsedStyles =
|
|
1738
|
-
stylesPartPath && sourcePackage.parts.has(stylesPartPath)
|
|
1739
|
-
? parseStylesXml(
|
|
1740
|
-
decodeUtf8(sourcePackage.parts.get(stylesPartPath)?.bytes ?? new Uint8Array()),
|
|
1741
|
-
)
|
|
1742
|
-
: parseStylesXml("");
|
|
1743
|
-
await scheduler.yield();
|
|
1744
|
-
|
|
1745
|
-
// ---- Parse fontTable.xml for canonical font catalog ----
|
|
1746
|
-
const fontTablePartPath = resolveDocumentRelatedPartPath(
|
|
1747
|
-
sourcePackage,
|
|
1748
|
-
mainDocumentPath,
|
|
1749
|
-
documentPart.relationships,
|
|
1750
|
-
FONT_TABLE_RELATIONSHIP_TYPE,
|
|
1751
|
-
FONT_TABLE_PART_PATH,
|
|
1752
|
-
);
|
|
1753
|
-
const parsedFontTable =
|
|
1754
|
-
fontTablePartPath && sourcePackage.parts.has(fontTablePartPath)
|
|
1755
|
-
? parseFontTable(
|
|
1756
|
-
decodeUtf8(sourcePackage.parts.get(fontTablePartPath)?.bytes ?? new Uint8Array()),
|
|
1757
|
-
)
|
|
1758
|
-
: undefined;
|
|
1759
|
-
|
|
1760
|
-
const mergedMedia = mergeSecondaryStoryMediaCatalog(normalizedDocument.media, {
|
|
1761
|
-
headers: parsedHeaders,
|
|
1762
|
-
footers: parsedFooters,
|
|
1763
|
-
footnoteCollection,
|
|
1764
|
-
mediaParts,
|
|
1765
|
-
});
|
|
1766
|
-
|
|
1767
|
-
const subParts: SubPartsCatalog | undefined =
|
|
1768
|
-
parsedHeaders.length > 0 ||
|
|
1769
|
-
parsedFooters.length > 0 ||
|
|
1770
|
-
footnoteCollection !== undefined ||
|
|
1771
|
-
parsedTheme !== undefined ||
|
|
1772
|
-
normalizedDocument.finalSectionProperties !== undefined ||
|
|
1773
|
-
resolvedTheme !== undefined ||
|
|
1774
|
-
canonicalTheme !== undefined ||
|
|
1775
|
-
parsedSettings !== undefined
|
|
1776
|
-
? {
|
|
1777
|
-
headers: parsedHeaders,
|
|
1778
|
-
footers: parsedFooters,
|
|
1779
|
-
...(footnoteCollection !== undefined ? { footnoteCollection } : {}),
|
|
1780
|
-
...(parsedTheme !== undefined ? { theme: parsedTheme } : {}),
|
|
1781
|
-
...(normalizedDocument.finalSectionProperties !== undefined
|
|
1782
|
-
? { finalSectionProperties: normalizedDocument.finalSectionProperties }
|
|
1783
|
-
: {}),
|
|
1784
|
-
...(resolvedTheme !== undefined ? { resolvedTheme } : {}),
|
|
1785
|
-
...(canonicalTheme !== undefined ? { canonicalTheme } : {}),
|
|
1786
|
-
...(parsedSettings !== undefined ? { settings: parsedSettings } : {}),
|
|
1787
|
-
}
|
|
1788
|
-
: undefined;
|
|
1789
|
-
|
|
1790
|
-
const timestamp = new Date().toISOString();
|
|
1791
|
-
const translatedWorkflowState = translateClmCommentsToWorkflow({
|
|
1792
|
-
comments: normalizedComments.threads,
|
|
1793
|
-
workflowOverlay: embeddedWorkflowOverlay,
|
|
1794
|
-
workflowMetadata: embeddedWorkflowMetadata,
|
|
1795
|
-
timestamp,
|
|
1796
|
-
});
|
|
1797
|
-
const importedDocument = createImportedCanonicalDocument({
|
|
1798
|
-
documentId: options.documentId,
|
|
1799
|
-
timestamp,
|
|
1800
|
-
numbering: parsedNumbering,
|
|
1801
|
-
media: mergedMedia,
|
|
1802
|
-
content: normalizedDocument.content,
|
|
1803
|
-
subParts,
|
|
1804
|
-
parsedStyles,
|
|
1805
|
-
fontTable: parsedFontTable,
|
|
1806
|
-
preservation: {
|
|
1807
|
-
...normalizedDocument.preservation,
|
|
1808
|
-
packageParts: {
|
|
1809
|
-
...normalizedDocument.preservation.packageParts,
|
|
1810
|
-
...collectPreservedPackageParts(sourcePackage, [
|
|
1811
|
-
mainDocumentPath,
|
|
1812
|
-
numberingPartPath,
|
|
1813
|
-
commentsPartPath,
|
|
1814
|
-
commentsExtendedPartPath,
|
|
1815
|
-
commentsIdsPartPath,
|
|
1816
|
-
peoplePartPath,
|
|
1817
|
-
]),
|
|
1818
|
-
},
|
|
1819
|
-
},
|
|
1820
|
-
diagnostics: {
|
|
1821
|
-
warnings: [
|
|
1822
|
-
...createBrokenRelationshipWarnings(sourcePackage, mainDocumentPath),
|
|
1823
|
-
...normalizedDocument.diagnostics.warnings,
|
|
1824
|
-
...normalizedRevisions.diagnostics.map((diagnostic, index) => ({
|
|
1825
|
-
diagnosticId: `diagnostic:revision-import-${index + 1}`,
|
|
1826
|
-
warningId: `warning:revision-import-${diagnostic.revisionId}`,
|
|
1827
|
-
source: "review" as const,
|
|
1828
|
-
message: diagnostic.message,
|
|
1829
|
-
})),
|
|
1830
|
-
...importedStoryRevisionDiagnostics.map((diagnostic, index) => ({
|
|
1831
|
-
diagnosticId: `diagnostic:story-revision-import-${index + 1}`,
|
|
1832
|
-
warningId: `warning:story-revision-import-${diagnostic.revisionId}`,
|
|
1833
|
-
source: "review" as const,
|
|
1834
|
-
message: diagnostic.message,
|
|
1835
|
-
})),
|
|
1836
|
-
...normalizedComments.diagnostics.map((diagnostic, index) => ({
|
|
1837
|
-
diagnosticId: `diagnostic:comment-import-${index + 1}`,
|
|
1838
|
-
warningId: `warning:comment-import-${diagnostic.commentId}`,
|
|
1839
|
-
source: "review" as const,
|
|
1840
|
-
message: diagnostic.message,
|
|
1841
|
-
})),
|
|
1842
|
-
],
|
|
1843
|
-
errors: [],
|
|
1844
|
-
},
|
|
1845
|
-
review: {
|
|
1846
|
-
comments: toRuntimeCommentRecords(translatedWorkflowState.comments),
|
|
1847
|
-
revisions: toRuntimeRevisionRecords([
|
|
1848
|
-
...normalizedRevisions.revisions,
|
|
1849
|
-
...importedStoryRevisions,
|
|
1850
|
-
]),
|
|
1851
|
-
},
|
|
1852
|
-
});
|
|
1853
|
-
// Stage 0B.1: if the host implements `renderChartPreview`, resolve
|
|
1854
|
-
// chart_preview nodes inline so the first snapshot already carries the
|
|
1855
|
-
// synthesized `previewMediaId`. Fallback-safe: returning null or throwing
|
|
1856
|
-
// is per-chart — the typed badge renders as if the adapter weren't set.
|
|
1857
|
-
// C3b: when onChartPreviewsReady is provided, defer resolution out of
|
|
1858
|
-
// the critical path (same pattern as the short-circuit branch above).
|
|
1859
|
-
let document: CanonicalDocumentEnvelope;
|
|
1860
|
-
if (options.onChartPreviewsReady) {
|
|
1861
|
-
scheduleChartPreviewResolution(
|
|
1862
|
-
importedDocument,
|
|
1863
|
-
sourcePackage,
|
|
1864
|
-
options.hostAdapter,
|
|
1865
|
-
options.onChartPreviewsReady,
|
|
1866
|
-
);
|
|
1867
|
-
document = importedDocument as CanonicalDocumentEnvelope;
|
|
1868
|
-
} else {
|
|
1869
|
-
document = (await resolveChartPreviewsForDocument(
|
|
1870
|
-
importedDocument,
|
|
1871
|
-
sourcePackage,
|
|
1872
|
-
options.hostAdapter,
|
|
1873
|
-
)) as CanonicalDocumentEnvelope;
|
|
1874
|
-
}
|
|
1875
|
-
const compatibility = buildCompatibilityReport({
|
|
1876
|
-
document,
|
|
1877
|
-
generatedAt: timestamp,
|
|
1878
|
-
});
|
|
1879
|
-
await scheduler.yield();
|
|
1880
|
-
const snapshot = createImportedSnapshot({
|
|
1881
|
-
documentId: options.documentId,
|
|
1882
|
-
editorBuild,
|
|
1883
|
-
timestamp,
|
|
1884
|
-
document,
|
|
1885
|
-
compatibility: toPublicCompatibilityReport(compatibility),
|
|
1886
|
-
protectionSnapshot: importedProtectionSnapshot,
|
|
1887
|
-
sourcePackage: createPersistedSourcePackage(sourceBytes, options.sourceLabel),
|
|
1888
|
-
workflowOverlay: translatedWorkflowState.workflowOverlay,
|
|
1889
|
-
workflowMetadata: translatedWorkflowState.workflowMetadata,
|
|
1890
|
-
});
|
|
1891
|
-
const snapshotIssues = validatePersistedEditorSnapshot(snapshot);
|
|
1892
|
-
if (snapshotIssues.length > 0) {
|
|
1893
|
-
const firstIssue = snapshotIssues[0];
|
|
1894
|
-
return createDiagnosticsSession(
|
|
1895
|
-
options,
|
|
1896
|
-
createValidationImportDiagnostics({
|
|
1897
|
-
message: `DOCX import produced an invalid editor state during validation${firstIssue ? ` (${firstIssue.path}: ${firstIssue.message})` : "."}`,
|
|
1898
|
-
source: "import",
|
|
1899
|
-
details: {
|
|
1900
|
-
issueCount: snapshotIssues.length,
|
|
1901
|
-
firstIssuePath: firstIssue?.path,
|
|
1902
|
-
},
|
|
1903
|
-
}),
|
|
1904
|
-
);
|
|
1905
|
-
}
|
|
1906
|
-
const initialSessionState = editorSessionStateFromPersistedSnapshot(snapshot);
|
|
1907
|
-
const importedState: ImportedDocxState = {
|
|
1908
|
-
sourceBytes: new Uint8Array(sourceBytes),
|
|
1909
|
-
sourcePackage,
|
|
1910
|
-
sourceDocumentXml,
|
|
1911
|
-
sourceDocumentPartPath: mainDocumentPath,
|
|
1912
|
-
sourceDocumentRelationships: documentPart.relationships,
|
|
1913
|
-
sourceDocumentAttributes: extractDocumentRootAttributes(sourceDocumentXml),
|
|
1914
|
-
sourceNumberingPartPath: numberingPartPath,
|
|
1915
|
-
sourceNumberingRelationshipId: documentPart.relationships.find(
|
|
1916
|
-
(relationship) =>
|
|
1917
|
-
relationship.type === NUMBERING_RELATIONSHIP_TYPE &&
|
|
1918
|
-
relationship.targetMode === "internal",
|
|
1919
|
-
)?.id,
|
|
1920
|
-
sourceSettingsPartPath: settingsPartPath,
|
|
1921
|
-
sourceSettingsXml:
|
|
1922
|
-
settingsXmlForProtection.length > 0 ? settingsXmlForProtection : undefined,
|
|
1923
|
-
sourceCommentsPartPath: commentsPartPath,
|
|
1924
|
-
sourceCommentsRelationshipId: documentPart.relationships.find(
|
|
1925
|
-
(relationship) =>
|
|
1926
|
-
relationship.type === COMMENTS_RELATIONSHIP_TYPE &&
|
|
1927
|
-
relationship.targetMode === "internal",
|
|
1928
|
-
)?.id,
|
|
1929
|
-
sourceCommentsRootTag: normalizedComments.sourceRootTag,
|
|
1930
|
-
sourceCommentsExtendedPartPath: commentsExtendedPartPath,
|
|
1931
|
-
sourceCommentsExtendedRelationshipId: documentPart.relationships.find(
|
|
1932
|
-
(relationship) =>
|
|
1933
|
-
relationship.type === COMMENTS_EXTENDED_RELATIONSHIP_TYPE &&
|
|
1934
|
-
relationship.targetMode === "internal",
|
|
1935
|
-
)?.id,
|
|
1936
|
-
sourceCommentsExtendedRootTag: normalizedComments.sourceExtendedRootTag,
|
|
1937
|
-
sourceCommentsIdsPartPath: commentsIdsPartPath,
|
|
1938
|
-
sourceCommentsIdsRelationshipId: documentPart.relationships.find(
|
|
1939
|
-
(relationship) =>
|
|
1940
|
-
relationship.type === COMMENTS_IDS_RELATIONSHIP_TYPE &&
|
|
1941
|
-
relationship.targetMode === "internal",
|
|
1942
|
-
)?.id,
|
|
1943
|
-
sourceCommentsIdsRootTag: normalizedComments.sourceIdsRootTag,
|
|
1944
|
-
sourcePeoplePartPath: peoplePartPath,
|
|
1945
|
-
sourcePeopleRelationshipId: documentPart.relationships.find(
|
|
1946
|
-
(relationship) =>
|
|
1947
|
-
relationship.type === PEOPLE_RELATIONSHIP_TYPE &&
|
|
1948
|
-
relationship.targetMode === "internal",
|
|
1949
|
-
)?.id,
|
|
1950
|
-
sourcePeopleRootTag: normalizedComments.sourcePeopleRootTag,
|
|
1951
|
-
sourcePeopleAuthors: normalizedComments.peopleAuthors,
|
|
1952
|
-
protectionSnapshot: buildProtectionSnapshot(documentProtection, protectionRanges),
|
|
1953
|
-
preservedCommentDefinitions: normalizedComments.preservedDefinitions,
|
|
1954
|
-
blockingCommentDiagnostics: normalizedComments.diagnostics.filter((diagnostic) =>
|
|
1955
|
-
BLOCKING_COMMENT_DIAGNOSTIC_CODES.has(diagnostic.code),
|
|
1956
|
-
),
|
|
1957
|
-
initialCanonicalSignature: serializeCanonicalDocumentForExport(document),
|
|
1958
|
-
sourceSubPartPaths: {
|
|
1959
|
-
headers: sourceHeaderPaths,
|
|
1960
|
-
footers: sourceFooterPaths,
|
|
1961
|
-
footnotesPartPath,
|
|
1962
|
-
footnotesRelationshipId,
|
|
1963
|
-
endnotesPartPath,
|
|
1964
|
-
endnotesRelationshipId,
|
|
1965
|
-
themePartPath,
|
|
1966
|
-
themeRelationshipId: themeRelationship?.id,
|
|
1967
|
-
},
|
|
1968
|
-
};
|
|
1969
|
-
|
|
1970
|
-
stages.emit("skeleton-ready");
|
|
1971
|
-
return {
|
|
1972
|
-
initialSessionState,
|
|
1973
|
-
initialSnapshot: snapshot,
|
|
1974
|
-
readOnly: false,
|
|
1975
|
-
protectionSnapshot: importedProtectionSnapshot,
|
|
1976
|
-
exportDocx: async (nextSessionState, exportOptions) =>
|
|
1977
|
-
exportDocxEditorSession(importedState, nextSessionState, exportOptions),
|
|
1978
|
-
...(embeddedWorkflowPayload?.editorState
|
|
1979
|
-
? { initialEditorStatePayload: embeddedWorkflowPayload.editorState }
|
|
1980
|
-
: {}),
|
|
1981
|
-
};
|
|
1982
|
-
} catch (error) {
|
|
1983
|
-
return createDiagnosticsSession(
|
|
1984
|
-
options,
|
|
1985
|
-
createImportDiagnosticsFromError(error),
|
|
1986
|
-
);
|
|
1987
|
-
}
|
|
1988
|
-
}
|
|
1989
|
-
|
|
1990
|
-
function loadStageNow(): number {
|
|
1991
|
-
return typeof performance !== "undefined" && typeof performance.now === "function"
|
|
1992
|
-
? performance.now()
|
|
1993
|
-
: Date.now();
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
function exportDocxEditorSession(
|
|
1997
|
-
state: ImportedDocxState,
|
|
1998
|
-
sessionStateOrSnapshot: EditorSessionState | PersistedEditorSnapshot,
|
|
1999
|
-
options?: ExportDocxOptions,
|
|
2000
|
-
): ExportResult {
|
|
2001
|
-
const sessionState = toEditorSessionState(sessionStateOrSnapshot);
|
|
2002
|
-
|
|
2003
|
-
if (sessionState.compatibility.blockExport) {
|
|
2004
|
-
throw new Error("DOCX export is blocked by the current compatibility report.");
|
|
2005
|
-
}
|
|
2006
|
-
|
|
2007
|
-
const currentDocument = sessionState.canonicalDocument as CanonicalDocumentEnvelope;
|
|
2008
|
-
const signatureMatch = serializeCanonicalDocumentForExport(currentDocument) ===
|
|
2009
|
-
state.initialCanonicalSignature;
|
|
2010
|
-
const canReuse = canReuseSourceBytesForCurrentDocument(state, currentDocument);
|
|
2011
|
-
const durableWorkflowMetadata = getDocumentBackedWorkflowMetadata(sessionState.workflowMetadata);
|
|
2012
|
-
const hasWorkflowOverlay = Boolean(sessionState.workflowOverlay);
|
|
2013
|
-
const sourceHasWorkflowPayload = resolvePayloadPartPath(state.sourcePackage) !== null;
|
|
2014
|
-
const commentCount = Object.keys(currentDocument.review?.comments ?? {}).length;
|
|
2015
|
-
const currentRevisions = toReviewRevisionRecords(currentDocument.review.revisions);
|
|
2016
|
-
const hasActiveImportedPreserveOnlyRevisions = currentRevisions.some(
|
|
2017
|
-
(revision) =>
|
|
2018
|
-
revision.status === "active" &&
|
|
2019
|
-
revision.metadata.source === "import" &&
|
|
2020
|
-
typeof revision.metadata.preserveOnlyReason === "string" &&
|
|
2021
|
-
revision.metadata.preserveOnlyReason.length > 0,
|
|
2022
|
-
);
|
|
2023
|
-
|
|
2024
|
-
if (
|
|
2025
|
-
signatureMatch &&
|
|
2026
|
-
canReuse &&
|
|
2027
|
-
durableWorkflowMetadata.definitions.length === 0 &&
|
|
2028
|
-
durableWorkflowMetadata.entries.length === 0 &&
|
|
2029
|
-
!hasWorkflowOverlay &&
|
|
2030
|
-
!sourceHasWorkflowPayload
|
|
2031
|
-
) {
|
|
2032
|
-
return {
|
|
2033
|
-
bytes: new Uint8Array(state.sourceBytes),
|
|
2034
|
-
mimeType: DOCX_MIME_TYPE,
|
|
2035
|
-
fileName: options?.fileName ?? `${sessionState.documentId}.docx`,
|
|
2036
|
-
delivery: {
|
|
2037
|
-
mode: "exported-bytes-only",
|
|
2038
|
-
},
|
|
2039
|
-
};
|
|
2040
|
-
}
|
|
2041
|
-
const preservedCommentIds = new Set(
|
|
2042
|
-
state.preservedCommentDefinitions.map((definition) => definition.commentId),
|
|
2043
|
-
);
|
|
2044
|
-
const blockingCommentCount = Math.max(
|
|
2045
|
-
state.blockingCommentDiagnostics.length,
|
|
2046
|
-
preservedCommentIds.size,
|
|
2047
|
-
);
|
|
2048
|
-
if (blockingCommentCount > 0) {
|
|
2049
|
-
throw new Error(
|
|
2050
|
-
`DOCX export is blocked because ${blockingCommentCount} preserve-only comment anchors cannot be safely remapped after runtime edits.`,
|
|
2051
|
-
);
|
|
2052
|
-
}
|
|
2053
|
-
const actionableRevisions = currentRevisions.filter(
|
|
2054
|
-
(revision) => getRevisionActionability(revision) === "actionable",
|
|
2055
|
-
);
|
|
2056
|
-
const mainStoryActionableRevisions = actionableRevisions.filter((revision) =>
|
|
2057
|
-
!revision.metadata.storyTarget?.kind || revision.metadata.storyTarget.kind === "main"
|
|
2058
|
-
);
|
|
2059
|
-
const commentThreads = Object.values(
|
|
2060
|
-
createCommentStoreFromRuntimeComments(currentDocument.review.comments).threads,
|
|
2061
|
-
);
|
|
2062
|
-
const ownedCommentThreads = commentThreads.filter(
|
|
2063
|
-
(thread) => !preservedCommentIds.has(thread.commentId),
|
|
2064
|
-
);
|
|
2065
|
-
const revisionReadyMainContent = {
|
|
2066
|
-
...currentDocument.content,
|
|
2067
|
-
children: splitStoryBlocksForRuntimeRevisions(
|
|
2068
|
-
currentDocument.content.children,
|
|
2069
|
-
mainStoryActionableRevisions,
|
|
2070
|
-
),
|
|
2071
|
-
};
|
|
2072
|
-
const serialized = serializeMainDocument(
|
|
2073
|
-
splitDocumentAtReviewBoundaries(
|
|
2074
|
-
revisionReadyMainContent as never,
|
|
2075
|
-
ownedCommentThreads,
|
|
2076
|
-
mainStoryActionableRevisions,
|
|
2077
|
-
) as never,
|
|
2078
|
-
currentDocument.preservation as never,
|
|
2079
|
-
state.sourceDocumentRelationships,
|
|
2080
|
-
{
|
|
2081
|
-
documentAttributes: state.sourceDocumentAttributes,
|
|
2082
|
-
media: currentDocument.media as MediaCatalog,
|
|
2083
|
-
finalSectionProperties: currentDocument.subParts?.finalSectionProperties,
|
|
2084
|
-
namespaceFlavor: options?.exportStrictOoxml ? "strict" : "transitional",
|
|
2085
|
-
},
|
|
2086
|
-
);
|
|
2087
|
-
const revisionDocument = serializeRuntimeRevisionsIntoDocumentXml(
|
|
2088
|
-
serialized.documentXml,
|
|
2089
|
-
mainStoryActionableRevisions,
|
|
2090
|
-
);
|
|
2091
|
-
if (revisionDocument.skippedRevisionIds.length > 0) {
|
|
2092
|
-
throw new Error(
|
|
2093
|
-
`DOCX export is blocked because ${revisionDocument.skippedRevisionIds.length} active revisions overlap unsupported serialization boundaries.`,
|
|
2094
|
-
);
|
|
2095
|
-
}
|
|
2096
|
-
|
|
2097
|
-
const strippedDocumentXml = stripCommentMarkup(
|
|
2098
|
-
revisionDocument.documentXml,
|
|
2099
|
-
ownedCommentThreads.map((thread) => thread.commentId),
|
|
2100
|
-
);
|
|
2101
|
-
const exportCommentIds = createCommentExportIdMap(
|
|
2102
|
-
ownedCommentThreads,
|
|
2103
|
-
state.preservedCommentDefinitions,
|
|
2104
|
-
);
|
|
2105
|
-
const serializedComments = serializeMergedCommentsXml(ownedCommentThreads, {
|
|
2106
|
-
exportCommentIds,
|
|
2107
|
-
preservedDefinitions: state.preservedCommentDefinitions,
|
|
2108
|
-
sourceRootTag: state.sourceCommentsRootTag,
|
|
2109
|
-
sourceExtendedRootTag: state.sourceCommentsExtendedRootTag,
|
|
2110
|
-
sourceIdsRootTag: state.sourceCommentsIdsRootTag,
|
|
2111
|
-
sourcePeopleRootTag: state.sourcePeopleRootTag,
|
|
2112
|
-
peopleAuthors: state.sourcePeopleAuthors,
|
|
2113
|
-
});
|
|
2114
|
-
const annotatedDocument = serializeCommentAnchorsIntoDocumentXml(
|
|
2115
|
-
strippedDocumentXml,
|
|
2116
|
-
ownedCommentThreads,
|
|
2117
|
-
undefined,
|
|
2118
|
-
{
|
|
2119
|
-
exportCommentIds,
|
|
2120
|
-
},
|
|
2121
|
-
);
|
|
2122
|
-
const protectedDocumentXml = serializeProtectionRangesIntoDocumentXml(
|
|
2123
|
-
annotatedDocument.documentXml,
|
|
2124
|
-
state.protectionSnapshot,
|
|
2125
|
-
);
|
|
2126
|
-
const blockingSkippedCommentIds = annotatedDocument.skippedCommentIds.filter((commentId) => {
|
|
2127
|
-
const thread = ownedCommentThreads.find((candidate) => candidate.commentId === commentId);
|
|
2128
|
-
return !thread || thread.anchor.kind !== "detached";
|
|
2129
|
-
});
|
|
2130
|
-
if (blockingSkippedCommentIds.length > 0) {
|
|
2131
|
-
throw new Error(
|
|
2132
|
-
`DOCX export is blocked because ${blockingSkippedCommentIds.length} comments no longer map to serializable ranges.`,
|
|
2133
|
-
);
|
|
2134
|
-
}
|
|
2135
|
-
const commentsPartPath =
|
|
2136
|
-
state.sourceCommentsPartPath ?? COMMENTS_PART_PATH;
|
|
2137
|
-
const commentsExtendedPartPath =
|
|
2138
|
-
state.sourceCommentsExtendedPartPath ?? COMMENTS_EXTENDED_PART_PATH;
|
|
2139
|
-
const commentsIdsPartPath =
|
|
2140
|
-
state.sourceCommentsIdsPartPath ?? COMMENTS_IDS_PART_PATH;
|
|
2141
|
-
const peoplePartPath =
|
|
2142
|
-
state.sourcePeoplePartPath ?? PEOPLE_PART_PATH;
|
|
2143
|
-
const numberingPartPath =
|
|
2144
|
-
state.sourceNumberingPartPath ?? NUMBERING_PART_PATH;
|
|
2145
|
-
const serializedNumberingXml = hasSerializableNumberingEntries(
|
|
2146
|
-
currentDocument.numbering as NumberingCatalog,
|
|
2147
|
-
)
|
|
2148
|
-
? serializeNumberingXml(currentDocument.numbering as NumberingCatalog)
|
|
2149
|
-
: undefined;
|
|
2150
|
-
const nextRelationships = withDocumentRelatedParts(
|
|
2151
|
-
serialized.relationships,
|
|
2152
|
-
[
|
|
2153
|
-
{
|
|
2154
|
-
relationshipType: NUMBERING_RELATIONSHIP_TYPE,
|
|
2155
|
-
partPath: numberingPartPath,
|
|
2156
|
-
existingRelationshipId: state.sourceNumberingRelationshipId,
|
|
2157
|
-
include:
|
|
2158
|
-
Boolean(serializedNumberingXml) ||
|
|
2159
|
-
Boolean(state.sourceNumberingPartPath),
|
|
2160
|
-
},
|
|
2161
|
-
{
|
|
2162
|
-
relationshipType: COMMENTS_RELATIONSHIP_TYPE,
|
|
2163
|
-
partPath: commentsPartPath,
|
|
2164
|
-
existingRelationshipId: state.sourceCommentsRelationshipId,
|
|
2165
|
-
include:
|
|
2166
|
-
serializedComments.serializedCommentIds.length > 0 ||
|
|
2167
|
-
Boolean(state.sourceCommentsPartPath),
|
|
2168
|
-
},
|
|
2169
|
-
{
|
|
2170
|
-
relationshipType: COMMENTS_EXTENDED_RELATIONSHIP_TYPE,
|
|
2171
|
-
partPath: commentsExtendedPartPath,
|
|
2172
|
-
existingRelationshipId: state.sourceCommentsExtendedRelationshipId,
|
|
2173
|
-
include:
|
|
2174
|
-
Boolean(serializedComments.commentsExtendedXml) ||
|
|
2175
|
-
Boolean(state.sourceCommentsExtendedPartPath),
|
|
2176
|
-
},
|
|
2177
|
-
{
|
|
2178
|
-
relationshipType: COMMENTS_IDS_RELATIONSHIP_TYPE,
|
|
2179
|
-
partPath: commentsIdsPartPath,
|
|
2180
|
-
existingRelationshipId: state.sourceCommentsIdsRelationshipId,
|
|
2181
|
-
include:
|
|
2182
|
-
Boolean(serializedComments.commentsIdsXml) ||
|
|
2183
|
-
Boolean(state.sourceCommentsIdsPartPath),
|
|
2184
|
-
},
|
|
2185
|
-
{
|
|
2186
|
-
relationshipType: PEOPLE_RELATIONSHIP_TYPE,
|
|
2187
|
-
partPath: peoplePartPath,
|
|
2188
|
-
existingRelationshipId: state.sourcePeopleRelationshipId,
|
|
2189
|
-
include:
|
|
2190
|
-
Boolean(serializedComments.peopleXml) ||
|
|
2191
|
-
Boolean(state.sourcePeoplePartPath),
|
|
2192
|
-
},
|
|
2193
|
-
],
|
|
2194
|
-
);
|
|
2195
|
-
|
|
2196
|
-
const exportedSubParts = currentDocument.subParts as SubPartsCatalog | undefined;
|
|
2197
|
-
const subPartOwnedPaths: string[] = [];
|
|
2198
|
-
if (exportedSubParts) {
|
|
2199
|
-
for (const header of exportedSubParts.headers) {
|
|
2200
|
-
subPartOwnedPaths.push(header.partPath);
|
|
2201
|
-
}
|
|
2202
|
-
for (const footer of exportedSubParts.footers) {
|
|
2203
|
-
subPartOwnedPaths.push(footer.partPath);
|
|
2204
|
-
}
|
|
2205
|
-
if (exportedSubParts.footnoteCollection) {
|
|
2206
|
-
if (state.sourceSubPartPaths.footnotesPartPath) {
|
|
2207
|
-
subPartOwnedPaths.push(state.sourceSubPartPaths.footnotesPartPath);
|
|
2208
|
-
}
|
|
2209
|
-
if (state.sourceSubPartPaths.endnotesPartPath) {
|
|
2210
|
-
subPartOwnedPaths.push(state.sourceSubPartPaths.endnotesPartPath);
|
|
2211
|
-
}
|
|
2212
|
-
}
|
|
2213
|
-
if (exportedSubParts.theme && state.sourceSubPartPaths.themePartPath) {
|
|
2214
|
-
subPartOwnedPaths.push(state.sourceSubPartPaths.themePartPath);
|
|
2215
|
-
}
|
|
2216
|
-
}
|
|
2217
|
-
|
|
2218
|
-
// Settings.xml is owned when the source package carried one OR the canonical
|
|
2219
|
-
// model carries settings we need to re-emit. The `canReuse && signatureMatch`
|
|
2220
|
-
// short-circuit above already skips re-export entirely for no-edit sessions,
|
|
2221
|
-
// so every path that reaches here is willing to emit a rebuilt settings.xml.
|
|
2222
|
-
const settingsPartPath =
|
|
2223
|
-
state.sourceSettingsPartPath ?? SETTINGS_PART_PATH;
|
|
2224
|
-
const hasSettingsSurface =
|
|
2225
|
-
Boolean(state.sourceSettingsPartPath) ||
|
|
2226
|
-
exportedSubParts?.settings !== undefined;
|
|
2227
|
-
const workflowPayloadPartPaths = resolveWorkflowPayloadPartPaths(
|
|
2228
|
-
state.sourcePackage,
|
|
2229
|
-
sessionState.documentId,
|
|
2230
|
-
);
|
|
2231
|
-
const internalEditorState = (
|
|
2232
|
-
options as { _editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload } | undefined
|
|
2233
|
-
)?._editorState;
|
|
2234
|
-
|
|
2235
|
-
const exportSession = createExportSession(state.sourcePackage, [
|
|
2236
|
-
state.sourceDocumentPartPath,
|
|
2237
|
-
APP_PROPERTIES_PART_PATH,
|
|
2238
|
-
CORE_PROPERTIES_PART_PATH,
|
|
2239
|
-
workflowPayloadPartPaths.payloadPartPath,
|
|
2240
|
-
workflowPayloadPartPaths.itemPropsPartPath,
|
|
2241
|
-
WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
|
|
2242
|
-
numberingPartPath,
|
|
2243
|
-
commentsPartPath,
|
|
2244
|
-
commentsExtendedPartPath,
|
|
2245
|
-
commentsIdsPartPath,
|
|
2246
|
-
peoplePartPath,
|
|
2247
|
-
...(hasSettingsSurface ? [settingsPartPath] : []),
|
|
2248
|
-
...subPartOwnedPaths,
|
|
2249
|
-
]);
|
|
2250
|
-
|
|
2251
|
-
const mainDocumentXmlForExport =
|
|
2252
|
-
signatureMatch &&
|
|
2253
|
-
durableWorkflowMetadata.definitions.length === 0 &&
|
|
2254
|
-
durableWorkflowMetadata.entries.length === 0 &&
|
|
2255
|
-
!hasWorkflowOverlay &&
|
|
2256
|
-
commentCount === 0 &&
|
|
2257
|
-
hasActiveImportedPreserveOnlyRevisions
|
|
2258
|
-
? state.sourceDocumentXml
|
|
2259
|
-
: protectedDocumentXml;
|
|
2260
|
-
|
|
2261
|
-
exportSession.replaceOwnedPart({
|
|
2262
|
-
path: state.sourceDocumentPartPath,
|
|
2263
|
-
bytes: new TextEncoder().encode(mainDocumentXmlForExport),
|
|
2264
|
-
contentType: MAIN_DOCUMENT_CONTENT_TYPE,
|
|
2265
|
-
relationships: nextRelationships,
|
|
2266
|
-
});
|
|
2267
|
-
|
|
2268
|
-
if (serializedNumberingXml || state.sourceNumberingPartPath) {
|
|
2269
|
-
exportSession.replaceOwnedPart({
|
|
2270
|
-
path: numberingPartPath,
|
|
2271
|
-
bytes: new TextEncoder().encode(
|
|
2272
|
-
serializedNumberingXml ??
|
|
2273
|
-
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w:numbering xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"></w:numbering>`,
|
|
2274
|
-
),
|
|
2275
|
-
contentType:
|
|
2276
|
-
state.sourcePackage.parts.get(numberingPartPath)?.contentType ??
|
|
2277
|
-
WORD_NUMBERING_CONTENT_TYPE,
|
|
2278
|
-
});
|
|
2279
|
-
}
|
|
2280
|
-
|
|
2281
|
-
if (hasSettingsSurface) {
|
|
2282
|
-
// Canonical settings ∅ + no source settings → omit the owned-part write
|
|
2283
|
-
// (hasSettingsSurface is already false in that case). Otherwise route
|
|
2284
|
-
// through the graft serializer so unmodelled children round-trip via
|
|
2285
|
-
// source bytes while canonical mutations land.
|
|
2286
|
-
const canonicalSettings = exportedSubParts?.settings ?? {};
|
|
2287
|
-
const settingsXml = serializeSettingsXml(canonicalSettings, state.sourceSettingsXml);
|
|
2288
|
-
exportSession.replaceOwnedPart({
|
|
2289
|
-
path: settingsPartPath,
|
|
2290
|
-
bytes: new TextEncoder().encode(settingsXml),
|
|
2291
|
-
contentType:
|
|
2292
|
-
state.sourcePackage.parts.get(settingsPartPath)?.contentType ??
|
|
2293
|
-
WORD_SETTINGS_CONTENT_TYPE,
|
|
2294
|
-
});
|
|
2295
|
-
}
|
|
2296
|
-
|
|
2297
|
-
if (serializedComments.serializedCommentIds.length > 0 || state.sourceCommentsPartPath) {
|
|
2298
|
-
exportSession.replaceOwnedPart({
|
|
2299
|
-
path: commentsPartPath,
|
|
2300
|
-
bytes: new TextEncoder().encode(serializedComments.commentsXml),
|
|
2301
|
-
contentType:
|
|
2302
|
-
state.sourcePackage.parts.get(commentsPartPath)?.contentType ?? COMMENTS_CONTENT_TYPE,
|
|
2303
|
-
});
|
|
2304
|
-
}
|
|
2305
|
-
|
|
2306
|
-
if (serializedComments.commentsExtendedXml || state.sourceCommentsExtendedPartPath) {
|
|
2307
|
-
exportSession.replaceOwnedPart({
|
|
2308
|
-
path: commentsExtendedPartPath,
|
|
2309
|
-
bytes: new TextEncoder().encode(
|
|
2310
|
-
serializedComments.commentsExtendedXml ?? `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w15:commentsEx xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"></w15:commentsEx>`,
|
|
2311
|
-
),
|
|
2312
|
-
contentType:
|
|
2313
|
-
state.sourcePackage.parts.get(commentsExtendedPartPath)?.contentType ??
|
|
2314
|
-
COMMENTS_EXTENDED_CONTENT_TYPE,
|
|
2315
|
-
});
|
|
2316
|
-
}
|
|
2317
|
-
|
|
2318
|
-
if (serializedComments.commentsIdsXml || state.sourceCommentsIdsPartPath) {
|
|
2319
|
-
exportSession.replaceOwnedPart({
|
|
2320
|
-
path: commentsIdsPartPath,
|
|
2321
|
-
bytes: new TextEncoder().encode(
|
|
2322
|
-
serializedComments.commentsIdsXml ?? `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w16cid:commentsIds xmlns:w16cid="http://schemas.microsoft.com/office/word/2016/wordml/cid"></w16cid:commentsIds>`,
|
|
2323
|
-
),
|
|
2324
|
-
contentType:
|
|
2325
|
-
state.sourcePackage.parts.get(commentsIdsPartPath)?.contentType ??
|
|
2326
|
-
COMMENTS_IDS_CONTENT_TYPE,
|
|
2327
|
-
});
|
|
2328
|
-
}
|
|
2329
|
-
|
|
2330
|
-
if (serializedComments.peopleXml || state.sourcePeoplePartPath) {
|
|
2331
|
-
exportSession.replaceOwnedPart({
|
|
2332
|
-
path: peoplePartPath,
|
|
2333
|
-
bytes: new TextEncoder().encode(
|
|
2334
|
-
serializedComments.peopleXml ?? `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<w15:people xmlns:w15="http://schemas.microsoft.com/office/word/2012/wordml"></w15:people>`,
|
|
2335
|
-
),
|
|
2336
|
-
contentType:
|
|
2337
|
-
state.sourcePackage.parts.get(peoplePartPath)?.contentType ??
|
|
2338
|
-
PEOPLE_CONTENT_TYPE,
|
|
2339
|
-
});
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
if (exportedSubParts) {
|
|
2343
|
-
const headersByPartPath = new Map<
|
|
2344
|
-
string,
|
|
2345
|
-
{ header: (typeof exportedSubParts.headers)[number]; revisions: ReviewRevisionRecord[] }
|
|
2346
|
-
>();
|
|
2347
|
-
for (const header of exportedSubParts.headers) {
|
|
2348
|
-
const entry = headersByPartPath.get(header.partPath) ?? { header, revisions: [] };
|
|
2349
|
-
const matchingRevisions = actionableRevisions.filter((revision) =>
|
|
2350
|
-
storyTargetsEqual(revision.metadata.storyTarget ?? { kind: "main" }, {
|
|
2351
|
-
kind: "header",
|
|
2352
|
-
relationshipId: header.relationshipId,
|
|
2353
|
-
variant: header.variant,
|
|
2354
|
-
...(header.sectionIndex !== undefined ? { sectionIndex: header.sectionIndex } : {}),
|
|
2355
|
-
})
|
|
2356
|
-
);
|
|
2357
|
-
entry.revisions.push(...matchingRevisions);
|
|
2358
|
-
if (matchingRevisions.length > 0) {
|
|
2359
|
-
entry.header = header;
|
|
2360
|
-
}
|
|
2361
|
-
headersByPartPath.set(header.partPath, entry);
|
|
2362
|
-
}
|
|
2363
|
-
for (const { header, revisions } of headersByPartPath.values()) {
|
|
2364
|
-
const serializedHeaderXml = serializeSecondaryStoryWithRuntimeRevisions(
|
|
2365
|
-
serializeHeaderXmlWithRevisions(header, revisions),
|
|
2366
|
-
revisions,
|
|
2367
|
-
`header ${header.partPath}`,
|
|
2368
|
-
);
|
|
2369
|
-
exportSession.replaceOwnedPart({
|
|
2370
|
-
path: header.partPath,
|
|
2371
|
-
bytes: new TextEncoder().encode(serializedHeaderXml),
|
|
2372
|
-
contentType:
|
|
2373
|
-
state.sourcePackage.parts.get(header.partPath)?.contentType ?? WORD_HEADER_CONTENT_TYPE,
|
|
2374
|
-
});
|
|
2375
|
-
}
|
|
2376
|
-
const footersByPartPath = new Map<
|
|
2377
|
-
string,
|
|
2378
|
-
{ footer: (typeof exportedSubParts.footers)[number]; revisions: ReviewRevisionRecord[] }
|
|
2379
|
-
>();
|
|
2380
|
-
for (const footer of exportedSubParts.footers) {
|
|
2381
|
-
const entry = footersByPartPath.get(footer.partPath) ?? { footer, revisions: [] };
|
|
2382
|
-
const matchingRevisions = actionableRevisions.filter((revision) =>
|
|
2383
|
-
storyTargetsEqual(revision.metadata.storyTarget ?? { kind: "main" }, {
|
|
2384
|
-
kind: "footer",
|
|
2385
|
-
relationshipId: footer.relationshipId,
|
|
2386
|
-
variant: footer.variant,
|
|
2387
|
-
...(footer.sectionIndex !== undefined ? { sectionIndex: footer.sectionIndex } : {}),
|
|
2388
|
-
})
|
|
2389
|
-
);
|
|
2390
|
-
entry.revisions.push(...matchingRevisions);
|
|
2391
|
-
if (matchingRevisions.length > 0) {
|
|
2392
|
-
entry.footer = footer;
|
|
2393
|
-
}
|
|
2394
|
-
footersByPartPath.set(footer.partPath, entry);
|
|
2395
|
-
}
|
|
2396
|
-
for (const { footer, revisions } of footersByPartPath.values()) {
|
|
2397
|
-
const serializedFooterXml = serializeSecondaryStoryWithRuntimeRevisions(
|
|
2398
|
-
serializeFooterXmlWithRevisions(footer, revisions),
|
|
2399
|
-
revisions,
|
|
2400
|
-
`footer ${footer.partPath}`,
|
|
2401
|
-
);
|
|
2402
|
-
exportSession.replaceOwnedPart({
|
|
2403
|
-
path: footer.partPath,
|
|
2404
|
-
bytes: new TextEncoder().encode(serializedFooterXml),
|
|
2405
|
-
contentType:
|
|
2406
|
-
state.sourcePackage.parts.get(footer.partPath)?.contentType ?? WORD_FOOTER_CONTENT_TYPE,
|
|
2407
|
-
});
|
|
2408
|
-
}
|
|
2409
|
-
if (exportedSubParts.footnoteCollection) {
|
|
2410
|
-
if (state.sourceSubPartPaths.footnotesPartPath) {
|
|
2411
|
-
const serializedFootnotesXml = serializeFootnotesXml(
|
|
2412
|
-
exportedSubParts.footnoteCollection,
|
|
2413
|
-
Object.fromEntries(
|
|
2414
|
-
actionableRevisions
|
|
2415
|
-
.filter((revision) => revision.metadata.storyTarget?.kind === "footnote")
|
|
2416
|
-
.map((revision) =>
|
|
2417
|
-
revision.metadata.storyTarget?.kind === "footnote"
|
|
2418
|
-
? [revision.metadata.storyTarget.noteId, [] as ReviewRevisionRecord[]] as const
|
|
2419
|
-
: null,
|
|
2420
|
-
)
|
|
2421
|
-
.filter((entry): entry is readonly [string, ReviewRevisionRecord[]] => entry !== null)
|
|
2422
|
-
.map(([noteId]) => [
|
|
2423
|
-
noteId,
|
|
2424
|
-
actionableRevisions.filter(
|
|
2425
|
-
(revision) =>
|
|
2426
|
-
revision.metadata.storyTarget?.kind === "footnote" &&
|
|
2427
|
-
revision.metadata.storyTarget.noteId === noteId,
|
|
2428
|
-
),
|
|
2429
|
-
]),
|
|
2430
|
-
),
|
|
2431
|
-
);
|
|
2432
|
-
exportSession.replaceOwnedPart({
|
|
2433
|
-
path: state.sourceSubPartPaths.footnotesPartPath,
|
|
2434
|
-
bytes: new TextEncoder().encode(serializedFootnotesXml),
|
|
2435
|
-
contentType:
|
|
2436
|
-
state.sourcePackage.parts.get(state.sourceSubPartPaths.footnotesPartPath)?.contentType ??
|
|
2437
|
-
WORD_FOOTNOTES_CONTENT_TYPE,
|
|
2438
|
-
});
|
|
2439
|
-
}
|
|
2440
|
-
if (state.sourceSubPartPaths.endnotesPartPath) {
|
|
2441
|
-
const serializedEndnotesXml = serializeEndnotesXml(
|
|
2442
|
-
exportedSubParts.footnoteCollection,
|
|
2443
|
-
Object.fromEntries(
|
|
2444
|
-
actionableRevisions
|
|
2445
|
-
.filter((revision) => revision.metadata.storyTarget?.kind === "endnote")
|
|
2446
|
-
.map((revision) =>
|
|
2447
|
-
revision.metadata.storyTarget?.kind === "endnote"
|
|
2448
|
-
? [revision.metadata.storyTarget.noteId, [] as ReviewRevisionRecord[]] as const
|
|
2449
|
-
: null,
|
|
2450
|
-
)
|
|
2451
|
-
.filter((entry): entry is readonly [string, ReviewRevisionRecord[]] => entry !== null)
|
|
2452
|
-
.map(([noteId]) => [
|
|
2453
|
-
noteId,
|
|
2454
|
-
actionableRevisions.filter(
|
|
2455
|
-
(revision) =>
|
|
2456
|
-
revision.metadata.storyTarget?.kind === "endnote" &&
|
|
2457
|
-
revision.metadata.storyTarget.noteId === noteId,
|
|
2458
|
-
),
|
|
2459
|
-
]),
|
|
2460
|
-
),
|
|
2461
|
-
);
|
|
2462
|
-
exportSession.replaceOwnedPart({
|
|
2463
|
-
path: state.sourceSubPartPaths.endnotesPartPath,
|
|
2464
|
-
bytes: new TextEncoder().encode(serializedEndnotesXml),
|
|
2465
|
-
contentType:
|
|
2466
|
-
state.sourcePackage.parts.get(state.sourceSubPartPaths.endnotesPartPath)?.contentType ??
|
|
2467
|
-
WORD_ENDNOTES_CONTENT_TYPE,
|
|
2468
|
-
});
|
|
2469
|
-
}
|
|
2470
|
-
}
|
|
2471
|
-
if (exportedSubParts.theme && state.sourceSubPartPaths.themePartPath) {
|
|
2472
|
-
const sourceThemePart = state.sourcePackage.parts.get(state.sourceSubPartPaths.themePartPath);
|
|
2473
|
-
if (sourceThemePart) {
|
|
2474
|
-
exportSession.replaceOwnedPart({
|
|
2475
|
-
path: state.sourceSubPartPaths.themePartPath,
|
|
2476
|
-
bytes: sourceThemePart.bytes,
|
|
2477
|
-
contentType:
|
|
2478
|
-
sourceThemePart.contentType ??
|
|
2479
|
-
"application/vnd.openxmlformats-officedocument.theme+xml",
|
|
2480
|
-
relationships: sourceThemePart.relationships,
|
|
2481
|
-
compression: sourceThemePart.compression,
|
|
2482
|
-
});
|
|
2483
|
-
}
|
|
2484
|
-
}
|
|
2485
|
-
}
|
|
2486
|
-
|
|
2487
|
-
ensureHostMetadataParts(exportSession, state.sourcePackage, currentDocument);
|
|
2488
|
-
// Schema 1.2: pass through editorState payload collected by the runtime channel.
|
|
2489
|
-
ensureWorkflowPayloadParts(
|
|
2490
|
-
exportSession,
|
|
2491
|
-
sessionState,
|
|
2492
|
-
currentDocument,
|
|
2493
|
-
state.sourcePackage,
|
|
2494
|
-
workflowPayloadPartPaths,
|
|
2495
|
-
internalEditorState,
|
|
2496
|
-
);
|
|
2497
|
-
|
|
2498
|
-
return {
|
|
2499
|
-
bytes: exportSession.serialize(),
|
|
2500
|
-
mimeType: DOCX_MIME_TYPE,
|
|
2501
|
-
fileName: options?.fileName ?? `${sessionState.documentId}.docx`,
|
|
2502
|
-
delivery: {
|
|
2503
|
-
mode: "exported-bytes-only",
|
|
2504
|
-
},
|
|
2505
|
-
};
|
|
2506
|
-
}
|
|
2507
|
-
|
|
2508
|
-
function toEditorSessionState(
|
|
2509
|
-
sessionStateOrSnapshot: EditorSessionState | PersistedEditorSnapshot,
|
|
2510
|
-
): EditorSessionState {
|
|
2511
|
-
return "sessionVersion" in sessionStateOrSnapshot
|
|
2512
|
-
? sessionStateOrSnapshot
|
|
2513
|
-
: editorSessionStateFromPersistedSnapshot(sessionStateOrSnapshot);
|
|
2514
|
-
}
|
|
2515
|
-
|
|
2516
|
-
function createImportedCanonicalDocument(input: {
|
|
2517
|
-
documentId: string;
|
|
2518
|
-
timestamp: string;
|
|
2519
|
-
numbering: CanonicalDocumentEnvelope["numbering"];
|
|
2520
|
-
media: CanonicalDocumentEnvelope["media"];
|
|
2521
|
-
content: CanonicalDocumentEnvelope["content"];
|
|
2522
|
-
subParts?: SubPartsCatalog;
|
|
2523
|
-
parsedStyles?: ParseStylesResult;
|
|
2524
|
-
fontTable?: CanonicalDocumentEnvelope["fontTable"];
|
|
2525
|
-
preservation: CanonicalDocumentEnvelope["preservation"];
|
|
2526
|
-
diagnostics: CanonicalDocumentEnvelope["diagnostics"];
|
|
2527
|
-
review: CanonicalDocumentEnvelope["review"];
|
|
2528
|
-
}): CanonicalDocumentEnvelope {
|
|
2529
|
-
const numbering = ensureImportedNumberingCatalogSupportsContent(
|
|
2530
|
-
input.numbering,
|
|
2531
|
-
input.content,
|
|
2532
|
-
);
|
|
2533
|
-
|
|
2534
|
-
// Use package-backed style catalog when available; fall back to synthetic
|
|
2535
|
-
// styles derived from referenced styleId values when styles.xml is missing
|
|
2536
|
-
// or could not be parsed.
|
|
2537
|
-
const styles = buildStylesCatalog(input.parsedStyles, input.content, input.subParts);
|
|
2538
|
-
|
|
2539
|
-
return {
|
|
2540
|
-
schemaVersion: "cds/1.0.0",
|
|
2541
|
-
docId: createCanonicalDocumentId(input.documentId),
|
|
2542
|
-
createdAt: input.timestamp,
|
|
2543
|
-
updatedAt: input.timestamp,
|
|
2544
|
-
metadata: {
|
|
2545
|
-
customProperties: {},
|
|
2546
|
-
},
|
|
2547
|
-
styles,
|
|
2548
|
-
numbering,
|
|
2549
|
-
media: input.media,
|
|
2550
|
-
content: input.content,
|
|
2551
|
-
review: input.review,
|
|
2552
|
-
preservation: input.preservation,
|
|
2553
|
-
diagnostics: input.diagnostics,
|
|
2554
|
-
...(input.subParts !== undefined ? { subParts: input.subParts } : {}),
|
|
2555
|
-
...(input.fontTable !== undefined ? { fontTable: input.fontTable } : {}),
|
|
2556
|
-
};
|
|
2557
|
-
}
|
|
2558
|
-
|
|
2559
|
-
type SubPartOpaqueImportState = {
|
|
2560
|
-
nextFragmentIndex: number;
|
|
2561
|
-
nextWarningIndex: number;
|
|
2562
|
-
nextDiagnosticIndex: number;
|
|
2563
|
-
cursor: number;
|
|
2564
|
-
};
|
|
2565
|
-
|
|
2566
|
-
function createSubPartOpaqueImportState(
|
|
2567
|
-
opaqueFragments: Record<string, OpaqueFragmentRecord>,
|
|
2568
|
-
warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
|
|
2569
|
-
): SubPartOpaqueImportState {
|
|
2570
|
-
const maxByPrefix = (values: Iterable<string>, prefix: string): number => {
|
|
2571
|
-
let max = 0;
|
|
2572
|
-
for (const value of values) {
|
|
2573
|
-
const match = new RegExp(`^${prefix}(\\d+)$`).exec(value);
|
|
2574
|
-
if (!match) {
|
|
2575
|
-
continue;
|
|
2576
|
-
}
|
|
2577
|
-
const parsed = Number.parseInt(match[1] ?? "0", 10);
|
|
2578
|
-
if (Number.isFinite(parsed) && parsed > max) {
|
|
2579
|
-
max = parsed;
|
|
2580
|
-
}
|
|
2581
|
-
}
|
|
2582
|
-
return max;
|
|
2583
|
-
};
|
|
2584
|
-
|
|
2585
|
-
const maxCursor = Object.values(opaqueFragments).reduce(
|
|
2586
|
-
(currentMax, fragment) => Math.max(currentMax, fragment.lastKnownRange?.to ?? 0),
|
|
2587
|
-
0,
|
|
2588
|
-
);
|
|
2589
|
-
|
|
2590
|
-
return {
|
|
2591
|
-
nextFragmentIndex:
|
|
2592
|
-
maxByPrefix(Object.keys(opaqueFragments), "fragment:import-") + 1,
|
|
2593
|
-
nextWarningIndex:
|
|
2594
|
-
maxByPrefix(
|
|
2595
|
-
[
|
|
2596
|
-
...Object.values(opaqueFragments).map((fragment) => fragment.warningId),
|
|
2597
|
-
...warnings.map((warning) => warning.warningId),
|
|
2598
|
-
],
|
|
2599
|
-
"warning:import-",
|
|
2600
|
-
) + 1,
|
|
2601
|
-
nextDiagnosticIndex:
|
|
2602
|
-
maxByPrefix(warnings.map((warning) => warning.diagnosticId), "diagnostic:import-") + 1,
|
|
2603
|
-
cursor: maxCursor,
|
|
2604
|
-
};
|
|
2605
|
-
}
|
|
2606
|
-
|
|
2607
|
-
function normalizeSubPartOpaqueBlocks(
|
|
2608
|
-
blocks: BlockNode[],
|
|
2609
|
-
opaqueFragments: Record<string, OpaqueFragmentRecord>,
|
|
2610
|
-
warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
|
|
2611
|
-
packagePartName: string,
|
|
2612
|
-
state: SubPartOpaqueImportState,
|
|
2613
|
-
): BlockNode[] {
|
|
2614
|
-
return blocks.map((block) => {
|
|
2615
|
-
if (block.type !== "opaque_block" || typeof block.rawXml !== "string") {
|
|
2616
|
-
return block;
|
|
2617
|
-
}
|
|
2618
|
-
return recordImportedOpaqueBlock(
|
|
2619
|
-
block.rawXml,
|
|
2620
|
-
opaqueFragments,
|
|
2621
|
-
warnings,
|
|
2622
|
-
packagePartName,
|
|
2623
|
-
state,
|
|
2624
|
-
);
|
|
2625
|
-
});
|
|
2626
|
-
}
|
|
2627
|
-
|
|
2628
|
-
function normalizeFootnoteCollectionOpaqueBlocks(
|
|
2629
|
-
collection: FootnoteCollection | undefined,
|
|
2630
|
-
kind: "footnote" | "endnote",
|
|
2631
|
-
opaqueFragments: Record<string, OpaqueFragmentRecord>,
|
|
2632
|
-
warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
|
|
2633
|
-
packagePartName: string,
|
|
2634
|
-
state: SubPartOpaqueImportState,
|
|
2635
|
-
): void {
|
|
2636
|
-
if (!collection) {
|
|
2637
|
-
return;
|
|
2638
|
-
}
|
|
2639
|
-
const notes = kind === "footnote" ? collection.footnotes : collection.endnotes;
|
|
2640
|
-
for (const definition of Object.values(notes)) {
|
|
2641
|
-
definition.blocks = normalizeSubPartOpaqueBlocks(
|
|
2642
|
-
definition.blocks,
|
|
2643
|
-
opaqueFragments,
|
|
2644
|
-
warnings,
|
|
2645
|
-
packagePartName,
|
|
2646
|
-
state,
|
|
2647
|
-
);
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
|
|
2651
|
-
function recordImportedOpaqueBlock(
|
|
2652
|
-
rawXml: string,
|
|
2653
|
-
opaqueFragments: Record<string, OpaqueFragmentRecord>,
|
|
2654
|
-
warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
|
|
2655
|
-
packagePartName: string,
|
|
2656
|
-
state: SubPartOpaqueImportState,
|
|
2657
|
-
): BlockNode {
|
|
2658
|
-
const fragmentId = `fragment:import-${state.nextFragmentIndex}`;
|
|
2659
|
-
state.nextFragmentIndex += 1;
|
|
2660
|
-
const warningId = `warning:import-${state.nextWarningIndex}`;
|
|
2661
|
-
state.nextWarningIndex += 1;
|
|
2662
|
-
const diagnosticId = `diagnostic:import-${state.nextDiagnosticIndex}`;
|
|
2663
|
-
state.nextDiagnosticIndex += 1;
|
|
2664
|
-
|
|
2665
|
-
const rangeStart = state.cursor;
|
|
2666
|
-
const rangeEnd = state.cursor + 1;
|
|
2667
|
-
state.cursor = rangeEnd;
|
|
2668
|
-
|
|
2669
|
-
opaqueFragments[fragmentId] = {
|
|
2670
|
-
fragmentId,
|
|
2671
|
-
payloadKind: "xml-subtree",
|
|
2672
|
-
payloadReference: rawXml,
|
|
2673
|
-
featureClass: "preserve-only",
|
|
2674
|
-
lastKnownRange: {
|
|
2675
|
-
from: rangeStart,
|
|
2676
|
-
to: rangeEnd,
|
|
2677
|
-
},
|
|
2678
|
-
warningId,
|
|
2679
|
-
packagePartName,
|
|
2680
|
-
};
|
|
2681
|
-
warnings.push({
|
|
2682
|
-
diagnosticId,
|
|
2683
|
-
warningId,
|
|
2684
|
-
source: "import",
|
|
2685
|
-
message: "Unsupported sub-part OOXML was preserved as an opaque placeholder.",
|
|
2686
|
-
});
|
|
2687
|
-
|
|
2688
|
-
return {
|
|
2689
|
-
type: "opaque_block",
|
|
2690
|
-
fragmentId,
|
|
2691
|
-
warningId,
|
|
2692
|
-
rawXml,
|
|
2693
|
-
};
|
|
2694
|
-
}
|
|
2695
|
-
|
|
2696
|
-
function mergeSecondaryStoryMediaCatalog(
|
|
2697
|
-
media: MediaCatalog,
|
|
2698
|
-
input: {
|
|
2699
|
-
headers: readonly HeaderDocument[];
|
|
2700
|
-
footers: readonly FooterDocument[];
|
|
2701
|
-
footnoteCollection?: FootnoteCollection;
|
|
2702
|
-
mediaParts: ReadonlyMap<string, { path: string; contentType: string }>;
|
|
2703
|
-
},
|
|
2704
|
-
): MediaCatalog {
|
|
2705
|
-
const items = { ...media.items };
|
|
2706
|
-
let changed = false;
|
|
2707
|
-
|
|
2708
|
-
const registerMediaItem = (
|
|
2709
|
-
mediaId: string,
|
|
2710
|
-
record: Omit<NonNullable<MediaCatalog["items"][string]>, "mediaId">,
|
|
2711
|
-
) => {
|
|
2712
|
-
const existing = items[mediaId];
|
|
2713
|
-
items[mediaId] = existing
|
|
2714
|
-
? {
|
|
2715
|
-
...existing,
|
|
2716
|
-
...record,
|
|
2717
|
-
mediaId,
|
|
2718
|
-
}
|
|
2719
|
-
: {
|
|
2720
|
-
mediaId,
|
|
2721
|
-
...record,
|
|
2722
|
-
};
|
|
2723
|
-
changed = true;
|
|
2724
|
-
};
|
|
2725
|
-
|
|
2726
|
-
const visitInline = (node: InlineNode) => {
|
|
2727
|
-
if (node.type === "image") {
|
|
2728
|
-
const packagePartName = `/${node.mediaId.slice("media:".length)}`;
|
|
2729
|
-
registerMediaItem(node.mediaId, {
|
|
2730
|
-
contentType:
|
|
2731
|
-
items[node.mediaId]?.contentType ??
|
|
2732
|
-
input.mediaParts.get(packagePartName)?.contentType ??
|
|
2733
|
-
"application/octet-stream",
|
|
2734
|
-
filename: packagePartName.slice(packagePartName.lastIndexOf("/") + 1) || "image.bin",
|
|
2735
|
-
packagePartName,
|
|
2736
|
-
...(node.altText ? { altText: node.altText } : {}),
|
|
2737
|
-
});
|
|
2738
|
-
return;
|
|
2739
|
-
}
|
|
2740
|
-
if (node.type === "drawing_frame" && node.content.type === "picture" && node.content.mediaId) {
|
|
2741
|
-
const packagePartName =
|
|
2742
|
-
typeof node.content.packagePartName === "string" && node.content.packagePartName.length > 0
|
|
2743
|
-
? node.content.packagePartName
|
|
2744
|
-
: `/${node.content.mediaId.slice("media:".length)}`;
|
|
2745
|
-
registerMediaItem(node.content.mediaId, {
|
|
2746
|
-
contentType:
|
|
2747
|
-
items[node.content.mediaId]?.contentType ??
|
|
2748
|
-
input.mediaParts.get(packagePartName)?.contentType ??
|
|
2749
|
-
"application/octet-stream",
|
|
2750
|
-
filename: packagePartName.slice(packagePartName.lastIndexOf("/") + 1) || "image.bin",
|
|
2751
|
-
packagePartName,
|
|
2752
|
-
relationshipId: node.content.blipRef,
|
|
2753
|
-
...(node.anchor.docPr?.descr ? { altText: node.anchor.docPr.descr } : {}),
|
|
2754
|
-
widthEmu: node.anchor.extent.widthEmu,
|
|
2755
|
-
heightEmu: node.anchor.extent.heightEmu,
|
|
2756
|
-
});
|
|
2757
|
-
return;
|
|
2758
|
-
}
|
|
2759
|
-
if (node.type === "hyperlink" || node.type === "field") {
|
|
2760
|
-
for (const child of node.children) {
|
|
2761
|
-
visitInline(child);
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
};
|
|
2765
|
-
|
|
2766
|
-
const visitBlocks = (blocks: ReadonlyArray<BlockNode>) => {
|
|
2767
|
-
for (const block of blocks) {
|
|
2768
|
-
if (block.type === "paragraph") {
|
|
2769
|
-
for (const child of block.children) {
|
|
2770
|
-
visitInline(child);
|
|
2771
|
-
}
|
|
2772
|
-
continue;
|
|
2773
|
-
}
|
|
2774
|
-
if (block.type === "table") {
|
|
2775
|
-
for (const row of block.rows) {
|
|
2776
|
-
for (const cell of row.cells) {
|
|
2777
|
-
visitBlocks(cell.children);
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
continue;
|
|
2781
|
-
}
|
|
2782
|
-
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
2783
|
-
visitBlocks(block.children);
|
|
2784
|
-
}
|
|
2785
|
-
}
|
|
2786
|
-
};
|
|
2787
|
-
|
|
2788
|
-
for (const header of input.headers) {
|
|
2789
|
-
visitBlocks(header.blocks);
|
|
2790
|
-
}
|
|
2791
|
-
for (const footer of input.footers) {
|
|
2792
|
-
visitBlocks(footer.blocks);
|
|
2793
|
-
}
|
|
2794
|
-
if (input.footnoteCollection) {
|
|
2795
|
-
for (const note of Object.values(input.footnoteCollection.footnotes)) {
|
|
2796
|
-
visitBlocks(note.blocks);
|
|
2797
|
-
}
|
|
2798
|
-
for (const note of Object.values(input.footnoteCollection.endnotes)) {
|
|
2799
|
-
visitBlocks(note.blocks);
|
|
2800
|
-
}
|
|
2801
|
-
}
|
|
2802
|
-
|
|
2803
|
-
return changed ? { ...media, items } : media;
|
|
2804
|
-
}
|
|
2805
|
-
|
|
2806
|
-
// Canonical model styleId validation pattern — styleIds that don't match
|
|
2807
|
-
// are excluded from the catalog to avoid snapshot validation failures.
|
|
2808
|
-
const VALID_STYLE_ID = /^[A-Za-z_][A-Za-z0-9._-]{0,127}$/;
|
|
2809
|
-
|
|
2810
|
-
function buildStylesCatalog(
|
|
2811
|
-
parsedStyles: ParseStylesResult | undefined,
|
|
2812
|
-
content: CanonicalDocumentEnvelope["content"],
|
|
2813
|
-
subParts?: SubPartsCatalog,
|
|
2814
|
-
): CanonicalDocumentEnvelope["styles"] {
|
|
2815
|
-
if (parsedStyles?.fromPackage) {
|
|
2816
|
-
// Package-backed catalog: filter entries whose styleId does not satisfy
|
|
2817
|
-
// the canonical model pattern (e.g. numeric-only ids from Word).
|
|
2818
|
-
const catalog = filterValidStyleIds(parsedStyles.catalog);
|
|
2819
|
-
|
|
2820
|
-
// Merge in any referenced styleIds that the package styles.xml did not
|
|
2821
|
-
// define (rare but defensive).
|
|
2822
|
-
const referencedIds = collectReferencedParagraphStyleIds(content, subParts);
|
|
2823
|
-
for (const styleId of referencedIds) {
|
|
2824
|
-
if (!catalog.paragraphs[styleId] && VALID_STYLE_ID.test(styleId)) {
|
|
2825
|
-
catalog.paragraphs[styleId] = {
|
|
2826
|
-
styleId,
|
|
2827
|
-
displayName: styleId,
|
|
2828
|
-
kind: "paragraph",
|
|
2829
|
-
isDefault: styleId === "Normal",
|
|
2830
|
-
};
|
|
2831
|
-
}
|
|
2832
|
-
}
|
|
2833
|
-
return {
|
|
2834
|
-
...catalog,
|
|
2835
|
-
fromPackage: true,
|
|
2836
|
-
};
|
|
2837
|
-
}
|
|
2838
|
-
|
|
2839
|
-
// Synthetic fallback: no styles.xml available
|
|
2840
|
-
const paragraphStyles = Object.fromEntries(
|
|
2841
|
-
[...collectReferencedParagraphStyleIds(content, subParts)]
|
|
2842
|
-
.sort((left, right) => left.localeCompare(right))
|
|
2843
|
-
.filter((styleId) => VALID_STYLE_ID.test(styleId))
|
|
2844
|
-
.map((styleId) => [
|
|
2845
|
-
styleId,
|
|
2846
|
-
{
|
|
2847
|
-
styleId,
|
|
2848
|
-
displayName: styleId,
|
|
2849
|
-
kind: "paragraph" as const,
|
|
2850
|
-
isDefault: styleId === "Normal",
|
|
2851
|
-
},
|
|
2852
|
-
]),
|
|
2853
|
-
);
|
|
2854
|
-
return {
|
|
2855
|
-
paragraphs: paragraphStyles,
|
|
2856
|
-
characters: {},
|
|
2857
|
-
tables: {},
|
|
2858
|
-
fromPackage: false,
|
|
2859
|
-
};
|
|
2860
|
-
}
|
|
2861
|
-
|
|
2862
|
-
function filterValidStyleIds(
|
|
2863
|
-
catalog: CanonicalDocumentEnvelope["styles"],
|
|
2864
|
-
): CanonicalDocumentEnvelope["styles"] {
|
|
2865
|
-
const filterRecord = <T extends { styleId: string }>(
|
|
2866
|
-
record: Record<string, T>,
|
|
2867
|
-
): Record<string, T> => {
|
|
2868
|
-
const result: Record<string, T> = {};
|
|
2869
|
-
for (const [key, value] of Object.entries(record)) {
|
|
2870
|
-
if (VALID_STYLE_ID.test(key)) {
|
|
2871
|
-
result[key] = value;
|
|
2872
|
-
}
|
|
2873
|
-
}
|
|
2874
|
-
return result;
|
|
2875
|
-
};
|
|
2876
|
-
|
|
2877
|
-
return {
|
|
2878
|
-
paragraphs: filterRecord(catalog.paragraphs),
|
|
2879
|
-
characters: filterRecord(catalog.characters),
|
|
2880
|
-
tables: filterRecord(catalog.tables),
|
|
2881
|
-
...(catalog.latentStyles ? { latentStyles: catalog.latentStyles } : {}),
|
|
2882
|
-
...(catalog.docDefaults ? { docDefaults: catalog.docDefaults } : {}),
|
|
2883
|
-
...(catalog.fromPackage !== undefined ? { fromPackage: catalog.fromPackage } : {}),
|
|
2884
|
-
};
|
|
2885
|
-
}
|
|
2886
|
-
|
|
2887
|
-
function ensureImportedNumberingCatalogSupportsContent(
|
|
2888
|
-
catalog: NumberingCatalog,
|
|
2889
|
-
content: CanonicalDocumentEnvelope["content"],
|
|
2890
|
-
): NumberingCatalog {
|
|
2891
|
-
if (
|
|
2892
|
-
catalog.instances[DOCX_NULL_NUMBERING_INSTANCE_ID] ||
|
|
2893
|
-
!collectReferencedNumberingInstanceIds(content).has(DOCX_NULL_NUMBERING_INSTANCE_ID)
|
|
2894
|
-
) {
|
|
2895
|
-
return catalog;
|
|
2896
|
-
}
|
|
2897
|
-
|
|
2898
|
-
const syntheticNullCatalog = createSyntheticDocxNullNumberingCatalog();
|
|
2899
|
-
return {
|
|
2900
|
-
abstractDefinitions: {
|
|
2901
|
-
...catalog.abstractDefinitions,
|
|
2902
|
-
...syntheticNullCatalog.abstractDefinitions,
|
|
2903
|
-
},
|
|
2904
|
-
instances: {
|
|
2905
|
-
...catalog.instances,
|
|
2906
|
-
...syntheticNullCatalog.instances,
|
|
2907
|
-
},
|
|
2908
|
-
...(catalog.numPicBullets !== undefined ? { numPicBullets: catalog.numPicBullets } : {}),
|
|
2909
|
-
};
|
|
2910
|
-
}
|
|
2911
|
-
|
|
2912
|
-
function collectReferencedNumberingInstanceIds(
|
|
2913
|
-
content: CanonicalDocumentEnvelope["content"],
|
|
2914
|
-
): Set<string> {
|
|
2915
|
-
const numberingInstanceIds = new Set<string>();
|
|
2916
|
-
|
|
2917
|
-
const visitBlocks = (blocks: ReadonlyArray<BlockNode>) => {
|
|
2918
|
-
for (const block of blocks) {
|
|
2919
|
-
if (block.type === "paragraph" && block.numbering?.numberingInstanceId) {
|
|
2920
|
-
numberingInstanceIds.add(block.numbering.numberingInstanceId);
|
|
2921
|
-
}
|
|
2922
|
-
if (block.type === "table") {
|
|
2923
|
-
for (const row of block.rows) {
|
|
2924
|
-
for (const cell of row.cells) {
|
|
2925
|
-
visitBlocks(cell.children);
|
|
2926
|
-
}
|
|
2927
|
-
}
|
|
2928
|
-
} else if (block.type === "sdt" || block.type === "custom_xml") {
|
|
2929
|
-
visitBlocks(block.children);
|
|
2930
|
-
}
|
|
2931
|
-
}
|
|
2932
|
-
};
|
|
2933
|
-
|
|
2934
|
-
visitBlocks(content.children);
|
|
2935
|
-
return numberingInstanceIds;
|
|
2936
|
-
}
|
|
2937
|
-
|
|
2938
|
-
function collectReferencedParagraphStyleIds(
|
|
2939
|
-
content: CanonicalDocumentEnvelope["content"],
|
|
2940
|
-
subParts?: SubPartsCatalog,
|
|
2941
|
-
): Set<string> {
|
|
2942
|
-
const styleIds = new Set<string>();
|
|
2943
|
-
|
|
2944
|
-
const visitBlocks = (blocks: ReadonlyArray<BlockNode>) => {
|
|
2945
|
-
for (const block of blocks) {
|
|
2946
|
-
if ("styleId" in block && typeof block.styleId === "string" && block.styleId.length > 0) {
|
|
2947
|
-
styleIds.add(block.styleId);
|
|
2948
|
-
}
|
|
2949
|
-
if (block.type === "table") {
|
|
2950
|
-
for (const row of block.rows) {
|
|
2951
|
-
for (const cell of row.cells) {
|
|
2952
|
-
visitBlocks(cell.children);
|
|
2953
|
-
}
|
|
2954
|
-
}
|
|
2955
|
-
} else if (block.type === "sdt" || block.type === "custom_xml") {
|
|
2956
|
-
visitBlocks(block.children);
|
|
2957
|
-
}
|
|
2958
|
-
}
|
|
2959
|
-
};
|
|
2960
|
-
|
|
2961
|
-
visitBlocks(content.children);
|
|
2962
|
-
if (subParts) {
|
|
2963
|
-
for (const header of subParts.headers) {
|
|
2964
|
-
visitBlocks(header.blocks);
|
|
2965
|
-
}
|
|
2966
|
-
for (const footer of subParts.footers) {
|
|
2967
|
-
visitBlocks(footer.blocks);
|
|
2968
|
-
}
|
|
2969
|
-
if (subParts.footnoteCollection) {
|
|
2970
|
-
for (const note of Object.values(subParts.footnoteCollection.footnotes)) {
|
|
2971
|
-
visitBlocks(note.blocks);
|
|
2972
|
-
}
|
|
2973
|
-
for (const note of Object.values(subParts.footnoteCollection.endnotes)) {
|
|
2974
|
-
visitBlocks(note.blocks);
|
|
2975
|
-
}
|
|
2976
|
-
}
|
|
2977
|
-
}
|
|
2978
|
-
|
|
2979
|
-
return styleIds;
|
|
2980
|
-
}
|
|
2981
|
-
|
|
2982
|
-
function createImportedSnapshot(input: {
|
|
2983
|
-
documentId: string;
|
|
2984
|
-
editorBuild: string;
|
|
2985
|
-
timestamp: string;
|
|
2986
|
-
document: CanonicalDocumentEnvelope;
|
|
2987
|
-
compatibility: PersistedEditorSnapshot["compatibility"];
|
|
2988
|
-
protectionSnapshot: ProtectionSnapshot;
|
|
2989
|
-
sourcePackage?: PersistedEditorSnapshot["sourcePackage"];
|
|
2990
|
-
workflowOverlay?: PersistedEditorSnapshot["workflowOverlay"];
|
|
2991
|
-
workflowMetadata?: PersistedEditorSnapshot["workflowMetadata"];
|
|
2992
|
-
}): PersistedEditorSnapshot {
|
|
2993
|
-
return {
|
|
2994
|
-
snapshotVersion: "persisted-editor-snapshot/2",
|
|
2995
|
-
schemaVersion: input.document.schemaVersion,
|
|
2996
|
-
documentId: input.documentId,
|
|
2997
|
-
docId: input.document.docId,
|
|
2998
|
-
createdAt: input.document.createdAt,
|
|
2999
|
-
updatedAt: input.document.updatedAt,
|
|
3000
|
-
savedAt: input.timestamp,
|
|
3001
|
-
editorBuild: input.editorBuild,
|
|
3002
|
-
canonicalDocument: input.document,
|
|
3003
|
-
compatibility: input.compatibility,
|
|
3004
|
-
warningLog: input.compatibility.warnings,
|
|
3005
|
-
protectionSnapshot: input.protectionSnapshot,
|
|
3006
|
-
sourcePackage: input.sourcePackage,
|
|
3007
|
-
workflowOverlay: input.workflowOverlay,
|
|
3008
|
-
workflowMetadata: input.workflowMetadata,
|
|
3009
|
-
};
|
|
3010
|
-
}
|
|
3011
|
-
|
|
3012
|
-
function toPublicAnchorProjection(
|
|
3013
|
-
anchor: InternalEditorAnchorProjection,
|
|
3014
|
-
): PublicEditorAnchorProjection {
|
|
3015
|
-
switch (anchor.kind) {
|
|
3016
|
-
case "range":
|
|
3017
|
-
return {
|
|
3018
|
-
kind: "range",
|
|
3019
|
-
from: anchor.range.from,
|
|
3020
|
-
to: anchor.range.to,
|
|
3021
|
-
assoc: anchor.assoc,
|
|
3022
|
-
};
|
|
3023
|
-
case "node":
|
|
3024
|
-
return {
|
|
3025
|
-
kind: "node",
|
|
3026
|
-
at: anchor.at,
|
|
3027
|
-
assoc: anchor.assoc,
|
|
3028
|
-
};
|
|
3029
|
-
case "detached":
|
|
3030
|
-
return {
|
|
3031
|
-
kind: "detached",
|
|
3032
|
-
lastKnownRange: anchor.lastKnownRange,
|
|
3033
|
-
reason: anchor.reason,
|
|
3034
|
-
};
|
|
3035
|
-
}
|
|
3036
|
-
}
|
|
3037
|
-
|
|
3038
|
-
function toPublicCompatibilityFeatureEntry(entry: InternalCompatibilityFeatureEntry) {
|
|
3039
|
-
return {
|
|
3040
|
-
...entry,
|
|
3041
|
-
affectedAnchor: entry.affectedAnchor
|
|
3042
|
-
? toPublicAnchorProjection(entry.affectedAnchor)
|
|
3043
|
-
: undefined,
|
|
3044
|
-
};
|
|
3045
|
-
}
|
|
3046
|
-
|
|
3047
|
-
function toPublicWarning(warning: InternalEditorWarning): PublicEditorWarning {
|
|
3048
|
-
return {
|
|
3049
|
-
...warning,
|
|
3050
|
-
affectedAnchor: warning.affectedAnchor
|
|
3051
|
-
? toPublicAnchorProjection(warning.affectedAnchor)
|
|
3052
|
-
: undefined,
|
|
3053
|
-
};
|
|
3054
|
-
}
|
|
3055
|
-
|
|
3056
|
-
function toPublicError(error: InternalEditorError): EditorError {
|
|
3057
|
-
return { ...error };
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
function toPublicCompatibilityReport(
|
|
3061
|
-
report: InternalCompatibilityReport,
|
|
3062
|
-
): PublicCompatibilityReport {
|
|
3063
|
-
return {
|
|
3064
|
-
reportVersion: report.reportVersion,
|
|
3065
|
-
generatedAt: report.generatedAt,
|
|
3066
|
-
blockExport: report.blockExport,
|
|
3067
|
-
featureEntries: report.featureEntries.map((entry) =>
|
|
3068
|
-
toPublicCompatibilityFeatureEntry(entry),
|
|
3069
|
-
),
|
|
3070
|
-
warnings: report.warnings.map((warning) => toPublicWarning(warning)),
|
|
3071
|
-
errors: report.errors.map((error) => toPublicError(error)),
|
|
3072
|
-
};
|
|
3073
|
-
}
|
|
3074
|
-
|
|
3075
|
-
function createDiagnosticsSession(
|
|
3076
|
-
options: LoadDocxEditorSessionOptions,
|
|
3077
|
-
diagnostics: ImportDiagnosticsResult,
|
|
3078
|
-
): LoadedDocxEditorSession {
|
|
3079
|
-
const timestamp = new Date().toISOString();
|
|
3080
|
-
const editorBuild =
|
|
3081
|
-
typeof options.editorBuild === "string" && options.editorBuild.length > 0
|
|
3082
|
-
? options.editorBuild
|
|
3083
|
-
: "dev";
|
|
3084
|
-
const runtime = createReadOnlyDiagnosticsRuntime({
|
|
3085
|
-
documentId: options.documentId,
|
|
3086
|
-
sourceLabel: options.sourceLabel,
|
|
3087
|
-
editorBuild,
|
|
3088
|
-
generatedAt: timestamp,
|
|
3089
|
-
diagnostics,
|
|
3090
|
-
});
|
|
3091
|
-
const initialSnapshot = runtime.getPersistedSnapshot();
|
|
3092
|
-
const initialSessionState = editorSessionStateFromPersistedSnapshot(initialSnapshot);
|
|
3093
|
-
|
|
3094
|
-
return {
|
|
3095
|
-
initialSessionState,
|
|
3096
|
-
initialSnapshot,
|
|
3097
|
-
fatalError: diagnostics.fatalError,
|
|
3098
|
-
readOnly: true,
|
|
3099
|
-
protectionSnapshot: EMPTY_PROTECTION_SNAPSHOT,
|
|
3100
|
-
exportDocx: async (_sessionState, exportOptions) => runtime.exportDocx(exportOptions),
|
|
3101
|
-
};
|
|
3102
|
-
}
|
|
3103
|
-
|
|
3104
|
-
function createImportDiagnosticsFromError(error: unknown): ImportDiagnosticsResult {
|
|
3105
|
-
if (isPackageImportError(error)) {
|
|
3106
|
-
return createPackageImportDiagnostics({
|
|
3107
|
-
issue: classifyCorruptPackageError(error),
|
|
3108
|
-
});
|
|
3109
|
-
}
|
|
3110
|
-
|
|
3111
|
-
return createValidationImportDiagnostics({
|
|
3112
|
-
message:
|
|
3113
|
-
error instanceof Error
|
|
3114
|
-
? error.message
|
|
3115
|
-
: "DOCX import failed during validation.",
|
|
3116
|
-
});
|
|
3117
|
-
}
|
|
3118
|
-
|
|
3119
|
-
function normalizeImportedRevisionRecords(
|
|
3120
|
-
parsed: ParsedRevisionsResult,
|
|
3121
|
-
content: CanonicalDocumentEnvelope["content"],
|
|
3122
|
-
opaqueFragments: Record<string, OpaqueFragmentRecord>,
|
|
3123
|
-
): ParsedRevisionsResult {
|
|
3124
|
-
const opaqueRanges = Object.values(opaqueFragments).map((fragment) => fragment.lastKnownRange);
|
|
3125
|
-
const paragraphRanges = collectCanonicalParagraphRanges(content);
|
|
3126
|
-
if (opaqueRanges.length === 0) {
|
|
3127
|
-
return {
|
|
3128
|
-
...parsed,
|
|
3129
|
-
revisions: parsed.revisions.map((revision) => {
|
|
3130
|
-
if (revision.anchor.kind !== "range" || revision.metadata.preserveOnlyReason) {
|
|
3131
|
-
return revision;
|
|
3132
|
-
}
|
|
3133
|
-
|
|
3134
|
-
const preserveOnlyReason = getStructuralPreserveOnlyReason(
|
|
3135
|
-
revision,
|
|
3136
|
-
paragraphRanges,
|
|
3137
|
-
);
|
|
3138
|
-
if (!preserveOnlyReason) {
|
|
3139
|
-
return revision;
|
|
3140
|
-
}
|
|
3141
|
-
|
|
3142
|
-
return {
|
|
3143
|
-
...revision,
|
|
3144
|
-
metadata: {
|
|
3145
|
-
...revision.metadata,
|
|
3146
|
-
preserveOnlyReason,
|
|
3147
|
-
},
|
|
3148
|
-
};
|
|
3149
|
-
}),
|
|
3150
|
-
};
|
|
3151
|
-
}
|
|
3152
|
-
|
|
3153
|
-
return {
|
|
3154
|
-
...parsed,
|
|
3155
|
-
revisions: parsed.revisions.map((revision) => {
|
|
3156
|
-
const { anchor } = revision;
|
|
3157
|
-
if (anchor.kind !== "range" || revision.metadata.preserveOnlyReason) {
|
|
3158
|
-
return revision;
|
|
3159
|
-
}
|
|
3160
|
-
|
|
3161
|
-
const preserveOnlyReason =
|
|
3162
|
-
getStructuralPreserveOnlyReason(revision, paragraphRanges) ??
|
|
3163
|
-
(opaqueRanges.some((range) => rangesIntersect(range, anchor.range))
|
|
3164
|
-
? "Imported revision overlaps preserve-only OOXML and remains preserve-only."
|
|
3165
|
-
: undefined);
|
|
3166
|
-
|
|
3167
|
-
if (!preserveOnlyReason) {
|
|
3168
|
-
return revision;
|
|
3169
|
-
}
|
|
3170
|
-
|
|
3171
|
-
return {
|
|
3172
|
-
...revision,
|
|
3173
|
-
metadata: {
|
|
3174
|
-
...revision.metadata,
|
|
3175
|
-
preserveOnlyReason,
|
|
3176
|
-
},
|
|
3177
|
-
};
|
|
3178
|
-
}),
|
|
3179
|
-
};
|
|
3180
|
-
}
|
|
3181
|
-
|
|
3182
|
-
export function normalizeImportedCommentThreads(
|
|
3183
|
-
parsed: ParsedCommentsResult,
|
|
3184
|
-
opaqueFragments: Record<string, OpaqueFragmentRecord>,
|
|
3185
|
-
revisions: readonly ReviewRevisionRecord[],
|
|
3186
|
-
): NormalizedImportedCommentsResult {
|
|
3187
|
-
const opaqueRanges = Object.values(opaqueFragments).map((fragment) => fragment.lastKnownRange);
|
|
3188
|
-
// Use getRevisionActionability() — not the raw preserveOnlyReason flag — so
|
|
3189
|
-
// Lane 7b-promoted shapes (cellIns/cellDel/cellMerge, linked-pair moves) that
|
|
3190
|
-
// still carry a default preserveOnlyReason aren't treated as preserve-only
|
|
3191
|
-
// for comment-anchor detachment. See revision-types.ts:192-200.
|
|
3192
|
-
const preserveOnlyRevisionRanges = revisions.flatMap((revision) => {
|
|
3193
|
-
if (
|
|
3194
|
-
revision.anchor.kind !== "range" ||
|
|
3195
|
-
getRevisionActionability(revision) !== "preserve-only"
|
|
3196
|
-
) {
|
|
3197
|
-
return [];
|
|
3198
|
-
}
|
|
3199
|
-
return [revision.anchor.range];
|
|
3200
|
-
});
|
|
3201
|
-
const preserveOnlyCommentIds = new Set(parsed.diagnostics.map((diagnostic) => diagnostic.commentId));
|
|
3202
|
-
const additionalDiagnostics: CommentImportDiagnostic[] = [];
|
|
3203
|
-
const normalizedThreads: CommentThread[] = parsed.threads.map((thread) => {
|
|
3204
|
-
const { anchor } = thread;
|
|
3205
|
-
if (anchor.kind !== "range") {
|
|
3206
|
-
preserveOnlyCommentIds.add(thread.commentId);
|
|
3207
|
-
return thread;
|
|
3208
|
-
}
|
|
3209
|
-
|
|
3210
|
-
const opaqueOverlap = opaqueRanges.some((range) => rangesIntersect(range, anchor.range));
|
|
3211
|
-
if (opaqueOverlap) {
|
|
3212
|
-
preserveOnlyCommentIds.add(thread.commentId);
|
|
3213
|
-
additionalDiagnostics.push({
|
|
3214
|
-
commentId: thread.commentId,
|
|
3215
|
-
code: "opaque_anchor_preserve_only",
|
|
3216
|
-
message:
|
|
3217
|
-
"Comment anchor intersects preserve-only OOXML content. Thread is visible but detached; anchor cannot be safely remapped.",
|
|
3218
|
-
featureClass: "preserve-only",
|
|
3219
|
-
detachedReason: "opaque-region" as const,
|
|
3220
|
-
actionabilityNote: "The comment body is preserved. The anchor overlaps opaque content that the editor cannot safely modify.",
|
|
3221
|
-
});
|
|
3222
|
-
return {
|
|
3223
|
-
...thread,
|
|
3224
|
-
anchor: createDetachedAnchor(anchor.range, "importAmbiguity"),
|
|
3225
|
-
status: "detached",
|
|
3226
|
-
metadata: {
|
|
3227
|
-
...thread.metadata,
|
|
3228
|
-
detachedReason: "opaque-region",
|
|
3229
|
-
actionabilityNote:
|
|
3230
|
-
"The comment body is preserved. The anchor overlaps opaque content that the editor cannot safely modify.",
|
|
3231
|
-
},
|
|
3232
|
-
};
|
|
3233
|
-
}
|
|
3234
|
-
|
|
3235
|
-
const preserveOnlyRevisionOverlap = preserveOnlyRevisionRanges.some((range) =>
|
|
3236
|
-
rangesIntersect(range, anchor.range),
|
|
3237
|
-
);
|
|
3238
|
-
if (preserveOnlyRevisionOverlap) {
|
|
3239
|
-
preserveOnlyCommentIds.add(thread.commentId);
|
|
3240
|
-
additionalDiagnostics.push({
|
|
3241
|
-
commentId: thread.commentId,
|
|
3242
|
-
code: "preserve_only_revision_overlap",
|
|
3243
|
-
message:
|
|
3244
|
-
"Comment anchor overlaps preserve-only review markup. Thread is visible but detached; anchor cannot be safely remapped during editing.",
|
|
3245
|
-
featureClass: "preserve-only",
|
|
3246
|
-
detachedReason: "revision-overlap" as const,
|
|
3247
|
-
actionabilityNote: "The comment body is preserved. The anchor overlaps preserve-only revision markup that the editor cannot safely modify.",
|
|
3248
|
-
});
|
|
3249
|
-
return {
|
|
3250
|
-
...thread,
|
|
3251
|
-
anchor: createDetachedAnchor(anchor.range, "importAmbiguity"),
|
|
3252
|
-
status: "detached",
|
|
3253
|
-
metadata: {
|
|
3254
|
-
...thread.metadata,
|
|
3255
|
-
detachedReason: "revision-overlap",
|
|
3256
|
-
actionabilityNote:
|
|
3257
|
-
"The comment body is preserved. The anchor overlaps preserve-only revision markup that the editor cannot safely modify.",
|
|
3258
|
-
},
|
|
3259
|
-
};
|
|
3260
|
-
}
|
|
3261
|
-
|
|
3262
|
-
return thread;
|
|
3263
|
-
});
|
|
3264
|
-
|
|
3265
|
-
return {
|
|
3266
|
-
...parsed,
|
|
3267
|
-
threads: normalizedThreads,
|
|
3268
|
-
diagnostics: [...parsed.diagnostics, ...additionalDiagnostics],
|
|
3269
|
-
definitions: parsed.definitions,
|
|
3270
|
-
preservedDefinitions: parsed.definitions.filter((definition) =>
|
|
3271
|
-
preserveOnlyCommentIds.has(
|
|
3272
|
-
resolveDefinitionRootCommentId(definition, parsed.definitions),
|
|
3273
|
-
),
|
|
3274
|
-
),
|
|
3275
|
-
};
|
|
3276
|
-
}
|
|
3277
|
-
|
|
3278
|
-
function translateClmCommentsToWorkflow(input: {
|
|
3279
|
-
comments: readonly CommentThread[];
|
|
3280
|
-
workflowOverlay?: WorkflowOverlay;
|
|
3281
|
-
workflowMetadata?: WorkflowMetadataSnapshot;
|
|
3282
|
-
timestamp: string;
|
|
3283
|
-
}): {
|
|
3284
|
-
comments: CommentThread[];
|
|
3285
|
-
workflowOverlay?: WorkflowOverlay;
|
|
3286
|
-
workflowMetadata?: WorkflowMetadataSnapshot;
|
|
3287
|
-
} {
|
|
3288
|
-
const nextComments = input.comments.map((thread) => structuredClone(thread));
|
|
3289
|
-
let nextOverlay = input.workflowOverlay ? structuredClone(input.workflowOverlay) : undefined;
|
|
3290
|
-
let overlayChanged = false;
|
|
3291
|
-
|
|
3292
|
-
for (const [index, thread] of nextComments.entries()) {
|
|
3293
|
-
const directive = parseClmCommentDirective(thread);
|
|
3294
|
-
if (!directive || thread.anchor.kind !== "range") {
|
|
3295
|
-
continue;
|
|
3296
|
-
}
|
|
3297
|
-
|
|
3298
|
-
if (!nextOverlay) {
|
|
3299
|
-
nextOverlay = {
|
|
3300
|
-
overlayVersion: "workflow-overlay/1",
|
|
3301
|
-
scopes: [],
|
|
3302
|
-
};
|
|
3303
|
-
}
|
|
3304
|
-
|
|
3305
|
-
const existingScope = findExistingClmScope(nextOverlay.scopes, directive);
|
|
3306
|
-
if (!existingScope) {
|
|
3307
|
-
const version = getNextClmScopeVersion(nextOverlay.scopes, thread.anchor);
|
|
3308
|
-
const scopeId = `clm-scope-${thread.commentId}-v${version}`;
|
|
3309
|
-
const workItem = directive.tag === "TASK"
|
|
3310
|
-
? createClmWorkflowWorkItem(thread, directive.description, scopeId)
|
|
3311
|
-
: undefined;
|
|
3312
|
-
const scope = createClmWorkflowScope(thread, directive, version, scopeId, workItem?.workItemId);
|
|
3313
|
-
|
|
3314
|
-
nextOverlay.scopes = [...nextOverlay.scopes, scope];
|
|
3315
|
-
if (workItem) {
|
|
3316
|
-
nextOverlay.workItems = [...(nextOverlay.workItems ?? []), workItem];
|
|
3317
|
-
if (!nextOverlay.activeWorkItemId) {
|
|
3318
|
-
nextOverlay.activeWorkItemId = workItem.workItemId;
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
|
-
overlayChanged = true;
|
|
3322
|
-
}
|
|
3323
|
-
|
|
3324
|
-
nextComments[index] = resolveImportedCommentThreadOnTranslation(thread, input.timestamp);
|
|
3325
|
-
}
|
|
3326
|
-
|
|
3327
|
-
return {
|
|
3328
|
-
comments: nextComments,
|
|
3329
|
-
workflowOverlay: overlayChanged || input.workflowOverlay ? nextOverlay : undefined,
|
|
3330
|
-
workflowMetadata: input.workflowMetadata,
|
|
3331
|
-
};
|
|
3332
|
-
}
|
|
3333
|
-
|
|
3334
|
-
function parseClmCommentDirective(
|
|
3335
|
-
thread: CommentThread,
|
|
3336
|
-
): { tag: "TASK" | "READ" | "COMMENT" | "EDIT"; description: string; sourceCommentId: string; sourceCommentDurableId?: string; sourceCommentParaId?: string; mode: WorkflowScope["mode"] } | undefined {
|
|
3337
|
-
if (thread.metadata?.rootParaId === undefined && thread.entries[0]?.metadata?.parentParaId) {
|
|
3338
|
-
return undefined;
|
|
3339
|
-
}
|
|
3340
|
-
const firstMeaningfulLine = thread.entries
|
|
3341
|
-
.flatMap((entry) => entry.body.split(/\r?\n/u))
|
|
3342
|
-
.map((line) => line.trim())
|
|
3343
|
-
.find((line) => line.length > 0);
|
|
3344
|
-
if (!firstMeaningfulLine) {
|
|
3345
|
-
return undefined;
|
|
3346
|
-
}
|
|
3347
|
-
const match = /^CLM:([A-Z]+):(.*)$/u.exec(firstMeaningfulLine);
|
|
3348
|
-
if (!match) {
|
|
3349
|
-
return undefined;
|
|
3350
|
-
}
|
|
3351
|
-
const tag = match[1] as "TASK" | "READ" | "COMMENT" | "EDIT";
|
|
3352
|
-
const description = (match[2] ?? "").trim();
|
|
3353
|
-
const mode = getClmWorkflowScopeMode(tag);
|
|
3354
|
-
if (!mode || description.length === 0) {
|
|
3355
|
-
return undefined;
|
|
3356
|
-
}
|
|
3357
|
-
return {
|
|
3358
|
-
tag,
|
|
3359
|
-
description,
|
|
3360
|
-
sourceCommentId: thread.metadata?.rootOoxmlCommentId ?? thread.commentId,
|
|
3361
|
-
sourceCommentDurableId: thread.entries[0]?.metadata?.durableId,
|
|
3362
|
-
sourceCommentParaId: thread.metadata?.rootParaId ?? thread.entries[0]?.metadata?.paraId,
|
|
3363
|
-
mode,
|
|
3364
|
-
};
|
|
3365
|
-
}
|
|
3366
|
-
|
|
3367
|
-
function getClmWorkflowScopeMode(tag: string): WorkflowScope["mode"] | undefined {
|
|
3368
|
-
switch (tag) {
|
|
3369
|
-
case "TASK":
|
|
3370
|
-
return "suggest";
|
|
3371
|
-
case "READ":
|
|
3372
|
-
return "view";
|
|
3373
|
-
case "COMMENT":
|
|
3374
|
-
return "comment";
|
|
3375
|
-
case "EDIT":
|
|
3376
|
-
return "edit";
|
|
3377
|
-
default:
|
|
3378
|
-
return undefined;
|
|
3379
|
-
}
|
|
3380
|
-
}
|
|
3381
|
-
|
|
3382
|
-
function createClmWorkflowWorkItem(
|
|
3383
|
-
thread: CommentThread,
|
|
3384
|
-
description: string,
|
|
3385
|
-
scopeId: string,
|
|
3386
|
-
): WorkflowWorkItem {
|
|
3387
|
-
return {
|
|
3388
|
-
workItemId: `clm-task-${thread.commentId}`,
|
|
3389
|
-
title: description,
|
|
3390
|
-
description,
|
|
3391
|
-
status: "pending",
|
|
3392
|
-
scopeIds: [scopeId],
|
|
3393
|
-
};
|
|
3394
|
-
}
|
|
3395
|
-
|
|
3396
|
-
function createClmWorkflowScope(
|
|
3397
|
-
thread: CommentThread,
|
|
3398
|
-
directive: NonNullable<ReturnType<typeof parseClmCommentDirective>>,
|
|
3399
|
-
version: number,
|
|
3400
|
-
scopeId: string,
|
|
3401
|
-
workItemId?: string,
|
|
3402
|
-
): WorkflowScope {
|
|
3403
|
-
if (workItemId) {
|
|
3404
|
-
return {
|
|
3405
|
-
scopeId,
|
|
3406
|
-
version,
|
|
3407
|
-
mode: directive.mode,
|
|
3408
|
-
anchor: toPublicAnchorProjection(thread.anchor),
|
|
3409
|
-
storyTarget: { kind: "main" },
|
|
3410
|
-
workItemId,
|
|
3411
|
-
label: directive.description,
|
|
3412
|
-
metadata: createClmScopeMetadata(directive),
|
|
3413
|
-
};
|
|
3414
|
-
}
|
|
3415
|
-
return {
|
|
3416
|
-
scopeId,
|
|
3417
|
-
version,
|
|
3418
|
-
mode: directive.mode,
|
|
3419
|
-
anchor: toPublicAnchorProjection(thread.anchor),
|
|
3420
|
-
storyTarget: { kind: "main" },
|
|
3421
|
-
label: directive.description,
|
|
3422
|
-
metadata: createClmScopeMetadata(directive),
|
|
3423
|
-
};
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3426
|
-
function createClmScopeMetadata(
|
|
3427
|
-
directive: NonNullable<ReturnType<typeof parseClmCommentDirective>>,
|
|
3428
|
-
): WorkflowScopeMetadataField[] {
|
|
3429
|
-
return [
|
|
3430
|
-
{
|
|
3431
|
-
key: "workblock.clm.tag",
|
|
3432
|
-
valueType: "string",
|
|
3433
|
-
value: directive.tag,
|
|
3434
|
-
},
|
|
3435
|
-
{
|
|
3436
|
-
key: "workblock.clm.description",
|
|
3437
|
-
valueType: "string",
|
|
3438
|
-
value: directive.description,
|
|
3439
|
-
},
|
|
3440
|
-
{
|
|
3441
|
-
key: "workblock.sourceCommentId",
|
|
3442
|
-
valueType: "string",
|
|
3443
|
-
value: directive.sourceCommentId,
|
|
3444
|
-
},
|
|
3445
|
-
...(directive.sourceCommentDurableId
|
|
3446
|
-
? [{
|
|
3447
|
-
key: "workblock.sourceCommentDurableId",
|
|
3448
|
-
valueType: "string" as const,
|
|
3449
|
-
value: directive.sourceCommentDurableId,
|
|
3450
|
-
}]
|
|
3451
|
-
: []),
|
|
3452
|
-
...(directive.sourceCommentParaId
|
|
3453
|
-
? [{
|
|
3454
|
-
key: "workblock.sourceCommentParaId",
|
|
3455
|
-
valueType: "string" as const,
|
|
3456
|
-
value: directive.sourceCommentParaId,
|
|
3457
|
-
}]
|
|
3458
|
-
: []),
|
|
3459
|
-
];
|
|
3460
|
-
}
|
|
3461
|
-
|
|
3462
|
-
function findExistingClmScope(
|
|
3463
|
-
scopes: readonly WorkflowScope[],
|
|
3464
|
-
directive: NonNullable<ReturnType<typeof parseClmCommentDirective>>,
|
|
3465
|
-
): WorkflowScope | undefined {
|
|
3466
|
-
return scopes.find((scope) => {
|
|
3467
|
-
const sourceCommentId = getWorkflowScopeMetadataValue(scope.metadata, "workblock.sourceCommentId");
|
|
3468
|
-
const sourceCommentDurableId = getWorkflowScopeMetadataValue(
|
|
3469
|
-
scope.metadata,
|
|
3470
|
-
"workblock.sourceCommentDurableId",
|
|
3471
|
-
);
|
|
3472
|
-
return (
|
|
3473
|
-
sourceCommentId === directive.sourceCommentId ||
|
|
3474
|
-
(directive.sourceCommentDurableId !== undefined &&
|
|
3475
|
-
sourceCommentDurableId === directive.sourceCommentDurableId)
|
|
3476
|
-
);
|
|
3477
|
-
});
|
|
3478
|
-
}
|
|
3479
|
-
|
|
3480
|
-
function getWorkflowScopeMetadataValue(
|
|
3481
|
-
metadata: WorkflowScope["metadata"] | undefined,
|
|
3482
|
-
key: string,
|
|
3483
|
-
): string | undefined {
|
|
3484
|
-
const field = metadata?.find((entry) => entry.key === key);
|
|
3485
|
-
return typeof field?.value === "string" ? field.value : undefined;
|
|
3486
|
-
}
|
|
3487
|
-
|
|
3488
|
-
function getNextClmScopeVersion(
|
|
3489
|
-
scopes: readonly WorkflowScope[],
|
|
3490
|
-
anchor: Extract<CommentThread["anchor"], { kind: "range" }>,
|
|
3491
|
-
): number {
|
|
3492
|
-
const anchorRange = {
|
|
3493
|
-
from: anchor.range.from,
|
|
3494
|
-
to: anchor.range.to,
|
|
3495
|
-
};
|
|
3496
|
-
const overlappingVersions = scopes.flatMap((scope) => {
|
|
3497
|
-
if (scope.anchor.kind !== "range") {
|
|
3498
|
-
return [];
|
|
3499
|
-
}
|
|
3500
|
-
return rangesIntersect(scope.anchor, anchorRange) && typeof scope.version === "number"
|
|
3501
|
-
? [scope.version]
|
|
3502
|
-
: [];
|
|
3503
|
-
});
|
|
3504
|
-
return overlappingVersions.length > 0 ? Math.max(...overlappingVersions) + 1 : 1;
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
|
-
function resolveImportedCommentThreadOnTranslation(
|
|
3508
|
-
thread: CommentThread,
|
|
3509
|
-
timestamp: string,
|
|
3510
|
-
): CommentThread {
|
|
3511
|
-
if (thread.status === "resolved") {
|
|
3512
|
-
return thread;
|
|
3513
|
-
}
|
|
3514
|
-
return {
|
|
3515
|
-
...thread,
|
|
3516
|
-
status: thread.status === "detached" ? "detached" : "resolved",
|
|
3517
|
-
resolution: thread.resolution ?? {
|
|
3518
|
-
resolvedAt: timestamp,
|
|
3519
|
-
resolvedBy: thread.entries[thread.entries.length - 1]?.authorId ?? thread.createdBy,
|
|
3520
|
-
},
|
|
3521
|
-
};
|
|
3522
|
-
}
|
|
3523
|
-
|
|
3524
|
-
function resolveDefinitionRootCommentId(
|
|
3525
|
-
definition: ImportedCommentDefinition,
|
|
3526
|
-
definitions: readonly ImportedCommentDefinition[],
|
|
3527
|
-
): string {
|
|
3528
|
-
if (!definition.parentParaId) {
|
|
3529
|
-
return definition.commentId;
|
|
3530
|
-
}
|
|
3531
|
-
|
|
3532
|
-
const definitionsByParaId = new Map(
|
|
3533
|
-
definitions
|
|
3534
|
-
.filter((candidate) => typeof candidate.paraId === "string")
|
|
3535
|
-
.map((candidate) => [candidate.paraId!, candidate]),
|
|
3536
|
-
);
|
|
3537
|
-
const visited = new Set<string>();
|
|
3538
|
-
let current: ImportedCommentDefinition | undefined = definition;
|
|
3539
|
-
|
|
3540
|
-
while (current?.parentParaId) {
|
|
3541
|
-
if (visited.has(current.parentParaId)) {
|
|
3542
|
-
break;
|
|
3543
|
-
}
|
|
3544
|
-
visited.add(current.parentParaId);
|
|
3545
|
-
const parent = definitionsByParaId.get(current.parentParaId);
|
|
3546
|
-
if (!parent) {
|
|
3547
|
-
break;
|
|
3548
|
-
}
|
|
3549
|
-
current = parent;
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
return current?.commentId ?? definition.commentId;
|
|
3553
|
-
}
|
|
3554
|
-
|
|
3555
|
-
function getStructuralPreserveOnlyReason(
|
|
3556
|
-
revision: ReviewRevisionRecord,
|
|
3557
|
-
paragraphRanges: ReadonlyArray<{ start: number; end: number }>,
|
|
3558
|
-
): string | undefined {
|
|
3559
|
-
const form = revision.metadata.importedRevisionForm;
|
|
3560
|
-
const { anchor } = revision;
|
|
3561
|
-
if (!form || anchor.kind !== "range") {
|
|
3562
|
-
return undefined;
|
|
3563
|
-
}
|
|
3564
|
-
|
|
3565
|
-
if (
|
|
3566
|
-
(form === "run-insertion" || form === "run-deletion") &&
|
|
3567
|
-
anchor.range.from === anchor.range.to
|
|
3568
|
-
) {
|
|
3569
|
-
return "Imported zero-width run revision remains preserve-only.";
|
|
3570
|
-
}
|
|
3571
|
-
|
|
3572
|
-
if (form === "paragraph-insertion" || form === "paragraph-deletion") {
|
|
3573
|
-
const paragraphBoundary = paragraphRanges.find(
|
|
3574
|
-
(boundary) =>
|
|
3575
|
-
boundary.end === anchor.range.from ||
|
|
3576
|
-
(anchor.range.from >= boundary.start &&
|
|
3577
|
-
anchor.range.from <= boundary.end),
|
|
3578
|
-
);
|
|
3579
|
-
return paragraphBoundary
|
|
3580
|
-
? undefined
|
|
3581
|
-
: "Imported revision spans paragraph-level structure and remains preserve-only.";
|
|
3582
|
-
}
|
|
3583
|
-
|
|
3584
|
-
const paragraphBoundary = paragraphRanges.find(
|
|
3585
|
-
(boundary) =>
|
|
3586
|
-
anchor.range.from >= boundary.start &&
|
|
3587
|
-
anchor.range.to <= boundary.end,
|
|
3588
|
-
);
|
|
3589
|
-
return paragraphBoundary
|
|
3590
|
-
? undefined
|
|
3591
|
-
: "Imported revision spans structural boundaries and remains preserve-only.";
|
|
3592
|
-
}
|
|
3593
|
-
|
|
3594
|
-
function collectCanonicalParagraphRanges(
|
|
3595
|
-
content: CanonicalDocumentEnvelope["content"],
|
|
3596
|
-
): Array<{ start: number; end: number }> {
|
|
3597
|
-
const ranges: Array<{ start: number; end: number }> = [];
|
|
3598
|
-
let cursor = 0;
|
|
3599
|
-
let previousWasParagraph = false;
|
|
3600
|
-
|
|
3601
|
-
for (const block of content.children) {
|
|
3602
|
-
if (block.type === "paragraph") {
|
|
3603
|
-
if (previousWasParagraph) {
|
|
3604
|
-
cursor += 1;
|
|
3605
|
-
}
|
|
3606
|
-
const start = cursor;
|
|
3607
|
-
cursor += measureCanonicalParagraph(block);
|
|
3608
|
-
ranges.push({ start, end: cursor });
|
|
3609
|
-
previousWasParagraph = true;
|
|
3610
|
-
continue;
|
|
3611
|
-
}
|
|
3612
|
-
|
|
3613
|
-
cursor += 1;
|
|
3614
|
-
previousWasParagraph = false;
|
|
3615
|
-
}
|
|
3616
|
-
|
|
3617
|
-
return ranges;
|
|
3618
|
-
}
|
|
3619
|
-
|
|
3620
|
-
function measureCanonicalParagraph(paragraph: CanonicalDocumentEnvelope["content"]["children"][number] & { type: "paragraph" }): number {
|
|
3621
|
-
return paragraph.children.reduce<number>((size, child) => {
|
|
3622
|
-
if (child.type === "text") {
|
|
3623
|
-
return size + Array.from(child.text).length;
|
|
3624
|
-
}
|
|
3625
|
-
if (child.type === "hyperlink") {
|
|
3626
|
-
return (
|
|
3627
|
-
size +
|
|
3628
|
-
child.children.reduce<number>((childSize, entry) => {
|
|
3629
|
-
if (entry.type === "text") {
|
|
3630
|
-
return childSize + Array.from(entry.text).length;
|
|
3631
|
-
}
|
|
3632
|
-
return childSize + 1;
|
|
3633
|
-
}, 0)
|
|
3634
|
-
);
|
|
3635
|
-
}
|
|
3636
|
-
return size + 1;
|
|
3637
|
-
}, 0);
|
|
3638
|
-
}
|
|
3639
|
-
|
|
3640
|
-
function rangesIntersect(
|
|
3641
|
-
left: { from: number; to: number },
|
|
3642
|
-
right: { from: number; to: number },
|
|
3643
|
-
): boolean {
|
|
3644
|
-
return left.from < right.to && right.from < left.to;
|
|
3645
|
-
}
|
|
3646
|
-
|
|
3647
|
-
function resolveCommentsPartPath(
|
|
3648
|
-
sourcePackage: OpcPackage,
|
|
3649
|
-
sourceDocumentPartPath: string,
|
|
3650
|
-
relationships: readonly OpcRelationship[],
|
|
3651
|
-
): string | undefined {
|
|
3652
|
-
return resolveDocumentRelatedPartPath(
|
|
3653
|
-
sourcePackage,
|
|
3654
|
-
sourceDocumentPartPath,
|
|
3655
|
-
relationships,
|
|
3656
|
-
COMMENTS_RELATIONSHIP_TYPE,
|
|
3657
|
-
COMMENTS_PART_PATH,
|
|
3658
|
-
);
|
|
3659
|
-
}
|
|
3660
|
-
|
|
3661
|
-
function resolveDocumentRelatedPartPath(
|
|
3662
|
-
sourcePackage: OpcPackage,
|
|
3663
|
-
sourceDocumentPartPath: string,
|
|
3664
|
-
relationships: readonly OpcRelationship[],
|
|
3665
|
-
relationshipType: string,
|
|
3666
|
-
fallbackPartPath: string,
|
|
3667
|
-
): string | undefined {
|
|
3668
|
-
const relationship = relationships.find(
|
|
3669
|
-
(candidate) =>
|
|
3670
|
-
candidate.type === relationshipType &&
|
|
3671
|
-
candidate.targetMode === "internal",
|
|
3672
|
-
);
|
|
3673
|
-
if (!relationship) {
|
|
3674
|
-
return sourcePackage.parts.has(fallbackPartPath) ? fallbackPartPath : undefined;
|
|
3675
|
-
}
|
|
3676
|
-
|
|
3677
|
-
const targetPath = resolveRelationshipTarget(sourceDocumentPartPath, relationship);
|
|
3678
|
-
return sourcePackage.parts.has(targetPath) ? targetPath : undefined;
|
|
3679
|
-
}
|
|
3680
|
-
|
|
3681
|
-
function resolveMainDocumentPartPath(sourcePackage: OpcPackage): string | undefined {
|
|
3682
|
-
const relationship = sourcePackage.manifest.packageRelationships.find(
|
|
3683
|
-
(candidate) =>
|
|
3684
|
-
candidate.type === OFFICE_DOCUMENT_RELATIONSHIP_TYPE &&
|
|
3685
|
-
candidate.targetMode === "internal",
|
|
3686
|
-
);
|
|
3687
|
-
if (relationship) {
|
|
3688
|
-
return resolveRelationshipTarget(null, relationship);
|
|
3689
|
-
}
|
|
3690
|
-
|
|
3691
|
-
return sourcePackage.parts.has(MAIN_DOCUMENT_PATH) ? MAIN_DOCUMENT_PATH : undefined;
|
|
3692
|
-
}
|
|
3693
|
-
|
|
3694
|
-
function toRuntimeCommentRecords(
|
|
3695
|
-
threads: readonly CommentThread[],
|
|
3696
|
-
): Record<string, CommentThreadRecord> {
|
|
3697
|
-
return Object.fromEntries(
|
|
3698
|
-
threads.map((thread) => {
|
|
3699
|
-
return [
|
|
3700
|
-
thread.commentId,
|
|
3701
|
-
{
|
|
3702
|
-
commentId: thread.commentId,
|
|
3703
|
-
body: thread.entries.map((entry) => entry.body).join("\n"),
|
|
3704
|
-
anchor: thread.anchor,
|
|
3705
|
-
createdBy: thread.createdBy,
|
|
3706
|
-
authorId: thread.createdBy,
|
|
3707
|
-
createdAt: thread.createdAt,
|
|
3708
|
-
entries: thread.entries.map((entry) => ({
|
|
3709
|
-
entryId: entry.entryId,
|
|
3710
|
-
authorId: entry.authorId,
|
|
3711
|
-
body: entry.body,
|
|
3712
|
-
createdAt: entry.createdAt,
|
|
3713
|
-
metadata: entry.metadata
|
|
3714
|
-
? {
|
|
3715
|
-
ooxmlCommentId: entry.metadata.ooxmlCommentId,
|
|
3716
|
-
paraId: entry.metadata.paraId,
|
|
3717
|
-
parentParaId: entry.metadata.parentParaId,
|
|
3718
|
-
durableId: entry.metadata.durableId,
|
|
3719
|
-
initials: entry.metadata.initials,
|
|
3720
|
-
}
|
|
3721
|
-
: undefined,
|
|
3722
|
-
})),
|
|
3723
|
-
status: thread.status,
|
|
3724
|
-
resolution: thread.resolution
|
|
3725
|
-
? {
|
|
3726
|
-
resolvedAt: thread.resolution.resolvedAt,
|
|
3727
|
-
resolvedBy: thread.resolution.resolvedBy,
|
|
3728
|
-
}
|
|
3729
|
-
: undefined,
|
|
3730
|
-
resolvedAt: thread.resolution?.resolvedAt,
|
|
3731
|
-
warningIds: [...thread.warningIds],
|
|
3732
|
-
isResolved: thread.status === "resolved",
|
|
3733
|
-
metadata: thread.metadata
|
|
3734
|
-
? {
|
|
3735
|
-
source: thread.metadata.source,
|
|
3736
|
-
rootOoxmlCommentId: thread.metadata.rootOoxmlCommentId,
|
|
3737
|
-
rootParaId: thread.metadata.rootParaId,
|
|
3738
|
-
}
|
|
3739
|
-
: undefined,
|
|
3740
|
-
} satisfies CommentThreadRecord,
|
|
3741
|
-
];
|
|
3742
|
-
}),
|
|
3743
|
-
);
|
|
3744
|
-
}
|
|
3745
|
-
|
|
3746
|
-
function toRuntimeRevisionRecords(
|
|
3747
|
-
revisions: readonly ReviewRevisionRecord[],
|
|
3748
|
-
): Record<string, RuntimeRevisionRecord> {
|
|
3749
|
-
return Object.fromEntries(
|
|
3750
|
-
revisions.map((revision) => [
|
|
3751
|
-
revision.revisionId,
|
|
3752
|
-
{
|
|
3753
|
-
changeId: revision.revisionId,
|
|
3754
|
-
kind: revision.kind,
|
|
3755
|
-
anchor: revision.anchor,
|
|
3756
|
-
authorId: revision.authorId,
|
|
3757
|
-
createdAt: revision.createdAt,
|
|
3758
|
-
warningIds: [...revision.warningIds],
|
|
3759
|
-
metadata: {
|
|
3760
|
-
source: revision.metadata.source,
|
|
3761
|
-
storyTarget: revision.metadata.storyTarget,
|
|
3762
|
-
preserveOnlyReason: revision.metadata.preserveOnlyReason,
|
|
3763
|
-
suggestionId: revision.metadata.suggestionId,
|
|
3764
|
-
semanticKind: revision.metadata.semanticKind,
|
|
3765
|
-
linkedRevisionIds: revision.metadata.linkedRevisionIds,
|
|
3766
|
-
predecessorSuggestionId: revision.metadata.predecessorSuggestionId,
|
|
3767
|
-
importedRevisionForm: revision.metadata.importedRevisionForm,
|
|
3768
|
-
originalRevisionType: revision.metadata.originalRevisionType,
|
|
3769
|
-
ooxmlRevisionId: revision.metadata.ooxmlRevisionId,
|
|
3770
|
-
propertyChangeData: revision.metadata.propertyChangeData,
|
|
3771
|
-
moveData: revision.metadata.moveData,
|
|
3772
|
-
},
|
|
3773
|
-
status: revision.status === "active" ? "open" : revision.status,
|
|
3774
|
-
} satisfies RuntimeRevisionRecord,
|
|
3775
|
-
]),
|
|
3776
|
-
);
|
|
3777
|
-
}
|
|
3778
|
-
|
|
3779
|
-
function toReviewRevisionRecords(
|
|
3780
|
-
revisions: CanonicalDocumentEnvelope["review"]["revisions"],
|
|
3781
|
-
): ReviewRevisionRecord[] {
|
|
3782
|
-
return Object.values(revisions).map((revision) => ({
|
|
3783
|
-
revisionId: revision.changeId,
|
|
3784
|
-
kind: revision.kind,
|
|
3785
|
-
anchor: revision.anchor,
|
|
3786
|
-
authorId: revision.authorId ?? "unknown",
|
|
3787
|
-
createdAt: revision.createdAt,
|
|
3788
|
-
warningIds: [...(revision.warningIds ?? [])],
|
|
3789
|
-
metadata: {
|
|
3790
|
-
source: revision.metadata?.source ?? "runtime",
|
|
3791
|
-
storyTarget: revision.metadata?.storyTarget,
|
|
3792
|
-
preserveOnlyReason: revision.metadata?.preserveOnlyReason,
|
|
3793
|
-
suggestionId: revision.metadata?.suggestionId,
|
|
3794
|
-
semanticKind: revision.metadata?.semanticKind,
|
|
3795
|
-
linkedRevisionIds: revision.metadata?.linkedRevisionIds,
|
|
3796
|
-
predecessorSuggestionId: revision.metadata?.predecessorSuggestionId,
|
|
3797
|
-
importedRevisionForm: revision.metadata?.importedRevisionForm,
|
|
3798
|
-
originalRevisionType: revision.metadata?.originalRevisionType,
|
|
3799
|
-
ooxmlRevisionId: revision.metadata?.ooxmlRevisionId,
|
|
3800
|
-
propertyChangeData: revision.metadata?.propertyChangeData,
|
|
3801
|
-
moveData: revision.metadata?.moveData,
|
|
3802
|
-
},
|
|
3803
|
-
status: revision.status === "open" ? "active" : revision.status,
|
|
3804
|
-
}));
|
|
3805
|
-
}
|
|
3806
|
-
|
|
3807
|
-
function serializeSecondaryStoryWithRuntimeRevisions(
|
|
3808
|
-
xml: string,
|
|
3809
|
-
revisions: readonly ReviewRevisionRecord[],
|
|
3810
|
-
label: string,
|
|
3811
|
-
): string {
|
|
3812
|
-
if (revisions.length === 0) {
|
|
3813
|
-
return xml;
|
|
3814
|
-
}
|
|
3815
|
-
|
|
3816
|
-
const serialized = serializeRuntimeRevisionsIntoStoryXml(xml, revisions);
|
|
3817
|
-
if (serialized.skippedRevisionIds.length > 0) {
|
|
3818
|
-
throw new Error(
|
|
3819
|
-
`DOCX export is blocked because ${serialized.skippedRevisionIds.length} active revisions overlap unsupported serialization boundaries in ${label}.`,
|
|
3820
|
-
);
|
|
3821
|
-
}
|
|
3822
|
-
return serialized.documentXml;
|
|
3823
|
-
}
|
|
3824
|
-
|
|
3825
|
-
export function stripCommentMarkup(
|
|
3826
|
-
documentXml: string,
|
|
3827
|
-
ownedCommentIds: readonly string[],
|
|
3828
|
-
): string {
|
|
3829
|
-
if (ownedCommentIds.length === 0) {
|
|
3830
|
-
return documentXml;
|
|
3831
|
-
}
|
|
3832
|
-
|
|
3833
|
-
let output = documentXml;
|
|
3834
|
-
for (const commentId of ownedCommentIds) {
|
|
3835
|
-
const escapedCommentId = escapeRegExp(commentId);
|
|
3836
|
-
const attributePattern = `(?:w:id|id)=["']${escapedCommentId}["']`;
|
|
3837
|
-
output = output
|
|
3838
|
-
.replace(
|
|
3839
|
-
new RegExp(`<w:commentRangeStart\\b[^>]*${attributePattern}[^>]*/>`, "gu"),
|
|
3840
|
-
"",
|
|
3841
|
-
)
|
|
3842
|
-
.replace(
|
|
3843
|
-
new RegExp(`<w:commentRangeEnd\\b[^>]*${attributePattern}[^>]*/>`, "gu"),
|
|
3844
|
-
"",
|
|
3845
|
-
)
|
|
3846
|
-
.replace(
|
|
3847
|
-
new RegExp(
|
|
3848
|
-
`<w:r\\b[^>]*>(?:(?!<w:t\\b|</w:r>)[\\s\\S])*?<w:commentReference\\b[^>]*${attributePattern}[^>]*/>(?:(?!<w:t\\b|</w:r>)[\\s\\S])*?</w:r>`,
|
|
3849
|
-
"gu",
|
|
3850
|
-
),
|
|
3851
|
-
"",
|
|
3852
|
-
);
|
|
3853
|
-
}
|
|
3854
|
-
|
|
3855
|
-
return output;
|
|
3856
|
-
}
|
|
3857
|
-
|
|
3858
|
-
function escapeRegExp(value: string): string {
|
|
3859
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3860
|
-
}
|
|
3861
|
-
|
|
3862
|
-
function withDocumentRelatedParts(
|
|
3863
|
-
relationships: readonly OpcRelationship[],
|
|
3864
|
-
relatedParts: ReadonlyArray<{
|
|
3865
|
-
relationshipType: string;
|
|
3866
|
-
partPath: string;
|
|
3867
|
-
existingRelationshipId?: string;
|
|
3868
|
-
include: boolean;
|
|
3869
|
-
}>,
|
|
3870
|
-
): OpcRelationship[] {
|
|
3871
|
-
const filteredTypes = new Set(relatedParts.map((part) => part.relationshipType));
|
|
3872
|
-
const nextRelationships = relationships
|
|
3873
|
-
.filter((relationship) => !filteredTypes.has(relationship.type))
|
|
3874
|
-
.map(cloneRelationship);
|
|
3875
|
-
|
|
3876
|
-
for (const part of relatedParts) {
|
|
3877
|
-
if (!part.include) {
|
|
3878
|
-
continue;
|
|
3879
|
-
}
|
|
3880
|
-
|
|
3881
|
-
nextRelationships.push({
|
|
3882
|
-
id: part.existingRelationshipId ?? createCommentsRelationshipId(nextRelationships),
|
|
3883
|
-
type: part.relationshipType,
|
|
3884
|
-
target: toDocumentRelativeTarget(part.partPath),
|
|
3885
|
-
targetMode: "internal",
|
|
3886
|
-
});
|
|
3887
|
-
}
|
|
3888
|
-
|
|
3889
|
-
return nextRelationships;
|
|
3890
|
-
}
|
|
3891
|
-
|
|
3892
|
-
function collectPreservedPackageParts(
|
|
3893
|
-
sourcePackage: OpcPackage,
|
|
3894
|
-
ownedPartPaths: ReadonlyArray<string | undefined>,
|
|
3895
|
-
): Record<string, PreservedPackagePart> {
|
|
3896
|
-
const normalizedOwnedPaths = new Set(
|
|
3897
|
-
ownedPartPaths
|
|
3898
|
-
.filter((value): value is string => typeof value === "string")
|
|
3899
|
-
.map((path) => normalizePartPath(path)),
|
|
3900
|
-
);
|
|
3901
|
-
return Object.fromEntries(
|
|
3902
|
-
[...sourcePackage.parts.values()]
|
|
3903
|
-
.filter((part) =>
|
|
3904
|
-
shouldPreservePackagePart(part.path, part.surfaceKind, normalizedOwnedPaths),
|
|
3905
|
-
)
|
|
3906
|
-
.map((part) => [
|
|
3907
|
-
part.path,
|
|
3908
|
-
{
|
|
3909
|
-
packagePartName: part.path,
|
|
3910
|
-
contentType: part.contentType ?? "application/octet-stream",
|
|
3911
|
-
relationshipIds: findRelationshipIdsTargetingPart(sourcePackage, part.path),
|
|
3912
|
-
} satisfies PreservedPackagePart,
|
|
3913
|
-
]),
|
|
3914
|
-
);
|
|
3915
|
-
}
|
|
3916
|
-
|
|
3917
|
-
function collectInlineMediaParts(
|
|
3918
|
-
sourcePackage: OpcPackage,
|
|
3919
|
-
): ReadonlyMap<string, { path: string; contentType: string }> {
|
|
3920
|
-
return new Map(
|
|
3921
|
-
[...sourcePackage.parts.values()]
|
|
3922
|
-
.filter(
|
|
3923
|
-
(part) =>
|
|
3924
|
-
part.path.startsWith("/word/media/") && typeof part.contentType === "string",
|
|
3925
|
-
)
|
|
3926
|
-
.map((part) => [
|
|
3927
|
-
part.path,
|
|
3928
|
-
{
|
|
3929
|
-
path: part.path,
|
|
3930
|
-
contentType: part.contentType ?? "application/octet-stream",
|
|
3931
|
-
},
|
|
3932
|
-
]),
|
|
3933
|
-
);
|
|
3934
|
-
}
|
|
3935
|
-
|
|
3936
|
-
function createEmptyNumberingCatalog(): NumberingCatalog {
|
|
3937
|
-
return {
|
|
3938
|
-
abstractDefinitions: {},
|
|
3939
|
-
instances: {},
|
|
3940
|
-
};
|
|
3941
|
-
}
|
|
3942
|
-
|
|
3943
|
-
function collectBrokenInternalRelationshipIssues(
|
|
3944
|
-
sourcePackage: OpcPackage,
|
|
3945
|
-
mainDocumentPath?: string,
|
|
3946
|
-
): ReturnType<typeof createBrokenRelationshipIssue>[] {
|
|
3947
|
-
const brokenTargets = new Map<string, ReturnType<typeof createBrokenRelationshipIssue>>();
|
|
3948
|
-
|
|
3949
|
-
for (const relationship of sourcePackage.manifest.packageRelationships) {
|
|
3950
|
-
if (relationship.targetMode !== "internal") {
|
|
3951
|
-
continue;
|
|
3952
|
-
}
|
|
3953
|
-
|
|
3954
|
-
const target = resolveRelationshipTarget(null, relationship);
|
|
3955
|
-
if (
|
|
3956
|
-
!sourcePackage.parts.has(target) &&
|
|
3957
|
-
isFatalBrokenRelationship({
|
|
3958
|
-
relationshipSourcePath: null,
|
|
3959
|
-
relationship,
|
|
3960
|
-
targetPartPath: target,
|
|
3961
|
-
mainDocumentPath,
|
|
3962
|
-
})
|
|
3963
|
-
) {
|
|
3964
|
-
brokenTargets.set(
|
|
3965
|
-
`package:${relationship.id}:${target}`,
|
|
3966
|
-
createBrokenRelationshipIssue({
|
|
3967
|
-
relationshipSourcePath: null,
|
|
3968
|
-
relationshipId: relationship.id,
|
|
3969
|
-
targetPartPath: target,
|
|
3970
|
-
}),
|
|
3971
|
-
);
|
|
3972
|
-
}
|
|
3973
|
-
}
|
|
3974
|
-
|
|
3975
|
-
for (const part of sourcePackage.parts.values()) {
|
|
3976
|
-
for (const relationship of part.relationships) {
|
|
3977
|
-
if (relationship.targetMode !== "internal") {
|
|
3978
|
-
continue;
|
|
3979
|
-
}
|
|
3980
|
-
|
|
3981
|
-
const target = resolveRelationshipTarget(part.path, relationship);
|
|
3982
|
-
if (
|
|
3983
|
-
!sourcePackage.parts.has(target) &&
|
|
3984
|
-
isFatalBrokenRelationship({
|
|
3985
|
-
relationshipSourcePath: part.path,
|
|
3986
|
-
relationship,
|
|
3987
|
-
targetPartPath: target,
|
|
3988
|
-
mainDocumentPath,
|
|
3989
|
-
})
|
|
3990
|
-
) {
|
|
3991
|
-
brokenTargets.set(
|
|
3992
|
-
`${part.path}:${relationship.id}:${target}`,
|
|
3993
|
-
createBrokenRelationshipIssue({
|
|
3994
|
-
relationshipSourcePath: part.path,
|
|
3995
|
-
relationshipId: relationship.id,
|
|
3996
|
-
targetPartPath: target,
|
|
3997
|
-
}),
|
|
3998
|
-
);
|
|
3999
|
-
}
|
|
4000
|
-
}
|
|
4001
|
-
}
|
|
4002
|
-
|
|
4003
|
-
return [...brokenTargets.values()].sort((left, right) =>
|
|
4004
|
-
`${left.relationshipSourcePath ?? ""}:${left.relationshipId}:${left.targetPartPath}`.localeCompare(
|
|
4005
|
-
`${right.relationshipSourcePath ?? ""}:${right.relationshipId}:${right.targetPartPath}`,
|
|
4006
|
-
),
|
|
4007
|
-
);
|
|
4008
|
-
}
|
|
4009
|
-
|
|
4010
|
-
function summarizeBrokenRelationshipIssues(
|
|
4011
|
-
issues: readonly ReturnType<typeof createBrokenRelationshipIssue>[],
|
|
4012
|
-
): string {
|
|
4013
|
-
return `DOCX package has unresolved internal relationships: ${issues
|
|
4014
|
-
.map((issue) => issue.targetPartPath ?? issue.message)
|
|
4015
|
-
.slice(0, 3)
|
|
4016
|
-
.join(", ")}${issues.length > 3 ? ", ..." : ""}.`;
|
|
4017
|
-
}
|
|
4018
|
-
|
|
4019
|
-
function createBrokenRelationshipWarnings(
|
|
4020
|
-
sourcePackage: OpcPackage,
|
|
4021
|
-
mainDocumentPath?: string,
|
|
4022
|
-
): CanonicalDocumentEnvelope["diagnostics"]["warnings"] {
|
|
4023
|
-
const warnings = new Map<string, CanonicalDocumentEnvelope["diagnostics"]["warnings"][number]>();
|
|
4024
|
-
|
|
4025
|
-
for (const relationship of sourcePackage.manifest.packageRelationships) {
|
|
4026
|
-
if (relationship.targetMode !== "internal") {
|
|
4027
|
-
continue;
|
|
4028
|
-
}
|
|
4029
|
-
|
|
4030
|
-
const target = resolveRelationshipTarget(null, relationship);
|
|
4031
|
-
if (
|
|
4032
|
-
sourcePackage.parts.has(target) ||
|
|
4033
|
-
isFatalBrokenRelationship({
|
|
4034
|
-
relationshipSourcePath: null,
|
|
4035
|
-
relationship,
|
|
4036
|
-
targetPartPath: target,
|
|
4037
|
-
mainDocumentPath,
|
|
4038
|
-
})
|
|
4039
|
-
) {
|
|
4040
|
-
continue;
|
|
4041
|
-
}
|
|
4042
|
-
|
|
4043
|
-
warnings.set(`package:${relationship.id}:${target}`, {
|
|
4044
|
-
diagnosticId: `diagnostic:broken-relationship-package-${relationship.id}`,
|
|
4045
|
-
warningId: `warning:broken-relationship:${relationship.id}`,
|
|
4046
|
-
source: "preservation",
|
|
4047
|
-
message: `DOCX package has unresolved internal relationships outside the editor-owned graph: ${target}.`,
|
|
4048
|
-
});
|
|
4049
|
-
}
|
|
4050
|
-
|
|
4051
|
-
for (const part of sourcePackage.parts.values()) {
|
|
4052
|
-
for (const relationship of part.relationships) {
|
|
4053
|
-
if (relationship.targetMode !== "internal") {
|
|
4054
|
-
continue;
|
|
4055
|
-
}
|
|
4056
|
-
|
|
4057
|
-
const target = resolveRelationshipTarget(part.path, relationship);
|
|
4058
|
-
if (
|
|
4059
|
-
sourcePackage.parts.has(target) ||
|
|
4060
|
-
isFatalBrokenRelationship({
|
|
4061
|
-
relationshipSourcePath: part.path,
|
|
4062
|
-
relationship,
|
|
4063
|
-
targetPartPath: target,
|
|
4064
|
-
mainDocumentPath,
|
|
4065
|
-
})
|
|
4066
|
-
) {
|
|
4067
|
-
continue;
|
|
4068
|
-
}
|
|
4069
|
-
|
|
4070
|
-
warnings.set(`${part.path}:${relationship.id}:${target}`, {
|
|
4071
|
-
diagnosticId: `diagnostic:broken-relationship-${relationship.id}`,
|
|
4072
|
-
warningId: `warning:broken-relationship:${relationship.id}`,
|
|
4073
|
-
source: "preservation",
|
|
4074
|
-
message: `DOCX package has unresolved internal relationships outside the editor-owned graph: ${target}.`,
|
|
4075
|
-
});
|
|
4076
|
-
}
|
|
4077
|
-
}
|
|
4078
|
-
|
|
4079
|
-
return [...warnings.values()];
|
|
4080
|
-
}
|
|
4081
|
-
|
|
4082
|
-
function isFatalBrokenRelationship(input: {
|
|
4083
|
-
relationshipSourcePath: string | null;
|
|
4084
|
-
relationship: OpcRelationship;
|
|
4085
|
-
targetPartPath: string;
|
|
4086
|
-
mainDocumentPath?: string;
|
|
4087
|
-
}): boolean {
|
|
4088
|
-
return (
|
|
4089
|
-
input.relationshipSourcePath === null &&
|
|
4090
|
-
input.relationship.type === OFFICE_DOCUMENT_RELATIONSHIP_TYPE &&
|
|
4091
|
-
input.targetPartPath === input.mainDocumentPath
|
|
4092
|
-
);
|
|
4093
|
-
}
|
|
4094
|
-
|
|
4095
|
-
function shouldPreservePackagePart(
|
|
4096
|
-
partPath: string,
|
|
4097
|
-
surfaceKind: OpcPackage["parts"] extends Map<string, infer T>
|
|
4098
|
-
? T extends { surfaceKind: infer U }
|
|
4099
|
-
? U
|
|
4100
|
-
: never
|
|
4101
|
-
: never,
|
|
4102
|
-
ownedPartPaths: ReadonlySet<string>,
|
|
4103
|
-
): boolean {
|
|
4104
|
-
if (surfaceKind !== "content") {
|
|
4105
|
-
return false;
|
|
4106
|
-
}
|
|
4107
|
-
|
|
4108
|
-
if (
|
|
4109
|
-
ownedPartPaths.has(partPath) ||
|
|
4110
|
-
partPath.startsWith("/word/media/") ||
|
|
4111
|
-
CORE_NON_PRESERVED_PART_PATHS.has(partPath)
|
|
4112
|
-
) {
|
|
4113
|
-
return false;
|
|
4114
|
-
}
|
|
4115
|
-
|
|
4116
|
-
return true;
|
|
4117
|
-
}
|
|
4118
|
-
|
|
4119
|
-
function findRelationshipIdsTargetingPart(
|
|
4120
|
-
sourcePackage: OpcPackage,
|
|
4121
|
-
targetPartPath: string,
|
|
4122
|
-
): string[] {
|
|
4123
|
-
const ids = new Set<string>();
|
|
4124
|
-
|
|
4125
|
-
for (const relationship of sourcePackage.manifest.packageRelationships) {
|
|
4126
|
-
if (
|
|
4127
|
-
relationship.targetMode === "internal" &&
|
|
4128
|
-
resolveRelationshipTarget(null, relationship) === targetPartPath
|
|
4129
|
-
) {
|
|
4130
|
-
ids.add(relationship.id);
|
|
4131
|
-
}
|
|
4132
|
-
}
|
|
4133
|
-
|
|
4134
|
-
for (const part of sourcePackage.parts.values()) {
|
|
4135
|
-
for (const relationship of part.relationships) {
|
|
4136
|
-
if (
|
|
4137
|
-
relationship.targetMode === "internal" &&
|
|
4138
|
-
resolveRelationshipTarget(part.path, relationship) === targetPartPath
|
|
4139
|
-
) {
|
|
4140
|
-
ids.add(relationship.id);
|
|
4141
|
-
}
|
|
4142
|
-
}
|
|
4143
|
-
}
|
|
4144
|
-
|
|
4145
|
-
return [...ids].sort();
|
|
4146
|
-
}
|
|
4147
|
-
|
|
4148
|
-
function extractDocumentRootAttributes(documentXml: string): Record<string, string> {
|
|
4149
|
-
const match = documentXml.match(
|
|
4150
|
-
/<(?:[A-Za-z_][A-Za-z0-9:._-]*:)?document\b([^>]*)>/u,
|
|
4151
|
-
);
|
|
4152
|
-
if (!match) {
|
|
4153
|
-
return {};
|
|
4154
|
-
}
|
|
4155
|
-
|
|
4156
|
-
const attributes: Record<string, string> = {};
|
|
4157
|
-
const pattern = /([A-Za-z_][A-Za-z0-9:._-]*)\s*=\s*("([^"]*)"|'([^']*)')/gu;
|
|
4158
|
-
for (const attributeMatch of (match[1] ?? "").matchAll(pattern)) {
|
|
4159
|
-
const name = attributeMatch[1];
|
|
4160
|
-
const value = attributeMatch[3] ?? attributeMatch[4] ?? "";
|
|
4161
|
-
if (!name) {
|
|
4162
|
-
continue;
|
|
4163
|
-
}
|
|
4164
|
-
attributes[name] = value;
|
|
4165
|
-
}
|
|
4166
|
-
|
|
4167
|
-
return attributes;
|
|
4168
|
-
}
|
|
4169
|
-
|
|
4170
|
-
function isPackageImportError(error: unknown): boolean {
|
|
4171
|
-
if (!(error instanceof Error)) {
|
|
4172
|
-
return false;
|
|
4173
|
-
}
|
|
4174
|
-
|
|
4175
|
-
const normalized = error.message.toLowerCase();
|
|
4176
|
-
return (
|
|
4177
|
-
normalized.includes("zip") ||
|
|
4178
|
-
normalized.includes("opc package") ||
|
|
4179
|
-
normalized.includes("compression") ||
|
|
4180
|
-
normalized.includes("relationship") ||
|
|
4181
|
-
normalized.includes("/[content_types].xml") ||
|
|
4182
|
-
normalized.includes("/word/document.xml") ||
|
|
4183
|
-
normalized.includes("xml")
|
|
4184
|
-
);
|
|
4185
|
-
}
|
|
4186
|
-
|
|
4187
|
-
const CORE_NON_PRESERVED_PART_PATHS = new Set([
|
|
4188
|
-
"/docProps/app.xml",
|
|
4189
|
-
"/docProps/core.xml",
|
|
4190
|
-
"/word/numbering.xml",
|
|
4191
|
-
]);
|
|
4192
|
-
|
|
4193
|
-
function createCommentsRelationshipId(
|
|
4194
|
-
relationships: readonly OpcRelationship[],
|
|
4195
|
-
): string {
|
|
4196
|
-
let nextIndex = 1;
|
|
4197
|
-
while (relationships.some((relationship) => relationship.id === `rIdComments${nextIndex}`)) {
|
|
4198
|
-
nextIndex += 1;
|
|
4199
|
-
}
|
|
4200
|
-
|
|
4201
|
-
return `rIdComments${nextIndex}`;
|
|
4202
|
-
}
|
|
4203
|
-
|
|
4204
|
-
function toDocumentRelativeTarget(partPath: string): string {
|
|
4205
|
-
const normalized = normalizePartPath(partPath);
|
|
4206
|
-
return normalized.startsWith("/word/") ? normalized.slice("/word/".length) : normalized.slice(1);
|
|
4207
|
-
}
|
|
4208
|
-
|
|
4209
|
-
function cloneRelationship(relationship: OpcRelationship): OpcRelationship {
|
|
4210
|
-
return { ...relationship };
|
|
4211
|
-
}
|
|
4212
|
-
|
|
4213
|
-
const UTF8_DECODER = new TextDecoder("utf-8");
|
|
4214
|
-
|
|
4215
|
-
function decodeUtf8(bytes: Uint8Array | undefined): string {
|
|
4216
|
-
if (!bytes) {
|
|
4217
|
-
return "";
|
|
4218
|
-
}
|
|
4219
|
-
|
|
4220
|
-
return UTF8_DECODER.decode(bytes);
|
|
4221
|
-
}
|
|
4222
|
-
|
|
4223
|
-
function toUint8Array(bytes: Uint8Array | ArrayBuffer): Uint8Array {
|
|
4224
|
-
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
4225
|
-
}
|
|
4226
|
-
|
|
4227
|
-
function serializeCanonicalDocumentForExport(document: CanonicalDocumentEnvelope): string {
|
|
4228
|
-
return createCanonicalDocumentSignature({
|
|
4229
|
-
...document,
|
|
4230
|
-
docId: "__export__",
|
|
4231
|
-
createdAt: "__export__",
|
|
4232
|
-
updatedAt: "__export__",
|
|
4233
|
-
});
|
|
4234
|
-
}
|
|
4235
|
-
|
|
4236
|
-
/**
|
|
4237
|
-
* Read a Node-style environment variable without referencing the Node-only
|
|
4238
|
-
* `process` global in the production build (which excludes @types/node).
|
|
4239
|
-
* Returns `undefined` in browser environments or when the var is unset.
|
|
4240
|
-
*/
|
|
4241
|
-
function readNodeEnvVar(name: string): string | undefined {
|
|
4242
|
-
const proc = (globalThis as unknown as {
|
|
4243
|
-
process?: { env?: Record<string, string | undefined> };
|
|
4244
|
-
}).process;
|
|
4245
|
-
return proc?.env?.[name];
|
|
4246
|
-
}
|
|
4247
|
-
|
|
4248
|
-
function canReuseSourceBytesForCurrentDocument(
|
|
4249
|
-
state: ImportedDocxState,
|
|
4250
|
-
document: CanonicalDocumentEnvelope,
|
|
4251
|
-
): boolean {
|
|
4252
|
-
// The validator-loop CI harness (§1 / §7) needs to exercise the full
|
|
4253
|
-
// serializer pipeline — including A.1 / A.2 / A.3 / A.6 / A.7 / A.8 /
|
|
4254
|
-
// A.9 fixes — so it sets DOCX_VALIDATOR_FORCE_REGEN=1 to disable the
|
|
4255
|
-
// fast-path byte-reuse. Never set this flag in production; it forces a
|
|
4256
|
-
// full XML regeneration on every export and is only used by the CCEP
|
|
4257
|
-
// validator harness to prove our serializer's correctness.
|
|
4258
|
-
if (readNodeEnvVar("DOCX_VALIDATOR_FORCE_REGEN") === "1") {
|
|
4259
|
-
return false;
|
|
4260
|
-
}
|
|
4261
|
-
if (requiresHostMetadataNormalization(state.sourcePackage, state.sourceDocumentPartPath)) {
|
|
4262
|
-
return false;
|
|
4263
|
-
}
|
|
4264
|
-
|
|
4265
|
-
const commentThreads = Object.values(document.review.comments);
|
|
4266
|
-
const hasLiveComments = commentThreads.some((thread) => thread.anchor.kind !== "detached");
|
|
4267
|
-
const hasRuntimeAuthoredActiveRevisions = Object.values(document.review.revisions).some((revision) =>
|
|
4268
|
-
revision.status === "open" && revision.metadata?.source === "runtime"
|
|
4269
|
-
);
|
|
4270
|
-
if (hasRuntimeAuthoredActiveRevisions) {
|
|
4271
|
-
return false;
|
|
4272
|
-
}
|
|
4273
|
-
if (!hasLiveComments) {
|
|
4274
|
-
return true;
|
|
4275
|
-
}
|
|
4276
|
-
|
|
4277
|
-
return Boolean(
|
|
4278
|
-
state.sourceCommentsPartPath &&
|
|
4279
|
-
state.sourceCommentsExtendedPartPath &&
|
|
4280
|
-
state.sourceCommentsIdsPartPath &&
|
|
4281
|
-
state.sourcePeoplePartPath,
|
|
4282
|
-
);
|
|
4283
|
-
}
|
|
4284
|
-
|
|
4285
|
-
function requiresHostMetadataNormalization(
|
|
4286
|
-
sourcePackage: OpcPackage,
|
|
4287
|
-
sourceDocumentPartPath: string,
|
|
4288
|
-
): boolean {
|
|
4289
|
-
return (
|
|
4290
|
-
isSuspiciouslySkeletalWordPackage(sourcePackage, sourceDocumentPartPath) &&
|
|
4291
|
-
!hasHostSafeMetadataPackageStructure(sourcePackage)
|
|
4292
|
-
);
|
|
4293
|
-
}
|
|
4294
|
-
|
|
4295
|
-
function ensureHostMetadataParts(
|
|
4296
|
-
exportSession: ReturnType<typeof createExportSession>,
|
|
4297
|
-
sourcePackage: OpcPackage,
|
|
4298
|
-
document: CanonicalDocumentEnvelope,
|
|
4299
|
-
): void {
|
|
4300
|
-
const corePropertiesPart = sourcePackage.parts.get(CORE_PROPERTIES_PART_PATH);
|
|
4301
|
-
if (!corePropertiesPart || corePropertiesPart.contentType !== CORE_PROPERTIES_CONTENT_TYPE) {
|
|
4302
|
-
exportSession.replaceOwnedPart({
|
|
4303
|
-
path: CORE_PROPERTIES_PART_PATH,
|
|
4304
|
-
bytes: corePropertiesPart?.bytes ?? new TextEncoder().encode(buildCorePropertiesXml(document)),
|
|
4305
|
-
contentType: CORE_PROPERTIES_CONTENT_TYPE,
|
|
4306
|
-
compression: corePropertiesPart?.compression,
|
|
4307
|
-
});
|
|
4308
|
-
}
|
|
4309
|
-
|
|
4310
|
-
const appPropertiesPart = sourcePackage.parts.get(APP_PROPERTIES_PART_PATH);
|
|
4311
|
-
if (!appPropertiesPart || appPropertiesPart.contentType !== APP_PROPERTIES_CONTENT_TYPE) {
|
|
4312
|
-
exportSession.replaceOwnedPart({
|
|
4313
|
-
path: APP_PROPERTIES_PART_PATH,
|
|
4314
|
-
bytes: appPropertiesPart?.bytes ?? new TextEncoder().encode(buildAppPropertiesXml()),
|
|
4315
|
-
contentType: APP_PROPERTIES_CONTENT_TYPE,
|
|
4316
|
-
compression: appPropertiesPart?.compression,
|
|
4317
|
-
});
|
|
4318
|
-
}
|
|
4319
|
-
|
|
4320
|
-
exportSession.ensurePackageRelationship({
|
|
4321
|
-
type: CORE_PROPERTIES_RELATIONSHIP_TYPE,
|
|
4322
|
-
target: CORE_PROPERTIES_PART_PATH,
|
|
4323
|
-
preferredId: "rIdDocPropsCore",
|
|
4324
|
-
});
|
|
4325
|
-
exportSession.ensurePackageRelationship({
|
|
4326
|
-
type: APP_PROPERTIES_RELATIONSHIP_TYPE,
|
|
4327
|
-
target: APP_PROPERTIES_PART_PATH,
|
|
4328
|
-
preferredId: "rIdDocPropsApp",
|
|
4329
|
-
});
|
|
4330
|
-
}
|
|
4331
|
-
|
|
4332
|
-
function ensureWorkflowPayloadParts(
|
|
4333
|
-
exportSession: ReturnType<typeof createExportSession>,
|
|
4334
|
-
sessionState: EditorSessionState,
|
|
4335
|
-
document: CanonicalDocumentEnvelope,
|
|
4336
|
-
sourcePackage: OpcPackage,
|
|
4337
|
-
resolvedPartPaths: {
|
|
4338
|
-
payloadPartPath: string;
|
|
4339
|
-
itemPropsPartPath: string;
|
|
4340
|
-
},
|
|
4341
|
-
editorState?: import("./ooxml/workflow-payload.ts").EditorStatePayload,
|
|
4342
|
-
): void {
|
|
4343
|
-
const payloadParts = buildWorkflowPayloadParts({
|
|
4344
|
-
sourcePackage,
|
|
4345
|
-
workflowMetadata: sessionState.workflowMetadata,
|
|
4346
|
-
workflowOverlay: sessionState.workflowOverlay,
|
|
4347
|
-
editorState,
|
|
4348
|
-
documentId: sessionState.documentId,
|
|
4349
|
-
createdAt: document.createdAt,
|
|
4350
|
-
updatedAt: document.updatedAt,
|
|
4351
|
-
producerVersion: sessionState.editorBuild,
|
|
4352
|
-
});
|
|
4353
|
-
if (!payloadParts) {
|
|
4354
|
-
return;
|
|
4355
|
-
}
|
|
4356
|
-
if (
|
|
4357
|
-
payloadParts.payloadPartPath !== resolvedPartPaths.payloadPartPath ||
|
|
4358
|
-
payloadParts.itemPropsPartPath !== resolvedPartPaths.itemPropsPartPath
|
|
4359
|
-
) {
|
|
4360
|
-
throw new Error(
|
|
4361
|
-
"Workflow payload export resolved inconsistent customXml paths; export session ownership no longer matches payload serialization.",
|
|
4362
|
-
);
|
|
4363
|
-
}
|
|
4364
|
-
|
|
4365
|
-
const payloadPart = sourcePackage.parts.get(payloadParts.payloadPartPath);
|
|
4366
|
-
const itemPropsPart = sourcePackage.parts.get(payloadParts.itemPropsPartPath);
|
|
4367
|
-
const customPropsPart = sourcePackage.parts.get(WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH);
|
|
4368
|
-
|
|
4369
|
-
exportSession.replaceOwnedPart({
|
|
4370
|
-
path: payloadParts.payloadPartPath,
|
|
4371
|
-
bytes: new TextEncoder().encode(payloadParts.payloadPartXml),
|
|
4372
|
-
contentType: payloadPart?.contentType ?? WORKFLOW_PAYLOAD_CONTENT_TYPE,
|
|
4373
|
-
relationships: payloadParts.payloadRelationships,
|
|
4374
|
-
compression: payloadPart?.compression,
|
|
4375
|
-
});
|
|
4376
|
-
exportSession.replaceOwnedPart({
|
|
4377
|
-
path: payloadParts.itemPropsPartPath,
|
|
4378
|
-
bytes: new TextEncoder().encode(payloadParts.itemPropsXml),
|
|
4379
|
-
contentType: itemPropsPart?.contentType ?? WORKFLOW_PAYLOAD_ITEM_PROPS_CONTENT_TYPE,
|
|
4380
|
-
compression: itemPropsPart?.compression,
|
|
4381
|
-
});
|
|
4382
|
-
exportSession.replaceOwnedPart({
|
|
4383
|
-
path: WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
|
|
4384
|
-
bytes: new TextEncoder().encode(payloadParts.customPropertiesXml),
|
|
4385
|
-
contentType: customPropsPart?.contentType ?? WORKFLOW_PAYLOAD_CUSTOM_PROPS_CONTENT_TYPE,
|
|
4386
|
-
compression: customPropsPart?.compression,
|
|
4387
|
-
});
|
|
4388
|
-
|
|
4389
|
-
exportSession.ensurePackageRelationship({
|
|
4390
|
-
type: WORKFLOW_PAYLOAD_CUSTOM_PROPS_RELATIONSHIP_TYPE,
|
|
4391
|
-
target: WORKFLOW_PAYLOAD_CUSTOM_PROPS_PART_PATH,
|
|
4392
|
-
preferredId: "rIdBwWorkflowCustomProps",
|
|
4393
|
-
});
|
|
4394
|
-
}
|
|
4395
|
-
|
|
4396
|
-
function hasHostSafeMetadataPackageStructure(sourcePackage: OpcPackage): boolean {
|
|
4397
|
-
const corePropertiesPart = sourcePackage.parts.get(CORE_PROPERTIES_PART_PATH);
|
|
4398
|
-
const appPropertiesPart = sourcePackage.parts.get(APP_PROPERTIES_PART_PATH);
|
|
4399
|
-
return (
|
|
4400
|
-
corePropertiesPart?.contentType === CORE_PROPERTIES_CONTENT_TYPE &&
|
|
4401
|
-
appPropertiesPart?.contentType === APP_PROPERTIES_CONTENT_TYPE &&
|
|
4402
|
-
hasPackageRelationshipTarget(
|
|
4403
|
-
sourcePackage,
|
|
4404
|
-
CORE_PROPERTIES_RELATIONSHIP_TYPE,
|
|
4405
|
-
CORE_PROPERTIES_PART_PATH,
|
|
4406
|
-
) &&
|
|
4407
|
-
hasPackageRelationshipTarget(
|
|
4408
|
-
sourcePackage,
|
|
4409
|
-
APP_PROPERTIES_RELATIONSHIP_TYPE,
|
|
4410
|
-
APP_PROPERTIES_PART_PATH,
|
|
4411
|
-
)
|
|
4412
|
-
);
|
|
4413
|
-
}
|
|
4414
|
-
|
|
4415
|
-
function hasPackageRelationshipTarget(
|
|
4416
|
-
sourcePackage: OpcPackage,
|
|
4417
|
-
relationshipType: string,
|
|
4418
|
-
targetPartPath: string,
|
|
4419
|
-
): boolean {
|
|
4420
|
-
return sourcePackage.manifest.packageRelationships.some(
|
|
4421
|
-
(relationship) =>
|
|
4422
|
-
relationship.type === relationshipType &&
|
|
4423
|
-
relationship.targetMode === "internal" &&
|
|
4424
|
-
resolveRelationshipTarget(null, relationship) === targetPartPath,
|
|
4425
|
-
);
|
|
4426
|
-
}
|
|
4427
|
-
|
|
4428
|
-
function isSuspiciouslySkeletalWordPackage(
|
|
4429
|
-
sourcePackage: OpcPackage,
|
|
4430
|
-
sourceDocumentPartPath: string,
|
|
4431
|
-
): boolean {
|
|
4432
|
-
const allowedPaths = new Set<string>([
|
|
4433
|
-
CONTENT_TYPES_PATH,
|
|
4434
|
-
PACKAGE_RELATIONSHIPS_PATH,
|
|
4435
|
-
sourceDocumentPartPath,
|
|
4436
|
-
]);
|
|
4437
|
-
const relationshipsPartPath = getRelationshipsPartPath(sourceDocumentPartPath);
|
|
4438
|
-
if (relationshipsPartPath) {
|
|
4439
|
-
allowedPaths.add(relationshipsPartPath);
|
|
4440
|
-
}
|
|
4441
|
-
|
|
4442
|
-
return [...sourcePackage.parts.keys()].every((partPath) => allowedPaths.has(partPath));
|
|
4443
|
-
}
|
|
4444
|
-
|
|
4445
|
-
function buildCorePropertiesXml(document: CanonicalDocumentEnvelope): string {
|
|
4446
|
-
const { metadata } = document;
|
|
4447
|
-
const keywords =
|
|
4448
|
-
Array.isArray(metadata.keywords) && metadata.keywords.length > 0
|
|
4449
|
-
? metadata.keywords.join(", ")
|
|
4450
|
-
: undefined;
|
|
4451
|
-
const propertyLines = [
|
|
4452
|
-
xmlNode("dc:title", metadata.title),
|
|
4453
|
-
xmlNode("dc:subject", metadata.subject),
|
|
4454
|
-
xmlNode("dc:description", metadata.description),
|
|
4455
|
-
xmlNode("dc:language", metadata.language),
|
|
4456
|
-
xmlNode("cp:keywords", keywords),
|
|
4457
|
-
xmlNode("cp:category", metadata.category),
|
|
4458
|
-
xmlNode('dcterms:created xsi:type="dcterms:W3CDTF"', document.createdAt),
|
|
4459
|
-
xmlNode('dcterms:modified xsi:type="dcterms:W3CDTF"', document.updatedAt),
|
|
4460
|
-
].filter((line): line is string => Boolean(line));
|
|
4461
|
-
|
|
4462
|
-
return [
|
|
4463
|
-
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>`,
|
|
4464
|
-
`<cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:dcmitype="http://purl.org/dc/dcmitype/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">`,
|
|
4465
|
-
...propertyLines.map((line) => ` ${line}`),
|
|
4466
|
-
`</cp:coreProperties>`,
|
|
4467
|
-
].join("\n");
|
|
4468
|
-
}
|
|
4469
|
-
|
|
4470
|
-
// buildAppPropertiesXml moved to src/io/export/build-app-properties-xml.ts
|
|
4471
|
-
// per close-render-fidelity §2 A.5. Re-import as `buildAppPropertiesXmlFn`.
|
|
4472
|
-
|
|
4473
|
-
function xmlNode(tagName: string, value: string | undefined): string | undefined {
|
|
4474
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
4475
|
-
return undefined;
|
|
4476
|
-
}
|
|
4477
|
-
|
|
4478
|
-
return `<${tagName}>${escapeXml(value)}</${tagName.split(" ", 1)[0]}>`;
|
|
4479
|
-
}
|
|
4480
|
-
|
|
4481
|
-
function serializeProtectionRangesIntoDocumentXml(
|
|
4482
|
-
documentXml: string,
|
|
4483
|
-
protection: ProtectionSnapshot,
|
|
4484
|
-
paragraphs = mapParagraphBoundaries(documentXml),
|
|
4485
|
-
): string {
|
|
4486
|
-
if (protection.ranges.length === 0) {
|
|
4487
|
-
return documentXml;
|
|
4488
|
-
}
|
|
4489
|
-
|
|
4490
|
-
const insertions = new Map<number, string[]>();
|
|
4491
|
-
|
|
4492
|
-
for (const range of protection.ranges) {
|
|
4493
|
-
if (typeof range.start !== "number" || typeof range.end !== "number") {
|
|
4494
|
-
continue;
|
|
4495
|
-
}
|
|
4496
|
-
const rangeStart = range.start;
|
|
4497
|
-
const rangeEnd = range.end;
|
|
4498
|
-
|
|
4499
|
-
const startParagraph = paragraphs.find(
|
|
4500
|
-
(candidate) => rangeStart >= candidate.start && rangeStart <= candidate.end,
|
|
4501
|
-
);
|
|
4502
|
-
const endParagraph = paragraphs.find(
|
|
4503
|
-
(candidate) => rangeEnd >= candidate.start && rangeEnd <= candidate.end,
|
|
4504
|
-
);
|
|
4505
|
-
if (!startParagraph || !endParagraph) {
|
|
4506
|
-
continue;
|
|
4507
|
-
}
|
|
4508
|
-
|
|
4509
|
-
const startIndex =
|
|
4510
|
-
startParagraph.boundaries.get(rangeStart) ??
|
|
4511
|
-
findNearestBoundaryIndex(startParagraph.boundaries, rangeStart, "backward");
|
|
4512
|
-
const endIndex =
|
|
4513
|
-
endParagraph.boundaries.get(rangeEnd) ??
|
|
4514
|
-
findNearestBoundaryIndex(endParagraph.boundaries, rangeEnd, "forward");
|
|
4515
|
-
if (startIndex === undefined || endIndex === undefined) {
|
|
4516
|
-
continue;
|
|
4517
|
-
}
|
|
4518
|
-
|
|
4519
|
-
const permStartXml = [
|
|
4520
|
-
`<w:permStart`,
|
|
4521
|
-
` w:id="${escapeXmlAttribute(range.rangeId)}"`,
|
|
4522
|
-
range.editorGroup ? ` w:edGrp="${escapeXmlAttribute(range.editorGroup)}"` : "",
|
|
4523
|
-
range.editor ? ` w:ed="${escapeXmlAttribute(range.editor)}"` : "",
|
|
4524
|
-
`/>`,
|
|
4525
|
-
].join("");
|
|
4526
|
-
const permEndXml = `<w:permEnd w:id="${escapeXmlAttribute(range.rangeId)}"/>`;
|
|
4527
|
-
|
|
4528
|
-
pushProtectionInsertion(insertions, startIndex, permStartXml);
|
|
4529
|
-
pushProtectionInsertion(insertions, endIndex, permEndXml);
|
|
4530
|
-
}
|
|
4531
|
-
|
|
4532
|
-
if (insertions.size === 0) {
|
|
4533
|
-
return documentXml;
|
|
4534
|
-
}
|
|
4535
|
-
|
|
4536
|
-
const parts: string[] = [];
|
|
4537
|
-
let cursor = 0;
|
|
4538
|
-
for (const [index, snippets] of [...insertions.entries()].sort(([left], [right]) => left - right)) {
|
|
4539
|
-
parts.push(documentXml.slice(cursor, index));
|
|
4540
|
-
parts.push(...snippets);
|
|
4541
|
-
cursor = index;
|
|
4542
|
-
}
|
|
4543
|
-
parts.push(documentXml.slice(cursor));
|
|
4544
|
-
return parts.join("");
|
|
4545
|
-
}
|
|
4546
|
-
|
|
4547
|
-
function pushProtectionInsertion(
|
|
4548
|
-
insertions: Map<number, string[]>,
|
|
4549
|
-
index: number,
|
|
4550
|
-
xml: string,
|
|
4551
|
-
): void {
|
|
4552
|
-
const existing = insertions.get(index);
|
|
4553
|
-
if (existing) {
|
|
4554
|
-
existing.push(xml);
|
|
4555
|
-
return;
|
|
4556
|
-
}
|
|
4557
|
-
insertions.set(index, [xml]);
|
|
4558
|
-
}
|
|
4559
|
-
|
|
4560
|
-
function findNearestBoundaryIndex(
|
|
4561
|
-
boundaries: Map<number, number>,
|
|
4562
|
-
position: number,
|
|
4563
|
-
direction: "backward" | "forward",
|
|
4564
|
-
): number | undefined {
|
|
4565
|
-
const ordered = [...boundaries.entries()].sort(([left], [right]) => left - right);
|
|
4566
|
-
if (direction === "backward") {
|
|
4567
|
-
for (let index = ordered.length - 1; index >= 0; index -= 1) {
|
|
4568
|
-
const [boundaryPos, boundaryIndex] = ordered[index]!;
|
|
4569
|
-
if (boundaryPos <= position) {
|
|
4570
|
-
return boundaryIndex;
|
|
4571
|
-
}
|
|
4572
|
-
}
|
|
4573
|
-
return undefined;
|
|
4574
|
-
}
|
|
4575
|
-
for (const [boundaryPos, boundaryIndex] of ordered) {
|
|
4576
|
-
if (boundaryPos >= position) {
|
|
4577
|
-
return boundaryIndex;
|
|
4578
|
-
}
|
|
4579
|
-
}
|
|
4580
|
-
return undefined;
|
|
4581
|
-
}
|
|
4582
|
-
|
|
4583
|
-
function escapeXml(value: string): string {
|
|
4584
|
-
return value
|
|
4585
|
-
.replace(/&/g, "&")
|
|
4586
|
-
.replace(/</g, "<")
|
|
4587
|
-
.replace(/>/g, ">")
|
|
4588
|
-
.replace(/\"/g, """)
|
|
4589
|
-
.replace(/'/g, "'");
|
|
4590
|
-
}
|
|
4591
|
-
|
|
4592
|
-
function escapeXmlAttribute(value: string): string {
|
|
4593
|
-
return value
|
|
4594
|
-
.replace(/&/g, "&")
|
|
4595
|
-
.replace(/</g, "<")
|
|
4596
|
-
.replace(/>/g, ">")
|
|
4597
|
-
.replace(/"/g, """);
|
|
4598
|
-
}
|
|
4599
|
-
|
|
4600
|
-
// ---------------------------------------------------------------------------
|
|
4601
|
-
// Protection range extraction
|
|
4602
|
-
// ---------------------------------------------------------------------------
|
|
4603
|
-
|
|
4604
|
-
const EMPTY_PROTECTION_SNAPSHOT: ProtectionSnapshot = {
|
|
4605
|
-
hasDocumentProtection: false,
|
|
4606
|
-
enforcementActive: false,
|
|
4607
|
-
ranges: [],
|
|
4608
|
-
enforcedRangeCount: 0,
|
|
4609
|
-
preservedRangeCount: 0,
|
|
4610
|
-
};
|
|
4611
|
-
|
|
4612
|
-
interface DocumentProtectionMeta {
|
|
4613
|
-
editType?: string;
|
|
4614
|
-
enforcement: boolean;
|
|
4615
|
-
}
|
|
4616
|
-
|
|
4617
|
-
function extractDocumentProtection(settingsXml: string): DocumentProtectionMeta {
|
|
4618
|
-
if (!settingsXml) return { enforcement: false };
|
|
4619
|
-
const match = settingsXml.match(/<w:documentProtection\b([^/>]*)\/?>/);
|
|
4620
|
-
if (!match) return { enforcement: false };
|
|
4621
|
-
const attrs = match[1];
|
|
4622
|
-
const editTypeMatch = attrs.match(/w:edit="([^"]*)"/);
|
|
4623
|
-
const enforcementMatch = attrs.match(/w:enforcement="([^"]*)"/);
|
|
4624
|
-
const editType = editTypeMatch?.[1];
|
|
4625
|
-
const enforcement = enforcementMatch?.[1] === "1" || enforcementMatch?.[1] === "true";
|
|
4626
|
-
return { editType, enforcement };
|
|
4627
|
-
}
|
|
4628
|
-
|
|
4629
|
-
function extractProtectionRanges(blocks: readonly ParsedBlockNode[]): ProtectionRange[] {
|
|
4630
|
-
const ranges: ProtectionRange[] = [];
|
|
4631
|
-
const openRanges = new Map<string, Omit<ProtectionRange, "end">>();
|
|
4632
|
-
collectProtectionRangesFromBlocks(blocks, ranges, openRanges, 0);
|
|
4633
|
-
return ranges;
|
|
4634
|
-
}
|
|
4635
|
-
|
|
4636
|
-
function collectProtectionRangesFromBlocks(
|
|
4637
|
-
blocks: readonly ParsedBlockNode[],
|
|
4638
|
-
ranges: ProtectionRange[],
|
|
4639
|
-
openRanges: Map<string, Omit<ProtectionRange, "end">>,
|
|
4640
|
-
cursor: number,
|
|
4641
|
-
): number {
|
|
4642
|
-
let nextCursor = cursor;
|
|
4643
|
-
let previousParagraph = false;
|
|
4644
|
-
|
|
4645
|
-
for (const block of blocks) {
|
|
4646
|
-
if (block.type === "paragraph") {
|
|
4647
|
-
if (previousParagraph) {
|
|
4648
|
-
nextCursor += 1;
|
|
4649
|
-
}
|
|
4650
|
-
nextCursor = collectProtectionRangesFromInlines(
|
|
4651
|
-
block.children,
|
|
4652
|
-
ranges,
|
|
4653
|
-
openRanges,
|
|
4654
|
-
nextCursor,
|
|
4655
|
-
);
|
|
4656
|
-
previousParagraph = true;
|
|
4657
|
-
continue;
|
|
4658
|
-
}
|
|
4659
|
-
|
|
4660
|
-
if (block.type === "table") {
|
|
4661
|
-
nextCursor += 1;
|
|
4662
|
-
previousParagraph = false;
|
|
4663
|
-
for (const row of block.rows) {
|
|
4664
|
-
for (const cell of row.cells) {
|
|
4665
|
-
nextCursor = collectProtectionRangesFromBlocks(
|
|
4666
|
-
cell.children,
|
|
4667
|
-
ranges,
|
|
4668
|
-
openRanges,
|
|
4669
|
-
nextCursor,
|
|
4670
|
-
);
|
|
4671
|
-
}
|
|
4672
|
-
}
|
|
4673
|
-
continue;
|
|
4674
|
-
}
|
|
4675
|
-
|
|
4676
|
-
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
4677
|
-
nextCursor = collectProtectionRangesFromBlocks(
|
|
4678
|
-
block.children,
|
|
4679
|
-
ranges,
|
|
4680
|
-
openRanges,
|
|
4681
|
-
nextCursor,
|
|
4682
|
-
);
|
|
4683
|
-
previousParagraph = false;
|
|
4684
|
-
continue;
|
|
4685
|
-
}
|
|
4686
|
-
|
|
4687
|
-
nextCursor += 1;
|
|
4688
|
-
previousParagraph = false;
|
|
4689
|
-
}
|
|
4690
|
-
|
|
4691
|
-
return nextCursor;
|
|
4692
|
-
}
|
|
4693
|
-
|
|
4694
|
-
function collectProtectionRangesFromInlines(
|
|
4695
|
-
nodes: readonly ParsedInlineNode[],
|
|
4696
|
-
ranges: ProtectionRange[],
|
|
4697
|
-
openRanges: Map<string, Omit<ProtectionRange, "end">>,
|
|
4698
|
-
cursor: number,
|
|
4699
|
-
): number {
|
|
4700
|
-
let nextCursor = cursor;
|
|
4701
|
-
|
|
4702
|
-
for (const node of nodes) {
|
|
4703
|
-
if (node.type === "perm_start") {
|
|
4704
|
-
openRanges.set(node.rangeId, {
|
|
4705
|
-
rangeId: node.rangeId,
|
|
4706
|
-
start: nextCursor,
|
|
4707
|
-
...(node.editorGroup ? { editorGroup: node.editorGroup } : {}),
|
|
4708
|
-
...(node.editor ? { editor: node.editor } : {}),
|
|
4709
|
-
enforced: false,
|
|
4710
|
-
enforcementReason:
|
|
4711
|
-
"preserve-only: runtime does not yet enforce permission range boundaries",
|
|
4712
|
-
});
|
|
4713
|
-
continue;
|
|
4714
|
-
}
|
|
4715
|
-
|
|
4716
|
-
if (node.type === "perm_end") {
|
|
4717
|
-
const openRange = openRanges.get(node.rangeId);
|
|
4718
|
-
if (openRange) {
|
|
4719
|
-
ranges.push({
|
|
4720
|
-
...openRange,
|
|
4721
|
-
end: nextCursor,
|
|
4722
|
-
});
|
|
4723
|
-
openRanges.delete(node.rangeId);
|
|
4724
|
-
}
|
|
4725
|
-
continue;
|
|
4726
|
-
}
|
|
4727
|
-
|
|
4728
|
-
nextCursor += measureParsedInlineNode(node);
|
|
4729
|
-
}
|
|
4730
|
-
|
|
4731
|
-
return nextCursor;
|
|
4732
|
-
}
|
|
4733
|
-
|
|
4734
|
-
function measureParsedInlineNode(node: ParsedInlineNode): number {
|
|
4735
|
-
switch (node.type) {
|
|
4736
|
-
case "text":
|
|
4737
|
-
return Array.from(node.text).length;
|
|
4738
|
-
case "tab":
|
|
4739
|
-
case "hard_break":
|
|
4740
|
-
case "column_break":
|
|
4741
|
-
case "footnote_ref":
|
|
4742
|
-
case "image":
|
|
4743
|
-
case "bookmark_start":
|
|
4744
|
-
case "bookmark_end":
|
|
4745
|
-
return 1;
|
|
4746
|
-
case "hyperlink":
|
|
4747
|
-
return node.children.reduce((size, child) => size + measureParsedInlineNode(child), 0);
|
|
4748
|
-
case "field": {
|
|
4749
|
-
const content = parseMainDocumentXml(
|
|
4750
|
-
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"><w:body><w:p>${node.contentXml}</w:p></w:body></w:document>`,
|
|
4751
|
-
);
|
|
4752
|
-
if (content.blocks[0]?.type === "paragraph") {
|
|
4753
|
-
return content.blocks[0].children.reduce(
|
|
4754
|
-
(size, child) => size + measureParsedInlineNode(child),
|
|
4755
|
-
0,
|
|
4756
|
-
);
|
|
4757
|
-
}
|
|
4758
|
-
return 1;
|
|
4759
|
-
}
|
|
4760
|
-
default:
|
|
4761
|
-
return 1;
|
|
4762
|
-
}
|
|
4763
|
-
}
|
|
4764
|
-
|
|
4765
|
-
function buildProtectionSnapshot(
|
|
4766
|
-
documentProtection: DocumentProtectionMeta,
|
|
4767
|
-
ranges: ProtectionRange[],
|
|
4768
|
-
): ProtectionSnapshot {
|
|
4769
|
-
const hasDocumentProtection =
|
|
4770
|
-
documentProtection.editType !== undefined || documentProtection.enforcement;
|
|
4771
|
-
const enforceRanges =
|
|
4772
|
-
documentProtection.editType === "readOnly" || documentProtection.editType === "comments";
|
|
4773
|
-
const normalizedRanges = ranges.map((range) => {
|
|
4774
|
-
const canEnforce =
|
|
4775
|
-
hasDocumentProtection &&
|
|
4776
|
-
documentProtection.enforcement &&
|
|
4777
|
-
enforceRanges &&
|
|
4778
|
-
typeof range.start === "number" &&
|
|
4779
|
-
typeof range.end === "number" &&
|
|
4780
|
-
range.end >= range.start;
|
|
4781
|
-
return {
|
|
4782
|
-
...range,
|
|
4783
|
-
enforced: canEnforce,
|
|
4784
|
-
enforcementReason: canEnforce
|
|
4785
|
-
? "runtime-enforced: permission range is mapped to canonical positions"
|
|
4786
|
-
: "preserve-only: runtime does not yet enforce permission range boundaries",
|
|
4787
|
-
};
|
|
4788
|
-
});
|
|
4789
|
-
return {
|
|
4790
|
-
hasDocumentProtection,
|
|
4791
|
-
editType: documentProtection.editType,
|
|
4792
|
-
enforcementActive: documentProtection.enforcement,
|
|
4793
|
-
ranges: normalizedRanges,
|
|
4794
|
-
enforcedRangeCount: normalizedRanges.filter((r) => r.enforced).length,
|
|
4795
|
-
preservedRangeCount: normalizedRanges.filter((r) => !r.enforced).length,
|
|
4796
|
-
};
|
|
4797
|
-
}
|
|
1
|
+
// Back-compat shim.
|
|
2
|
+
//
|
|
3
|
+
// `src/io/docx-session.ts` was the pre-refactor home for session
|
|
4
|
+
// orchestration. The Layer-01 refactor extracted the contents to
|
|
5
|
+
// `src/session/**` (see `docs/architecture/01-package-session.md`).
|
|
6
|
+
//
|
|
7
|
+
// This file is retained so that the public export
|
|
8
|
+
// `@beyondwork/docx-react-component/io/docx-session` (declared in
|
|
9
|
+
// `package.json`) and any consumers of the old internal path keep
|
|
10
|
+
// working. New code MUST import from `./session` / `src/session/**`
|
|
11
|
+
// directly — see `MIGRATION.md`.
|
|
12
|
+
//
|
|
13
|
+
// Symbol map (old → new):
|
|
14
|
+
// loadDocxEditorSessionAsync → loadDocxSessionAsync (renamed)
|
|
15
|
+
// LoadDocxEditorSessionAsyncOptions → same name, new home
|
|
16
|
+
// LoadDocxEditorSessionOptions → same name, new home
|
|
17
|
+
// LoadedDocxEditorSession → same name, new home
|
|
18
|
+
// normalizeImportedCommentThreads → same name, new home
|
|
19
|
+
// stripCommentMarkup → same name, new home
|
|
20
|
+
// NormalizedImportedCommentsResult → same name, new home
|
|
21
|
+
//
|
|
22
|
+
// The sync `loadDocxEditorSession` entry point is not re-exported —
|
|
23
|
+
// the runtime is async-first after the refactor. Migrate callers to
|
|
24
|
+
// `DocxSession.open(bytes)` or `loadDocxSessionAsync(...)`.
|
|
25
|
+
|
|
26
|
+
export { loadDocxSessionAsync as loadDocxEditorSessionAsync } from "../session/import/loader.ts";
|
|
27
|
+
export type {
|
|
28
|
+
LoadDocxEditorSessionAsyncOptions,
|
|
29
|
+
LoadDocxEditorSessionOptions,
|
|
30
|
+
LoadedDocxEditorSession,
|
|
31
|
+
} from "../session/import/loader-types.ts";
|
|
32
|
+
export {
|
|
33
|
+
normalizeImportedCommentThreads,
|
|
34
|
+
stripCommentMarkup,
|
|
35
|
+
type NormalizedImportedCommentsResult,
|
|
36
|
+
} from "../session/import/review-import.ts";
|