@stll/folio-core 0.47.5 → 0.47.7

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.
@@ -82,6 +82,12 @@ const NOTE_TYPES = ["footnote", "endnote"];
82
82
  const NOTE_REF_VERT_ALIGNS = ["baseline", "superscript"];
83
83
  const TEXT_BOX_DOCX_PLACEMENTS = ["standalone", "inlineWithPrevious"];
84
84
  const TRACKED_CHANGE_MOVE_KINDS = ["moveTo", "moveFrom"];
85
+ const TRACKED_REVISION_ANCESTOR_TYPES = [
86
+ "insertion",
87
+ "deletion",
88
+ "moveFrom",
89
+ "moveTo"
90
+ ];
85
91
  const TEXT_BOX_TRACKED_CHANGE_TYPES = [
86
92
  "insertion",
87
93
  "deletion",
@@ -792,6 +798,7 @@ const readCommentMarkAttrs = (mark) => {
792
798
  return attrsResult(attrs, issues);
793
799
  };
794
800
  const expectCommentMarkAttrs = (mark) => expectCachedMarkAttrs(mark, commentAttrsCache, readCommentMarkAttrs, "comment attrs");
801
+ const isTrackedRevisionAncestorArray = (value) => Array.isArray(value) && value.length > 0 && value.every((ancestor) => isRecord(ancestor) && TRACKED_REVISION_ANCESTOR_TYPES.some((type) => type === ancestor["type"]) && typeof ancestor["revisionId"] === "number" && Number.isSafeInteger(ancestor["revisionId"]) && ancestor["revisionId"] >= 0 && typeof ancestor["author"] === "string" && (ancestor["date"] === void 0 || typeof ancestor["date"] === "string") && (ancestor["utcDate"] === void 0 || typeof ancestor["utcDate"] === "string") && (ancestor["initials"] === void 0 || typeof ancestor["initials"] === "string") && typeof ancestor["outerWrapperCount"] === "number" && Number.isSafeInteger(ancestor["outerWrapperCount"]) && ancestor["outerWrapperCount"] >= 0);
795
802
  const readTrackedChangeMarkAttrs = (mark) => {
796
803
  const attrs = attrsRecord(mark.attrs);
797
804
  const issues = [];
@@ -801,6 +808,16 @@ const readTrackedChangeMarkAttrs = (mark) => {
801
808
  optionalString(attrs, "date", `${mark.type.name}.attrs.date`, issues);
802
809
  optionalString(attrs, "utcDate", `${mark.type.name}.attrs.utcDate`, issues);
803
810
  optionalString(attrs, "initials", `${mark.type.name}.attrs.initials`, issues);
811
+ const outerWrapperCount = attrs["_docxOuterWrapperCount"];
812
+ if (outerWrapperCount !== void 0 && outerWrapperCount !== null && (typeof outerWrapperCount !== "number" || !Number.isSafeInteger(outerWrapperCount) || outerWrapperCount < 0)) issues.push({
813
+ path: `${mark.type.name}.attrs._docxOuterWrapperCount`,
814
+ message: "Expected a non-negative safe integer."
815
+ });
816
+ const ancestors = attrs["_docxRevisionAncestors"];
817
+ if (ancestors !== void 0 && ancestors !== null && !isTrackedRevisionAncestorArray(ancestors)) issues.push({
818
+ path: `${mark.type.name}.attrs._docxRevisionAncestors`,
819
+ message: "Expected a non-empty array of valid revision ancestors."
820
+ });
804
821
  optionalOneOf(attrs, "moveKind", `${mark.type.name}.attrs.moveKind`, issues, TRACKED_CHANGE_MOVE_KINDS);
805
822
  optionalOneOf(attrs, "provenance", `${mark.type.name}.attrs.provenance`, issues, TRACKED_CHANGE_PROVENANCE_VALUES);
806
823
  optionalString(attrs, "suggestionId", `${mark.type.name}.attrs.suggestionId`, issues);
@@ -2700,4 +2717,4 @@ const optionalEmptyHyperlinkArray = (value, issues) => {
2700
2717
  }
2701
2718
  };
2702
2719
  //#endregion
2703
- export { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectInlineWrapperMarkAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectPageBreakRunAttrs, expectParagraphAttrs, expectPreservedBlockAttrs, expectPreservedXmlAttrs, expectRunFormattingOverrideMarkAttrs, expectRunIdentityMarkAttrs, expectRunPropertyChangeMarkAttrs, expectRunShadingMarkAttrs, expectSdtAttrs, expectShapeAttrs, expectStrikeMarkAttrs, expectSymbolAttrs, expectTabAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs, mergeImageAttrs, mergeParagraphAttrs, mergeTableAttrs, mergeTableCellAttrs, mergeTableRowAttrs, readBlockSdtAttrs, readCharacterSpacingMarkAttrs, readCharacterStyleMarkAttrs, readCommentMarkAttrs, readEmphasisMarkAttrs, readFieldAttrs, readFontFamilyMarkAttrs, readFontSizeMarkAttrs, readFootnoteRefMarkAttrs, readHardBreakAttrs, readHighlightMarkAttrs, readHyperlinkMarkAttrs, readImageAttrs, readInlineWrapperMarkAttrs, readInlineWrapperStack, readLanguageMarkAttrs, readMathAttrs, readPageBreakRunAttrs, readParagraphAttrs, readPreservedBlockAttrs, readPreservedXmlAttrs, readRunFormattingOverrideMarkAttrs, readRunIdentityMarkAttrs, readRunPropertyChangeMarkAttrs, readRunShadingMarkAttrs, readSdtAttrs, readShapeAttrs, readStrikeMarkAttrs, readSymbolAttrs, readTabAttrs, readTableAttrs, readTableCellAttrs, readTableRowAttrs, readTextBoxAttrs, readTextColorMarkAttrs, readTextEffectMarkAttrs, readTrackedChangeMarkAttrs, readUnderlineMarkAttrs };
2720
+ export { expectBlockSdtAttrs, expectCharacterSpacingMarkAttrs, expectCharacterStyleMarkAttrs, expectCommentMarkAttrs, expectEmphasisMarkAttrs, expectFieldAttrs, expectFontFamilyMarkAttrs, expectFontSizeMarkAttrs, expectFootnoteRefMarkAttrs, expectHardBreakAttrs, expectHighlightMarkAttrs, expectHyperlinkMarkAttrs, expectImageAttrs, expectInlineWrapperMarkAttrs, expectLanguageMarkAttrs, expectMathAttrs, expectPageBreakRunAttrs, expectParagraphAttrs, expectPreservedBlockAttrs, expectPreservedXmlAttrs, expectRunFormattingOverrideMarkAttrs, expectRunIdentityMarkAttrs, expectRunPropertyChangeMarkAttrs, expectRunShadingMarkAttrs, expectSdtAttrs, expectShapeAttrs, expectStrikeMarkAttrs, expectSymbolAttrs, expectTabAttrs, expectTableAttrs, expectTableCellAttrs, expectTableRowAttrs, expectTextBoxAttrs, expectTextColorMarkAttrs, expectTextEffectMarkAttrs, expectTrackedChangeMarkAttrs, expectUnderlineMarkAttrs, isTrackedRevisionAncestorArray, mergeImageAttrs, mergeParagraphAttrs, mergeTableAttrs, mergeTableCellAttrs, mergeTableRowAttrs, readBlockSdtAttrs, readCharacterSpacingMarkAttrs, readCharacterStyleMarkAttrs, readCommentMarkAttrs, readEmphasisMarkAttrs, readFieldAttrs, readFontFamilyMarkAttrs, readFontSizeMarkAttrs, readFootnoteRefMarkAttrs, readHardBreakAttrs, readHighlightMarkAttrs, readHyperlinkMarkAttrs, readImageAttrs, readInlineWrapperMarkAttrs, readInlineWrapperStack, readLanguageMarkAttrs, readMathAttrs, readPageBreakRunAttrs, readParagraphAttrs, readPreservedBlockAttrs, readPreservedXmlAttrs, readRunFormattingOverrideMarkAttrs, readRunIdentityMarkAttrs, readRunPropertyChangeMarkAttrs, readRunShadingMarkAttrs, readSdtAttrs, readShapeAttrs, readStrikeMarkAttrs, readSymbolAttrs, readTabAttrs, readTableAttrs, readTableCellAttrs, readTableRowAttrs, readTextBoxAttrs, readTextColorMarkAttrs, readTextEffectMarkAttrs, readTrackedChangeMarkAttrs, readUnderlineMarkAttrs };
@@ -2,7 +2,7 @@ import { sameStatedParagraphNumbering as sameStatedParagraphNumbering$1 } from "
2
2
  import { joinProseParagraphsWithRightPropertySource } from "../../docx/paragraphPropertySource.js";
3
3
  import { appendHeadlessInlineResolution } from "../../internal/headlessRevisionResolution.js";
4
4
  import { stateAllowsHeadlessRevisionResolution } from "../../internal/headlessRevisionResolutionGuard.js";
5
- import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../attrs/index.js";
5
+ import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
6
6
  import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../containerFinalParagraph.js";
7
7
  import { resolveParagraphDefaultTextFormatting } from "../conversion/toProseDoc.js";
8
8
  import { markChangedParagraphRanges, markStructuralChange, markTrackedSectionEndpointRemoval } from "../extensions/features/ParagraphChangeTrackerExtension.js";
@@ -19,6 +19,7 @@ import { reconcileTableGridAfterColumnRemoval } from "../tableGridMutation.js";
19
19
  import { holdsNoContent } from "../zeroWidthAnchors.js";
20
20
  import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot, paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, removeParagraphPropertyChanges, sectionRejectProperties, tableCellRejectAttrPatch, tablePropertyExceptionsRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch } from "./propertyChangeScope.js";
21
21
  import { hasMatchingCollapsedTableCellMerge, resolveCollapsedTableCellMerge, resolveVisibleTableCellMerge } from "./tableCellMergeResolution.js";
22
+ import { panic } from "better-result";
22
23
  import { PARAGRAPH_MARK_CHANGE_KINDS, REVIEW_CARRIERS } from "@stll/docx-core/model";
23
24
  import { TableMap, removeRow } from "prosemirror-tables";
24
25
  import { Mapping } from "prosemirror-transform";
@@ -55,6 +56,33 @@ function removeCommentMark(commentId) {
55
56
  return true;
56
57
  };
57
58
  }
59
+ const revisionLayerOf = (mark) => {
60
+ const attrs = expectTrackedChangeMarkAttrs(mark);
61
+ let type;
62
+ if (mark.type.name === "insertion") type = attrs.moveKind === "moveTo" ? "moveTo" : "insertion";
63
+ else type = attrs.moveKind === "moveFrom" ? "moveFrom" : "deletion";
64
+ return {
65
+ type,
66
+ revisionId: attrs.revisionId,
67
+ author: attrs.author,
68
+ ...attrs.date ? { date: attrs.date } : {},
69
+ ...attrs.utcDate ? { utcDate: attrs.utcDate } : {},
70
+ ...attrs.initials ? { initials: attrs.initials } : {},
71
+ outerWrapperCount: attrs._docxOuterWrapperCount ?? 0
72
+ };
73
+ };
74
+ const revisionLayerRemovesContent = (layer, mode) => mode === "accept" ? layer.type === "deletion" || layer.type === "moveFrom" : layer.type === "insertion" || layer.type === "moveTo";
75
+ const containsNestedRevision = (doc) => {
76
+ let found = false;
77
+ doc.descendants((node) => {
78
+ if (found) return false;
79
+ for (const mark of node.marks) if ((mark.type.name === "insertion" || mark.type.name === "deletion") && (expectTrackedChangeMarkAttrs(mark)._docxRevisionAncestors?.length ?? 0) > 0) {
80
+ found = true;
81
+ return false;
82
+ }
83
+ });
84
+ return found;
85
+ };
58
86
  /**
59
87
  * Resolve a tracked change: accept or reject.
60
88
  * - Accept: keep insertions (remove mark), delete deletions (remove text)
@@ -76,7 +104,7 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
76
104
  const removeType = mode === "accept" ? deletionType : insertionType;
77
105
  const revisionSet = revisionIds === void 0 ? null : new Set(revisionIds);
78
106
  const removeKeptMarksInBulk = revisionSet === null && keepType !== void 0;
79
- const canBulkInlineResolution = execution === "headless-bulk-inline" && revisionSet === null && from === 0 && to === state.doc.content.size && stateAllowsHeadlessRevisionResolution(state);
107
+ const canBulkInlineResolution = execution === "headless-bulk-inline" && revisionSet === null && from === 0 && to === state.doc.content.size && stateAllowsHeadlessRevisionResolution(state) && !containsNestedRevision(state.doc);
80
108
  const matchesRevision = (mark) => revisionSet === null || typeof mark.attrs["revisionId"] === "number" && revisionSet.has(mark.attrs["revisionId"]);
81
109
  if (dispatch) {
82
110
  const tr = state.tr;
@@ -189,6 +217,36 @@ function resolveChange(from, to, mode, revisionIds, execution = "legacy") {
189
217
  revisionSet,
190
218
  styleResolver
191
219
  });
220
+ const nestedMark = node.marks.find((mark) => (mark.type === insertionType || mark.type === deletionType) && (expectTrackedChangeMarkAttrs(mark)._docxRevisionAncestors?.length ?? 0) > 0);
221
+ if (nestedMark) {
222
+ const path = [...expectTrackedChangeMarkAttrs(nestedMark)._docxRevisionAncestors ?? [], revisionLayerOf(nestedMark)];
223
+ const selected = (layer) => revisionSet === null || revisionSet.has(layer.revisionId);
224
+ if (path.some(selected)) if (path.some((layer) => selected(layer) && revisionLayerRemovesContent(layer, mode))) deleteRanges.push({
225
+ from: rangeFrom,
226
+ to: rangeTo
227
+ });
228
+ else {
229
+ tr.removeMark(rangeFrom, rangeTo, nestedMark);
230
+ const remaining = path.filter((layer) => !selected(layer));
231
+ const active = remaining.at(-1);
232
+ if (active) {
233
+ const type = active.type === "insertion" || active.type === "moveTo" ? insertionType : deletionType;
234
+ if (!type) panic("A nested tracked revision has no editor mark type");
235
+ tr.addMark(rangeFrom, rangeTo, type.create({
236
+ revisionId: active.revisionId,
237
+ author: active.author,
238
+ date: active.date ?? null,
239
+ utcDate: active.utcDate ?? null,
240
+ initials: active.initials ?? null,
241
+ moveKind: active.type === "moveTo" || active.type === "moveFrom" ? active.type : null,
242
+ _historicalFormatting: active.type === "deletion" || active.type === "moveFrom" ? true : null,
243
+ _docxOuterWrapperCount: active.outerWrapperCount,
244
+ _docxRevisionAncestors: remaining.length > 1 ? remaining.slice(0, -1) : null
245
+ }));
246
+ }
247
+ }
248
+ return true;
249
+ }
192
250
  if (removesNode) deleteRanges.push({
193
251
  from: rangeFrom,
194
252
  to: rangeTo
@@ -794,7 +852,7 @@ function findAIEditRevisionRange(state, revisionIds) {
794
852
  includeRange(pos, pos + node.nodeSize);
795
853
  break;
796
854
  }
797
- if ((mark.type === insertionType || mark.type === deletionType) && typeof mark.attrs["revisionId"] === "number" && idSet.has(mark.attrs["revisionId"])) {
855
+ if ((mark.type === insertionType || mark.type === deletionType) && (idSet.has(expectTrackedChangeMarkAttrs(mark).revisionId) || (expectTrackedChangeMarkAttrs(mark)._docxRevisionAncestors ?? []).some((ancestor) => idSet.has(ancestor.revisionId)))) {
798
856
  includeRange(pos, pos + node.nodeSize);
799
857
  break;
800
858
  }
@@ -1162,12 +1162,10 @@ const nestInlineWrappers = (stack, content) => {
1162
1162
  * The emitted sequence with each wrapper group nested back into the wrappers
1163
1163
  * `toProseDoc` lifted off it.
1164
1164
  *
1165
- * The revision stays outermost. folio already writes a revision outside the
1166
- * hyperlink it spans, the parse leg is revision-owned, and accepting or
1167
- * rejecting one is a range operation over the revision's own content; a
1168
- * wrapper placed outside two revisions would also have to mint one revision id
1169
- * per wrapper. So a revision in the group keeps its place and takes the nest
1170
- * inside it, and the items around it share a nest of their own.
1165
+ * A revision may be inside some wrapper layers and outside others. Its mark
1166
+ * records how many layers preceded it in the source, since a ProseMirror mark
1167
+ * set cannot express that order. Adjacent items with the same outer layers
1168
+ * share one wrapper rather than splitting it around each revision.
1171
1169
  *
1172
1170
  * A group with nothing in it writes no wrapper: an authored wrapper whose
1173
1171
  * content was deleted or rejected is gone, and an empty one says nothing.
@@ -1185,10 +1183,83 @@ const nestInsideHyperlink = (item, stack, hyperlinkIndex, indices) => {
1185
1183
  }
1186
1184
  return false;
1187
1185
  };
1188
- const nestInlineWrapperGroups = (items, groups, hyperlinkIndices) => {
1186
+ const nestRevisionAwareInlineWrappers = (items, stack, revisionOuterWrapperCounts, layerIndex = 0) => {
1187
+ if (layerIndex === stack.length) return [...items];
1188
+ const nested = [];
1189
+ let pending = [];
1190
+ const closeLayer = () => {
1191
+ if (pending.length === 0) return;
1192
+ const layer = stack[layerIndex];
1193
+ if (layer === void 0) panic("An inline wrapper layer disappeared during reconstruction");
1194
+ nested.push(inlineWrapperMember(layer, nestRevisionAwareInlineWrappers(pending, stack, revisionOuterWrapperCounts, layerIndex + 1)));
1195
+ pending = [];
1196
+ };
1197
+ for (const item of items) {
1198
+ if (!isRevisionWrapper(item) || (revisionOuterWrapperCounts.get(item) ?? 0) > layerIndex) {
1199
+ pending.push(item);
1200
+ continue;
1201
+ }
1202
+ closeLayer();
1203
+ item.content = [nestInlineWrappers(stack.slice(layerIndex), item.content)];
1204
+ nested.push(item);
1205
+ }
1206
+ closeLayer();
1207
+ return nested;
1208
+ };
1209
+ const sameRevisionLayer = (left, right) => left.type === right.type && left.revisionId === right.revisionId && left.author === right.author && left.date === right.date && left.utcDate === right.utcDate && left.initials === right.initials && left.outerWrapperCount === right.outerWrapperCount;
1210
+ const revisionInfoFromLayer = (layer) => ({
1211
+ id: layer.revisionId,
1212
+ author: layer.author,
1213
+ ...layer.date ? { date: layer.date } : {},
1214
+ ...layer.utcDate ? { utcDate: {
1215
+ attribute: DATE_UTC_ATTRIBUTE,
1216
+ value: layer.utcDate
1217
+ } } : {},
1218
+ ...layer.initials ? { initials: layer.initials } : {}
1219
+ });
1220
+ const nestRevisionAncestors = (items, ancestorsByWrapper, revisionOuterWrapperCounts) => {
1221
+ const nested = [];
1222
+ const open = [];
1223
+ for (const item of items) {
1224
+ if (!isRevisionWrapper(item)) {
1225
+ open.length = 0;
1226
+ nested.push(item);
1227
+ continue;
1228
+ }
1229
+ const ownLayer = {
1230
+ type: item.type,
1231
+ revisionId: item.info.id,
1232
+ author: item.info.author,
1233
+ ...item.info.date ? { date: item.info.date } : {},
1234
+ ...item.info.utcDate ? { utcDate: item.info.utcDate.value } : {},
1235
+ ...item.info.initials ? { initials: item.info.initials } : {},
1236
+ outerWrapperCount: revisionOuterWrapperCounts.get(item) ?? 0
1237
+ };
1238
+ const path = [...ancestorsByWrapper.get(item) ?? [], ownLayer];
1239
+ let shared = 0;
1240
+ while (shared < open.length && path[shared] && sameRevisionLayer(open[shared].layer, path[shared])) shared += 1;
1241
+ open.length = shared;
1242
+ for (const layer of path.slice(shared)) {
1243
+ const wrapper = createTrackedRunWrapper(layer.type, revisionInfoFromLayer(layer));
1244
+ revisionOuterWrapperCounts.set(wrapper, layer.outerWrapperCount);
1245
+ const parent = open.at(-1)?.wrapper;
1246
+ if (parent) parent.content.push(wrapper);
1247
+ else nested.push(wrapper);
1248
+ open.push({
1249
+ layer,
1250
+ wrapper
1251
+ });
1252
+ }
1253
+ const owner = open.at(-1)?.wrapper;
1254
+ if (!owner) panic("A tracked revision path has no owner");
1255
+ owner.content.push(...item.content);
1256
+ }
1257
+ return nested;
1258
+ };
1259
+ const nestInlineWrapperGroups = (items, groups, hyperlinkIndices, revisionOuterWrapperCounts, ancestorsByWrapper) => {
1189
1260
  const nested = [];
1190
1261
  for (const [index, group] of groups.entries()) {
1191
- const slice = items.slice(group.start, groups[index + 1]?.start ?? items.length);
1262
+ const slice = nestRevisionAncestors(items.slice(group.start, groups[index + 1]?.start ?? items.length), ancestorsByWrapper, revisionOuterWrapperCounts);
1192
1263
  if (group.stack.length === 0) {
1193
1264
  nested.push(...slice);
1194
1265
  continue;
@@ -1205,24 +1276,18 @@ const nestInlineWrapperGroups = (items, groups, hyperlinkIndices) => {
1205
1276
  nested.push(...slice);
1206
1277
  continue;
1207
1278
  }
1208
- let pending = [];
1209
- const closeNest = () => {
1210
- if (pending.length === 0) return;
1211
- nested.push(nestInlineWrappers(outerStack, pending));
1212
- pending = [];
1213
- };
1214
- for (const item of slice) {
1215
- if (!isRevisionWrapper(item)) {
1216
- pending.push(item);
1217
- continue;
1218
- }
1219
- closeNest();
1220
- if (item.content.length > 0) item.content = [nestInlineWrappers(outerStack, item.content)];
1221
- nested.push(item);
1279
+ nested.push(...nestRevisionAwareInlineWrappers(slice, outerStack, revisionOuterWrapperCounts));
1280
+ }
1281
+ const coalesced = [];
1282
+ for (const item of nested) {
1283
+ const previous = coalesced.at(-1);
1284
+ if (previous && isRevisionWrapper(previous) && isRevisionWrapper(item) && previous.type === item.type && JSON.stringify(previous.info) === JSON.stringify(item.info) && revisionOuterWrapperCounts.get(previous) === revisionOuterWrapperCounts.get(item)) {
1285
+ previous.content.push(...item.content);
1286
+ continue;
1222
1287
  }
1223
- closeNest();
1288
+ coalesced.push(item);
1224
1289
  }
1225
- return nested;
1290
+ return coalesced;
1226
1291
  };
1227
1292
  function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, textBoxAnchorMarkers, skipLeadingRenderedPageBreak = false, inheritedFormattingOverride) {
1228
1293
  const content = [];
@@ -1252,6 +1317,8 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1252
1317
  let currentTrackedChange;
1253
1318
  const sourceRunOwners = /* @__PURE__ */ new WeakMap();
