@stll/folio-core 0.47.4 → 0.47.5

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 (35) 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/docx/commentRangeIntegrity.js +7 -0
  5. package/dist/docx/documentParser.js +55 -3
  6. package/dist/docx/inlineWrapperContent.d.ts +18 -16
  7. package/dist/docx/inlineWrapperContent.js +18 -16
  8. package/dist/docx/normalizeBaseDirection.js +4 -0
  9. package/dist/docx/rezip.js +1 -0
  10. package/dist/docx/serializer/documentSerializer.js +16 -1
  11. package/dist/docx/serializer/paragraphSerializer.js +8 -0
  12. package/dist/docx/serializer/tableSerializer.js +5 -2
  13. package/dist/docx/server/createBilingualDocument.js +23 -14
  14. package/dist/docx/tableParser.js +101 -75
  15. package/dist/markdown/renderRuns.js +4 -0
  16. package/dist/markdown/renderTable.js +46 -12
  17. package/dist/prosemirror/attrs/index.js +67 -3
  18. package/dist/prosemirror/commentReferenceAttrs.js +2 -2
  19. package/dist/prosemirror/commentReferenceIntegrity.d.ts +7 -3
  20. package/dist/prosemirror/commentReferenceIntegrity.js +9 -1
  21. package/dist/prosemirror/conversion/fromProseDoc.js +74 -18
  22. package/dist/prosemirror/conversion/toProseDoc.js +75 -42
  23. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +13 -6
  24. package/dist/prosemirror/extensions/marks/InlineWrapperExtension.js +24 -6
  25. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +9 -5
  26. package/dist/prosemirror/extensions/nodes/TableExtension.js +2 -2
  27. package/dist/prosemirror/schema/marks.d.ts +11 -0
  28. package/dist/prosemirror/schema/nodes.d.ts +6 -4
  29. package/dist/prosemirror/trackedRunInlineAtoms.d.ts +1 -1
  30. package/dist/prosemirror/trackedRunInlineAtoms.js +1 -1
  31. package/dist/prosemirror/validation.js +48 -7
  32. package/dist/types/content.d.ts +2 -2
  33. package/dist/types/index.d.ts +3 -1
  34. package/dist/utils/mergeDocumentContent.js +1 -7
  35. 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) => {
@@ -1160,7 +1172,20 @@ const nestInlineWrappers = (stack, content) => {
1160
1172
  * A group with nothing in it writes no wrapper: an authored wrapper whose
1161
1173
  * content was deleted or rejected is gone, and an empty one says nothing.
1162
1174
  */
1163
- const nestInlineWrapperGroups = (items, groups) => {
1175
+ const nestInsideHyperlink = (item, stack, hyperlinkIndex, indices) => {
1176
+ if (item.type === "hyperlink") {
1177
+ if (indices.get(item) !== hyperlinkIndex) return false;
1178
+ item.children = [nestInlineWrappers(stack, item.children)];
1179
+ return true;
1180
+ }
1181
+ if (isRevisionWrapper(item)) {
1182
+ let nested = false;
1183
+ for (const child of item.content) if (nestInsideHyperlink(child, stack, hyperlinkIndex, indices)) nested = true;
1184
+ return nested;
1185
+ }
1186
+ return false;
1187
+ };
1188
+ const nestInlineWrapperGroups = (items, groups, hyperlinkIndices) => {
1164
1189
  const nested = [];
1165
1190
  for (const [index, group] of groups.entries()) {
1166
1191
  const slice = items.slice(group.start, groups[index + 1]?.start ?? items.length);
@@ -1168,10 +1193,22 @@ const nestInlineWrapperGroups = (items, groups) => {
1168
1193
  nested.push(...slice);
1169
1194
  continue;
1170
1195
  }
1196
+ const origin = group.origin;
1197
+ let outerStack = group.stack;
1198
+ if (origin !== void 0) {
1199
+ const innerStack = group.stack.slice(origin.stackStart);
1200
+ let nestedInsideOrigin = false;
1201
+ for (const item of slice) if (nestInsideHyperlink(item, innerStack, origin.hyperlinkIndex, hyperlinkIndices)) nestedInsideOrigin = true;
1202
+ if (nestedInsideOrigin) outerStack = group.stack.slice(0, origin.stackStart);
1203
+ }
1204
+ if (outerStack.length === 0) {
1205
+ nested.push(...slice);
1206
+ continue;
1207
+ }
1171
1208
  let pending = [];
1172
1209
  const closeNest = () => {
1173
1210
  if (pending.length === 0) return;
1174
- nested.push(nestInlineWrappers(group.stack, pending));
1211
+ nested.push(nestInlineWrappers(outerStack, pending));
1175
1212
  pending = [];
1176
1213
  };
1177
1214
  for (const item of slice) {
@@ -1180,7 +1217,7 @@ const nestInlineWrapperGroups = (items, groups) => {
1180
1217
  continue;
1181
1218
  }
1182
1219
  closeNest();
1183
- if (item.content.length > 0) item.content = [nestInlineWrappers(group.stack, item.content)];
1220
+ if (item.content.length > 0) item.content = [nestInlineWrappers(outerStack, item.content)];
1184
1221
  nested.push(item);
1185
1222
  }
1186
1223
  closeNest();
@@ -1214,7 +1251,14 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1214
1251
  let currentHyperlinkKey = null;
1215
1252
  let currentTrackedChange;
1216
1253
  const sourceRunOwners = /* @__PURE__ */ new WeakMap();
1254
+ const sourceHyperlinkIndices = /* @__PURE__ */ new WeakMap();
1217
1255
  const openedComments = /* @__PURE__ */ new Set();
1256
+ const createIndexedHyperlink = (mark) => {
1257
+ const hyperlink = createHyperlink(mark);
1258
+ const index = expectHyperlinkMarkAttrs(mark)._docxHyperlinkIndex;
1259
+ if (typeof index === "number") sourceHyperlinkIndices.set(hyperlink, index);
1260
+ return hyperlink;
1261
+ };
1218
1262
  const commentLastOffset = /* @__PURE__ */ new Map();
1219
1263
  paragraph.forEach((node, offset) => {
1220
1264
  for (const commentId of getCommentMarkIds(node.marks)) commentLastOffset.set(commentId, offset);
@@ -1298,14 +1342,20 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1298
1342
  */
1299
1343
  const enterWrapperGroup = (node) => {
1300
1344
  const stack = inlineWrapperStackOf(node);
1301
- const key = inlineWrapperStackKey(stack);
1345
+ const origin = inlineWrapperHyperlinkOriginOf(node);
1346
+ const key = [
1347
+ inlineWrapperStackKey(stack),
1348
+ origin?.hyperlinkIndex ?? "",
1349
+ origin?.stackStart ?? ""
1350
+ ].join("\0");
1302
1351
  if (key === wrapperGroupKey) return;
1303
1352
  flushCurrentInline();
1304
1353
  currentTrackedChange = void 0;
1305
1354
  wrapperGroupKey = key;
1306
1355
  wrapperGroups.push({
1307
1356
  start: content.length,
1308
- stack
1357
+ stack,
1358
+ ...origin ? { origin } : {}
1309
1359
  });
1310
1360
  };
1311
1361
  const processInlineNode = (node, offset) => {
@@ -1332,12 +1382,6 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1332
1382
  content.push({ ...start }, { ...end });
1333
1383
  return;
1334
1384
  }
1335
- if (node.type.name === "moveRangeBoundary") {
1336
- flushCurrentInline();
1337
- currentTrackedChange = void 0;
1338
- content.push({ ...expectMoveRangeBoundaryAttrs(node) });
1339
- return;
1340
- }
1341
1385
  const linkMark = node.marks.find((m) => m.type.name === "hyperlink");
1342
1386
  const noteRefMark = node.marks.find((m) => m.type.name === "footnoteRef");
1343
1387
  const insertionMark = node.marks.find((m) => m.type.name === "insertion");
@@ -1354,10 +1398,8 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1354
1398
  };
1355
1399
  if (textBoxAnchorMarkers.has(anchorId)) return;
1356
1400
  textBoxAnchorMarkers.set(anchorId, marker);
1357
- const anchoredContent = linkMark ? {
1358
- ...createHyperlink(linkMark),
1359
- children: [marker]
1360
- } : marker;
1401
+ const anchoredContent = linkMark ? createIndexedHyperlink(linkMark) : marker;
1402
+ if (anchoredContent.type === "hyperlink") anchoredContent.children.push(marker);
1361
1403
  const changeMark = insertionMark ?? deletionMark;
1362
1404
  if (!changeMark) {
1363
1405
  content.push(anchoredContent);
@@ -1409,7 +1451,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1409
1451
  if (linkMark) {
1410
1452
  const linkKey = getLinkKey(linkMark);
1411
1453
  if (!currentTrackedChange || currentTrackedChange.type !== "hyperlink" || currentTrackedChange.key !== trackedChangeKey || currentTrackedChange.hyperlinkKey !== linkKey) {
1412
- const hyperlink = createHyperlink(linkMark);
1454
+ const hyperlink = createIndexedHyperlink(linkMark);
1413
1455
  const wrapper = createTrackedRunWrapper(type, info, hyperlink);
1414
1456
  content.push(wrapper);
1415
1457
  currentTrackedChange = {
@@ -1455,6 +1497,10 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1455
1497
  currentTrackedChange.wrapper.content.push(bookmarkMarkerFromAttrs(expectBookmarkBoundaryAttrs(node)));
1456
1498
  return;
1457
1499
  }
1500
+ if (node.type.name === "moveRangeBoundary") {
1501
+ currentTrackedChange.wrapper.content.push({ ...expectMoveRangeBoundaryAttrs(node) });
1502
+ return;
1503
+ }
1458
1504
  if (node.type.name === "field" || node.type.name === "structuredField") {
1459
1505
  currentTrackedChange.wrapper.content.push(createFieldFromNode(node, {
1460
1506
  ...formattingContext,
@@ -1488,6 +1534,12 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1488
1534
  appendDirectRun(node, ownedRun, node.isText);
1489
1535
  return;
1490
1536
  }
1537
+ if (node.type.name === "moveRangeBoundary") {
1538
+ flushCurrentInline();
1539
+ currentTrackedChange = void 0;
1540
+ content.push({ ...expectMoveRangeBoundaryAttrs(node) });
1541
+ return;
1542
+ }
1491
1543
  if (noteRefMark && !linkMark) {
1492
1544
  flushCurrentInline();
1493
1545
  content.push(createNoteReferenceRun(noteRefMark, node.marks, formattingContext));
@@ -1497,7 +1549,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1497
1549
  const linkKey = getLinkKey(linkMark);
1498
1550
  if (currentHyperlink && currentHyperlinkKey === linkKey) {} else {
1499
1551
  flushCurrentInline();
1500
- currentHyperlink = createHyperlink(linkMark);
1552
+ currentHyperlink = createIndexedHyperlink(linkMark);
1501
1553
  currentHyperlinkKey = linkKey;
1502
1554
  }
1503
1555
  addNodeToHyperlink({
@@ -1588,7 +1640,7 @@ function extractParagraphContent(paragraph, _documentCounts, emptyHyperlinks, te
1588
1640
  type: "commentRangeEnd",
1589
1641
  id: commentId
1590
1642
  });
1591
- return nestInlineWrapperGroups(content, wrapperGroups);
1643
+ return nestInlineWrapperGroups(content, wrapperGroups, sourceHyperlinkIndices);
1592
1644
  }
1593
1645
  function createTrackedChangeRun({ baseParagraphFormatting, inheritedFormatting, marks, node, paragraphMarkFormatting, paragraphMarkPrecedesStyle, styleResolver }) {
1594
1646
  const formattingContext = {
@@ -1646,6 +1698,7 @@ function createEmptyHyperlink(attrs) {
1646
1698
  if (attrs.target !== void 0) hyperlink.target = attrs.target;
1647
1699
  if (attrs.history !== void 0) hyperlink.history = attrs.history;
1648
1700
  if (attrs.docLocation !== void 0) hyperlink.docLocation = attrs.docLocation;
1701
+ for (const stack of attrs._docxEmptyWrapperStacks ?? []) hyperlink.children.push(nestInlineWrappers(stack, []));
1649
1702
  return hyperlink;
1650
1703
  }
1651
1704
  function getCommentMarkIds(marks) {
@@ -3298,6 +3351,9 @@ function convertPMTableCell(node, documentCounts, styleResolver = null) {
3298
3351
  } else if (contentNode.type.name === "table") {
3299
3352
  content.push(convertPMTable(contentNode, documentCounts, styleResolver));
3300
3353
  previousStandaloneTextBox = null;
3354
+ } else if (contentNode.type.name === "blockSdt") {
3355
+ content.push(convertPMBlockSdt(contentNode, styleResolver));
3356
+ previousStandaloneTextBox = null;
3301
3357
  } else if (contentNode.type.name === "textBox") previousStandaloneTextBox = appendTextBoxBlock(content, contentNode, {
3302
3358
  pendingPageBreaks: 0,
3303
3359
  previousStandaloneTextBox,
@@ -486,6 +486,7 @@ function convertParagraph(paragraph, styleResolver, context, extraRunFormatting,
486
486
  nextRunIdentityId
487
487
  });
488
488
  if (linkNodes.length === 0) {
489
+ const emptyWrapperStacks = emptyInlineWrapperStacks(content.children);
489
490
  emptyHyperlinks ??= [];
490
491
  emptyHyperlinks.push({
491
492
  offset: inlineOffset,
@@ -495,7 +496,8 @@ function convertParagraph(paragraph, styleResolver, context, extraRunFormatting,
495
496
  ...content.rId !== void 0 ? { rId: content.rId } : {},
496
497
  ...content.target !== void 0 ? { target: content.target } : {},
497
498
  ...content.history !== void 0 ? { history: content.history } : {},
498
- ...content.docLocation !== void 0 ? { docLocation: content.docLocation } : {}
499
+ ...content.docLocation !== void 0 ? { docLocation: content.docLocation } : {},
500
+ ...emptyWrapperStacks.length === 0 ? {} : { _docxEmptyWrapperStacks: emptyWrapperStacks }
499
501
  });
500
502
  break;
501
503
  }
@@ -667,6 +669,7 @@ function convertTrackedChange(change, markType, nextHyperlinkInstanceIndex, next
667
669
  id: item.id,
668
670
  displacedByCustomXml: item.displacedByCustomXml
669
671
  }));
672
+ 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
673
  else if (item.type === "preservedInline") itemNodes.push(preservedInlineNode(item));
671
674
  else panic(`Unsupported tracked-run content: ${JSON.stringify(item)}`);
672
675
  nodes.push(...withInlineWrapperMark(itemNodes, stack));
@@ -1035,6 +1038,7 @@ function blockHasMeaningfulContent(block) {
1035
1038
  if (block.type === "preservedBlock") return true;
1036
1039
  if (block.type === "bookmarkStart" || block.type === "bookmarkEnd") return true;
1037
1040
  if (block.type === "table") return block.rows.some((row) => row.cells.some((cell) => tableCellHasMeaningfulContent(cell)));
1041
+ if (block.type === "blockSdt") return true;
1038
1042
  return block.content.some(paragraphContentHasMeaningfulContent);
1039
1043
  }
1040
1044
  function paragraphContentHasMeaningfulContent(content) {
@@ -1394,28 +1398,35 @@ function convertTableCell({ cell, styleResolver, context, isHeader, gridWidthPer
1394
1398
  }
1395
1399
  if (preserveVMergeRestart) attrs._preserveVMergeRestart = true;
1396
1400
  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
- }
1401
+ const convertCellBlocks = (blocks) => {
1402
+ const nodes = [];
1403
+ for (const block of blocks) switch (block.type) {
1404
+ case "paragraph":
1405
+ nodes.push(...convertParagraphWithTextBoxes(block, styleResolver, {
1406
+ textBoxGroupId: context.nextTextBoxGroupId(),
1407
+ context,
1408
+ ...conditionalStyle?.rPr !== void 0 ? { extraRunFormatting: conditionalStyle.rPr } : {},
1409
+ ...conditionalStyle?.pPr !== void 0 ? { tableParagraphOverlay: conditionalStyle.pPr } : {}
1410
+ }));
1411
+ break;
1412
+ case "table":
1413
+ nodes.push(convertTable(block, styleResolver, context));
1414
+ break;
1415
+ case "blockSdt":
1416
+ nodes.push(convertBlockSdt(block, convertCellBlocks));
1417
+ break;
1418
+ case "preservedBlock":
1419
+ nodes.push(convertPreservedBlock(block));
1420
+ break;
1421
+ case "bookmarkStart":
1422
+ case "bookmarkEnd":
1423
+ nodes.push(convertBlockBookmarkBoundary(block));
1424
+ break;
1425
+ default: panic(`Unsupported table cell content: ${JSON.stringify(block)}`);
1426
+ }
1427
+ return nodes;
1428
+ };
1429
+ const contentNodes = convertCellBlocks(cell.content);
1419
1430
  if (contentNodes.length === 0) contentNodes.push(schema.node("paragraph", {}, []));
1420
1431
  const nodeType = isHeader ? "tableHeader" : "tableCell";
1421
1432
  return schema.node(nodeType, attrs, contentNodes);
@@ -1527,27 +1538,32 @@ const withInlineWrapperStacks = (content, stack = []) => content.flatMap((item)
1527
1538
  content: item,
1528
1539
  stack
1529
1540
  }]);
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) => {
1541
+ /** Wrapper nests with no leaf to carry their stack as a ProseMirror mark. */
1542
+ const emptyInlineWrapperStacks = (content, stack = []) => content.flatMap((item) => {
1543
+ if (item.type !== "inlineWrapper") return [];
1544
+ const nested = [...stack, inlineWrapperLayer(item)];
1545
+ return item.content.length === 0 ? [nested] : emptyInlineWrapperStacks(item.content, nested);
1546
+ });
1547
+ const withInlineWrapperMark = (nodes, stack, origin) => {
1544
1548
  const markType = schema.marks["inlineWrapper"];
1545
1549
  if (stack.length === 0 || !markType) return nodes;
1546
1550
  return nodes.map((node) => {
1547
- if (!node.isText && (!node.isInline || !node.type.allowsMarkType(markType))) return node;
1551
+ if (!node.isInline) return node;
1548
1552
  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));
1553
+ const innerAttrs = inner === void 0 ? void 0 : expectInlineWrapperMarkAttrs(inner);
1554
+ const layers = innerAttrs === void 0 ? stack : [...stack, ...innerAttrs.stack];
1555
+ const inheritedOrigin = typeof innerAttrs?._docxHyperlinkIndex !== "number" || typeof innerAttrs._docxInsideHyperlinkStackStart !== "number" ? void 0 : {
1556
+ hyperlinkIndex: innerAttrs._docxHyperlinkIndex,
1557
+ stackStart: stack.length + innerAttrs._docxInsideHyperlinkStackStart
1558
+ };
1559
+ const provenance = origin ?? inheritedOrigin;
1560
+ return node.mark(markType.create({
1561
+ stack: layers,
1562
+ ...provenance === void 0 ? {} : {
1563
+ _docxHyperlinkIndex: provenance.hyperlinkIndex,
1564
+ _docxInsideHyperlinkStackStart: provenance.stackStart
1565
+ }
1566
+ }).addToSet(node.marks));
1551
1567
  });
1552
1568
  };
1553
1569
  /**
@@ -1570,9 +1586,17 @@ function convertMathEquation(math) {
1570
1586
  function convertInlineSdt(sdt, nextHyperlinkInstanceIndex, nextRunIdentityId, getInheritedRunFormatting, trackedRunFormattingResolvers, styleResolver, textBoxAnchors, wrappedBy = []) {
1571
1587
  const props = sdt.properties;
1572
1588
  const inlineNodes = [];
1573
- for (const { content, stack } of withInlineWrapperStacks(sdt.content, wrappedBy)) {
1589
+ const stacked = withInlineWrapperStacks(sdt.content, wrappedBy);
1590
+ const emptyRanges = planEmptyRanges(stacked.map(({ content }) => content));
1591
+ for (const [index, { content, stack }] of stacked.entries()) {
1574
1592
  if (!isInlineSdtContent(content)) continue;
1575
1593
  const itemNodes = [];
1594
+ const anchored = emptyRanges.anchorAt.get(index);
1595
+ if (anchored) {
1596
+ inlineNodes.push(...withInlineWrapperMark([schema.node(RANGE_ANCHOR_NODE_NAME, anchored)], stack));
1597
+ continue;
1598
+ }
1599
+ if (emptyRanges.closedAt.has(index)) continue;
1576
1600
  switch (content.type) {
1577
1601
  case "run":
1578
1602
  itemNodes.push(...convertRun(content, getInheritedRunFormatting(content.formatting), nextRunIdentityId, styleResolver, textBoxAnchors));
@@ -1633,6 +1657,12 @@ function convertInlineSdt(sdt, nextHyperlinkInstanceIndex, nextRunIdentityId, ge
1633
1657
  displacedByCustomXml: content.displacedByCustomXml
1634
1658
  }));
1635
1659
  break;
1660
+ case "moveFromRangeStart":
1661
+ case "moveFromRangeEnd":
1662
+ case "moveToRangeStart":
1663
+ case "moveToRangeEnd":
1664
+ itemNodes.push(schema.node(MOVE_RANGE_BOUNDARY_NODE_NAME, { marker: content }));
1665
+ break;
1636
1666
  case "preservedInline":
1637
1667
  itemNodes.push(preservedInlineNode(content));
1638
1668
  break;
@@ -2331,7 +2361,10 @@ function convertHyperlink(hyperlink, { getInheritedRunFormatting, styleResolver,
2331
2361
  }
2332
2362
  default: break;
2333
2363
  }
2334
- nodes.push(...withInlineWrapperMark(childNodes, stack));
2364
+ nodes.push(...withInlineWrapperMark(childNodes, stack, {
2365
+ hyperlinkIndex,
2366
+ stackStart: 0
2367
+ }));
2335
2368
  }
2336
2369
  return nodes;
2337
2370
  }
@@ -7,6 +7,7 @@ import { panic } from "better-result";
7
7
  /**
8
8
  * Hyperlink Mark Extension
9
9
  */
10
+ const DOCX_HYPERLINK_INDEX_ATTRIBUTE = "data-docx-hyperlink-index";
10
11
  /**
11
12
  * Sanitize an href arriving from (or being emitted to) the DOM: pasted HTML,
12
13
  * clipboard content, or a mark attr already stored on the document. Only
@@ -90,19 +91,25 @@ const HyperlinkExtension = createMarkExtension({
90
91
  inclusive: false,
91
92
  parseDOM: [{
92
93
  tag: "a[href]",
93
- getAttrs: (dom) => ({
94
- href: sanitizeStoredHref(dom.getAttribute("href") ?? void 0),
95
- tooltip: dom.getAttribute("title") ?? void 0,
96
- target: dom.getAttribute("target") ?? void 0
97
- })
94
+ getAttrs: (dom) => {
95
+ const index = dom.getAttribute(DOCX_HYPERLINK_INDEX_ATTRIBUTE);
96
+ const docxHyperlinkIndex = index !== null && /^\d+$/u.test(index) && Number.isSafeInteger(Number(index)) ? Number(index) : void 0;
97
+ return {
98
+ href: sanitizeStoredHref(dom.getAttribute("href") ?? void 0),
99
+ tooltip: dom.getAttribute("title") ?? void 0,
100
+ target: dom.getAttribute("target") ?? void 0,
101
+ ...docxHyperlinkIndex === void 0 ? {} : { _docxHyperlinkIndex: docxHyperlinkIndex }
102
+ };
103
+ }
98
104
  }],
99
105
  toDOM(mark) {
100
- const { href, target, tooltip } = expectHyperlinkMarkAttrs(mark);
106
+ const { href, target, tooltip, _docxHyperlinkIndex } = expectHyperlinkMarkAttrs(mark);
101
107
  const domAttrs = {
102
108
  href: sanitizeStoredHref(href),
103
109
  ...anchorTargetAttrs(target)
104
110
  };
105
111
  if (tooltip) domAttrs["title"] = tooltip;
112
+ if (typeof _docxHyperlinkIndex === "number") domAttrs[DOCX_HYPERLINK_INDEX_ATTRIBUTE] = String(_docxHyperlinkIndex);
106
113
  return [
107
114
  "a",
108
115
  domAttrs,
@@ -16,6 +16,7 @@ import { panic } from "better-result";
16
16
  * extend the wrapper over text the author wrote outside it.
17
17
  */
18
18
  const INLINE_WRAPPER_MARK_NAME = "inlineWrapper";
19
+ const INLINE_WRAPPER_HYPERLINK_ORIGIN_ATTRIBUTE = "data-inline-wrapper-hyperlink-origin";
19
20
  /** What a tagged layer's element name is spelled as on the rendered span. */
20
21
  const TAGGED_ELEMENT_ATTRIBUTES = {
21
22
  smartTag: "data-smart-tag-element",
@@ -54,22 +55,39 @@ const InlineWrapperExtension = createMarkExtension({
54
55
  name: INLINE_WRAPPER_MARK_NAME,
55
56
  schemaMarkName: INLINE_WRAPPER_MARK_NAME,
56
57
  markSpec: {
57
- attrs: {
58
- /** Outermost first. `null` is no wrapper, which is the absence of the mark. */
59
- stack: { default: null } },
58
+ attrs: {
59
+ /** Outermost first. `null` is no wrapper, which is the absence of the mark. */
60
+ stack: { default: null },
61
+ _docxHyperlinkIndex: { default: null },
62
+ _docxInsideHyperlinkStackStart: { default: null }
63
+ },
60
64
  inclusive: false,
61
65
  parseDOM: [{
62
66
  tag: `[${INLINE_WRAPPER_STACK_ATTRIBUTE}]`,
63
67
  getAttrs(dom) {
64
68
  const stack = parseInlineWrapperStack(dom.getAttribute(INLINE_WRAPPER_STACK_ATTRIBUTE));
65
- return stack === null ? false : { stack };
69
+ if (stack === null) return false;
70
+ const origin = dom.getAttribute(INLINE_WRAPPER_HYPERLINK_ORIGIN_ATTRIBUTE);
71
+ if (origin === null) return { stack };
72
+ const match = /^(\d+):(\d+)$/u.exec(origin);
73
+ if (match === null) return false;
74
+ const hyperlinkIndex = Number(match[1]);
75
+ const stackStart = Number(match[2]);
76
+ if (!Number.isSafeInteger(hyperlinkIndex) || !Number.isSafeInteger(stackStart) || stackStart >= stack.length) return false;
77
+ return {
78
+ stack,
79
+ _docxHyperlinkIndex: hyperlinkIndex,
80
+ _docxInsideHyperlinkStackStart: stackStart
81
+ };
66
82
  }
67
83
  }],
68
84
  toDOM(mark) {
69
- const { stack } = expectInlineWrapperMarkAttrs(mark);
85
+ const { stack, _docxHyperlinkIndex, _docxInsideHyperlinkStackStart } = expectInlineWrapperMarkAttrs(mark);
70
86
  const innermost = stack.at(-1);
71
87
  if (innermost === void 0) panic("An inline wrapper mark carries at least one layer");
72
- return layerElement(innermost, { [INLINE_WRAPPER_STACK_ATTRIBUTE]: serializeInlineWrapperStack(stack) });
88
+ const attributes = { [INLINE_WRAPPER_STACK_ATTRIBUTE]: serializeInlineWrapperStack(stack) };
89
+ if (typeof _docxHyperlinkIndex === "number" && typeof _docxInsideHyperlinkStackStart === "number") attributes[INLINE_WRAPPER_HYPERLINK_ORIGIN_ATTRIBUTE] = `${_docxHyperlinkIndex}:${_docxInsideHyperlinkStackStart}`;
90
+ return layerElement(innermost, attributes);
73
91
  }
74
92
  }
75
93
  });
@@ -13,11 +13,15 @@ const surveyCommentReferences = (doc) => {
13
13
  doc.descendants((node, position) => {
14
14
  if (node.type.name === "commentReference") {
15
15
  const attrs = readCommentReferenceAttrs(node);
16
- references.push({
17
- commentId: attrs.ok ? attrs.value.commentId : -1,
16
+ references.push(attrs.ok ? {
17
+ status: "valid",
18
+ commentId: attrs.value.commentId,
18
19
  position,
19
- size: node.nodeSize,
20
- malformed: !attrs.ok
20
+ size: node.nodeSize
21
+ } : {
22
+ status: "malformed",
23
+ position,
24
+ size: node.nodeSize
21
25
  });
22
26
  return false;
23
27
  }
@@ -60,7 +64,7 @@ const CommentReferenceExtension = createNodeExtension({
60
64
  tag: "span[data-docx-comment-reference]",
61
65
  getAttrs(dom) {
62
66
  const raw = dom.getAttribute("data-docx-comment-reference");
63
- if (raw === null || !/^(?:0|[1-9]\d*)$/.test(raw)) return false;
67
+ if (raw === null || !/^-?(?:0|[1-9]\d*)$/.test(raw)) return false;
64
68
  const commentId = Number(raw);
65
69
  return Number.isSafeInteger(commentId) ? { commentId } : false;
66
70
  }
@@ -367,7 +367,7 @@ const statedCellWidth = (value, type) => ({
367
367
  }
368
368
  });
369
369
  const tableCellSpec = {
370
- content: "(paragraph | table | textBox | preservedBlock | blockBookmarkBoundary)+",
370
+ content: "(paragraph | table | textBox | blockSdt | preservedBlock | blockBookmarkBoundary)+",
371
371
  tableRole: "cell",
372
372
  isolating: true,
373
373
  attrs: {
@@ -436,7 +436,7 @@ const tableCellSpec = {
436
436
  }
437
437
  };
438
438
  const tableHeaderSpec = {
439
- content: "(paragraph | table | textBox | preservedBlock | blockBookmarkBoundary)+",
439
+ content: "(paragraph | table | textBox | blockSdt | preservedBlock | blockBookmarkBoundary)+",
440
440
  tableRole: "header_cell",
441
441
  isolating: true,
442
442
  attrs: {
@@ -170,6 +170,17 @@ type InlineWrapperKind = InlineWrapperLayer["kind"];
170
170
  */
171
171
  type InlineWrapperAttrs = {
172
172
  stack: readonly InlineWrapperLayer[];
173
+ /**
174
+ * The imported hyperlink whose children the suffix of `stack` came from.
175
+ *
176
+ * ProseMirror stores marks as an unordered set, so a link mark and a wrapper
177
+ * mark otherwise cannot say whether `<w:bdo>` surrounded `<w:hyperlink>` or
178
+ * was one of its children. This is parser provenance, not editable wrapper
179
+ * data: it is meaningful only alongside the matching hyperlink mark.
180
+ */
181
+ _docxHyperlinkIndex?: number;
182
+ /** The first stack layer authored inside `_docxHyperlinkIndex`. */
183
+ _docxInsideHyperlinkStackStart?: number;
173
184
  };
174
185
  declare const COMPLEX_SCRIPT_RUN_PROPERTY_KEYS: readonly ["boldCs", "italicCs", "fontSizeCs"];
175
186
  type ComplexScriptRunPropertyKey = (typeof COMPLEX_SCRIPT_RUN_PROPERTY_KEYS)[number];
@@ -1,5 +1,5 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
- import { TrackedChangeProvenance } from "./marks.js";
2
+ import { InlineWrapperLayer, TrackedChangeProvenance } from "./marks.js";
3
3
  import "../../types/index.js";
4
4
  import { OutlineStyleAttr } from "../../types/documentEnumValues.js";
5
5
  import { SpacingExplicit } from "../../types/formatting.js";
@@ -212,9 +212,9 @@ type ParagraphAttrs = {
212
212
  id: number;
213
213
  name: string;
214
214
  }[];
215
- /** Empty `w:hyperlink` elements cannot be represented as text marks.
216
- * Preserve their relationship metadata at the paragraph boundary so a
217
- * no-edit DOCX round-trip does not silently drop hyperlink elements. */
215
+ /** A `w:hyperlink` with no projectable leaf cannot be represented as a text mark.
216
+ * Preserve its metadata and any empty transparent-wrapper children at the
217
+ * paragraph boundary so an editor round trip does not drop either element. */
218
218
  _emptyHyperlinks?: {
219
219
  /** ProseMirror inline offset where the zero-width hyperlink appeared. */
220
220
  offset: number;
@@ -225,6 +225,8 @@ type ParagraphAttrs = {
225
225
  target?: document_d_exports.Hyperlink["target"];
226
226
  history?: document_d_exports.Hyperlink["history"];
227
227
  docLocation?: document_d_exports.Hyperlink["docLocation"];
228
+ /** Empty transparent-wrapper nests authored as children of this link. */
229
+ _docxEmptyWrapperStacks?: readonly (readonly InlineWrapperLayer[])[];
228
230
  }[];
229
231
  /**
230
232
  * Run-in heading flag: this paragraph's mark carries
@@ -16,7 +16,7 @@ declare const TRACKED_RUN_INLINE_ATOM_DISPOSITIONS: Readonly<{
16
16
  readonly hardBreak: "carry";
17
17
  readonly image: "carry";
18
18
  readonly math: "carry";
19
- readonly moveRangeBoundary: "transparent";
19
+ readonly moveRangeBoundary: "carry";
20
20
  readonly pageBreakRun: "page-break-carrier";
21
21
  readonly preservedXml: "carry";
22
22
  readonly rangeAnchor: "transparent";
@@ -15,7 +15,7 @@ const TRACKED_RUN_INLINE_ATOM_DISPOSITIONS = Object.freeze({
15
15
  hardBreak: "carry",
16
16
  image: "carry",
17
17
  math: "carry",
18
- moveRangeBoundary: "transparent",
18
+ moveRangeBoundary: "carry",
19
19
  pageBreakRun: "page-break-carrier",
20
20
  preservedXml: "carry",
21
21
  rangeAnchor: "transparent",