@stll/folio-core 0.35.0 → 0.36.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 (80) hide show
  1. package/dist/ai-edits/apply.js +370 -118
  2. package/dist/ai-edits/headless.js +16 -10
  3. package/dist/ai-edits/index.d.ts +2 -2
  4. package/dist/ai-edits/index.js +2 -2
  5. package/dist/ai-edits/snapshot.js +6 -1
  6. package/dist/ai-edits/types.d.ts +19 -6
  7. package/dist/ai-edits/word-diff.d.ts +18 -3
  8. package/dist/ai-edits/word-diff.js +556 -67
  9. package/dist/compare/compare.js +7 -16
  10. package/dist/compare/plan.js +8 -5
  11. package/dist/compare/verification.d.ts +8 -9
  12. package/dist/compare/verification.js +51 -46
  13. package/dist/compat/eigenpal.d.ts +2 -2
  14. package/dist/compat/eigenpal.js +2 -2
  15. package/dist/controller/headerFooterEditorManager.js +5 -5
  16. package/dist/controller/hiddenEditorApi.js +1 -1
  17. package/dist/controller/noteEditorManager.js +2 -2
  18. package/dist/document-operations.d.ts +5 -3
  19. package/dist/document-operations.js +32 -5
  20. package/dist/docx/commentParser.js +2 -4
  21. package/dist/docx/commentRangeIntegrity.js +2 -4
  22. package/dist/docx/documentClone.d.ts +2 -0
  23. package/dist/docx/documentClone.js +2 -0
  24. package/dist/docx/headerFooterParser.js +2 -4
  25. package/dist/docx/normalizeBaseDirection.js +5 -7
  26. package/dist/docx/paragraphParser.js +27 -29
  27. package/dist/docx/paragraphPropertySource.d.ts +49 -0
  28. package/dist/docx/paragraphPropertySource.js +113 -0
  29. package/dist/docx/runConsolidator.js +3 -4
  30. package/dist/docx/runParser.js +2 -15
  31. package/dist/docx/sectionParser.js +1 -15
  32. package/dist/docx/serializer/paragraphSerializer.js +287 -30
  33. package/dist/docx/serializer/runSerializer.js +2 -10
  34. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -10
  35. package/dist/docx/serializer/tableSerializer.js +4 -23
  36. package/dist/docx/serializer/trackedChangeAttributes.d.ts +9 -0
  37. package/dist/docx/serializer/trackedChangeAttributes.js +21 -0
  38. package/dist/docx/server/createBilingualDocument.js +8 -10
  39. package/dist/docx/server/evaluateDocxXmlPatchProposal.js +22 -13
  40. package/dist/docx/server/validateDocxConformance.js +17 -13
  41. package/dist/docx/tableParser.js +1 -37
  42. package/dist/docx/trackedChangeInfo.d.ts +11 -0
  43. package/dist/docx/trackedChangeInfo.js +38 -0
  44. package/dist/docx/verbatimCapture.d.ts +20 -9
  45. package/dist/docx/verbatimCapture.js +208 -8
  46. package/dist/docx/xmlParser.d.ts +3 -1
  47. package/dist/docx/xmlParser.js +6 -1
  48. package/dist/docx/xmlSafety.d.ts +1 -1
  49. package/dist/docx/xmlSafety.js +157 -0
  50. package/dist/index.d.ts +2 -2
  51. package/dist/index.js +2 -2
  52. package/dist/layout-bridge/convert/headerFooterLayout.js +4 -4
  53. package/dist/layout-bridge/convert/toFlowBlocks.js +12 -7
  54. package/dist/markdown/renderBlock.js +7 -10
  55. package/dist/prosemirror/attrs/index.js +15 -0
  56. package/dist/prosemirror/commands/comments.js +365 -32
  57. package/dist/prosemirror/commands/pageBreak.js +1 -1
  58. package/dist/prosemirror/commands/propertyChangeScope.d.ts +26 -4
  59. package/dist/prosemirror/commands/propertyChangeScope.js +65 -3
  60. package/dist/prosemirror/conversion/fromProseDoc.d.ts +2 -4
  61. package/dist/prosemirror/conversion/fromProseDoc.js +130 -18
  62. package/dist/prosemirror/conversion/toProseDoc.js +27 -12
  63. package/dist/prosemirror/extensions/core/ParagraphExtension.js +48 -4
  64. package/dist/prosemirror/extensions/features/AutoBidiDetectionExtension.js +7 -1
  65. package/dist/prosemirror/extensions/features/ParaIdAllocatorExtension.js +20 -7
  66. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +2 -0
  67. package/dist/prosemirror/paragraphAlignment.d.ts +12 -0
  68. package/dist/prosemirror/paragraphAlignment.js +13 -0
  69. package/dist/prosemirror/plugins/suggestionMode.js +1 -1
  70. package/dist/prosemirror/schema/marks.d.ts +2 -0
  71. package/dist/prosemirror/schema/nodes.d.ts +12 -1
  72. package/dist/prosemirror/styles/resolvedStyleAttrs.js +1 -0
  73. package/dist/redline.js +4 -1
  74. package/dist/server.d.ts +2 -2
  75. package/dist/server.js +2 -2
  76. package/dist/utils/mergeDocumentContent.js +3 -3
  77. package/dist/utils/replaceText.js +2 -1
  78. package/dist/version-comparison.js +6 -4
  79. package/dist/watermark/index.js +2 -4
  80. package/package.json +7 -2
