@beyondwork/docx-react-component 1.0.18 → 1.0.20
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 +8 -2
- package/package.json +24 -34
- package/src/api/README.md +5 -1
- package/src/api/public-types.ts +710 -4
- package/src/api/session-state.ts +60 -0
- package/src/core/commands/formatting-commands.ts +2 -1
- package/src/core/commands/image-commands.ts +147 -0
- package/src/core/commands/index.ts +19 -3
- package/src/core/commands/list-commands.ts +231 -36
- package/src/core/commands/paragraph-layout-commands.ts +339 -0
- package/src/core/commands/section-layout-commands.ts +680 -0
- package/src/core/commands/style-commands.ts +262 -0
- package/src/core/search/search-text.ts +357 -0
- package/src/core/selection/mapping.ts +41 -0
- package/src/core/state/editor-state.ts +4 -1
- package/src/index.ts +51 -0
- package/src/io/docx-session.ts +623 -56
- package/src/io/export/serialize-comments.ts +104 -34
- package/src/io/export/serialize-footnotes.ts +198 -1
- package/src/io/export/serialize-headers-footers.ts +203 -10
- package/src/io/export/serialize-main-document.ts +285 -8
- package/src/io/export/serialize-numbering.ts +28 -7
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +144 -32
- package/src/io/ooxml/highlight-colors.ts +39 -0
- package/src/io/ooxml/numbering-sentinels.ts +44 -0
- package/src/io/ooxml/parse-comments.ts +85 -19
- package/src/io/ooxml/parse-fields.ts +396 -0
- package/src/io/ooxml/parse-footnotes.ts +452 -22
- package/src/io/ooxml/parse-headers-footers.ts +657 -29
- package/src/io/ooxml/parse-inline-media.ts +30 -0
- package/src/io/ooxml/parse-main-document.ts +807 -20
- package/src/io/ooxml/parse-numbering.ts +7 -0
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/io/ooxml/parse-settings.ts +184 -0
- package/src/io/ooxml/parse-shapes.ts +25 -0
- package/src/io/ooxml/parse-styles.ts +463 -0
- package/src/io/ooxml/parse-theme.ts +32 -0
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +250 -4
- package/src/model/cds-1.0.0.ts +13 -0
- package/src/model/snapshot.ts +87 -2
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-layout.ts +332 -0
- package/src/runtime/document-navigation.ts +603 -0
- package/src/runtime/document-runtime.ts +1754 -78
- package/src/runtime/document-search.ts +145 -0
- package/src/runtime/numbering-prefix.ts +47 -26
- package/src/runtime/page-layout-estimation.ts +212 -0
- package/src/runtime/read-only-diagnostics-runtime.ts +9 -0
- package/src/runtime/session-capabilities.ts +35 -3
- package/src/runtime/story-context.ts +164 -0
- package/src/runtime/story-targeting.ts +162 -0
- package/src/runtime/surface-projection.ts +324 -36
- package/src/runtime/table-schema.ts +89 -7
- package/src/runtime/view-state.ts +477 -0
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +2469 -1344
- package/src/ui/browser-export.ts +52 -0
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1422 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +51 -0
- package/src/ui/headless/preserve-editor-selection.ts +5 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/headless/selection-helpers.ts +20 -0
- package/src/ui/headless/selection-toolbar-model.ts +22 -0
- package/src/ui/headless/use-editor-keyboard.ts +6 -1
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui-tailwind/chrome/tw-alert-banner.tsx +18 -2
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +129 -0
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +114 -0
- package/src/ui-tailwind/chrome/tw-object-context-toolbar.tsx +34 -0
- package/src/ui-tailwind/chrome/tw-page-ruler.tsx +386 -0
- package/src/ui-tailwind/chrome/tw-selection-toolbar.tsx +150 -14
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +179 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +46 -7
- package/src/ui-tailwind/editor-surface/pm-contextual-ui.ts +31 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +35 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +3 -3
- package/src/ui-tailwind/editor-surface/pm-schema.ts +186 -13
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +191 -68
- package/src/ui-tailwind/editor-surface/search-plugin.ts +19 -68
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +51 -0
- package/src/ui-tailwind/editor-surface/tw-inline-token.tsx +11 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +528 -85
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +0 -1
- package/src/ui-tailwind/index.ts +2 -1
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +277 -147
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +8 -8
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +127 -0
- package/src/ui-tailwind/toolbar/tw-toolbar-icon-button.tsx +4 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +829 -12
- package/src/ui-tailwind/tw-review-workspace.tsx +1238 -42
- package/src/validation/compatibility-engine.ts +119 -24
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/diagnostics.ts +1 -0
- package/src/validation/docx-comment-proof.ts +707 -0
package/src/io/docx-session.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CompatibilityReport as PublicCompatibilityReport,
|
|
3
3
|
EditorError,
|
|
4
|
+
EditorSessionState,
|
|
4
5
|
EditorWarning as PublicEditorWarning,
|
|
5
6
|
EditorAnchorProjection as PublicEditorAnchorProjection,
|
|
6
7
|
ExportDocxOptions,
|
|
7
8
|
ExportResult,
|
|
8
9
|
PersistedEditorSnapshot,
|
|
10
|
+
ProtectionRange,
|
|
11
|
+
ProtectionSnapshot,
|
|
9
12
|
} from "../api/public-types.ts";
|
|
13
|
+
import { editorSessionStateFromPersistedSnapshot } from "../api/session-state.ts";
|
|
10
14
|
import type {
|
|
11
15
|
CanonicalDocumentEnvelope,
|
|
12
16
|
CompatibilityFeatureEntry as InternalCompatibilityFeatureEntry,
|
|
@@ -23,7 +27,12 @@ import {
|
|
|
23
27
|
} from "../core/selection/mapping.ts";
|
|
24
28
|
import { DOCX_MIME_TYPE } from "./opc/docx-package.ts";
|
|
25
29
|
import { readOpcPackage, type OpcPackage } from "./opc/package-reader.ts";
|
|
26
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
parseMainDocumentXml,
|
|
32
|
+
type ParsedBlockNode,
|
|
33
|
+
type ParsedInlineNode,
|
|
34
|
+
type ParsedPermStartInlineNode,
|
|
35
|
+
} from "./ooxml/parse-main-document.ts";
|
|
27
36
|
import { normalizeParsedTextDocument } from "./normalize/normalize-text.ts";
|
|
28
37
|
import {
|
|
29
38
|
CONTENT_TYPES_PATH,
|
|
@@ -45,6 +54,7 @@ import { parseCommentsFromOoxml } from "./ooxml/parse-comments.ts";
|
|
|
45
54
|
import { parseNumberingXml } from "./ooxml/parse-numbering.ts";
|
|
46
55
|
import {
|
|
47
56
|
createCommentExportIdMap,
|
|
57
|
+
mapParagraphBoundaries,
|
|
48
58
|
serializeCommentAnchorsIntoDocumentXml,
|
|
49
59
|
serializeMergedCommentsXml,
|
|
50
60
|
} from "./export/serialize-comments.ts";
|
|
@@ -80,6 +90,7 @@ import type {
|
|
|
80
90
|
import { createReadOnlyDiagnosticsRuntime } from "../runtime/read-only-diagnostics-runtime.ts";
|
|
81
91
|
import {
|
|
82
92
|
WORD_NUMBERING_CONTENT_TYPE,
|
|
93
|
+
hasSerializableNumberingEntries,
|
|
83
94
|
serializeNumberingXml,
|
|
84
95
|
} from "./export/serialize-numbering.ts";
|
|
85
96
|
import {
|
|
@@ -89,6 +100,9 @@ import {
|
|
|
89
100
|
} from "./ooxml/parse-headers-footers.ts";
|
|
90
101
|
import { parseFootnotesXml, parseEndnotesXml } from "./ooxml/parse-footnotes.ts";
|
|
91
102
|
import { parseThemeXml } from "./ooxml/parse-theme.ts";
|
|
103
|
+
import { resolveTheme } from "./ooxml/parse-theme.ts";
|
|
104
|
+
import { parseSettingsXml } from "./ooxml/parse-settings.ts";
|
|
105
|
+
import { parseStylesXml, type ParseStylesResult } from "./ooxml/parse-styles.ts";
|
|
92
106
|
import {
|
|
93
107
|
serializeHeaderXml,
|
|
94
108
|
serializeFooterXml,
|
|
@@ -102,6 +116,11 @@ import {
|
|
|
102
116
|
WORD_ENDNOTES_CONTENT_TYPE,
|
|
103
117
|
} from "./export/serialize-footnotes.ts";
|
|
104
118
|
import { createPersistedSourcePackage } from "./source-package-provenance.ts";
|
|
119
|
+
import { validatePersistedEditorSnapshot } from "../model/snapshot.ts";
|
|
120
|
+
import {
|
|
121
|
+
createSyntheticDocxNullNumberingCatalog,
|
|
122
|
+
DOCX_NULL_NUMBERING_INSTANCE_ID,
|
|
123
|
+
} from "./ooxml/numbering-sentinels.ts";
|
|
105
124
|
|
|
106
125
|
const MAIN_DOCUMENT_PATH = "/word/document.xml";
|
|
107
126
|
const NUMBERING_PART_PATH = "/word/numbering.xml";
|
|
@@ -149,22 +168,30 @@ const FOOTNOTES_RELATIONSHIP_TYPE =
|
|
|
149
168
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
|
|
150
169
|
const ENDNOTES_RELATIONSHIP_TYPE =
|
|
151
170
|
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes";
|
|
171
|
+
const SETTINGS_RELATIONSHIP_TYPE =
|
|
172
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings";
|
|
173
|
+
const STYLES_RELATIONSHIP_TYPE =
|
|
174
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles";
|
|
175
|
+
const STYLES_PART_PATH = "/word/styles.xml";
|
|
152
176
|
const FOOTNOTES_PART_PATH = "/word/footnotes.xml";
|
|
153
177
|
const ENDNOTES_PART_PATH = "/word/endnotes.xml";
|
|
178
|
+
const SETTINGS_PART_PATH = "/word/settings.xml";
|
|
154
179
|
|
|
155
180
|
interface LoadDocxEditorSessionOptions {
|
|
156
181
|
documentId: string;
|
|
157
182
|
sourceLabel?: string;
|
|
158
183
|
bytes: Uint8Array | ArrayBuffer;
|
|
159
|
-
editorBuild
|
|
184
|
+
editorBuild?: string;
|
|
160
185
|
}
|
|
161
186
|
|
|
162
187
|
export interface LoadedDocxEditorSession {
|
|
188
|
+
initialSessionState: EditorSessionState;
|
|
163
189
|
initialSnapshot: PersistedEditorSnapshot;
|
|
164
190
|
fatalError?: EditorError;
|
|
165
191
|
readOnly: boolean;
|
|
192
|
+
protectionSnapshot: ProtectionSnapshot;
|
|
166
193
|
exportDocx: (
|
|
167
|
-
|
|
194
|
+
sessionState: EditorSessionState | PersistedEditorSnapshot,
|
|
168
195
|
options?: ExportDocxOptions,
|
|
169
196
|
) => Promise<ExportResult>;
|
|
170
197
|
}
|
|
@@ -190,6 +217,7 @@ interface ImportedDocxState {
|
|
|
190
217
|
sourcePeopleRelationshipId?: string;
|
|
191
218
|
sourcePeopleRootTag?: string;
|
|
192
219
|
sourcePeopleAuthors: readonly string[];
|
|
220
|
+
protectionSnapshot: ProtectionSnapshot;
|
|
193
221
|
preservedCommentDefinitions: readonly ImportedCommentDefinition[];
|
|
194
222
|
blockingCommentDiagnostics: readonly CommentImportDiagnostic[];
|
|
195
223
|
initialCanonicalSignature: string;
|
|
@@ -220,6 +248,10 @@ const BLOCKING_COMMENT_DIAGNOSTIC_CODES = new Set<CommentImportDiagnostic["code"
|
|
|
220
248
|
export function loadDocxEditorSession(
|
|
221
249
|
options: LoadDocxEditorSessionOptions,
|
|
222
250
|
): LoadedDocxEditorSession {
|
|
251
|
+
const editorBuild =
|
|
252
|
+
typeof options.editorBuild === "string" && options.editorBuild.length > 0
|
|
253
|
+
? options.editorBuild
|
|
254
|
+
: "dev";
|
|
223
255
|
const sourceBytes = toUint8Array(options.bytes);
|
|
224
256
|
let sourcePackage: OpcPackage;
|
|
225
257
|
|
|
@@ -304,6 +336,7 @@ export function loadDocxEditorSession(
|
|
|
304
336
|
mediaParts,
|
|
305
337
|
mainDocumentPath,
|
|
306
338
|
);
|
|
339
|
+
const protectionRanges = extractProtectionRanges(parsedDocument.blocks);
|
|
307
340
|
const normalizedDocument = normalizeParsedTextDocument(
|
|
308
341
|
parsedDocument,
|
|
309
342
|
mainDocumentPath,
|
|
@@ -376,13 +409,14 @@ export function loadDocxEditorSession(
|
|
|
376
409
|
const parsedFooters: FooterDocument[] = [];
|
|
377
410
|
const sourceHeaderPaths: Array<{ partPath: string; relationshipId: string }> = [];
|
|
378
411
|
const sourceFooterPaths: Array<{ partPath: string; relationshipId: string }> = [];
|
|
379
|
-
const
|
|
412
|
+
const seenSubPartKeys = new Set<string>();
|
|
380
413
|
|
|
381
414
|
for (const ref of headerFooterRefs) {
|
|
382
|
-
|
|
415
|
+
const dedupeKey = `${ref.kind}:${ref.variant}:${ref.relationshipId}`;
|
|
416
|
+
if (seenSubPartKeys.has(dedupeKey)) {
|
|
383
417
|
continue;
|
|
384
418
|
}
|
|
385
|
-
|
|
419
|
+
seenSubPartKeys.add(dedupeKey);
|
|
386
420
|
|
|
387
421
|
const relationship = documentPart.relationships.find(
|
|
388
422
|
(r) => r.id === ref.relationshipId && r.targetMode === "internal",
|
|
@@ -404,6 +438,7 @@ export function loadDocxEditorSession(
|
|
|
404
438
|
variant: ref.variant,
|
|
405
439
|
partPath,
|
|
406
440
|
relationshipId: ref.relationshipId,
|
|
441
|
+
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
407
442
|
blocks: parsed.blocks,
|
|
408
443
|
});
|
|
409
444
|
sourceHeaderPaths.push({ partPath, relationshipId: ref.relationshipId });
|
|
@@ -413,6 +448,7 @@ export function loadDocxEditorSession(
|
|
|
413
448
|
variant: ref.variant,
|
|
414
449
|
partPath,
|
|
415
450
|
relationshipId: ref.relationshipId,
|
|
451
|
+
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
416
452
|
blocks: parsed.blocks,
|
|
417
453
|
});
|
|
418
454
|
sourceFooterPaths.push({ partPath, relationshipId: ref.relationshipId });
|
|
@@ -466,17 +502,60 @@ export function loadDocxEditorSession(
|
|
|
466
502
|
decodeUtf8(sourcePackage.parts.get(themePartPath)?.bytes ?? new Uint8Array()),
|
|
467
503
|
)
|
|
468
504
|
: undefined;
|
|
505
|
+
const resolvedTheme = parsedTheme ? resolveTheme(parsedTheme) : undefined;
|
|
506
|
+
const settingsPartPath = resolveDocumentRelatedPartPath(
|
|
507
|
+
sourcePackage,
|
|
508
|
+
mainDocumentPath,
|
|
509
|
+
documentPart.relationships,
|
|
510
|
+
SETTINGS_RELATIONSHIP_TYPE,
|
|
511
|
+
SETTINGS_PART_PATH,
|
|
512
|
+
);
|
|
513
|
+
const parsedSettings =
|
|
514
|
+
settingsPartPath && sourcePackage.parts.has(settingsPartPath)
|
|
515
|
+
? parseSettingsXml(
|
|
516
|
+
decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array()),
|
|
517
|
+
)
|
|
518
|
+
: undefined;
|
|
519
|
+
const settingsXmlForProtection =
|
|
520
|
+
settingsPartPath && sourcePackage.parts.has(settingsPartPath)
|
|
521
|
+
? decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array())
|
|
522
|
+
: "";
|
|
523
|
+
const documentProtection = extractDocumentProtection(settingsXmlForProtection);
|
|
524
|
+
const importedProtectionSnapshot = buildProtectionSnapshot(documentProtection, protectionRanges);
|
|
525
|
+
|
|
526
|
+
// ---- Parse styles.xml for canonical style catalog ----
|
|
527
|
+
const stylesPartPath = resolveDocumentRelatedPartPath(
|
|
528
|
+
sourcePackage,
|
|
529
|
+
mainDocumentPath,
|
|
530
|
+
documentPart.relationships,
|
|
531
|
+
STYLES_RELATIONSHIP_TYPE,
|
|
532
|
+
STYLES_PART_PATH,
|
|
533
|
+
);
|
|
534
|
+
const parsedStyles =
|
|
535
|
+
stylesPartPath && sourcePackage.parts.has(stylesPartPath)
|
|
536
|
+
? parseStylesXml(
|
|
537
|
+
decodeUtf8(sourcePackage.parts.get(stylesPartPath)?.bytes ?? new Uint8Array()),
|
|
538
|
+
)
|
|
539
|
+
: parseStylesXml("");
|
|
469
540
|
|
|
470
541
|
const subParts: SubPartsCatalog | undefined =
|
|
471
542
|
parsedHeaders.length > 0 ||
|
|
472
543
|
parsedFooters.length > 0 ||
|
|
473
544
|
footnoteCollection !== undefined ||
|
|
474
|
-
parsedTheme !== undefined
|
|
545
|
+
parsedTheme !== undefined ||
|
|
546
|
+
normalizedDocument.finalSectionProperties !== undefined ||
|
|
547
|
+
resolvedTheme !== undefined ||
|
|
548
|
+
parsedSettings !== undefined
|
|
475
549
|
? {
|
|
476
550
|
headers: parsedHeaders,
|
|
477
551
|
footers: parsedFooters,
|
|
478
552
|
...(footnoteCollection !== undefined ? { footnoteCollection } : {}),
|
|
479
553
|
...(parsedTheme !== undefined ? { theme: parsedTheme } : {}),
|
|
554
|
+
...(normalizedDocument.finalSectionProperties !== undefined
|
|
555
|
+
? { finalSectionProperties: normalizedDocument.finalSectionProperties }
|
|
556
|
+
: {}),
|
|
557
|
+
...(resolvedTheme !== undefined ? { resolvedTheme } : {}),
|
|
558
|
+
...(parsedSettings !== undefined ? { settings: parsedSettings } : {}),
|
|
480
559
|
}
|
|
481
560
|
: undefined;
|
|
482
561
|
|
|
@@ -488,6 +567,7 @@ export function loadDocxEditorSession(
|
|
|
488
567
|
media: normalizedDocument.media,
|
|
489
568
|
content: normalizedDocument.content,
|
|
490
569
|
subParts,
|
|
570
|
+
parsedStyles,
|
|
491
571
|
preservation: {
|
|
492
572
|
...normalizedDocument.preservation,
|
|
493
573
|
packageParts: {
|
|
@@ -532,12 +612,29 @@ export function loadDocxEditorSession(
|
|
|
532
612
|
});
|
|
533
613
|
const snapshot = createImportedSnapshot({
|
|
534
614
|
documentId: options.documentId,
|
|
535
|
-
editorBuild
|
|
615
|
+
editorBuild,
|
|
536
616
|
timestamp,
|
|
537
617
|
document,
|
|
538
618
|
compatibility: toPublicCompatibilityReport(compatibility),
|
|
619
|
+
protectionSnapshot: importedProtectionSnapshot,
|
|
539
620
|
sourcePackage: createPersistedSourcePackage(sourceBytes, options.sourceLabel),
|
|
540
621
|
});
|
|
622
|
+
const snapshotIssues = validatePersistedEditorSnapshot(snapshot);
|
|
623
|
+
if (snapshotIssues.length > 0) {
|
|
624
|
+
const firstIssue = snapshotIssues[0];
|
|
625
|
+
return createDiagnosticsSession(
|
|
626
|
+
options,
|
|
627
|
+
createValidationImportDiagnostics({
|
|
628
|
+
message: `DOCX import produced an invalid editor state during validation${firstIssue ? ` (${firstIssue.path}: ${firstIssue.message})` : "."}`,
|
|
629
|
+
source: "import",
|
|
630
|
+
details: {
|
|
631
|
+
issueCount: snapshotIssues.length,
|
|
632
|
+
firstIssuePath: firstIssue?.path,
|
|
633
|
+
},
|
|
634
|
+
}),
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
const initialSessionState = editorSessionStateFromPersistedSnapshot(snapshot);
|
|
541
638
|
const importedState: ImportedDocxState = {
|
|
542
639
|
sourceBytes: new Uint8Array(sourceBytes),
|
|
543
640
|
sourcePackage,
|
|
@@ -579,6 +676,7 @@ export function loadDocxEditorSession(
|
|
|
579
676
|
)?.id,
|
|
580
677
|
sourcePeopleRootTag: normalizedComments.sourcePeopleRootTag,
|
|
581
678
|
sourcePeopleAuthors: normalizedComments.peopleAuthors,
|
|
679
|
+
protectionSnapshot: buildProtectionSnapshot(documentProtection, protectionRanges),
|
|
582
680
|
preservedCommentDefinitions: normalizedComments.preservedDefinitions,
|
|
583
681
|
blockingCommentDiagnostics: normalizedComments.diagnostics.filter((diagnostic) =>
|
|
584
682
|
BLOCKING_COMMENT_DIAGNOSTIC_CODES.has(diagnostic.code),
|
|
@@ -597,10 +695,12 @@ export function loadDocxEditorSession(
|
|
|
597
695
|
};
|
|
598
696
|
|
|
599
697
|
return {
|
|
698
|
+
initialSessionState,
|
|
600
699
|
initialSnapshot: snapshot,
|
|
601
700
|
readOnly: false,
|
|
602
|
-
|
|
603
|
-
|
|
701
|
+
protectionSnapshot: importedProtectionSnapshot,
|
|
702
|
+
exportDocx: async (nextSessionState, exportOptions) =>
|
|
703
|
+
exportDocxEditorSession(importedState, nextSessionState, exportOptions),
|
|
604
704
|
};
|
|
605
705
|
} catch (error) {
|
|
606
706
|
return createDiagnosticsSession(
|
|
@@ -612,28 +712,41 @@ export function loadDocxEditorSession(
|
|
|
612
712
|
|
|
613
713
|
function exportDocxEditorSession(
|
|
614
714
|
state: ImportedDocxState,
|
|
615
|
-
|
|
715
|
+
sessionStateOrSnapshot: EditorSessionState | PersistedEditorSnapshot,
|
|
616
716
|
options?: ExportDocxOptions,
|
|
617
717
|
): ExportResult {
|
|
618
|
-
|
|
718
|
+
const sessionState = toEditorSessionState(sessionStateOrSnapshot);
|
|
719
|
+
|
|
720
|
+
if (sessionState.compatibility.blockExport) {
|
|
619
721
|
throw new Error("DOCX export is blocked by the current compatibility report.");
|
|
620
722
|
}
|
|
621
723
|
|
|
622
|
-
const currentDocument =
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
724
|
+
const currentDocument = sessionState.canonicalDocument as CanonicalDocumentEnvelope;
|
|
725
|
+
const signatureMatch = serializeCanonicalDocumentForExport(currentDocument) ===
|
|
726
|
+
state.initialCanonicalSignature;
|
|
727
|
+
const canReuse = canReuseSourceBytesForCurrentDocument(state, currentDocument);
|
|
728
|
+
const commentCount = Object.keys(currentDocument.review?.comments ?? {}).length;
|
|
729
|
+
console.error(`[DEBUG-EXPORT] docId=${sessionState.documentId} signatureMatch=${signatureMatch} canReuse=${canReuse} preservedDefs=${state.preservedCommentDefinitions.length} blockingDiags=${state.blockingCommentDiagnostics.length} comments=${commentCount}`);
|
|
730
|
+
if (signatureMatch && canReuse) {
|
|
628
731
|
return {
|
|
629
732
|
bytes: new Uint8Array(state.sourceBytes),
|
|
630
733
|
mimeType: DOCX_MIME_TYPE,
|
|
631
|
-
fileName: options?.fileName ?? `${
|
|
734
|
+
fileName: options?.fileName ?? `${sessionState.documentId}.docx`,
|
|
735
|
+
delivery: {
|
|
736
|
+
mode: "exported-bytes-only",
|
|
737
|
+
},
|
|
632
738
|
};
|
|
633
739
|
}
|
|
634
|
-
|
|
740
|
+
const preservedCommentIds = new Set(
|
|
741
|
+
state.preservedCommentDefinitions.map((definition) => definition.commentId),
|
|
742
|
+
);
|
|
743
|
+
const blockingCommentCount = Math.max(
|
|
744
|
+
state.blockingCommentDiagnostics.length,
|
|
745
|
+
preservedCommentIds.size,
|
|
746
|
+
);
|
|
747
|
+
if (blockingCommentCount > 0) {
|
|
635
748
|
throw new Error(
|
|
636
|
-
`DOCX export is blocked because ${
|
|
749
|
+
`DOCX export is blocked because ${blockingCommentCount} preserve-only comment anchors cannot be safely remapped after runtime edits.`,
|
|
637
750
|
);
|
|
638
751
|
}
|
|
639
752
|
const currentRevisions = toReviewRevisionRecords(currentDocument.review.revisions);
|
|
@@ -643,9 +756,6 @@ function exportDocxEditorSession(
|
|
|
643
756
|
const commentThreads = Object.values(
|
|
644
757
|
createCommentStoreFromRuntimeComments(currentDocument.review.comments).threads,
|
|
645
758
|
);
|
|
646
|
-
const preservedCommentIds = new Set(
|
|
647
|
-
state.preservedCommentDefinitions.map((definition) => definition.commentId),
|
|
648
|
-
);
|
|
649
759
|
const ownedCommentThreads = commentThreads.filter(
|
|
650
760
|
(thread) => !preservedCommentIds.has(thread.commentId),
|
|
651
761
|
);
|
|
@@ -660,6 +770,7 @@ function exportDocxEditorSession(
|
|
|
660
770
|
{
|
|
661
771
|
documentAttributes: state.sourceDocumentAttributes,
|
|
662
772
|
media: currentDocument.media as MediaCatalog,
|
|
773
|
+
finalSectionProperties: currentDocument.subParts?.finalSectionProperties,
|
|
663
774
|
},
|
|
664
775
|
);
|
|
665
776
|
const revisionDocument = serializeRuntimeRevisionsIntoDocumentXml(
|
|
@@ -698,6 +809,10 @@ function exportDocxEditorSession(
|
|
|
698
809
|
exportCommentIds,
|
|
699
810
|
},
|
|
700
811
|
);
|
|
812
|
+
const protectedDocumentXml = serializeProtectionRangesIntoDocumentXml(
|
|
813
|
+
annotatedDocument.documentXml,
|
|
814
|
+
state.protectionSnapshot,
|
|
815
|
+
);
|
|
701
816
|
const blockingSkippedCommentIds = annotatedDocument.skippedCommentIds.filter((commentId) => {
|
|
702
817
|
const thread = ownedCommentThreads.find((candidate) => candidate.commentId === commentId);
|
|
703
818
|
return !thread || thread.anchor.kind !== "detached";
|
|
@@ -717,7 +832,9 @@ function exportDocxEditorSession(
|
|
|
717
832
|
state.sourcePeoplePartPath ?? PEOPLE_PART_PATH;
|
|
718
833
|
const numberingPartPath =
|
|
719
834
|
state.sourceNumberingPartPath ?? NUMBERING_PART_PATH;
|
|
720
|
-
const serializedNumberingXml =
|
|
835
|
+
const serializedNumberingXml = hasSerializableNumberingEntries(
|
|
836
|
+
currentDocument.numbering as NumberingCatalog,
|
|
837
|
+
)
|
|
721
838
|
? serializeNumberingXml(currentDocument.numbering as NumberingCatalog)
|
|
722
839
|
: undefined;
|
|
723
840
|
const nextRelationships = withDocumentRelatedParts(
|
|
@@ -802,7 +919,7 @@ function exportDocxEditorSession(
|
|
|
802
919
|
|
|
803
920
|
exportSession.replaceOwnedPart({
|
|
804
921
|
path: state.sourceDocumentPartPath,
|
|
805
|
-
bytes: new TextEncoder().encode(
|
|
922
|
+
bytes: new TextEncoder().encode(protectedDocumentXml),
|
|
806
923
|
contentType: MAIN_DOCUMENT_CONTENT_TYPE,
|
|
807
924
|
relationships: nextRelationships,
|
|
808
925
|
});
|
|
@@ -923,10 +1040,21 @@ function exportDocxEditorSession(
|
|
|
923
1040
|
return {
|
|
924
1041
|
bytes: exportSession.serialize(),
|
|
925
1042
|
mimeType: DOCX_MIME_TYPE,
|
|
926
|
-
fileName: options?.fileName ?? `${
|
|
1043
|
+
fileName: options?.fileName ?? `${sessionState.documentId}.docx`,
|
|
1044
|
+
delivery: {
|
|
1045
|
+
mode: "exported-bytes-only",
|
|
1046
|
+
},
|
|
927
1047
|
};
|
|
928
1048
|
}
|
|
929
1049
|
|
|
1050
|
+
function toEditorSessionState(
|
|
1051
|
+
sessionStateOrSnapshot: EditorSessionState | PersistedEditorSnapshot,
|
|
1052
|
+
): EditorSessionState {
|
|
1053
|
+
return "sessionVersion" in sessionStateOrSnapshot
|
|
1054
|
+
? sessionStateOrSnapshot
|
|
1055
|
+
: editorSessionStateFromPersistedSnapshot(sessionStateOrSnapshot);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
930
1058
|
function createImportedCanonicalDocument(input: {
|
|
931
1059
|
documentId: string;
|
|
932
1060
|
timestamp: string;
|
|
@@ -934,23 +1062,21 @@ function createImportedCanonicalDocument(input: {
|
|
|
934
1062
|
media: CanonicalDocumentEnvelope["media"];
|
|
935
1063
|
content: CanonicalDocumentEnvelope["content"];
|
|
936
1064
|
subParts?: SubPartsCatalog;
|
|
1065
|
+
parsedStyles?: ParseStylesResult;
|
|
937
1066
|
preservation: CanonicalDocumentEnvelope["preservation"];
|
|
938
1067
|
diagnostics: CanonicalDocumentEnvelope["diagnostics"];
|
|
939
1068
|
review: CanonicalDocumentEnvelope["review"];
|
|
940
1069
|
}): CanonicalDocumentEnvelope {
|
|
941
|
-
const
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
.map((styleId) => [
|
|
945
|
-
styleId,
|
|
946
|
-
{
|
|
947
|
-
styleId,
|
|
948
|
-
displayName: styleId,
|
|
949
|
-
kind: "paragraph" as const,
|
|
950
|
-
isDefault: styleId === "Normal",
|
|
951
|
-
},
|
|
952
|
-
]),
|
|
1070
|
+
const numbering = ensureImportedNumberingCatalogSupportsContent(
|
|
1071
|
+
input.numbering,
|
|
1072
|
+
input.content,
|
|
953
1073
|
);
|
|
1074
|
+
|
|
1075
|
+
// Use package-backed style catalog when available; fall back to synthetic
|
|
1076
|
+
// styles derived from referenced styleId values when styles.xml is missing
|
|
1077
|
+
// or could not be parsed.
|
|
1078
|
+
const styles = buildStylesCatalog(input.parsedStyles, input.content, input.subParts);
|
|
1079
|
+
|
|
954
1080
|
return {
|
|
955
1081
|
schemaVersion: "cds/1.0.0",
|
|
956
1082
|
docId: createCanonicalDocumentId(input.documentId),
|
|
@@ -959,12 +1085,8 @@ function createImportedCanonicalDocument(input: {
|
|
|
959
1085
|
metadata: {
|
|
960
1086
|
customProperties: {},
|
|
961
1087
|
},
|
|
962
|
-
styles
|
|
963
|
-
|
|
964
|
-
characters: {},
|
|
965
|
-
tables: {},
|
|
966
|
-
},
|
|
967
|
-
numbering: input.numbering,
|
|
1088
|
+
styles,
|
|
1089
|
+
numbering,
|
|
968
1090
|
media: input.media,
|
|
969
1091
|
content: input.content,
|
|
970
1092
|
review: input.review,
|
|
@@ -974,6 +1096,136 @@ function createImportedCanonicalDocument(input: {
|
|
|
974
1096
|
};
|
|
975
1097
|
}
|
|
976
1098
|
|
|
1099
|
+
// Canonical model styleId validation pattern — styleIds that don't match
|
|
1100
|
+
// are excluded from the catalog to avoid snapshot validation failures.
|
|
1101
|
+
const VALID_STYLE_ID = /^[A-Za-z_][A-Za-z0-9._-]{0,127}$/;
|
|
1102
|
+
|
|
1103
|
+
function buildStylesCatalog(
|
|
1104
|
+
parsedStyles: ParseStylesResult | undefined,
|
|
1105
|
+
content: CanonicalDocumentEnvelope["content"],
|
|
1106
|
+
subParts?: SubPartsCatalog,
|
|
1107
|
+
): CanonicalDocumentEnvelope["styles"] {
|
|
1108
|
+
if (parsedStyles?.fromPackage) {
|
|
1109
|
+
// Package-backed catalog: filter entries whose styleId does not satisfy
|
|
1110
|
+
// the canonical model pattern (e.g. numeric-only ids from Word).
|
|
1111
|
+
const catalog = filterValidStyleIds(parsedStyles.catalog);
|
|
1112
|
+
|
|
1113
|
+
// Merge in any referenced styleIds that the package styles.xml did not
|
|
1114
|
+
// define (rare but defensive).
|
|
1115
|
+
const referencedIds = collectReferencedParagraphStyleIds(content, subParts);
|
|
1116
|
+
for (const styleId of referencedIds) {
|
|
1117
|
+
if (!catalog.paragraphs[styleId] && VALID_STYLE_ID.test(styleId)) {
|
|
1118
|
+
catalog.paragraphs[styleId] = {
|
|
1119
|
+
styleId,
|
|
1120
|
+
displayName: styleId,
|
|
1121
|
+
kind: "paragraph",
|
|
1122
|
+
isDefault: styleId === "Normal",
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
return {
|
|
1127
|
+
...catalog,
|
|
1128
|
+
fromPackage: true,
|
|
1129
|
+
};
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
// Synthetic fallback: no styles.xml available
|
|
1133
|
+
const paragraphStyles = Object.fromEntries(
|
|
1134
|
+
[...collectReferencedParagraphStyleIds(content, subParts)]
|
|
1135
|
+
.sort((left, right) => left.localeCompare(right))
|
|
1136
|
+
.filter((styleId) => VALID_STYLE_ID.test(styleId))
|
|
1137
|
+
.map((styleId) => [
|
|
1138
|
+
styleId,
|
|
1139
|
+
{
|
|
1140
|
+
styleId,
|
|
1141
|
+
displayName: styleId,
|
|
1142
|
+
kind: "paragraph" as const,
|
|
1143
|
+
isDefault: styleId === "Normal",
|
|
1144
|
+
},
|
|
1145
|
+
]),
|
|
1146
|
+
);
|
|
1147
|
+
return {
|
|
1148
|
+
paragraphs: paragraphStyles,
|
|
1149
|
+
characters: {},
|
|
1150
|
+
tables: {},
|
|
1151
|
+
fromPackage: false,
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
function filterValidStyleIds(
|
|
1156
|
+
catalog: CanonicalDocumentEnvelope["styles"],
|
|
1157
|
+
): CanonicalDocumentEnvelope["styles"] {
|
|
1158
|
+
const filterRecord = <T extends { styleId: string }>(
|
|
1159
|
+
record: Record<string, T>,
|
|
1160
|
+
): Record<string, T> => {
|
|
1161
|
+
const result: Record<string, T> = {};
|
|
1162
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1163
|
+
if (VALID_STYLE_ID.test(key)) {
|
|
1164
|
+
result[key] = value;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
return result;
|
|
1168
|
+
};
|
|
1169
|
+
|
|
1170
|
+
return {
|
|
1171
|
+
paragraphs: filterRecord(catalog.paragraphs),
|
|
1172
|
+
characters: filterRecord(catalog.characters),
|
|
1173
|
+
tables: filterRecord(catalog.tables),
|
|
1174
|
+
...(catalog.latentStyles ? { latentStyles: catalog.latentStyles } : {}),
|
|
1175
|
+
...(catalog.fromPackage !== undefined ? { fromPackage: catalog.fromPackage } : {}),
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function ensureImportedNumberingCatalogSupportsContent(
|
|
1180
|
+
catalog: NumberingCatalog,
|
|
1181
|
+
content: CanonicalDocumentEnvelope["content"],
|
|
1182
|
+
): NumberingCatalog {
|
|
1183
|
+
if (
|
|
1184
|
+
catalog.instances[DOCX_NULL_NUMBERING_INSTANCE_ID] ||
|
|
1185
|
+
!collectReferencedNumberingInstanceIds(content).has(DOCX_NULL_NUMBERING_INSTANCE_ID)
|
|
1186
|
+
) {
|
|
1187
|
+
return catalog;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
const syntheticNullCatalog = createSyntheticDocxNullNumberingCatalog();
|
|
1191
|
+
return {
|
|
1192
|
+
abstractDefinitions: {
|
|
1193
|
+
...catalog.abstractDefinitions,
|
|
1194
|
+
...syntheticNullCatalog.abstractDefinitions,
|
|
1195
|
+
},
|
|
1196
|
+
instances: {
|
|
1197
|
+
...catalog.instances,
|
|
1198
|
+
...syntheticNullCatalog.instances,
|
|
1199
|
+
},
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
function collectReferencedNumberingInstanceIds(
|
|
1204
|
+
content: CanonicalDocumentEnvelope["content"],
|
|
1205
|
+
): Set<string> {
|
|
1206
|
+
const numberingInstanceIds = new Set<string>();
|
|
1207
|
+
|
|
1208
|
+
const visitBlocks = (blocks: ReadonlyArray<BlockNode>) => {
|
|
1209
|
+
for (const block of blocks) {
|
|
1210
|
+
if (block.type === "paragraph" && block.numbering?.numberingInstanceId) {
|
|
1211
|
+
numberingInstanceIds.add(block.numbering.numberingInstanceId);
|
|
1212
|
+
}
|
|
1213
|
+
if (block.type === "table") {
|
|
1214
|
+
for (const row of block.rows) {
|
|
1215
|
+
for (const cell of row.cells) {
|
|
1216
|
+
visitBlocks(cell.children);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
} else if (block.type === "sdt" || block.type === "custom_xml") {
|
|
1220
|
+
visitBlocks(block.children);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
|
|
1225
|
+
visitBlocks(content.children);
|
|
1226
|
+
return numberingInstanceIds;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
977
1229
|
function collectReferencedParagraphStyleIds(
|
|
978
1230
|
content: CanonicalDocumentEnvelope["content"],
|
|
979
1231
|
subParts?: SubPartsCatalog,
|
|
@@ -1024,6 +1276,7 @@ function createImportedSnapshot(input: {
|
|
|
1024
1276
|
timestamp: string;
|
|
1025
1277
|
document: CanonicalDocumentEnvelope;
|
|
1026
1278
|
compatibility: PersistedEditorSnapshot["compatibility"];
|
|
1279
|
+
protectionSnapshot: ProtectionSnapshot;
|
|
1027
1280
|
sourcePackage?: PersistedEditorSnapshot["sourcePackage"];
|
|
1028
1281
|
}): PersistedEditorSnapshot {
|
|
1029
1282
|
return {
|
|
@@ -1038,6 +1291,7 @@ function createImportedSnapshot(input: {
|
|
|
1038
1291
|
canonicalDocument: input.document,
|
|
1039
1292
|
compatibility: input.compatibility,
|
|
1040
1293
|
warningLog: input.compatibility.warnings,
|
|
1294
|
+
protectionSnapshot: input.protectionSnapshot,
|
|
1041
1295
|
sourcePackage: input.sourcePackage,
|
|
1042
1296
|
};
|
|
1043
1297
|
}
|
|
@@ -1110,20 +1364,27 @@ function createDiagnosticsSession(
|
|
|
1110
1364
|
diagnostics: ImportDiagnosticsResult,
|
|
1111
1365
|
): LoadedDocxEditorSession {
|
|
1112
1366
|
const timestamp = new Date().toISOString();
|
|
1367
|
+
const editorBuild =
|
|
1368
|
+
typeof options.editorBuild === "string" && options.editorBuild.length > 0
|
|
1369
|
+
? options.editorBuild
|
|
1370
|
+
: "dev";
|
|
1113
1371
|
const runtime = createReadOnlyDiagnosticsRuntime({
|
|
1114
1372
|
documentId: options.documentId,
|
|
1115
1373
|
sourceLabel: options.sourceLabel,
|
|
1116
|
-
editorBuild
|
|
1374
|
+
editorBuild,
|
|
1117
1375
|
generatedAt: timestamp,
|
|
1118
1376
|
diagnostics,
|
|
1119
1377
|
});
|
|
1120
1378
|
const initialSnapshot = runtime.getPersistedSnapshot();
|
|
1379
|
+
const initialSessionState = editorSessionStateFromPersistedSnapshot(initialSnapshot);
|
|
1121
1380
|
|
|
1122
1381
|
return {
|
|
1382
|
+
initialSessionState,
|
|
1123
1383
|
initialSnapshot,
|
|
1124
1384
|
fatalError: diagnostics.fatalError,
|
|
1125
1385
|
readOnly: true,
|
|
1126
|
-
|
|
1386
|
+
protectionSnapshot: EMPTY_PROTECTION_SNAPSHOT,
|
|
1387
|
+
exportDocx: async (_sessionState, exportOptions) => runtime.exportDocx(exportOptions),
|
|
1127
1388
|
};
|
|
1128
1389
|
}
|
|
1129
1390
|
|
|
@@ -1238,8 +1499,10 @@ function normalizeImportedCommentThreads(
|
|
|
1238
1499
|
commentId: thread.commentId,
|
|
1239
1500
|
code: "opaque_anchor_preserve_only",
|
|
1240
1501
|
message:
|
|
1241
|
-
"Comment anchor intersects preserve-only OOXML
|
|
1502
|
+
"Comment anchor intersects preserve-only OOXML content. Thread is visible but detached; anchor cannot be safely remapped.",
|
|
1242
1503
|
featureClass: "preserve-only",
|
|
1504
|
+
detachedReason: "opaque-region" as const,
|
|
1505
|
+
actionabilityNote: "The comment body is preserved. The anchor overlaps opaque content that the editor cannot safely modify.",
|
|
1243
1506
|
});
|
|
1244
1507
|
return {
|
|
1245
1508
|
...thread,
|
|
@@ -1257,8 +1520,10 @@ function normalizeImportedCommentThreads(
|
|
|
1257
1520
|
commentId: thread.commentId,
|
|
1258
1521
|
code: "preserve_only_revision_overlap",
|
|
1259
1522
|
message:
|
|
1260
|
-
"Comment anchor overlaps preserve-only review markup
|
|
1523
|
+
"Comment anchor overlaps preserve-only review markup. Thread is visible but detached; anchor cannot be safely remapped during editing.",
|
|
1261
1524
|
featureClass: "preserve-only",
|
|
1525
|
+
detachedReason: "revision-overlap" as const,
|
|
1526
|
+
actionabilityNote: "The comment body is preserved. The anchor overlaps preserve-only revision markup that the editor cannot safely modify.",
|
|
1262
1527
|
});
|
|
1263
1528
|
return {
|
|
1264
1529
|
...thread,
|
|
@@ -1670,13 +1935,6 @@ function createEmptyNumberingCatalog(): NumberingCatalog {
|
|
|
1670
1935
|
};
|
|
1671
1936
|
}
|
|
1672
1937
|
|
|
1673
|
-
function hasNumberingEntries(catalog: NumberingCatalog): boolean {
|
|
1674
|
-
return (
|
|
1675
|
-
Object.keys(catalog.abstractDefinitions ?? {}).length > 0 ||
|
|
1676
|
-
Object.keys(catalog.instances ?? {}).length > 0
|
|
1677
|
-
);
|
|
1678
|
-
}
|
|
1679
|
-
|
|
1680
1938
|
function collectBrokenInternalRelationshipIssues(
|
|
1681
1939
|
sourcePackage: OpcPackage,
|
|
1682
1940
|
mainDocumentPath?: string,
|
|
@@ -2126,6 +2384,108 @@ function xmlNode(tagName: string, value: string | undefined): string | undefined
|
|
|
2126
2384
|
return `<${tagName}>${escapeXml(value)}</${tagName.split(" ", 1)[0]}>`;
|
|
2127
2385
|
}
|
|
2128
2386
|
|
|
2387
|
+
function serializeProtectionRangesIntoDocumentXml(
|
|
2388
|
+
documentXml: string,
|
|
2389
|
+
protection: ProtectionSnapshot,
|
|
2390
|
+
paragraphs = mapParagraphBoundaries(documentXml),
|
|
2391
|
+
): string {
|
|
2392
|
+
if (protection.ranges.length === 0) {
|
|
2393
|
+
return documentXml;
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
const insertions = new Map<number, string[]>();
|
|
2397
|
+
|
|
2398
|
+
for (const range of protection.ranges) {
|
|
2399
|
+
if (typeof range.start !== "number" || typeof range.end !== "number") {
|
|
2400
|
+
continue;
|
|
2401
|
+
}
|
|
2402
|
+
const rangeStart = range.start;
|
|
2403
|
+
const rangeEnd = range.end;
|
|
2404
|
+
|
|
2405
|
+
const startParagraph = paragraphs.find(
|
|
2406
|
+
(candidate) => rangeStart >= candidate.start && rangeStart <= candidate.end,
|
|
2407
|
+
);
|
|
2408
|
+
const endParagraph = paragraphs.find(
|
|
2409
|
+
(candidate) => rangeEnd >= candidate.start && rangeEnd <= candidate.end,
|
|
2410
|
+
);
|
|
2411
|
+
if (!startParagraph || !endParagraph) {
|
|
2412
|
+
continue;
|
|
2413
|
+
}
|
|
2414
|
+
|
|
2415
|
+
const startIndex =
|
|
2416
|
+
startParagraph.boundaries.get(rangeStart) ??
|
|
2417
|
+
findNearestBoundaryIndex(startParagraph.boundaries, rangeStart, "backward");
|
|
2418
|
+
const endIndex =
|
|
2419
|
+
endParagraph.boundaries.get(rangeEnd) ??
|
|
2420
|
+
findNearestBoundaryIndex(endParagraph.boundaries, rangeEnd, "forward");
|
|
2421
|
+
if (startIndex === undefined || endIndex === undefined) {
|
|
2422
|
+
continue;
|
|
2423
|
+
}
|
|
2424
|
+
|
|
2425
|
+
const permStartXml = [
|
|
2426
|
+
`<w:permStart`,
|
|
2427
|
+
` w:id="${escapeXmlAttribute(range.rangeId)}"`,
|
|
2428
|
+
range.editorGroup ? ` w:edGrp="${escapeXmlAttribute(range.editorGroup)}"` : "",
|
|
2429
|
+
range.editor ? ` w:ed="${escapeXmlAttribute(range.editor)}"` : "",
|
|
2430
|
+
`/>`,
|
|
2431
|
+
].join("");
|
|
2432
|
+
const permEndXml = `<w:permEnd w:id="${escapeXmlAttribute(range.rangeId)}"/>`;
|
|
2433
|
+
|
|
2434
|
+
pushProtectionInsertion(insertions, startIndex, permStartXml);
|
|
2435
|
+
pushProtectionInsertion(insertions, endIndex, permEndXml);
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
if (insertions.size === 0) {
|
|
2439
|
+
return documentXml;
|
|
2440
|
+
}
|
|
2441
|
+
|
|
2442
|
+
const parts: string[] = [];
|
|
2443
|
+
let cursor = 0;
|
|
2444
|
+
for (const [index, snippets] of [...insertions.entries()].sort(([left], [right]) => left - right)) {
|
|
2445
|
+
parts.push(documentXml.slice(cursor, index));
|
|
2446
|
+
parts.push(...snippets);
|
|
2447
|
+
cursor = index;
|
|
2448
|
+
}
|
|
2449
|
+
parts.push(documentXml.slice(cursor));
|
|
2450
|
+
return parts.join("");
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
function pushProtectionInsertion(
|
|
2454
|
+
insertions: Map<number, string[]>,
|
|
2455
|
+
index: number,
|
|
2456
|
+
xml: string,
|
|
2457
|
+
): void {
|
|
2458
|
+
const existing = insertions.get(index);
|
|
2459
|
+
if (existing) {
|
|
2460
|
+
existing.push(xml);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
insertions.set(index, [xml]);
|
|
2464
|
+
}
|
|
2465
|
+
|
|
2466
|
+
function findNearestBoundaryIndex(
|
|
2467
|
+
boundaries: Map<number, number>,
|
|
2468
|
+
position: number,
|
|
2469
|
+
direction: "backward" | "forward",
|
|
2470
|
+
): number | undefined {
|
|
2471
|
+
const ordered = [...boundaries.entries()].sort(([left], [right]) => left - right);
|
|
2472
|
+
if (direction === "backward") {
|
|
2473
|
+
for (let index = ordered.length - 1; index >= 0; index -= 1) {
|
|
2474
|
+
const [boundaryPos, boundaryIndex] = ordered[index]!;
|
|
2475
|
+
if (boundaryPos <= position) {
|
|
2476
|
+
return boundaryIndex;
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
return undefined;
|
|
2480
|
+
}
|
|
2481
|
+
for (const [boundaryPos, boundaryIndex] of ordered) {
|
|
2482
|
+
if (boundaryPos >= position) {
|
|
2483
|
+
return boundaryIndex;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
return undefined;
|
|
2487
|
+
}
|
|
2488
|
+
|
|
2129
2489
|
function escapeXml(value: string): string {
|
|
2130
2490
|
return value
|
|
2131
2491
|
.replace(/&/g, "&")
|
|
@@ -2134,3 +2494,210 @@ function escapeXml(value: string): string {
|
|
|
2134
2494
|
.replace(/\"/g, """)
|
|
2135
2495
|
.replace(/'/g, "'");
|
|
2136
2496
|
}
|
|
2497
|
+
|
|
2498
|
+
function escapeXmlAttribute(value: string): string {
|
|
2499
|
+
return value
|
|
2500
|
+
.replace(/&/g, "&")
|
|
2501
|
+
.replace(/</g, "<")
|
|
2502
|
+
.replace(/>/g, ">")
|
|
2503
|
+
.replace(/"/g, """);
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// ---------------------------------------------------------------------------
|
|
2507
|
+
// Protection range extraction
|
|
2508
|
+
// ---------------------------------------------------------------------------
|
|
2509
|
+
|
|
2510
|
+
const EMPTY_PROTECTION_SNAPSHOT: ProtectionSnapshot = {
|
|
2511
|
+
hasDocumentProtection: false,
|
|
2512
|
+
enforcementActive: false,
|
|
2513
|
+
ranges: [],
|
|
2514
|
+
enforcedRangeCount: 0,
|
|
2515
|
+
preservedRangeCount: 0,
|
|
2516
|
+
};
|
|
2517
|
+
|
|
2518
|
+
interface DocumentProtectionMeta {
|
|
2519
|
+
editType?: string;
|
|
2520
|
+
enforcement: boolean;
|
|
2521
|
+
}
|
|
2522
|
+
|
|
2523
|
+
function extractDocumentProtection(settingsXml: string): DocumentProtectionMeta {
|
|
2524
|
+
if (!settingsXml) return { enforcement: false };
|
|
2525
|
+
const match = settingsXml.match(/<w:documentProtection\b([^/>]*)\/?>/);
|
|
2526
|
+
if (!match) return { enforcement: false };
|
|
2527
|
+
const attrs = match[1];
|
|
2528
|
+
const editTypeMatch = attrs.match(/w:edit="([^"]*)"/);
|
|
2529
|
+
const enforcementMatch = attrs.match(/w:enforcement="([^"]*)"/);
|
|
2530
|
+
const editType = editTypeMatch?.[1];
|
|
2531
|
+
const enforcement = enforcementMatch?.[1] === "1" || enforcementMatch?.[1] === "true";
|
|
2532
|
+
return { editType, enforcement };
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
function extractProtectionRanges(blocks: readonly ParsedBlockNode[]): ProtectionRange[] {
|
|
2536
|
+
const ranges: ProtectionRange[] = [];
|
|
2537
|
+
const openRanges = new Map<string, Omit<ProtectionRange, "end">>();
|
|
2538
|
+
collectProtectionRangesFromBlocks(blocks, ranges, openRanges, 0);
|
|
2539
|
+
return ranges;
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2542
|
+
function collectProtectionRangesFromBlocks(
|
|
2543
|
+
blocks: readonly ParsedBlockNode[],
|
|
2544
|
+
ranges: ProtectionRange[],
|
|
2545
|
+
openRanges: Map<string, Omit<ProtectionRange, "end">>,
|
|
2546
|
+
cursor: number,
|
|
2547
|
+
): number {
|
|
2548
|
+
let nextCursor = cursor;
|
|
2549
|
+
let previousParagraph = false;
|
|
2550
|
+
|
|
2551
|
+
for (const block of blocks) {
|
|
2552
|
+
if (block.type === "paragraph") {
|
|
2553
|
+
if (previousParagraph) {
|
|
2554
|
+
nextCursor += 1;
|
|
2555
|
+
}
|
|
2556
|
+
nextCursor = collectProtectionRangesFromInlines(
|
|
2557
|
+
block.children,
|
|
2558
|
+
ranges,
|
|
2559
|
+
openRanges,
|
|
2560
|
+
nextCursor,
|
|
2561
|
+
);
|
|
2562
|
+
previousParagraph = true;
|
|
2563
|
+
continue;
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
if (block.type === "table") {
|
|
2567
|
+
nextCursor += 1;
|
|
2568
|
+
previousParagraph = false;
|
|
2569
|
+
for (const row of block.rows) {
|
|
2570
|
+
for (const cell of row.cells) {
|
|
2571
|
+
nextCursor = collectProtectionRangesFromBlocks(
|
|
2572
|
+
cell.children,
|
|
2573
|
+
ranges,
|
|
2574
|
+
openRanges,
|
|
2575
|
+
nextCursor,
|
|
2576
|
+
);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
|
|
2582
|
+
if (block.type === "sdt" || block.type === "custom_xml") {
|
|
2583
|
+
nextCursor = collectProtectionRangesFromBlocks(
|
|
2584
|
+
block.children,
|
|
2585
|
+
ranges,
|
|
2586
|
+
openRanges,
|
|
2587
|
+
nextCursor,
|
|
2588
|
+
);
|
|
2589
|
+
previousParagraph = false;
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
|
|
2593
|
+
nextCursor += 1;
|
|
2594
|
+
previousParagraph = false;
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
return nextCursor;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
function collectProtectionRangesFromInlines(
|
|
2601
|
+
nodes: readonly ParsedInlineNode[],
|
|
2602
|
+
ranges: ProtectionRange[],
|
|
2603
|
+
openRanges: Map<string, Omit<ProtectionRange, "end">>,
|
|
2604
|
+
cursor: number,
|
|
2605
|
+
): number {
|
|
2606
|
+
let nextCursor = cursor;
|
|
2607
|
+
|
|
2608
|
+
for (const node of nodes) {
|
|
2609
|
+
if (node.type === "perm_start") {
|
|
2610
|
+
openRanges.set(node.rangeId, {
|
|
2611
|
+
rangeId: node.rangeId,
|
|
2612
|
+
start: nextCursor,
|
|
2613
|
+
...(node.editorGroup ? { editorGroup: node.editorGroup } : {}),
|
|
2614
|
+
...(node.editor ? { editor: node.editor } : {}),
|
|
2615
|
+
enforced: false,
|
|
2616
|
+
enforcementReason:
|
|
2617
|
+
"preserve-only: runtime does not yet enforce permission range boundaries",
|
|
2618
|
+
});
|
|
2619
|
+
continue;
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2622
|
+
if (node.type === "perm_end") {
|
|
2623
|
+
const openRange = openRanges.get(node.rangeId);
|
|
2624
|
+
if (openRange) {
|
|
2625
|
+
ranges.push({
|
|
2626
|
+
...openRange,
|
|
2627
|
+
end: nextCursor,
|
|
2628
|
+
});
|
|
2629
|
+
openRanges.delete(node.rangeId);
|
|
2630
|
+
}
|
|
2631
|
+
continue;
|
|
2632
|
+
}
|
|
2633
|
+
|
|
2634
|
+
nextCursor += measureParsedInlineNode(node);
|
|
2635
|
+
}
|
|
2636
|
+
|
|
2637
|
+
return nextCursor;
|
|
2638
|
+
}
|
|
2639
|
+
|
|
2640
|
+
function measureParsedInlineNode(node: ParsedInlineNode): number {
|
|
2641
|
+
switch (node.type) {
|
|
2642
|
+
case "text":
|
|
2643
|
+
return node.text.length;
|
|
2644
|
+
case "tab":
|
|
2645
|
+
case "hard_break":
|
|
2646
|
+
case "column_break":
|
|
2647
|
+
case "footnote_ref":
|
|
2648
|
+
case "image":
|
|
2649
|
+
case "bookmark_start":
|
|
2650
|
+
case "bookmark_end":
|
|
2651
|
+
return 1;
|
|
2652
|
+
case "hyperlink":
|
|
2653
|
+
return node.children.reduce((size, child) => size + measureParsedInlineNode(child), 0);
|
|
2654
|
+
case "field": {
|
|
2655
|
+
const content = parseMainDocumentXml(
|
|
2656
|
+
`<?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>`,
|
|
2657
|
+
);
|
|
2658
|
+
if (content.blocks[0]?.type === "paragraph") {
|
|
2659
|
+
return content.blocks[0].children.reduce(
|
|
2660
|
+
(size, child) => size + measureParsedInlineNode(child),
|
|
2661
|
+
0,
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
return 1;
|
|
2665
|
+
}
|
|
2666
|
+
default:
|
|
2667
|
+
return 1;
|
|
2668
|
+
}
|
|
2669
|
+
}
|
|
2670
|
+
|
|
2671
|
+
function buildProtectionSnapshot(
|
|
2672
|
+
documentProtection: DocumentProtectionMeta,
|
|
2673
|
+
ranges: ProtectionRange[],
|
|
2674
|
+
): ProtectionSnapshot {
|
|
2675
|
+
const hasDocumentProtection =
|
|
2676
|
+
documentProtection.editType !== undefined || documentProtection.enforcement;
|
|
2677
|
+
const enforceRanges =
|
|
2678
|
+
documentProtection.editType === "readOnly" || documentProtection.editType === "comments";
|
|
2679
|
+
const normalizedRanges = ranges.map((range) => {
|
|
2680
|
+
const canEnforce =
|
|
2681
|
+
hasDocumentProtection &&
|
|
2682
|
+
documentProtection.enforcement &&
|
|
2683
|
+
enforceRanges &&
|
|
2684
|
+
typeof range.start === "number" &&
|
|
2685
|
+
typeof range.end === "number" &&
|
|
2686
|
+
range.end >= range.start;
|
|
2687
|
+
return {
|
|
2688
|
+
...range,
|
|
2689
|
+
enforced: canEnforce,
|
|
2690
|
+
enforcementReason: canEnforce
|
|
2691
|
+
? "runtime-enforced: permission range is mapped to canonical positions"
|
|
2692
|
+
: "preserve-only: runtime does not yet enforce permission range boundaries",
|
|
2693
|
+
};
|
|
2694
|
+
});
|
|
2695
|
+
return {
|
|
2696
|
+
hasDocumentProtection,
|
|
2697
|
+
editType: documentProtection.editType,
|
|
2698
|
+
enforcementActive: documentProtection.enforcement,
|
|
2699
|
+
ranges: normalizedRanges,
|
|
2700
|
+
enforcedRangeCount: normalizedRanges.filter((r) => r.enforced).length,
|
|
2701
|
+
preservedRangeCount: normalizedRanges.filter((r) => !r.enforced).length,
|
|
2702
|
+
};
|
|
2703
|
+
}
|