@beyondwork/docx-react-component 1.0.73 → 1.0.75
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 +1 -1
- package/src/api/anchor-conversion.ts +2 -2
- package/src/api/public-types.ts +40 -6
- package/src/api/v3/_runtime-handle.ts +15 -0
- package/src/api/v3/runtime/workflow.ts +130 -1
- package/src/api/v3/ui/_types.ts +21 -0
- package/src/api/v3/ui/overlays.ts +276 -2
- package/src/api/v3/ui/scope.ts +113 -1
- package/src/compare/diff-engine.ts +1 -2
- package/src/core/commands/index.ts +14 -15
- package/src/core/selection/anchor-conversion.ts +2 -2
- package/src/core/selection/mapping.ts +10 -8
- package/src/core/selection/review-anchors.ts +3 -3
- package/src/io/export/export-session.ts +53 -0
- package/src/io/export/serialize-comments.ts +4 -4
- package/src/io/export/serialize-runtime-revisions.ts +10 -10
- package/src/io/export/split-review-boundaries.ts +4 -4
- package/src/io/export/split-story-blocks-for-runtime-revisions.ts +2 -2
- package/src/io/ooxml/parse-comments.ts +2 -2
- package/src/io/ooxml/parse-headers-footers.ts +7 -13
- package/src/io/ooxml/parse-main-document.ts +7 -31
- package/src/io/ooxml/table-opaque-preservation.ts +171 -0
- package/src/model/anchor.ts +9 -1
- package/src/model/canonical-document.ts +76 -3
- package/src/preservation/store.ts +24 -0
- package/src/review/store/comment-anchors.ts +1 -1
- package/src/review/store/comment-remapping.ts +1 -1
- package/src/review/store/revision-actions.ts +4 -4
- package/src/review/store/revision-types.ts +1 -1
- package/src/review/store/scope-tag-diff.ts +1 -1
- package/src/runtime/collab/map-local-selection-on-remote-replay.ts +7 -7
- package/src/runtime/document-runtime.ts +233 -38
- package/src/runtime/formatting/formatting-context.ts +1 -1
- package/src/runtime/layout/inert-layout-facet.ts +1 -0
- package/src/runtime/layout/layout-engine-version.ts +9 -1
- package/src/runtime/layout/public-facet.ts +27 -0
- package/src/runtime/scopes/evidence.ts +1 -1
- package/src/runtime/scopes/review-bundle.ts +1 -1
- package/src/runtime/scopes/scope-range.ts +1 -1
- package/src/runtime/selection/post-edit-validator.ts +4 -4
- package/src/runtime/surface-projection.ts +48 -4
- package/src/runtime/workflow/scope-writer.ts +212 -10
- package/src/session/import/review-import.ts +12 -12
- package/src/session/import/workflow-scope-import.ts +9 -8
- package/src/shell/session-bootstrap.ts +4 -0
- package/src/ui-tailwind/editor-surface/pm-schema.ts +22 -2
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +5 -2
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +22 -3
- package/src/ui-tailwind/page-stack/floating-image-overlay-model.ts +7 -3
- package/src/ui-tailwind/page-stack/tw-endnote-area.tsx +5 -1
- package/src/ui-tailwind/page-stack/tw-footnote-area.tsx +5 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +7 -5
- package/src/ui-tailwind/page-stack/use-visible-block-range.ts +99 -43
- package/src/ui-tailwind/review-workspace/use-page-markers.ts +48 -7
- package/src/ui-tailwind/review-workspace/use-workspace-side-effects.ts +0 -13
- package/src/ui-tailwind/tw-review-workspace.tsx +13 -41
- package/src/validation/compatibility-engine.ts +1 -1
- package/src/ui-tailwind/chrome/tw-layout-panel.tsx +0 -114
- package/src/ui-tailwind/review-workspace/tw-review-workspace-page-toolbar.tsx +0 -240
package/src/api/v3/ui/scope.ts
CHANGED
|
@@ -33,9 +33,92 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
36
|
-
import type {
|
|
36
|
+
import type {
|
|
37
|
+
ScopeBundle,
|
|
38
|
+
ScopeCardModel,
|
|
39
|
+
ScopeRailSnapshot,
|
|
40
|
+
SemanticScope,
|
|
41
|
+
SemanticScopeKind,
|
|
42
|
+
} from "../../public-types.ts";
|
|
37
43
|
import type { UiApiContext } from "./_context.ts";
|
|
38
44
|
|
|
45
|
+
export interface UiScopeListFilter {
|
|
46
|
+
readonly kind?: SemanticScopeKind;
|
|
47
|
+
readonly limit?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UiScopeRailOptions {
|
|
51
|
+
/**
|
|
52
|
+
* Narrow the snapshot to a single page index. Omit (default) to
|
|
53
|
+
* span every page the runtime's page graph knows about.
|
|
54
|
+
*/
|
|
55
|
+
readonly pageIndex?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const cardMetadata: ApiV3FnMetadata = {
|
|
59
|
+
name: "ui.scope.card",
|
|
60
|
+
status: "live-with-adapter",
|
|
61
|
+
sourceLayer: "presentation",
|
|
62
|
+
liveEvidence: {
|
|
63
|
+
runnerTest: "test/api/v3/ui/scope-card-rail.test.ts",
|
|
64
|
+
commit: "refactor-10-ki-008-ui-scope-card-rail",
|
|
65
|
+
},
|
|
66
|
+
uxIntent: { uiVisible: false },
|
|
67
|
+
agentMetadata: {
|
|
68
|
+
readOrMutate: "read",
|
|
69
|
+
boundedScope: "scope",
|
|
70
|
+
auditCategory: "ui-scope-card-read",
|
|
71
|
+
},
|
|
72
|
+
// Same classification as other `ui.scope.*` reads — canonical scope
|
|
73
|
+
// projection (not class-C local preference). KI-008 close.
|
|
74
|
+
stateClass: "A-canonical",
|
|
75
|
+
persistsTo: "canonical",
|
|
76
|
+
rwdReference:
|
|
77
|
+
"§UI API § ui.scope.card. Wraps handle.compileScopeCardById(scopeId) which projects runtime.workflow.getAllScopeCardModels() filtered to the matching scope. Returns plain-value ScopeCardModel — no React handlers, no DOM refs. Hosts wire event handlers in their own chrome tree.",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export const railMetadata: ApiV3FnMetadata = {
|
|
81
|
+
name: "ui.scope.rail",
|
|
82
|
+
status: "live-with-adapter",
|
|
83
|
+
sourceLayer: "presentation",
|
|
84
|
+
liveEvidence: {
|
|
85
|
+
runnerTest: "test/api/v3/ui/scope-card-rail.test.ts",
|
|
86
|
+
commit: "refactor-10-ki-008-ui-scope-card-rail",
|
|
87
|
+
},
|
|
88
|
+
uxIntent: { uiVisible: false },
|
|
89
|
+
agentMetadata: {
|
|
90
|
+
readOrMutate: "read",
|
|
91
|
+
boundedScope: "document",
|
|
92
|
+
auditCategory: "ui-scope-rail-read",
|
|
93
|
+
},
|
|
94
|
+
stateClass: "A-canonical",
|
|
95
|
+
persistsTo: "canonical",
|
|
96
|
+
rwdReference:
|
|
97
|
+
"§UI API § ui.scope.rail. Wraps handle.compileScopeRailSnapshot({pageIndex?}) which projects runtime.workflow.getRailSegments(pageIndex) / getAllRailSegments() into a ScopeRailSnapshot envelope. Plain-value data; no React handlers.",
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const listMetadata: ApiV3FnMetadata = {
|
|
101
|
+
name: "ui.scope.list",
|
|
102
|
+
status: "live-with-adapter",
|
|
103
|
+
sourceLayer: "presentation",
|
|
104
|
+
liveEvidence: {
|
|
105
|
+
runnerTest: "test/api/v3/ui/scope-list.test.ts",
|
|
106
|
+
commit: "refactor-10-ki-008-ui-scope-list",
|
|
107
|
+
},
|
|
108
|
+
uxIntent: { uiVisible: false },
|
|
109
|
+
agentMetadata: {
|
|
110
|
+
readOrMutate: "read",
|
|
111
|
+
boundedScope: "document",
|
|
112
|
+
auditCategory: "ui-scope-list",
|
|
113
|
+
},
|
|
114
|
+
// Same classification as `ui.scope.getBundle` — reads canonical
|
|
115
|
+
// scope state (not class-C local preference). KI-008 closure.
|
|
116
|
+
stateClass: "A-canonical",
|
|
117
|
+
persistsTo: "canonical",
|
|
118
|
+
rwdReference:
|
|
119
|
+
"§UI API § ui.scope.list. Wraps handle.compileScopeList(filter?) to enumerate compiled SemanticScope records through the L10 seam without importing src/runtime/scopes/**. Filter matches ai.listScopes({kind?, limit?}) for identity-set symmetry.",
|
|
120
|
+
};
|
|
121
|
+
|
|
39
122
|
export const getBundleMetadata: ApiV3FnMetadata = {
|
|
40
123
|
name: "ui.scope.getBundle",
|
|
41
124
|
status: "live-with-adapter",
|
|
@@ -67,5 +150,34 @@ export function createScopeFamily(ctx: UiApiContext) {
|
|
|
67
150
|
const nowUtc = new Date().toISOString();
|
|
68
151
|
return compile.call(ctx.handle, scopeId, nowUtc);
|
|
69
152
|
},
|
|
153
|
+
list(filter?: UiScopeListFilter): readonly SemanticScope[] {
|
|
154
|
+
// KI-008 — enumeration through the handle's batch primitive
|
|
155
|
+
// (L10 purity blocks importing src/runtime/scopes/**). Returns
|
|
156
|
+
// a fresh array so callers that sort / splice don't leak into
|
|
157
|
+
// internal compiler state.
|
|
158
|
+
const enumerate = ctx.handle.compileScopeList;
|
|
159
|
+
if (typeof enumerate !== "function") return Object.freeze([]);
|
|
160
|
+
return enumerate.call(ctx.handle, filter);
|
|
161
|
+
},
|
|
162
|
+
card(scopeId: string): ScopeCardModel | null {
|
|
163
|
+
// KI-008 close — single-scope presentation projection. Returns
|
|
164
|
+
// a plain-value ScopeCardModel (posture, label, issue, suggestion
|
|
165
|
+
// groups, review actions, agent-pending flag). Host chrome wires
|
|
166
|
+
// event handlers around it.
|
|
167
|
+
const compile = ctx.handle.compileScopeCardById;
|
|
168
|
+
if (typeof compile !== "function") return null;
|
|
169
|
+
return compile.call(ctx.handle, scopeId);
|
|
170
|
+
},
|
|
171
|
+
rail(options?: UiScopeRailOptions): ScopeRailSnapshot {
|
|
172
|
+
// KI-008 close — rail snapshot. When `options.pageIndex` is
|
|
173
|
+
// supplied, narrows to that page; otherwise spans every page
|
|
174
|
+
// in the runtime's page graph. Envelope shape is stable either
|
|
175
|
+
// way; callers check `snapshot.pageIndex` to detect narrowing.
|
|
176
|
+
const compile = ctx.handle.compileScopeRailSnapshot;
|
|
177
|
+
if (typeof compile !== "function") {
|
|
178
|
+
return { segments: Object.freeze([]) };
|
|
179
|
+
}
|
|
180
|
+
return compile.call(ctx.handle, options);
|
|
181
|
+
},
|
|
70
182
|
};
|
|
71
183
|
}
|
|
@@ -410,8 +410,7 @@ function createParagraphRevisionRecords(
|
|
|
410
410
|
changeId: revision.changeId,
|
|
411
411
|
kind: revision.kind,
|
|
412
412
|
anchor: {
|
|
413
|
-
kind: "range",
|
|
414
|
-
range: { from: position, to: position },
|
|
413
|
+
kind: "range", from: position, to: position,
|
|
415
414
|
assoc: { start: -1, end: 1 },
|
|
416
415
|
},
|
|
417
416
|
authorId,
|
|
@@ -1525,9 +1525,9 @@ export function remapSelection(
|
|
|
1525
1525
|
|
|
1526
1526
|
if (activeRange.kind === "range") {
|
|
1527
1527
|
return {
|
|
1528
|
-
anchor: activeRange.
|
|
1529
|
-
head: activeRange.
|
|
1530
|
-
isCollapsed: activeRange.
|
|
1528
|
+
anchor: activeRange.from,
|
|
1529
|
+
head: activeRange.to,
|
|
1530
|
+
isCollapsed: activeRange.from === activeRange.to,
|
|
1531
1531
|
activeRange,
|
|
1532
1532
|
};
|
|
1533
1533
|
}
|
|
@@ -1664,8 +1664,8 @@ function normalizeSelection(selection: SelectionSnapshot): SelectionSnapshot {
|
|
|
1664
1664
|
);
|
|
1665
1665
|
|
|
1666
1666
|
if (activeRange.kind === "range") {
|
|
1667
|
-
const rangeFrom = activeRange.
|
|
1668
|
-
const rangeTo = activeRange.
|
|
1667
|
+
const rangeFrom = activeRange.from;
|
|
1668
|
+
const rangeTo = activeRange.to;
|
|
1669
1669
|
const collapsed = rangeFrom === rangeTo;
|
|
1670
1670
|
const anchorWithinRange = selection.anchor >= rangeFrom && selection.anchor <= rangeTo;
|
|
1671
1671
|
const headWithinRange = selection.head >= rangeFrom && selection.head <= rangeTo;
|
|
@@ -2256,7 +2256,8 @@ function createAuthoredRevision(
|
|
|
2256
2256
|
kind,
|
|
2257
2257
|
anchor: {
|
|
2258
2258
|
kind: "range",
|
|
2259
|
-
|
|
2259
|
+
from,
|
|
2260
|
+
to,
|
|
2260
2261
|
assoc: { start: 1, end: -1 },
|
|
2261
2262
|
},
|
|
2262
2263
|
authorId,
|
|
@@ -2396,8 +2397,7 @@ function applySuggestingInsert(
|
|
|
2396
2397
|
const updatedRevision: CanonicalRevisionRecord = {
|
|
2397
2398
|
...existingInsertion,
|
|
2398
2399
|
anchor: {
|
|
2399
|
-
kind: "range",
|
|
2400
|
-
range: { from: existingInsertion.anchor.range.from, to: insertedTo },
|
|
2400
|
+
kind: "range", from: existingInsertion.anchor.from, to: insertedTo,
|
|
2401
2401
|
assoc: { start: 1, end: -1 },
|
|
2402
2402
|
},
|
|
2403
2403
|
};
|
|
@@ -2635,19 +2635,18 @@ function applySuggestingDelete(
|
|
|
2635
2635
|
if (existingDeletion) {
|
|
2636
2636
|
// Extend the existing deletion revision to cover the new range
|
|
2637
2637
|
const extendedFrom = Math.min(
|
|
2638
|
-
existingDeletion.anchor.kind === "range" ? existingDeletion.anchor.
|
|
2638
|
+
existingDeletion.anchor.kind === "range" ? existingDeletion.anchor.from : deleteFrom,
|
|
2639
2639
|
deleteFrom,
|
|
2640
2640
|
);
|
|
2641
2641
|
const extendedTo = Math.max(
|
|
2642
|
-
existingDeletion.anchor.kind === "range" ? existingDeletion.anchor.
|
|
2642
|
+
existingDeletion.anchor.kind === "range" ? existingDeletion.anchor.to : deleteTo,
|
|
2643
2643
|
deleteTo,
|
|
2644
2644
|
);
|
|
2645
2645
|
|
|
2646
2646
|
const updatedRevision: CanonicalRevisionRecord = {
|
|
2647
2647
|
...existingDeletion,
|
|
2648
2648
|
anchor: {
|
|
2649
|
-
kind: "range",
|
|
2650
|
-
range: { from: extendedFrom, to: extendedTo },
|
|
2649
|
+
kind: "range", from: extendedFrom, to: extendedTo,
|
|
2651
2650
|
assoc: { start: 1, end: -1 },
|
|
2652
2651
|
},
|
|
2653
2652
|
};
|
|
@@ -2933,7 +2932,7 @@ function findAdjacentInsertionRevision(
|
|
|
2933
2932
|
revision.status === "open" &&
|
|
2934
2933
|
revision.metadata?.source === "runtime" &&
|
|
2935
2934
|
revision.anchor.kind === "range" &&
|
|
2936
|
-
revision.anchor.
|
|
2935
|
+
revision.anchor.to === cursorPos &&
|
|
2937
2936
|
revision.authorId === authorId
|
|
2938
2937
|
) {
|
|
2939
2938
|
return revision;
|
|
@@ -2954,8 +2953,8 @@ function findOverlappingAuthoredDeletion(
|
|
|
2954
2953
|
revision.metadata?.source === "runtime" &&
|
|
2955
2954
|
revision.anchor.kind === "range"
|
|
2956
2955
|
) {
|
|
2957
|
-
const revFrom = revision.anchor.
|
|
2958
|
-
const revTo = revision.anchor.
|
|
2956
|
+
const revFrom = revision.anchor.from;
|
|
2957
|
+
const revTo = revision.anchor.to;
|
|
2959
2958
|
// Adjacent or overlapping: the deletion is directly next to or overlapping the range
|
|
2960
2959
|
if (
|
|
2961
2960
|
(from >= revFrom && from <= revTo) ||
|
|
@@ -66,9 +66,11 @@ export function createRangeAnchor(
|
|
|
66
66
|
to = from,
|
|
67
67
|
assoc: BoundaryAssoc = DEFAULT_BOUNDARY_ASSOC,
|
|
68
68
|
): RangeAnchor {
|
|
69
|
+
const normalized = normalizeRange({ from, to });
|
|
69
70
|
return {
|
|
70
71
|
kind: "range",
|
|
71
|
-
|
|
72
|
+
from: normalized.from,
|
|
73
|
+
to: normalized.to,
|
|
72
74
|
assoc,
|
|
73
75
|
};
|
|
74
76
|
}
|
|
@@ -89,7 +91,7 @@ export function createEmptyMapping(): TransactionMapping {
|
|
|
89
91
|
|
|
90
92
|
export function getEffectiveRange(anchor: EditorAnchorProjection): DocRange {
|
|
91
93
|
if (anchor.kind === "range") {
|
|
92
|
-
return anchor.
|
|
94
|
+
return { from: anchor.from, to: anchor.to };
|
|
93
95
|
}
|
|
94
96
|
if (anchor.kind === "node") {
|
|
95
97
|
return { from: anchor.at, to: anchor.at };
|
|
@@ -140,13 +142,13 @@ export function mapAnchor(
|
|
|
140
142
|
return createNodeAnchor(mapped.position, anchor.assoc);
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
const from = mapPosition(anchor.
|
|
144
|
-
const to = mapPosition(anchor.
|
|
145
|
+
const from = mapPosition(anchor.from, anchor.assoc.start, mapping);
|
|
146
|
+
const to = mapPosition(anchor.to, anchor.assoc.end, mapping);
|
|
145
147
|
const mappedRange = normalizeRange({ from: from.position, to: to.position });
|
|
146
148
|
|
|
147
149
|
if (from.deleted && to.deleted && mappedRange.from === mappedRange.to) {
|
|
148
150
|
return createDetachedAnchor(
|
|
149
|
-
anchor.
|
|
151
|
+
{ from: anchor.from, to: anchor.to },
|
|
150
152
|
mapping.metadata?.invalidatesStructures
|
|
151
153
|
? "invalidatedByStructureChange"
|
|
152
154
|
: "deleted",
|
|
@@ -187,8 +189,8 @@ export function areAnchorsEqual(
|
|
|
187
189
|
|
|
188
190
|
if (left.kind === "range" && right.kind === "range") {
|
|
189
191
|
return (
|
|
190
|
-
left.
|
|
191
|
-
left.
|
|
192
|
+
left.from === right.from &&
|
|
193
|
+
left.to === right.to &&
|
|
192
194
|
left.assoc.start === right.assoc.start &&
|
|
193
195
|
left.assoc.end === right.assoc.end
|
|
194
196
|
);
|
|
@@ -213,7 +215,7 @@ export function anchorUnaffectedByMapping(
|
|
|
213
215
|
): boolean {
|
|
214
216
|
if (anchor.kind === "detached") return true;
|
|
215
217
|
if (mapping.steps.length === 0) return true;
|
|
216
|
-
const end = anchor.kind === "range" ? anchor.
|
|
218
|
+
const end = anchor.kind === "range" ? anchor.to : anchor.at;
|
|
217
219
|
return mapping.steps.every((s) => s.from > end);
|
|
218
220
|
}
|
|
219
221
|
|
|
@@ -40,7 +40,7 @@ export function mapReviewAnchor(
|
|
|
40
40
|
|
|
41
41
|
export function getAnchorRange(anchor: ReviewAnchor): DocRange {
|
|
42
42
|
if (anchor.kind === "range") {
|
|
43
|
-
return normalizeRange(anchor.
|
|
43
|
+
return normalizeRange({ from: anchor.from, to: anchor.to });
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (anchor.kind === "node") {
|
|
@@ -136,7 +136,7 @@ export function commentAnchorRejectionReason(
|
|
|
136
136
|
return "invalid_comment_anchor";
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
-
const normalized = normalizeRange(anchor.
|
|
139
|
+
const normalized = normalizeRange({ from: anchor.from, to: anchor.to });
|
|
140
140
|
if (normalized.from === normalized.to) {
|
|
141
141
|
return "invalid_comment_anchor";
|
|
142
142
|
}
|
|
@@ -164,7 +164,7 @@ export function snapCommentAnchorAwayFromTable(
|
|
|
164
164
|
): ReviewAnchor | null {
|
|
165
165
|
if (anchor.kind !== "range") return null;
|
|
166
166
|
|
|
167
|
-
const normalized = normalizeRange(anchor.
|
|
167
|
+
const normalized = normalizeRange({ from: anchor.from, to: anchor.to });
|
|
168
168
|
if (normalized.from === normalized.to) return null;
|
|
169
169
|
|
|
170
170
|
// If the anchor is already flagged invalid for other reasons (crosses an
|
|
@@ -119,6 +119,7 @@ export class ExportSession {
|
|
|
119
119
|
this.packageRelationships,
|
|
120
120
|
this.ownedPaths,
|
|
121
121
|
);
|
|
122
|
+
ensurePicNamespaceOnStoryParts(this.workingParts);
|
|
122
123
|
const parts = pruneReservedManifestParts(this.workingParts);
|
|
123
124
|
const manifest = buildManifest(parts, this.packageRelationships, this.sourcePackage.manifest.contentTypes);
|
|
124
125
|
return {
|
|
@@ -140,6 +141,58 @@ export function createExportSession(
|
|
|
140
141
|
return new ExportSession(sourcePackage, ownedOutputPaths);
|
|
141
142
|
}
|
|
142
143
|
|
|
144
|
+
/**
|
|
145
|
+
* LibreOffice strict-parser safety net. Word tolerates `pic:` prefix
|
|
146
|
+
* usage (from `<pic:pic>` / `<pic:spPr>` inside drawings) without
|
|
147
|
+
* declaring `xmlns:pic` at the part root, because it inherits scope
|
|
148
|
+
* from `<wp:inline xmlns:pic=…>` wrappers or from `mc:AlternateContent`
|
|
149
|
+
* fallbacks. LibreOffice refuses the file ("Namespace prefix pic on
|
|
150
|
+
* spPr is not defined"). Our own serializers already inject
|
|
151
|
+
* `xmlns:pic` on freshly serialized parts — but parts that pass
|
|
152
|
+
* through unmodified from the source package keep the original
|
|
153
|
+
* declaration (which may be missing it). This sweep runs at toPackage
|
|
154
|
+
* time over the main story + every header / footer part, and injects
|
|
155
|
+
* `xmlns:pic` on the root element when the part uses the `pic:` prefix
|
|
156
|
+
* but doesn't declare it. Byte-stable for parts already correct.
|
|
157
|
+
*
|
|
158
|
+
* See commit a3e05a8e (serializer-side fix) + this (pass-through fix).
|
|
159
|
+
*/
|
|
160
|
+
const PIC_NAMESPACE_URI = "http://schemas.openxmlformats.org/drawingml/2006/picture";
|
|
161
|
+
const PIC_NAMESPACE_DECL = ` xmlns:pic="${PIC_NAMESPACE_URI}"`;
|
|
162
|
+
|
|
163
|
+
function ensurePicNamespaceOnStoryParts(
|
|
164
|
+
parts: Map<string, OpcPackagePart>,
|
|
165
|
+
): void {
|
|
166
|
+
for (const [path, part] of parts.entries()) {
|
|
167
|
+
if (!isPicSensitiveStoryPath(path)) continue;
|
|
168
|
+
const patched = ensurePicNamespaceOnRoot(part.bytes);
|
|
169
|
+
if (patched !== part.bytes) {
|
|
170
|
+
parts.set(path, { ...part, bytes: patched, crc32: 0 });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isPicSensitiveStoryPath(path: string): boolean {
|
|
176
|
+
// Normalize for leading slash variation.
|
|
177
|
+
const normalized = path.startsWith("/") ? path.slice(1) : path;
|
|
178
|
+
if (normalized === "word/document.xml") return true;
|
|
179
|
+
if (/^word\/(?:header|footer|footnotes|endnotes)[^/]*\.xml$/.test(normalized)) return true;
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function ensurePicNamespaceOnRoot(bytes: Uint8Array): Uint8Array {
|
|
184
|
+
const xml = new TextDecoder().decode(bytes);
|
|
185
|
+
// Match the first element (skipping the XML prolog + whitespace).
|
|
186
|
+
const rootMatch = xml.match(/^([\s\S]*?)<([A-Za-z][\w.:-]*)([^>]*)>/);
|
|
187
|
+
if (!rootMatch) return bytes;
|
|
188
|
+
const [fullMatch, prefix, tagName, rootAttrs] = rootMatch;
|
|
189
|
+
if (rootAttrs.includes("xmlns:pic=")) return bytes; // already declared
|
|
190
|
+
if (!/\bpic:[A-Za-z]/.test(xml)) return bytes; // doesn't use pic: prefix
|
|
191
|
+
const patchedRoot = `<${tagName}${rootAttrs}${PIC_NAMESPACE_DECL}>`;
|
|
192
|
+
const patched = `${prefix}${patchedRoot}${xml.slice(fullMatch.length)}`;
|
|
193
|
+
return new TextEncoder().encode(patched);
|
|
194
|
+
}
|
|
195
|
+
|
|
143
196
|
function clonePartsForExport(
|
|
144
197
|
parts: Map<string, OpcPackagePart>,
|
|
145
198
|
ownedPaths: ReadonlySet<string>,
|
|
@@ -269,16 +269,16 @@ export function serializeCommentAnchorsIntoDocumentXml(
|
|
|
269
269
|
continue;
|
|
270
270
|
}
|
|
271
271
|
|
|
272
|
-
const startParagraph = findParagraphForEndpoint(paragraphs, anchor.
|
|
273
|
-
const endParagraph = findParagraphForEndpoint(paragraphs, anchor.
|
|
272
|
+
const startParagraph = findParagraphForEndpoint(paragraphs, anchor.from, "start");
|
|
273
|
+
const endParagraph = findParagraphForEndpoint(paragraphs, anchor.to, "end");
|
|
274
274
|
|
|
275
275
|
if (!startParagraph || !endParagraph) {
|
|
276
276
|
skippedCommentIds.push(thread.commentId);
|
|
277
277
|
continue;
|
|
278
278
|
}
|
|
279
279
|
|
|
280
|
-
const startIndex = startParagraph.boundaries.get(anchor.
|
|
281
|
-
const endIndex = endParagraph.boundaries.get(anchor.
|
|
280
|
+
const startIndex = startParagraph.boundaries.get(anchor.from);
|
|
281
|
+
const endIndex = endParagraph.boundaries.get(anchor.to);
|
|
282
282
|
|
|
283
283
|
if (startIndex === undefined || endIndex === undefined) {
|
|
284
284
|
skippedCommentIds.push(thread.commentId);
|
|
@@ -135,8 +135,8 @@ function createParagraphPropertyChangeReplacement(
|
|
|
135
135
|
}
|
|
136
136
|
const paragraphBoundary = findParagraphBoundaryForRange(
|
|
137
137
|
boundaries,
|
|
138
|
-
revision.anchor.
|
|
139
|
-
revision.anchor.
|
|
138
|
+
revision.anchor.from,
|
|
139
|
+
revision.anchor.to,
|
|
140
140
|
);
|
|
141
141
|
if (!paragraphBoundary) {
|
|
142
142
|
return undefined;
|
|
@@ -168,14 +168,14 @@ function createRunPropertyChangeReplacement(
|
|
|
168
168
|
}
|
|
169
169
|
const paragraphBoundary = findParagraphBoundaryForRange(
|
|
170
170
|
boundaries,
|
|
171
|
-
revision.anchor.
|
|
172
|
-
revision.anchor.
|
|
171
|
+
revision.anchor.from,
|
|
172
|
+
revision.anchor.to,
|
|
173
173
|
);
|
|
174
174
|
if (!paragraphBoundary) {
|
|
175
175
|
return undefined;
|
|
176
176
|
}
|
|
177
|
-
const startIndex = paragraphBoundary.boundaries.get(revision.anchor.
|
|
178
|
-
const endIndex = paragraphBoundary.boundaries.get(revision.anchor.
|
|
177
|
+
const startIndex = paragraphBoundary.boundaries.get(revision.anchor.from);
|
|
178
|
+
const endIndex = paragraphBoundary.boundaries.get(revision.anchor.to);
|
|
179
179
|
if (startIndex === undefined || endIndex === undefined || endIndex < startIndex) {
|
|
180
180
|
return undefined;
|
|
181
181
|
}
|
|
@@ -245,13 +245,13 @@ function createRangeRevisionReplacement(
|
|
|
245
245
|
if (anchor.kind !== "range") {
|
|
246
246
|
return undefined;
|
|
247
247
|
}
|
|
248
|
-
const paragraphBoundary = findParagraphBoundaryForRange(boundaries, anchor.
|
|
248
|
+
const paragraphBoundary = findParagraphBoundaryForRange(boundaries, anchor.from, anchor.to);
|
|
249
249
|
if (!paragraphBoundary) {
|
|
250
250
|
return undefined;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
const startIndex = paragraphBoundary.boundaries.get(anchor.
|
|
254
|
-
const endIndex = paragraphBoundary.boundaries.get(anchor.
|
|
253
|
+
const startIndex = paragraphBoundary.boundaries.get(anchor.from);
|
|
254
|
+
const endIndex = paragraphBoundary.boundaries.get(anchor.to);
|
|
255
255
|
if (startIndex === undefined || endIndex === undefined || endIndex < startIndex) {
|
|
256
256
|
return undefined;
|
|
257
257
|
}
|
|
@@ -340,7 +340,7 @@ function findParagraphBoundaryForAnchor(
|
|
|
340
340
|
boundaries: readonly RevisionParagraphBoundary[],
|
|
341
341
|
revision: RevisionRecord,
|
|
342
342
|
): RevisionParagraphBoundary | undefined {
|
|
343
|
-
const anchor = revision.anchor.kind === "range" ? revision.anchor.
|
|
343
|
+
const anchor = revision.anchor.kind === "range" ? revision.anchor.from : undefined;
|
|
344
344
|
if (anchor === undefined) {
|
|
345
345
|
return undefined;
|
|
346
346
|
}
|
|
@@ -35,8 +35,8 @@ function collectSplitPositions(
|
|
|
35
35
|
continue;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
positions.add(thread.anchor.
|
|
39
|
-
positions.add(thread.anchor.
|
|
38
|
+
positions.add(thread.anchor.from);
|
|
39
|
+
positions.add(thread.anchor.to);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
for (const revision of revisions) {
|
|
@@ -51,8 +51,8 @@ function collectSplitPositions(
|
|
|
51
51
|
continue;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
positions.add(revision.anchor.
|
|
55
|
-
positions.add(revision.anchor.
|
|
54
|
+
positions.add(revision.anchor.from);
|
|
55
|
+
positions.add(revision.anchor.to);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
return positions;
|
|
@@ -200,8 +200,8 @@ function collectSplitPoints(
|
|
|
200
200
|
): number[] {
|
|
201
201
|
const points = new Set<number>();
|
|
202
202
|
for (const revision of revisions) {
|
|
203
|
-
const from = revision.anchor.
|
|
204
|
-
const to = revision.anchor.
|
|
203
|
+
const from = revision.anchor.from;
|
|
204
|
+
const to = revision.anchor.to;
|
|
205
205
|
if (from > start && from < end) {
|
|
206
206
|
points.add(from);
|
|
207
207
|
}
|
|
@@ -652,9 +652,9 @@ function extractNodeText(node: XmlNode): string {
|
|
|
652
652
|
|
|
653
653
|
function compareThreadsByAnchor(left: CommentThread, right: CommentThread): number {
|
|
654
654
|
const leftStart =
|
|
655
|
-
left.anchor.kind === "range" ? left.anchor.
|
|
655
|
+
left.anchor.kind === "range" ? left.anchor.from : Number.MAX_SAFE_INTEGER;
|
|
656
656
|
const rightStart =
|
|
657
|
-
right.anchor.kind === "range" ? right.anchor.
|
|
657
|
+
right.anchor.kind === "range" ? right.anchor.from : Number.MAX_SAFE_INTEGER;
|
|
658
658
|
|
|
659
659
|
if (leftStart !== rightStart) {
|
|
660
660
|
return leftStart - rightStart;
|
|
@@ -17,6 +17,7 @@ import { resolveHighlightColor } from "./highlight-colors.ts";
|
|
|
17
17
|
import type { ParseDrawingOpts } from "./parse-drawing.ts";
|
|
18
18
|
import { parseFFDataFromFldChar } from "./parse-ffdata.ts";
|
|
19
19
|
import { classifyFieldInstruction } from "./parse-fields.ts";
|
|
20
|
+
import { isSafeTableFieldInstruction } from "./table-opaque-preservation.ts";
|
|
20
21
|
import { parseXmlWithOffsets as parseXml } from "./xml-parser.ts";
|
|
21
22
|
import { localName, readStringAttr } from "./xml-attr-helpers.ts";
|
|
22
23
|
import {
|
|
@@ -1018,8 +1019,12 @@ function containsRiskyElement(element: XmlElementNode): boolean {
|
|
|
1018
1019
|
const instruction =
|
|
1019
1020
|
readStringAttr(child, "w:instr") ??
|
|
1020
1021
|
extractTextContent(child);
|
|
1021
|
-
|
|
1022
|
-
|
|
1022
|
+
// Coord-01 §11 unification (2026-04-24): the field-safety check
|
|
1023
|
+
// is delegated to the shared `isSafeTableFieldInstruction` helper
|
|
1024
|
+
// so body-direct + secondary-story tables apply the same
|
|
1025
|
+
// allowlist. Legacy form fields (FORMTEXT/FORMCHECKBOX/
|
|
1026
|
+
// FORMDROPDOWN) now pass here just like in main-story tables.
|
|
1027
|
+
if (!isSafeTableFieldInstruction(instruction)) {
|
|
1023
1028
|
return true;
|
|
1024
1029
|
}
|
|
1025
1030
|
continue;
|
|
@@ -1041,17 +1046,6 @@ function containsRiskyElement(element: XmlElementNode): boolean {
|
|
|
1041
1046
|
return false;
|
|
1042
1047
|
}
|
|
1043
1048
|
|
|
1044
|
-
function isSafeSecondaryStoryFieldFamily(family: string): boolean {
|
|
1045
|
-
return (
|
|
1046
|
-
family === "REF" ||
|
|
1047
|
-
family === "PAGEREF" ||
|
|
1048
|
-
family === "NOTEREF" ||
|
|
1049
|
-
family === "TOC" ||
|
|
1050
|
-
family === "PAGE" ||
|
|
1051
|
-
family === "NUMPAGES"
|
|
1052
|
-
);
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
1049
|
function parseSimpleTableElement(
|
|
1056
1050
|
tblElement: XmlElementNode,
|
|
1057
1051
|
sourceXml: string,
|
|
@@ -41,6 +41,7 @@ import { parseShapeXml, parseVmlXml } from "./parse-shapes.ts";
|
|
|
41
41
|
import { parseObject } from "./parse-object.ts";
|
|
42
42
|
import { parseDrawingFrame } from "./parse-drawing.ts";
|
|
43
43
|
import { readFrameProperties } from "./parse-paragraph-formatting.ts";
|
|
44
|
+
import { tableRequiresOpaquePreservation } from "./table-opaque-preservation.ts";
|
|
44
45
|
import { classifyFieldInstruction } from "./parse-fields.ts";
|
|
45
46
|
import { parseFFDataFromFldChar } from "./parse-ffdata.ts";
|
|
46
47
|
import { resolveHighlightColor } from "./highlight-colors.ts";
|
|
@@ -2020,37 +2021,12 @@ function readCellCnfStyle(node: XmlElementNode): string | undefined {
|
|
|
2020
2021
|
* Tables matching this check stay opaque until the respective features
|
|
2021
2022
|
* are implemented in the table editing path.
|
|
2022
2023
|
*/
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
return true;
|
|
2030
|
-
}
|
|
2031
|
-
|
|
2032
|
-
const simpleInstructions = [...rawXml.matchAll(/\bw:instr="([^"]*)"/g)].map((match) => match[1] ?? "");
|
|
2033
|
-
const complexInstructions = extractComplexFieldInstructions(rawXml);
|
|
2034
|
-
for (const instruction of [...simpleInstructions, ...complexInstructions]) {
|
|
2035
|
-
const classification = classifyFieldInstruction(instruction);
|
|
2036
|
-
if (!isSafeMainStoryTableFieldFamily(classification.family)) {
|
|
2037
|
-
return true;
|
|
2038
|
-
}
|
|
2039
|
-
}
|
|
2040
|
-
|
|
2041
|
-
return false;
|
|
2042
|
-
}
|
|
2043
|
-
|
|
2044
|
-
function isSafeMainStoryTableFieldFamily(family: string): boolean {
|
|
2045
|
-
return (
|
|
2046
|
-
family === "REF" ||
|
|
2047
|
-
family === "PAGEREF" ||
|
|
2048
|
-
family === "NOTEREF" ||
|
|
2049
|
-
family === "TOC" ||
|
|
2050
|
-
family === "PAGE" ||
|
|
2051
|
-
family === "NUMPAGES"
|
|
2052
|
-
);
|
|
2053
|
-
}
|
|
2024
|
+
// `tableRequiresOpaquePreservation` now lives in
|
|
2025
|
+
// `src/io/ooxml/table-opaque-preservation.ts` as a shared helper used by
|
|
2026
|
+
// `parse-main-document.ts`, `parse-headers-footers.ts`, and
|
|
2027
|
+
// `parse-footnotes.ts`. Unified 2026-04-24 per coord-01 §11 to stop the
|
|
2028
|
+
// three parsers from drifting out of alignment. The top-of-file import
|
|
2029
|
+
// `tableRequiresOpaquePreservation` points at that module.
|
|
2054
2030
|
|
|
2055
2031
|
function extractComplexFieldInstructions(rawXml: string): string[] {
|
|
2056
2032
|
const tokenPattern =
|