@stll/folio-core 0.60.0 → 0.61.0

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 (126) hide show
  1. package/README.md +13 -0
  2. package/dist/ai-edits/apply.js +139 -32
  3. package/dist/ai-edits/headless.d.ts +3 -0
  4. package/dist/ai-edits/headless.js +57 -63
  5. package/dist/ai-edits/note-story-inventory.d.ts +7 -0
  6. package/dist/ai-edits/note-story-inventory.js +11 -0
  7. package/dist/ai-edits/read.js +16 -9
  8. package/dist/ai-edits/revisionStretches.js +52 -1
  9. package/dist/ai-edits/table-row-column-mutations.js +6 -1
  10. package/dist/compare/reproducible-package.js +2 -1
  11. package/dist/compare/verification.js +1 -0
  12. package/dist/compat/eigenpal.d.ts +3 -1
  13. package/dist/controller/canonicalClipboard.js +2 -1
  14. package/dist/controller/canonicalListRendering.d.ts +4 -6
  15. package/dist/controller/canonicalListRendering.js +3 -157
  16. package/dist/controller/headerFooterEditorManager.d.ts +1 -0
  17. package/dist/controller/headerFooterEditorManager.js +11 -0
  18. package/dist/controller/hiddenEditorManager.d.ts +1 -1
  19. package/dist/controller/hiddenEditorManager.js +3 -1
  20. package/dist/controller/layoutPipeline.d.ts +3 -1
  21. package/dist/controller/layoutPipeline.js +13 -4
  22. package/dist/controller/layoutSession.d.ts +2 -1
  23. package/dist/display-list/build/imagePrimitives.js +3 -6
  24. package/dist/docx/canonicalStoryRepack.js +3 -2
  25. package/dist/docx/commentTextParagraph.d.ts +6 -0
  26. package/dist/docx/commentTextParagraph.js +15 -0
  27. package/dist/docx/conformance.js +6 -1
  28. package/dist/docx/diagramPreview.js +2 -1
  29. package/dist/docx/documentParser.d.ts +13 -1
  30. package/dist/docx/documentParser.js +1 -1
  31. package/dist/docx/encryption/compoundFile.d.ts +0 -8
  32. package/dist/docx/encryption/compoundFile.js +2 -1
  33. package/dist/docx/encryption/cryptoBytes.d.ts +2 -2
  34. package/dist/docx/encryption/cryptoBytes.js +3 -18
  35. package/dist/docx/encryption/encryptionInfo.d.ts +0 -5
  36. package/dist/docx/encryption/encryptionInfo.js +2 -1
  37. package/dist/docx/ensureParaIds.js +2 -1
  38. package/dist/docx/groupDrawingParser.js +6 -1
  39. package/dist/docx/listModelNormalization.d.ts +10 -0
  40. package/dist/docx/listModelNormalization.js +166 -0
  41. package/dist/docx/metadataPrivacy.js +3 -2
  42. package/dist/docx/metafileRaster.js +2 -1
  43. package/dist/docx/packageParts.js +6 -5
  44. package/dist/docx/paragraphParser.js +6 -4
  45. package/dist/docx/parser.js +5 -4
  46. package/dist/docx/preservedRunContent.js +11 -2
  47. package/dist/docx/removeHeaderFooterParts.js +8 -7
  48. package/dist/docx/replyToComment.js +3 -13
  49. package/dist/docx/rezip.js +88 -89
  50. package/dist/docx/runParser.js +12 -6
  51. package/dist/docx/runPropertyPresence.d.ts +11 -0
  52. package/dist/docx/runPropertyPresence.js +14 -0
  53. package/dist/docx/selectiveSave.js +15 -14
  54. package/dist/docx/serializer/runSerializer.js +3 -2
  55. package/dist/docx/serializer/tableSerializer.js +2 -1
  56. package/dist/docx/server/applyDocxXmlPatchProposal.js +2 -2
  57. package/dist/docx/server/boundedArchive.js +2 -1
  58. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +2 -1
  59. package/dist/docx/server/inspectDocxPackage.js +5 -1
  60. package/dist/docx/server/validateDocxConformance.js +2 -1
  61. package/dist/docx/tableParser.js +7 -4
  62. package/dist/docx/tableRowPropertyPresence.d.ts +9 -0
  63. package/dist/docx/tableRowPropertyPresence.js +15 -0
  64. package/dist/docx/unzip.js +2 -1
  65. package/dist/docx/xmlEncoding.js +6 -5
  66. package/dist/fonts/fontsourceFaces.js +2 -1
  67. package/dist/fonts/sfnt/parse.js +6 -9
  68. package/dist/fonts/sfnt/subset.js +2 -1
  69. package/dist/fonts/sfnt/tables.js +2 -3
  70. package/dist/fonts/sfnt/woff.js +2 -1
  71. package/dist/index.d.ts +3 -1
  72. package/dist/internal/revisionResolutionInline.d.ts +2 -1
  73. package/dist/internal/revisionResolutionInline.js +22 -1
  74. package/dist/internal/wholeStoryRevisionResolution.js +24 -7
  75. package/dist/layout-bridge/convert/flowConversionShared.d.ts +2 -0
  76. package/dist/layout-bridge/convert/footnoteLayout.d.ts +4 -0
  77. package/dist/layout-bridge/convert/footnoteLayout.js +13 -3
  78. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +5 -0
  79. package/dist/layout-bridge/convert/headerFooterLayout.js +12 -15
  80. package/dist/layout-bridge/convert/paragraphRuns.js +2 -1
  81. package/dist/layout-engine/measure/crossRunWords.js +12 -2
  82. package/dist/managers/AutoSaveManager.js +2 -1
  83. package/dist/managers/EditorModeManager.d.ts +10 -1
  84. package/dist/managers/EditorModeManager.js +15 -1
  85. package/dist/managers/TableSelectionManager.d.ts +1 -1
  86. package/dist/managers/autoSaveCodec.js +2 -6
  87. package/dist/managers/reviewDisplayMode.d.ts +8 -0
  88. package/dist/managers/reviewDisplayMode.js +15 -0
  89. package/dist/paged-layout/pageText.d.ts +11 -2
  90. package/dist/paged-layout/pageText.js +8 -3
  91. package/dist/pdf/fonts.js +5 -14
  92. package/dist/pdf/images.js +3 -2
  93. package/dist/pdf/objects.d.ts +0 -11
  94. package/dist/pdf/objects.js +7 -9
  95. package/dist/pdf/writePdf.js +2 -1
  96. package/dist/prosemirror/commands/comments.js +51 -13
  97. package/dist/prosemirror/containerFinalParagraph.d.ts +8 -8
  98. package/dist/prosemirror/containerFinalParagraph.js +8 -8
  99. package/dist/prosemirror/conversion/fromProseDoc.js +9 -4
  100. package/dist/prosemirror/conversion/toProseDoc.js +4 -4
  101. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +16 -2
  102. package/dist/prosemirror/findReplaceSelection.d.ts +11 -1
  103. package/dist/prosemirror/findReplaceSelection.js +15 -1
  104. package/dist/prosemirror/markupViewProjection.d.ts +10 -1
  105. package/dist/prosemirror/markupViewProjection.js +21 -3
  106. package/dist/prosemirror/plugins/transactionInvariants.js +6 -1
  107. package/dist/prosemirror/replacedAnnotations.d.ts +15 -1
  108. package/dist/prosemirror/replacedAnnotations.js +71 -12
  109. package/dist/prosemirror/runFormattingReconciliation.d.ts +3 -1
  110. package/dist/prosemirror/runFormattingReconciliation.js +10 -2
  111. package/dist/prosemirror/runIdentityAcrossRevisions.d.ts +5 -7
  112. package/dist/prosemirror/runIdentityAcrossRevisions.js +66 -38
  113. package/dist/prosemirror/runPropertyChangeResolution.js +13 -1
  114. package/dist/utils/base64.d.ts +1 -31
  115. package/dist/utils/base64.js +19 -33
  116. package/dist/utils/clipboard.js +5 -4
  117. package/dist/utils/colorResolver.js +11 -2
  118. package/dist/utils/findReplace.d.ts +5 -0
  119. package/dist/utils/replaceText.js +21 -4
  120. package/dist/utils/sliceText.d.ts +2 -0
  121. package/dist/utils/sliceText.js +2 -0
  122. package/dist/utils/textEncoding.d.ts +2 -0
  123. package/dist/utils/textEncoding.js +2 -0
  124. package/dist/utils/tiffConverter.d.ts +0 -9
  125. package/dist/utils/tiffConverter.js +2 -4
  126. package/package.json +51 -25
@@ -13,11 +13,13 @@ import "../extensions/nodes/SdtExtension.js";
13
13
  import { canonicalFieldNode } from "../fieldRepresentation.js";
14
14
  import { resolveStateStory } from "../markupViewProjection.js";
15
15
  import { joinAtParagraphMark } from "../paragraphMarkJoin.js";
16
+ import { carryParagraphProperties, paragraphLeftAfter } from "../paragraphPropertyCarry.js";
16
17
  import { getDocumentNumbering } from "../plugins/documentNumbering.js";
17
18
  import { getDocumentStyleResolver } from "../plugins/documentStyleState.js";
18
19
  import { resolvedPreservedXmlAttrs } from "../preservedXmlReview.js";
19
20
  import { setParagraphAttrsWithRebasedRunFormatting } from "../rebaseParagraphRunFormatting.js";
20
21
  import { rejoinRunsAt } from "../rejoinRunCarriers.js";
22
+ import { commentPoint, resolvedCommentPoints } from "../replacedAnnotations.js";
21
23
  import { getFolioNodeRevisionCarriers, nodePropertyRevisionSites } from "../revisionCarriers.js";
22
24
  import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
23
25
  import { reconstructRejectedRunFormattingMarks, restoreHistoricalRunFormatting } from "../runPropertyChangeResolution.js";
