@stll/folio-core 0.47.4 → 0.47.6

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.
Files changed (50) hide show
  1. package/dist/compare/inline-atoms.js +218 -31
  2. package/dist/compare/section-boundary-properties.js +1 -0
  3. package/dist/content-controls/mutateContentControls.js +1 -3
  4. package/dist/controller/headerFooterEditorManager.d.ts +2 -1
  5. package/dist/controller/headerFooterEditorManager.js +2 -1
  6. package/dist/controller/noteEditorManager.d.ts +2 -1
  7. package/dist/controller/noteEditorManager.js +2 -1
  8. package/dist/docx/commentRangeIntegrity.js +7 -0
  9. package/dist/docx/documentParser.js +55 -3
  10. package/dist/docx/inlineWrapperContent.d.ts +18 -16
  11. package/dist/docx/inlineWrapperContent.js +18 -16
  12. package/dist/docx/normalizeBaseDirection.js +4 -0
  13. package/dist/docx/rezip.js +1 -0
  14. package/dist/docx/selectiveSave.js +11 -2
  15. package/dist/docx/serializer/documentSerializer.js +16 -1
  16. package/dist/docx/serializer/paragraphSerializer.js +8 -0
  17. package/dist/docx/serializer/tableSerializer.js +5 -2
  18. package/dist/docx/server/createBilingualDocument.js +23 -14
  19. package/dist/docx/tableParser.js +101 -75
  20. package/dist/markdown/renderRuns.js +4 -0
  21. package/dist/markdown/renderTable.js +46 -12
  22. package/dist/prosemirror/attrs/index.d.ts +3 -2
  23. package/dist/prosemirror/attrs/index.js +85 -4
  24. package/dist/prosemirror/commands/comments.js +61 -3
  25. package/dist/prosemirror/commentReferenceAttrs.js +2 -2
  26. package/dist/prosemirror/commentReferenceIntegrity.d.ts +7 -3
  27. package/dist/prosemirror/commentReferenceIntegrity.js +9 -1
  28. package/dist/prosemirror/conversion/fromProseDoc.js +177 -51
  29. package/dist/prosemirror/conversion/toProseDoc.js +108 -55
  30. package/dist/prosemirror/extensions/markRegistry.d.ts +5 -8
  31. package/dist/prosemirror/extensions/markRegistry.js +5 -8
  32. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +13 -6
  33. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.js +24 -6
  34. package/dist/prosemirror/extensions/marks/RunIdentityExtension.js +2 -1
  35. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +32 -7
  36. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +9 -5
  37. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -2
  38. package/dist/prosemirror/runIdentity.d.ts +5 -5
  39. package/dist/prosemirror/runIdentity.js +5 -5
  40. package/dist/prosemirror/schema/index.d.ts +2 -2
  41. package/dist/prosemirror/schema/marks.d.ts +28 -2
  42. package/dist/prosemirror/schema/nodes.d.ts +6 -4
  43. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -1
  44. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -1
  45. package/dist/prosemirror/utils/extractTrackedChanges.js +21 -6
  46. package/dist/prosemirror/validation.js +48 -7
  47. package/dist/types/content.d.ts +2 -2
  48. package/dist/types/index.d.ts +3 -1
  49. package/dist/utils/mergeDocumentContent.js +1 -7
  50. package/package.json +2 -2
