@beyondwork/docx-react-component 1.0.19 → 1.0.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +44 -25
- package/src/api/public-types.ts +336 -0
- package/src/api/session-state.ts +2 -0
- package/src/core/commands/formatting-commands.ts +1 -1
- package/src/core/commands/index.ts +14 -2
- package/src/core/search/search-text.ts +28 -0
- package/src/core/state/editor-state.ts +3 -0
- package/src/index.ts +21 -0
- package/src/io/docx-session.ts +363 -17
- 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 +83 -3
- package/src/io/export/split-review-boundaries.ts +181 -19
- package/src/io/normalize/normalize-text.ts +82 -8
- package/src/io/ooxml/highlight-colors.ts +39 -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 +240 -2
- package/src/io/ooxml/parse-headers-footers.ts +431 -7
- package/src/io/ooxml/parse-inline-media.ts +15 -1
- package/src/io/ooxml/parse-main-document.ts +396 -14
- package/src/io/ooxml/parse-revisions.ts +317 -38
- package/src/legal/bookmarks.ts +44 -0
- package/src/legal/cross-references.ts +59 -1
- package/src/model/canonical-document.ts +117 -1
- package/src/model/snapshot.ts +85 -1
- package/src/review/store/revision-store.ts +6 -0
- package/src/review/store/revision-types.ts +1 -0
- package/src/runtime/document-navigation.ts +52 -13
- package/src/runtime/document-runtime.ts +1521 -75
- package/src/runtime/read-only-diagnostics-runtime.ts +8 -0
- package/src/runtime/session-capabilities.ts +33 -3
- package/src/runtime/surface-projection.ts +86 -25
- package/src/runtime/table-schema.ts +2 -2
- package/src/runtime/view-state.ts +24 -6
- package/src/runtime/workflow-markup.ts +349 -0
- package/src/ui/WordReviewEditor.tsx +915 -1314
- package/src/ui/editor-command-bag.ts +120 -0
- package/src/ui/editor-runtime-boundary.ts +1448 -0
- package/src/ui/editor-shell-view.tsx +134 -0
- package/src/ui/editor-surface-controller.tsx +55 -0
- package/src/ui/headless/revision-decoration-model.ts +4 -4
- package/src/ui/runtime-snapshot-selectors.ts +197 -0
- package/src/ui/workflow-surface-blocked-rails.ts +94 -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-selection-toolbar.tsx +27 -2
- package/src/ui-tailwind/chrome/tw-table-context-toolbar.tsx +128 -0
- package/src/ui-tailwind/editor-surface/perf-probe.ts +86 -14
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +2 -2
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +237 -0
- package/src/ui-tailwind/editor-surface/pm-position-map.ts +1 -1
- package/src/ui-tailwind/editor-surface/pm-schema.ts +139 -8
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +98 -48
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +55 -0
- package/src/ui-tailwind/editor-surface/tw-opaque-block.tsx +7 -1
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +190 -48
- package/src/ui-tailwind/page-chrome-model.ts +27 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +7 -7
- package/src/ui-tailwind/review/tw-health-panel.tsx +31 -2
- package/src/ui-tailwind/review/tw-review-rail.tsx +3 -3
- package/src/ui-tailwind/review/tw-revision-sidebar.tsx +15 -15
- package/src/ui-tailwind/theme/editor-theme.css +130 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +543 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +316 -19
- package/src/validation/compatibility-engine.ts +27 -4
- package/src/validation/compatibility-report.ts +1 -0
- package/src/validation/docx-comment-proof.ts +220 -0
package/src/io/docx-session.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type {
|
|
|
7
7
|
ExportDocxOptions,
|
|
8
8
|
ExportResult,
|
|
9
9
|
PersistedEditorSnapshot,
|
|
10
|
+
ProtectionRange,
|
|
11
|
+
ProtectionSnapshot,
|
|
10
12
|
} from "../api/public-types.ts";
|
|
11
13
|
import { editorSessionStateFromPersistedSnapshot } from "../api/session-state.ts";
|
|
12
14
|
import type {
|
|
@@ -25,7 +27,12 @@ import {
|
|
|
25
27
|
} from "../core/selection/mapping.ts";
|
|
26
28
|
import { DOCX_MIME_TYPE } from "./opc/docx-package.ts";
|
|
27
29
|
import { readOpcPackage, type OpcPackage } from "./opc/package-reader.ts";
|
|
28
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
parseMainDocumentXml,
|
|
32
|
+
type ParsedBlockNode,
|
|
33
|
+
type ParsedInlineNode,
|
|
34
|
+
type ParsedPermStartInlineNode,
|
|
35
|
+
} from "./ooxml/parse-main-document.ts";
|
|
29
36
|
import { normalizeParsedTextDocument } from "./normalize/normalize-text.ts";
|
|
30
37
|
import {
|
|
31
38
|
CONTENT_TYPES_PATH,
|
|
@@ -47,6 +54,7 @@ import { parseCommentsFromOoxml } from "./ooxml/parse-comments.ts";
|
|
|
47
54
|
import { parseNumberingXml } from "./ooxml/parse-numbering.ts";
|
|
48
55
|
import {
|
|
49
56
|
createCommentExportIdMap,
|
|
57
|
+
mapParagraphBoundaries,
|
|
50
58
|
serializeCommentAnchorsIntoDocumentXml,
|
|
51
59
|
serializeMergedCommentsXml,
|
|
52
60
|
} from "./export/serialize-comments.ts";
|
|
@@ -181,6 +189,7 @@ export interface LoadedDocxEditorSession {
|
|
|
181
189
|
initialSnapshot: PersistedEditorSnapshot;
|
|
182
190
|
fatalError?: EditorError;
|
|
183
191
|
readOnly: boolean;
|
|
192
|
+
protectionSnapshot: ProtectionSnapshot;
|
|
184
193
|
exportDocx: (
|
|
185
194
|
sessionState: EditorSessionState | PersistedEditorSnapshot,
|
|
186
195
|
options?: ExportDocxOptions,
|
|
@@ -208,6 +217,7 @@ interface ImportedDocxState {
|
|
|
208
217
|
sourcePeopleRelationshipId?: string;
|
|
209
218
|
sourcePeopleRootTag?: string;
|
|
210
219
|
sourcePeopleAuthors: readonly string[];
|
|
220
|
+
protectionSnapshot: ProtectionSnapshot;
|
|
211
221
|
preservedCommentDefinitions: readonly ImportedCommentDefinition[];
|
|
212
222
|
blockingCommentDiagnostics: readonly CommentImportDiagnostic[];
|
|
213
223
|
initialCanonicalSignature: string;
|
|
@@ -326,6 +336,7 @@ export function loadDocxEditorSession(
|
|
|
326
336
|
mediaParts,
|
|
327
337
|
mainDocumentPath,
|
|
328
338
|
);
|
|
339
|
+
const protectionRanges = extractProtectionRanges(parsedDocument.blocks);
|
|
329
340
|
const normalizedDocument = normalizeParsedTextDocument(
|
|
330
341
|
parsedDocument,
|
|
331
342
|
mainDocumentPath,
|
|
@@ -398,13 +409,14 @@ export function loadDocxEditorSession(
|
|
|
398
409
|
const parsedFooters: FooterDocument[] = [];
|
|
399
410
|
const sourceHeaderPaths: Array<{ partPath: string; relationshipId: string }> = [];
|
|
400
411
|
const sourceFooterPaths: Array<{ partPath: string; relationshipId: string }> = [];
|
|
401
|
-
const
|
|
412
|
+
const seenSubPartKeys = new Set<string>();
|
|
402
413
|
|
|
403
414
|
for (const ref of headerFooterRefs) {
|
|
404
|
-
|
|
415
|
+
const dedupeKey = `${ref.kind}:${ref.variant}:${ref.relationshipId}`;
|
|
416
|
+
if (seenSubPartKeys.has(dedupeKey)) {
|
|
405
417
|
continue;
|
|
406
418
|
}
|
|
407
|
-
|
|
419
|
+
seenSubPartKeys.add(dedupeKey);
|
|
408
420
|
|
|
409
421
|
const relationship = documentPart.relationships.find(
|
|
410
422
|
(r) => r.id === ref.relationshipId && r.targetMode === "internal",
|
|
@@ -504,6 +516,12 @@ export function loadDocxEditorSession(
|
|
|
504
516
|
decodeUtf8(sourcePackage.parts.get(settingsPartPath)?.bytes ?? new Uint8Array()),
|
|
505
517
|
)
|
|
506
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);
|
|
507
525
|
|
|
508
526
|
// ---- Parse styles.xml for canonical style catalog ----
|
|
509
527
|
const stylesPartPath = resolveDocumentRelatedPartPath(
|
|
@@ -598,6 +616,7 @@ export function loadDocxEditorSession(
|
|
|
598
616
|
timestamp,
|
|
599
617
|
document,
|
|
600
618
|
compatibility: toPublicCompatibilityReport(compatibility),
|
|
619
|
+
protectionSnapshot: importedProtectionSnapshot,
|
|
601
620
|
sourcePackage: createPersistedSourcePackage(sourceBytes, options.sourceLabel),
|
|
602
621
|
});
|
|
603
622
|
const snapshotIssues = validatePersistedEditorSnapshot(snapshot);
|
|
@@ -657,6 +676,7 @@ export function loadDocxEditorSession(
|
|
|
657
676
|
)?.id,
|
|
658
677
|
sourcePeopleRootTag: normalizedComments.sourcePeopleRootTag,
|
|
659
678
|
sourcePeopleAuthors: normalizedComments.peopleAuthors,
|
|
679
|
+
protectionSnapshot: buildProtectionSnapshot(documentProtection, protectionRanges),
|
|
660
680
|
preservedCommentDefinitions: normalizedComments.preservedDefinitions,
|
|
661
681
|
blockingCommentDiagnostics: normalizedComments.diagnostics.filter((diagnostic) =>
|
|
662
682
|
BLOCKING_COMMENT_DIAGNOSTIC_CODES.has(diagnostic.code),
|
|
@@ -678,6 +698,7 @@ export function loadDocxEditorSession(
|
|
|
678
698
|
initialSessionState,
|
|
679
699
|
initialSnapshot: snapshot,
|
|
680
700
|
readOnly: false,
|
|
701
|
+
protectionSnapshot: importedProtectionSnapshot,
|
|
681
702
|
exportDocx: async (nextSessionState, exportOptions) =>
|
|
682
703
|
exportDocxEditorSession(importedState, nextSessionState, exportOptions),
|
|
683
704
|
};
|
|
@@ -701,11 +722,12 @@ function exportDocxEditorSession(
|
|
|
701
722
|
}
|
|
702
723
|
|
|
703
724
|
const currentDocument = sessionState.canonicalDocument as CanonicalDocumentEnvelope;
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
725
|
+
const signatureMatch = serializeCanonicalDocumentForExport(currentDocument) ===
|
|
726
|
+
state.initialCanonicalSignature;
|
|
727
|
+
const canReuse = canReuseSourceBytesForCurrentDocument(state, currentDocument);
|
|
728
|
+
const commentCount = Object.keys(currentDocument.review?.comments ?? {}).length;
|
|
729
|
+
|
|
730
|
+
if (signatureMatch && canReuse) {
|
|
709
731
|
return {
|
|
710
732
|
bytes: new Uint8Array(state.sourceBytes),
|
|
711
733
|
mimeType: DOCX_MIME_TYPE,
|
|
@@ -715,9 +737,16 @@ function exportDocxEditorSession(
|
|
|
715
737
|
},
|
|
716
738
|
};
|
|
717
739
|
}
|
|
718
|
-
|
|
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) {
|
|
719
748
|
throw new Error(
|
|
720
|
-
`DOCX export is blocked because ${
|
|
749
|
+
`DOCX export is blocked because ${blockingCommentCount} preserve-only comment anchors cannot be safely remapped after runtime edits.`,
|
|
721
750
|
);
|
|
722
751
|
}
|
|
723
752
|
const currentRevisions = toReviewRevisionRecords(currentDocument.review.revisions);
|
|
@@ -727,9 +756,6 @@ function exportDocxEditorSession(
|
|
|
727
756
|
const commentThreads = Object.values(
|
|
728
757
|
createCommentStoreFromRuntimeComments(currentDocument.review.comments).threads,
|
|
729
758
|
);
|
|
730
|
-
const preservedCommentIds = new Set(
|
|
731
|
-
state.preservedCommentDefinitions.map((definition) => definition.commentId),
|
|
732
|
-
);
|
|
733
759
|
const ownedCommentThreads = commentThreads.filter(
|
|
734
760
|
(thread) => !preservedCommentIds.has(thread.commentId),
|
|
735
761
|
);
|
|
@@ -783,6 +809,10 @@ function exportDocxEditorSession(
|
|
|
783
809
|
exportCommentIds,
|
|
784
810
|
},
|
|
785
811
|
);
|
|
812
|
+
const protectedDocumentXml = serializeProtectionRangesIntoDocumentXml(
|
|
813
|
+
annotatedDocument.documentXml,
|
|
814
|
+
state.protectionSnapshot,
|
|
815
|
+
);
|
|
786
816
|
const blockingSkippedCommentIds = annotatedDocument.skippedCommentIds.filter((commentId) => {
|
|
787
817
|
const thread = ownedCommentThreads.find((candidate) => candidate.commentId === commentId);
|
|
788
818
|
return !thread || thread.anchor.kind !== "detached";
|
|
@@ -889,7 +919,7 @@ function exportDocxEditorSession(
|
|
|
889
919
|
|
|
890
920
|
exportSession.replaceOwnedPart({
|
|
891
921
|
path: state.sourceDocumentPartPath,
|
|
892
|
-
bytes: new TextEncoder().encode(
|
|
922
|
+
bytes: new TextEncoder().encode(protectedDocumentXml),
|
|
893
923
|
contentType: MAIN_DOCUMENT_CONTENT_TYPE,
|
|
894
924
|
relationships: nextRelationships,
|
|
895
925
|
});
|
|
@@ -1246,6 +1276,7 @@ function createImportedSnapshot(input: {
|
|
|
1246
1276
|
timestamp: string;
|
|
1247
1277
|
document: CanonicalDocumentEnvelope;
|
|
1248
1278
|
compatibility: PersistedEditorSnapshot["compatibility"];
|
|
1279
|
+
protectionSnapshot: ProtectionSnapshot;
|
|
1249
1280
|
sourcePackage?: PersistedEditorSnapshot["sourcePackage"];
|
|
1250
1281
|
}): PersistedEditorSnapshot {
|
|
1251
1282
|
return {
|
|
@@ -1260,6 +1291,7 @@ function createImportedSnapshot(input: {
|
|
|
1260
1291
|
canonicalDocument: input.document,
|
|
1261
1292
|
compatibility: input.compatibility,
|
|
1262
1293
|
warningLog: input.compatibility.warnings,
|
|
1294
|
+
protectionSnapshot: input.protectionSnapshot,
|
|
1263
1295
|
sourcePackage: input.sourcePackage,
|
|
1264
1296
|
};
|
|
1265
1297
|
}
|
|
@@ -1351,6 +1383,7 @@ function createDiagnosticsSession(
|
|
|
1351
1383
|
initialSnapshot,
|
|
1352
1384
|
fatalError: diagnostics.fatalError,
|
|
1353
1385
|
readOnly: true,
|
|
1386
|
+
protectionSnapshot: EMPTY_PROTECTION_SNAPSHOT,
|
|
1354
1387
|
exportDocx: async (_sessionState, exportOptions) => runtime.exportDocx(exportOptions),
|
|
1355
1388
|
};
|
|
1356
1389
|
}
|
|
@@ -1466,8 +1499,10 @@ function normalizeImportedCommentThreads(
|
|
|
1466
1499
|
commentId: thread.commentId,
|
|
1467
1500
|
code: "opaque_anchor_preserve_only",
|
|
1468
1501
|
message:
|
|
1469
|
-
"Comment anchor intersects preserve-only OOXML
|
|
1502
|
+
"Comment anchor intersects preserve-only OOXML content. Thread is visible but detached; anchor cannot be safely remapped.",
|
|
1470
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.",
|
|
1471
1506
|
});
|
|
1472
1507
|
return {
|
|
1473
1508
|
...thread,
|
|
@@ -1485,8 +1520,10 @@ function normalizeImportedCommentThreads(
|
|
|
1485
1520
|
commentId: thread.commentId,
|
|
1486
1521
|
code: "preserve_only_revision_overlap",
|
|
1487
1522
|
message:
|
|
1488
|
-
"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.",
|
|
1489
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.",
|
|
1490
1527
|
});
|
|
1491
1528
|
return {
|
|
1492
1529
|
...thread,
|
|
@@ -2347,6 +2384,108 @@ function xmlNode(tagName: string, value: string | undefined): string | undefined
|
|
|
2347
2384
|
return `<${tagName}>${escapeXml(value)}</${tagName.split(" ", 1)[0]}>`;
|
|
2348
2385
|
}
|
|
2349
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
|
+
|
|
2350
2489
|
function escapeXml(value: string): string {
|
|
2351
2490
|
return value
|
|
2352
2491
|
.replace(/&/g, "&")
|
|
@@ -2355,3 +2494,210 @@ function escapeXml(value: string): string {
|
|
|
2355
2494
|
.replace(/\"/g, """)
|
|
2356
2495
|
.replace(/'/g, "'");
|
|
2357
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
|
+
}
|
|
@@ -577,56 +577,126 @@ function parseOoxmlNumericId(value: string): number | undefined {
|
|
|
577
577
|
return Number.isFinite(numericId) ? numericId : undefined;
|
|
578
578
|
}
|
|
579
579
|
|
|
580
|
-
function mapParagraphBoundaries(documentXml: string): ParagraphBoundaryMap[] {
|
|
580
|
+
export function mapParagraphBoundaries(documentXml: string): ParagraphBoundaryMap[] {
|
|
581
581
|
const root = parseXml(documentXml);
|
|
582
582
|
const documentElement = findChildElement(root, "document");
|
|
583
583
|
const bodyElement = findChildElement(documentElement, "body");
|
|
584
584
|
const paragraphs: ParagraphBoundaryMap[] = [];
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
585
|
+
walkBlockNodesForParagraphBoundaries(
|
|
586
|
+
bodyElement.children,
|
|
587
|
+
documentXml,
|
|
588
|
+
paragraphs,
|
|
589
|
+
0,
|
|
590
|
+
-1,
|
|
591
|
+
true,
|
|
592
|
+
);
|
|
593
|
+
|
|
594
|
+
return paragraphs;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function walkBlockNodesForParagraphBoundaries(
|
|
598
|
+
nodes: readonly XmlNode[],
|
|
599
|
+
documentXml: string,
|
|
600
|
+
paragraphs: ParagraphBoundaryMap[],
|
|
601
|
+
globalCursor: number,
|
|
602
|
+
paragraphIndex: number,
|
|
603
|
+
useSurfaceParagraphSeparators: boolean,
|
|
604
|
+
): {
|
|
605
|
+
globalCursor: number;
|
|
606
|
+
paragraphIndex: number;
|
|
607
|
+
} {
|
|
608
|
+
let nextCursor = globalCursor;
|
|
609
|
+
let nextParagraphIndex = paragraphIndex;
|
|
610
|
+
let elementIndex = -1;
|
|
588
611
|
|
|
589
|
-
for (const
|
|
590
|
-
if (
|
|
612
|
+
for (const node of nodes) {
|
|
613
|
+
if (node.type !== "element") {
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
elementIndex += 1;
|
|
617
|
+
|
|
618
|
+
const name = localName(node.name);
|
|
619
|
+
if (name === "p") {
|
|
620
|
+
if (useSurfaceParagraphSeparators && elementIndex > 0) {
|
|
621
|
+
nextCursor += 1;
|
|
622
|
+
}
|
|
623
|
+
nextParagraphIndex += 1;
|
|
624
|
+
const boundaries = new Map<number, number>();
|
|
625
|
+
boundaries.set(nextCursor, node.start + openingTagLength(documentXml, node.start));
|
|
626
|
+
|
|
627
|
+
walkParagraphForBoundaries(
|
|
628
|
+
node,
|
|
629
|
+
documentXml,
|
|
630
|
+
boundaries,
|
|
631
|
+
() => nextCursor,
|
|
632
|
+
(next) => {
|
|
633
|
+
nextCursor = next;
|
|
634
|
+
},
|
|
635
|
+
);
|
|
636
|
+
|
|
637
|
+
if (!boundaries.has(nextCursor)) {
|
|
638
|
+
boundaries.set(nextCursor, node.end - 4);
|
|
639
|
+
}
|
|
640
|
+
paragraphs.push({
|
|
641
|
+
paragraphIndex: nextParagraphIndex,
|
|
642
|
+
start: Math.min(...boundaries.keys()),
|
|
643
|
+
end: Math.max(...boundaries.keys()),
|
|
644
|
+
boundaries,
|
|
645
|
+
});
|
|
591
646
|
continue;
|
|
592
647
|
}
|
|
593
648
|
|
|
594
|
-
if (
|
|
595
|
-
|
|
596
|
-
|
|
649
|
+
if (name === "tbl") {
|
|
650
|
+
for (const child of node.children) {
|
|
651
|
+
if (child.type !== "element" || localName(child.name) !== "tr") {
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
for (const rowChild of child.children) {
|
|
655
|
+
if (rowChild.type !== "element" || localName(rowChild.name) !== "tc") {
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
const result = walkBlockNodesForParagraphBoundaries(
|
|
659
|
+
rowChild.children,
|
|
660
|
+
documentXml,
|
|
661
|
+
paragraphs,
|
|
662
|
+
nextCursor,
|
|
663
|
+
nextParagraphIndex,
|
|
664
|
+
false,
|
|
665
|
+
);
|
|
666
|
+
nextCursor = result.globalCursor;
|
|
667
|
+
nextParagraphIndex = result.paragraphIndex;
|
|
668
|
+
}
|
|
669
|
+
}
|
|
597
670
|
continue;
|
|
598
671
|
}
|
|
599
672
|
|
|
600
|
-
if (
|
|
601
|
-
|
|
673
|
+
if (name === "sdt") {
|
|
674
|
+
const sdtContent = findChildElement(node, "sdtContent");
|
|
675
|
+
const result = walkBlockNodesForParagraphBoundaries(
|
|
676
|
+
sdtContent.children,
|
|
677
|
+
documentXml,
|
|
678
|
+
paragraphs,
|
|
679
|
+
nextCursor,
|
|
680
|
+
nextParagraphIndex,
|
|
681
|
+
false,
|
|
682
|
+
);
|
|
683
|
+
nextCursor = result.globalCursor;
|
|
684
|
+
nextParagraphIndex = result.paragraphIndex;
|
|
685
|
+
continue;
|
|
602
686
|
}
|
|
603
|
-
paragraphIndex += 1;
|
|
604
|
-
const boundaries = new Map<number, number>();
|
|
605
|
-
boundaries.set(globalCursor, child.start + openingTagLength(documentXml, child.start));
|
|
606
|
-
|
|
607
|
-
walkParagraphForBoundaries(
|
|
608
|
-
child,
|
|
609
|
-
documentXml,
|
|
610
|
-
boundaries,
|
|
611
|
-
() => globalCursor,
|
|
612
|
-
(next) => {
|
|
613
|
-
globalCursor = next;
|
|
614
|
-
},
|
|
615
|
-
);
|
|
616
687
|
|
|
617
|
-
if (
|
|
618
|
-
|
|
688
|
+
if (name === "customXml") {
|
|
689
|
+
nextCursor += 1;
|
|
690
|
+
continue;
|
|
619
691
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
start: Math.min(...boundaries.keys()),
|
|
623
|
-
end: Math.max(...boundaries.keys()),
|
|
624
|
-
boundaries,
|
|
625
|
-
});
|
|
626
|
-
previousWasParagraph = true;
|
|
692
|
+
|
|
693
|
+
nextCursor += 0;
|
|
627
694
|
}
|
|
628
695
|
|
|
629
|
-
return
|
|
696
|
+
return {
|
|
697
|
+
globalCursor: nextCursor,
|
|
698
|
+
paragraphIndex: nextParagraphIndex,
|
|
699
|
+
};
|
|
630
700
|
}
|
|
631
701
|
|
|
632
702
|
function walkParagraphForBoundaries(
|