@stll/folio-core 0.12.0 → 0.14.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 (138) hide show
  1. package/dist/ai-edits/apply.d.ts +2 -1
  2. package/dist/ai-edits/apply.js +118 -40
  3. package/dist/ai-edits/blockRange.js +1 -1
  4. package/dist/ai-edits/clean-text.js +2 -1
  5. package/dist/ai-edits/headless.js +3 -3
  6. package/dist/ai-edits/snapshot.js +21 -3
  7. package/dist/ai-edits/table-cell-mutations.js +2 -1
  8. package/dist/ai-edits/table-row-column-mutations.d.ts +9 -1
  9. package/dist/ai-edits/table-row-column-mutations.js +1 -1
  10. package/dist/ai-edits/types.d.ts +28 -1
  11. package/dist/ai-edits/word-diff.js +21 -0
  12. package/dist/compat/eigenpal.d.ts +3 -3
  13. package/dist/compat/eigenpal.js +3 -3
  14. package/dist/controller/headerFooterEditorManager.js +1 -1
  15. package/dist/controller/hiddenEditorManager.js +12 -1
  16. package/dist/controller/layoutPipeline.js +1 -1
  17. package/dist/controller/noteEditorManager.js +1 -1
  18. package/dist/document-operations.d.ts +13 -13
  19. package/dist/document-operations.js +31 -21
  20. package/dist/docx/blockContentParser.js +19 -5
  21. package/dist/docx/commentRangeIntegrity.d.ts +6 -0
  22. package/dist/docx/commentRangeIntegrity.js +186 -0
  23. package/dist/docx/conformance.js +57 -3
  24. package/dist/docx/encryption/agileDecryption.js +12 -2
  25. package/dist/docx/encryption/compoundFile.js +6 -1
  26. package/dist/docx/encryption/encryptionInfo.d.ts +9 -1
  27. package/dist/docx/encryption/encryptionInfo.js +18 -2
  28. package/dist/docx/encryption/openEncryptedDocx.js +1 -1
  29. package/dist/docx/ensureParaIds.js +7 -6
  30. package/dist/docx/groupDrawingParser.js +10 -5
  31. package/dist/docx/hyperlinkParser.js +1 -1
  32. package/dist/docx/imageParser.js +10 -6
  33. package/dist/docx/metadataPrivacy.js +17 -3
  34. package/dist/docx/paragraphParser.js +7 -3
  35. package/dist/docx/rezip.js +19 -8
  36. package/dist/docx/runParser.js +13 -4
  37. package/dist/docx/sectionParser.js +17 -4
  38. package/dist/docx/selectiveSave.js +4 -0
  39. package/dist/docx/selectiveXmlPatch.d.ts +22 -1
  40. package/dist/docx/selectiveXmlPatch.js +61 -1
  41. package/dist/docx/serializer/blockSdtSerializer.js +3 -2
  42. package/dist/docx/serializer/commentSerializer.js +4 -4
  43. package/dist/docx/serializer/paragraphSerializer.js +16 -14
  44. package/dist/docx/serializer/runSerializer.js +21 -19
  45. package/dist/docx/serializer/sectionPropertiesSerializer.js +2 -1
  46. package/dist/docx/serializer/tableSerializer.js +9 -7
  47. package/dist/docx/serializer/xmlUtils.d.ts +13 -1
  48. package/dist/docx/serializer/xmlUtils.js +27 -1
  49. package/dist/docx/server/docxToMarkdown.d.ts +15 -0
  50. package/dist/docx/server/docxToMarkdown.js +17 -0
  51. package/dist/docx/server/extractDocxText.js +55 -8
  52. package/dist/docx/settingsParser.js +15 -2
  53. package/dist/docx/tableParser.d.ts +8 -1
  54. package/dist/docx/tableParser.js +29 -8
  55. package/dist/docx/unzip.js +1 -0
  56. package/dist/docx/vmlImageParser.js +11 -3
  57. package/dist/docx/xmlParser.js +11 -1
  58. package/dist/fonts/embeddedFonts.d.ts +33 -5
  59. package/dist/fonts/embeddedFonts.js +37 -8
  60. package/dist/index.d.ts +3 -3
  61. package/dist/index.js +3 -3
  62. package/dist/layout-bridge/convert/toFlowBlocks.js +15 -0
  63. package/dist/layout-engine/index.js +19 -12
  64. package/dist/layout-engine/measure/cache.js +2 -0
  65. package/dist/layout-engine/measure/effectiveLineBreakPolicy.js +8 -2
  66. package/dist/layout-engine/measure/floatingTablePosition.d.ts +9 -0
  67. package/dist/layout-engine/measure/floatingTablePosition.js +17 -0
  68. package/dist/layout-engine/measure/index.d.ts +2 -1
  69. package/dist/layout-engine/measure/index.js +2 -1
  70. package/dist/layout-engine/measure/lineBreakProvider.d.ts +2 -2
  71. package/dist/layout-engine/measure/lineBreakProvider.js +3 -3
  72. package/dist/layout-engine/measure/measureBlocks.js +11 -8
  73. package/dist/layout-engine/measure/measureHelpers.d.ts +10 -1
  74. package/dist/layout-engine/measure/measureHelpers.js +12 -1
  75. package/dist/layout-engine/measure/measureParagraph.js +16 -0
  76. package/dist/layout-engine/types.d.ts +22 -2
  77. package/dist/layout-painter/renderImage.d.ts +12 -1
  78. package/dist/layout-painter/renderImage.js +21 -4
  79. package/dist/layout-painter/renderPage.js +5 -8
  80. package/dist/layout-painter/renderParagraph.js +33 -9
  81. package/dist/layout-painter/renderTable.js +2 -1
  82. package/dist/layout-painter/renderWatermark.d.ts +1 -1
  83. package/dist/layout-painter/renderWatermark.js +2 -1
  84. package/dist/markdown/fromMarkdown.js +1 -1
  85. package/dist/prosemirror/attrs/index.d.ts +1 -1
  86. package/dist/prosemirror/attrs/index.js +21 -0
  87. package/dist/prosemirror/commands/comments.d.ts +76 -1
  88. package/dist/prosemirror/commands/comments.js +401 -28
  89. package/dist/prosemirror/commands/hyperlink.js +15 -1
  90. package/dist/prosemirror/commands/image.js +7 -1
  91. package/dist/prosemirror/commands/index.d.ts +2 -2
  92. package/dist/prosemirror/commands/index.js +2 -2
  93. package/dist/prosemirror/commands/tableCellMergeResolution.js +1 -1
  94. package/dist/prosemirror/commentIdAllocator.js +13 -3
  95. package/dist/prosemirror/conversion/fromProseDoc.js +122 -5
  96. package/dist/prosemirror/conversion/index.js +1 -1
  97. package/dist/prosemirror/conversion/toProseDoc.js +20 -8
  98. package/dist/prosemirror/extensions/core/ParagraphExtension.js +2 -1
  99. package/dist/prosemirror/extensions/features/ImagePasteExtension.js +10 -2
  100. package/dist/prosemirror/extensions/features/pasteCleanup.js +15 -2
  101. package/dist/prosemirror/extensions/marks/HyperlinkExtension.js +30 -4
  102. package/dist/prosemirror/extensions/marks/TrackedChangeExtensions.js +49 -13
  103. package/dist/prosemirror/extensions/nodes/TableExtension.d.ts +1 -1
  104. package/dist/prosemirror/extensions/nodes/TableExtension.js +5 -3
  105. package/dist/prosemirror/index.d.ts +1 -1
  106. package/dist/prosemirror/index.js +1 -1
  107. package/dist/prosemirror/plugins/contentControlWidgets.d.ts +2 -2
  108. package/dist/prosemirror/plugins/contentControlWidgets.js +4 -4
  109. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -0
  110. package/dist/prosemirror/plugins/revisionIds.js +57 -0
  111. package/dist/prosemirror/plugins/suggestionMode.js +4 -3
  112. package/dist/prosemirror/runFormattingMarkNames.d.ts +14 -0
  113. package/dist/prosemirror/runFormattingMarkNames.js +40 -0
  114. package/dist/prosemirror/schema/index.d.ts +1 -1
  115. package/dist/prosemirror/schema/marks.d.ts +27 -4
  116. package/dist/prosemirror/schema/marks.js +15 -0
  117. package/dist/prosemirror/schema/nodes.d.ts +50 -2
  118. package/dist/prosemirror/utils/tabCalculator.js +3 -3
  119. package/dist/render-dom/RemoteSelectionOverlay.js +2 -2
  120. package/dist/server.d.ts +3 -2
  121. package/dist/server.js +4 -3
  122. package/dist/utils/clipboard.d.ts +10 -1
  123. package/dist/utils/clipboard.js +33 -6
  124. package/dist/utils/colorResolver.d.ts +8 -1
  125. package/dist/utils/colorResolver.js +35 -5
  126. package/dist/utils/fontResolver.d.ts +2 -1
  127. package/dist/utils/fontResolver.js +9 -3
  128. package/dist/utils/hexId.d.ts +11 -1
  129. package/dist/utils/hexId.js +11 -1
  130. package/dist/utils/sanitizeImageSrc.d.ts +21 -0
  131. package/dist/utils/sanitizeImageSrc.js +30 -0
  132. package/dist/utils/stripXmlDeclarations.d.ts +11 -0
  133. package/dist/utils/stripXmlDeclarations.js +35 -0
  134. package/dist/version-comparison.d.ts +22 -2
  135. package/dist/version-comparison.js +41 -12
  136. package/dist/watermark/index.d.ts +11 -1
  137. package/dist/watermark/index.js +22 -1
  138. package/package.json +3 -2