@@ -1,12 +1,13 @@
1
1
  import { expectParagraphAttrs, expectRunPropertyChangeMarkAttrs } from "../attrs/index.js";
2
- import { paragraphEndsItsContainer } from "../containerFinalParagraph.js";
2
+ import { addedBreakCarrierBefore, finalParagraphsOf, paragraphEndsItsContainer } from "../containerFinalParagraph.js";
3
3
  import { textFormattingToMarks } from "../conversion/toProseDoc.js";
4
4
  import { markStructuralChange } from "../extensions/features/ParagraphChangeTrackerExtension.js";
5
+ import { getDocumentStyleResolver } from "../plugins/documentStyles.js";
5
6
  import { getFolioNodeRevisionCarriers } from "../revisionCarriers.js";
6
7
  import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
7
8
  import { getTableCellMergeChange } from "../tableCellMergeRevision.js";
8
9
  import { holdsNoContent } from "../zeroWidthAnchors.js";
9
- import { paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, sectionRejectProperties, tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch } from "./propertyChangeScope.js";
10
+ import { paragraphPropertiesSnapshot, paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, removeParagraphPropertyChanges, sectionRejectProperties, tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch } from "./propertyChangeScope.js";
10
11
  import { hasMatchingCollapsedTableCellMerge, resolveCollapsedTableCellMerge, resolveVisibleTableCellMerge } from "./tableCellMergeResolution.js";
11
12
  import { TableMap, removeRow } from "prosemirror-tables";
12
13
  import { PARAGRAPH_MARK_CHANGE_KINDS } from "@stll/docx-core/model";
