@stll/folio-core 0.56.0 → 0.58.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 (158) hide show
  1. package/dist/ai-edits/apply.js +122 -22
  2. package/dist/ai-edits/batch-claims.d.ts +3 -1
  3. package/dist/ai-edits/batch-claims.js +25 -1
  4. package/dist/ai-edits/table-row-column-mutations.js +17 -3
  5. package/dist/ai-suggestions/apply.js +20 -1
  6. package/dist/compare/inline-atoms.js +40 -16
  7. package/dist/compare/inline-provenance.js +12 -5
  8. package/dist/compat/eigenpal.d.ts +2 -1
  9. package/dist/compat/eigenpal.js +2 -1
  10. package/dist/controller/canonicalClipboard.d.ts +3 -1
  11. package/dist/controller/canonicalClipboard.js +31 -13
  12. package/dist/controller/canonicalComments.d.ts +41 -0
  13. package/dist/controller/canonicalComments.js +152 -0
  14. package/dist/controller/canonicalComposition.d.ts +17 -4
  15. package/dist/controller/canonicalComposition.js +125 -20
  16. package/dist/controller/canonicalInlineProjection.d.ts +6 -1
  17. package/dist/controller/canonicalInlineProjection.js +86 -3
  18. package/dist/controller/canonicalInput.d.ts +1 -1
  19. package/dist/controller/canonicalInput.js +14 -4
  20. package/dist/controller/canonicalInputTimer.d.ts +10 -1
  21. package/dist/controller/canonicalInputTimer.js +24 -1
  22. package/dist/controller/canonicalPublicOperations.d.ts +2 -2
  23. package/dist/controller/canonicalPublicOperations.js +107 -15
  24. package/dist/controller/canonicalSelection.d.ts +18 -0
  25. package/dist/controller/canonicalSelection.js +41 -0
  26. package/dist/controller/canonicalSession.d.ts +4 -1
  27. package/dist/controller/canonicalSession.js +106 -36
  28. package/dist/controller/canonicalStoryEditor.d.ts +3 -1
  29. package/dist/controller/canonicalStoryEditor.js +21 -1
  30. package/dist/controller/canonicalStructure.js +8 -3
  31. package/dist/controller/folioEditor.js +13 -0
  32. package/dist/controller/headerFooterEditorManager.d.ts +1 -1
  33. package/dist/controller/headerFooterEditorManager.js +25 -11
  34. package/dist/controller/hiddenEditorApi.d.ts +13 -2
  35. package/dist/controller/hiddenEditorApi.js +5 -1
  36. package/dist/controller/hiddenEditorManager.d.ts +3 -1
  37. package/dist/controller/hiddenEditorManager.js +167 -14
  38. package/dist/controller/noteEditorManager.d.ts +1 -1
  39. package/dist/controller/noteEditorManager.js +28 -13
  40. package/dist/docx/builtInStyles.js +13 -6
  41. package/dist/docx/containerChildren.gen.d.ts +3 -1
  42. package/dist/docx/containerChildren.gen.js +33 -1
  43. package/dist/docx/fieldParser.js +14 -2
  44. package/dist/docx/hyperlinkParser.d.ts +1 -1
  45. package/dist/docx/hyperlinkParser.js +20 -1
  46. package/dist/docx/inlineWrapperContent.d.ts +4 -4
  47. package/dist/docx/inlineWrapperContent.js +4 -4
  48. package/dist/docx/linkedSdtPreservation.d.ts +10 -0
  49. package/dist/docx/linkedSdtPreservation.js +37 -0
  50. package/dist/docx/listRendering.d.ts +18 -0
  51. package/dist/{prosemirror/conversion/listRenderingDefinition.js → docx/listRendering.js} +17 -3
  52. package/dist/docx/paragraphParser.js +88 -67
  53. package/dist/docx/paragraphPropertySource.d.ts +22 -1
  54. package/dist/docx/paragraphPropertySource.js +107 -1
  55. package/dist/docx/preservedRunContent.js +2 -1
  56. package/dist/docx/relsParser.js +26 -2
  57. package/dist/docx/rezip.js +9 -6
  58. package/dist/docx/selectiveSave.js +10 -8
  59. package/dist/docx/serializer/commentSerializer.d.ts +4 -1
  60. package/dist/docx/serializer/commentSerializer.js +7 -3
  61. package/dist/docx/serializer/paragraphSerializer.js +207 -94
  62. package/dist/docx/server/materializeYjsDocx.js +15 -7
  63. package/dist/docx/server/migrateYjsAttrSchema.js +17 -5
  64. package/dist/i18n/messages/ar.gen.js +4 -0
  65. package/dist/i18n/messages/catalogs.gen.d.ts +68 -0
  66. package/dist/i18n/messages/catalogs.gen.js +68 -0
  67. package/dist/i18n/messages/cs.gen.js +4 -0
  68. package/dist/i18n/messages/de.gen.js +4 -0
  69. package/dist/i18n/messages/en.gen.js +4 -0
  70. package/dist/i18n/messages/es.gen.js +4 -0
  71. package/dist/i18n/messages/et.gen.js +4 -0
  72. package/dist/i18n/messages/fr.gen.js +4 -0
  73. package/dist/i18n/messages/he.gen.js +4 -0
  74. package/dist/i18n/messages/hi.gen.js +4 -0
  75. package/dist/i18n/messages/hu.gen.js +4 -0
  76. package/dist/i18n/messages/lt.gen.js +4 -0
  77. package/dist/i18n/messages/lv.gen.js +4 -0
  78. package/dist/i18n/messages/messages.gen.d.ts +4 -0
  79. package/dist/i18n/messages/pl.gen.js +4 -0
  80. package/dist/i18n/messages/pt-BR.gen.js +4 -0
  81. package/dist/i18n/messages/sk.gen.js +4 -0
  82. package/dist/i18n/messages/tr.gen.js +4 -0
  83. package/dist/i18n/messages/zh-CN.gen.js +4 -0
  84. package/dist/index.d.ts +2 -1
  85. package/dist/index.js +2 -1
  86. package/dist/internal/revisionResolutionInline.js +25 -3
  87. package/dist/markdown/internals.js +7 -0
  88. package/dist/markdown/renderParagraph.js +11 -12
  89. package/dist/markdown/renderRuns.js +3 -13
  90. package/dist/markdown/types.d.ts +3 -0
  91. package/dist/panel-layout.d.ts +11 -9
  92. package/dist/panel-layout.js +13 -9
  93. package/dist/prosemirror/addTrackedDeletionMark.d.ts +18 -0
  94. package/dist/prosemirror/addTrackedDeletionMark.js +87 -0
  95. package/dist/prosemirror/attrs/index.js +9 -0
  96. package/dist/prosemirror/commands/comments.js +60 -35
  97. package/dist/prosemirror/commands/propertyChangeScope.js +3 -6
  98. package/dist/prosemirror/commands/resolveParagraphProperties.js +22 -13
  99. package/dist/prosemirror/commentOps.js +15 -1
  100. package/dist/prosemirror/conversion/fromProseDoc.d.ts +8 -1
  101. package/dist/prosemirror/conversion/fromProseDoc.js +150 -30
  102. package/dist/prosemirror/conversion/toProseDoc.js +128 -40
  103. package/dist/prosemirror/extensions/core/ParagraphExtension.js +22 -5
  104. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +74 -25
  105. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +11 -0
  106. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +22 -14
  107. package/dist/prosemirror/extensions/marks/markUtils.js +8 -7
  108. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +5 -2
  109. package/dist/prosemirror/extensions/nodes/ImageExtension.js +8 -1
  110. package/dist/prosemirror/fieldRepresentation.d.ts +8 -0
  111. package/dist/prosemirror/fieldRepresentation.js +18 -0
  112. package/dist/prosemirror/hyperlinkRemoval.d.ts +13 -1
  113. package/dist/prosemirror/hyperlinkRemoval.js +28 -9
  114. package/dist/prosemirror/imagePictureNames.d.ts +12 -0
  115. package/dist/prosemirror/imagePictureNames.js +20 -0
  116. package/dist/prosemirror/listNumbering.js +11 -5
  117. package/dist/prosemirror/noteReferenceOccurrenceId.d.ts +5 -0
  118. package/dist/prosemirror/noteReferenceOccurrenceId.js +5 -0
  119. package/dist/prosemirror/noteReferenceOccurrences.d.ts +21 -0
  120. package/dist/prosemirror/noteReferenceOccurrences.js +185 -0
  121. package/dist/prosemirror/noteReferenceReview.js +14 -1
  122. package/dist/prosemirror/paragraphFormattingProvenance.js +1 -0
  123. package/dist/prosemirror/plugins/documentStyles.d.ts +4 -1
  124. package/dist/prosemirror/plugins/documentStyles.js +17 -10
  125. package/dist/prosemirror/plugins/suggestionMode.js +47 -49
  126. package/dist/prosemirror/preservedXmlReview.d.ts +6 -0
  127. package/dist/prosemirror/preservedXmlReview.js +35 -0
  128. package/dist/prosemirror/runFormattingFromMarks.js +2 -2
  129. package/dist/prosemirror/runFormattingReconciliation.js +5 -4
  130. package/dist/prosemirror/runPropertyChangeResolution.d.ts +8 -1
  131. package/dist/prosemirror/runPropertyChangeResolution.js +24 -2
  132. package/dist/prosemirror/runStyleFormatting.d.ts +74 -2
  133. package/dist/prosemirror/runStyleFormatting.js +20 -1
  134. package/dist/prosemirror/schema/marks.d.ts +1 -0
  135. package/dist/prosemirror/schema/nodes.d.ts +3 -0
  136. package/dist/prosemirror/styles/paragraphStyleCascade.js +4 -1
  137. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +15 -15
  138. package/dist/prosemirror/styles/resolvedStyleAttrs.js +69 -28
  139. package/dist/prosemirror/styles/styleResolver.d.ts +4 -1
  140. package/dist/prosemirror/styles/styleResolver.js +32 -11
  141. package/dist/prosemirror/textInput.d.ts +5 -1
  142. package/dist/prosemirror/textInput.js +3 -3
  143. package/dist/prosemirror/trackedRevisionPath.d.ts +5 -1
  144. package/dist/prosemirror/trackedRevisionPath.js +39 -1
  145. package/dist/prosemirror/validation.js +3 -5
  146. package/dist/prosemirror/yjsDocumentMetadata.d.ts +9 -3
  147. package/dist/prosemirror/yjsDocumentMetadata.js +37 -8
  148. package/dist/types/canonicalCapabilities.d.ts +18 -18
  149. package/dist/types/canonicalCapabilities.js +18 -18
  150. package/dist/types/canonicalComments.d.ts +51 -0
  151. package/dist/types/canonicalComments.js +0 -0
  152. package/dist/types/canonicalSections.d.ts +12 -0
  153. package/dist/types/canonicalSections.js +0 -0
  154. package/dist/utils/headingCollector.js +45 -4
  155. package/dist/utils/outlineDepth.d.ts +9 -0
  156. package/dist/utils/outlineDepth.js +9 -0
  157. package/package.json +2 -2
  158. package/dist/prosemirror/conversion/listRenderingDefinition.d.ts +0 -6