@@ -1,4 +1,5 @@
1
1
  import { expectRunPropertyChangeMarkAttrs } from "../attrs/index.js";
2
+ import { RUN_FORMATTING_MARK_NAMES } from "../runFormattingMarkNames.js";
2
3
  import { markStructuralChange } from "../extensions/features/ParagraphChangeTrackerExtension.js";
3
4
  import { paragraphRejectAttrPatch, paragraphRejectOriginalFormatting, sectionRejectProperties, tableCellRejectAttrPatch, tableRejectAttrPatch, tableRowRejectAttrPatch } from "./propertyChangeScope.js";
4
5
  import { textFormattingToMarks } from "../conversion/toProseDoc.js";
@@ -200,33 +201,6 @@ function resolveChange(from, to, mode, revisionIds) {
200
201
  return true;
201
202
  };
202
203
  }
203
- const RUN_FORMATTING_MARK_NAMES = /* @__PURE__ */ new Set([
204
- "bold",
205
- "italic",
206
- "underline",
207
- "strike",
208
- "textColor",
209
- "highlight",
210
- "runShading",
211
- "fontSize",
212
- "fontFamily",
213
- "language",
214
- "superscript",
215
- "subscript",
216
- "allCaps",
217
- "smallCaps",
218
- "characterSpacing",
219
- "emboss",
220
- "imprint",
221
- "hidden",
222
- "textShadow",
223
- "emphasisMark",
224
- "textOutline",
225
- "rtl",
226
- "textEffect",
227
- "runFormattingOverride",
228
- "characterStyle"
229
- ]);
230
204
  const resolveRunPropertyChange = ({ tr, node, from, to, mark, mode, revisionSet }) => {
231
205
  const { changes } = expectRunPropertyChangeMarkAttrs(mark);
232
206
  const matches = changes.filter((change) => revisionSet === null || revisionSet.has(change.info.id));
@@ -574,6 +548,405 @@ function rejectAIEditRevision(revisionIds) {
574
548
  };
575
549
  }