@@ -100,6 +102,16 @@ function resolveChange(from, to, mode, revisionIds) {
100
102
  const removeKeptMarksInBulk = revisionSet === null && keepType !== void 0;
101
103
  const matchesRevision = (mark) => revisionSet === null || typeof mark.attrs["revisionId"] === "number" && revisionSet.has(mark.attrs["revisionId"]);
102
104
  if (dispatch) {
105
+ const points = mode === "accept" ? resolvedCommentPoints({
106
+ doc: state.doc,
107
+ retiresStructure: (node, position) => {
108
+ if (node.type.name === "paragraph") return collectPPrMarkOp(node, position, from, to, mode, revisionSet)?.action === "join";
109
+ if (!rangeCoversNode(from, to, position, node)) return false;
110
+ if (node.type.name === "tableRow") return collectTableRowStructuralOp(node, position, mode, revisionSet)?.action === "remove";
111
+ return collectTableCellStructuralOps(node, position, mode, revisionSet).some((op) => op.type === "membership" && op.action === "remove");
112
+ },
113
+ removes: (node, position) => position >= from && position + node.nodeSize <= to && node.marks.some((mark) => mark.type === removeType && matchesRevision(mark))
114
+ }) : /* @__PURE__ */ new Map();
103
115
  const tr = state.tr;
104
116
  tr.setMeta(JOINED_RUNS_RESTYLED_META, true);
105
117
  const deleteRanges = [];
@@ -110,6 +122,25 @@ function resolveChange(from, to, mode, revisionIds) {
110
122
  const tableCellStructuralOps = [];
111
123
  let removedSectionEndpointCount = 0;
112
124
  const removedSectionReferences = [];
125
+ const carryRestoredProperties = (position) => {
126
+ if (mode !== "reject" || revisionSet === null) return;
127
+ const source = tr.doc.nodeAt(position);
128
+ const mark = source ? expectParagraphAttrs(source).pPrMark : null;
129
+ if (!source || mark?.kind !== "del" && mark?.kind !== "moveFrom" || revisionSet.has(mark.info.id)) return;
130
+ const survivor = paragraphLeftAfter({
131
+ doc: tr.doc,
132
+ paragraphPos: position
133
+ });
134
+ if (survivor === null) return;
135
+ carryParagraphProperties({
136
+ tr,
137
+ position: survivor,
138
+ source,
139
+ styleResolver,
140
+ numbering,
141
+ revision: mark.info
142
+ });
143
+ };
113
144
  state.doc.nodesBetween(from, to, (node, pos) => {
114
145
  if (node.type.name === "paragraph") {
115
146
  const op = collectPPrMarkOp(node, pos, from, to, mode, revisionSet);
@@ -122,18 +153,21 @@ function resolveChange(from, to, mode, revisionIds) {
122
153
  styleResolver,
123
154
  numbering
124
155
  });
125
- if (nextAttrs) if (nextAttrs["styleId"] !== node.attrs["styleId"] && styleResolver) setParagraphAttrsWithRebasedRunFormatting({
126
- nextAttrs,
127
- paragraphPosition: pos,
128
- styleResolver,
129
- tr,
130
- ...mode === "reject" && { shouldRebase: (inline) => {
131
- const deleted = inline.marks.some(({ type }) => type.name === "deletion");
132
- const direct = inline.marks.some(({ type }) => type.name === "runFormattingOverride");
133
- return !deleted || !direct;
134
- } }
135
- });
136
- else tr.setNodeMarkup(pos, void 0, nextAttrs);
156
+ if (nextAttrs) {
157
+ if (nextAttrs["styleId"] !== node.attrs["styleId"] && styleResolver) setParagraphAttrsWithRebasedRunFormatting({
158
+ nextAttrs,
159
+ paragraphPosition: pos,
160
+ styleResolver,
161
+ tr,
162
+ ...mode === "reject" && { shouldRebase: (inline) => {
163
+ const deleted = inline.marks.some(({ type }) => type.name === "deletion");
164
+ const direct = inline.marks.some(({ type }) => type.name === "runFormattingOverride");
165
+ return !deleted || !direct;
166
+ } }
167
+ });
168
+ else tr.setNodeMarkup(pos, void 0, nextAttrs);
169
+ carryRestoredProperties(pos);
170
+ }
137
171
  return true;
138
172
  }
139
173
  if (node.type.name === "tableRow" && rangeCoversNode(from, to, pos, node)) {
@@ -388,7 +422,10 @@ function resolveChange(from, to, mode, revisionIds) {
388
422
  removedSectionEndpointCount++;
389
423
  removedSectionReferences.push(...sectionReferencesOf(paragraph));
390
424
  }
391
- } catch {}
425
+ } catch {
426
+ continue;
427
+ }
428
+ if (op.markWasAdded) carryRestoredProperties(mappedPos);
392
429
  }
393
430
  if (removedSectionEndpointCount > 0) markTrackedSectionEndpointRemoval(tr, {
394
431
  sourceDoc: state.doc,
@@ -416,6 +453,7 @@ function resolveChange(from, to, mode, revisionIds) {
416
453
  if (field.type === node.type) tr.setNodeMarkup(position, void 0, node.attrs, node.marks);
417
454
  else tr.replaceWith(position, position + field.nodeSize, node);
418
455
  }
456
+ for (const [id, position] of [...points].toReversed()) tr.insert(tr.mapping.map(position, -1), commentPoint(tr.doc.type.schema, id));
419
457
  if (tr.steps.length > 0) dispatch(tr);
420
458
  }
421
459
  return true;
@@ -2,15 +2,15 @@ import { Node, ResolvedPos } from "prosemirror-model";
2
2
  //#region src/prosemirror/containerFinalParagraph.d.ts
3
3
  /**
4
4
  * Whether the paragraph starting at `at` is the one its container ENDS with,
5
- * which is the one place a paragraph mark cannot carry a revision.
5
+ * where a paragraph break cannot join into a following paragraph.
6
6
  *
7
- * A deleted mark asks a reader to join the paragraph it ends with the one
8
- * after it, and an inserted mark says that break was added, so rejecting it
9
- * closes the paragraph back over the next one. A body, a table cell, a header
10
- * or footer, a note and a text box each end with a paragraph that nothing
11
- * follows, so neither direction can be resolved there: a consumer is left with
12
- * a revision it can only ignore, and the redline no longer resolves to the two
13
- * documents it was written from.
7
+ * Added boundaries normally rotate onto a preceding paragraph, so rejecting
8
+ * them joins back into this one. If an occupied boundary or a table prevents
9
+ * rotation, the final inserted paragraph must retain its own revision: the
10
+ * resolver removes the whole paragraph after rejecting its inserted content.
11
+ * A terminal deletion, by contrast, clears without removing the container's
12
+ * required ending. Bodies, table cells, headers, footers, notes and text boxes
13
+ * share these rules.
14
14
  *
15
15
  * A paragraph before a TABLE is not one of those: the table is a following
16
16
  * sibling, so the paragraph does not end anything and carries its own mark
@@ -14,15 +14,15 @@ const nothingFollowsIn = (parent, fromIndex, paragraphTypeName) => {
14
14
  };
15
15
  /**
16
16
  * Whether the paragraph starting at `at` is the one its container ENDS with,
17
- * which is the one place a paragraph mark cannot carry a revision.
17
+ * where a paragraph break cannot join into a following paragraph.
18
18
  *
19
- * A deleted mark asks a reader to join the paragraph it ends with the one
20
- * after it, and an inserted mark says that break was added, so rejecting it
21
- * closes the paragraph back over the next one. A body, a table cell, a header
22
- * or footer, a note and a text box each end with a paragraph that nothing
23
- * follows, so neither direction can be resolved there: a consumer is left with
24
- * a revision it can only ignore, and the redline no longer resolves to the two
25
- * documents it was written from.
19
+ * Added boundaries normally rotate onto a preceding paragraph, so rejecting
20
+ * them joins back into this one. If an occupied boundary or a table prevents
21
+ * rotation, the final inserted paragraph must retain its own revision: the
22
+ * resolver removes the whole paragraph after rejecting its inserted content.
23
+ * A terminal deletion, by contrast, clears without removing the container's
24
+ * required ending. Bodies, table cells, headers, footers, notes and text boxes
25
+ * share these rules.
26
26
  *
27
27
  * A paragraph before a TABLE is not one of those: the table is a following
28
28
  * sibling, so the paragraph does not end anything and carries its own mark
@@ -1,5 +1,6 @@
1
1
  import { joinCommentRangesAcrossParagraphs } from "../../docx/commentRangeJoin.js";
2
2
  import { completeCommentReferences } from "../../docx/commentReferenceCompletion.js";
3
+ import { buildSections } from "../../docx/documentParser.js";
3
4
  import { normalizeFoldedListNumbers } from "../../docx/foldedListNumberFields.js";
4
5
  import { getDocumentSourceStyles } from "../../docx/headerFooterVerbatim.js";
5
6
  import { EDITED_PREVIEW_FINGERPRINT, imageRawXmlFingerprint } from "../../docx/imageRawXml.js";
@@ -397,7 +398,7 @@ function fromProseDoc(pmDoc, baseDocument, { stylesheetSource, reuse = "none" })
397
398
  const documentBody = { content: blocks };
398
399
  if (baseDocument?.package.document.background) documentBody.background = baseDocument.package.document.background;
399
400
  if (baseDocument?.package.document.finalSectionProperties) documentBody.finalSectionProperties = baseDocument.package.document.finalSectionProperties;
400
- if (baseDocument?.package.document.sections) documentBody.sections = baseDocument.package.document.sections;
401
+ if (baseDocument?.package.document.sections) documentBody.sections = buildSections(blocks, documentBody.finalSectionProperties);
401
402
  if (baseDocument?.package.document.comments) documentBody.comments = baseDocument.package.document.comments;
402
403
  const numbering = completeNumberingForDoc(baseDocument?.package.numbering, pmDoc);
403
404
  if (baseDocument) {
@@ -1582,12 +1583,16 @@ function extractParagraphContent(originalParagraph, _documentCounts, emptyHyperl
1582
1583
  };
1583
1584
  const appendDirectRun = (node, run, coalescePlainText = false) => {
1584
1585
  if (currentHyperlink) flushCurrentInline();
1585
- const marksKey = getMarksKey(node.marks);
1586
+ const marksKey = getMarksKey(node.marks.filter((mark) => {
1587
+ if (mark.type.name !== "runIdentity") return true;
1588
+ const { emptyFormatting, preservedAttributes, preserved } = expectRunIdentityMarkAttrs(mark);
1589
+ return emptyFormatting !== true || (preservedAttributes?.length ?? 0) > 0 || (preserved?.children?.length ?? 0) > 0;
1590
+ }));
1586
1591
  const ownerId = runIdentityId(node);
1587
1592
  const currentOwnerId = currentRun ? sourceRunOwners.get(currentRun) : void 0;
1588
1593
  const currentRunIsPlainText = currentRun?.content.every((runContent) => runContent.type === "text");
1589
1594
  const joinsOwnedSourceRun = ownerId !== void 0 && ownerId === currentOwnerId && currentRun !== null && runsShareProperties(currentRun, run);
1590
- const joinsOrdinaryText = coalescePlainText && ownerId === void 0 && currentOwnerId === void 0 && currentRunIsPlainText === true && currentMarksKey === marksKey;
1595
+ const joinsOrdinaryText = coalescePlainText && currentRun !== null && runsShareProperties(currentRun, run) && currentRunIsPlainText === true && currentMarksKey === marksKey;
1591
1596
  if (currentRun && (joinsOwnedSourceRun || joinsOrdinaryText)) {
1592
1597
  if (joinsOrdinaryText) {
1593
1598
  for (const runContent of run.content) if (runContent.type === "text") appendTextToRun(currentRun, runContent.text);
@@ -3319,7 +3324,7 @@ function tableCellAttrsToFormatting(attrs) {
3319
3324
  if (attrs.margins && !sameResolvedValue(attrs.margins, attrs._resolvedMargins)) result.margins = buildCellMarginsFromAttrs(attrs.margins);
3320
3325
  if (attrs.textDirection !== (orig.textDirection ?? void 0)) if (attrs.textDirection) result.textDirection = attrs.textDirection;
3321
3326
  else delete result.textDirection;
3322
- return result;
3327
+ return Object.keys(result).length > 0 ? result : void 0;
3323
3328
  }
3324
3329
  const authoredWidth = attrs._authoredWidth;
3325
3330
  if (!(attrs.colspan > 1 || attrs.rowspan > 1 || authoredWidth !== void 0 || attrs.verticalAlign || backgroundChanged || attrs.borders || attrs.margins || attrs.textDirection)) return;
@@ -42,7 +42,7 @@ import { directionFromBidi } from "../paragraphDirection.js";
42
42
  import { paragraphIndentationFromFormatting } from "../paragraphIndentation.js";
43
43
  import { lineSpacingProvenanceFromSpacing } from "../paragraphSpacing.js";
44
44
  import { RUN_IDENTITY_MARK_NAME, hasRunIdentityPayload, runIdentityAttrs } from "../runIdentity.js";
45
- import { continueRunIdentitiesAcrossRevisions } from "../runIdentityAcrossRevisions.js";
45
+ import { continueSourceCarrierIdentitiesAcrossRevisions } from "../runIdentityAcrossRevisions.js";
46
46
  import { getParagraphMarkSuppressionOverrides, hasDirectRunFormatting, isParagraphMarkSuppressionEligible, resolveParagraphInheritedRunFormatting, stripParagraphMarkFormattingForBodyRuns, stripParagraphMarkOnlyFormatting, suppressParagraphMarkFormatting } from "../runStyleFormatting.js";
47
47
  import { schema } from "../schema/index.js";
48
48
  import { PRESERVED_XML_LEVELS } from "../schema/nodes.js";
@@ -112,7 +112,7 @@ const runIdentityMark = (run, nextRunIdentityId) => {
112
112
  const payload = {
113
113
  preservedAttributes: run.preservedAttributes,
114
114
  preserved: run.formatting?.preserved,
115
- emptyFormatting: run.formatting !== void 0 && Object.keys(run.formatting).length === 0 && (run.propertyChanges?.length ?? 0) === 0 ? true : void 0
115
+ emptyFormatting: run.formatting !== void 0 && Object.keys(run.formatting).length === 0 ? true : void 0
116
116
  };
117
117
  if (!runHasPageBreakContent(run) && !runHasMixedContent(run) && !hasRunIdentityPayload(payload)) return null;
118
118
  return schema.mark(RUN_IDENTITY_MARK_NAME, runIdentityAttrs(nextRunIdentityId(), payload));
@@ -676,7 +676,7 @@ function convertParagraph(paragraph, styleResolver, context, extraRunFormatting,
676
676
  if (emptyHyperlinks) attrs._emptyHyperlinks = emptyHyperlinks;
677
677
  return createProseParagraphWithPropertySource(schema.nodes["paragraph"], paragraph, {
678
678
  attrs,
679
- content: continueRunIdentitiesAcrossRevisions(inlineNodes)
679
+ content: continueSourceCarrierIdentitiesAcrossRevisions(inlineNodes)
680
680
  });
681
681
  }
682
682
  /**
@@ -1256,7 +1256,7 @@ function convertTableCell({ cell, styleResolver, context, isHeader, gridWidthPer
1256
1256
  attrs.margins = effectiveFormatting.margins;
1257
1257
  attrs._resolvedMargins = effectiveFormatting.margins;
1258
1258
  }
1259
- if (formatting) attrs._originalFormatting = formatting;
1259
+ attrs._originalFormatting = formatting ?? {};
1260
1260
  if (cell.propertyChanges && cell.propertyChanges.length > 0) attrs.tcPrChange = [...cell.propertyChanges];
1261
1261
  if (cell.contentControls && cell.contentControls.length > 0) attrs.contentControls = cell.contentControls;
1262
1262
  if (cell.carrierStack?.length) attrs.carrierStack = cell.carrierStack;
@@ -1,6 +1,7 @@
1
1
  import { readCommentReferenceAttrs } from "../../commentReferenceAttrs.js";
2
2
  import { planCommentReferenceRepairs } from "../../commentReferenceIntegrity.js";
3
3
  import { readRangeAnchorAttrs } from "../../rangeAnchorAttrs.js";
4
+ import { trackedRunInlineAtomDisposition } from "../../trackedRunInlineAtoms.js";
4
5
  import { createNodeExtension } from "../create.js";
5
6
  import "./RangeAnchorExtension.js";
6
7
  import { Plugin } from "prosemirror-state";
@@ -10,7 +11,16 @@ const COMMENT_REFERENCE_NODE_NAME = "commentReference";
10
11
  const surveyCommentReferences = (doc) => {
11
12
  const references = [];
12
13
  const markEnds = /* @__PURE__ */ new Map();
14
+ const unanchoredMarks = [];
13
15
  doc.descendants((node, position) => {
16
+ const transparent = trackedRunInlineAtomDisposition(node) === "transparent";
17
+ if (transparent) {
18
+ for (const mark of node.marks) if (mark.type.name === "comment") unanchoredMarks.push({
19
+ position,
20
+ size: node.nodeSize,
21
+ mark
22
+ });
23
+ }
14
24
  if (node.type.name === "commentReference") {
15
25
  const attrs = readCommentReferenceAttrs(node);
16
26
  references.push(attrs.ok ? {
@@ -33,6 +43,7 @@ const surveyCommentReferences = (doc) => {
33
43
  }
34
44
  return false;
35
45
  }
46
+ if (transparent) return false;
36
47
  if (!node.isInline) return true;
37
48
  for (const mark of node.marks) {
38
49
  if (mark.type.name !== "comment") continue;
@@ -44,6 +55,7 @@ const surveyCommentReferences = (doc) => {
44
55
  });
45
56
  return {
46
57
  references,
58
+ unanchoredMarks,
47
59
  markExtents: [...markEnds].map(([commentId, end]) => ({
48
60
  commentId,
49
61
  end
@@ -84,9 +96,11 @@ const CommentReferenceExtension = createNodeExtension({
84
96
  if (!transactions.some(({ docChanged }) => docChanged)) return null;
85
97
  const referenceType = newState.schema.nodes[COMMENT_REFERENCE_NODE_NAME];
86
98
  if (!referenceType) return null;
87
- const repairs = planCommentReferenceRepairs(surveyCommentReferences(newState.doc));
88
- if (repairs.length === 0) return null;
99
+ const survey = surveyCommentReferences(newState.doc);
100
+ const repairs = planCommentReferenceRepairs(survey);
101
+ if (repairs.length === 0 && survey.unanchoredMarks.length === 0) return null;
89
102
  const transaction = newState.tr;
103
+ for (const { position, size, mark } of survey.unanchoredMarks) transaction.removeMark(position, position + size, mark);
90
104
  for (const repair of repairs) {
91
105
  if (repair.type === "delete") {
92
106
  transaction.delete(repair.position, repair.position + repair.size);
@@ -1,5 +1,7 @@
1
+ import { DisplayMode } from "../managers/EditorModeManager.js";
1
2
  import { FindOptions } from "../utils/findReplace.js";
2
3
  import { Node } from "prosemirror-model";
4
+ import { EditorState } from "prosemirror-state";
3
5
  //#region src/prosemirror/findReplaceSelection.d.ts
4
6
  /**
5
7
  * Positional fields of a document search match that the range resolver needs.
@@ -26,5 +28,13 @@ type ProseMirrorFindMatch = FindMatchPosition & FindMatchRange & {
26
28
  };
27
29
  declare function resolveFindMatchRange(doc: Node, match: FindMatchPosition): FindMatchRange | null;
28
30
  declare function findInProseMirrorDocument(doc: Node, searchText: string, options: FindOptions): ProseMirrorFindMatch[];
31
+ type FindInMarkupViewOptions = {
32
+ state: EditorState;
33
+ markupView: DisplayMode;
34
+ searchText: string;
35
+ options: FindOptions;
36
+ };
37
+ /** Search the displayed story; returned ranges address the authored editor document. */
38
+ declare const findInMarkupView: ({ state, markupView, searchText, options }: FindInMarkupViewOptions) => ProseMirrorFindMatch[];
29
39
  //#endregion
30
- export { FindMatchPosition, FindMatchRange, ProseMirrorFindMatch, findInProseMirrorDocument, resolveFindMatchRange };
40
+ export { FindMatchPosition, FindMatchRange, ProseMirrorFindMatch, findInMarkupView, findInProseMirrorDocument, resolveFindMatchRange };
@@ -1,6 +1,7 @@
1
1
  import { findAllMatches } from "../utils/findReplace.js";
2
2
  import { decodeOoxmlSymbolCharacter } from "../utils/ooxmlSymbol.js";
3
3
  import { expectSymbolAttrs } from "./attrs/index.js";
4
+ import { projectMarkupView } from "./markupViewProjection.js";
4
5
  //#region src/prosemirror/findReplaceSelection.ts
5
6
  function resolveFindMatchRange(doc, match) {
6
7
  let resolved = null;
@@ -40,6 +41,19 @@ function findInProseMirrorDocument(doc, searchText, options) {
40
41
  });
41
42
  return matches;
42
43
  }
44
+ /** Search the displayed story; returned ranges address the authored editor document. */
45
+ const findInMarkupView = ({ state, markupView, searchText, options }) => {
46
+ const projection = projectMarkupView(state, markupView);
47
+ if (projection.type === "authored") return findInProseMirrorDocument(state.doc, searchText, options);
48
+ return findInProseMirrorDocument(projection.doc, searchText, options).map((match) => ({
49
+ paragraphIndex: match.paragraphIndex,
50
+ startOffset: match.startOffset,
51
+ endOffset: match.endOffset,
52
+ text: match.text,
53
+ from: projection.toEditor(match.from, 1),
54
+ to: projection.toEditor(match.to, -1)
55
+ }));
56
+ };
43
57
  function forEachSearchParagraph(doc, visit) {
44
58
  let paragraphIndex = 0;
45
59
  const walkBlocks = (container, contentStart) => {
@@ -139,4 +153,4 @@ function getSearchableParagraphText(paragraph) {
139
153
  return text;
140
154
  }
141
155
  //#endregion
142
- export { findInProseMirrorDocument, resolveFindMatchRange };
156
+ export { findInMarkupView, findInProseMirrorDocument, resolveFindMatchRange };
@@ -1,3 +1,4 @@
1
+ import { document_d_exports } from "../types/document.js";
1
2
  import { DisplayMode } from "../managers/EditorModeManager.js";
2
3
  import { RemovedSectionReference } from "../internal/sectionEndpointResolution.js";
3
4
  import { indexedPositionMap } from "../internal/indexedPositionMap.js";
@@ -93,5 +94,13 @@ declare const projectMarkupView: (state: EditorState, view: DisplayMode) => Mark
93
94
  * of text it cannot enter.
94
95
  */
95
96
  declare const visibleCaretPosition: (projection: MarkupViewProjection, position: number) => number;
97
+ type ProjectStoryMarkupViewOptions = {
98
+ doc: Node;
99
+ markupView: DisplayMode;
100
+ styles?: document_d_exports.StyleDefinitions | null | undefined;
101
+ numbering?: document_d_exports.NumberingDefinitions | undefined;
102
+ };
103
+ /** Detached stories use the same state-aware projection owner as the body. */
104
+ declare const projectStoryMarkupView: ({ doc, markupView, styles, numbering }: ProjectStoryMarkupViewOptions) => MarkupViewProjection;
96
105
  //#endregion
97
- export { MarkupViewProjection, markupViewResolutionMode, projectMarkupView, resolveStateStory, visibleCaretPosition };
106
+ export { MarkupViewProjection, markupViewResolutionMode, projectMarkupView, projectStoryMarkupView, resolveStateStory, visibleCaretPosition };
@@ -1,9 +1,10 @@
1
1
  import { indexedPositionMap } from "../internal/indexedPositionMap.js";
2
2
  import { finalRevisionParagraphRanges } from "../internal/revisionResolutionTracking.js";
3
3
  import { resolveWholeStory } from "../internal/wholeStoryRevisionResolution.js";
4
- import { getDocumentNumbering } from "./plugins/documentNumbering.js";
4
+ import { createDocumentNumberingPlugin, getDocumentNumbering } from "./plugins/documentNumbering.js";
5
5
  import { getDocumentStyleResolver } from "./plugins/documentStyleState.js";
6
- import { Selection } from "prosemirror-state";
6
+ import { createDocumentStyleContextPlugin } from "./plugins/documentStyles.js";
7
+ import { EditorState, Selection } from "prosemirror-state";
7
8
  //#region src/prosemirror/markupViewProjection.ts
8
9
  /** Resolve every revision in the state's story with the state's own styles and numbering. */
9
10
  const resolveStateStory = (state, mode) => resolveWholeStory({
@@ -84,5 +85,22 @@ const visibleCaretPosition = (projection, position) => {
84
85
  const near = Selection.near(projection.doc.resolve(bounded), -1);
85
86
  return projection.toEditor(near.head, -1);
86
87
  };
88
+ const storyProjectionStates = /* @__PURE__ */ new WeakMap();
89
+ /** Detached stories use the same state-aware projection owner as the body. */
90
+ const projectStoryMarkupView = ({ doc, markupView, styles, numbering }) => {
91
+ if (markupView === "all-markup") return AUTHORED;
92
+ const cached = storyProjectionStates.get(doc);
93
+ if (cached && cached.styles === styles && cached.numbering === numbering) return projectMarkupView(cached.state, markupView);
94
+ const state = EditorState.create({
95
+ doc,
96
+ plugins: [createDocumentStyleContextPlugin(styles), createDocumentNumberingPlugin(numbering)]
97
+ });
98
+ storyProjectionStates.set(doc, {
99
+ styles,
100
+ numbering,
101
+ state
102
+ });
103
+ return projectMarkupView(state, markupView);
104
+ };
87
105
  //#endregion
88
- export { markupViewResolutionMode, projectMarkupView, resolveStateStory, visibleCaretPosition };
106
+ export { markupViewResolutionMode, projectMarkupView, projectStoryMarkupView, resolveStateStory, visibleCaretPosition };
@@ -1,3 +1,4 @@
1
+ import { sliceText } from "../../utils/sliceText.js";
1
2
  import { expectParagraphAttrs } from "../attrs/index.js";
2
3
  import { effectiveParagraphNumberingReference, paragraphNumberingSourcesFromBoundary } from "../numberingAttr.js";
3
4
  import { createPluginKeyOwner } from "../pluginKeyOwner.js";
@@ -30,7 +31,11 @@ const MAX_REPORTED_STEPS_LENGTH = 4e3;
30
31
  const describeSteps = (transactions) => {
31
32
  const steps = transactions.flatMap((transaction) => transaction.steps.map((step) => step.toJSON()));
32
33
  const json = JSON.stringify(steps);
33
- return json.length > MAX_REPORTED_STEPS_LENGTH ? `${json.slice(0, MAX_REPORTED_STEPS_LENGTH)}… (${json.length} characters)` : json;
34
+ return json.length > MAX_REPORTED_STEPS_LENGTH ? `${sliceText({
35
+ text: json,
36
+ units: "codeUnits",
37
+ end: MAX_REPORTED_STEPS_LENGTH
38
+ })}… (${json.length} characters)` : json;
34
39
  };
35
40
  var TransactionInvariantError = class extends Error {
36
41
  issues;
@@ -19,6 +19,20 @@ declare const NON_INCLUSIVE_MARK_DISPOSITION: {
19
19
  };
20
20
  /** The mark names in the schema that a replacement must not silently drop. */
21
21
  declare const nonInclusiveMarkNames: (schema: Schema) => string[];
22
+ /** One typed point for a comment whose aligned coverage has no content left. */
23
+ declare const commentPoint: (schema: Schema, id: number) => Node;
24
+ type ResolvedCommentPointsOptions = {
25
+ doc: Node;
26
+ removes: (node: Node, position: number) => boolean;
27
+ retiresStructure: (node: Node, position: number) => boolean;
28
+ };
29
+ /** The same collector serves standalone commands and the existing bulk walk. */
30
+ declare const createResolvedCommentPointCollector: ({ removes, retiresStructure }: Omit<ResolvedCommentPointsOptions, "doc">) => {
31
+ visit: (node: Node, position: number, previous?: Node) => boolean;
32
+ finish: () => Map<number, number>;
33
+ };
34
+ /** Point fallback excludes comments belonging to retired blocks and table cells. */
35
+ declare const resolvedCommentPoints: ({ doc, removes, retiresStructure }: ResolvedCommentPointsOptions) => Map<number, number>;
22
36
  type ReplacedAnnotations = {
23
37
  /** Marks the replacement must carry, in document order. */
24
38
  carried: readonly Mark[];
@@ -88,4 +102,4 @@ declare const annotatedReplacement: ({ annotations: { carried, leading, trailing
88
102
  */
89
103
  declare const inheritedReplacementMarks: (doc: Node, from: number, to: number) => readonly Mark[];
90
104
  //#endregion
91
- export { NON_INCLUSIVE_MARK_DISPOSITION, ReplacedAnnotations, addCarriedMarks, annotatedReplacement, commentsAcrossBlockBoundary, commentsForTrackedInsertion, hasReplacedAnnotations, inheritedReplacementMarks, nonInclusiveMarkNames, placeTrackedInsertion, surveyReplacedAnnotations };
105
+ export { NON_INCLUSIVE_MARK_DISPOSITION, ReplacedAnnotations, addCarriedMarks, annotatedReplacement, commentPoint, commentsAcrossBlockBoundary, commentsForTrackedInsertion, createResolvedCommentPointCollector, hasReplacedAnnotations, inheritedReplacementMarks, nonInclusiveMarkNames, placeTrackedInsertion, resolvedCommentPoints, surveyReplacedAnnotations };
@@ -1,9 +1,11 @@
1
+ import { expectParagraphAttrs, expectTableCellAttrs, expectTableRowAttrs } from "./attrs/index.js";
1
2
  import { BOOKMARK_BOUNDARY_NODE_NAME } from "./extensions/nodes/BookmarkBoundaryExtension.js";
2
3
  import { COMMENT_REFERENCE_NODE_NAME } from "./extensions/nodes/CommentReferenceExtension.js";
3
4
  import { MOVE_RANGE_BOUNDARY_NODE_NAME } from "./extensions/nodes/MoveRangeBoundaryExtension.js";
4
5
  import { RANGE_ANCHOR_NODE_NAME } from "./extensions/nodes/RangeAnchorExtension.js";
5
6
  import { TEXT_BOX_ANCHOR_NODE_NAME } from "./extensions/nodes/TextBoxAnchorExtension.js";
6
7
  import { readMoveRangeBoundaryAttrs } from "./moveRangeBoundaryAttrs.js";
8
+ import { readRangeAnchorAttrs } from "./rangeAnchorAttrs.js";
7
9
  import { isZeroWidthAnchor } from "./zeroWidthAnchors.js";
8
10
  import { Fragment, Mark } from "prosemirror-model";
9
11
  //#region src/prosemirror/replacedAnnotations.ts
@@ -21,15 +23,12 @@ import { Fragment, Mark } from "prosemirror-model";
21
23
  * hyperlink simply stops being a link. Zero-width anchors fare worse still: a
22
24
  * plain replace deletes them outright.
23
25
  *
24
- * Word moves a range start that sits inside a replaced span to the beginning
25
- * of the replacement and lets the range keep covering the new text; it keeps
26
- * a link across a replacement of its display text. A range wholly inside the
27
- * span collapses onto the replacement, and disappears only when every
28
- * character it covered is gone. One rule per marker kind, and that is the
29
- * whole contract:
26
+ * Text replacement preserves a comment's aligned coverage. Unchanged text
27
+ * keeps its ranges; inserted text takes the comments of the replaced aligned
28
+ * span. A range with no remaining content becomes a point at the replacement
29
+ * start. The applier decides alignment once for direct and tracked edits.
30
30
  *
31
- * - `comment` mark: the replacement carries every comment the span covered, so
32
- * a range that started inside it starts at the replacement instead.
31
+ * - `comment` mark: carried over its aligned replacement span.
33
32
  * - `hyperlink` mark: the replacement carries the first link the span held, so
34
33
  * replacing a link's display text leaves the link on the new text. A span
35
34
  * crossing two links keeps the one it opens in, because the mark excludes
@@ -66,9 +65,8 @@ import { Fragment, Mark } from "prosemirror-model";
66
65
  * - every other mark: formatting, and left to boundary inheritance, which
67
66
  * already gives the replacement the formatting of the text it replaces.
68
67
  *
69
- * Deleting the whole of a comment's span leaves it no mark anywhere, so
70
- * `CommentReferenceExtension` drops the orphan reference and the save drops
71
- * the comment: the one case where a range is meant to disappear.
68
+ * Removing the owning block removes its comment. Replacing its last covered
69
+ * text within a surviving block retains a point rather than dropping it.
72
70
  */
73
71
  /**
74
72
  * What a replacement does with each mark the schema declares
@@ -90,6 +88,67 @@ const NON_INCLUSIVE_MARK_DISPOSITION = {
90
88
  /** The mark names in the schema that a replacement must not silently drop. */
91
89
  const nonInclusiveMarkNames = (schema) => Object.entries(schema.marks).filter(([, type]) => type.spec.inclusive === false).map(([name]) => name);
92
90
  const isCarried = (mark) => NON_INCLUSIVE_MARK_DISPOSITION[mark.type.name] === "carry";
91
+ /** One typed point for a comment whose aligned coverage has no content left. */
92
+ const commentPoint = (schema, id) => schema.node(RANGE_ANCHOR_NODE_NAME, {
93
+ start: {
94
+ type: "commentRangeStart",
95
+ id
96
+ },
97
+ end: {
98
+ type: "commentRangeEnd",
99
+ id
100
+ }
101
+ });
102
+ /** The same collector serves standalone commands and the existing bulk walk. */
103
+ const createResolvedCommentPointCollector = ({ removes, retiresStructure }) => {
104
+ const points = /* @__PURE__ */ new Map();
105
+ const retained = /* @__PURE__ */ new Set();
106
+ const visit = (node, position, previous) => {
107
+ if (node.type.name === "paragraph") {
108
+ const mark = expectParagraphAttrs(node).pPrMark;
109
+ const precedingMark = previous?.type.name === "paragraph" ? expectParagraphAttrs(previous).pPrMark : null;
110
+ if ((mark?.kind === "del" || mark?.kind === "moveFrom") && retiresStructure(node, position) || (precedingMark?.kind === "del" || precedingMark?.kind === "moveFrom") && previous !== void 0 && retiresStructure(previous, position - previous.nodeSize)) {
111
+ let survivingContent = false;
112
+ node.descendants((inline, offset) => {
113
+ if (inline.isInline && !isZeroWidthAnchor(inline) && !removes(inline, position + 1 + offset)) survivingContent = true;
114
+ return !survivingContent;
115
+ });
116
+ if (!survivingContent) return false;
117
+ }
118
+ }
119
+ if (node.type.name === "tableRow" && expectTableRowAttrs(node).trDel && retiresStructure(node, position)) return false;
120
+ if ((node.type.name === "tableCell" || node.type.name === "tableHeader") && expectTableCellAttrs(node).cellMarker?.kind === "del" && retiresStructure(node, position)) return false;
121
+ if (node.type.name === "rangeAnchor") {
122
+ const attrs = readRangeAnchorAttrs(node);
123
+ if (attrs.ok && attrs.value.start.type === "commentRangeStart") retained.add(attrs.value.start.id);
124
+ }
125
+ if (!node.isInline || isZeroWidthAnchor(node)) return true;
126
+ for (const mark of node.marks) {
127
+ if (mark.type.name !== "comment") continue;
128
+ const id = mark.attrs["commentId"];
129
+ if (!removes(node, position)) retained.add(id);
130
+ else if (!points.has(id)) points.set(id, position);
131
+ }
132
+ return true;
133
+ };
134
+ const finish = () => {
135
+ for (const id of retained) points.delete(id);
136
+ return points;
137
+ };
138
+ return {
139
+ visit,
140
+ finish
141
+ };
142
+ };
143
+ /** Point fallback excludes comments belonging to retired blocks and table cells. */
144
+ const resolvedCommentPoints = ({ doc, removes, retiresStructure }) => {
145
+ const collector = createResolvedCommentPointCollector({
146
+ removes,
147
+ retiresStructure
148
+ });
149
+ doc.descendants((node, position, parent, index) => collector.visit(node, position, index > 0 ? parent?.maybeChild(index - 1) ?? void 0 : void 0));
150
+ return collector.finish();
151
+ };
93
152
  /** Anchors an edit must put back, and where relative to the replacement. */
94
153
  const ANCHOR_PLACEMENT = {
95
154
  [COMMENT_REFERENCE_NODE_NAME]: "after",
@@ -349,4 +408,4 @@ const inheritedReplacementMarks = (doc, from, to) => {
349
408
  return (to === from ? $from.marks() : $from.marksAcross(doc.resolve(to))) ?? Mark.none;
350
409
  };
351
410
  //#endregion
352
- export { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, commentsAcrossBlockBoundary, commentsForTrackedInsertion, hasReplacedAnnotations, inheritedReplacementMarks, nonInclusiveMarkNames, placeTrackedInsertion, surveyReplacedAnnotations };
411
+ export { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, commentPoint, commentsAcrossBlockBoundary, commentsForTrackedInsertion, createResolvedCommentPointCollector, hasReplacedAnnotations, inheritedReplacementMarks, nonInclusiveMarkNames, placeTrackedInsertion, resolvedCommentPoints, surveyReplacedAnnotations };
@@ -9,6 +9,8 @@ type ReadAuthoredRunFormattingOptions = {
9
9
  };
10
10
  /** Read authored run properties without confusing inherited visuals for direct formatting. */
11
11
  declare const readAuthoredRunFormatting: ({ context, marks, styleResolver }: ReadAuthoredRunFormattingOptions) => document_d_exports.TextFormatting;
12
+ /** Preserve the authored distinction between an absent and empty property set in revisions. */
13
+ declare const readAuthoredRunPropertySnapshot: (options: ReadAuthoredRunFormattingOptions) => document_d_exports.TextFormatting | undefined;
12
14
  type ReconcileRunFormattingMarksOptions = {
13
15
  authoredFormatting: document_d_exports.TextFormatting;
14
16
  context: ParagraphRunStyleContext;
@@ -22,4 +24,4 @@ type ReconcileRunFormattingMarksOptions = {
22
24
  */
23
25
  declare const reconcileRunFormattingMarks: ({ authoredFormatting, context, node, styleResolver }: ReconcileRunFormattingMarksOptions) => readonly Mark[];
24
26
  //#endregion
25
- export { readAuthoredRunFormatting, reconcileRunFormattingMarks };
27
+ export { readAuthoredRunFormatting, readAuthoredRunPropertySnapshot, reconcileRunFormattingMarks };
@@ -1,8 +1,9 @@
1
1
  import { mergeTextFormatting } from "../utils/textFormattingMerge.js";
2
- import { expectCharacterStyleMarkAttrs } from "./attrs/index.js";
2
+ import { expectCharacterStyleMarkAttrs, expectRunIdentityMarkAttrs } from "./attrs/index.js";
3
3
  import { textFormattingToMarks } from "./extensions/marks/markUtils.js";
4
4
  import { marksToTextFormatting } from "./runFormattingFromMarks.js";
5
5
  import { RUN_FORMATTING_MARK_NAMES } from "./runFormattingMarkNames.js";
6
+ import { RUN_IDENTITY_MARK_NAME } from "./runIdentity.js";
6
7
  import { getParagraphMarkSuppressionOverrides, hasDirectRunFormatting, isParagraphMarkSuppressionEligible, paragraphFormattingForRun, resolveEffectiveRunStyleFormatting } from "./runStyleFormatting.js";
7
8
  import { Mark } from "prosemirror-model";
8
9
  //#region src/prosemirror/runFormattingReconciliation.ts
@@ -14,6 +15,13 @@ const readAuthoredRunFormatting = ({ context, marks, styleResolver }) => marksTo
14
15
  paragraphMarkPrecedesStyle: context.paragraphMarkPrecedesStyle,
15
16
  ...styleResolver !== void 0 ? { styleResolver } : {}
16
17
  });
18
+ /** Preserve the authored distinction between an absent and empty property set in revisions. */
19
+ const readAuthoredRunPropertySnapshot = (options) => {
20
+ const formatting = readAuthoredRunFormatting(options);
21
+ if (Object.keys(formatting).length > 0) return formatting;
22
+ const identity = options.marks.find(({ type }) => type.name === RUN_IDENTITY_MARK_NAME);
23
+ return identity && expectRunIdentityMarkAttrs(identity).emptyFormatting ? formatting : void 0;
24
+ };
17
25
  /**
18
26
  * Rebuild a run's physical formatting marks from its authored properties and
19
27
  * current style context. This is the inverse of {@link readAuthoredRunFormatting}:
@@ -54,4 +62,4 @@ const reconcileRunFormattingMarks = ({ authoredFormatting, context, node, styleR
54
62
  ]);
55
63
  };
56
64
  //#endregion
57
- export { readAuthoredRunFormatting, reconcileRunFormattingMarks };
65
+ export { readAuthoredRunFormatting, readAuthoredRunPropertySnapshot, reconcileRunFormattingMarks };