@beyondwork/docx-react-component 1.0.21 → 1.0.23
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 +763 -38
- package/package.json +25 -36
- package/src/api/public-types.ts +66 -1
- package/src/core/commands/index.ts +574 -5
- package/src/index.ts +5 -0
- package/src/io/docx-session.ts +181 -2
- package/src/io/export/serialize-main-document.ts +21 -1
- package/src/io/normalize/normalize-text.ts +4 -0
- package/src/io/ooxml/parse-main-document.ts +88 -7
- package/src/model/canonical-document.ts +22 -0
- package/src/review/store/revision-store.ts +1 -0
- package/src/review/store/revision-types.ts +2 -0
- package/src/runtime/document-runtime.ts +503 -51
- package/src/runtime/session-capabilities.ts +6 -5
- package/src/runtime/surface-projection.ts +2 -0
- package/src/runtime/table-schema.ts +2 -0
- package/src/runtime/workflow-markup.ts +5 -1
- package/src/ui/WordReviewEditor.tsx +661 -132
- package/src/ui/editor-runtime-boundary.ts +10 -1
- package/src/ui/editor-shell-view.tsx +8 -0
- package/src/ui/editor-surface-controller.tsx +5 -0
- package/src/ui/headless/selection-toolbar-model.ts +12 -0
- package/src/ui-tailwind/chrome/tw-suggestion-card.tsx +139 -0
- package/src/ui-tailwind/editor-surface/pm-command-bridge.ts +6 -0
- package/src/ui-tailwind/editor-surface/pm-decorations.ts +44 -16
- package/src/ui-tailwind/editor-surface/pm-state-from-snapshot.ts +2 -0
- package/src/ui-tailwind/editor-surface/surface-build-keys.ts +4 -0
- package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +127 -10
- package/src/ui-tailwind/editor-surface/tw-table-node-view.tsx +82 -1
- package/src/ui-tailwind/status/tw-status-bar.tsx +4 -1
- package/src/ui-tailwind/theme/editor-theme.css +10 -0
- package/src/ui-tailwind/toolbar/tw-toolbar.tsx +21 -5
- package/src/ui-tailwind/tw-review-workspace.tsx +110 -32
package/src/io/docx-session.ts
CHANGED
|
@@ -403,6 +403,10 @@ export function loadDocxEditorSession(
|
|
|
403
403
|
normalizedDocument.preservation.opaqueFragments,
|
|
404
404
|
normalizedRevisions.revisions,
|
|
405
405
|
);
|
|
406
|
+
const subPartOpaqueState = createSubPartOpaqueImportState(
|
|
407
|
+
normalizedDocument.preservation.opaqueFragments,
|
|
408
|
+
normalizedDocument.diagnostics.warnings,
|
|
409
|
+
);
|
|
406
410
|
// ---- Parse sub-parts: headers, footers, footnotes, endnotes, theme ----
|
|
407
411
|
const headerFooterRefs = parseHeaderFooterReferences(sourceDocumentXml);
|
|
408
412
|
const parsedHeaders: HeaderDocument[] = [];
|
|
@@ -439,7 +443,13 @@ export function loadDocxEditorSession(
|
|
|
439
443
|
partPath,
|
|
440
444
|
relationshipId: ref.relationshipId,
|
|
441
445
|
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
442
|
-
blocks:
|
|
446
|
+
blocks: normalizeSubPartOpaqueBlocks(
|
|
447
|
+
parsed.blocks,
|
|
448
|
+
normalizedDocument.preservation.opaqueFragments,
|
|
449
|
+
normalizedDocument.diagnostics.warnings,
|
|
450
|
+
partPath,
|
|
451
|
+
subPartOpaqueState,
|
|
452
|
+
),
|
|
443
453
|
});
|
|
444
454
|
sourceHeaderPaths.push({ partPath, relationshipId: ref.relationshipId });
|
|
445
455
|
} else {
|
|
@@ -449,7 +459,13 @@ export function loadDocxEditorSession(
|
|
|
449
459
|
partPath,
|
|
450
460
|
relationshipId: ref.relationshipId,
|
|
451
461
|
...(ref.sectionIndex !== undefined ? { sectionIndex: ref.sectionIndex } : {}),
|
|
452
|
-
blocks:
|
|
462
|
+
blocks: normalizeSubPartOpaqueBlocks(
|
|
463
|
+
parsed.blocks,
|
|
464
|
+
normalizedDocument.preservation.opaqueFragments,
|
|
465
|
+
normalizedDocument.diagnostics.warnings,
|
|
466
|
+
partPath,
|
|
467
|
+
subPartOpaqueState,
|
|
468
|
+
),
|
|
453
469
|
});
|
|
454
470
|
sourceFooterPaths.push({ partPath, relationshipId: ref.relationshipId });
|
|
455
471
|
}
|
|
@@ -481,12 +497,28 @@ export function loadDocxEditorSession(
|
|
|
481
497
|
footnoteCollection = parseFootnotesXml(
|
|
482
498
|
decodeUtf8(sourcePackage.parts.get(footnotesPartPath)?.bytes ?? new Uint8Array()),
|
|
483
499
|
);
|
|
500
|
+
normalizeFootnoteCollectionOpaqueBlocks(
|
|
501
|
+
footnoteCollection,
|
|
502
|
+
"footnote",
|
|
503
|
+
normalizedDocument.preservation.opaqueFragments,
|
|
504
|
+
normalizedDocument.diagnostics.warnings,
|
|
505
|
+
footnotesPartPath,
|
|
506
|
+
subPartOpaqueState,
|
|
507
|
+
);
|
|
484
508
|
}
|
|
485
509
|
if (endnotesPartPath) {
|
|
486
510
|
footnoteCollection = parseEndnotesXml(
|
|
487
511
|
decodeUtf8(sourcePackage.parts.get(endnotesPartPath)?.bytes ?? new Uint8Array()),
|
|
488
512
|
footnoteCollection,
|
|
489
513
|
);
|
|
514
|
+
normalizeFootnoteCollectionOpaqueBlocks(
|
|
515
|
+
footnoteCollection,
|
|
516
|
+
"endnote",
|
|
517
|
+
normalizedDocument.preservation.opaqueFragments,
|
|
518
|
+
normalizedDocument.diagnostics.warnings,
|
|
519
|
+
endnotesPartPath,
|
|
520
|
+
subPartOpaqueState,
|
|
521
|
+
);
|
|
490
522
|
}
|
|
491
523
|
|
|
492
524
|
const themeRelationship = documentPart.relationships.find(
|
|
@@ -753,6 +785,14 @@ function exportDocxEditorSession(
|
|
|
753
785
|
const actionableRevisions = currentRevisions.filter(
|
|
754
786
|
(revision) => getRevisionActionability(revision) === "actionable",
|
|
755
787
|
);
|
|
788
|
+
const secondaryStoryActionableRevisions = actionableRevisions.filter(
|
|
789
|
+
(revision) => revision.metadata.storyTarget?.kind && revision.metadata.storyTarget.kind !== "main",
|
|
790
|
+
);
|
|
791
|
+
if (secondaryStoryActionableRevisions.length > 0) {
|
|
792
|
+
throw new Error(
|
|
793
|
+
`DOCX export is blocked because ${secondaryStoryActionableRevisions.length} secondary-story tracked changes cannot yet be serialized safely.`,
|
|
794
|
+
);
|
|
795
|
+
}
|
|
756
796
|
const commentThreads = Object.values(
|
|
757
797
|
createCommentStoreFromRuntimeComments(currentDocument.review.comments).threads,
|
|
758
798
|
);
|
|
@@ -1096,6 +1136,143 @@ function createImportedCanonicalDocument(input: {
|
|
|
1096
1136
|
};
|
|
1097
1137
|
}
|
|
1098
1138
|
|
|
1139
|
+
type SubPartOpaqueImportState = {
|
|
1140
|
+
nextFragmentIndex: number;
|
|
1141
|
+
nextWarningIndex: number;
|
|
1142
|
+
nextDiagnosticIndex: number;
|
|
1143
|
+
cursor: number;
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
function createSubPartOpaqueImportState(
|
|
1147
|
+
opaqueFragments: Record<string, OpaqueFragmentRecord>,
|
|
1148
|
+
warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
|
|
1149
|
+
): SubPartOpaqueImportState {
|
|
1150
|
+
const maxByPrefix = (values: Iterable<string>, prefix: string): number => {
|
|
1151
|
+
let max = 0;
|
|
1152
|
+
for (const value of values) {
|
|
1153
|
+
const match = new RegExp(`^${prefix}(\\d+)$`).exec(value);
|
|
1154
|
+
if (!match) {
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
const parsed = Number.parseInt(match[1] ?? "0", 10);
|
|
1158
|
+
if (Number.isFinite(parsed) && parsed > max) {
|
|
1159
|
+
max = parsed;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return max;
|
|
1163
|
+
};
|
|
1164
|
+
|
|
1165
|
+
const maxCursor = Object.values(opaqueFragments).reduce(
|
|
1166
|
+
(currentMax, fragment) => Math.max(currentMax, fragment.lastKnownRange?.to ?? 0),
|
|
1167
|
+
0,
|
|
1168
|
+
);
|
|
1169
|
+
|
|
1170
|
+
return {
|
|
1171
|
+
nextFragmentIndex:
|
|
1172
|
+
maxByPrefix(Object.keys(opaqueFragments), "fragment:import-") + 1,
|
|
1173
|
+
nextWarningIndex:
|
|
1174
|
+
maxByPrefix(
|
|
1175
|
+
[
|
|
1176
|
+
...Object.values(opaqueFragments).map((fragment) => fragment.warningId),
|
|
1177
|
+
...warnings.map((warning) => warning.warningId),
|
|
1178
|
+
],
|
|
1179
|
+
"warning:import-",
|
|
1180
|
+
) + 1,
|
|
1181
|
+
nextDiagnosticIndex:
|
|
1182
|
+
maxByPrefix(warnings.map((warning) => warning.diagnosticId), "diagnostic:import-") + 1,
|
|
1183
|
+
cursor: maxCursor,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function normalizeSubPartOpaqueBlocks(
|
|
1188
|
+
blocks: BlockNode[],
|
|
1189
|
+
opaqueFragments: Record<string, OpaqueFragmentRecord>,
|
|
1190
|
+
warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
|
|
1191
|
+
packagePartName: string,
|
|
1192
|
+
state: SubPartOpaqueImportState,
|
|
1193
|
+
): BlockNode[] {
|
|
1194
|
+
return blocks.map((block) => {
|
|
1195
|
+
if (block.type !== "opaque_block" || typeof block.rawXml !== "string") {
|
|
1196
|
+
return block;
|
|
1197
|
+
}
|
|
1198
|
+
return recordImportedOpaqueBlock(
|
|
1199
|
+
block.rawXml,
|
|
1200
|
+
opaqueFragments,
|
|
1201
|
+
warnings,
|
|
1202
|
+
packagePartName,
|
|
1203
|
+
state,
|
|
1204
|
+
);
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function normalizeFootnoteCollectionOpaqueBlocks(
|
|
1209
|
+
collection: FootnoteCollection | undefined,
|
|
1210
|
+
kind: "footnote" | "endnote",
|
|
1211
|
+
opaqueFragments: Record<string, OpaqueFragmentRecord>,
|
|
1212
|
+
warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
|
|
1213
|
+
packagePartName: string,
|
|
1214
|
+
state: SubPartOpaqueImportState,
|
|
1215
|
+
): void {
|
|
1216
|
+
if (!collection) {
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const notes = kind === "footnote" ? collection.footnotes : collection.endnotes;
|
|
1220
|
+
for (const definition of Object.values(notes)) {
|
|
1221
|
+
definition.blocks = normalizeSubPartOpaqueBlocks(
|
|
1222
|
+
definition.blocks,
|
|
1223
|
+
opaqueFragments,
|
|
1224
|
+
warnings,
|
|
1225
|
+
packagePartName,
|
|
1226
|
+
state,
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
function recordImportedOpaqueBlock(
|
|
1232
|
+
rawXml: string,
|
|
1233
|
+
opaqueFragments: Record<string, OpaqueFragmentRecord>,
|
|
1234
|
+
warnings: CanonicalDocumentEnvelope["diagnostics"]["warnings"],
|
|
1235
|
+
packagePartName: string,
|
|
1236
|
+
state: SubPartOpaqueImportState,
|
|
1237
|
+
): BlockNode {
|
|
1238
|
+
const fragmentId = `fragment:import-${state.nextFragmentIndex}`;
|
|
1239
|
+
state.nextFragmentIndex += 1;
|
|
1240
|
+
const warningId = `warning:import-${state.nextWarningIndex}`;
|
|
1241
|
+
state.nextWarningIndex += 1;
|
|
1242
|
+
const diagnosticId = `diagnostic:import-${state.nextDiagnosticIndex}`;
|
|
1243
|
+
state.nextDiagnosticIndex += 1;
|
|
1244
|
+
|
|
1245
|
+
const rangeStart = state.cursor;
|
|
1246
|
+
const rangeEnd = state.cursor + 1;
|
|
1247
|
+
state.cursor = rangeEnd;
|
|
1248
|
+
|
|
1249
|
+
opaqueFragments[fragmentId] = {
|
|
1250
|
+
fragmentId,
|
|
1251
|
+
payloadKind: "xml-subtree",
|
|
1252
|
+
payloadReference: rawXml,
|
|
1253
|
+
featureClass: "preserve-only",
|
|
1254
|
+
lastKnownRange: {
|
|
1255
|
+
from: rangeStart,
|
|
1256
|
+
to: rangeEnd,
|
|
1257
|
+
},
|
|
1258
|
+
warningId,
|
|
1259
|
+
packagePartName,
|
|
1260
|
+
};
|
|
1261
|
+
warnings.push({
|
|
1262
|
+
diagnosticId,
|
|
1263
|
+
warningId,
|
|
1264
|
+
source: "import",
|
|
1265
|
+
message: "Unsupported sub-part OOXML was preserved as an opaque placeholder.",
|
|
1266
|
+
});
|
|
1267
|
+
|
|
1268
|
+
return {
|
|
1269
|
+
type: "opaque_block",
|
|
1270
|
+
fragmentId,
|
|
1271
|
+
warningId,
|
|
1272
|
+
rawXml,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1099
1276
|
// Canonical model styleId validation pattern — styleIds that don't match
|
|
1100
1277
|
// are excluded from the catalog to avoid snapshot validation failures.
|
|
1101
1278
|
const VALID_STYLE_ID = /^[A-Za-z_][A-Za-z0-9._-]{0,127}$/;
|
|
@@ -1785,6 +1962,7 @@ function toRuntimeRevisionRecords(
|
|
|
1785
1962
|
warningIds: [...revision.warningIds],
|
|
1786
1963
|
metadata: {
|
|
1787
1964
|
source: revision.metadata.source,
|
|
1965
|
+
storyTarget: revision.metadata.storyTarget,
|
|
1788
1966
|
preserveOnlyReason: revision.metadata.preserveOnlyReason,
|
|
1789
1967
|
importedRevisionForm: revision.metadata.importedRevisionForm,
|
|
1790
1968
|
originalRevisionType: revision.metadata.originalRevisionType,
|
|
@@ -1808,6 +1986,7 @@ function toReviewRevisionRecords(
|
|
|
1808
1986
|
warningIds: [...(revision.warningIds ?? [])],
|
|
1809
1987
|
metadata: {
|
|
1810
1988
|
source: revision.metadata?.source ?? "runtime",
|
|
1989
|
+
storyTarget: revision.metadata?.storyTarget,
|
|
1811
1990
|
preserveOnlyReason: revision.metadata?.preserveOnlyReason,
|
|
1812
1991
|
importedRevisionForm: revision.metadata?.importedRevisionForm,
|
|
1813
1992
|
originalRevisionType: revision.metadata?.originalRevisionType,
|
|
@@ -218,7 +218,7 @@ function serializeTableNode(
|
|
|
218
218
|
: "";
|
|
219
219
|
const rowsXml = table.rows
|
|
220
220
|
.map((row) => {
|
|
221
|
-
const rowPropertiesXml = row.propertiesXml ??
|
|
221
|
+
const rowPropertiesXml = row.propertiesXml ?? buildTableRowPropertiesXml(row);
|
|
222
222
|
const cellsXml = row.cells
|
|
223
223
|
.map((cell) => serializeTableCellNode(cell, state))
|
|
224
224
|
.join("");
|
|
@@ -280,6 +280,26 @@ function buildCellPropertiesXml(cell: TableCellNode): string {
|
|
|
280
280
|
return children.length > 0 ? `<w:tcPr>${children.join("")}</w:tcPr>` : "";
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
function buildTableRowPropertiesXml(row: TableNode["rows"][number]): string {
|
|
284
|
+
const children: string[] = [];
|
|
285
|
+
if (row.gridBefore !== undefined) {
|
|
286
|
+
children.push(`<w:gridBefore w:val="${row.gridBefore}"/>`);
|
|
287
|
+
}
|
|
288
|
+
if (row.widthBefore) {
|
|
289
|
+
children.push(`<w:wBefore w:w="${row.widthBefore.value}" w:type="${row.widthBefore.type}"/>`);
|
|
290
|
+
}
|
|
291
|
+
if (row.gridAfter !== undefined) {
|
|
292
|
+
children.push(`<w:gridAfter w:val="${row.gridAfter}"/>`);
|
|
293
|
+
}
|
|
294
|
+
if (row.widthAfter) {
|
|
295
|
+
children.push(`<w:wAfter w:w="${row.widthAfter.value}" w:type="${row.widthAfter.type}"/>`);
|
|
296
|
+
}
|
|
297
|
+
if (children.length === 0) {
|
|
298
|
+
return "";
|
|
299
|
+
}
|
|
300
|
+
return `<w:trPr>${children.join("")}</w:trPr>`;
|
|
301
|
+
}
|
|
302
|
+
|
|
283
303
|
function serializeSdtNode(
|
|
284
304
|
block: SdtNode,
|
|
285
305
|
state: SerializationState,
|
|
@@ -211,6 +211,10 @@ function normalizeTableRow(
|
|
|
211
211
|
return {
|
|
212
212
|
type: "table_row",
|
|
213
213
|
...(row.propertiesXml ? { propertiesXml: row.propertiesXml } : {}),
|
|
214
|
+
...(row.gridBefore !== undefined ? { gridBefore: row.gridBefore } : {}),
|
|
215
|
+
...(row.widthBefore ? { widthBefore: row.widthBefore } : {}),
|
|
216
|
+
...(row.gridAfter !== undefined ? { gridAfter: row.gridAfter } : {}),
|
|
217
|
+
...(row.widthAfter ? { widthAfter: row.widthAfter } : {}),
|
|
214
218
|
cells,
|
|
215
219
|
};
|
|
216
220
|
}
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
ParagraphIndentation,
|
|
8
8
|
TabStop,
|
|
9
9
|
TableLook,
|
|
10
|
+
TableWidth,
|
|
10
11
|
SectionProperties,
|
|
11
12
|
PageSize,
|
|
12
13
|
PageMargins,
|
|
@@ -309,6 +310,10 @@ export interface ParsedTableRowNode {
|
|
|
309
310
|
type: "table_row";
|
|
310
311
|
propertiesXml?: string;
|
|
311
312
|
cells: ParsedTableCellNode[];
|
|
313
|
+
gridBefore?: number;
|
|
314
|
+
widthBefore?: TableWidth;
|
|
315
|
+
gridAfter?: number;
|
|
316
|
+
widthAfter?: TableWidth;
|
|
312
317
|
rawXml: string;
|
|
313
318
|
}
|
|
314
319
|
|
|
@@ -843,6 +848,10 @@ function parseTableRowElement(
|
|
|
843
848
|
sourcePartPath: string,
|
|
844
849
|
): ParsedTableRowNode {
|
|
845
850
|
let propertiesXml: string | undefined;
|
|
851
|
+
let gridBefore: number | undefined;
|
|
852
|
+
let widthBefore: TableWidth | undefined;
|
|
853
|
+
let gridAfter: number | undefined;
|
|
854
|
+
let widthAfter: TableWidth | undefined;
|
|
846
855
|
const cells: ParsedTableCellNode[] = [];
|
|
847
856
|
|
|
848
857
|
for (const child of node.children) {
|
|
@@ -851,6 +860,10 @@ function parseTableRowElement(
|
|
|
851
860
|
switch (localName(child.name)) {
|
|
852
861
|
case "trPr":
|
|
853
862
|
propertiesXml = sourceXml.slice(child.start, child.end);
|
|
863
|
+
gridBefore = readTableRowGridPosition(child, "gridBefore");
|
|
864
|
+
widthBefore = readTableRowWidth(child, "wBefore");
|
|
865
|
+
gridAfter = readTableRowGridPosition(child, "gridAfter");
|
|
866
|
+
widthAfter = readTableRowWidth(child, "wAfter");
|
|
854
867
|
break;
|
|
855
868
|
case "tc":
|
|
856
869
|
cells.push(parseTableCellElement(child, sourceXml, relationshipMap, relationships, mediaParts, sourcePartPath));
|
|
@@ -861,6 +874,10 @@ function parseTableRowElement(
|
|
|
861
874
|
return {
|
|
862
875
|
type: "table_row",
|
|
863
876
|
...(propertiesXml ? { propertiesXml } : {}),
|
|
877
|
+
...(gridBefore !== undefined ? { gridBefore } : {}),
|
|
878
|
+
...(widthBefore ? { widthBefore } : {}),
|
|
879
|
+
...(gridAfter !== undefined ? { gridAfter } : {}),
|
|
880
|
+
...(widthAfter ? { widthAfter } : {}),
|
|
864
881
|
cells,
|
|
865
882
|
rawXml: sourceXml.slice(node.start, node.end),
|
|
866
883
|
};
|
|
@@ -1185,14 +1202,13 @@ function tableRequiresOpaquePreservation(rawXml: string): boolean {
|
|
|
1185
1202
|
// nested tables, floating images, VML preview atoms, and bounded field
|
|
1186
1203
|
// families already owned by the current field slice. Risky table-local
|
|
1187
1204
|
// semantics still fail closed to preserve-only.
|
|
1188
|
-
if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag|
|
|
1205
|
+
if (/<w:(ins|del|rPrChange|pPrChange|tblPrChange|trPrChange|tcPrChange|sectPrChange|cellIns|cellDel|cellMerge|smartTag|tblCellSpacing)\b/.test(rawXml)) {
|
|
1189
1206
|
return true;
|
|
1190
1207
|
}
|
|
1191
1208
|
|
|
1192
|
-
const
|
|
1193
|
-
const complexInstructions =
|
|
1194
|
-
|
|
1195
|
-
for (const instruction of [...fldSimpleInstructions, ...complexInstructions]) {
|
|
1209
|
+
const simpleInstructions = [...rawXml.matchAll(/\bw:instr="([^"]*)"/g)].map((match) => match[1] ?? "");
|
|
1210
|
+
const complexInstructions = extractComplexFieldInstructions(rawXml);
|
|
1211
|
+
for (const instruction of [...simpleInstructions, ...complexInstructions]) {
|
|
1196
1212
|
const classification = classifyFieldInstruction(instruction);
|
|
1197
1213
|
if (!isSafeMainStoryTableFieldFamily(classification.family)) {
|
|
1198
1214
|
return true;
|
|
@@ -1213,6 +1229,69 @@ function isSafeMainStoryTableFieldFamily(family: string): boolean {
|
|
|
1213
1229
|
);
|
|
1214
1230
|
}
|
|
1215
1231
|
|
|
1232
|
+
function extractComplexFieldInstructions(rawXml: string): string[] {
|
|
1233
|
+
const tokenPattern =
|
|
1234
|
+
/<(?:\w+:)?fldChar\b[^>]*?(?:\w+:)?fldCharType="(begin|separate|end)"[^>]*?(?:\/>|>[\s\S]*?<\/(?:\w+:)?fldChar>)|<(?:\w+:)?instrText\b[^>]*>([\s\S]*?)<\/(?:\w+:)?instrText>/gu;
|
|
1235
|
+
const instructions: string[] = [];
|
|
1236
|
+
let activeInstruction = "";
|
|
1237
|
+
let capturingInstruction = false;
|
|
1238
|
+
|
|
1239
|
+
for (const match of rawXml.matchAll(tokenPattern)) {
|
|
1240
|
+
const fldCharType = match[1];
|
|
1241
|
+
const instrText = match[2];
|
|
1242
|
+
if (fldCharType === "begin") {
|
|
1243
|
+
activeInstruction = "";
|
|
1244
|
+
capturingInstruction = true;
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (fldCharType === "separate" || fldCharType === "end") {
|
|
1248
|
+
if (capturingInstruction && activeInstruction.trim().length > 0) {
|
|
1249
|
+
instructions.push(activeInstruction);
|
|
1250
|
+
}
|
|
1251
|
+
activeInstruction = "";
|
|
1252
|
+
capturingInstruction = false;
|
|
1253
|
+
continue;
|
|
1254
|
+
}
|
|
1255
|
+
if (capturingInstruction && instrText !== undefined) {
|
|
1256
|
+
activeInstruction += decodeXmlEntities(instrText);
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
return instructions;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function readTableRowGridPosition(
|
|
1264
|
+
node: XmlElementNode,
|
|
1265
|
+
local: "gridBefore" | "gridAfter",
|
|
1266
|
+
): number | undefined {
|
|
1267
|
+
const positionNode = node.children.find(
|
|
1268
|
+
(child): child is XmlElementNode => child.type === "element" && localName(child.name) === local,
|
|
1269
|
+
);
|
|
1270
|
+
if (!positionNode) return undefined;
|
|
1271
|
+
const raw = positionNode.attributes["w:val"] ?? positionNode.attributes.val;
|
|
1272
|
+
const value = Number.parseInt(raw ?? "", 10);
|
|
1273
|
+
return Number.isFinite(value) && value > 0 ? value : 0;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function readTableRowWidth(
|
|
1277
|
+
node: XmlElementNode,
|
|
1278
|
+
local: "wBefore" | "wAfter",
|
|
1279
|
+
): TableWidth | undefined {
|
|
1280
|
+
const widthNode = node.children.find(
|
|
1281
|
+
(child): child is XmlElementNode => child.type === "element" && localName(child.name) === local,
|
|
1282
|
+
);
|
|
1283
|
+
if (!widthNode) return undefined;
|
|
1284
|
+
const rawValue = widthNode.attributes["w:w"] ?? widthNode.attributes.w;
|
|
1285
|
+
const value = Number.parseInt(rawValue ?? "", 10);
|
|
1286
|
+
if (!Number.isFinite(value)) {
|
|
1287
|
+
return undefined;
|
|
1288
|
+
}
|
|
1289
|
+
const rawType = (widthNode.attributes["w:type"] ?? widthNode.attributes.type ?? "dxa").toLowerCase();
|
|
1290
|
+
const type: TableWidth["type"] =
|
|
1291
|
+
rawType === "auto" ? "auto" : rawType === "pct" ? "pct" : rawType === "nil" ? "nil" : "dxa";
|
|
1292
|
+
return { value, type };
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1216
1295
|
function readCellGridSpan(node: XmlElementNode): number | undefined {
|
|
1217
1296
|
const gridSpanNode = node.children.find(
|
|
1218
1297
|
(child): child is XmlElementNode => child.type === "element" && localName(child.name) === "gridSpan",
|
|
@@ -1801,9 +1880,11 @@ function parseRevisionContainer(
|
|
|
1801
1880
|
},
|
|
1802
1881
|
];
|
|
1803
1882
|
case "permStart":
|
|
1804
|
-
|
|
1883
|
+
result.push(parsePermStartNode(child, sourceXml));
|
|
1884
|
+
break;
|
|
1805
1885
|
case "permEnd":
|
|
1806
|
-
|
|
1886
|
+
result.push(parsePermEndNode(child, sourceXml));
|
|
1887
|
+
break;
|
|
1807
1888
|
default:
|
|
1808
1889
|
return [
|
|
1809
1890
|
{
|
|
@@ -417,6 +417,10 @@ export interface TableRowNode {
|
|
|
417
417
|
type: "table_row";
|
|
418
418
|
propertiesXml?: string;
|
|
419
419
|
cells: TableCellNode[];
|
|
420
|
+
gridBefore?: number;
|
|
421
|
+
widthBefore?: TableWidth;
|
|
422
|
+
gridAfter?: number;
|
|
423
|
+
widthAfter?: TableWidth;
|
|
420
424
|
height?: number;
|
|
421
425
|
heightRule?: "auto" | "atLeast" | "exact";
|
|
422
426
|
isHeader?: boolean;
|
|
@@ -943,6 +947,23 @@ export interface RevisionMoveData {
|
|
|
943
947
|
direction: "from" | "to";
|
|
944
948
|
}
|
|
945
949
|
|
|
950
|
+
export type RevisionStoryTargetRecord =
|
|
951
|
+
| { kind: "main" }
|
|
952
|
+
| {
|
|
953
|
+
kind: "header";
|
|
954
|
+
relationshipId: string;
|
|
955
|
+
variant: "default" | "first" | "even";
|
|
956
|
+
sectionIndex?: number;
|
|
957
|
+
}
|
|
958
|
+
| {
|
|
959
|
+
kind: "footer";
|
|
960
|
+
relationshipId: string;
|
|
961
|
+
variant: "default" | "first" | "even";
|
|
962
|
+
sectionIndex?: number;
|
|
963
|
+
}
|
|
964
|
+
| { kind: "footnote"; noteId: string }
|
|
965
|
+
| { kind: "endnote"; noteId: string };
|
|
966
|
+
|
|
946
967
|
export interface RevisionRecord {
|
|
947
968
|
changeId: string;
|
|
948
969
|
kind: "insertion" | "deletion" | "formatting" | "move" | "property-change";
|
|
@@ -956,6 +977,7 @@ export interface RevisionRecord {
|
|
|
956
977
|
|
|
957
978
|
export interface RevisionMetadataRecord {
|
|
958
979
|
source?: "runtime" | "import";
|
|
980
|
+
storyTarget?: RevisionStoryTargetRecord;
|
|
959
981
|
preserveOnlyReason?: string;
|
|
960
982
|
importedRevisionForm?:
|
|
961
983
|
| "run-insertion"
|
|
@@ -81,6 +81,7 @@ export function createRevisionRecord(
|
|
|
81
81
|
warningIds: [...(params.warningIds ?? [])],
|
|
82
82
|
metadata: {
|
|
83
83
|
source: params.metadata?.source ?? "runtime",
|
|
84
|
+
storyTarget: params.metadata?.storyTarget,
|
|
84
85
|
preserveOnlyReason:
|
|
85
86
|
params.metadata?.preserveOnlyReason ??
|
|
86
87
|
(getRevisionActionability(params.kind) === "preserve-only"
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type EditorAnchorProjection,
|
|
10
10
|
type TransactionMapping,
|
|
11
11
|
} from "../../core/selection/mapping.ts";
|
|
12
|
+
import type { RevisionStoryTargetRecord } from "../../model/canonical-document.ts";
|
|
12
13
|
|
|
13
14
|
export type RevisionAnchor = EditorAnchorProjection;
|
|
14
15
|
|
|
@@ -33,6 +34,7 @@ export interface MoveData {
|
|
|
33
34
|
|
|
34
35
|
export interface RevisionMetadataEnvelope {
|
|
35
36
|
source: "runtime" | "import";
|
|
37
|
+
storyTarget?: RevisionStoryTargetRecord;
|
|
36
38
|
preserveOnlyReason?: string;
|
|
37
39
|
importedRevisionForm?:
|
|
38
40
|
| "run-insertion"
|