576
550
  /**
551
+ * Suggestion (AI-proposed tracked change) commands.
552
+ *
553
+ * A suggestion is a set of `insertion` / `deletion` / `runPropertyChange` marks
554
+ * sharing one `suggestionId` and carrying `provenance: "suggested"`. Accepting
555
+ * rewrites those marks to normal (`"user"`) tracked changes authored by the
556
+ * accepting user; rejecting inverse-applies them like `rejectChange`, reusing
557
+ * the same {@link resolveChange} machinery scoped to the suggestion's revision
558
+ * ids. All three operate as ordinary PM transactions, so they sync via collab
559
+ * and are undoable.
560
+ */
561
+ const SUGGESTION_MARK_NAMES = /* @__PURE__ */ new Set([
562
+ "insertion",
563
+ "deletion",
564
+ "runPropertyChange"
565
+ ]);
566
+ const isSuggestionMark = (mark) => mark.attrs["provenance"] === "suggested" && SUGGESTION_MARK_NAMES.has(mark.type.name);
567
+ const suggestionKindOf = (mark) => {
568
+ if (mark.type.name === "insertion") return "insertion";
569
+ if (mark.type.name === "deletion") return "deletion";
570
+ return "formatting";
571
+ };
572
+ const readSuggestedMarker = (marker) => {
573
+ if (typeof marker !== "object" || marker === null || marker.provenance !== "suggested") return null;
574
+ const suggestionId = marker.suggestionId;
575
+ const revisionId = marker.revisionId;
576
+ if (typeof suggestionId !== "string" || typeof revisionId !== "number") return null;
577
+ return {
578
+ suggestionId,
579
+ revisionId
580
+ };
581
+ };
582
+ const readStructuralSuggestion = (node) => {
583
+ const attrs = node.attrs;
584
+ const name = node.type.name;
585
+ const insertMarker = attrs["_suggestedInsert"];
586
+ if ((name === "paragraph" || name === "table") && typeof insertMarker === "object" && insertMarker !== null) {
587
+ const suggestionId = insertMarker.suggestionId;
588
+ const revisionId = insertMarker.revisionId;
589
+ if (typeof suggestionId === "string" && typeof revisionId === "number") return {
590
+ suggestionId,
591
+ revisionId,
592
+ kind: name === "table" ? "insertTable" : "insertBlock",
593
+ isNodeInsert: true
594
+ };
595
+ }
596
+ if (node.type.name === "tableRow") {
597
+ const ins = readSuggestedMarker(attrs["trIns"]);
598
+ if (ins) return {
599
+ ...ins,
600
+ kind: "insertRow",
601
+ isNodeInsert: false
602
+ };
603
+ const del = readSuggestedMarker(attrs["trDel"]);
604
+ if (del) return {
605
+ ...del,
606
+ kind: "deleteRow",
607
+ isNodeInsert: false
608
+ };
609
+ }
610
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
611
+ const cellMarker = attrs["cellMarker"];
612
+ if (typeof cellMarker === "object" && cellMarker !== null) {
613
+ const markerKind = cellMarker.kind;
614
+ if (markerKind === "ins" || markerKind === "del") {
615
+ const marker = readSuggestedMarker(cellMarker.info);
616
+ if (marker) {
617
+ const kind = markerKind === "del" ? "deleteColumn" : "insertColumn";
618
+ return {
619
+ ...marker,
620
+ kind,
621
+ isNodeInsert: false
622
+ };
623
+ }
624
+ }
625
+ }
626
+ }
627
+ return null;
628
+ };
629
+ const collectSuggestions = (state) => {
630
+ const runPropertyChangeType = state.schema.marks["runPropertyChange"];
631
+ const bySuggestion = /* @__PURE__ */ new Map();
632
+ const entryFor = (suggestionId) => {
633
+ const existing = bySuggestion.get(suggestionId);
634
+ if (existing) return existing;
635
+ const created = {
636
+ segments: [],
637
+ kinds: /* @__PURE__ */ new Set(),
638
+ revisionIds: /* @__PURE__ */ new Set()
639
+ };
640
+ bySuggestion.set(suggestionId, created);
641
+ return created;
642
+ };
643
+ state.doc.descendants((node, pos) => {
644
+ const structural = readStructuralSuggestion(node);
645
+ if (structural) {
646
+ const entry = entryFor(structural.suggestionId);
647
+ entry.segments.push({
648
+ from: pos,
649
+ to: pos + node.nodeSize
650
+ });
651
+ entry.kinds.add(structural.kind);
652
+ entry.revisionIds.add(structural.revisionId);
653
+ }
654
+ if (!node.isInline) return;
655
+ for (const mark of node.marks) {
656
+ if (!isSuggestionMark(mark)) continue;
657
+ const suggestionId = mark.attrs["suggestionId"];
658
+ if (typeof suggestionId !== "string" || suggestionId.length === 0) continue;
659
+ const entry = entryFor(suggestionId);
660
+ entry.segments.push({
661
+ from: pos,
662
+ to: pos + node.nodeSize
663
+ });
664
+ entry.kinds.add(suggestionKindOf(mark));
665
+ if (mark.type === runPropertyChangeType) for (const change of expectRunPropertyChangeMarkAttrs(mark).changes) entry.revisionIds.add(change.info.id);
666
+ else if (typeof mark.attrs["revisionId"] === "number") entry.revisionIds.add(mark.attrs["revisionId"]);
667
+ }
668
+ });
669
+ return bySuggestion;
670
+ };
671
+ /**
672
+ * A whole inserted table (`insertTable`) has no OOXML tracked representation and
673
+ * accepts directly; every other kind accepts as a tracked change. A group that
674
+ * mixes a table insert with any other kind (a caller stamping heterogeneous
675
+ * operations with one `suggestionId`) is `"mixed"`.
676
+ */
677
+ const suggestionAppliedAs = (kinds) => {
678
+ if (!kinds.has("insertTable")) return "tracked";
679
+ return [...kinds].some((kind) => kind !== "insertTable") ? "mixed" : "direct";
680
+ };
681
+ /** Merge sorted, possibly adjacent/overlapping segments into contiguous ranges. */
682
+ const mergeSegments = (segments) => {
683
+ const sorted = [...segments].toSorted((a, b) => a.from - b.from || a.to - b.to);
684
+ const merged = [];
685
+ for (const segment of sorted) {
686
+ const last = merged.at(-1);
687
+ if (last && segment.from <= last.to) {
688
+ last.to = Math.max(last.to, segment.to);
689
+ continue;
690
+ }
691
+ merged.push({ ...segment });
692
+ }
693
+ return merged;
694
+ };
695
+ const SUGGESTION_KIND_ORDER = [
696
+ "insertion",
697
+ "deletion",
698
+ "formatting",
699
+ "insertBlock",
700
+ "insertTable",
701
+ "insertRow",
702
+ "deleteRow",
703
+ "insertColumn",
704
+ "deleteColumn"
705
+ ];
706
+ /**
707
+ * List every suggestion in the document for host consumption: its id, the
708
+ * contiguous ranges it covers, which kinds of change it contains, and how
709
+ * accepting it applies (tracked vs direct).
710
+ */
711
+ function getSuggestions(state) {
712
+ const bySuggestion = collectSuggestions(state);
713
+ const suggestions = [];
714
+ for (const [suggestionId, entry] of bySuggestion) suggestions.push({
715
+ suggestionId,
716
+ ranges: mergeSegments(entry.segments),
717
+ kinds: SUGGESTION_KIND_ORDER.filter((kind) => entry.kinds.has(kind)),
718
+ appliedAs: suggestionAppliedAs(entry.kinds)
719
+ });
720
+ return suggestions.toSorted((a, b) => (a.ranges[0]?.from ?? 0) - (b.ranges[0]?.from ?? 0));
721
+ }
722
+ /**
723
+ * The document range covering every mark belonging to `suggestionId`, plus the
724
+ * revision ids those marks carry. Returns null when the suggestion is absent
725
+ * (already accepted/rejected, or never existed).
726
+ */
727
+ function findSuggestionRange(state, suggestionId) {
728
+ const entry = collectSuggestions(state).get(suggestionId);
729
+ if (!entry || entry.segments.length === 0) return null;
730
+ const ranges = mergeSegments(entry.segments);
731
+ const from = ranges[0]?.from ?? 0;
732
+ return {
733
+ from,
734
+ to: ranges.at(-1)?.to ?? from,
735
+ revisionIds: [...entry.revisionIds]
736
+ };
737
+ }
738
+ /**
739
+ * Normalize an optional acceptance date to a canonical ISO 8601 string. Absent
740
+ * or unparseable input falls back to the current time — a malformed `w:date`
741
+ * must never reach the serialized document (fail-safe boundary validation).
742
+ */
743
+ const normalizeAcceptDate = (date) => {
744
+ if (date === void 0) return (/* @__PURE__ */ new Date()).toISOString();
745
+ const parsed = new Date(date);
746
+ return Number.isNaN(parsed.getTime()) ? (/* @__PURE__ */ new Date()).toISOString() : parsed.toISOString();
747
+ };
748
+ /**
749
+ * The node-attr patch that converts a suggested block/table revision into a
750
+ * normal (`"user"`) one authored by the accepting user. Returns `null` when the
751
+ * node carries no matching suggested revision. Position-stable (attr writes
752
+ * only), so callers can apply it during a single descendants walk.
753
+ */
754
+ const convertStructuralSuggestionAttrs = (node, author, date) => {
755
+ const attrs = node.attrs;
756
+ const userInfo = (marker) => {
757
+ const source = marker.info ?? marker;
758
+ return {
759
+ revisionId: source.revisionId,
760
+ author,
761
+ date,
762
+ ...typeof source.initials === "string" ? { initials: source.initials } : {}
763
+ };
764
+ };
765
+ const name = node.type.name;
766
+ const insertMarker = attrs["_suggestedInsert"];
767
+ if ((name === "paragraph" || name === "table") && typeof insertMarker === "object" && insertMarker !== null) {
768
+ const marker = insertMarker;
769
+ if (name === "table") return {
770
+ ...attrs,
771
+ _suggestedInsert: null
772
+ };
773
+ if (typeof marker.revisionId === "number") return {
774
+ ...attrs,
775
+ _suggestedInsert: null,
776
+ pPrMark: {
777
+ kind: "ins",
778
+ info: {
779
+ id: marker.revisionId,
780
+ author,
781
+ date,
782
+ ...typeof marker.initials === "string" ? { initials: marker.initials } : {}
783
+ }
784
+ }
785
+ };
786
+ }
787
+ if (node.type.name === "tableRow") {
788
+ if (readSuggestedMarker(attrs["trIns"])) return {
789
+ ...attrs,
790
+ trIns: userInfo(attrs["trIns"])
791
+ };
792
+ if (readSuggestedMarker(attrs["trDel"])) return {
793
+ ...attrs,
794
+ trDel: userInfo(attrs["trDel"])
795
+ };
796
+ }
797
+ if (node.type.name === "tableCell" || node.type.name === "tableHeader") {
798
+ const cellMarker = attrs["cellMarker"];
799
+ if (cellMarker && (cellMarker.kind === "ins" || cellMarker.kind === "del") && readSuggestedMarker(cellMarker.info)) return {
800
+ ...attrs,
801
+ cellMarker: {
802
+ kind: cellMarker.kind,
803
+ info: userInfo(cellMarker)
804
+ }
805
+ };
806
+ }
807
+ return null;
808
+ };
809
+ /**
810
+ * Convert suggested marks AND block/table node revisions to normal (`"user"`)
811
+ * tracked changes. `matchesSuggestion(id)` selects which suggestion to convert
812
+ * (one id, or all).
813
+ */
814
+ const acceptSuggestions = (matchesSuggestion, options) => {
815
+ return (state, dispatch) => {
816
+ const insertionType = state.schema.marks["insertion"];
817
+ const deletionType = state.schema.marks["deletion"];
818
+ const runPropertyChangeType = state.schema.marks["runPropertyChange"];
819
+ const date = normalizeAcceptDate(options.date);
820
+ const tr = state.tr;
821
+ let changed = false;
822
+ state.doc.descendants((node, pos) => {
823
+ const structural = readStructuralSuggestion(node);
824
+ if (structural && matchesSuggestion(structural.suggestionId)) {
825
+ const nextAttrs = convertStructuralSuggestionAttrs(node, options.author, date);
826
+ if (nextAttrs) {
827
+ tr.setNodeMarkup(pos, void 0, nextAttrs);
828
+ changed = true;
829
+ }
830
+ }
831
+ if (!node.isInline) return;
832
+ const from = pos;
833
+ const to = pos + node.nodeSize;
834
+ for (const mark of node.marks) {
835
+ if (!isSuggestionMark(mark) || typeof mark.attrs["suggestionId"] !== "string" || !matchesSuggestion(mark.attrs["suggestionId"])) continue;
836
+ changed = true;
837
+ tr.removeMark(from, to, mark);
838
+ if (mark.type === runPropertyChangeType) {
839
+ const nextChanges = [];
840
+ for (const change of expectRunPropertyChangeMarkAttrs(mark).changes) nextChanges.push({
841
+ ...change,
842
+ info: {
843
+ ...change.info,
844
+ author: options.author,
845
+ date
846
+ }
847
+ });
848
+ tr.addMark(from, to, mark.type.create({
849
+ changes: nextChanges,
850
+ provenance: "user",
851
+ suggestionId: null
852
+ }));
853
+ continue;
854
+ }
855
+ if (mark.type === insertionType || mark.type === deletionType) tr.addMark(from, to, mark.type.create({
856
+ ...mark.attrs,
857
+ author: options.author,
858
+ date,
859
+ provenance: "user",
860
+ suggestionId: null
861
+ }));
862
+ }
863
+ });
864
+ if (!changed) return false;
865
+ if (dispatch) dispatch(tr);
866
+ return true;
867
+ };
868
+ };
869
+ /**
870
+ * Accept one suggestion: convert its inline marks and block/table node
871
+ * revisions into normal tracked changes authored by `author`, keeping revision
872
+ * ids. A whole inserted table (which OOXML cannot track) is applied directly;
873
+ * everything else becomes a tracked change. See {@link getSuggestions} for the
874
+ * per-suggestion `appliedAs`.
875
+ */
876
+ function acceptSuggestion(suggestionId, options) {
877
+ return acceptSuggestions((id) => id === suggestionId, options);
878
+ }
879
+ /** Accept every suggestion in the document. */
880
+ function acceptAllSuggestions(options) {
881
+ return acceptSuggestions(() => true, options);
882
+ }
883
+ /**
884
+ * Reject one suggestion: inverse-apply it in a single transaction. Runs BOTH
885
+ * phases so a heterogeneous group (one `suggestionId` spanning whole-node
886
+ * inserts AND other edits) is fully resolved:
887
+ * 1. inline marks and suggested `trIns`/`trDel`/`cellMarker` revisions are
888
+ * inverse-applied through {@link resolveChange} (delete suggested-inserted
889
+ * text/rows/cells, drop suggested deletions/formatting);
890
+ * 2. any remaining whole-node inserts (paragraph/table) are then deleted,
891
+ * mapped through the accumulated transaction so positions stay valid.
892
+ *
893
+ * A paragraph whole-node insert carries inline marks with the same revision id,
894
+ * so phase 1 empties its text and phase 2 removes the now-empty node; a whole
895
+ * inserted table has no matching revision, so only phase 2 removes it.
896
+ */
897
+ function rejectSuggestion(suggestionId) {
898
+ return (state, dispatch) => {
899
+ const entry = collectSuggestions(state).get(suggestionId);
900
+ if (!entry) return false;
901
+ let tr = null;
902
+ if (entry.revisionIds.size > 0) {
903
+ const ranges = mergeSegments(entry.segments);
904
+ const from = ranges[0]?.from ?? 0;
905
+ resolveChange(from, ranges.at(-1)?.to ?? from, "reject", [...entry.revisionIds])(state, (resolved) => {
906
+ tr = resolved;
907
+ });
908
+ }
909
+ const workingTr = tr ?? state.tr;
910
+ const positions = [];
911
+ workingTr.doc.descendants((node, pos) => {
912
+ const structural = readStructuralSuggestion(node);
913
+ if (structural?.isNodeInsert && structural.suggestionId === suggestionId) positions.push(pos);
914
+ });
915
+ for (const pos of positions.toSorted((a, b) => b - a)) {
916
+ const node = workingTr.doc.nodeAt(pos);
917
+ if (node) workingTr.delete(pos, pos + node.nodeSize);
918
+ }
919
+ if (workingTr.steps.length === 0) return false;
920
+ if (dispatch) dispatch(workingTr);
921
+ return true;
922
+ };
923
+ }
924
+ /** Reject every suggestion in the document. */
925
+ function rejectAllSuggestions() {
926
+ return (state, dispatch) => {
927
+ const bySuggestion = collectSuggestions(state);
928
+ if (bySuggestion.size === 0) return false;
929
+ const revisionIds = /* @__PURE__ */ new Set();
930
+ for (const entry of bySuggestion.values()) for (const id of entry.revisionIds) revisionIds.add(id);
931
+ let tr = null;
932
+ if (revisionIds.size > 0) resolveChange(0, state.doc.content.size, "reject", [...revisionIds])(state, (resolved) => {
933
+ tr = resolved;
934
+ });
935
+ const workingTr = tr ?? state.tr;
936
+ const positions = [];
937
+ workingTr.doc.descendants((node, pos) => {
938
+ if (readStructuralSuggestion(node)?.isNodeInsert) positions.push(pos);
939
+ });
940
+ for (const pos of positions.toSorted((a, b) => b - a)) {
941
+ const node = workingTr.doc.nodeAt(pos);
942
+ if (node) workingTr.delete(pos, pos + node.nodeSize);
943
+ }
944
+ if (workingTr.steps.length === 0) return false;
945
+ if (dispatch) dispatch(workingTr);
946
+ return true;
947
+ };
948
+ }
949
+ /**
577
950
  * Find the tracked change mark range at a given cursor position.
578
951
  * If the cursor is inside a tracked change, returns the full extent
579
952
  * of that mark (expanding to cover all adjacent nodes with the same
@@ -738,4 +1111,4 @@ function findPreviousChange(state, startPos) {
738
1111
  return result.value;
739
1112
  }
740
1113
  //#endregion
741
- export { acceptAIEditRevision, acceptAllChanges, acceptChange, addCommentMark, findAIEditRevisionRange, findChangeAtPosition, findNextChange, findParagraphBoundaryChangeAtPosition, findPreviousChange, rejectAIEditRevision, rejectAllChanges, rejectChange, removeCommentMark };
1114
+ export { acceptAIEditRevision, acceptAllChanges, acceptAllSuggestions, acceptChange, acceptSuggestion, addCommentMark, findAIEditRevisionRange, findChangeAtPosition, findNextChange, findParagraphBoundaryChangeAtPosition, findPreviousChange, findSuggestionRange, getSuggestions, rejectAIEditRevision, rejectAllChanges, rejectAllSuggestions, rejectChange, rejectSuggestion, removeCommentMark };
@@ -1,7 +1,21 @@
1
1
  import { expectHyperlinkMarkAttrs } from "../attrs/index.js";
2
+ import { normalizeUserUrl } from "../../utils/urlSecurity.js";
2
3
  //#region src/prosemirror/commands/hyperlink.ts
3
4
  const hyperlinkHref = (mark) => expectHyperlinkMarkAttrs(mark).href;
4
5
  /**
6
+ * Normalize/sanitize a URL typed by a user (hyperlink popup edit) before it
7
+ * is written into a mark. Internal bookmark anchors (`#name`) bypass
8
+ * `normalizeUserUrl` (which parses via the `URL` constructor and would
9
+ * reject a bare fragment); protocol-less input (e.g. "example.com") is
10
+ * accepted and normalized to https; anything resolving to a disallowed
11
+ * scheme (javascript:, data:, file:, ...) is dropped to an empty href.
12
+ */
13
+ const normalizeHyperlinkInput = (rawHref) => {
14
+ const trimmed = rawHref.trim();
15
+ if (trimmed.startsWith("#")) return trimmed;
16
+ return normalizeUserUrl(trimmed);
17
+ };
18
+ /**
5
19
  * Contiguous ranges of text nodes in `parent` (starting at `parentStart`) that
6
20
  * carry a hyperlink mark with the given `href`.
7
21
  */