@@ -272,6 +272,7 @@ function fromProseDoc(pmDoc, baseDocument, { reuse = "none" } = {}) {
272
272
  if (tokenSources) restoreParagraphPropertySourcesByToken(blocks, tokenSources);
273
273
  else if (baseDocument) restoreParagraphPropertySources(blocks, baseDocument.package.document.content, linkedSources.targets, linkedSources.sources);
274
274
  const documentBody = { content: blocks };
275
+ if (baseDocument?.package.document.background) documentBody.background = baseDocument.package.document.background;
275
276
  if (baseDocument?.package.document.finalSectionProperties) documentBody.finalSectionProperties = baseDocument.package.document.finalSectionProperties;
276
277
  if (baseDocument?.package.document.sections) documentBody.sections = baseDocument.package.document.sections;
277
278
  if (baseDocument?.package.document.comments) documentBody.comments = baseDocument.package.document.comments;
@@ -1137,6 +1138,17 @@ const inlineWrapperStackOf = (node) => {
1137
1138
  const mark = node.marks.find((candidate) => candidate.type.name === INLINE_WRAPPER_MARK_NAME);
1138
1139
  return mark ? expectInlineWrapperMarkAttrs(mark).stack : [];
1139
1140
  };
1141
+ /** The authored hyperlink that owns a suffix of the node's wrapper stack. */
1142
+ const inlineWrapperHyperlinkOriginOf = (node) => {
1143
+ const mark = node.marks.find((candidate) => candidate.type.name === INLINE_WRAPPER_MARK_NAME);
1144
+ if (!mark) return;
1145
+ const attrs = expectInlineWrapperMarkAttrs(mark);
1146
+ if (typeof attrs._docxHyperlinkIndex !== "number" || typeof attrs._docxInsideHyperlinkStackStart !== "number") return;
1147
+ return {
1148
+ hyperlinkIndex: attrs._docxHyperlinkIndex,
1149
+ stackStart: attrs._docxInsideHyperlinkStackStart
1150
+ };
1151
+ };
1140
1152
  const isRevisionWrapper = (item) => item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo";
1141
1153
  /** `stack` closed around `content`, outermost layer first. */
1142
1154
  const nestInlineWrappers = (stack, content) => {
@@ -1150,42 +1162,132 @@ const nestInlineWrappers = (stack, content) => {
1150
1162
  * The emitted sequence with each wrapper group nested back into the wrappers
1151
1163
  * `toProseDoc` lifted off it.
1152
1164
  *
1153
- * The revision stays outermost. folio already writes a revision outside the
1154
- * hyperlink it spans, the parse leg is revision-owned, and accepting or
1155
- * rejecting one is a range operation over the revision's own content; a
1156
- * wrapper placed outside two revisions would also have to mint one revision id
1157
- * per wrapper. So a revision in the group keeps its place and takes the nest
1158
- * 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.
1159
1169
  *
1160
1170
  * A group with nothing in it writes no wrapper: an authored wrapper whose
1161
1171
  * content was deleted or rejected is gone, and an empty one says nothing.
1162
1172
  */
1163
- const nestInlineWrapperGroups = (items, groups) => {
1173
+ const nestInsideHyperlink = (item, stack, hyperlinkIndex, indices) => {
1174
+ if (item.type === "hyperlink") {
1175
+ if (indices.get(item) !== hyperlinkIndex) return false;
1176
+ item.children = [nestInlineWrappers(stack, item.children)];
1177
+ return true;
1178
+ }
1179
+ if (isRevisionWrapper(item)) {
1180
+ let nested = false;
1181
+ for (const child of item.content) if (nestInsideHyperlink(child, stack, hyperlinkIndex, indices)) nested = true;
1182
+ return nested;
1183
+ }
1184
+ return false;
1185
+ };
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) => {
1164
1260
  const nested = [];
1165
1261
  for (const [index, group] of groups.entries()) {
1166
- 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);
1167
1263
  if (group.stack.length === 0) {
1168
1264
  nested.push(...slice);
1169
1265
  continue;
1170
1266
  }
1171
- let pending = [];
1172
- const closeNest = () => {
1173
- if (pending.length === 0) return;
1174
- nested.push(nestInlineWrappers(group.stack, pending));
1175
- pending = [];
1176
- };
1177
- for (const item of slice) {
1178
- if (!isRevisionWrapper(item)) {
1179
- pending.push(item);
1180
- continue;
1181
- }
1182
- closeNest();
1183
- if (item.content.length > 0) item.content = [nestInlineWrappers(group.stack, item.content)];
1184
- nested.push(item);
1267
+ const origin = group.origin;
1268
+ let outerStack = group.stack;
1269
+ if (origin !== void 0) {
1270
+ const innerStack = group.stack.slice(origin.stackStart);
1271
+ let nestedInsideOrigin = false;
1272
+ for (const item of slice) if (nestInsideHyperlink(item, innerStack, origin.hyperlinkIndex, hyperlinkIndices)) nestedInsideOrigin = true;
1273
+ if (nestedInsideOrigin) outerStack = group.stack.slice(0, origin.stackStart);
1274
+ }
1275
+ if (outerStack.length === 0) {
1276
+ nested.push(...slice);
1277
+ continue;
1185
1278
  }
1186
- closeNest();
1279
+ nested.push(...nestRevisionAwareInlineWrappers(slice, outerStack, revisionOuterWrapperCounts));
1187
1280
  }
1188
- return nested;
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;
1287
+ }
1288
+ coalesced.push(item);
1289
+ }
1290
+ return coalesced;
1189
1291
  };
1190
1292
  function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, textBoxAnchorMarkers, skipLeadingRenderedPageBreak = false, inheritedFormattingOverride) {
1191
1293
  const content = [];
@@ -1214,7 +1316,16 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1214
1316
  let currentHyperlinkKey = null;
1215
1317
  let currentTrackedChange;
1216
1318
  const sourceRunOwners = /* @__PURE__ */ new WeakMap();
1319
+ const sourceHyperlinkIndices = /* @__PURE__ */ new WeakMap();
1320
+ const revisionOuterWrapperCounts = /* @__PURE__ */ new WeakMap();
1321
+ const revisionAncestorsByWrapper = /* @__PURE__ */ new WeakMap();
1217
1322
  const openedComments = /* @__PURE__ */ new Set();
1323
+ const createIndexedHyperlink = (mark) => {
1324
+ const hyperlink = createHyperlink(mark);
1325
+ const index = expectHyperlinkMarkAttrs(mark)._docxHyperlinkIndex;
1326
+ if (typeof index === "number") sourceHyperlinkIndices.set(hyperlink, index);
1327
+ return hyperlink;
1328
+ };
1218
1329
  const commentLastOffset = /* @__PURE__ */ new Map();
1219
1330
  paragraph.forEach((node, offset) => {
1220
1331
  for (const commentId of getCommentMarkIds(node.marks)) commentLastOffset.set(commentId, offset);
@@ -1298,14 +1409,20 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1298
1409
  */
1299
1410
  const enterWrapperGroup = (node) => {
1300
1411
  const stack = inlineWrapperStackOf(node);
1301
- const key = inlineWrapperStackKey(stack);
1412
+ const origin = inlineWrapperHyperlinkOriginOf(node);
1413
+ const key = [
1414
+ inlineWrapperStackKey(stack),
1415
+ origin?.hyperlinkIndex ?? "",
1416
+ origin?.stackStart ?? ""
1417
+ ].join("\0");
1302
1418
  if (key === wrapperGroupKey) return;
1303
1419
  flushCurrentInline();
1304
1420
  currentTrackedChange = void 0;
1305
1421
  wrapperGroupKey = key;
1306
1422
  wrapperGroups.push({
1307
1423
  start: content.length,
1308
- stack
1424
+ stack,
1425
+ ...origin ? { origin } : {}
1309
1426
  });
1310
1427
  };
1311
1428
  const processInlineNode = (node, offset) => {
@@ -1332,12 +1449,6 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1332
1449
  content.push({ ...start }, { ...end });
1333
1450
  return;
1334
1451
  }
1335
- if (node.type.name === "moveRangeBoundary") {
1336
- flushCurrentInline();
1337
- currentTrackedChange = void 0;
1338
- content.push({ ...expectMoveRangeBoundaryAttrs(node) });
1339
- return;
1340
- }
1341
1452
  const linkMark = node.marks.find((m) => m.type.name === "hyperlink");
1342
1453
  const noteRefMark = node.marks.find((m) => m.type.name === "footnoteRef");
1343
1454
  const insertionMark = node.marks.find((m) => m.type.name === "insertion");
@@ -1354,10 +1465,8 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1354
1465
  };
1355
1466
  if (textBoxAnchorMarkers.has(anchorId)) return;
1356
1467
  textBoxAnchorMarkers.set(anchorId, marker);
1357
- const anchoredContent = linkMark ? {
1358
- ...createHyperlink(linkMark),
1359
- children: [marker]
1360
- } : marker;
1468
+ const anchoredContent = linkMark ? createIndexedHyperlink(linkMark) : marker;
1469
+ if (anchoredContent.type === "hyperlink") anchoredContent.children.push(marker);
1361
1470
  const changeMark = insertionMark ?? deletionMark;
1362
1471
  if (!changeMark) {
1363
1472
  content.push(anchoredContent);
@@ -1374,16 +1483,13 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1374
1483
  } } : {},
1375
1484
  ...changeAttrs.initials ? { initials: changeAttrs.initials } : {}
1376
1485
  };
