@beyondwork/docx-react-component 1.0.57 → 1.0.59
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 +1 -1
- package/package.json +2 -1
- package/src/api/awareness-identity-types.ts +4 -2
- package/src/api/comment-negotiation-types.ts +4 -1
- package/src/api/external-custody-types.ts +16 -0
- package/src/api/internal/build-ref-projections.ts +108 -0
- package/src/api/package-version.ts +1 -1
- package/src/api/participants-types.ts +11 -1
- package/src/api/public-types.ts +1149 -8
- package/src/api/scope-metadata-resolver-types.ts +6 -0
- package/src/compare/diff-engine.ts +3 -0
- package/src/core/commands/formatting-commands.ts +1 -0
- package/src/core/commands/index.ts +225 -16
- package/src/core/commands/legacy-form-field-commands.ts +181 -0
- package/src/core/commands/table-structure-commands.ts +149 -31
- package/src/core/selection/mapping.ts +20 -0
- package/src/core/state/editor-state.ts +2 -1
- package/src/index.ts +28 -0
- package/src/io/docx-session.ts +22 -3
- package/src/io/export/export-session.ts +11 -7
- package/src/io/export/ooxml-namespaces.ts +47 -0
- package/src/io/export/reattach-preserved-parts.ts +4 -16
- package/src/io/export/serialize-comments.ts +3 -131
- package/src/io/export/serialize-ffdata.ts +89 -0
- package/src/io/export/serialize-headers-footers.ts +5 -0
- package/src/io/export/serialize-main-document.ts +224 -34
- package/src/io/export/serialize-numbering.ts +22 -2
- package/src/io/export/serialize-revisions.ts +99 -0
- package/src/io/export/serialize-tables.ts +9 -0
- package/src/io/export/split-review-boundaries.ts +1 -0
- package/src/io/export/table-properties-xml.ts +14 -0
- package/src/io/load-scheduler.ts +70 -28
- package/src/io/normalize/normalize-text.ts +13 -0
- package/src/io/ooxml/_mini-xml.ts +198 -0
- package/src/io/ooxml/canonicalize-payload.ts +1 -4
- package/src/io/ooxml/chart/chart-style-table.ts +4 -3
- package/src/io/ooxml/chart/parse-chart-space.ts +2 -4
- package/src/io/ooxml/chart/parse-series.ts +2 -1
- package/src/io/ooxml/chart/resolve-color.ts +2 -2
- package/src/io/ooxml/chart/types.ts +6 -434
- package/src/io/ooxml/comment-presentation-payload.ts +6 -5
- package/src/io/ooxml/highlight-colors.ts +8 -5
- package/src/io/ooxml/parse-anchor.ts +68 -53
- package/src/io/ooxml/parse-comments.ts +14 -142
- package/src/io/ooxml/parse-complex-content.ts +3 -106
- package/src/io/ooxml/parse-drawing.ts +100 -195
- package/src/io/ooxml/parse-ffdata.ts +93 -0
- package/src/io/ooxml/parse-fields.ts +7 -146
- package/src/io/ooxml/parse-fill.ts +88 -8
- package/src/io/ooxml/parse-font-table.ts +5 -105
- package/src/io/ooxml/parse-footnotes.ts +28 -152
- package/src/io/ooxml/parse-headers-footers.ts +106 -212
- package/src/io/ooxml/parse-inline-media.ts +3 -200
- package/src/io/ooxml/parse-main-document.ts +180 -217
- package/src/io/ooxml/parse-numbering.ts +154 -335
- package/src/io/ooxml/parse-object.ts +147 -0
- package/src/io/ooxml/parse-ole-relationship.ts +82 -0
- package/src/io/ooxml/parse-paragraph-formatting.ts +7 -10
- package/src/io/ooxml/parse-picture-sdt.ts +85 -0
- package/src/io/ooxml/parse-picture.ts +120 -39
- package/src/io/ooxml/parse-revisions.ts +285 -51
- package/src/io/ooxml/parse-settings.ts +6 -99
- package/src/io/ooxml/parse-shapes.ts +25 -140
- package/src/io/ooxml/parse-styles.ts +3 -218
- package/src/io/ooxml/parse-tables.ts +76 -256
- package/src/io/ooxml/parse-theme.ts +1 -4
- package/src/io/ooxml/property-grab-bag.ts +5 -47
- package/src/io/ooxml/xml-element-serialize.ts +32 -0
- package/src/io/ooxml/xml-parser.ts +183 -0
- package/src/legal/bookmarks.ts +1 -1
- package/src/legal/cross-references.ts +1 -1
- package/src/legal/defined-terms.ts +1 -1
- package/src/legal/{_document-root.ts → document-root.ts} +8 -0
- package/src/legal/signature-blocks.ts +1 -1
- package/src/model/canonical-document.ts +165 -6
- package/src/model/chart-types.ts +439 -0
- package/src/model/snapshot.ts +3 -1
- package/src/review/store/comment-remapping.ts +24 -11
- package/src/review/store/revision-actions.ts +482 -2
- package/src/review/store/revision-store.ts +15 -0
- package/src/review/store/revision-types.ts +76 -0
- package/src/runtime/collab/remote-cursor-awareness.ts +24 -0
- package/src/runtime/collab/runtime-collab-sync.ts +33 -0
- package/src/runtime/diagnostics/build-diagnostic.ts +151 -0
- package/src/runtime/diagnostics/code-metadata-table.ts +221 -0
- package/src/runtime/document-runtime.ts +544 -35
- package/src/runtime/document-search.ts +176 -0
- package/src/runtime/edit-ops/index.ts +18 -2
- package/src/runtime/footnote-resolver.ts +130 -0
- package/src/runtime/layout/layout-engine-instance.ts +31 -4
- package/src/runtime/layout/layout-engine-version.ts +37 -1
- package/src/runtime/layout/page-graph.ts +14 -1
- package/src/runtime/layout/resolved-formatting-state.ts +21 -0
- package/src/runtime/numbering-prefix.ts +17 -0
- package/src/runtime/query-scopes.ts +183 -0
- package/src/runtime/resolved-numbering-geometry.ts +37 -6
- package/src/runtime/revision-runtime.ts +27 -1
- package/src/runtime/scope-resolver.ts +60 -0
- package/src/runtime/selection/post-edit-validator.ts +60 -6
- package/src/runtime/structure-ops/index.ts +20 -4
- package/src/runtime/surface-projection.ts +293 -18
- package/src/runtime/table-schema.ts +6 -0
- package/src/runtime/theme-color-resolver.ts +2 -2
- package/src/runtime/units.ts +9 -0
- package/src/runtime/workflow-rail-segments.ts +4 -0
- package/src/ui/WordReviewEditor.tsx +258 -44
- package/src/ui/editor-runtime-boundary.ts +13 -0
- package/src/ui/editor-shell-view.tsx +4 -1
- package/src/ui/headless/chrome-registry.ts +53 -0
- package/src/ui/headless/selection-tool-resolver.ts +11 -1
- package/src/ui-tailwind/chrome/chrome-preset-model.ts +13 -0
- package/src/ui-tailwind/chrome/tw-command-palette-mount.tsx +96 -0
- package/src/ui-tailwind/chrome/tw-context-menu.tsx +2 -1
- package/src/ui-tailwind/chrome/tw-image-context-toolbar.tsx +5 -4
- package/src/ui-tailwind/chrome/tw-mode-dock.tsx +6 -2
- package/src/ui-tailwind/chrome/use-container-breakpoint.ts +111 -0
- package/src/ui-tailwind/chrome-overlay/tw-chrome-overlay.tsx +23 -9
- package/src/ui-tailwind/chrome-overlay/tw-object-selection-overlay.tsx +158 -0
- package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +6 -7
- package/src/ui-tailwind/editor-surface/pm-schema.ts +105 -17
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +13 -0
- package/src/ui-tailwind/editor-surface/shape-renderer.ts +76 -14
- package/src/ui-tailwind/editor-surface/tw-page-block-view.helpers.ts +18 -1
- package/src/ui-tailwind/editor-surface/tw-page-block-view.tsx +2 -0
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +18 -2
- package/src/ui-tailwind/index.ts +9 -0
- package/src/ui-tailwind/page-chrome-model.ts +77 -5
- package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +56 -1
- package/src/ui-tailwind/page-stack/tw-region-block-renderer.tsx +2 -0
- package/src/ui-tailwind/review/tw-comment-sidebar.tsx +116 -113
- package/src/ui-tailwind/review/tw-review-rail-footer.tsx +2 -2
- package/src/ui-tailwind/theme/tokens.ts +14 -0
- package/src/ui-tailwind/toolbar/tw-shell-header.tsx +5 -0
- package/src/ui-tailwind/tw-review-workspace.tsx +52 -87
- package/src/validation/diagnostics.ts +1 -0
|
@@ -75,6 +75,8 @@ const SUPPORTED_CONTAINER_TYPES = new Set(["ins", "del"]);
|
|
|
75
75
|
const PRESERVE_ONLY_CONTAINER_TYPES = new Set(["moveFrom", "moveTo"]);
|
|
76
76
|
const PROPERTY_CHANGE_REVISION_TYPES = new Set(["rPrChange", "pPrChange"]);
|
|
77
77
|
const STRUCTURAL_TABLE_REVISION_TYPES = new Set(["cellIns", "cellDel", "cellMerge"]);
|
|
78
|
+
const MOVE_RANGE_START_TYPES = new Set(["moveFromRangeStart", "moveToRangeStart"]);
|
|
79
|
+
const MOVE_RANGE_END_TYPES = new Set(["moveFromRangeEnd", "moveToRangeEnd"]);
|
|
78
80
|
|
|
79
81
|
export function parseRevisionsFromDocumentXml(
|
|
80
82
|
documentXml: string,
|
|
@@ -92,65 +94,118 @@ export function parseRevisionsFromDocumentXml(
|
|
|
92
94
|
nextGeneratedIndex: 1,
|
|
93
95
|
};
|
|
94
96
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
97
|
+
const walkState = {
|
|
98
|
+
paragraphIndex: -1,
|
|
99
|
+
cursor: 0,
|
|
100
|
+
previousWasParagraph: false,
|
|
101
|
+
tableOrdinal: -1,
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
walkBodyChildren(bodyElement.children, walkState, state);
|
|
105
|
+
|
|
106
|
+
linkMoveRevisionPairs(state);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
revisions: state.revisions,
|
|
110
|
+
preservedMarkup: state.preservedMarkup.sort((left, right) => left.xmlStart - right.xmlStart),
|
|
111
|
+
diagnostics: state.diagnostics,
|
|
112
|
+
boundaries,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
98
115
|
|
|
99
|
-
|
|
116
|
+
interface BodyWalkState {
|
|
117
|
+
paragraphIndex: number;
|
|
118
|
+
cursor: number;
|
|
119
|
+
previousWasParagraph: boolean;
|
|
120
|
+
tableOrdinal: number;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function walkBodyChildren(
|
|
124
|
+
children: XmlNode[],
|
|
125
|
+
walkState: BodyWalkState,
|
|
126
|
+
state: ParseState,
|
|
127
|
+
): void {
|
|
128
|
+
for (const child of children) {
|
|
100
129
|
if (child.type !== "element") {
|
|
101
130
|
continue;
|
|
102
131
|
}
|
|
103
132
|
|
|
104
133
|
const childType = localName(child.name);
|
|
105
134
|
|
|
106
|
-
if (childType
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
const nested = parseNestedParagraphContainers(
|
|
110
|
-
child.children,
|
|
111
|
-
paragraphIndex,
|
|
112
|
-
cursor,
|
|
113
|
-
previousWasParagraph,
|
|
114
|
-
state,
|
|
115
|
-
);
|
|
116
|
-
paragraphIndex = nested.paragraphIndex;
|
|
117
|
-
cursor = nested.cursor;
|
|
118
|
-
previousWasParagraph = nested.previousWasParagraph;
|
|
119
|
-
} else if (childType === "sectPr") {
|
|
120
|
-
parseSectPrRevisions(child, cursor, state);
|
|
121
|
-
cursor += 1;
|
|
122
|
-
previousWasParagraph = false;
|
|
123
|
-
} else {
|
|
124
|
-
cursor += 1;
|
|
125
|
-
previousWasParagraph = false;
|
|
135
|
+
if (childType === "p") {
|
|
136
|
+
if (walkState.previousWasParagraph) {
|
|
137
|
+
walkState.cursor += 1;
|
|
126
138
|
}
|
|
139
|
+
walkState.paragraphIndex += 1;
|
|
140
|
+
const paragraphBoundary = state.boundaries[walkState.paragraphIndex];
|
|
141
|
+
if (!paragraphBoundary) {
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
parseParagraphMarkRevisions(child, paragraphBoundary, state);
|
|
145
|
+
walkParagraphContent(
|
|
146
|
+
child.children,
|
|
147
|
+
walkState.paragraphIndex,
|
|
148
|
+
state,
|
|
149
|
+
() => walkState.cursor,
|
|
150
|
+
(next) => {
|
|
151
|
+
walkState.cursor = next;
|
|
152
|
+
},
|
|
153
|
+
);
|
|
154
|
+
walkState.previousWasParagraph = true;
|
|
127
155
|
continue;
|
|
128
156
|
}
|
|
129
157
|
|
|
130
|
-
if (
|
|
131
|
-
|
|
158
|
+
if (childType === "tbl") {
|
|
159
|
+
walkState.tableOrdinal += 1;
|
|
160
|
+
parseTblPropertyRevisions(child, walkState.cursor, walkState.tableOrdinal, state);
|
|
161
|
+
const nested = parseNestedParagraphContainers(
|
|
162
|
+
child.children,
|
|
163
|
+
walkState.paragraphIndex,
|
|
164
|
+
walkState.cursor,
|
|
165
|
+
walkState.previousWasParagraph,
|
|
166
|
+
state,
|
|
167
|
+
[walkState.tableOrdinal],
|
|
168
|
+
);
|
|
169
|
+
walkState.paragraphIndex = nested.paragraphIndex;
|
|
170
|
+
walkState.cursor = nested.cursor;
|
|
171
|
+
walkState.previousWasParagraph = nested.previousWasParagraph;
|
|
172
|
+
continue;
|
|
132
173
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
174
|
+
|
|
175
|
+
// Phase S: body-level SDT / customXml wrappers contribute their children
|
|
176
|
+
// to body scope. Tables inside them get flat body-level tableOrdinals
|
|
177
|
+
// (not nested paths) because the canonical-document projection preserves
|
|
178
|
+
// the SDT wrapper and `resolveCanonicalTableRow` descends into it to
|
|
179
|
+
// match ordinals.
|
|
180
|
+
if (
|
|
181
|
+
childType === "sdt" ||
|
|
182
|
+
childType === "sdtContent" ||
|
|
183
|
+
childType === "customXml" ||
|
|
184
|
+
childType === "smartTag"
|
|
185
|
+
) {
|
|
186
|
+
walkBodyChildren(child.children, walkState, state);
|
|
136
187
|
continue;
|
|
137
188
|
}
|
|
138
189
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
cursor
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
190
|
+
if (childType === "sectPr") {
|
|
191
|
+
parseSectPrRevisions(child, walkState.cursor, state);
|
|
192
|
+
walkState.cursor += 1;
|
|
193
|
+
walkState.previousWasParagraph = false;
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
145
196
|
|
|
146
|
-
|
|
197
|
+
if (
|
|
198
|
+
MOVE_RANGE_START_TYPES.has(childType) ||
|
|
199
|
+
MOVE_RANGE_END_TYPES.has(childType)
|
|
200
|
+
) {
|
|
201
|
+
parseMoveRangeMarker(child, childType, walkState.cursor, state);
|
|
202
|
+
walkState.previousWasParagraph = false;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
147
205
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
diagnostics: state.diagnostics,
|
|
152
|
-
boundaries,
|
|
153
|
-
};
|
|
206
|
+
walkState.cursor += 1;
|
|
207
|
+
walkState.previousWasParagraph = false;
|
|
208
|
+
}
|
|
154
209
|
}
|
|
155
210
|
|
|
156
211
|
function parseNestedParagraphContainers(
|
|
@@ -159,6 +214,8 @@ function parseNestedParagraphContainers(
|
|
|
159
214
|
cursor: number,
|
|
160
215
|
previousWasParagraph: boolean,
|
|
161
216
|
state: ParseState,
|
|
217
|
+
parentPath?: readonly number[],
|
|
218
|
+
nestedTableCounter?: { value: number },
|
|
162
219
|
): {
|
|
163
220
|
paragraphIndex: number;
|
|
164
221
|
cursor: number;
|
|
@@ -167,6 +224,12 @@ function parseNestedParagraphContainers(
|
|
|
167
224
|
let nextParagraphIndex = paragraphIndex;
|
|
168
225
|
let nextCursor = cursor;
|
|
169
226
|
let nextPreviousWasParagraph = previousWasParagraph;
|
|
227
|
+
// SHARED counter for nested <w:tbl> elements across the entire DFS walk of a
|
|
228
|
+
// single outer table. Created when first entering with parentPath (body
|
|
229
|
+
// loop or a new nested table); passed through recursive tr/tc/sdtContent
|
|
230
|
+
// calls so two sibling cells each containing a nested table get ordinals
|
|
231
|
+
// 0, 1 (matching `findNestedTableByOrdinal`'s DFS counter).
|
|
232
|
+
const counter = nestedTableCounter ?? { value: -1 };
|
|
170
233
|
|
|
171
234
|
for (const node of nodes) {
|
|
172
235
|
if (node.type !== "element") {
|
|
@@ -196,12 +259,30 @@ function parseNestedParagraphContainers(
|
|
|
196
259
|
type === "customXml" ||
|
|
197
260
|
type === "smartTag"
|
|
198
261
|
) {
|
|
262
|
+
let childParentPath = parentPath;
|
|
263
|
+
let childCounter = counter;
|
|
264
|
+
|
|
265
|
+
// For nested tables: call parseTblPropertyRevisions so that row/cell
|
|
266
|
+
// structural revisions inside the nested table get an ordinal path
|
|
267
|
+
// (tableOrdinalPath) instead of a body-level flat ordinal. (Phase Q)
|
|
268
|
+
if (type === "tbl" && parentPath !== undefined) {
|
|
269
|
+
counter.value += 1;
|
|
270
|
+
const myOrdinal = counter.value;
|
|
271
|
+
// parentPath (not nestedPath) because parseTableRowAndCellRevisions
|
|
272
|
+
// appends tableOrdinal itself to build the full tableOrdinalPath.
|
|
273
|
+
parseTblPropertyRevisions(node as XmlElementNode, nextCursor, myOrdinal, state, parentPath);
|
|
274
|
+
childParentPath = [...parentPath, myOrdinal];
|
|
275
|
+
// A new counter for THIS nested table's own direct-child tables.
|
|
276
|
+
childCounter = { value: -1 };
|
|
277
|
+
}
|
|
199
278
|
const nested = parseNestedParagraphContainers(
|
|
200
279
|
node.children,
|
|
201
280
|
nextParagraphIndex,
|
|
202
281
|
nextCursor,
|
|
203
282
|
nextPreviousWasParagraph,
|
|
204
283
|
state,
|
|
284
|
+
childParentPath,
|
|
285
|
+
childCounter,
|
|
205
286
|
);
|
|
206
287
|
nextParagraphIndex = nested.paragraphIndex;
|
|
207
288
|
nextCursor = nested.cursor;
|
|
@@ -398,11 +479,7 @@ function walkContentNode(
|
|
|
398
479
|
const length = measureStoryLength(node);
|
|
399
480
|
const end = start + length;
|
|
400
481
|
const hasNestedRevision = containsNestedRevision(node);
|
|
401
|
-
const metadata = readRevisionMetadata(
|
|
402
|
-
node,
|
|
403
|
-
state,
|
|
404
|
-
type === "moveFrom" || type === "moveTo" ? "move" : type,
|
|
405
|
-
);
|
|
482
|
+
const metadata = readRevisionMetadata(node, state, type);
|
|
406
483
|
|
|
407
484
|
if (hasNestedRevision) {
|
|
408
485
|
const nestedKind = type === "ins" ? "insertion" : type === "del" ? "deletion" : "move";
|
|
@@ -634,7 +711,9 @@ function parseRunFormattingRevisions(
|
|
|
634
711
|
function parseTblPropertyRevisions(
|
|
635
712
|
table: XmlElementNode,
|
|
636
713
|
position: number,
|
|
714
|
+
tableOrdinal: number,
|
|
637
715
|
state: ParseState,
|
|
716
|
+
parentPath?: readonly number[],
|
|
638
717
|
): void {
|
|
639
718
|
const tblPr = findChildElement(table, "tblPr");
|
|
640
719
|
if (tblPr) {
|
|
@@ -679,18 +758,87 @@ function parseTblPropertyRevisions(
|
|
|
679
758
|
}
|
|
680
759
|
}
|
|
681
760
|
|
|
682
|
-
parseTableRowAndCellRevisions(table, position, state);
|
|
761
|
+
parseTableRowAndCellRevisions(table, position, tableOrdinal, state, parentPath);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
function parseMoveRangeMarker(
|
|
765
|
+
node: XmlElementNode,
|
|
766
|
+
markerType: string,
|
|
767
|
+
position: number,
|
|
768
|
+
state: ParseState,
|
|
769
|
+
): void {
|
|
770
|
+
const direction: "from" | "to" = markerType.startsWith("moveFrom")
|
|
771
|
+
? "from"
|
|
772
|
+
: "to";
|
|
773
|
+
const isStart =
|
|
774
|
+
markerType === "moveFromRangeStart" || markerType === "moveToRangeStart";
|
|
775
|
+
|
|
776
|
+
if (!isStart) {
|
|
777
|
+
// End markers serialize as-is; record only preservedMarkup so the
|
|
778
|
+
// serializer round-trips them losslessly. No revision record — the
|
|
779
|
+
// start marker owns the pairing.
|
|
780
|
+
state.preservedMarkup.push({
|
|
781
|
+
revisionId: `revision:${markerType}-${sanitizeRevisionToken(
|
|
782
|
+
node.attributes["w:id"] ?? node.attributes.id ?? `unknown-${state.nextGeneratedIndex++}`,
|
|
783
|
+
)}`,
|
|
784
|
+
rawXml: state.documentXml.slice(node.start, node.end),
|
|
785
|
+
xmlStart: node.start,
|
|
786
|
+
xmlEnd: node.end,
|
|
787
|
+
originalRevisionType: markerType,
|
|
788
|
+
});
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const metadata = readRevisionMetadata(node, state, markerType);
|
|
793
|
+
const moveData: MoveData = {
|
|
794
|
+
moveId: metadata.ooxmlRevisionId ?? `generated-${metadata.revisionId}`,
|
|
795
|
+
direction,
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
state.revisions.push(
|
|
799
|
+
createRevisionRecord({
|
|
800
|
+
revisionId: metadata.revisionId,
|
|
801
|
+
kind: "move",
|
|
802
|
+
anchor: createRevisionRangeAnchor(position, position),
|
|
803
|
+
authorId: metadata.authorId,
|
|
804
|
+
createdAt: metadata.createdAt,
|
|
805
|
+
metadata: {
|
|
806
|
+
source: "import",
|
|
807
|
+
originalRevisionType: markerType,
|
|
808
|
+
ooxmlRevisionId: metadata.ooxmlRevisionId,
|
|
809
|
+
preserveOnlyReason: "Imported preserve-only revision.",
|
|
810
|
+
moveData,
|
|
811
|
+
},
|
|
812
|
+
}),
|
|
813
|
+
);
|
|
814
|
+
state.preservedMarkup.push({
|
|
815
|
+
revisionId: metadata.revisionId,
|
|
816
|
+
rawXml: state.documentXml.slice(node.start, node.end),
|
|
817
|
+
xmlStart: node.start,
|
|
818
|
+
xmlEnd: node.end,
|
|
819
|
+
originalRevisionType: markerType,
|
|
820
|
+
});
|
|
821
|
+
state.diagnostics.push({
|
|
822
|
+
revisionId: metadata.revisionId,
|
|
823
|
+
code: "preserve_only_move_revision",
|
|
824
|
+
message: `Move range marker (${markerType}) parsed as preserve-only; pairing resolved in post-parse step.`,
|
|
825
|
+
featureClass: "preserve-only",
|
|
826
|
+
});
|
|
683
827
|
}
|
|
684
828
|
|
|
685
829
|
function parseTableRowAndCellRevisions(
|
|
686
830
|
table: XmlElementNode,
|
|
687
831
|
position: number,
|
|
832
|
+
tableOrdinal: number,
|
|
688
833
|
state: ParseState,
|
|
834
|
+
parentPath?: readonly number[],
|
|
689
835
|
): void {
|
|
836
|
+
let rowIndex = -1;
|
|
690
837
|
for (const child of table.children) {
|
|
691
838
|
if (child.type !== "element" || localName(child.name) !== "tr") {
|
|
692
839
|
continue;
|
|
693
840
|
}
|
|
841
|
+
rowIndex += 1;
|
|
694
842
|
|
|
695
843
|
const trPr = findChildElement(child, "trPr");
|
|
696
844
|
if (trPr) {
|
|
@@ -728,12 +876,56 @@ function parseTableRowAndCellRevisions(
|
|
|
728
876
|
beforeContainerXml: beforeXml,
|
|
729
877
|
});
|
|
730
878
|
}
|
|
879
|
+
|
|
880
|
+
// X4.a.3 — bare <w:ins>/<w:del> inside <w:trPr> mark the entire
|
|
881
|
+
// row as inserted/deleted. Distinct from cellIns/cellDel (which
|
|
882
|
+
// live inside tcPr) and from trPrChange (property diff).
|
|
883
|
+
for (const trChild of trPr.children) {
|
|
884
|
+
if (trChild.type !== "element") {
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const trChildName = localName(trChild.name);
|
|
888
|
+
if (trChildName !== "ins" && trChildName !== "del") {
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
const markerType = trChildName === "ins" ? "row-ins" : "row-del";
|
|
892
|
+
const metadata = readRevisionMetadata(trChild, state, markerType);
|
|
893
|
+
state.revisions.push(
|
|
894
|
+
createRevisionRecord({
|
|
895
|
+
revisionId: metadata.revisionId,
|
|
896
|
+
kind: trChildName === "ins" ? "insertion" : "deletion",
|
|
897
|
+
anchor: createRevisionRangeAnchor(position, position),
|
|
898
|
+
authorId: metadata.authorId,
|
|
899
|
+
createdAt: metadata.createdAt,
|
|
900
|
+
metadata: {
|
|
901
|
+
source: "import",
|
|
902
|
+
originalRevisionType: markerType,
|
|
903
|
+
ooxmlRevisionId: metadata.ooxmlRevisionId,
|
|
904
|
+
semanticKind: "structural-change",
|
|
905
|
+
tableRevisionCoordinates: {
|
|
906
|
+
tableOrdinal,
|
|
907
|
+
rowIndex,
|
|
908
|
+
...(parentPath ? { tableOrdinalPath: [...parentPath, tableOrdinal] } : {}),
|
|
909
|
+
},
|
|
910
|
+
},
|
|
911
|
+
}),
|
|
912
|
+
);
|
|
913
|
+
state.preservedMarkup.push({
|
|
914
|
+
revisionId: metadata.revisionId,
|
|
915
|
+
rawXml: state.documentXml.slice(trChild.start, trChild.end),
|
|
916
|
+
xmlStart: trChild.start,
|
|
917
|
+
xmlEnd: trChild.end,
|
|
918
|
+
originalRevisionType: markerType,
|
|
919
|
+
});
|
|
920
|
+
}
|
|
731
921
|
}
|
|
732
922
|
|
|
923
|
+
let cellIndex = -1;
|
|
733
924
|
for (const tcNode of child.children) {
|
|
734
925
|
if (tcNode.type !== "element" || localName(tcNode.name) !== "tc") {
|
|
735
926
|
continue;
|
|
736
927
|
}
|
|
928
|
+
cellIndex += 1;
|
|
737
929
|
|
|
738
930
|
const tcPr = findChildElement(tcNode, "tcPr");
|
|
739
931
|
if (!tcPr) {
|
|
@@ -783,6 +975,44 @@ function parseTableRowAndCellRevisions(
|
|
|
783
975
|
const tcChildName = localName(tcChild.name);
|
|
784
976
|
if (STRUCTURAL_TABLE_REVISION_TYPES.has(tcChildName)) {
|
|
785
977
|
const metadata = readRevisionMetadata(tcChild, state, tcChildName);
|
|
978
|
+
// Phase U: capture w:vMerge direction for cellMerge accept/reject.
|
|
979
|
+
const cellMergeDirection =
|
|
980
|
+
tcChildName === "cellMerge"
|
|
981
|
+
? ((tcChild.attributes["w:vMerge"] ?? tcChild.attributes.vMerge ?? "rest") === "cont"
|
|
982
|
+
? "cont"
|
|
983
|
+
: "rest")
|
|
984
|
+
: undefined;
|
|
985
|
+
const isActionable =
|
|
986
|
+
tcChildName === "cellIns" ||
|
|
987
|
+
tcChildName === "cellDel" ||
|
|
988
|
+
tcChildName === "cellMerge";
|
|
989
|
+
state.revisions.push(
|
|
990
|
+
createRevisionRecord({
|
|
991
|
+
revisionId: metadata.revisionId,
|
|
992
|
+
kind: "formatting",
|
|
993
|
+
anchor: createRevisionRangeAnchor(position, position),
|
|
994
|
+
authorId: metadata.authorId,
|
|
995
|
+
createdAt: metadata.createdAt,
|
|
996
|
+
metadata: {
|
|
997
|
+
source: "import",
|
|
998
|
+
originalRevisionType: tcChildName,
|
|
999
|
+
ooxmlRevisionId: metadata.ooxmlRevisionId,
|
|
1000
|
+
semanticKind: "structural-change",
|
|
1001
|
+
tableRevisionCoordinates: {
|
|
1002
|
+
tableOrdinal,
|
|
1003
|
+
rowIndex,
|
|
1004
|
+
cellIndex,
|
|
1005
|
+
...(parentPath ? { tableOrdinalPath: [...parentPath, tableOrdinal] } : {}),
|
|
1006
|
+
},
|
|
1007
|
+
...(cellMergeDirection ? { cellMergeData: { direction: cellMergeDirection } } : {}),
|
|
1008
|
+
...(isActionable
|
|
1009
|
+
? {}
|
|
1010
|
+
: {
|
|
1011
|
+
preserveOnlyReason: `Structural table revision (${tcChildName}) preserved for round-trip.`,
|
|
1012
|
+
}),
|
|
1013
|
+
},
|
|
1014
|
+
}),
|
|
1015
|
+
);
|
|
786
1016
|
state.preservedMarkup.push({
|
|
787
1017
|
revisionId: metadata.revisionId,
|
|
788
1018
|
rawXml: state.documentXml.slice(tcChild.start, tcChild.end),
|
|
@@ -792,9 +1022,13 @@ function parseTableRowAndCellRevisions(
|
|
|
792
1022
|
});
|
|
793
1023
|
state.diagnostics.push({
|
|
794
1024
|
revisionId: metadata.revisionId,
|
|
795
|
-
code:
|
|
796
|
-
|
|
797
|
-
|
|
1025
|
+
code: isActionable
|
|
1026
|
+
? "table_property_revision_parsed"
|
|
1027
|
+
: "table_structural_revision_preserve_only",
|
|
1028
|
+
message: isActionable
|
|
1029
|
+
? `Structural table revision (${tcChildName}) parsed as actionable structural-change.`
|
|
1030
|
+
: `Structural table revision (${tcChildName}) remains preserve-only; topology-altering changes cannot be safely applied.`,
|
|
1031
|
+
featureClass: isActionable ? "supported" : "preserve-only",
|
|
798
1032
|
});
|
|
799
1033
|
}
|
|
800
1034
|
}
|
|
@@ -5,14 +5,16 @@ import type {
|
|
|
5
5
|
DocumentSettings,
|
|
6
6
|
ThemeColorSlot,
|
|
7
7
|
} from "../../model/canonical-document.ts";
|
|
8
|
+
import { parseXml } from "./xml-parser.ts";
|
|
9
|
+
import { localName } from "./xml-attr-helpers.ts";
|
|
8
10
|
|
|
9
|
-
const CLRSCHEME_MAPPING_SLOTS = new Set<
|
|
11
|
+
const CLRSCHEME_MAPPING_SLOTS = new Set<ClrSchemeMappingSlot>([
|
|
10
12
|
"bg1", "bg2", "t1", "t2",
|
|
11
13
|
"accent1", "accent2", "accent3", "accent4", "accent5", "accent6",
|
|
12
14
|
"hlink", "followedHyperlink",
|
|
13
15
|
]);
|
|
14
16
|
|
|
15
|
-
const THEME_COLOR_SLOTS = new Set<
|
|
17
|
+
const THEME_COLOR_SLOTS = new Set<ThemeColorSlot>([
|
|
16
18
|
"dk1", "lt1", "dk2", "lt2",
|
|
17
19
|
"accent1", "accent2", "accent3", "accent4", "accent5", "accent6",
|
|
18
20
|
"hlink", "folHlink",
|
|
@@ -161,8 +163,8 @@ function parseClrSchemeMapping(
|
|
|
161
163
|
for (const [attr, value] of Object.entries(el.attributes)) {
|
|
162
164
|
if (!value) continue;
|
|
163
165
|
const local = localName(attr);
|
|
164
|
-
if (!CLRSCHEME_MAPPING_SLOTS.has(local)) continue;
|
|
165
|
-
if (!THEME_COLOR_SLOTS.has(value)) continue;
|
|
166
|
+
if (!CLRSCHEME_MAPPING_SLOTS.has(local as ClrSchemeMappingSlot)) continue;
|
|
167
|
+
if (!THEME_COLOR_SLOTS.has(value as ThemeColorSlot)) continue;
|
|
166
168
|
mapping[local as ClrSchemeMappingSlot] = value as ThemeColorSlot;
|
|
167
169
|
}
|
|
168
170
|
return Object.keys(mapping).length > 0 ? mapping : undefined;
|
|
@@ -178,10 +180,6 @@ function findChildElementOptional(
|
|
|
178
180
|
);
|
|
179
181
|
}
|
|
180
182
|
|
|
181
|
-
function localName(name: string): string {
|
|
182
|
-
const idx = name.indexOf(":");
|
|
183
|
-
return idx >= 0 ? name.slice(idx + 1) : name;
|
|
184
|
-
}
|
|
185
183
|
|
|
186
184
|
function readOnOffValue(
|
|
187
185
|
element: XmlElementNode,
|
|
@@ -221,94 +219,3 @@ function readZoomLevel(
|
|
|
221
219
|
return { zoomLevel: parsed };
|
|
222
220
|
}
|
|
223
221
|
|
|
224
|
-
function parseXml(xml: string): XmlElementNode {
|
|
225
|
-
const root: XmlElementNode = {
|
|
226
|
-
type: "element",
|
|
227
|
-
name: "__root__",
|
|
228
|
-
attributes: {},
|
|
229
|
-
children: [],
|
|
230
|
-
};
|
|
231
|
-
const stack: XmlElementNode[] = [root];
|
|
232
|
-
let cursor = 0;
|
|
233
|
-
|
|
234
|
-
while (cursor < xml.length) {
|
|
235
|
-
if (xml.startsWith("<!--", cursor)) {
|
|
236
|
-
const end = xml.indexOf("-->", cursor);
|
|
237
|
-
cursor = end >= 0 ? end + 3 : xml.length;
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (xml.startsWith("<?", cursor)) {
|
|
242
|
-
const end = xml.indexOf("?>", cursor);
|
|
243
|
-
cursor = end >= 0 ? end + 2 : xml.length;
|
|
244
|
-
continue;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const nextLt = xml.indexOf("<", cursor);
|
|
248
|
-
if (nextLt < 0) {
|
|
249
|
-
pushText(xml.slice(cursor));
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (nextLt > cursor) {
|
|
254
|
-
pushText(xml.slice(cursor, nextLt));
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (xml.startsWith("</", nextLt)) {
|
|
258
|
-
const end = xml.indexOf(">", nextLt);
|
|
259
|
-
if (end < 0) break;
|
|
260
|
-
if (stack.length > 1) {
|
|
261
|
-
stack.pop();
|
|
262
|
-
}
|
|
263
|
-
cursor = end + 1;
|
|
264
|
-
continue;
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const end = xml.indexOf(">", nextLt);
|
|
268
|
-
if (end < 0) {
|
|
269
|
-
break;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
const rawTag = xml.slice(nextLt + 1, end).trim();
|
|
273
|
-
const selfClosing = rawTag.endsWith("/");
|
|
274
|
-
const tagBody = selfClosing ? rawTag.slice(0, -1).trim() : rawTag;
|
|
275
|
-
const spaceIndex = tagBody.search(/\s/u);
|
|
276
|
-
const name =
|
|
277
|
-
spaceIndex >= 0 ? tagBody.slice(0, spaceIndex) : tagBody;
|
|
278
|
-
const attrs = spaceIndex >= 0 ? tagBody.slice(spaceIndex + 1) : "";
|
|
279
|
-
const element: XmlElementNode = {
|
|
280
|
-
type: "element",
|
|
281
|
-
name,
|
|
282
|
-
attributes: parseAttributes(attrs),
|
|
283
|
-
children: [],
|
|
284
|
-
};
|
|
285
|
-
stack[stack.length - 1]?.children.push(element);
|
|
286
|
-
if (!selfClosing) {
|
|
287
|
-
stack.push(element);
|
|
288
|
-
}
|
|
289
|
-
cursor = end + 1;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return root;
|
|
293
|
-
|
|
294
|
-
function pushText(raw: string): void {
|
|
295
|
-
const normalized = raw.replace(/\r\n?/gu, "\n");
|
|
296
|
-
if (!normalized.trim()) {
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
299
|
-
stack[stack.length - 1]?.children.push({
|
|
300
|
-
type: "text",
|
|
301
|
-
text: normalized,
|
|
302
|
-
});
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function parseAttributes(raw: string): Record<string, string> {
|
|
307
|
-
const attributes: Record<string, string> = {};
|
|
308
|
-
const pattern = /([^\s=]+)\s*=\s*("([^"]*)"|'([^']*)')/gu;
|
|
309
|
-
for (const match of raw.matchAll(pattern)) {
|
|
310
|
-
const [, name, , dq, sq] = match;
|
|
311
|
-
attributes[name] = dq ?? sq ?? "";
|
|
312
|
-
}
|
|
313
|
-
return attributes;
|
|
314
|
-
}
|