@@ -44,7 +58,7 @@ const editHyperlinkAtCursor = (view, { displayText, href }) => {
44
58
  if (!targetRange) return false;
45
59
  const { tooltip } = expectHyperlinkMarkAttrs(linkMark);
46
60
  const newMark = hlType.create({
47
- href,
61
+ href: normalizeHyperlinkInput(href),
48
62
  tooltip
49
63
  });
50
64
  const textNode = view.state.schema.text(displayText, [...$from.marks().filter((m) => m.type !== hlType), newMark]);
@@ -1,5 +1,6 @@
1
1
  import { IMAGE_HORIZONTAL_ALIGNMENT_VALUES, IMAGE_HORIZONTAL_RELATIVE_TO_VALUES, IMAGE_VERTICAL_ALIGNMENT_VALUES, IMAGE_VERTICAL_RELATIVE_TO_VALUES } from "../../types/documentEnumValues.js";
2
2
  import { expectImageAttrs, mergeImageAttrs } from "../attrs/index.js";
3
+ import { sanitizeImageSrc } from "../../utils/sanitizeImageSrc.js";
3
4
  import { isSafeImageFile } from "../../utils/imageValidation.js";
4
5
  //#region src/prosemirror/commands/image.ts
5
6
  /** Map a toolbar wrap selection to the image's wrapType/displayMode/cssFloat, or null when unknown. */
@@ -203,6 +204,11 @@ function readFileAsDataUrl(file) {
203
204
  }
204
205
  function loadImageDimensions(src) {
205
206
  return new Promise((resolve, reject) => {
207
+ const safeSrc = sanitizeImageSrc(src);
208
+ if (!safeSrc) {
209
+ reject(/* @__PURE__ */ new Error("Rejected unsafe image source"));
210
+ return;
211
+ }
206
212
  const img = new Image();
207
213
  img.addEventListener("load", () => {
208
214
  resolve({
@@ -213,7 +219,7 @@ function loadImageDimensions(src) {
213
219
  img.addEventListener("error", () => {
214
220
  reject(/* @__PURE__ */ new Error("Failed to load image"));
215
221
  });
216
- img.src = src;
222
+ img.src = safeSrc;
217
223
  });
218
224
  }
219
225
  //#endregion
@@ -1,4 +1,4 @@
1
- import { acceptAIEditRevision, acceptAllChanges, acceptChange, addCommentMark, findAIEditRevisionRange, findNextChange, findPreviousChange, rejectAIEditRevision, rejectAllChanges, rejectChange, removeCommentMark } from "./comments.js";
1
+ import { AcceptSuggestionOptions, FolioSuggestion, SuggestionAppliedAs, SuggestionKind, acceptAIEditRevision, acceptAllChanges, acceptAllSuggestions, acceptChange, acceptSuggestion, addCommentMark, findAIEditRevisionRange, findNextChange, findPreviousChange, findSuggestionRange, getSuggestions, rejectAIEditRevision, rejectAllChanges, rejectAllSuggestions, rejectChange, rejectSuggestion, removeCommentMark } from "./comments.js";
2
2
  import { PAINTABLE_MARK_NAMES, applyFormatMarks, captureFormatMarks } from "./formatPainter.js";
3
3
  import { clearFormatting, createRemoveMarkCommand, createSetMarkCommand, getMarkAttr, isMarkActive } from "../extensions/marks/markUtils.js";
4
4
  import { getHyperlinkAttrs, getSelectedText, isHyperlinkActive } from "../extensions/marks/HyperlinkExtension.js";
@@ -10,4 +10,4 @@ import { BorderPreset, TableBorderPreset, TableContextInfo, getTableContext, isI
10
10
  import { addColumnLeft, addColumnRight, addRowAbove, addRowBelow, applyTableStyle, autoFitContents, deleteColumn, deleteRow, deleteTable, distributeColumns, insertTable, mergeCells, removeTableBorders, selectColumn, selectRow, selectTable, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setInsideTableBorders, setOutsideTableBorders, setRowHeight, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, splitCell, toggleHeaderRow, toggleNoWrap } from "./table.js";
11
11
  import { insertPageBreak } from "./pageBreak.js";
12
12
  import { CLIPBOARD_READ_ERROR_EVENT, buildPlainTextSlice, pasteWithoutFormatting } from "./pastePlainText.js";
13
- export { type BorderPreset, CLIPBOARD_READ_ERROR_EVENT, PAINTABLE_MARK_NAMES, type ResolvedStyleAttrs, type TableBorderPreset, type TableContextInfo, acceptAIEditRevision, acceptAllChanges, acceptChange, addColumnLeft, addColumnRight, addCommentMark, addRowAbove, addRowBelow, addTabStop, alignCenter, alignJustify, alignLeft, alignRight, applyFormatMarks, applyStyle, applyTableStyle, autoFitContents, buildPlainTextSlice, captureFormatMarks, clearFontFamily, clearFontSize, clearFormatting, clearHighlight, clearStyle, clearTextColor, createRemoveMarkCommand, createSetMarkCommand, decreaseIndent, decreaseListLevel, deleteColumn, deleteRow, deleteTable, distributeColumns, doubleSpacing, findAIEditRevisionRange, findNextChange, findPreviousChange, generateTOC, getHyperlinkAttrs, getListInfo, getMarkAttr, getParagraphAlignment, getParagraphBidi, getSelectedText, getStyleId, getTableContext, increaseIndent, increaseListLevel, insertHyperlink, insertPageBreak, insertTable, isHyperlinkActive, isInList, isInTableCell as isInTable, isMarkActive, mergeCells, oneAndHalfSpacing, pasteWithoutFormatting, rejectAIEditRevision, rejectAllChanges, rejectChange, removeCommentMark, removeHyperlink, removeList, removeTabStop, removeTableBorders, selectColumn, selectRow, selectTable, setAlignment, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setFontFamily, setFontSize, setHighlight, setHyperlink, setIndentFirstLine, setIndentLeft, setIndentRight, setInsideTableBorders, setLineSpacing, setLtr, setOutsideTableBorders, setRowHeight, setRtl, setSpaceAfter, setSpaceBefore, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, setTextColor, setUnderlineStyle, singleSpacing, splitCell, toggleBidi, toggleBold, toggleBulletList, toggleHeaderRow, toggleItalic, toggleNoWrap, toggleNumberedList, toggleStrike, toggleSubscript, toggleSuperscript, toggleUnderline };
13
+ export { type AcceptSuggestionOptions, type BorderPreset, CLIPBOARD_READ_ERROR_EVENT, type FolioSuggestion, PAINTABLE_MARK_NAMES, type ResolvedStyleAttrs, type SuggestionAppliedAs, type SuggestionKind, type TableBorderPreset, type TableContextInfo, acceptAIEditRevision, acceptAllChanges, acceptAllSuggestions, acceptChange, acceptSuggestion, addColumnLeft, addColumnRight, addCommentMark, addRowAbove, addRowBelow, addTabStop, alignCenter, alignJustify, alignLeft, alignRight, applyFormatMarks, applyStyle, applyTableStyle, autoFitContents, buildPlainTextSlice, captureFormatMarks, clearFontFamily, clearFontSize, clearFormatting, clearHighlight, clearStyle, clearTextColor, createRemoveMarkCommand, createSetMarkCommand, decreaseIndent, decreaseListLevel, deleteColumn, deleteRow, deleteTable, distributeColumns, doubleSpacing, findAIEditRevisionRange, findNextChange, findPreviousChange, findSuggestionRange, generateTOC, getHyperlinkAttrs, getListInfo, getMarkAttr, getParagraphAlignment, getParagraphBidi, getSelectedText, getStyleId, getSuggestions, getTableContext, increaseIndent, increaseListLevel, insertHyperlink, insertPageBreak, insertTable, isHyperlinkActive, isInList, isInTableCell as isInTable, isMarkActive, mergeCells, oneAndHalfSpacing, pasteWithoutFormatting, rejectAIEditRevision, rejectAllChanges, rejectAllSuggestions, rejectChange, rejectSuggestion, removeCommentMark, removeHyperlink, removeList, removeTabStop, removeTableBorders, selectColumn, selectRow, selectTable, setAlignment, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setFontFamily, setFontSize, setHighlight, setHyperlink, setIndentFirstLine, setIndentLeft, setIndentRight, setInsideTableBorders, setLineSpacing, setLtr, setOutsideTableBorders, setRowHeight, setRtl, setSpaceAfter, setSpaceBefore, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, setTextColor, setUnderlineStyle, singleSpacing, splitCell, toggleBidi, toggleBold, toggleBulletList, toggleHeaderRow, toggleItalic, toggleNoWrap, toggleNumberedList, toggleStrike, toggleSubscript, toggleSuperscript, toggleUnderline };
@@ -4,10 +4,10 @@ import { getTableContext, isInTable as isInTableCell } from "../extensions/nodes
4
4
  import { getListInfo, isInList } from "../extensions/features/ListExtension.js";
5
5
  import { CLIPBOARD_READ_ERROR_EVENT, buildPlainTextSlice, pasteWithoutFormatting } from "./pastePlainText.js";
6
6
  import { getHyperlinkAttrs, getSelectedText, isHyperlinkActive } from "../extensions/marks/HyperlinkExtension.js";
7
- import { acceptAIEditRevision, acceptAllChanges, acceptChange, addCommentMark, findAIEditRevisionRange, findNextChange, findPreviousChange, rejectAIEditRevision, rejectAllChanges, rejectChange, removeCommentMark } from "./comments.js";
7
+ import { acceptAIEditRevision, acceptAllChanges, acceptAllSuggestions, acceptChange, acceptSuggestion, addCommentMark, findAIEditRevisionRange, findNextChange, findPreviousChange, findSuggestionRange, getSuggestions, rejectAIEditRevision, rejectAllChanges, rejectAllSuggestions, rejectChange, rejectSuggestion, removeCommentMark } from "./comments.js";
8
8
  import { addColumnLeft, addColumnRight, addRowAbove, addRowBelow, applyTableStyle, autoFitContents, deleteColumn, deleteRow, deleteTable, distributeColumns, insertTable, mergeCells, removeTableBorders, selectColumn, selectRow, selectTable, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setInsideTableBorders, setOutsideTableBorders, setRowHeight, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, splitCell, toggleHeaderRow, toggleNoWrap } from "./table.js";
9
9
  import { clearFontFamily, clearFontSize, clearHighlight, clearTextColor, insertHyperlink, removeHyperlink, setFontFamily, setFontSize, setHighlight, setHyperlink, setTextColor, setUnderlineStyle, toggleBold, toggleItalic, toggleStrike, toggleSubscript, toggleSuperscript, toggleUnderline } from "./formatting.js";
10
10
  import { addTabStop, alignCenter, alignJustify, alignLeft, alignRight, applyStyle, clearStyle, decreaseIndent, decreaseListLevel, doubleSpacing, generateTOC, increaseIndent, increaseListLevel, oneAndHalfSpacing, removeList, removeTabStop, setAlignment, setIndentFirstLine, setIndentLeft, setIndentRight, setLineSpacing, setLtr, setRtl, setSpaceAfter, setSpaceBefore, singleSpacing, toggleBidi, toggleBulletList, toggleNumberedList } from "./paragraph.js";
11
11
  import { PAINTABLE_MARK_NAMES, applyFormatMarks, captureFormatMarks } from "./formatPainter.js";
12
12
  import { insertPageBreak } from "./pageBreak.js";
13
- export { CLIPBOARD_READ_ERROR_EVENT, PAINTABLE_MARK_NAMES, acceptAIEditRevision, acceptAllChanges, acceptChange, addColumnLeft, addColumnRight, addCommentMark, addRowAbove, addRowBelow, addTabStop, alignCenter, alignJustify, alignLeft, alignRight, applyFormatMarks, applyStyle, applyTableStyle, autoFitContents, buildPlainTextSlice, captureFormatMarks, clearFontFamily, clearFontSize, clearFormatting, clearHighlight, clearStyle, clearTextColor, createRemoveMarkCommand, createSetMarkCommand, decreaseIndent, decreaseListLevel, deleteColumn, deleteRow, deleteTable, distributeColumns, doubleSpacing, findAIEditRevisionRange, findNextChange, findPreviousChange, generateTOC, getHyperlinkAttrs, getListInfo, getMarkAttr, getParagraphAlignment, getParagraphBidi, getSelectedText, getStyleId, getTableContext, increaseIndent, increaseListLevel, insertHyperlink, insertPageBreak, insertTable, isHyperlinkActive, isInList, isInTableCell as isInTable, isMarkActive, mergeCells, oneAndHalfSpacing, pasteWithoutFormatting, rejectAIEditRevision, rejectAllChanges, rejectChange, removeCommentMark, removeHyperlink, removeList, removeTabStop, removeTableBorders, selectColumn, selectRow, selectTable, setAlignment, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setFontFamily, setFontSize, setHighlight, setHyperlink, setIndentFirstLine, setIndentLeft, setIndentRight, setInsideTableBorders, setLineSpacing, setLtr, setOutsideTableBorders, setRowHeight, setRtl, setSpaceAfter, setSpaceBefore, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, setTextColor, setUnderlineStyle, singleSpacing, splitCell, toggleBidi, toggleBold, toggleBulletList, toggleHeaderRow, toggleItalic, toggleNoWrap, toggleNumberedList, toggleStrike, toggleSubscript, toggleSuperscript, toggleUnderline };
13
+ export { CLIPBOARD_READ_ERROR_EVENT, PAINTABLE_MARK_NAMES, acceptAIEditRevision, acceptAllChanges, acceptAllSuggestions, acceptChange, acceptSuggestion, addColumnLeft, addColumnRight, addCommentMark, addRowAbove, addRowBelow, addTabStop, alignCenter, alignJustify, alignLeft, alignRight, applyFormatMarks, applyStyle, applyTableStyle, autoFitContents, buildPlainTextSlice, captureFormatMarks, clearFontFamily, clearFontSize, clearFormatting, clearHighlight, clearStyle, clearTextColor, createRemoveMarkCommand, createSetMarkCommand, decreaseIndent, decreaseListLevel, deleteColumn, deleteRow, deleteTable, distributeColumns, doubleSpacing, findAIEditRevisionRange, findNextChange, findPreviousChange, findSuggestionRange, generateTOC, getHyperlinkAttrs, getListInfo, getMarkAttr, getParagraphAlignment, getParagraphBidi, getSelectedText, getStyleId, getSuggestions, getTableContext, increaseIndent, increaseListLevel, insertHyperlink, insertPageBreak, insertTable, isHyperlinkActive, isInList, isInTableCell as isInTable, isMarkActive, mergeCells, oneAndHalfSpacing, pasteWithoutFormatting, rejectAIEditRevision, rejectAllChanges, rejectAllSuggestions, rejectChange, rejectSuggestion, removeCommentMark, removeHyperlink, removeList, removeTabStop, removeTableBorders, selectColumn, selectRow, selectTable, setAlignment, setAllTableBorders, setCellBorder, setCellFillColor, setCellMargins, setCellTextDirection, setCellVerticalAlign, setFontFamily, setFontSize, setHighlight, setHyperlink, setIndentFirstLine, setIndentLeft, setIndentRight, setInsideTableBorders, setLineSpacing, setLtr, setOutsideTableBorders, setRowHeight, setRtl, setSpaceAfter, setSpaceBefore, setTableBorderColor, setTableBorderPreset, setTableBorderWidth, setTableBorders, setTableProperties, setTextColor, setUnderlineStyle, singleSpacing, splitCell, toggleBidi, toggleBold, toggleBulletList, toggleHeaderRow, toggleItalic, toggleNoWrap, toggleNumberedList, toggleStrike, toggleSubscript, toggleSuperscript, toggleUnderline };
@@ -1,5 +1,5 @@
1
- import { standaloneTableCellFromProseMirror } from "../conversion/fromProseDoc.js";
2
1
  import { standaloneTableCellToProseMirror } from "../conversion/toProseDoc.js";
2
+ import { standaloneTableCellFromProseMirror } from "../conversion/fromProseDoc.js";
3
3
  import { getTableCellMergeChange } from "../tableCellMergeRevision.js";
4
4
  import { Result } from "better-result";
5
5
  import { TableMap } from "prosemirror-tables";
@@ -1,3 +1,5 @@
1
+ import { seedRevisionIdsAbove } from "./plugins/revisionIds.js";
2
+ import { MAX_REVISION_ID } from "@stll/docx-core/model";
1
3
  //#region src/prosemirror/commentIdAllocator.ts
2
4
  /** Sentinel ID for a comment that hasn't been persisted yet (anchored to selection). */
3
5
  const PENDING_COMMENT_ID = -1;
@@ -9,8 +11,12 @@ const PENDING_COMMENT_ID = -1;
9
11
  function createCommentIdAllocator() {
10
12
  let nextId = 1;
11
13
  return {
12
- next: () => nextId++,
14
+ next: () => {
15
+ if (nextId > MAX_REVISION_ID) nextId = 1;
16
+ return nextId++;
17
+ },
13
18
  seedAbove(maxId) {
19
+ if (!Number.isInteger(maxId) || maxId < 0 || maxId >= MAX_REVISION_ID) return;
14
20
  if (maxId >= nextId) nextId = maxId + 1;
15
21
  }
16
22
  };
@@ -25,11 +31,15 @@ function createCommentIdAllocator() {
25
31
  */
26
32
  function seedCommentAllocator(allocator, comments, view) {
27
33
  let max = 0;
28
- for (const comment of comments ?? []) max = Math.max(max, comment.id);
34
+ for (const comment of comments ?? []) if (Number.isInteger(comment.id) && comment.id > max && comment.id <= MAX_REVISION_ID) max = comment.id;
29
35
  if (view) view.state.doc.descendants((node) => {
30
- for (const mark of node.marks) if (mark.attrs["revisionId"] != null) max = Math.max(max, mark.attrs["revisionId"]);
36
+ for (const mark of node.marks) {
37
+ const revisionId = mark.attrs["revisionId"];
38
+ if (typeof revisionId === "number" && Number.isInteger(revisionId) && revisionId > max && revisionId <= MAX_REVISION_ID) max = revisionId;
39
+ }
31
40
  });
32
41
  allocator.seedAbove(max);
42
+ seedRevisionIdsAbove(max);
33
43
  }
34
44
  //#endregion
35
45
  export { PENDING_COMMENT_ID, createCommentIdAllocator, seedCommentAllocator };