1377
- if (insertionMark) content.push({
1378
- type: changeAttrs.moveKind === "moveTo" ? "moveTo" : "insertion",
1379
- info,
1380
- content: [anchoredContent]
1381
- });
1382
- else content.push({
1383
- type: changeAttrs.moveKind === "moveFrom" ? "moveFrom" : "deletion",
1384
- info,
1385
- content: [anchoredContent]
1386
- });
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);
1387
1493
  return;
1388
1494
  }
1389
1495
  if (insertionMark || deletionMark) {
@@ -1405,12 +1511,15 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1405
1511
  let type;
1406
1512
  if (insertionMark) type = changeAttrs.moveKind === "moveTo" ? "moveTo" : "insertion";
1407
1513
  else type = changeAttrs.moveKind === "moveFrom" ? "moveFrom" : "deletion";
1408
- 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 ?? [])}`;
1409
1516
  if (linkMark) {
1410
1517
  const linkKey = getLinkKey(linkMark);
1411
1518
  if (!currentTrackedChange || currentTrackedChange.type !== "hyperlink" || currentTrackedChange.key !== trackedChangeKey || currentTrackedChange.hyperlinkKey !== linkKey) {
1412
- const hyperlink = createHyperlink(linkMark);
1519
+ const hyperlink = createIndexedHyperlink(linkMark);
1413
1520
  const wrapper = createTrackedRunWrapper(type, info, hyperlink);
1521
+ revisionOuterWrapperCounts.set(wrapper, outerWrapperCount);
1522
+ revisionAncestorsByWrapper.set(wrapper, changeAttrs._docxRevisionAncestors ?? []);
1414
1523
  content.push(wrapper);
1415
1524
  currentTrackedChange = {
1416
1525
  type: "hyperlink",
@@ -1444,6 +1553,8 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1444
1553
  }
1445
1554
  if (!currentTrackedChange || currentTrackedChange.type !== "direct" || currentTrackedChange.key !== trackedChangeKey) {
1446
1555
  const wrapper = createTrackedRunWrapper(type, info);
1556
+ revisionOuterWrapperCounts.set(wrapper, outerWrapperCount);
1557
+ revisionAncestorsByWrapper.set(wrapper, changeAttrs._docxRevisionAncestors ?? []);
1447
1558
  content.push(wrapper);
1448
1559
  currentTrackedChange = {
1449
1560
  type: "direct",
@@ -1455,6 +1566,10 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1455
1566
  currentTrackedChange.wrapper.content.push(bookmarkMarkerFromAttrs(expectBookmarkBoundaryAttrs(node)));
1456
1567
  return;
1457
1568
  }
1569
+ if (node.type.name === "moveRangeBoundary") {
1570
+ currentTrackedChange.wrapper.content.push({ ...expectMoveRangeBoundaryAttrs(node) });
1571
+ return;
1572
+ }
1458
1573
  if (node.type.name === "field" || node.type.name === "structuredField") {
1459
1574
  currentTrackedChange.wrapper.content.push(createFieldFromNode(node, {
1460
1575
  ...formattingContext,
@@ -1488,6 +1603,12 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1488
1603
  appendDirectRun(node, ownedRun, node.isText);
1489
1604
  return;
1490
1605
  }
1606
+ if (node.type.name === "moveRangeBoundary") {
1607
+ flushCurrentInline();
1608
+ currentTrackedChange = void 0;
1609
+ content.push({ ...expectMoveRangeBoundaryAttrs(node) });
1610
+ return;
1611
+ }
1491
1612
  if (noteRefMark && !linkMark) {
1492
1613
  flushCurrentInline();
1493
1614
  content.push(createNoteReferenceRun(noteRefMark, node.marks, formattingContext));
@@ -1497,7 +1618,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1497
1618
  const linkKey = getLinkKey(linkMark);
1498
1619
  if (currentHyperlink && currentHyperlinkKey === linkKey) {} else {
1499
1620
  flushCurrentInline();
1500
- currentHyperlink = createHyperlink(linkMark);
1621
+ currentHyperlink = createIndexedHyperlink(linkMark);
1501
1622
  currentHyperlinkKey = linkKey;
1502
1623
  }
1503
1624
  addNodeToHyperlink({
@@ -1588,7 +1709,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1588
1709
  type: "commentRangeEnd",
1589
1710
  id: commentId
1590
1711
  });
1591
- return nestInlineWrapperGroups(content, wrapperGroups);
1712
+ return nestInlineWrapperGroups(content, wrapperGroups, sourceHyperlinkIndices, revisionOuterWrapperCounts, revisionAncestorsByWrapper);
1592
1713
  }
1593
1714
  function createTrackedChangeRun({ baseParagraphFormatting, inheritedFormatting, marks, node, paragraphMarkFormatting, paragraphMarkPrecedesStyle, styleResolver }) {
1594
1715
  const formattingContext = {
@@ -1646,6 +1767,7 @@ function createEmptyHyperlink(attrs) {
1646
1767
  if (attrs.target !== void 0) hyperlink.target = attrs.target;
1647
1768
  if (attrs.history !== void 0) hyperlink.history = attrs.history;
1648
1769
  if (attrs.docLocation !== void 0) hyperlink.docLocation = attrs.docLocation;
1770
+ for (const stack of attrs._docxEmptyWrapperStacks ?? []) hyperlink.children.push(nestInlineWrappers(stack, []));
1649
1771
  return hyperlink;
1650
1772
  }
1651
1773
  function getCommentMarkIds(marks) {
@@ -1948,7 +2070,7 @@ function restoreRunRecord(run, marks) {
1948
2070
  }
1949
2071
  const identityMark = marks.find((mark) => mark.type.name === RUN_IDENTITY_MARK_NAME);
1950
2072
  if (!identityMark) return;
1951
- const { preservedAttributes, preserved } = expectRunIdentityMarkAttrs(identityMark);
2073
+ const { preservedAttributes, preserved, emptyFormatting } = expectRunIdentityMarkAttrs(identityMark);
1952
2074
  if (preservedAttributes && preservedAttributes.length > 0) run.preservedAttributes = preservedAttributes.map(({ namespace, name, value }) => namespace === void 0 ? {
1953
2075
  name,
1954
2076
  value
@@ -1961,6 +2083,7 @@ function restoreRunRecord(run, marks) {
1961
2083
  ...run.formatting,
1962
2084
  preserved
1963
2085
  };
2086
+ if (emptyFormatting && run.formatting === void 0) run.formatting = {};
1964
2087
  }
1965
2088
  function getRunFormattingFromMarks(marks, options) {
1966
2089
  if (!marks || marks.length === 0 && !options) return;
@@ -3298,6 +3421,9 @@ function convertPMTableCell(node, documentCounts, styleResolver = null) {
3298
3421
  } else if (contentNode.type.name === "table") {
3299
3422
  content.push(convertPMTable(contentNode, documentCounts, styleResolver));
3300
3423
  previousStandaloneTextBox = null;
3424
+ } else if (contentNode.type.name === "blockSdt") {
3425
+ content.push(convertPMBlockSdt(contentNode, styleResolver));
3426
+ previousStandaloneTextBox = null;
3301
3427
  } else if (contentNode.type.name === "textBox") previousStandaloneTextBox = appendTextBoxBlock(content, contentNode, {
3302
3428
  pendingPageBreaks: 0,
3303
3429
  previousStandaloneTextBox,
@@ -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
  }
@@ -486,6 +487,7 @@ function convertParagraph(paragraph, styleResolver, context, extraRunFormatting,
486
487
  nextRunIdentityId
487
488
  });
488
489
  if (linkNodes.length === 0) {
490
+ const emptyWrapperStacks = emptyInlineWrapperStacks(content.children);
489
491
  emptyHyperlinks ??= [];
490
492
  emptyHyperlinks.push({
491
493
  offset: inlineOffset,
@@ -495,7 +497,8 @@ function convertParagraph(paragraph, styleResolver, context, extraRunFormatting,
495
497
  ...content.rId !== void 0 ? { rId: content.rId } : {},
496
498
  ...content.target !== void 0 ? { target: content.target } : {},
497
499
  ...content.history !== void 0 ? { history: content.history } : {},
498
- ...content.docLocation !== void 0 ? { docLocation: content.docLocation } : {}
500
+ ...content.docLocation !== void 0 ? { docLocation: content.docLocation } : {},
501
+ ...emptyWrapperStacks.length === 0 ? {} : { _docxEmptyWrapperStacks: emptyWrapperStacks }
499
502
  });
500
503
  break;
501
504
  }
@@ -604,13 +607,21 @@ function anchorPointComment(nodes, commentId) {
604
607
  * and it is what lets the save leg hoist a revision covering all of them back
605
608
  * around the control instead of losing it.
606
609
  */
607
- const withTrackedRunMark = (node, mark) => {
608
- if (node.marks.some(({ type }) => type.name === "insertion" || type.name === "deletion")) return node;
610
+ const withTrackedRunMark = (node, mark, ancestor) => {
609
611
  if (node.type.name === "sdt") {
610
612
  const marked = [];
611
- 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));
612
614
  return recreateProseNodeWithParagraphPropertySource(node, { content: marked });
613
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
+ }
614
625
  if (trackedRunInlineAtomDisposition(node) === "outside-wrapper") panic(`Inline atom ${JSON.stringify(node.type.name)} cannot occur in a tracked-run wrapper`);
615
626
  if (canCarryTrackedRunMark(node)) return node.mark(mark.addToSet(node.marks));
616
627
  return node;
@@ -650,7 +661,7 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, next
650
661
  } else if (item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo") {
651
662
  const nestedMarkType = item.type === "insertion" || item.type === "moveTo" ? "insertion" : "deletion";
652
663
  const nestedMoveKind = item.type === "moveFrom" || item.type === "moveTo" ? item.type : null;
653
- 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]));
654
665
  } else if (item.type === "inlineSdt") {
655
666
  const sdtNode = convertInlineSdt(item, nextHyperlinkInstanceIndex, nextRunIdentityId, getTrackedRunFormatting, runFormattingResolvers, styleResolver, textBoxAnchors, [...wrappedBy, ...stack]);
656
667
  if (sdtNode) itemNodes.push(sdtNode);
@@ -667,6 +678,7 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, next
667
678
  id: item.id,
668
679
  displacedByCustomXml: item.displacedByCustomXml
669
680
  }));
681
+ else if (item.type === "moveFromRangeStart" || item.type === "moveFromRangeEnd" || item.type === "moveToRangeStart" || item.type === "moveToRangeEnd") itemNodes.push(schema.node(MOVE_RANGE_BOUNDARY_NODE_NAME, { marker: item }));
670
682
  else if (item.type === "preservedInline") itemNodes.push(preservedInlineNode(item));
671
683
  else panic(`Unsupported tracked-run content: ${JSON.stringify(item)}`);
672
684
  nodes.push(...withInlineWrapperMark(itemNodes, stack));
@@ -678,9 +690,19 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, next
678
690
  utcDate: change.info.utcDate?.value ?? null,
679
691
  initials: change.info.initials ?? null,
680
692
  moveKind,
693
+ _docxOuterWrapperCount: wrappedBy.length,
681
694
  ...markType === "deletion" ? { _historicalFormatting: true } : {}
682
695
  });
683
- 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));
684
706
  }
685
707
  function paragraphFormattingToAttrs(paragraph, styleResolver, tableParagraphOverlay) {
686
708
  const formatting = paragraph.formatting;
@@ -1035,6 +1057,7 @@ function blockHasMeaningfulContent(block) {
1035
1057
  if (block.type === "preservedBlock") return true;
1036
1058
  if (block.type === "bookmarkStart" || block.type === "bookmarkEnd") return true;
1037
1059
  if (block.type === "table") return block.rows.some((row) => row.cells.some((cell) => tableCellHasMeaningfulContent(cell)));
1060
+ if (block.type === "blockSdt") return true;
1038
1061
  return block.content.some(paragraphContentHasMeaningfulContent);
1039
1062
  }
1040
1063
  function paragraphContentHasMeaningfulContent(content) {
@@ -1394,28 +1417,35 @@ function convertTableCell({ cell, styleResolver, context, isHeader, gridWidthPer
1394
1417
  }
1395
1418
  if (preserveVMergeRestart) attrs._preserveVMergeRestart = true;
1396
1419
  if (vMergeContinuationCells && vMergeContinuationCells.length > 0) attrs._docxVMergeContinuationCells = transportTableCellsWithParagraphPropertySources(vMergeContinuationCells);
1397
- const contentNodes = [];
1398
- for (const content of cell.content) switch (content.type) {
1399
- case "paragraph":
1400
- contentNodes.push(...convertParagraphWithTextBoxes(content, styleResolver, {
1401
- textBoxGroupId: context.nextTextBoxGroupId(),
1402
- context,
1403
- ...conditionalStyle?.rPr !== void 0 ? { extraRunFormatting: conditionalStyle.rPr } : {},
1404
- ...conditionalStyle?.pPr !== void 0 ? { tableParagraphOverlay: conditionalStyle.pPr } : {}
1405
- }));
1406
- break;
1407
- case "table":
1408
- contentNodes.push(convertTable(content, styleResolver, context));
1409
- break;
1410
- case "preservedBlock":
1411
- contentNodes.push(convertPreservedBlock(content));
1412
- break;
1413
- case "bookmarkStart":
1414
- case "bookmarkEnd":
1415
- contentNodes.push(convertBlockBookmarkBoundary(content));
1416
- break;
1417
- default: panic(`Unsupported table cell content: ${JSON.stringify(content)}`);
1418
- }
1420
+ const convertCellBlocks = (blocks) => {
1421
+ const nodes = [];
1422
+ for (const block of blocks) switch (block.type) {
1423
+ case "paragraph":
1424
+ nodes.push(...convertParagraphWithTextBoxes(block, styleResolver, {
1425
+ textBoxGroupId: context.nextTextBoxGroupId(),
1426
+ context,
1427
+ ...conditionalStyle?.rPr !== void 0 ? { extraRunFormatting: conditionalStyle.rPr } : {},
1428
+ ...conditionalStyle?.pPr !== void 0 ? { tableParagraphOverlay: conditionalStyle.pPr } : {}
1429
+ }));
1430
+ break;
1431
+ case "table":
1432
+ nodes.push(convertTable(block, styleResolver, context));
1433
+ break;
1434
+ case "blockSdt":
1435
+ nodes.push(convertBlockSdt(block, convertCellBlocks));
1436
+ break;
1437
+ case "preservedBlock":
1438
+ nodes.push(convertPreservedBlock(block));
1439
+ break;
1440
+ case "bookmarkStart":
1441
+ case "bookmarkEnd":
1442
+ nodes.push(convertBlockBookmarkBoundary(block));
1443
+ break;
1444
+ default: panic(`Unsupported table cell content: ${JSON.stringify(block)}`);
1445
+ }
1446
+ return nodes;
1447
+ };
1448
+ const contentNodes = convertCellBlocks(cell.content);
1419
1449
  if (contentNodes.length === 0) contentNodes.push(schema.node("paragraph", {}, []));
1420
1450
  const nodeType = isHeader ? "tableHeader" : "tableCell";
1421
1451
  return schema.node(nodeType, attrs, contentNodes);
@@ -1527,27 +1557,32 @@ const withInlineWrapperStacks = (content, stack = []) => content.flatMap((item)
1527
1557
  content: item,
1528
1558
  stack
1529
1559
  }]);
1530
- /**
1531
- * `nodes` with `stack` recorded outside whatever wrapper they already carry.
1532
- *
1533
- * A revision converts its own content first, so a node that arrives here
1534
- * already marked sat inside the item this stack wraps: the two stacks
1535
- * concatenate, outermost first. Concatenating is what the schema forces — the
1536
- * mark excludes itself, so a second one replaces the first, and the inner
1537
- * wrapper would be the one lost.
1538
- *
1539
- * A node that holds its own leaves, a content control, is marked here as the
1540
- * one node it is; its leaves were marked with the whole enclosing stack when
1541
- * it was built, because the painter reads the wrapper off the leaf.
1542
- */
1543
- const withInlineWrapperMark = (nodes, stack) => {
1560
+ /** Wrapper nests with no leaf to carry their stack as a ProseMirror mark. */
1561
+ const emptyInlineWrapperStacks = (content, stack = []) => content.flatMap((item) => {
1562
+ if (item.type !== "inlineWrapper") return [];
1563
+ const nested = [...stack, inlineWrapperLayer(item)];
1564
+ return item.content.length === 0 ? [nested] : emptyInlineWrapperStacks(item.content, nested);
1565
+ });
1566
+ const withInlineWrapperMark = (nodes, stack, origin) => {
1544
1567
  const markType = schema.marks["inlineWrapper"];
1545
1568
  if (stack.length === 0 || !markType) return nodes;
1546
1569
  return nodes.map((node) => {
1547
- if (!node.isText && (!node.isInline || !node.type.allowsMarkType(markType))) return node;
1570
+ if (!node.isInline) return node;
1548
1571
  const inner = node.marks.find((mark) => mark.type === markType);
1549
- const layers = inner === void 0 ? stack : [...stack, ...expectInlineWrapperMarkAttrs(inner).stack];
1550
- return node.mark(markType.create({ stack: layers }).addToSet(node.marks));
1572
+ const innerAttrs = inner === void 0 ? void 0 : expectInlineWrapperMarkAttrs(inner);
1573
+ const layers = innerAttrs === void 0 ? stack : [...stack, ...innerAttrs.stack];
1574
+ const inheritedOrigin = typeof innerAttrs?._docxHyperlinkIndex !== "number" || typeof innerAttrs._docxInsideHyperlinkStackStart !== "number" ? void 0 : {
1575
+ hyperlinkIndex: innerAttrs._docxHyperlinkIndex,
1576
+ stackStart: stack.length + innerAttrs._docxInsideHyperlinkStackStart
1577
+ };
1578
+ const provenance = origin ?? inheritedOrigin;
1579
+ return node.mark(markType.create({
1580
+ stack: layers,
1581
+ ...provenance === void 0 ? {} : {
1582
+ _docxHyperlinkIndex: provenance.hyperlinkIndex,
1583
+ _docxInsideHyperlinkStackStart: provenance.stackStart
1584
+ }
1585
+ }).addToSet(node.marks));
1551
1586
  });
1552
1587
  };
1553
1588
  /**
@@ -1570,9 +1605,17 @@ function convertMathEquation(math) {
1570
1605
  function convertInlineSdt(sdt, nextHyperlinkInstanceIndex, nextRunIdentityId, getInheritedRunFormatting, trackedRunFormattingResolvers, styleResolver, textBoxAnchors, wrappedBy = []) {
1571
1606
  const props = sdt.properties;
1572
1607
  const inlineNodes = [];
1573
- for (const { content, stack } of withInlineWrapperStacks(sdt.content, wrappedBy)) {
1608
+ const stacked = withInlineWrapperStacks(sdt.content, wrappedBy);
1609
+ const emptyRanges = planEmptyRanges(stacked.map(({ content }) => content));
1610
+ for (const [index, { content, stack }] of stacked.entries()) {
1574
1611
  if (!isInlineSdtContent(content)) continue;
1575
1612
  const itemNodes = [];
1613
+ const anchored = emptyRanges.anchorAt.get(index);
1614
+ if (anchored) {
1615
+ inlineNodes.push(...withInlineWrapperMark([schema.node(RANGE_ANCHOR_NODE_NAME, anchored)], stack));
1616
+ continue;
1617
+ }
1618
+ if (emptyRanges.closedAt.has(index)) continue;
1576
1619
  switch (content.type) {
1577
1620
  case "run":
1578
1621
  itemNodes.push(...convertRun(content, getInheritedRunFormatting(content.formatting), nextRunIdentityId, styleResolver, textBoxAnchors));
@@ -1633,6 +1676,12 @@ function convertInlineSdt(sdt, nextHyperlinkInstanceIndex, nextRunIdentityId, ge
1633
1676
  displacedByCustomXml: content.displacedByCustomXml
1634
1677
  }));
1635
1678
  break;
1679
+ case "moveFromRangeStart":
1680
+ case "moveFromRangeEnd":
1681
+ case "moveToRangeStart":
1682
+ case "moveToRangeEnd":
1683
+ itemNodes.push(schema.node(MOVE_RANGE_BOUNDARY_NODE_NAME, { marker: content }));
1684
+ break;
1636
1685
  case "preservedInline":
1637
1686
  itemNodes.push(preservedInlineNode(content));
1638
1687
  break;
@@ -1661,6 +1710,7 @@ function convertRun(run, resolvedStyleFormatting, nextRunIdentityId, styleResolv
1661
1710
  return nodes;
1662
1711
  }
1663
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");
1664
1714
  /**
1665
1715
  * Whether a field's result holds an explicit page break.
1666
1716
  *
@@ -2331,7 +2381,10 @@ function convertHyperlink(hyperlink, { getInheritedRunFormatting, styleResolver,
2331
2381
  }
2332
2382
  default: break;
2333
2383
  }
2334
- nodes.push(...withInlineWrapperMark(childNodes, stack));
2384
+ nodes.push(...withInlineWrapperMark(childNodes, stack, {
2385
+ hyperlinkIndex,
2386
+ stackStart: 0
2387
+ }));
2335
2388
  }
2336
2389
  return nodes;
2337
2390
  }