@@ -59,6 +60,7 @@ function resolveChange(from, to, mode, revisionIds) {
59
60
  return (state, dispatch) => {
60
61
  const insertionType = state.schema.marks["insertion"];
61
62
  const deletionType = state.schema.marks["deletion"];
63
+ const styleResolver = getDocumentStyleResolver(state);
62
64
  const keepType = mode === "accept" ? insertionType : deletionType;
63
65
  const removeType = mode === "accept" ? deletionType : insertionType;
64
66
  const revisionSet = revisionIds === void 0 ? null : new Set(revisionIds);
@@ -78,16 +80,21 @@ function resolveChange(from, to, mode, revisionIds) {
78
80
  let nextAttrs = null;
79
81
  const propertyChanges = expectParagraphAttrs(node)._propertyChanges;
80
82
  if (Array.isArray(propertyChanges) && propertyChanges.length > 0 && boundaryCovered) {
81
- const matches = propertyChanges.filter((c) => revisionSet === null || c.info && revisionSet.has(c.info.id));
82
- if (matches.length > 0) {
83
- const remaining = propertyChanges.filter((c) => revisionSet !== null && (!c.info || !revisionSet.has(c.info.id)));
83
+ const matchesPropertyChange = (change) => revisionSet === null || revisionSet.has(change.info.id);
84
+ if (propertyChanges.some(matchesPropertyChange)) {
85
+ const rejection = mode === "reject" ? removeParagraphPropertyChanges(propertyChanges, matchesPropertyChange) : null;
86
+ let remaining;
87
+ if (rejection === null) remaining = propertyChanges.filter((change) => !matchesPropertyChange(change));
88
+ else if (rejection.type === "unchanged") remaining = propertyChanges;
89
+ else remaining = rejection.remaining;
84
90
  nextAttrs = {
85
91
  ...node.attrs,
86
92
  _propertyChanges: remaining.length > 0 ? remaining : null
87
93
  };
88
- if (mode === "reject") for (const change of matches.toReversed()) {
89
- Object.assign(nextAttrs, paragraphRejectAttrPatch(change.previousFormatting));
90
- nextAttrs["_originalFormatting"] = paragraphRejectOriginalFormatting(change.previousFormatting, node.attrs["_originalFormatting"]);
94
+ if (rejection?.type === "restore-previous") {
95
+ const previousAlignmentFromStyle = styleResolver ? styleResolver.resolveParagraphStyle(rejection.previousFormatting?.styleId).paragraphFormatting?.alignment : expectParagraphAttrs(node).alignmentFromStyle;
96
+ Object.assign(nextAttrs, paragraphRejectAttrPatch(rejection.previousFormatting, previousAlignmentFromStyle));
97
+ nextAttrs["_originalFormatting"] = paragraphRejectOriginalFormatting(rejection.previousFormatting, node.attrs["_originalFormatting"]);
91
98
  }
92
99
  }
93
100
  }
@@ -480,6 +487,7 @@ function isPPrMarkAttr(value) {
480
487
  * the two ends belong together, not because they resolve differently.
481
488
  */
482
489
  const paragraphMarkWasAdded = (kind) => kind === "ins" || kind === "moveTo";
490
+ const isAddedPPrMarkAttr = (value) => isPPrMarkAttr(value) && (value.kind === "ins" || value.kind === "moveTo");
483
491
  /**
484
492
  * Whether the paragraph's mark carries a section's properties.
485
493
  *
@@ -682,6 +690,13 @@ const readSuggestedMarker = (marker) => {
682
690
  revisionId
683
691
  };
684
692
  };
693
+ const readSuggestedParagraphPropertyChange = (change) => {
694
+ if (change.info.provenance !== "suggested" || typeof change.info.suggestionId !== "string") return null;
695
+ return {
696
+ suggestionId: change.info.suggestionId,
697
+ revisionId: change.info.id
698
+ };
699
+ };
685
700
  const readStructuralSuggestion = (node) => {
686
701
  const attrs = node.attrs;
687
702
  const name = node.type.name;
@@ -754,6 +769,20 @@ const collectSuggestions = (state) => {
754
769
  entry.kinds.add(structural.kind);
755
770
  entry.revisionIds.add(structural.revisionId);
756
771
  }
772
+ if (node.type.name === "paragraph") {
773
+ const propertyChanges = expectParagraphAttrs(node)._propertyChanges;
774
+ if (Array.isArray(propertyChanges)) for (const change of propertyChanges) {
775
+ const suggested = readSuggestedParagraphPropertyChange(change);
776
+ if (!suggested) continue;
777
+ const entry = entryFor(suggested.suggestionId);
778
+ entry.segments.push({
779
+ from: pos,
780
+ to: pos + node.nodeSize
781
+ });
782
+ entry.kinds.add("formatting");
783
+ entry.revisionIds.add(suggested.revisionId);
784
+ }
785
+ }
757
786
  if (!node.isInline) return;
758
787
  for (const mark of node.marks) {
759
788
  if (!isSuggestionMark(mark)) continue;
@@ -848,6 +877,22 @@ const normalizeAcceptDate = (date) => {
848
877
  const parsed = new Date(date);
849
878
  return Number.isNaN(parsed.getTime()) ? (/* @__PURE__ */ new Date()).toISOString() : parsed.toISOString();
850
879
  };
880
+ const acceptedParagraphMark = (node, author, date) => {
881
+ if (node.type.name !== "paragraph") return null;
882
+ const marker = node.attrs["_suggestedInsert"];
883
+ if (typeof marker !== "object" || marker === null || !("revisionId" in marker)) return null;
884
+ if (typeof marker.revisionId !== "number") return null;
885
+ const initials = "initials" in marker ? marker.initials : void 0;
886
+ return {
887
+ kind: "ins",
888
+ info: {
889
+ id: marker.revisionId,
890
+ author,
891
+ date,
892
+ ...typeof initials === "string" ? { initials } : {}
893
+ }
894
+ };
895
+ };
851
896
  /**
852
897
  * The node-attr patch that converts a suggested block/table revision into a
853
898
  * normal (`"user"`) one authored by the accepting user. Returns `null` when the
@@ -868,23 +913,15 @@ const convertStructuralSuggestionAttrs = (node, author, date) => {
868
913
  const name = node.type.name;
869
914
  const insertMarker = attrs["_suggestedInsert"];
870
915
  if ((name === "paragraph" || name === "table") && typeof insertMarker === "object" && insertMarker !== null) {
871
- const marker = insertMarker;
872
916
  if (name === "table") return {
873
917
  ...attrs,
874
918
  _suggestedInsert: null
875
919
  };
876
- if (typeof marker.revisionId === "number") return {
920
+ const paragraphMark = acceptedParagraphMark(node, author, date);
921
+ if (paragraphMark) return {
877
922
  ...attrs,
878
923
  _suggestedInsert: null,
879
- pPrMark: {
880
- kind: "ins",
881
- info: {
882
- id: marker.revisionId,
883
- author,
884
- date,
885
- ...typeof marker.initials === "string" ? { initials: marker.initials } : {}
886
- }
887
- }
924
+ pPrMark: attrs["pPrMark"] ?? paragraphMark
888
925
  };
889
926
  }
890
927
  if (node.type.name === "tableRow") {
@@ -909,6 +946,247 @@ const convertStructuralSuggestionAttrs = (node, author, date) => {
909
946
  }
910
947
  return null;
911
948
  };
949
+ const reauthorParagraphPropertySuggestions = ({ node, matchesSuggestion, author, date }) => {
950
+ if (node.type.name !== "paragraph") return null;
951
+ const propertyChanges = expectParagraphAttrs(node)._propertyChanges;
952
+ if (!Array.isArray(propertyChanges)) return null;
953
+ let changed = false;
954
+ const next = propertyChanges.map((change) => {
955
+ const suggested = readSuggestedParagraphPropertyChange(change);
956
+ if (!suggested || !matchesSuggestion(suggested.suggestionId)) return change;
957
+ changed = true;
958
+ return Object.assign({}, change, { info: {
959
+ ...change.info,
960
+ author,
961
+ date,
962
+ provenance: "user",
963
+ suggestionId: null
964
+ } });
965
+ });
966
+ return changed ? next : null;
967
+ };
968
+ const appendRotatedParagraphPropertyChange = ({ tr, currentPosition, previous, mark, fallbackAuthor }) => {
969
+ const current = tr.doc.nodeAt(currentPosition);
970
+ if (!current || current.type.name !== "paragraph") return false;
971
+ const author = typeof mark.info.author === "string" ? mark.info.author : fallbackAuthor;
972
+ if (author === void 0) return false;
973
+ const existing = expectParagraphAttrs(current)._propertyChanges;
974
+ tr.setNodeMarkup(currentPosition, void 0, {
975
+ ...current.attrs,
976
+ _propertyChanges: [...Array.isArray(existing) ? existing : [], {
977
+ type: "paragraphPropertyChange",
978
+ info: {
979
+ id: mark.info.id,
980
+ author,
981
+ ...typeof mark.info.date === "string" ? { date: mark.info.date } : {},
982
+ ...typeof mark.info.initials === "string" ? { initials: mark.info.initials } : {}
983
+ },
984
+ previousFormatting: paragraphPropertiesSnapshot(previous)
985
+ }]
986
+ });
987
+ return true;
988
+ };
989
+ /**
990
+ * Shift a final run of inserted paragraph marks one paragraph to the left.
991
+ * The container-final paragraph stays markless, while every inserted
992
+ * paragraph keeps a same-revision property snapshot that makes targeted
993
+ * rejection restore the paragraph whose mark now carries its break.
994
+ */
995
+ const rotateAddedFinalParagraphBreaks = ({ tr, shouldRotate, fallbackAuthor }) => {
996
+ const paragraphTypeName = tr.doc.type.schema.nodes["paragraph"]?.name ?? "paragraph";
997
+ const finals = finalParagraphsOf(tr.doc, paragraphTypeName).filter(({ node, position }) => shouldRotate(node, position));
998
+ for (const { position: finalPosition } of finals) {
999
+ const final = tr.doc.nodeAt(finalPosition);
1000
+ const finalMark = final?.attrs["pPrMark"];
1001
+ if (!final || !isAddedPPrMarkAttr(finalMark)) continue;
1002
+ tr.setNodeAttribute(finalPosition, "pPrMark", null);
1003
+ if (!shiftAddedParagraphBreakBefore({
1004
+ tr,
1005
+ paragraphPosition: finalPosition,
1006
+ mark: finalMark,
1007
+ ...fallbackAuthor !== void 0 && { fallbackAuthor }
1008
+ })) return false;
1009
+ }
1010
+ return true;
1011
+ };
1012
+ const rebaseRotatedParagraphPropertyChange = ({ tr, currentPosition, previous, revisionId }) => {
1013
+ const current = tr.doc.nodeAt(currentPosition);
1014
+ if (!current || current.type.name !== "paragraph") return false;
1015
+ const existing = expectParagraphAttrs(current)._propertyChanges;
1016
+ if (!Array.isArray(existing)) return false;
1017
+ if (existing.filter(({ info }) => info.id === revisionId).length !== 1) return false;
1018
+ tr.setNodeMarkup(currentPosition, void 0, {
1019
+ ...current.attrs,
1020
+ _propertyChanges: existing.map((change) => change.info.id === revisionId ? Object.assign({}, change, { previousFormatting: paragraphPropertiesSnapshot(previous) }) : change)
1021
+ });
1022
+ return true;
1023
+ };
1024
+ /**
1025
+ * Rebase every property change in one rotated paragraph-break chain to the
1026
+ * formatting of its original, leftmost carrier. A later targeted suggestion
1027
+ * may extend an already-rotated chain, so normalizing only the newly shifted
1028
+ * prefix would leave the serialized history dependent on acceptance order.
1029
+ */
1030
+ const rebaseAddedParagraphBreakChain = ({ tr, carrierPosition, previous }) => {
1031
+ const carrier = tr.doc.nodeAt(carrierPosition);
1032
+ if (!carrier || carrier.type.name !== "paragraph") return false;
1033
+ const resolved = tr.doc.resolve(carrierPosition);
1034
+ let current = carrier;
1035
+ let nextPosition = carrierPosition + carrier.nodeSize;
1036
+ for (let index = resolved.index() + 1; index < resolved.parent.childCount; index++) {
1037
+ const currentMark = current.attrs["pPrMark"];
1038
+ if (!isAddedPPrMarkAttr(currentMark)) return true;
1039
+ const sibling = resolved.parent.child(index);
1040
+ if (sibling.type.name !== carrier.type.name) {
1041
+ if (sibling.type.spec["tableRole"] === "table") return false;
1042
+ nextPosition += sibling.nodeSize;
1043
+ continue;
1044
+ }
1045
+ if (!rebaseRotatedParagraphPropertyChange({
1046
+ tr,
1047
+ currentPosition: nextPosition,
1048
+ previous,
1049
+ revisionId: currentMark.info.id
1050
+ })) return false;
1051
+ const next = tr.doc.nodeAt(nextPosition);
1052
+ if (!next || next.type.name !== carrier.type.name) return false;
1053
+ current = next;
1054
+ nextPosition += sibling.nodeSize;
1055
+ }
1056
+ return !isAddedPPrMarkAttr(current.attrs["pPrMark"]);
1057
+ };
1058
+ /**
1059
+ * Insert an added paragraph break immediately before `paragraphPosition`.
1060
+ * Existing added marks shift left as one chain until the first free carrier;
1061
+ * each shifted mark keeps a property snapshot on the paragraph to its right.
1062
+ */
1063
+ const shiftAddedParagraphBreakBefore = ({ tr, paragraphPosition, mark, fallbackAuthor, firstPropertyChange = { type: "append" } }) => {
1064
+ const paragraph = tr.doc.nodeAt(paragraphPosition);
1065
+ if (!paragraph || paragraph.type.name !== "paragraph") return false;
1066
+ const resolved = tr.doc.resolve(paragraphPosition);
1067
+ const carrier = addedBreakCarrierBefore(resolved, paragraph.type.name);
1068
+ if (!carrier) return false;
1069
+ const path = [{
1070
+ node: paragraph,
1071
+ position: paragraphPosition
1072
+ }];
1073
+ let position = paragraphPosition;
1074
+ for (let index = resolved.index() - 1; index >= 0; index--) {
1075
+ const sibling = resolved.parent.child(index);
1076
+ position -= sibling.nodeSize;
1077
+ if (sibling.type.name !== paragraph.type.name) {
1078
+ if (sibling.type.spec["tableRole"] === "table") break;
1079
+ continue;
1080
+ }
1081
+ path.push({
1082
+ node: sibling,
1083
+ position
1084
+ });
1085
+ if (position === carrier.position) break;
1086
+ }
1087
+ if (path.at(-1)?.position !== carrier.position) return false;
1088
+ const propertyChangeBaseline = carrier.node;
1089
+ let carriedMark = mark;
1090
+ for (let index = 1; index < path.length; index++) {
1091
+ const current = path[index - 1];
1092
+ const previous = path[index];
1093
+ if (!current || !previous) return false;
1094
+ const displacedMark = previous.node.attrs["pPrMark"];
1095
+ tr.setNodeAttribute(previous.position, "pPrMark", carriedMark);
1096
+ if (!(index === 1 && firstPropertyChange.type === "rebase" ? rebaseRotatedParagraphPropertyChange({
1097
+ tr,
1098
+ currentPosition: current.position,
1099
+ previous: propertyChangeBaseline,
1100
+ revisionId: firstPropertyChange.revisionId
1101
+ }) : appendRotatedParagraphPropertyChange({
1102
+ tr,
1103
+ currentPosition: current.position,
1104
+ previous: propertyChangeBaseline,
1105
+ mark: carriedMark,
1106
+ ...fallbackAuthor !== void 0 && { fallbackAuthor }
1107
+ }))) return false;
1108
+ if (displacedMark == null) return rebaseAddedParagraphBreakChain({
1109
+ tr,
1110
+ carrierPosition: carrier.position,
1111
+ previous: propertyChangeBaseline
1112
+ });
1113
+ if (!isAddedPPrMarkAttr(displacedMark)) return false;
1114
+ carriedMark = displacedMark;
1115
+ }
1116
+ return false;
1117
+ };
1118
+ /**
1119
+ * Place an accepted paragraph's break when a following accepted insertion has
1120
+ * already rotated its own break onto that paragraph.
1121
+ */
1122
+ const placeAcceptedParagraphBreakBefore = ({ tr, paragraphPosition, mark, fallbackAuthor }) => {
1123
+ const paragraph = tr.doc.nodeAt(paragraphPosition);
1124
+ const displaced = paragraph?.attrs["pPrMark"];
1125
+ if (!paragraph || paragraph.type.name !== "paragraph" || !isPPrMarkAttr(displaced) || !paragraphMarkWasAdded(displaced.kind)) return false;
1126
+ return shiftAddedParagraphBreakBefore({
1127
+ tr,
1128
+ paragraphPosition,
1129
+ mark,
1130
+ fallbackAuthor
1131
+ });
1132
+ };
1133
+ /**
1134
+ * Remember non-final added breaks before suggested nodes are removed. If one
1135
+ * becomes final because of those removals, it must rotate with its own
1136
+ * revision rather than remain as an unresolvable final mark.
1137
+ */
1138
+ const collectAddedBreakRotationCandidates = (doc) => {
1139
+ const paragraphTypeName = doc.type.schema.nodes["paragraph"]?.name ?? "paragraph";
1140
+ const finalPositions = new Set(finalParagraphsOf(doc, paragraphTypeName).map(({ position }) => position));
1141
+ const candidates = [];
1142
+ doc.descendants((node, position) => {
1143
+ if (node.type.name !== paragraphTypeName || finalPositions.has(position)) return;
1144
+ const mark = node.attrs["pPrMark"];
1145
+ if (isPPrMarkAttr(mark) && paragraphMarkWasAdded(mark.kind)) candidates.push({
1146
+ position,
1147
+ revisionId: mark.info.id
1148
+ });
1149
+ });
1150
+ return candidates;
1151
+ };
1152
+ const mappedRotationCandidatePredicate = ({ candidates, tr }) => {
1153
+ const revisionIdsByPosition = /* @__PURE__ */ new Map();
1154
+ for (const candidate of candidates) {
1155
+ const mapped = tr.mapping.mapResult(candidate.position, 1);
1156
+ if (mapped.deleted) continue;
1157
+ const mappedPosition = mapped.pos;
1158
+ const revisionIds = revisionIdsByPosition.get(mappedPosition) ?? /* @__PURE__ */ new Set();
1159
+ revisionIds.add(candidate.revisionId);
1160
+ revisionIdsByPosition.set(mappedPosition, revisionIds);
1161
+ }
1162
+ return (node, position) => {
1163
+ const mark = node.attrs["pPrMark"];
1164
+ return isPPrMarkAttr(mark) && paragraphMarkWasAdded(mark.kind) && revisionIdsByPosition.get(position)?.has(mark.info.id) === true;
1165
+ };
1166
+ };
1167
+ /**
1168
+ * Remove suggested nodes without deleting a following accepted insertion's
1169
+ * paragraph break that has rotated onto one of them.
1170
+ */
1171
+ const removeSuggestedInsertNodes = ({ tr, positions, resolvedRevisionIds }) => {
1172
+ for (const position of positions.toSorted((left, right) => right - left)) {
1173
+ const node = tr.doc.nodeAt(position);
1174
+ if (!node) continue;
1175
+ const mark = node.attrs["pPrMark"];
1176
+ const followingBreak = node.type.name === "paragraph" && isAddedPPrMarkAttr(mark) && !resolvedRevisionIds.has(mark.info.id) ? mark : null;
1177
+ tr.delete(position, position + node.nodeSize);
1178
+ if (followingBreak && !shiftAddedParagraphBreakBefore({
1179
+ tr,
1180
+ paragraphPosition: position,
1181
+ mark: followingBreak,
1182
+ firstPropertyChange: {
1183
+ type: "rebase",
1184
+ revisionId: followingBreak.info.id
1185
+ }
1186
+ })) return false;
1187
+ }
1188
+ return true;
1189
+ };
912
1190
  /**
913
1191
  * Convert suggested marks AND block/table node revisions to normal (`"user"`)
914
1192
  * tracked changes. `matchesSuggestion(id)` selects which suggestion to convert
@@ -921,16 +1199,37 @@ const acceptSuggestions = (matchesSuggestion, options) => {
921
1199
  const runPropertyChangeType = state.schema.marks["runPropertyChange"];
922
1200
  const date = normalizeAcceptDate(options.date);
923
1201
  const tr = state.tr;
1202
+ const acceptedParagraphBreaks = [];
924
1203
  let changed = false;
925
1204
  state.doc.descendants((node, pos) => {
926
1205
  const structural = readStructuralSuggestion(node);
1206
+ let nextAttrs = null;
927
1207
  if (structural && matchesSuggestion(structural.suggestionId)) {
928
- const nextAttrs = convertStructuralSuggestionAttrs(node, options.author, date);
929
- if (nextAttrs) {
930
- tr.setNodeMarkup(pos, void 0, nextAttrs);
931
- changed = true;
1208
+ nextAttrs = convertStructuralSuggestionAttrs(node, options.author, date);
1209
+ if (structural.kind === "insertBlock") {
1210
+ const mark = acceptedParagraphMark(node, options.author, date);
1211
+ if (!mark) return;
1212
+ acceptedParagraphBreaks.push({
1213
+ position: pos,
1214
+ mark,
1215
+ hadBreak: node.attrs["pPrMark"] != null
1216
+ });
932
1217
  }
933
1218
  }
1219
+ const propertyChanges = reauthorParagraphPropertySuggestions({
1220
+ node,
1221
+ matchesSuggestion,
1222
+ author: options.author,
1223
+ date
1224
+ });
1225
+ if (propertyChanges) nextAttrs = {
1226
+ ...nextAttrs ?? node.attrs,
1227
+ _propertyChanges: propertyChanges
1228
+ };
1229
+ if (nextAttrs) {
1230
+ tr.setNodeMarkup(pos, void 0, nextAttrs);
1231
+ changed = true;
1232
+ }
934
1233
  if (!node.isInline) return;
935
1234
  const from = pos;
936
1235
  const to = pos + node.nodeSize;
@@ -964,6 +1263,18 @@ const acceptSuggestions = (matchesSuggestion, options) => {
964
1263
  }));
965
1264
  }
966
1265
  });
1266
+ for (const accepted of acceptedParagraphBreaks) if (accepted.hadBreak && !placeAcceptedParagraphBreakBefore({
1267
+ tr,
1268
+ paragraphPosition: accepted.position,
1269
+ mark: accepted.mark,
1270
+ fallbackAuthor: options.author
1271
+ })) return false;
1272
+ const directlyMarkedParagraphPositions = new Set(acceptedParagraphBreaks.filter(({ hadBreak }) => !hadBreak).map(({ position }) => position));
1273
+ if (directlyMarkedParagraphPositions.size > 0 && !rotateAddedFinalParagraphBreaks({
1274
+ tr,
1275
+ shouldRotate: (_node, position) => directlyMarkedParagraphPositions.has(position),
1276
+ fallbackAuthor: options.author
1277
+ })) return false;
967
1278
  if (!changed) return false;
968
1279
  if (dispatch) dispatch(tr);
969
1280
  return true;
@@ -1001,6 +1312,7 @@ function rejectSuggestion(suggestionId) {
1001
1312
  return (state, dispatch) => {
1002
1313
  const entry = collectSuggestions(state).get(suggestionId);
1003
1314
  if (!entry) return false;
1315
+ const rotationCandidates = entry.kinds.has("insertBlock") || entry.kinds.has("insertTable") ? collectAddedBreakRotationCandidates(state.doc) : [];
1004
1316
  let tr = null;
1005
1317
  if (entry.revisionIds.size > 0) {
1006
1318
  const ranges = mergeSegments(entry.segments);
@@ -1015,10 +1327,18 @@ function rejectSuggestion(suggestionId) {
1015
1327
  const structural = readStructuralSuggestion(node);
1016
1328
  if (structural?.isNodeInsert && structural.suggestionId === suggestionId) positions.push(pos);
1017
1329
  });
1018
- for (const pos of positions.toSorted((a, b) => b - a)) {
1019
- const node = workingTr.doc.nodeAt(pos);
1020
- if (node) workingTr.delete(pos, pos + node.nodeSize);
1021
- }
1330
+ if (!removeSuggestedInsertNodes({
1331
+ tr: workingTr,
1332
+ positions,
1333
+ resolvedRevisionIds: entry.revisionIds
1334
+ })) return false;
1335
+ if (positions.length > 0 && rotationCandidates.length > 0 && !rotateAddedFinalParagraphBreaks({
1336
+ tr: workingTr,
1337
+ shouldRotate: mappedRotationCandidatePredicate({
1338
+ candidates: rotationCandidates,
1339
+ tr: workingTr
1340
+ })
1341
+ })) return false;
1022
1342
  if (workingTr.steps.length === 0) return false;
1023
1343
  if (dispatch) dispatch(workingTr);
1024
1344
  return true;
@@ -1030,7 +1350,12 @@ function rejectAllSuggestions() {
1030
1350
  const bySuggestion = collectSuggestions(state);
1031
1351
  if (bySuggestion.size === 0) return false;
1032
1352
  const revisionIds = /* @__PURE__ */ new Set();
1033
- for (const entry of bySuggestion.values()) for (const id of entry.revisionIds) revisionIds.add(id);
1353
+ let removesWholeNode = false;
1354
+ for (const entry of bySuggestion.values()) {
1355
+ removesWholeNode ||= entry.kinds.has("insertBlock") || entry.kinds.has("insertTable");
1356
+ for (const id of entry.revisionIds) revisionIds.add(id);
1357
+ }
1358
+ const rotationCandidates = removesWholeNode ? collectAddedBreakRotationCandidates(state.doc) : [];
1034
1359
  let tr = null;
1035
1360
  if (revisionIds.size > 0) resolveChange(0, state.doc.content.size, "reject", [...revisionIds])(state, (resolved) => {
1036
1361
  tr = resolved;
@@ -1040,10 +1365,18 @@ function rejectAllSuggestions() {
1040
1365
  workingTr.doc.descendants((node, pos) => {
1041
1366
  if (readStructuralSuggestion(node)?.isNodeInsert) positions.push(pos);
1042
1367
  });
1043
- for (const pos of positions.toSorted((a, b) => b - a)) {
1044
- const node = workingTr.doc.nodeAt(pos);
1045
- if (node) workingTr.delete(pos, pos + node.nodeSize);
1046
- }
1368
+ if (!removeSuggestedInsertNodes({
1369
+ tr: workingTr,
1370
+ positions,
1371
+ resolvedRevisionIds: revisionIds
1372
+ })) return false;
1373
+ if (positions.length > 0 && rotationCandidates.length > 0 && !rotateAddedFinalParagraphBreaks({
1374
+ tr: workingTr,
1375
+ shouldRotate: mappedRotationCandidatePredicate({
1376
+ candidates: rotationCandidates,
1377
+ tr: workingTr
1378
+ })
1379
+ })) return false;
1047
1380
  if (workingTr.steps.length === 0) return false;
1048
1381
  if (dispatch) dispatch(workingTr);
1049
1382
  return true;
@@ -1,5 +1,5 @@
1
- import { Fragment } from "prosemirror-model";
2
1
  import { TextSelection } from "prosemirror-state";
2
+ import { Fragment } from "prosemirror-model";
3
3
  //#region src/prosemirror/commands/pageBreak.ts
4
4
  /**
5
5
  * Page Break Commands
@@ -1,8 +1,27 @@
1
1
  import { document_d_exports } from "../../types/document.js";
2
- import { ParagraphPropertyChangeAttrs } from "../schema/nodes.js";
2
+ import { ParagraphAttrs, ParagraphPropertyChangeAttrs } from "../schema/nodes.js";
3
+ import { Node } from "prosemirror-model";
3
4
  //#region src/prosemirror/commands/propertyChangeScope.d.ts
4
5
  type AttrPatch = Record<string, unknown>;
5
6
  type ParagraphPropertySnapshot = NonNullable<ParagraphPropertyChangeAttrs["previousFormatting"]>;
7
+ type ParagraphPropertyChangeRemoval = {
8
+ type: "unchanged";
9
+ } | {
10
+ type: "keep-live";
11
+ remaining: ParagraphPropertyChangeAttrs[];
12
+ } | {
13
+ type: "restore-previous";
14
+ remaining: ParagraphPropertyChangeAttrs[];
15
+ previousFormatting: ParagraphPropertyChangeAttrs["previousFormatting"];
16
+ };
17
+ /**
18
+ * Remove selected entries from an oldest-to-newest paragraph-property chain.
19
+ * A retained entry immediately after a removed run inherits that run's
20
+ * earliest previous snapshot. Only a removed trailing run restores the live
21
+ * paragraph properties. This makes repeated scoped rejection independent of
22
+ * the order in which revisions are resolved.
23
+ */
24
+ declare const removeParagraphPropertyChanges: (changes: readonly ParagraphPropertyChangeAttrs[], shouldRemove: (change: ParagraphPropertyChangeAttrs) => boolean) => ParagraphPropertyChangeRemoval;
6
25
  /**
7
26
  * Paragraph attrs governed by a `w:pPrChange` — the attrs
8
27
  * `paragraphFormattingToAttrs` (conversion/toProseDoc.ts) can produce from a
@@ -19,9 +38,12 @@ type ParagraphPropertySnapshot = NonNullable<ParagraphPropertyChangeAttrs["previ
19
38
  * `runInWithNext` (`w:specVanish` lives in that rPr)
20
39
  * - load-time style/numbering bookkeeping the command layer cannot recompute
21
40
  * without a style resolver: `numPrFromStyle`, the `list*` rendering attrs,
22
- * `spacingFromDocDefaults`, `spacingFromImplicitDefaultStyle`
41
+ * `alignmentFromStyle`, `spacingFromDocDefaults`,
42
+ * `spacingFromImplicitDefaultStyle`
23
43
  */
24
44
  declare const PPR_CHANGE_SCOPED_ATTR_KEYS: readonly ["styleId", "numPr", "alignment", "spaceBefore", "spaceAfter", "lineSpacing", "lineSpacingRule", "lineSpacingExplicit", "snapToGrid", "spacingExplicit", "indentLeft", "indentRight", "indentFirstLine", "hangingIndent", "borders", "shading", "tabs", "pageBreakBefore", "keepNext", "keepLines", "widowControl", "contextualSpacing", "outlineLevel", "direction", "_autospacingBase"];
45
+ /** The in-scope paragraph properties as they stand, for a `w:pPrChange` record. */
46
+ declare const paragraphPropertiesSnapshot: (node: Node) => ParagraphPropertySnapshot;
25
47
  /**
26
48
  * Build the attr patch that rejecting one pPrChange applies to a paragraph:
27
49
  * every in-scope key set to the stored previous value, or reset to `null`
@@ -29,7 +51,7 @@ declare const PPR_CHANGE_SCOPED_ATTR_KEYS: readonly ["styleId", "numPr", "alignm
29
51
  * the scoped set (editor-created list suggestions snapshot list-rendering
30
52
  * bookkeeping attrs) merge through 1:1 so their pre-change values restore too.
31
53
  */
32
- declare function paragraphRejectAttrPatch(previousFormatting: ParagraphPropertySnapshot | null | undefined): AttrPatch;
54
+ declare function paragraphRejectAttrPatch(previousFormatting: ParagraphPropertySnapshot | null | undefined, alignmentFromStyle?: ParagraphAttrs["alignmentFromStyle"]): AttrPatch;
33
55
  /**
34
56
  * Rebuild the paragraph's `_originalFormatting` (the serializer's pPr source)
35
57
  * after a reject: the stored old pPr wholesale within CT_PPrBase scope, with
@@ -66,4 +88,4 @@ declare function tableRowRejectAttrPatch(previousFormatting: document_d_exports.
66
88
  */
67
89
  declare function tableCellRejectAttrPatch(previousFormatting: document_d_exports.TableCellFormatting | undefined, liveOriginalFormatting: document_d_exports.TableCellFormatting | null | undefined): AttrPatch;
68
90
  //#endregion
69
- export { PPR_CHANGE_SCOPED_ATTR_KEYS, paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, sectionRejectProperties, tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch };
91
+ export { PPR_CHANGE_SCOPED_ATTR_KEYS, ParagraphPropertyChangeRemoval, paragraphPropertiesSnapshot, paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, removeParagraphPropertyChanges, sectionRejectProperties, tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch };