@stll/folio-core 0.53.0 → 0.54.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 (148) hide show
  1. package/dist/ai-edits/apply.js +390 -166
  2. package/dist/ai-edits/batch-claims.d.ts +21 -6
  3. package/dist/ai-edits/batch-claims.js +47 -16
  4. package/dist/ai-edits/headless.d.ts +20 -0
  5. package/dist/ai-edits/headless.js +140 -11
  6. package/dist/ai-edits/pending-suggestions.js +1 -1
  7. package/dist/ai-edits/read.d.ts +5 -3
  8. package/dist/ai-edits/read.js +97 -47
  9. package/dist/ai-edits/revisionStretches.d.ts +17 -0
  10. package/dist/ai-edits/revisionStretches.js +80 -0
  11. package/dist/ai-edits/snapshot.js +3 -2
  12. package/dist/ai-edits/table-cell-mutations.d.ts +3 -1
  13. package/dist/ai-edits/table-cell-mutations.js +2 -1
  14. package/dist/ai-edits/table-row-column-mutations.d.ts +6 -1
  15. package/dist/ai-edits/table-row-column-mutations.js +27 -10
  16. package/dist/compare/content-alignment.d.ts +5 -1
  17. package/dist/compare/content-alignment.js +39 -32
  18. package/dist/compare/inline-provenance.js +1 -1
  19. package/dist/controller/layoutPipeline.js +3 -2
  20. package/dist/docx/archiveInflation.d.ts +127 -0
  21. package/dist/docx/archiveInflation.js +181 -0
  22. package/dist/docx/metadataPrivacy.js +24 -1
  23. package/dist/docx/noteReferenceMark.d.ts +15 -0
  24. package/dist/docx/noteReferenceMark.js +48 -0
  25. package/dist/docx/paragraphParser.js +5 -3
  26. package/dist/docx/paragraphPropertySource.d.ts +1 -1
  27. package/dist/docx/paragraphPropertySource.js +3 -1
  28. package/dist/docx/parser.js +2 -2
  29. package/dist/docx/rezip.js +4 -2
  30. package/dist/docx/selectiveXmlPatch.d.ts +3 -1
  31. package/dist/docx/selectiveXmlPatch.js +81 -3
  32. package/dist/docx/server/boundedArchive.d.ts +8 -13
  33. package/dist/docx/server/boundedArchive.js +70 -59
  34. package/dist/docx/server/extractDocxText.d.ts +8 -2
  35. package/dist/docx/server/extractDocxText.js +2 -2
  36. package/dist/docx/server/validateDocxConformance.d.ts +1 -1
  37. package/dist/docx/server/validateDocxConformance.js +1 -0
  38. package/dist/docx/unzip.d.ts +19 -2
  39. package/dist/docx/unzip.js +135 -24
  40. package/dist/fonts/embeddedFonts.js +1 -1
  41. package/dist/i18n/messages/catalogs.gen.d.ts +34 -34
  42. package/dist/i18n/messages/catalogs.gen.js +34 -34
  43. package/dist/i18n/messages/messages.gen.d.ts +2 -2
  44. package/dist/internal/wholeStoryRevisionResolution.js +95 -25
  45. package/dist/layout-engine/measure/measureParagraph.js +2 -2
  46. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +12 -1
  47. package/dist/layout-engine/measure/paragraphMeasureShared.js +14 -1
  48. package/dist/markdown/escape.d.ts +2 -2
  49. package/dist/markdown/escape.js +4 -5
  50. package/dist/markdown/renderBlock.js +91 -25
  51. package/dist/markdown/renderRuns.d.ts +10 -1
  52. package/dist/markdown/renderRuns.js +320 -59
  53. package/dist/prosemirror/clearRunColor.d.ts +6 -0
  54. package/dist/prosemirror/clearRunColor.js +46 -0
  55. package/dist/prosemirror/commands/comments.js +56 -21
  56. package/dist/prosemirror/commands/formatPainter.js +1 -1
  57. package/dist/prosemirror/commands/hyperlink.js +2 -1
  58. package/dist/prosemirror/commands/propertyChangeScope.d.ts +3 -2
  59. package/dist/prosemirror/commands/propertyChangeScope.js +24 -3
  60. package/dist/prosemirror/commands/resolveAllTableChanges.js +46 -41
  61. package/dist/prosemirror/commands/resolveParagraphProperties.js +20 -6
  62. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +8 -1
  63. package/dist/prosemirror/commands/tableCellMergeResolution.js +23 -7
  64. package/dist/prosemirror/containerFinalParagraph.d.ts +11 -5
  65. package/dist/prosemirror/containerFinalParagraph.js +7 -6
  66. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -13
  67. package/dist/prosemirror/conversion/fromProseDoc.js +12 -590
  68. package/dist/prosemirror/conversion/toProseDoc.d.ts +4 -11
  69. package/dist/prosemirror/conversion/toProseDoc.js +31 -150
  70. package/dist/prosemirror/documentSchema.d.ts +8 -0
  71. package/dist/prosemirror/documentSchema.js +18 -0
  72. package/dist/prosemirror/extensions/StarterKit.js +2 -0
  73. package/dist/prosemirror/extensions/core/ParagraphExtension.js +104 -63
  74. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +1 -1
  75. package/dist/prosemirror/extensions/features/EmptyParagraphFormatExtension.js +24 -3
  76. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.d.ts +20 -0
  77. package/dist/prosemirror/extensions/features/JoinedRunStyleExtension.js +43 -0
  78. package/dist/prosemirror/extensions/features/ListExtension.js +10 -9
  79. package/dist/prosemirror/extensions/features/pastedHeadingStyles.js +1 -1
  80. package/dist/prosemirror/extensions/features/pastedHtmlLists.js +2 -4
  81. package/dist/prosemirror/extensions/marks/FootnoteRefExtension.js +14 -8
  82. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +8 -4
  83. package/dist/prosemirror/extensions/marks/StrikeExtension.js +2 -2
  84. package/dist/prosemirror/extensions/marks/SubscriptExtension.js +2 -2
  85. package/dist/prosemirror/extensions/marks/SuperscriptExtension.js +2 -2
  86. package/dist/prosemirror/extensions/marks/TextColorExtension.js +4 -3
  87. package/dist/prosemirror/extensions/marks/markUtils.d.ts +8 -6
  88. package/dist/prosemirror/extensions/marks/markUtils.js +17 -8
  89. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.d.ts +3 -1
  90. package/dist/prosemirror/extensions/marks/noteReferenceDeletion.js +12 -1
  91. package/dist/prosemirror/extensions/nodes/CommentReferenceExtension.js +2 -2
  92. package/dist/prosemirror/extensions/nodes/HardBreakExtension.js +5 -3
  93. package/dist/prosemirror/extensions/nodes/TableExtension.js +104 -210
  94. package/dist/prosemirror/extensions/types.d.ts +7 -0
  95. package/dist/prosemirror/hyperlinkRemoval.d.ts +9 -0
  96. package/dist/prosemirror/hyperlinkRemoval.js +129 -0
  97. package/dist/prosemirror/index.d.ts +2 -1
  98. package/dist/prosemirror/index.js +2 -1
  99. package/dist/prosemirror/listNumbering.d.ts +3 -21
  100. package/dist/prosemirror/listNumbering.js +10 -25
  101. package/dist/prosemirror/listRendering.d.ts +27 -0
  102. package/dist/prosemirror/listRendering.js +27 -0
  103. package/dist/prosemirror/markupViewNotes.d.ts +8 -0
  104. package/dist/prosemirror/markupViewNotes.js +49 -0
  105. package/dist/prosemirror/markupViewProjection.d.ts +3 -1
  106. package/dist/prosemirror/markupViewProjection.js +7 -2
  107. package/dist/prosemirror/noteReferenceReview.d.ts +77 -0
  108. package/dist/prosemirror/noteReferenceReview.js +332 -0
  109. package/dist/prosemirror/paragraphIndentation.js +5 -2
  110. package/dist/prosemirror/paragraphMarkJoin.d.ts +6 -4
  111. package/dist/prosemirror/paragraphMarkJoin.js +21 -19
  112. package/dist/prosemirror/paragraphPropertyCarry.d.ts +64 -0
  113. package/dist/prosemirror/paragraphPropertyCarry.js +158 -0
  114. package/dist/prosemirror/plugins/documentStyleState.d.ts +26 -0
  115. package/dist/prosemirror/plugins/documentStyleState.js +32 -0
  116. package/dist/prosemirror/plugins/documentStyles.d.ts +2 -21
  117. package/dist/prosemirror/plugins/documentStyles.js +66 -29
  118. package/dist/prosemirror/plugins/index.d.ts +2 -1
  119. package/dist/prosemirror/plugins/index.js +2 -1
  120. package/dist/prosemirror/plugins/paragraphStyleResolution.d.ts +15 -0
  121. package/dist/prosemirror/plugins/paragraphStyleResolution.js +230 -0
  122. package/dist/prosemirror/plugins/suggestionMode.d.ts +12 -2
  123. package/dist/prosemirror/plugins/suggestionMode.js +391 -28
  124. package/dist/prosemirror/rebaseParagraphRunFormatting.d.ts +22 -2
  125. package/dist/prosemirror/rebaseParagraphRunFormatting.js +39 -13
  126. package/dist/prosemirror/rebaseParagraphRuns.d.ts +36 -0
  127. package/dist/prosemirror/rebaseParagraphRuns.js +99 -0
  128. package/dist/prosemirror/runFormattingFromMarks.d.ts +18 -0
  129. package/dist/prosemirror/runFormattingFromMarks.js +567 -0
  130. package/dist/prosemirror/runFormattingReconciliation.js +10 -9
  131. package/dist/prosemirror/schema/nodes.d.ts +2 -0
  132. package/dist/prosemirror/styles/paragraphStyleCascade.d.ts +59 -0
  133. package/dist/prosemirror/styles/paragraphStyleCascade.js +145 -0
  134. package/dist/prosemirror/styles/resolvedStyleAttrs.d.ts +21 -2
  135. package/dist/prosemirror/styles/resolvedStyleAttrs.js +38 -1
  136. package/dist/prosemirror/styles/tableStyleRegions.d.ts +31 -0
  137. package/dist/prosemirror/styles/tableStyleRegions.js +55 -0
  138. package/dist/prosemirror/tableCellPaste.d.ts +74 -0
  139. package/dist/prosemirror/tableCellPaste.js +525 -0
  140. package/dist/prosemirror/tableGridMutation.d.ts +53 -14
  141. package/dist/prosemirror/tableGridMutation.js +166 -18
  142. package/dist/prosemirror/tableRunIn.d.ts +51 -0
  143. package/dist/prosemirror/tableRunIn.js +165 -0
  144. package/dist/prosemirror/textInput.js +11 -2
  145. package/dist/prosemirror/trackedRevisionPath.d.ts +17 -0
  146. package/dist/prosemirror/trackedRevisionPath.js +32 -0
  147. package/dist/server.d.ts +2 -2
  148. package/package.json +5 -5
@@ -1,12 +1,23 @@
1
- import { expectParagraphAttrs } from "../attrs/index.js";
1
+ import { expectParagraphAttrs, expectTrackedChangeMarkAttrs } from "../attrs/index.js";
2
+ import { paragraphPropertiesSnapshot } from "../commands/propertyChangeScope.js";
3
+ import { paragraphEndsItsContainer } from "../containerFinalParagraph.js";
2
4
  import { encloseWholeControls } from "../contentControlRevisions.js";
3
5
  import { splitBlockClearBorders } from "../extensions/features/BaseKeymapExtension.js";
6
+ import { JOINED_RUNS_RESTYLED_META } from "../extensions/features/JoinedRunStyleExtension.js";
4
7
  import { expandNoteReferenceDeletionRange } from "../extensions/marks/noteReferenceDeletion.js";
8
+ import { carryParagraphProperties, paragraphLeftAfter, recordReplacedParagraphProperties } from "../paragraphPropertyCarry.js";
9
+ import { rebaseParagraphRuns } from "../rebaseParagraphRunFormatting.js";
10
+ import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
11
+ import { paragraphRunStyleContext } from "../runStyleFormatting.js";
12
+ import { cellPasteRange, pasteTableCells } from "../tableCellPaste.js";
5
13
  import { handleEditorBeforeInput } from "../textInput.js";