@@ -3,9 +3,11 @@ import { expectFieldAttrs, expectRunPropertyChangeMarkAttrs, expectTrackedChange
3
3
  import { resolutionRemovesControl, withoutResolvedEnclosures } from "../prosemirror/contentControlRevisions.js";
4
4
  import { resolveEmptyFieldResultRuns } from "../prosemirror/emptyFieldResultRuns.js";
5
5
  import "../prosemirror/extensions/nodes/SdtExtension.js";
6
+ import { canonicalFieldNode } from "../prosemirror/fieldRepresentation.js";
7
+ import { resolvedPreservedXmlAttrs } from "../prosemirror/preservedXmlReview.js";
6
8
  import { continuedRunMarks } from "../prosemirror/rejoinRunCarriers.js";
7
9
  import { RUN_FORMATTING_MARK_NAMES } from "../prosemirror/runFormattingMarkNames.js";
8
- import { reconstructRejectedRunFormattingMarks } from "../prosemirror/runPropertyChangeResolution.js";
10
+ import { reconstructRejectedRunFormattingMarks, restoreHistoricalRunFormatting } from "../prosemirror/runPropertyChangeResolution.js";
9
11
  import { paragraphRunStyleContext } from "../prosemirror/runStyleFormatting.js";
10
12
  import { recordNodeResolution } from "./revisionResolutionEdits.js";
11
13
  import { panic } from "better-result";
@@ -35,6 +37,11 @@ const rebuildNode = (source, attrs, content, marks = source.marks) => recreatePr
35
37
  const resolveInlineNode = ({ node, context, paragraphScope }) => {
36
38
  let marks = node.marks;
37
39
  let attrs = node.attrs;
40
+ if (context.mode === "reject" && node.marks.some((mark) => mark.type === context.keepType && expectTrackedChangeMarkAttrs(mark)._historicalFormatting)) marks = restoreHistoricalRunFormatting({
41
+ node,
42
+ paragraphContext: resolveParagraphRunStyleScope(paragraphScope, context.styleResolver),
43
+ styleResolver: context.styleResolver
44
+ });
38
45
  const runPropertyChangeMark = marks.find((mark) => mark.type.name === "runPropertyChange");
39
46
  if (runPropertyChangeMark) {
40
47
  const { changes } = expectRunPropertyChangeMarkAttrs(runPropertyChangeMark);
@@ -64,6 +71,7 @@ const resolveInlineNode = ({ node, context, paragraphScope }) => {
64
71
  }
65
72
  if (node.marks.some((mark) => (mark.type.name === "insertion" || mark.type.name === "deletion") && expectTrackedChangeMarkAttrs(mark)._docxRevisionAncestors?.some((layer) => context.mode === "accept" ? layer.type === "deletion" || layer.type === "moveFrom" : layer.type === "insertion" || layer.type === "moveTo")) || context.removeType && node.marks.some((mark) => mark.type === context.removeType)) return null;
66
73
  if (context.keepType) marks = marks.filter((mark) => mark.type !== context.keepType);
74
+ if (attrs === node.attrs) attrs = resolvedPreservedXmlAttrs(node, marks);
67
75
  if (attrs !== node.attrs) return rebuildNode(node, attrs, node.content, marks);
68
76
  return marksEqual(marks, node.marks) ? node : node.mark(marks);
69
77
  };
@@ -172,7 +180,7 @@ const resolveInlineContent = ({ node, position, context, inheritedParagraphScope
172
180
  context
173
181
  });
174
182
  let resolvedContent = Fragment.fromArray(children);
175
- if (!resolvedNode.type.validContent(resolvedContent)) {
183
+ if (!resolvedNode.type.validContent(resolvedContent) && !(resolvedNode.type.name === "structuredField" && resolvedContent.size === 0)) {
176
184
  const fitted = resolvedNode.type.createAndFill(resolvedNode.attrs, resolvedContent, resolvedNode.marks);
177
185
  if (!fitted || !resolvedNode.type.validContent(fitted.content)) {
178
186
  if (!resolvedNode.isInline) return panic(`Revision inline resolution invalidated ${resolvedNode.type.name} content`);
@@ -195,7 +203,21 @@ const resolveInlineContent = ({ node, position, context, inheritedParagraphScope
195
203
  from: position,
196
204
  to: position + resolvedNode.nodeSize
197
205
  });
198
- return rebuildNode(node, resolvedAttrs, resolvedContent, resolvedNode.marks);
206
+ const rebuilt = rebuildNode(node, resolvedAttrs, resolvedContent, resolvedNode.marks);
207
+ const canonical = canonicalFieldNode(rebuilt);
208
+ if (canonical !== rebuilt && canonical.type === rebuilt.type) recordNodeResolution({
209
+ before: rebuilt,
210
+ after: canonical,
211
+ position,
212
+ steps: context.steps
213
+ });
214
+ else if (canonical !== rebuilt) context.replacementRanges.splice(replacementRangeStart, context.replacementRanges.length - replacementRangeStart, {
215
+ from: position,
216
+ to: position + node.nodeSize,
217
+ newSize: canonical.nodeSize,
218
+ slice: new Slice(Fragment.from(canonical), 0, 0)
219
+ });
220
+ return canonical;
199
221
  };
200
222
  const coalesceReplacementRanges = (replacementRanges) => {
201
223
  const coalesced = [];
@@ -1,5 +1,6 @@
1
1
  import { createBuiltInStyleIndex } from "../docx/builtInStyles.js";
2
2
  import { createListLabelCounter } from "../prosemirror/listLabels.js";
3
+ import { createStyleResolver, resolveStyleParagraphNumbering } from "../prosemirror/styles/styleResolver.js";
3
4
  import { createNoteReferenceNumbering, noteReferenceMarker, noteReferenceToken } from "../utils/noteReferenceLabels.js";
4
5
  //#region src/markdown/internals.ts
5
6
  /**
@@ -18,7 +19,13 @@ import { createNoteReferenceNumbering, noteReferenceMarker, noteReferenceToken }
18
19
  * make the render quadratic.
19
20
  */
20
21
  function newContext(opts = {}, styles) {
22
+ const resolver = createStyleResolver(styles);
23
+ const numbering = /* @__PURE__ */ new Map();
21
24
  return {
25
+ paragraphNumbering: (styleId) => {
26
+ if (!numbering.has(styleId)) numbering.set(styleId, resolveStyleParagraphNumbering(resolver, styleId));
27
+ return numbering.get(styleId);
28
+ },
22
29
  builtInStyles: createBuiltInStyleIndex(styles?.styles ?? [], styles?.docDefaults),
23
30
  opts: {
24
31
  annotations: opts.annotations ?? "html",
@@ -1,4 +1,7 @@
1
1
  import { isQuoteStyle, resolveHeadingLevel } from "../docx/builtInStyles.js";
2
+ import { resolveCachedListRendering, resolveListRenderingDefinition } from "../docx/listRendering.js";
3
+ import { getCachedNumberingMap } from "../docx/numberingParser.js";
4
+ import { mergeParagraphNumbering } from "../docx/numberingReference.js";
2
5
  import { listLabelAttrsFromRendering } from "../prosemirror/listLabels.js";
3
6
  import { inlineBreaksToHtml } from "./escape.js";
4
7
  import { renderParagraphInline } from "./renderRuns.js";
@@ -32,7 +35,12 @@ function renderParagraph(ctx, pkg, para) {
32
35
  * does not define as prose, as every other reader reads them.
33
36
  */
34
37
  function renderParagraphBlock(ctx, pkg, para) {
35
- const label = ctx.nextListLabel(listLabelAttrs(para));
38
+ const numPrFromStyle = ctx.paragraphNumbering(para.formatting?.styleId) ?? para.formatting?.numPrFromStyle;
39
+ const numPr = mergeParagraphNumbering(numPrFromStyle, para.formatting?.numPr);
40
+ let list = para.listRendering;
41
+ if (numPr?.kind === "none" || pkg?.numbering !== void 0 && numPr === void 0) list = void 0;
42
+ else if (pkg?.numbering !== void 0 && numPr?.kind === "reference") list = resolveCachedListRendering(para.listRendering, resolveListRenderingDefinition(numPr, getCachedNumberingMap(pkg.numbering))) ?? void 0;
43
+ const label = ctx.nextListLabel(list ? listLabelAttrsFromRendering(list, numPrFromStyle) : UNNUMBERED);
36
44
  const inline = renderParagraphInline(ctx, pkg, para.content, para.paraId);
37
45
  const styleId = para.formatting?.styleId;
38
46
  const headingLevel = markdownHeadingLevel(ctx, para);
@@ -49,8 +57,8 @@ function renderParagraphBlock(ctx, pkg, para) {
49
57
  isListItem: false
50
58
  };
51
59
  }
52
- if (para.listRendering && label !== void 0) return {
53
- markdown: renderListItem(ctx, para.listRendering, label, inline),
60
+ if (list && label !== void 0) return {
61
+ markdown: renderListItem(ctx, list, label, inline),
54
62
  isListItem: true
55
63
  };
56
64
  ctx.listIndentWidths = [];
@@ -72,15 +80,6 @@ function markdownHeadingLevel(ctx, para) {
72
80
  }
73
81
  const UNNUMBERED = Object.freeze({});
74
82
  /**
75
- * The attrs the page's list counter reads, projected from the paragraph the
76
- * way the editor projects them (`toProseDoc`). A paragraph without a list
77
- * rendering counts as unnumbered.
78
- */
79
- function listLabelAttrs(para) {
80
- const list = para.listRendering;
81
- return list ? listLabelAttrsFromRendering(list, para.formatting?.numPrFromStyle) : UNNUMBERED;
82
- }
83
- /**
84
83
  * A plain paragraph whose visible text begins with markdown block syntax (e.g.
85
84
  * `# Not a heading`, `- value`, `1. value`, `> quote`) would be reclassified as
86
85
  * a heading/list/blockquote on re-parse, even though Word carries no matching
@@ -1,5 +1,5 @@
1
1
  import { getHyperlinkRuns } from "../docx/hyperlinkParser.js";
2
- import { RELATIONSHIP_TYPES, resolveRelationshipIdOfType } from "../docx/relsParser.js";
2
+ import { RELATIONSHIP_TYPES, resolveRelationshipIdOfType, resolveRelativePath } from "../docx/relsParser.js";
3
3
  import { decodeOoxmlSymbolCharacter } from "../utils/ooxmlSymbol.js";
4
4
  import { wrapComment, wrapDeletion, wrapInsertion, wrapMoveFrom, wrapMoveTo } from "./annotations.js";
5
5
  import { escapeAltText, escapeInline, escapeLinkUrl } from "./escape.js";
@@ -226,7 +226,7 @@ function renderRunContent(ctx, pkg, content, paraId) {
226
226
  case "noteMarker": break;
227
227
  case "drawing": {
228
228
  const ref = resolveRelationshipIdOfType(pkg?.relationships, item.image.rId, RELATIONSHIP_TYPES.image);
229
- const media = ref.status === "resolved" ? pkg?.media?.get(ref.relationship.target) : void 0;
229
+ const media = ref.status === "resolved" ? pkg?.media?.get(resolveRelativePath("word/document.xml", ref.relationship.target)) : void 0;
230
230
  if (media) {
231
231
  const reg = registerImage(ctx, media, item.image, paraId);
232
232
  const alt = reg.alt ? escapeAltText(reg.alt) : "";
@@ -490,17 +490,7 @@ function renderParagraphInline(ctx, pkg, content, paraId, edges = {
490
490
  case "simpleField":
491
491
  case "complexField": {
492
492
  const runs = item.type === "simpleField" ? item.content : item.fieldResult;
493
- for (const child of runs) {
494
- if (child.type === "run") {
495
- out += renderRuns(ctx, pkg, [child], paraId);
496
- continue;
497
- }
498
- if (child.type === "hyperlink") {
499
- out += renderHyperlink(ctx, pkg, child, paraId);
500
- continue;
501
- }
502
- out += child.type === "inlineWrapper" ? renderParagraphInline(ctx, pkg, child.content, paraId, UNKNOWN_NEIGHBORS) : child.text;
503
- }
493
+ for (const child of runs) out += child.type === "preservedInline" ? child.text : renderParagraphInline(ctx, pkg, [child], paraId, UNKNOWN_NEIGHBORS);
504
494
  break;
505
495
  }
506
496
  case "inlineSdt":
@@ -1,3 +1,4 @@
1
+ import { document_d_exports } from "../types/document.js";
1
2
  import { NoteReferenceKind, NoteReferenceNumbering } from "../utils/noteReferenceLabels.js";
2
3
  import { BuiltInStyleIndex } from "../docx/builtInStyles.js";
3
4
  import { ListLabelCounter } from "../prosemirror/listLabels.js";
@@ -140,6 +141,8 @@ type RenderContext = {
140
141
  nextListLabel: ListLabelCounter;
141
142
  /** The document's styles indexed by the built-in they are; classifies headings and quotes. */
142
143
  builtInStyles: BuiltInStyleIndex;
144
+ /** Authoritative paragraph numbering inherited through the style cascade. */
145
+ paragraphNumbering: (styleId: string | undefined) => document_d_exports.ParagraphFormatting["numPr"];
143
146
  /**
144
147
  * Active native Markdown ancestor marker widths, indexed by OOXML level.
145
148
  * Missing levels contribute no columns. Prose, headings and container
@@ -5,17 +5,17 @@
5
5
  *
6
6
  * The editor measures the width it has once, and this module decides how
7
7
  * each panel is presented. Every track the page shares the row with is
8
- * counted here, so the page, the outline and the comments cannot overlap:
9
- * a panel either gets a track of its own or becomes a drawer the user opens
10
- * on purpose. A drawer takes no track.
8
+ * counted here, so the page, the outline and the comments cannot overlap.
9
+ * Explicitly opening the outline gives it a clamped track beside the viewport;
10
+ * comments keep their existing column-or-drawer behavior.
11
11
  *
12
12
  * Space goes to the page first, then the outline rail, then the comments
13
13
  * column, then the full outline column:
14
14
  *
15
15
  * - comments: a column when the page, the comments and the outline rail fit;
16
16
  * otherwise a drawer.
17
- * - outline: a column when it fits beside the page and the comments column;
18
- * otherwise a rail of heading ticks when that fits; otherwise a drawer.
17
+ * - outline: a column when it fits beside the page and comments; otherwise a
18
+ * rail of heading ticks when that fits, or a toolbar toggle for the opened track.
19
19
  *
20
20
  * The tier names the result: `wide` when every panel has its full column,
21
21
  * `medium` when the outline is a rail, `narrow` when a panel is a drawer.
@@ -29,6 +29,8 @@ declare const PANEL_METRICS: {
29
29
  readonly outlineColumnWidth: 256;
30
30
  /** The outline rail in the `medium` tier, border included. */
31
31
  readonly outlineRailWidth: 32;
32
+ /** Minimum width and height of a shared panel control. */
33
+ readonly controlMinimumSize: 32;
32
34
  /** A comment card column. */
33
35
  readonly commentsWidth: 280;
34
36
  /** Space between the page edge and the comment cards, and after them. */
@@ -41,8 +43,8 @@ declare const PANEL_METRICS: {
41
43
  /** Width the comments take beside the page: gap, cards, gap. */
42
44
  declare const COMMENTS_TRACK_WIDTH: number;
43
45
  type PanelLayoutTier = "wide" | "medium" | "narrow";
44
- /** How the outline is shown: a column, a rail of ticks, a drawer, or not at all. */
45
- type OutlinePresentation = "none" | "column" | "rail" | "drawer";
46
+ /** How the outline is shown: a column, tick rail, drawer, or explicitly opened track. */
47
+ type OutlinePresentation = "none" | "column" | "rail" | "drawer" | "expanded";
46
48
  /** How the comments are shown: a column beside the page, a drawer, or not at all. */
47
49
  type CommentsPresentation = "hidden" | "column" | "drawer";
48
50
  type PanelLayoutInput = {
@@ -50,8 +52,8 @@ type PanelLayoutInput = {
50
52
  availableWidth: number;
51
53
  /** Width of the widest page at the current zoom. */
52
54
  pageWidth: number;
53
- /** Whether the document has an outline to show. */
54
- outline: "absent" | "available";
55
+ /** Whether the document outline is absent, available, or explicitly opened. */
56
+ outline: "absent" | "available" | "expanded";
55
57
  /** Whether the user (or the auto-open) wants the comments shown. */
56
58
  comments: "closed" | "open";
57
59
  };
@@ -5,17 +5,17 @@
5
5
  *
6
6
  * The editor measures the width it has once, and this module decides how
7
7
  * each panel is presented. Every track the page shares the row with is
8
- * counted here, so the page, the outline and the comments cannot overlap:
9
- * a panel either gets a track of its own or becomes a drawer the user opens
10
- * on purpose. A drawer takes no track.
8
+ * counted here, so the page, the outline and the comments cannot overlap.
9
+ * Explicitly opening the outline gives it a clamped track beside the viewport;
10
+ * comments keep their existing column-or-drawer behavior.
11
11
  *
12
12
  * Space goes to the page first, then the outline rail, then the comments
13
13
  * column, then the full outline column:
14
14
  *
15
15
  * - comments: a column when the page, the comments and the outline rail fit;
16
16
  * otherwise a drawer.
17
- * - outline: a column when it fits beside the page and the comments column;
18
- * otherwise a rail of heading ticks when that fits; otherwise a drawer.
17
+ * - outline: a column when it fits beside the page and comments; otherwise a
18
+ * rail of heading ticks when that fits, or a toolbar toggle for the opened track.
19
19
  *
20
20
  * The tier names the result: `wide` when every panel has its full column,
21
21
  * `medium` when the outline is a rail, `narrow` when a panel is a drawer.
@@ -29,6 +29,8 @@ const PANEL_METRICS = {
29
29
  outlineColumnWidth: 256,
30
30
  /** The outline rail in the `medium` tier, border included. */
31
31
  outlineRailWidth: 32,
32
+ /** Minimum width and height of a shared panel control. */
33
+ controlMinimumSize: 32,
32
34
  /** A comment card column. */
33
35
  commentsWidth: 280,
34
36
  /** Space between the page edge and the comment cards, and after them. */
@@ -49,7 +51,7 @@ const pageTrackWidth = (pageWidth) => pageWidth + 2 * PANEL_METRICS.pageMargin;
49
51
  const panelLayoutThresholds = ({ pageWidth, outline, comments }) => {
50
52
  const page = pageTrackWidth(pageWidth);
51
53
  const commentsTrack = comments === "open" ? COMMENTS_TRACK_WIDTH : 0;
52
- const hasOutline = outline === "available";
54
+ const hasOutline = outline !== "absent";
53
55
  return {
54
56
  wide: page + commentsTrack + (hasOutline ? PANEL_METRICS.outlineColumnWidth : 0),
55
57
  medium: page + commentsTrack + (hasOutline ? PANEL_METRICS.outlineRailWidth : 0)
@@ -62,7 +64,7 @@ const OUTLINE_TRACK_WIDTH = {
62
64
  drawer: 0
63
65
  };
64
66
  const tierFor = (outline, comments) => {
65
- if (outline === "drawer" || comments === "drawer") return "narrow";
67
+ if (outline === "drawer" || outline === "expanded" || comments === "drawer") return "narrow";
66
68
  if (outline === "rail") return "medium";
67
69
  return "wide";
68
70
  };
@@ -73,11 +75,13 @@ const computePanelLayout = (input) => {
73
75
  const page = pageTrackWidth(input.pageWidth);
74
76
  const comments = (() => {
75
77
  if (input.comments === "closed") return "hidden";
76
- return availableWidth >= thresholds.medium ? "column" : "drawer";
78
+ const requiredWidth = input.outline === "expanded" ? thresholds.wide : thresholds.medium;
79
+ return availableWidth >= requiredWidth ? "column" : "drawer";
77
80
  })();
78
81
  const commentsGutter = comments === "column" ? COMMENTS_TRACK_WIDTH : 0;
79
82
  const outline = (() => {
80
83
  if (input.outline === "absent") return "none";
84
+ if (input.outline === "expanded") return "expanded";
81
85
  const beside = page + commentsGutter;
82
86
  if (availableWidth >= thresholds.wide) return "column";
83
87
  if (availableWidth >= beside + PANEL_METRICS.outlineRailWidth) return "rail";
@@ -87,7 +91,7 @@ const computePanelLayout = (input) => {
87
91
  tier: tierFor(outline, comments),
88
92
  outline,
89
93
  comments,
90
- outlineTrackWidth: OUTLINE_TRACK_WIDTH[outline],
94
+ outlineTrackWidth: outline === "expanded" ? Math.min(PANEL_METRICS.outlineColumnWidth, Math.max(PANEL_METRICS.controlMinimumSize, (availableWidth - commentsGutter) / 2)) : OUTLINE_TRACK_WIDTH[outline],
91
95
  commentsGutter,
92
96
  thresholds
93
97
  };
@@ -0,0 +1,18 @@
1
+ import { Mark } from "prosemirror-model";
2
+ import { Transform } from "prosemirror-transform";
3
+ //#region src/prosemirror/addTrackedDeletionMark.d.ts
4
+ type AddTrackedDeletionMarkOptions = {
5
+ tr: Transform;
6
+ from: number;
7
+ to: number;
8
+ mark: Mark;
9
+ insertionPolicy: "preserve-pending" | "retract-own";
10
+ };
11
+ /**
12
+ * Already deleted runs keep their revision identity and enclosing insertion path.
13
+ * An own insertion containing deleted descendants is marked rather than retracted:
14
+ * removing its carrier would also remove another pending revision.
15
+ */
16
+ declare const addTrackedDeletionMark: ({ tr, from, to, mark, insertionPolicy }: AddTrackedDeletionMarkOptions) => void;
17
+ //#endregion
18
+ export { addTrackedDeletionMark };
@@ -0,0 +1,87 @@
1
+ import { expectTrackedChangeMarkAttrs } from "./attrs/index.js";
2
+ import { trackedRevisionLayerOf } from "./trackedRevisionPath.js";
3
+ import { canCarryTrackedRunMark } from "./trackedRunInlineAtoms.js";
4
+ import { panic } from "better-result";
5
+ //#region src/prosemirror/addTrackedDeletionMark.ts
6
+ /**
7
+ * Already deleted runs keep their revision identity and enclosing insertion path.
8
+ * An own insertion containing deleted descendants is marked rather than retracted:
9
+ * removing its carrier would also remove another pending revision.
10
+ */
11
+ const addTrackedDeletionMark = ({ tr, from, to, mark, insertionPolicy }) => {
12
+ if (mark.type.name !== "deletion") panic("A tracked deletion requires a deletion mark");
13
+ const preservedDeletions = [];
14
+ const protectedCarriers = /* @__PURE__ */ new Set();
15
+ tr.doc.nodesBetween(from, to, (node, position) => {
16
+ const existing = node.marks.find(({ type }) => type.name === "deletion");
17
+ if (node.isInline && existing) {
18
+ const start = Math.max(from, position);
19
+ const end = Math.min(to, position + node.nodeSize);
20
+ if (start < end) preservedDeletions.push({
21
+ from: start,
22
+ to: end,
23
+ mark: existing
24
+ });
25
+ const at = tr.doc.resolve(position);
26
+ for (let depth = 1; depth <= at.depth; depth++) {
27
+ const ancestor = at.node(depth);
28
+ if (canCarryTrackedRunMark(ancestor)) protectedCarriers.add(ancestor);
29
+ }
30
+ }
31
+ return true;
32
+ });
33
+ const mapFrom = tr.mapping.maps.length;
34
+ const ranges = [];
35
+ const descendantDeletions = [];
36
+ const deletionFor = (node) => {
37
+ const insertion = node.marks.find(({ type }) => type.name === "insertion");
38
+ const ancestors = insertion ? expectTrackedChangeMarkAttrs(insertion)._docxRevisionAncestors : null;
39
+ return insertion && ancestors?.length ? mark.type.create({
40
+ ...mark.attrs,
41
+ _docxRevisionAncestors: [...ancestors, trackedRevisionLayerOf(insertion, node)]
42
+ }) : mark;
43
+ };
44
+ tr.doc.nodesBetween(from, to, (node, position) => {
45
+ if (!canCarryTrackedRunMark(node)) return true;
46
+ if (node.marks.some(({ type }) => type.name === "deletion")) return false;
47
+ const start = Math.max(from, position);
48
+ const end = Math.min(to, position + node.nodeSize);
49
+ if (start >= end) return false;
50
+ const insertion = node.marks.find(({ type }) => type.name === "insertion");
51
+ const ownInsertion = insertion !== void 0 && insertion.attrs["author"] === mark.attrs["author"];
52
+ const deletion = deletionFor(node);
53
+ const disposition = insertionPolicy === "retract-own" && ownInsertion && !protectedCarriers.has(node) ? "retract" : "mark";
54
+ const previous = ranges.at(-1);
55
+ if (previous?.to === start && previous.disposition === disposition && previous.mark.eq(deletion)) previous.to = end;
56
+ else ranges.push({
57
+ from: start,
58
+ to: end,
59
+ mark: deletion,
60
+ disposition
61
+ });
62
+ if (disposition === "mark") node.descendants((child, offset) => {
63
+ if (child.marks.some(({ type }) => type.name === "deletion")) return false;
64
+ const childDeletion = deletionFor(child);
65
+ if (childDeletion === mark) return true;
66
+ const childStart = Math.max(from, position + 1 + offset);
67
+ const childEnd = Math.min(to, position + 1 + offset + child.nodeSize);
68
+ if (childStart < childEnd) descendantDeletions.push({
69
+ from: childStart,
70
+ to: childEnd,
71
+ mark: childDeletion
72
+ });
73
+ return true;
74
+ });
75
+ return false;
76
+ });
77
+ for (const range of ranges.toReversed()) if (range.disposition === "retract") tr.delete(range.from, range.to);
78
+ else tr.addMark(range.from, range.to, range.mark);
79
+ const mapping = tr.mapping.slice(mapFrom);
80
+ for (const preserved of [...descendantDeletions, ...preservedDeletions]) {
81
+ const start = mapping.map(preserved.from);
82
+ const end = mapping.map(preserved.to);
83
+ if (start < end) tr.addMark(start, end, preserved.mark);
84
+ }
85
+ };
86
+ //#endregion
87
+ export { addTrackedDeletionMark };
@@ -8,6 +8,8 @@ import { attrsRecord, attrsResult, expectAttrs, expectCachedNodeAttrs, expectNod
8
8
  import { COUNTER_FORMAT_VALUES, EMPHASIS_MARK_VALUES, FIELD_TYPE_VALUES, FONT_HINT_VALUES, FONT_THEME_VALUES, HIGHLIGHT_COLOR_VALUES, IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES, IMAGE_WRAP_TEXT_VALUES, IMAGE_WRAP_TYPE_VALUES, LINE_SPACING_RULE_VALUES, OUTLINE_STYLE_ATTR_VALUES, PARAGRAPH_ALIGNMENT_VALUES, POSITIONAL_TAB_ALIGNMENT_VALUES, POSITIONAL_TAB_LEADER_VALUES, POSITIONAL_TAB_RELATIVE_TO_VALUES, SDT_LOCK_VALUES, SDT_TYPE_VALUES, SHADING_PATTERN_VALUES, TABLE_CELL_VERTICAL_ALIGNMENT_VALUES, TABLE_JUSTIFICATION_VALUES, TABLE_ROW_HEIGHT_RULE_VALUES, TABLE_WIDTH_TYPE_VALUES, TAB_LEADER_VALUES, TAB_STOP_ALIGNMENT_VALUES, TEXT_DIRECTION_VALUES, TEXT_EFFECT_VALUES, UNDERLINE_STYLE_VALUES } from "../../types/documentEnumValues.js";
9
9
  import { canonicalJson } from "../../utils/canonicalJson.js";
10
10
  import { normalizeHorizontalScalePercent } from "../../utils/horizontalScale.js";
11
+ import { PICTURE_NAME_DOM_ATTRIBUTES } from "../imagePictureNames.js";
12
+ import { isNoteReferenceOccurrenceId } from "../noteReferenceOccurrenceId.js";
11
13
  import { paragraphNumberingFromAttrValue } from "../numberingAttr.js";
12
14
  import { outlineLevelFromAttrValue } from "../outlineLevelAttr.js";
13
15
  import { isParagraphDirection } from "../paragraphDirection.js";
@@ -488,6 +490,9 @@ const readImageAttrs = (node) => {
488
490
  expectNodeType(node, "image", issues);
489
491
  requiredString(attrs, "src", "image.attrs.src", issues);
490
492
  optionalString(attrs, "docPrName", "image.attrs.docPrName", issues);
493
+ optionalNestedRecord(attrs, "pictureNames", "image.attrs.pictureNames", issues, (names, path, nestedIssues) => {
494
+ for (const key of Object.keys(PICTURE_NAME_DOM_ATTRIBUTES)) optionalString(names, key, `${path}.${key}`, nestedIssues);
495
+ });
491
496
  optionalString(attrs, "alt", "image.attrs.alt", issues);
492
497
  optionalString(attrs, "title", "image.attrs.title", issues);
493
498
  optionalString(attrs, "rId", "image.attrs.rId", issues);
@@ -953,6 +958,10 @@ const readFootnoteRefMarkAttrs = (mark) => {
953
958
  const issues = [];
954
959
  expectMarkType(mark, "footnoteRef", issues);
955
960
  requiredStringOrNumber(attrs, "id", "footnoteRef.attrs.id", issues);
961
+ if (!isNoteReferenceOccurrenceId(attrs["occurrenceId"])) issues.push({
962
+ path: "footnoteRef.attrs.occurrenceId",
963
+ message: "Expected a nonblank occurrence identity."
964
+ });
956
965
  optionalOneOf(attrs, "noteType", "footnoteRef.attrs.noteType", issues, NOTE_TYPES);
957
966
  optionalOneOf(attrs, "vertAlign", "footnoteRef.attrs.vertAlign", issues, NOTE_REF_VERT_ALIGNS);
958
967
  optionalBoolean(attrs, "customMarkFollows", "footnoteRef.attrs.customMarkFollows", issues);
@@ -10,20 +10,23 @@ import { resolveEmptyFieldResultRuns } from "../emptyFieldResultRuns.js";
10
10
  import { JOINED_RUNS_RESTYLED_META } from "../extensions/features/JoinedRunStyleExtension.js";
11
11
  import { markChangedParagraphRanges, markStructuralChange, markTrackedSectionEndpointRemoval } from "../extensions/features/ParagraphChangeTrackerExtension.js";
12
12
  import "../extensions/nodes/SdtExtension.js";
13
+ import { canonicalFieldNode } from "../fieldRepresentation.js";
13
14
  import { resolveStateStory } from "../markupViewProjection.js";
14
15
  import { joinAtParagraphMark } from "../paragraphMarkJoin.js";
15
16
  import { getDocumentNumbering } from "../plugins/documentNumbering.js";
16
17
  import { getDocumentStyleResolver } from "../plugins/documentStyleState.js";
18
+ import { resolvedPreservedXmlAttrs } from "../preservedXmlReview.js";
17
19
  import { rebaseParagraphRuns, setParagraphAttrsWithRebasedRunFormatting } from "../rebaseParagraphRunFormatting.js";
18
20
  import { rejoinRunsAt } from "../rejoinRunCarriers.js";
19
21
  import { getFolioNodeRevisionCarriers, nodePropertyRevisionSites } from "../revisionCarriers.js";
20
22
  import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
21
- import { reconstructRejectedRunFormattingMarks } from "../runPropertyChangeResolution.js";
23
+ import { reconstructRejectedRunFormattingMarks, restoreHistoricalRunFormatting } from "../runPropertyChangeResolution.js";
22
24
  import { paragraphRunStyleContext, paragraphRunStyleContextAt } from "../runStyleFormatting.js";
23
25
  import { getTableCellMergeChange } from "../tableCellMergeRevision.js";
24
26
  import { isTableCellRetainedInReviewView } from "../tableCellRevisionVisibility.js";
25
27
  import { markRowEmptiedInBatch, reconcileTableGridAfterColumnRemoval, removeRowsWithoutCellsAfterBatch, removeTableRow } from "../tableGridMutation.js";
26
28
  import { runInCellPending, runParagraphIntoTable } from "../tableRunIn.js";
29
+ import { retainedTrackedRevisionMark, trackedRevisionPathOf } from "../trackedRevisionPath.js";
27
30
  import { holdsNoContent } from "../zeroWidthAnchors.js";
28
31
  import { inlineBookmarksForParagraphJoin } from "./paragraphBookmarkJoin.js";
29
32
  import { hasSerializableParagraphPropertyChange, paragraphPropertiesSnapshot } from "./propertyChangeScope.js";
@@ -32,10 +35,16 @@ import { resolveParagraphChangeAttrs } from "./resolveParagraphProperties.js";
32
35
  import { hasMatchingCollapsedTableCellMerge, resolveCollapsedTableCellMerge, resolveVisibleTableCellMerge } from "./tableCellMergeResolution.js";
33
36
  import { panic } from "better-result";
34
37
  import { PARAGRAPH_MARK_CHANGE_KINDS, REVIEW_CARRIERS } from "@stll/docx-core/model";
38
+ import { Fragment } from "prosemirror-model";
35
39
  import { TableMap } from "prosemirror-tables";
36
40
  import { Mapping } from "prosemirror-transform";
37
41
  //#region src/prosemirror/commands/comments.ts
38
42
  /**
43
+ * Comment and Track Changes Commands
44
+ *
45
+ * PM commands for adding/removing comments and accepting/rejecting tracked changes.
46
+ */
47
+ /**
39
48
  * Add a comment mark to the current selection.
40
49
  */
41
50
  function addCommentMark(commentId) {
@@ -67,21 +76,6 @@ function removeCommentMark(commentId) {
67
76
  return true;
68
77
  };
69
78
  }
70
- const revisionLayerOf = (mark) => {
71
- const attrs = expectTrackedChangeMarkAttrs(mark);
72
- let type;
73
- if (mark.type.name === "insertion") type = attrs.moveKind === "moveTo" ? "moveTo" : "insertion";
74
- else type = attrs.moveKind === "moveFrom" ? "moveFrom" : "deletion";
75
- return {
76
- type,
77
- revisionId: attrs.revisionId,
78
- author: attrs.author,
79
- ...attrs.date ? { date: attrs.date } : {},
80
- ...attrs.utcDate ? { utcDate: attrs.utcDate } : {},
81
- ...attrs.initials ? { initials: attrs.initials } : {},
82
- outerWrapperCount: attrs._docxOuterWrapperCount ?? 0
83
- };
84
- };
85
79
  const revisionLayerRemovesContent = (layer, mode) => mode === "accept" ? layer.type === "deletion" || layer.type === "moveFrom" : layer.type === "insertion" || layer.type === "moveTo";
86
80
  /**
87
81
  * Resolve a tracked change: accept or reject.
@@ -189,6 +183,19 @@ function resolveChange(from, to, mode, revisionIds) {
189
183
  const runPropertyChangeMark = node.marks.find((mark) => mark.type.name === "runPropertyChange");
190
184
  const removesNode = removeType !== void 0 && node.marks.some((mark) => mark.type === removeType && matchesRevision(mark));
191
185
  if (removesNode || runPropertyChangeMark !== void 0 || node.marks.some((mark) => (mark.type === insertionType || mark.type === deletionType) && matchesRevision(mark))) resolvedBoundaries.push(rangeFrom, rangeTo);
186
+ if (mode === "reject" && node.marks.find((mark) => mark.type === keepType && matchesRevision(mark) && expectTrackedChangeMarkAttrs(mark)._historicalFormatting && !expectTrackedChangeMarkAttrs(mark)._docxRevisionAncestors?.some((layer) => (layer.type === "deletion" || layer.type === "moveFrom") && revisionSet !== null && !revisionSet.has(layer.revisionId))) && !removesNode) {
187
+ const nextMarks = restoreHistoricalRunFormatting({
188
+ node,
189
+ paragraphContext: paragraphRunStyleContextAt({
190
+ doc: tr.doc,
191
+ pos: rangeFrom,
192
+ styleResolver
193
+ }),
194
+ styleResolver
195
+ });
196
+ for (const mark of node.marks) if (RUN_FORMATTING_MARK_NAMES.has(mark.type.name)) tr.removeMark(rangeFrom, rangeTo, mark);
197
+ for (const mark of nextMarks) if (RUN_FORMATTING_MARK_NAMES.has(mark.type.name)) tr.addMark(rangeFrom, rangeTo, mark);
198
+ }
192
199
  if (runPropertyChangeMark) resolveRunPropertyChange({
193
200
  tr,
194
201
  node,
@@ -201,7 +208,7 @@ function resolveChange(from, to, mode, revisionIds) {
201
208
  });
202
209
  const nestedMark = node.marks.find((mark) => (mark.type === insertionType || mark.type === deletionType) && (expectTrackedChangeMarkAttrs(mark)._docxRevisionAncestors?.length ?? 0) > 0);
203
210
  if (nestedMark) {
204
- const path = [...expectTrackedChangeMarkAttrs(nestedMark)._docxRevisionAncestors ?? [], revisionLayerOf(nestedMark)];
211
+ const path = trackedRevisionPathOf(nestedMark);
205
212
  const selected = (layer) => revisionSet === null || revisionSet.has(layer.revisionId);
206
213
  if (path.some(selected)) if (path.some((layer) => selected(layer) && revisionLayerRemovesContent(layer, mode))) deleteRanges.push({
207
214
  from: rangeFrom,
@@ -209,23 +216,13 @@ function resolveChange(from, to, mode, revisionIds) {
209
216
  });
210
217
  else {
211
218
  tr.removeMark(rangeFrom, rangeTo, nestedMark);
212
- const remaining = path.filter((layer) => !selected(layer));
213
- const active = remaining.at(-1);
214
- if (active) {
215
- const type = active.type === "insertion" || active.type === "moveTo" ? insertionType : deletionType;
216
- if (!type) panic("A nested tracked revision has no editor mark type");
217
- tr.addMark(rangeFrom, rangeTo, type.create({
218
- revisionId: active.revisionId,
219
- author: active.author,
220
- date: active.date ?? null,
221
- utcDate: active.utcDate ?? null,
222
- initials: active.initials ?? null,
223
- moveKind: active.type === "moveTo" || active.type === "moveFrom" ? active.type : null,
224
- _historicalFormatting: active.type === "deletion" || active.type === "moveFrom" ? true : null,
225
- _docxOuterWrapperCount: active.outerWrapperCount,
226
- _docxRevisionAncestors: remaining.length > 1 ? remaining.slice(0, -1) : null
227
- }));
228
- }
219
+ const retained = retainedTrackedRevisionMark(nestedMark, (layer) => !selected(layer));
220
+ if (retained) tr.addMark(rangeFrom, rangeTo, retained);
221
+ }
222
+ const current = tr.doc.nodeAt(pos);
223
+ if (current) {
224
+ const attrs = resolvedPreservedXmlAttrs(node, current.marks);
225
+ if (attrs !== node.attrs) tr.setNodeMarkup(pos, void 0, attrs);
229
226
  }
230
227
  return true;
231
228
  }
@@ -233,6 +230,10 @@ function resolveChange(from, to, mode, revisionIds) {
233
230
  from: rangeFrom,
234
231
  to: rangeTo
235
232
  });
233
+ if (!removesNode && keepType && node.marks.some((mark) => mark.type === keepType && matchesRevision(mark))) {
234
+ const attrs = resolvedPreservedXmlAttrs(node, node.marks.filter((mark) => !(mark.type === keepType && matchesRevision(mark))));
235
+ if (attrs !== node.attrs) tr.setNodeMarkup(pos, void 0, attrs);
236
+ }
236
237
  if (!removeKeptMarksInBulk) {
237
238
  for (const mark of node.marks) if (keepType && mark.type === keepType && matchesRevision(mark)) tr.removeMark(rangeFrom, rangeTo, mark);
238
239
  }
@@ -255,7 +256,17 @@ function resolveChange(from, to, mode, revisionIds) {
255
256
  }
256
257
  rangesToDelete = coalescedDeleteRanges;
257
258
  }
258
- for (const range of rangesToDelete.toReversed()) tr.delete(range.from, range.to);
259
+ for (const range of rangesToDelete.toReversed()) {
260
+ const start = tr.doc.resolve(range.from);
261
+ const field = start.parent;
262
+ if (field.type.name === "structuredField" && start.parentOffset === 0 && range.to === start.end()) {
263
+ const position = start.before();
264
+ const empty = canonicalFieldNode(field.copy(Fragment.empty));
265
+ tr.replaceWith(position, position + field.nodeSize, empty);
266
+ continue;
267
+ }
268
+ tr.delete(range.from, range.to);
269
+ }
259
270
  if (deleteTextBoxesAnchoredAt(tr, droppedTextBoxAnchorIds(state.doc, tr.doc))) markStructuralChange(tr);
260
271
  tableRowStructuralOps.sort((left, right) => right.rowPos - left.rowPos);
261
272
  let resolvedTableRowStructure = false;
@@ -418,6 +429,20 @@ function resolveChange(from, to, mode, revisionIds) {
418
429
  boundaries: resolvedBoundaries.map((position) => tr.mapping.map(position)).filter((position) => !seams.has(position)),
419
430
  styleResolver
420
431
  });
432
+ const resolvedFields = [];
433
+ tr.doc.descendants((node, position) => {
434
+ const canonical = canonicalFieldNode(node);
435
+ if (canonical !== node) resolvedFields.push({
436
+ node: canonical,
437
+ position
438
+ });
439
+ });
440
+ for (const { node, position } of resolvedFields.toReversed()) {
441
+ const field = tr.doc.nodeAt(position);
442
+ if (!field) continue;
443
+ if (field.type === node.type) tr.setNodeMarkup(position, void 0, node.attrs, node.marks);
444
+ else tr.replaceWith(position, position + field.nodeSize, node);
445
+ }
421
446
  if (tr.steps.length > 0) dispatch(tr);
422
447
  }
423
448
  return true;