1254
1319
  const sourceHyperlinkIndices = /* @__PURE__ */ new WeakMap();
1320
+ const revisionOuterWrapperCounts = /* @__PURE__ */ new WeakMap();
1321
+ const revisionAncestorsByWrapper = /* @__PURE__ */ new WeakMap();
1255
1322
  const openedComments = /* @__PURE__ */ new Set();
1256
1323
  const createIndexedHyperlink = (mark) => {
1257
1324
  const hyperlink = createHyperlink(mark);
@@ -1416,16 +1483,13 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1416
1483
  } } : {},
1417
1484
  ...changeAttrs.initials ? { initials: changeAttrs.initials } : {}
1418
1485
  };
1419
- if (insertionMark) content.push({
1420
- type: changeAttrs.moveKind === "moveTo" ? "moveTo" : "insertion",
1421
- info,
1422
- content: [anchoredContent]
1423
- });
1424
- else content.push({
1425
- type: changeAttrs.moveKind === "moveFrom" ? "moveFrom" : "deletion",
1426
- info,
1427
- content: [anchoredContent]
1428
- });
1486
+ let type;
1487
+ if (insertionMark) type = changeAttrs.moveKind === "moveTo" ? "moveTo" : "insertion";
1488
+ else type = changeAttrs.moveKind === "moveFrom" ? "moveFrom" : "deletion";
1489
+ const wrapper = createTrackedRunWrapper(type, info, anchoredContent);
1490
+ revisionOuterWrapperCounts.set(wrapper, changeAttrs._docxOuterWrapperCount ?? inlineWrapperStackOf(node).length);
1491
+ revisionAncestorsByWrapper.set(wrapper, changeAttrs._docxRevisionAncestors ?? []);
1492
+ content.push(wrapper);
1429
1493
  return;