6
14
  import { canCarryTrackedRunMark } from "../trackedRunInlineAtoms.js";
15
+ import { getDocumentNumbering } from "./documentNumbering.js";
16
+ import { getDocumentStyleResolver } from "./documentStyleState.js";
7
17
  import { mintRevisionId, seedRevisionIdsFromDoc } from "./revisionIds.js";
8
- import { Mapping } from "prosemirror-transform";
9
- import { Plugin, PluginKey, TextSelection } from "prosemirror-state";
18
+ import { Slice } from "prosemirror-model";
19
+ import { Mapping, ReplaceStep } from "prosemirror-transform";
20
+ import { AllSelection, Plugin, PluginKey, Selection, TextSelection } from "prosemirror-state";
10
21
  import { isHistoryTransaction } from "prosemirror-history";
11
22
  import { undoInputRule } from "prosemirror-inputrules";
12
23
  //#region src/prosemirror/plugins/suggestionMode.ts
@@ -48,6 +59,27 @@ function makeRevisionInfo(state) {
48
59
  date: attrs.date
49
60
  };
50
61
  }
62
+ /**
63
+ * A removed paragraph mark leaves the paragraph after it once accepted. An
64
+ * edit that reads as the paragraph before it (Backspace or Delete at the
65
+ * break, a deletion running from inside one paragraph into another) gives the
66
+ * paragraph left that paragraph's properties now, under the mark's revision:
67
+ * accepting reads as the edit made directly, rejecting restores its own.
68
+ */
69
+ function carryIntoParagraphLeft(state, tr, source, survivorPos, info) {
70
+ carryParagraphProperties({
71
+ tr,
72
+ position: survivorPos,
73
+ source,
74
+ styleResolver: getDocumentStyleResolver(state),
75
+ numbering: getDocumentNumbering(state),
76
+ revision: {
77
+ id: info.id,
78
+ author: info.author ?? "",
79
+ date: info.date ?? ""
80
+ }
81
+ });
82
+ }
51
83
  function makeParagraphMarkInfo(pluginState) {
52
84
  const attrs = makeMarkAttrs(pluginState);
53
85
  return {
@@ -82,7 +114,7 @@ function findAdjacentRevisionForRange(doc, from, to, markTypeName, author) {
82
114
  * Retract the current author's own inserted text.
83
115
  * Processes in reverse order to maintain position validity.
84
116
  */
85
- function markRangeAsDeleted(tr, doc, from, to, insertionType, deletionType, pluginState) {
117
+ function markRangeAsDeleted(tr, doc, from, to, insertionType, deletionType, pluginState, joinState) {
86
118
  const ranges = [];
87
119
  doc.nodesBetween(from, to, (node, pos) => {
88
120
  if (!canCarryTrackedRunMark(node)) return;
@@ -98,19 +130,38 @@ function markRangeAsDeleted(tr, doc, from, to, insertionType, deletionType, plug
98
130
  });
99
131
  });
100
132
  const delAttrs = findAdjacentRevisionForRange(doc, from, to, "deletion", pluginState.author) || makeMarkAttrs(pluginState);
133
+ let removedMark = false;
101
134
  doc.nodesBetween(from, to, (node, pos) => {
102
135
  if (node.type.name === "tableRow" && pos >= from && pos + node.nodeSize <= to && node.attrs["trIns"] == null && node.attrs["trDel"] == null) tr.setNodeAttribute(pos, "trDel", delAttrs);
103
136
  if (node.type.name !== "paragraph") return;
104
137
  const boundary = pos + node.nodeSize;
105
- if (boundary > from && boundary <= to && node.attrs["pPrMark"] == null) tr.setNodeAttribute(pos, "pPrMark", {
106
- kind: "del",
107
- info: {
108
- id: delAttrs.revisionId,
109
- author: delAttrs.author,
110
- date: delAttrs.date
111
- }
112
- });
138
+ if (boundary > from && boundary <= to && node.attrs["pPrMark"] == null) {
139
+ tr.setNodeAttribute(pos, "pPrMark", {
140
+ kind: "del",
141
+ info: {
142
+ id: delAttrs.revisionId,
143
+ author: delAttrs.author,
144
+ date: delAttrs.date
145
+ }
146
+ });
147
+ removedMark = true;
148
+ }
113
149
  });
150
+ const $first = doc.resolve(from);
151
+ const $last = doc.resolve(to);
152
+ if (joinState && removedMark && $first.parent.type.name === "paragraph" && $last.parent.type.name === "paragraph" && $first.parent !== $last.parent && from > $first.start()) {
153
+ const lastPos = doc === tr.doc ? $last.before() : tr.mapping.map($last.before());
154
+ const lastMark = tr.doc.nodeAt(lastPos)?.attrs["pPrMark"];
155
+ const survivorPos = lastMark?.kind === "del" || lastMark?.kind === "moveFrom" ? paragraphLeftAfter({
156
+ doc: tr.doc,
157
+ paragraphPos: lastPos
158
+ }) ?? lastPos : lastPos;
159
+ carryIntoParagraphLeft(joinState, tr, $first.parent, survivorPos, {
160
+ id: delAttrs.revisionId,
161
+ author: delAttrs.author,
162
+ date: delAttrs.date
163
+ });
164
+ }
114
165
  if (ranges.length === 0) return;
115
166
  encloseWholeControls({
116
167
  tr,
@@ -124,6 +175,23 @@ function markRangeAsDeleted(tr, doc, from, to, insertionType, deletionType, plug
124
175
  else tr.addMark(range.from, range.to, deletionType.create(delAttrs));
125
176
  }
126
177
  }
178
+ /** Preserve clipboard revisions inside the revision that inserted their content. */
179
+ function enclosePastedRunRevisions({ tr, from, to, revision: insertion }) {
180
+ tr.doc.nodesBetween(from, to, (node, pos) => {
181
+ if (!canCarryTrackedRunMark(node)) return;
182
+ const revision = node.marks.find(({ type }) => type.name === "insertion" || type.name === "deletion");
183
+ if (!revision) return;
184
+ const previous = expectTrackedChangeMarkAttrs(revision);
185
+ tr.addMark(Math.max(from, pos), Math.min(to, pos + node.nodeSize), revision.type.create({
186
+ ...revision.attrs,
187
+ _docxRevisionAncestors: [{
188
+ type: "insertion",
189
+ ...insertion,
190
+ outerWrapperCount: 0
191
+ }, ...previous._docxRevisionAncestors ?? []]
192
+ }));
193
+ });
194
+ }
127
195
  /**
128
196
  * Track every inserted run, paragraph break, and table row in `[from, to)`.
129
197
  * Content already carrying a revision keeps its attribution.
@@ -137,10 +205,13 @@ function markRangeAsInserted(tr, doc, from, to, insertionType, deletionType, att
137
205
  if (start >= end) return;
138
206
  tr.addMark(start, end, insertionType.create(attrs));
139
207
  });
208
+ const $end = doc.resolve(to);
209
+ const endsBetweenBlocks = !$end.parent.isTextblock && $end.nodeBefore !== null && $end.nodeBefore.type.name !== "paragraph";
140
210
  doc.nodesBetween(from, to, (node, pos) => {
141
211
  if (node.type.name === "paragraph") {
142
212
  const boundary = pos + node.nodeSize;
143
- if (boundary > from && boundary < to && node.attrs["pPrMark"] == null) tr.setNodeAttribute(pos, "pPrMark", {
213
+ const keepsOwnMark = endsBetweenBlocks && boundary - 1 === from;
214
+ if (boundary > from && boundary < to && !keepsOwnMark && node.attrs["pPrMark"] == null) tr.setNodeAttribute(pos, "pPrMark", {
144
215
  kind: "ins",
145
216
  info: {
146
217
  id: attrs.revisionId,
@@ -162,32 +233,221 @@ function markRangeAsInserted(tr, doc, from, to, insertionType, deletionType, att
162
233
  * With track changes on, pasting over a non-empty text selection marks the
163
234
  * replaced text as a tracked deletion and the pasted slice as a tracked
164
235
  * insertion (so they read as one replacement), matching typing over a
165
- * selection. Returns false for a collapsed cursor or non-text selection so the
236
+ * selection. Select-all (an `AllSelection`) counts as a text selection over the
237
+ * whole document. Returns false for a collapsed cursor or another selection so the
166
238
  * default paste + the `appendTransaction` catch-all marks a plain insertion
167
239
  * (eigenpal/docx-editor#784).
168
240
  */
169
241
  function handleSuggestionPaste(view, slice, pluginState) {
170
242
  const { selection } = view.state;
171
- if (!(selection instanceof TextSelection) || selection.empty) return false;
243
+ const selectsAll = selection instanceof AllSelection;
244
+ if (!(selection instanceof TextSelection || selectsAll) || selection.empty) return false;
172
245
  const insertionType = view.state.schema.marks["insertion"];
173
246
  const deletionType = view.state.schema.marks["deletion"];
174
247
  if (!insertionType || !deletionType) return false;
175
- const { from, to } = selection;
248
+ if (selectsAll && (slice.openStart > 0 || slice.openEnd > 0)) slice = new Slice(view.state.tr.replaceSelection(slice).doc.content, 0, 0);
249
+ const { from, to } = selectsAll ? {
250
+ from: Selection.atStart(view.state.doc).from,
251
+ to: Selection.atEnd(view.state.doc).to
252
+ } : selection;
176
253
  const tr = view.state.tr;
177
254
  tr.setMeta(SUGGESTION_META, true);
178
- markRangeAsDeleted(tr, view.state.doc, from, to, insertionType, deletionType, pluginState);
179
- const insertFrom = tr.mapping.map(to);
180
- const sizeBefore = tr.doc.content.size;
181
- tr.replaceRange(insertFrom, insertFrom, slice);
182
- const insertTo = insertFrom + (tr.doc.content.size - sizeBefore);
183
- tr.removeMark(insertFrom, insertTo, deletionType);
184
- const insertAttrs = findAdjacentRevision(view.state.doc, from, "insertion", pluginState.author) || makeMarkAttrs(pluginState);
255
+ const { doc } = view.state;
256
+ const $from = doc.resolve(from);
257
+ const $to = doc.resolve(to);
258
+ const closedBlocks = slice.openStart === 0 && slice.content.firstChild?.isBlock === true;
259
+ const beforeStruck = closedBlocks && $from.parent.isTextblock && $from.parentOffset === 0;
260
+ const wholeBlocks = beforeStruck && $to.parent.isTextblock && $to.parentOffset === $to.parent.content.size;
261
+ const endsContainer = wholeBlocks && paragraphEndsItsContainer(doc.resolve($to.before()), $to.parent.type.name);
262
+ const rotates = endsContainer && slice.content.firstChild?.type === $to.parent.type && slice.content.lastChild?.type === $to.parent.type;
263
+ const tableFirst = endsContainer && !rotates;
264
+ if (!tableFirst) markRangeAsDeleted(tr, doc, from, wholeBlocks && !endsContainer ? $to.after() : to, insertionType, deletionType, pluginState);
265
+ const carryAcross = () => {
266
+ if (selectsAll || $from.parent.type.name !== "paragraph" || $from.parent === $to.parent || from <= $from.start()) return;
267
+ const firstPos = tr.mapping.map($from.before());
268
+ const mark = tr.doc.nodeAt(firstPos)?.attrs["pPrMark"];
269
+ const survivorPos = paragraphLeftAfter({
270
+ doc: tr.doc,
271
+ paragraphPos: firstPos
272
+ });
273
+ if (mark?.kind === "del" && survivorPos !== null) carryIntoParagraphLeft(view.state, tr, $from.parent, survivorPos, mark.info);
274
+ };
275
+ const inlineSlice = slice.content.firstChild?.isInline === true;
276
+ if (inlineSlice) carryAcross();
277
+ let at = tr.mapping.map(to);
278
+ if (rotates) at = tr.mapping.map($to.after());
279
+ else if (tableFirst) at = $from.before();
280
+ else if (beforeStruck) at = tr.mapping.map(from, -1);
281
+ const firstStep = tr.steps.length;
282
+ tr.replaceRange(at, at, slice);
283
+ const placed = tr.steps.slice(firstStep).flatMap((step, offset) => {
284
+ const later = new Mapping(tr.mapping.maps.slice(firstStep + offset + 1));
285
+ const ranges = [];
286
+ step.getMap().forEach((_oldFrom, _oldTo, newFrom, newTo) => {
287
+ ranges.push({
288
+ from: later.map(newFrom, -1),
289
+ to: later.map(newTo, 1)
290
+ });
291
+ });
292
+ return ranges;
293
+ });
294
+ let insertFrom = placed.length === 0 ? at : Math.min(...placed.map((range) => range.from));
295
+ let insertTo = placed.length === 0 ? at : Math.max(...placed.map((range) => range.to));
296
+ if (tableFirst) {
297
+ const struck = tr.steps.length;
298
+ markRangeAsDeleted(tr, tr.doc, tr.mapping.map(from), tr.mapping.map(to), insertionType, deletionType, pluginState);
299
+ const since = tr.mapping.slice(struck);
300
+ insertFrom = since.map(insertFrom, -1);
301
+ insertTo = since.map(insertTo, -1);
302
+ }
303
+ const insertAttrs = findAdjacentRevision(doc, from, "insertion", pluginState.author) || makeMarkAttrs(pluginState);
304
+ enclosePastedRunRevisions({
305
+ tr,
306
+ from: insertFrom,
307
+ to: insertTo,
308
+ revision: insertAttrs
309
+ });
185
310
  markRangeAsInserted(tr, tr.doc, insertFrom, insertTo, insertionType, deletionType, insertAttrs);
311
+ if (!inlineSlice) carryAcross();
312
+ if (!closedBlocks && $to.parent.type.name === "paragraph") {
313
+ const $end = tr.doc.resolve(tr.mapping.map(to));
314
+ if ($end.parent.type.name === "paragraph" && $end.before() !== tr.mapping.map($from.before())) recordReplacedParagraphProperties({
315
+ tr,
316
+ position: $end.before(),
317
+ replaced: $to.parent,
318
+ revision: {
319
+ id: insertAttrs.revisionId,
320
+ author: insertAttrs.author,
321
+ date: insertAttrs.date
322
+ }
323
+ });
324
+ }
325
+ const firstPasted = slice.content.firstChild;
326
+ if (selectsAll && !closedBlocks && firstPasted?.type.name === "paragraph") {
327
+ const $first = tr.doc.resolve(insertFrom);
328
+ if ($first.parent.type.name === "paragraph") carryParagraphProperties({
329
+ tr,
330
+ position: $first.before(),
331
+ source: firstPasted,
332
+ revision: {
333
+ id: insertAttrs.revisionId,
334
+ author: insertAttrs.author,
335
+ date: insertAttrs.date
336
+ },
337
+ styleResolver: getDocumentStyleResolver(view.state),
338
+ numbering: getDocumentNumbering(view.state)
339
+ });
340
+ }
341
+ if (rotates) {
342
+ const rotation = tr.steps.length;
343
+ const joined = rotateIntoFinalParagraph(view.state, tr, tr.mapping.map($to.before()), insertAttrs);
344
+ insertFrom = Math.min(tr.mapping.slice(rotation).map(insertFrom, -1), joined);
345
+ insertTo = tr.mapping.slice(rotation).map(insertTo);
346
+ }
186
347
  tr.setSelection(TextSelection.near(tr.doc.resolve(insertTo)));
187
348
  view.dispatch(tr.scrollIntoView());
188
349
  return true;
189
350
  }
190
351
  /**
352
+ * With track changes on, a paste into a table's cells (a cell selection, or a
353
+ * block of copied cells) replaces each cell's content the way a paste over a
354
+ * text selection does: the old content struck through, the pasted content
355
+ * after it as an insertion. When the pasted content is whole paragraphs, the
356
+ * last one ends the cell, so its paragraph mark is tracked as added too:
357
+ * rejecting then drops it and accepting joins the struck paragraphs into it.
358
+ */
359
+ function handleSuggestionTableCellPaste(view, slice, pluginState) {
360
+ const insertionType = view.state.schema.marks["insertion"];
361
+ const deletionType = view.state.schema.marks["deletion"];
362
+ if (!insertionType || !deletionType) return false;
363
+ const revision = makeMarkAttrs(pluginState);
364
+ return pasteTableCells(view.state, slice, (tr) => view.dispatch(tr.setMeta(SUGGESTION_META, true)), {
365
+ revision,
366
+ replaceCellContent: (tr, cellPos, content) => {
367
+ const cell = tr.doc.nodeAt(cellPos);
368
+ if (!cell) return;
369
+ const { from, to } = cellPasteRange(cell, cellPos, content);
370
+ const mapFrom = tr.mapping.maps.length;
371
+ markRangeAsDeleted(tr, tr.doc, from, to, insertionType, deletionType, pluginState);
372
+ const insertFrom = tr.mapping.slice(mapFrom).map(to);
373
+ const sizeBefore = tr.doc.content.size;
374
+ tr.replaceRange(insertFrom, insertFrom, content);
375
+ const insertTo = insertFrom + (tr.doc.content.size - sizeBefore);
376
+ enclosePastedRunRevisions({
377
+ tr,
378
+ from: insertFrom,
379
+ to: insertTo,
380
+ revision
381
+ });
382
+ markRangeAsInserted(tr, tr.doc, insertFrom, insertTo, insertionType, deletionType, revision);
383
+ tr.doc.nodesBetween(insertFrom, insertTo, (node, pos) => {
384
+ if (node.type.name === "paragraph" && pos >= insertFrom && pos + node.nodeSize <= insertTo && node.attrs["pPrMark"] == null) tr.setNodeAttribute(pos, "pPrMark", {
385
+ kind: "ins",
386
+ info: {
387
+ id: revision.revisionId,
388
+ author: revision.author,
389
+ date: revision.date
390
+ }
391
+ });
392
+ });
393
+ }
394
+ });
395
+ }
396
+ /**
397
+ * Pasted paragraphs replaced a container's last paragraph. They were placed
398
+ * after it, and the paragraph was struck but keeps its mark, as a container's
399
+ * last paragraph must. Join the first pasted paragraph onto it, so the struck
400
+ * text and the first paste share one paragraph whose break is the inserted one,
401
+ * and record the replaced paragraph's formatting on the paragraph that now ends
402
+ * the container: accepting leaves only the paste, and rejecting closes every
403
+ * pasted break back into one paragraph that returns to its old formatting.
404
+ * Returns the position of the joined paragraph.
405
+ */
406
+ function rotateIntoFinalParagraph(state, tr, replacedPos, attrs) {
407
+ const replaced = tr.doc.nodeAt(replacedPos);
408
+ const first = replaced ? tr.doc.nodeAt(replacedPos + replaced.nodeSize) : null;
409
+ if (!replaced || !first) return replacedPos;
410
+ const replacedAttrs = expectParagraphAttrs(replaced);
411
+ const previousFormatting = {
412
+ ...paragraphPropertiesSnapshot(replaced),
413
+ numPr: replacedAttrs.numPr ?? null
414
+ };
415
+ const { _sectionProperties: sectionProperties } = replaced.attrs;
416
+ tr.join(replacedPos + replaced.nodeSize);
417
+ tr.setNodeMarkup(replacedPos, void 0, first.attrs);
418
+ const styleResolver = getDocumentStyleResolver(state);
419
+ if (styleResolver && replaced.content.size > 0) rebaseParagraphRuns({
420
+ previousContext: paragraphRunStyleContext(replaced, styleResolver),
421
+ paragraphPosition: replacedPos,
422
+ range: {
423
+ from: 0,
424
+ to: replaced.content.size
425
+ },
426
+ styleResolver,
427
+ tr
428
+ });
429
+ const $joined = tr.doc.resolve(replacedPos);
430
+ const container = $joined.parent;
431
+ let finalPos = replacedPos;
432
+ for (let index = $joined.index(); index < container.childCount - 1; index += 1) finalPos += container.child(index).nodeSize;
433
+ const final = tr.doc.nodeAt(finalPos);
434
+ if (final) tr.setNodeMarkup(finalPos, void 0, {
435
+ ...final.attrs,
436
+ pPrMark: null,
437
+ ...sectionProperties == null ? {} : { _sectionProperties: sectionProperties },
438
+ _propertyChanges: [{
439
+ type: "paragraphPropertyChange",
440
+ info: {
441
+ id: attrs.revisionId,
442
+ author: attrs.author,
443
+ date: attrs.date
444
+ },
445
+ previousFormatting
446
+ }]
447
+ });
448
+ return replacedPos;
449
+ }
450
+ /**
191
451
  * Insert text as a tracked insertion, optionally marking replaced selection as deletion.
192
452
  */
193
453
  function applySuggestionInsert(view, from, to, text, pluginState) {
@@ -196,20 +456,65 @@ function applySuggestionInsert(view, from, to, text, pluginState) {
196
456
  const tr = view.state.tr;
197
457
  tr.setMeta(SUGGESTION_META, true);
198
458
  tr.setMeta(SUGGESTED_TEXT_INPUT_META, text);
459
+ const selectsAll = view.state.selection instanceof AllSelection;
460
+ if (selectsAll) {
461
+ from = Math.max(from, Selection.atStart(view.state.doc).from);
462
+ to = Math.min(to, Selection.atEnd(view.state.doc).to);
463
+ }
464
+ const replacedFormatting = selectsAll ? (view.state.doc.resolve(from).nodeAfter?.marks ?? []).filter(({ type }) => RUN_FORMATTING_MARK_NAMES.has(type.name)) : null;
199
465
  const insertAttrs = findAdjacentRevision(view.state.doc, from, "insertion", pluginState.author) || makeMarkAttrs(pluginState);
200
466
  if (from !== to) {
201
467
  const deletionType = view.state.schema.marks["deletion"];
202
- if (deletionType) markRangeAsDeleted(tr, view.state.doc, from, to, insertionType, deletionType, pluginState);
468
+ if (deletionType) markRangeAsDeleted(tr, view.state.doc, from, to, insertionType, deletionType, pluginState, view.state);
203
469
  }
204
470
  const insertAt = tr.mapping.map(to);
205
471
  tr.insertText(text, insertAt, insertAt);
206
472
  const deletionType = view.state.schema.marks["deletion"];
207
473
  if (deletionType) tr.removeMark(insertAt, insertAt + text.length, deletionType);
208
474
  tr.addMark(insertAt, insertAt + text.length, insertionType.create(insertAttrs));
475
+ if (replacedFormatting) {
476
+ for (const mark of tr.doc.resolve(insertAt).nodeAfter?.marks ?? []) if (RUN_FORMATTING_MARK_NAMES.has(mark.type.name)) tr.removeMark(insertAt, insertAt + text.length, mark.type);
477
+ for (const mark of replacedFormatting) tr.addMark(insertAt, insertAt + text.length, mark);
478
+ }
479
+ separateSplitStretch(tr, insertAt, insertAt + text.length, insertAttrs.revisionId);
480
+ if (view.state.selection instanceof AllSelection) tr.setSelection(TextSelection.create(tr.doc, insertAt + text.length));
209
481
  view.dispatch(tr.scrollIntoView());
210
482
  return true;
211
483
  }
212
484
  /**
485
+ * Text typed inside another revision leaves that revision in two stretches.
486
+ * A saved package writes each stretch as a wrapper of its own with its own
487
+ * `w:id`, so the stretch after the typed text gets a fresh id now: the editor
488
+ * lists, resolves and saves the same changes a reopened package does.
489
+ */
490
+ function separateSplitStretch(tr, from, to, typedId) {
491
+ const $from = tr.doc.resolve(from);
492
+ const $to = tr.doc.resolve(to);
493
+ const before = $from.nodeBefore;
494
+ if (!before || $to.parent !== $from.parent) return;
495
+ const parent = $to.parent;
496
+ const start = $to.start();
497
+ for (const mark of before.marks) {
498
+ if (mark.type.name !== "insertion" && mark.type.name !== "deletion") continue;
499
+ const revisionId = mark.attrs["revisionId"];
500
+ if (revisionId === typedId || typeof revisionId !== "number") continue;
501
+ let end = to;
502
+ let offset = to - start;
503
+ while (offset < parent.content.size) {
504
+ const child = parent.childAfter(offset).node;
505
+ if (!child?.marks.some((candidate) => candidate.eq(mark))) break;
506
+ offset += child.nodeSize;
507
+ end = start + offset;
508
+ }
509
+ if (end === to) continue;
510
+ tr.removeMark(to, end, mark);
511
+ tr.addMark(to, end, mark.type.create({
512
+ ...mark.attrs,
513
+ revisionId: mintRevisionId()
514
+ }));
515
+ }
516
+ }
517
+ /**
213
518
  * Remove `from`..`to` of `tr.doc` as a suggestion: text the current author
214
519
  * inserted is retracted, anything else is marked deleted. `false` when the
215
520
  * editor is not suggesting.
@@ -248,6 +553,13 @@ function handleSuggestionEnter(view, pluginState) {
248
553
  info: makeParagraphMarkInfo(pluginState)
249
554
  };
250
555
  tr.setNodeAttribute(sourcePos, "pPrMark", markInfo);
556
+ const $caret = tr.selection.$from;
557
+ if ($caret.parent.type.name === "paragraph" && $caret.before() !== sourcePos) recordReplacedParagraphProperties({
558
+ tr,
559
+ position: $caret.before(),
560
+ replaced: $from.parent,
561
+ revision: markInfo.info
562
+ });
251
563
  }
252
564
  }
253
565
  view.dispatch(tr.scrollIntoView());
@@ -301,9 +613,22 @@ function applyPPrDel(view, targetParagraphPos, pluginState) {
301
613
  const tr = view.state.tr;
302
614
  tr.setMeta(SUGGESTION_META, true);
303
615
  const joinPos = targetParagraphPos + targetNode.nodeSize;
616
+ const joined = view.state.doc.nodeAt(joinPos);
304
617
  try {
305
618
  tr.join(joinPos);
306
619
  tr.setNodeAttribute(targetParagraphPos, "pPrMark", null);
620
+ const styleResolver = getDocumentStyleResolver(view.state);
621
+ if (styleResolver && joined?.type === targetNode.type && joined.content.size > 0) rebaseParagraphRuns({
622
+ previousContext: paragraphRunStyleContext(joined, styleResolver),
623
+ paragraphPosition: targetParagraphPos,
624
+ range: {
625
+ from: targetNode.content.size,
626
+ to: targetNode.content.size + joined.content.size
627
+ },
628
+ styleResolver,
629
+ tr
630
+ });
631
+ tr.setMeta(JOINED_RUNS_RESTYLED_META, true);
307
632
  view.dispatch(tr.scrollIntoView());
308
633
  } catch {
309
634
  return true;
@@ -318,6 +643,10 @@ function applyPPrDel(view, targetParagraphPos, pluginState) {
318
643
  info: makeParagraphMarkInfo(pluginState)
319
644
  };
320
645
  tr.setNodeAttribute(targetParagraphPos, "pPrMark", markInfo);
646
+ carryIntoParagraphLeft(view.state, tr, targetNode, paragraphLeftAfter({
647
+ doc: tr.doc,
648
+ paragraphPos: targetParagraphPos
649
+ }) ?? targetParagraphPos + targetNode.nodeSize, markInfo.info);
321
650
  view.dispatch(tr.scrollIntoView());
322
651
  return true;
323
652
  }
@@ -345,7 +674,7 @@ function handleSuggestionDelete(state, dispatch, direction) {
345
674
  from: rangeFrom.pos,
346
675
  to: rangeTo.pos
347
676
  }).sort((a, b) => b.from - a.from);
348
- for (const { from, to } of ranges) markRangeAsDeleted(tr, state.doc, from, to, insertionType, deletionType, pluginState);
677
+ for (const { from, to } of ranges) markRangeAsDeleted(tr, state.doc, from, to, insertionType, deletionType, pluginState, state);
349
678
  const cursorPos = tr.mapping.map($to.pos);
350
679
  tr.setSelection(TextSelection.near(tr.doc.resolve(cursorPos)));
351
680
  dispatch(tr.scrollIntoView());
@@ -414,6 +743,33 @@ function markComposedAsInsertion(view, from, pluginState, replaced) {
414
743
  view.dispatch(tr);
415
744
  }
416
745
  /**
746
+ * Paragraphs inserted into one (a paste at the caret) split it: its break ends
747
+ * the last part, which reads as the last inserted paragraph. Rejecting the
748
+ * inserted breaks leaves that part, so it records what the paragraph read as.
749
+ */
750
+ function recordSplitParagraph(tr, before, step, insertedFrom, insertedTo, attrs) {
751
+ if (!(step instanceof ReplaceStep) || !before) return;
752
+ const $at = before.resolve(step.from);
753
+ if ($at.parent.type.name !== "paragraph" || step.to > $at.after() || before.textBetween(step.from, step.to) !== "") return;
754
+ const split = $at.parent;
755
+ const $first = tr.doc.resolve(insertedFrom);
756
+ const $last = tr.doc.resolve(insertedTo);
757
+ let lastPos = null;
758
+ if ($last.parent.type.name === "paragraph") lastPos = $last.before();
759
+ else if ($last.nodeBefore?.type.name === "paragraph") lastPos = insertedTo - $last.nodeBefore.nodeSize;
760
+ if ($first.parent.type.name !== "paragraph" || lastPos === null || lastPos === $first.before()) return;
761
+ recordReplacedParagraphProperties({
762
+ tr,
763
+ position: lastPos,
764
+ replaced: split,
765
+ revision: {
766
+ id: attrs.revisionId,
767
+ author: attrs.author,
768
+ date: attrs.date
769
+ }
770
+ });
771
+ }
772
+ /**
417
773
  * Create the suggestion mode plugin.
418
774
  * When active, text edits become tracked changes.
419
775
  */
@@ -490,7 +846,7 @@ function createSuggestionModePlugin(initialActive = false, author = "User") {
490
846
  handleKeyDown(view, event) {
491
847
  const pluginState = suggestionModeKey.getState(view.state);
492
848
  if (!pluginState?.active) return false;
493
- if (event.key === "Enter") {
849
+ if (event.key === "Enter" && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
494
850
  const { $from, empty } = view.state.selection;
495
851
  if (empty && $from.parent.type.name === "paragraph" && expectParagraphAttrs($from.parent).numPr?.kind === "reference") return false;
496
852
  return handleSuggestionEnter(view, pluginState);
@@ -515,7 +871,7 @@ function createSuggestionModePlugin(initialActive = false, author = "User") {
515
871
  handlePaste(view, _event, slice) {
516
872
  const pluginState = suggestionModeKey.getState(view.state);
517
873
  if (!pluginState?.active) return false;
518
- return handleSuggestionPaste(view, slice, pluginState);
874
+ return handleSuggestionTableCellPaste(view, slice, pluginState) || handleSuggestionPaste(view, slice, pluginState);
519
875
  }
520
876
  },
521
877
  appendTransaction(transactions, _oldState, newState) {
@@ -538,7 +894,14 @@ function createSuggestionModePlugin(initialActive = false, author = "User") {
538
894
  const newFrom = following.map(stepFrom, -1);
539
895
  const newTo = following.map(stepTo, 1);
540
896
  if (newTo > newFrom) {
897
+ if (userTr.getMeta("paste")) enclosePastedRunRevisions({
898
+ tr,
899
+ from: newFrom,
900
+ to: newTo,
901
+ revision: markAttrs
902
+ });
541
903
  if (deletionType) markRangeAsInserted(tr, newState.doc, newFrom, newTo, insertionType, deletionType, markAttrs);
904
+ recordSplitParagraph(tr, userTr.docs[stepIndex], step, newFrom, newTo, markAttrs);
542
905
  }
543
906
  });
544
907
  }
@@ -573,4 +936,4 @@ function isSuggestionModeActive(state) {
573
936
  return suggestionModeKey.getState(state)?.active ?? false;
574
937
  }
575
938
  //#endregion
576
- export { SUGGESTED_TEXT_INPUT_META, SUGGESTION_BYPASS_META, SUGGESTION_META, createSuggestionModePlugin, deleteSelectionAsSuggestion, handleSuggestionEnter, handleSuggestionPaste, isSuggestionModeActive, makeRevisionInfo, paragraphBoundaryTarget, setSuggestionMode, suggestRangeDeletion, suggestionModeKey, toggleSuggestionMode };
939
+ export { SUGGESTED_TEXT_INPUT_META, SUGGESTION_BYPASS_META, SUGGESTION_META, createSuggestionModePlugin, deleteSelectionAsSuggestion, handleSuggestionEnter, handleSuggestionPaste, handleSuggestionTableCellPaste, isSuggestionModeActive, makeRevisionInfo, paragraphBoundaryTarget, setSuggestionMode, suggestRangeDeletion, suggestionModeKey, toggleSuggestionMode };
@@ -1,6 +1,7 @@
1
- import { RunStyleResolver } from "./runStyleFormatting.js";
1
+ import { ParagraphRunStyleContext, RunStyleResolver } from "./runStyleFormatting.js";
2
2
  import { Node } from "prosemirror-model";
3
3
  import { Transaction } from "prosemirror-state";
4
+ import { Transform } from "prosemirror-transform";
4
5
  //#region src/prosemirror/rebaseParagraphRunFormatting.d.ts
5
6
  type RebaseParagraphRunFormattingOptions = {
6
7
  nextAttrs: Record<string, unknown>;
@@ -9,10 +10,29 @@ type RebaseParagraphRunFormattingOptions = {
9
10
  styleResolver: RunStyleResolver;
10
11
  tr: Transaction;
11
12
  };
13
+ type RebaseParagraphRunsOptions<T extends Transform> = {
14
+ /** The style context the runs' marks were resolved in. */
15
+ previousContext: ParagraphRunStyleContext;
16
+ paragraphPosition: number;
17
+ /** Content offsets inside the paragraph to rebase; all of it when omitted. */
18
+ range?: {
19
+ from: number;
20
+ to: number;
21
+ };
22
+ shouldRebase?: (node: Node) => boolean;
23
+ styleResolver: RunStyleResolver;
24
+ tr: T;
25
+ };
26
+ /**
27
+ * Re-resolve the inherited marks of runs resolved in `previousContext` in the
28
+ * paragraph's current style context: direct formatting stays direct, and what
29
+ * the old context lent the runs goes with it.
30
+ */
31
+ declare const rebaseParagraphRuns: <T extends Transform>({ previousContext, paragraphPosition, range, shouldRebase, styleResolver, tr }: RebaseParagraphRunsOptions<T>) => T;
12
32
  /**
13
33
  * Change paragraph attrs and re-resolve inherited run marks in the new style
14
34
  * context without turning the old rendered style into direct run formatting.
15
35
  */
16
36
  declare const setParagraphAttrsWithRebasedRunFormatting: ({ nextAttrs, paragraphPosition, shouldRebase, styleResolver, tr }: RebaseParagraphRunFormattingOptions) => Transaction;
17
37
  //#endregion
18
- export { setParagraphAttrsWithRebasedRunFormatting };
38
+ export { rebaseParagraphRuns, setParagraphAttrsWithRebasedRunFormatting };