1430
1494
  }
1431
1495
  if (insertionMark || deletionMark) {
@@ -1447,12 +1511,15 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1447
1511
  let type;
1448
1512
  if (insertionMark) type = changeAttrs.moveKind === "moveTo" ? "moveTo" : "insertion";
1449
1513
  else type = changeAttrs.moveKind === "moveFrom" ? "moveFrom" : "deletion";
1450
- const trackedChangeKey = `${type}:${JSON.stringify(info)}`;
1514
+ const outerWrapperCount = changeAttrs._docxOuterWrapperCount ?? inlineWrapperStackOf(node).length;
1515
+ const trackedChangeKey = `${type}:${JSON.stringify(info)}:${outerWrapperCount}:${JSON.stringify(changeAttrs._docxRevisionAncestors ?? [])}`;
1451
1516
  if (linkMark) {
1452
1517
  const linkKey = getLinkKey(linkMark);
1453
1518
  if (!currentTrackedChange || currentTrackedChange.type !== "hyperlink" || currentTrackedChange.key !== trackedChangeKey || currentTrackedChange.hyperlinkKey !== linkKey) {
1454
1519
  const hyperlink = createIndexedHyperlink(linkMark);
1455
1520
  const wrapper = createTrackedRunWrapper(type, info, hyperlink);
1521
+ revisionOuterWrapperCounts.set(wrapper, outerWrapperCount);
1522
+ revisionAncestorsByWrapper.set(wrapper, changeAttrs._docxRevisionAncestors ?? []);
1456
1523
  content.push(wrapper);
1457
1524
  currentTrackedChange = {
1458
1525
  type: "hyperlink",
@@ -1486,6 +1553,8 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1486
1553
  }
1487
1554
  if (!currentTrackedChange || currentTrackedChange.type !== "direct" || currentTrackedChange.key !== trackedChangeKey) {
1488
1555
  const wrapper = createTrackedRunWrapper(type, info);
1556
+ revisionOuterWrapperCounts.set(wrapper, outerWrapperCount);
1557
+ revisionAncestorsByWrapper.set(wrapper, changeAttrs._docxRevisionAncestors ?? []);
1489
1558
  content.push(wrapper);
1490
1559
  currentTrackedChange = {
1491
1560
  type: "direct",
@@ -1640,7 +1709,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1640
1709
  type: "commentRangeEnd",
1641
1710
  id: commentId
1642
1711
  });
1643
- return nestInlineWrapperGroups(content, wrapperGroups, sourceHyperlinkIndices);
1712
+ return nestInlineWrapperGroups(content, wrapperGroups, sourceHyperlinkIndices, revisionOuterWrapperCounts, revisionAncestorsByWrapper);
1644
1713
  }
1645
1714
  function createTrackedChangeRun({ baseParagraphFormatting, inheritedFormatting, marks, node, paragraphMarkFormatting, paragraphMarkPrecedesStyle, styleResolver }) {
1646
1715
  const formattingContext = {
@@ -2001,7 +2070,7 @@ function restoreRunRecord(run, marks) {
2001
2070
  }
2002
2071
  const identityMark = marks.find((mark) => mark.type.name === RUN_IDENTITY_MARK_NAME);
2003
2072
  if (!identityMark) return;
2004
- const { preservedAttributes, preserved } = expectRunIdentityMarkAttrs(identityMark);
2073
+ const { preservedAttributes, preserved, emptyFormatting } = expectRunIdentityMarkAttrs(identityMark);
2005
2074
  if (preservedAttributes && preservedAttributes.length > 0) run.preservedAttributes = preservedAttributes.map(({ namespace, name, value }) => namespace === void 0 ? {
2006
2075
  name,
2007
2076
  value
@@ -2014,6 +2083,7 @@ function restoreRunRecord(run, marks) {
2014
2083
  ...run.formatting,
2015
2084
  preserved
2016
2085
  };
2086
+ if (emptyFormatting && run.formatting === void 0) run.formatting = {};
2017
2087
  }
2018
2088
  function getRunFormattingFromMarks(marks, options) {
2019
2089
  if (!marks || marks.length === 0 && !options) return;
@@ -16,7 +16,7 @@ import { mergeParagraphFormatting, mergeParagraphTabStops } from "../../utils/pa
16
16
  import { tableOfContentsStyleLevel } from "../../utils/tableOfContentsStyle.js";
17
17
  import { mergeTextFormatting } from "../../utils/textFormattingMerge.js";
18
18
  import { emuToPixels, emuToStrokePixels } from "../../utils/units.js";
19
- import { expectInlineWrapperMarkAttrs } from "../attrs/index.js";
19
+ import { expectInlineWrapperMarkAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
20
20
  import { authoredTransformAttrs } from "../authoredTransformAttrs.js";
21
21
  import { setAutospacingBaseValue } from "../autospacingBase.js";
22
22
  import { planEmptyRanges } from "../emptyRangeAnchor.js";
@@ -77,16 +77,17 @@ const createTextBoxGroupIdFactory = () => {
77
77
  * paragraph's and the hyperlink's — so the next payload field is added once
78
78
  * rather than to whichever site the author happened to open.
79
79
  *
80
- * Minted when the run holds a page break (the leaves it is cut into have to be
81
- * rejoined into one `w:r` on save), an attribute remainder, or a `w:rPr` sink.
82
- * 57.5% of corpus files hold none of the three and mint nothing.
80
+ * Minted when the run holds a page break or multiple content items including
81
+ * an inline atom (the leaves have to be rejoined into one `w:r` on save), an
82
+ * attribute remainder, a `w:rPr` sink, or an authored empty `w:rPr`.
83
83
  */
84
84
  const runIdentityMark = (run, nextRunIdentityId) => {
85
85
  const payload = {
86
86
  preservedAttributes: run.preservedAttributes,
87
- preserved: run.formatting?.preserved
87
+ preserved: run.formatting?.preserved,
88
+ emptyFormatting: run.formatting !== void 0 && Object.keys(run.formatting).length === 0 ? true : void 0
88
89
  };
89
- if (!runHasPageBreakContent(run) && !hasRunIdentityPayload(payload)) return null;
90
+ if (!runHasPageBreakContent(run) && !runHasMixedContent(run) && !hasRunIdentityPayload(payload)) return null;
90
91
  return schema.mark(RUN_IDENTITY_MARK_NAME, runIdentityAttrs(nextRunIdentityId(), payload));
91
92
  };
92
93
  /** Keep imported hyperlink identity unique across every nested conversion scope. */
@@ -396,8 +397,8 @@ function convertParagraph(paragraph, styleResolver, context, extraRunFormatting,
396
397
  type: "direct"
397
398
  }], { ordinaryFormatting: ordinaryDefaultRunFormatting });
398
399
  const defaultRunFormatting = defaultToggleCascade.formatting;
399
- if (extraRunFormatting !== void 0) {
400
- attrs._tableRunFormatting = extraRunFormatting;
400
+ if (extraRunFormatting !== void 0 || paragraph.content.length > 0) {
401
+ if (extraRunFormatting !== void 0) attrs._tableRunFormatting = extraRunFormatting;
401
402
  if (defaultRunFormatting) attrs.defaultTextFormatting = defaultRunFormatting;
402
403
  else delete attrs.defaultTextFormatting;
403
404
  }
@@ -606,13 +607,21 @@ function anchorPointComment(nodes, commentId) {
606
607
  * and it is what lets the save leg hoist a revision covering all of them back
607
608
  * around the control instead of losing it.
608
609
  */
609
- const withTrackedRunMark = (node, mark) => {
610
- if (node.marks.some(({ type }) => type.name === "insertion" || type.name === "deletion")) return node;
610
+ const withTrackedRunMark = (node, mark, ancestor) => {
611
611
  if (node.type.name === "sdt") {
612
612
  const marked = [];
613
- for (let index = 0; index < node.childCount; index += 1) marked.push(withTrackedRunMark(node.child(index), mark));
613
+ for (let index = 0; index < node.childCount; index += 1) marked.push(withTrackedRunMark(node.child(index), mark, ancestor));
614
614
  return recreateProseNodeWithParagraphPropertySource(node, { content: marked });
615
615
  }
616
+ const nestedMark = node.marks.find(({ type }) => type.name === "insertion" || type.name === "deletion");
617
+ if (nestedMark) {
618
+ const nestedAttrs = expectTrackedChangeMarkAttrs(nestedMark);
619
+ const updatedMark = schema.mark(nestedMark.type.name, {
620
+ ...nestedMark.attrs,
621
+ _docxRevisionAncestors: [ancestor, ...nestedAttrs._docxRevisionAncestors ?? []]
622
+ });
623
+ return node.mark(updatedMark.addToSet(node.marks));
624
+ }
616
625
  if (trackedRunInlineAtomDisposition(node) === "outside-wrapper") panic(`Inline atom ${JSON.stringify(node.type.name)} cannot occur in a tracked-run wrapper`);
617
626
  if (canCarryTrackedRunMark(node)) return node.mark(mark.addToSet(node.marks));
618
627
  return node;
@@ -652,7 +661,7 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, next
652
661
  } else if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") {
653
662
  const nestedMarkType = item.type === "insertion" || item.type === "moveTo" ? "insertion" : "deletion";
654
663
  const nestedMoveKind = item.type === "moveFrom" || item.type === "moveTo" ? item.type : null;
655
- itemNodes.push(...convertTrackedChange(item, nestedMarkType, nextHyperlinkInstanceIndex, nextRunIdentityId, runFormattingResolvers, styleResolver, nestedMoveKind, textBoxAnchors));
664
+ itemNodes.push(...convertTrackedChange(item, nestedMarkType, nextHyperlinkInstanceIndex, nextRunIdentityId, runFormattingResolvers, styleResolver, nestedMoveKind, textBoxAnchors, [...wrappedBy, ...stack]));
656
665
  } else if (item.type === "inlineSdt") {
657
666
  const sdtNode = convertInlineSdt(item, nextHyperlinkInstanceIndex, nextRunIdentityId, getTrackedRunFormatting, runFormattingResolvers, styleResolver, textBoxAnchors, [...wrappedBy, ...stack]);
658
667
  if (sdtNode) itemNodes.push(sdtNode);
@@ -681,9 +690,19 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, next
681
690
  utcDate: change.info.utcDate?.value ?? null,
682
691
  initials: change.info.initials ?? null,
683
692
  moveKind,
693
+ _docxOuterWrapperCount: wrappedBy.length,
684
694
  ...markType === "deletion" ? { _historicalFormatting: true } : {}
685
695
  });
686
- return nodes.map((node) => withTrackedRunMark(node, mark));
696
+ const ancestor = {
697
+ type: change.type,
698
+ revisionId: change.info.id,
699
+ author: change.info.author,
700
+ ...change.info.date ? { date: change.info.date } : {},
701
+ ...change.info.utcDate ? { utcDate: change.info.utcDate.value } : {},
702
+ ...change.info.initials ? { initials: change.info.initials } : {},
703
+ outerWrapperCount: wrappedBy.length
704
+ };
705
+ return nodes.map((node) => withTrackedRunMark(node, mark, ancestor));
687
706
  }
688
707
  function paragraphFormattingToAttrs(paragraph, styleResolver, tableParagraphOverlay) {
689
708
  const formatting = paragraph.formatting;
@@ -1691,6 +1710,7 @@ function convertRun(run, resolvedStyleFormatting, nextRunIdentityId, styleResolv
1691
1710
  return nodes;
1692
1711
  }
1693
1712
  const runHasPageBreakContent = (run) => run.content.some((content) => content.type === "break" && content.breakType === "page");
1713
+ const runHasMixedContent = (run) => run.content.length > 1 && run.content.some((content) => content.type !== "text");
1694
1714
  /**
1695
1715
  * Whether a field's result holds an explicit page break.
1696
1716
  *
@@ -44,13 +44,10 @@ type SchemaMarkName = keyof typeof MARK_EXTENSIONS;
44
44
  * Five of these marks are containers on the save leg as well, and their
45
45
  * relative order is that leg's rather than a choice made here.
46
46
  * `extractParagraphContent` opens a comment range around everything a leaf
47
- * produces (it flushes the open run and hyperlink and drops the tracked wrapper
48
- * before pushing the marker: `fromProseDoc.ts:2359-2369`), then the revision
49
- * wrapper (`fromProseDoc.ts:2530`), then the wrapper stack inside that revision
50
- * (`nestInlineWrapperGroups`, `fromProseDoc.ts:2200`), then the hyperlink and
51
- * the runs it holds (`fromProseDoc.ts:2567`). So the save writes
52
- * `w:commentRangeStart … w:ins > w:bdo > w:hyperlink > w:r`, and the editor DOM
53
- * nests `span.docx-comment > span.docx-insertion > bdo > a`.
47
+ * produces. For an editor-created revision, the existing wrapper stack owns
48
+ * the change, followed by the hyperlink and the runs it holds. So the save
49
+ * writes `w:commentRangeStart … w:bdo > w:ins > w:hyperlink > w:r`, and the
50
+ * editor DOM nests `span.docx-comment > bdo > span.docx-insertion > a`.
54
51
  * `markNestingOrder.test.ts` holds the two legs to each other.
55
52
  *
56
53
  * `runIdentity` is the fifth: `w:r` is the element it names, and it is the
@@ -60,6 +57,6 @@ type SchemaMarkName = keyof typeof MARK_EXTENSIONS;
60
57
  * own in OOXML, so the save leg ranks it against nothing and its place here is
61
58
  * a presentation decision.
62
59
  */
63
- declare const MARK_NESTING_ORDER: readonly ["bold", "italic", "underline", "strike", "textColor", "runShading", "highlight", "fontSize", "fontFamily", "language", "superscript", "subscript", "allCaps", "smallCaps", "footnoteRef", "characterSpacing", "emboss", "imprint", "hidden", "textShadow", "emphasisMark", "textOutline", "rtl", "textEffect", "runFormattingOverride", "characterStyle", "comment", "insertion", "deletion", "inlineWrapper", "hyperlink", "runPropertyChange", "runIdentity"];
60
+ declare const MARK_NESTING_ORDER: readonly ["bold", "italic", "underline", "strike", "textColor", "runShading", "highlight", "fontSize", "fontFamily", "language", "superscript", "subscript", "allCaps", "smallCaps", "footnoteRef", "characterSpacing", "emboss", "imprint", "hidden", "textShadow", "emphasisMark", "textOutline", "rtl", "textEffect", "runFormattingOverride", "characterStyle", "comment", "inlineWrapper", "insertion", "deletion", "hyperlink", "runPropertyChange", "runIdentity"];
64
61
  //#endregion
65
62
  export { MARK_EXTENSIONS, MARK_NESTING_ORDER, SchemaMarkName };
@@ -83,13 +83,10 @@ const markNestingOrder = (names) => names;
83
83
  * Five of these marks are containers on the save leg as well, and their
84
84
  * relative order is that leg's rather than a choice made here.
85
85
  * `extractParagraphContent` opens a comment range around everything a leaf
86
- * produces (it flushes the open run and hyperlink and drops the tracked wrapper
87
- * before pushing the marker: `fromProseDoc.ts:2359-2369`), then the revision
88
- * wrapper (`fromProseDoc.ts:2530`), then the wrapper stack inside that revision
89
- * (`nestInlineWrapperGroups`, `fromProseDoc.ts:2200`), then the hyperlink and
90
- * the runs it holds (`fromProseDoc.ts:2567`). So the save writes
91
- * `w:commentRangeStart … w:ins > w:bdo > w:hyperlink > w:r`, and the editor DOM
92
- * nests `span.docx-comment > span.docx-insertion > bdo > a`.
86
+ * produces. For an editor-created revision, the existing wrapper stack owns
87
+ * the change, followed by the hyperlink and the runs it holds. So the save
88
+ * writes `w:commentRangeStart … w:bdo > w:ins > w:hyperlink > w:r`, and the
89
+ * editor DOM nests `span.docx-comment > bdo > span.docx-insertion > a`.
93
90
  * `markNestingOrder.test.ts` holds the two legs to each other.
94
91
  *
95
92
  * `runIdentity` is the fifth: `w:r` is the element it names, and it is the
@@ -127,9 +124,9 @@ const MARK_NESTING_ORDER = markNestingOrder([
127
124
  "runFormattingOverride",
128
125
  "characterStyle",
129
126
  "comment",
127
+ "inlineWrapper",
130
128
  "insertion",
131
129
  "deletion",
132
- "inlineWrapper",
133
130
  "hyperlink",
134
131
  "runPropertyChange",
135
132
  "runIdentity"
@@ -105,7 +105,8 @@ const RunIdentityExtension = createMarkExtension({
105
105
  attrs: {
106
106
  id: {},
107
107
  preservedAttributes: { default: null },
108
- preserved: { default: null }
108
+ preserved: { default: null },
109
+ emptyFormatting: { default: null }
109
110
  },
110
111
  inclusive: false,
111
112
  parseDOM: [{