@stll/folio-core 0.54.0 → 0.54.2

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 (89) hide show
  1. package/dist/ai-edits/apply.d.ts +1 -1
  2. package/dist/ai-edits/apply.js +43 -31
  3. package/dist/ai-edits/headless.d.ts +1 -2
  4. package/dist/ai-edits/headless.js +12 -27
  5. package/dist/ai-edits/read.d.ts +1 -0
  6. package/dist/ai-edits/read.js +7 -2
  7. package/dist/ai-edits/table-mutation-plan.d.ts +2 -1
  8. package/dist/ai-edits/table-mutation-plan.js +39 -1
  9. package/dist/ai-suggestions/apply.d.ts +1 -1
  10. package/dist/ai-suggestions/apply.js +3 -2
  11. package/dist/compare/compare.js +2 -1
  12. package/dist/docx/bookmarkIds.d.ts +11 -0
  13. package/dist/docx/bookmarkIds.js +33 -0
  14. package/dist/docx/documentParser.d.ts +14 -1
  15. package/dist/docx/documentParser.js +17 -4
  16. package/dist/docx/drawingIdNormalization.js +23 -8
  17. package/dist/docx/ensureParaIds.d.ts +5 -0
  18. package/dist/docx/ensureParaIds.js +10 -2
  19. package/dist/docx/listNumberingInstances.js +11 -9
  20. package/dist/docx/noteIds.d.ts +4 -0
  21. package/dist/docx/noteIds.js +13 -0
  22. package/dist/docx/numberingIds.d.ts +14 -0
  23. package/dist/docx/numberingIds.js +16 -0
  24. package/dist/docx/numericIdAllocator.d.ts +12 -0
  25. package/dist/docx/numericIdAllocator.js +33 -0
  26. package/dist/docx/numericIdNormalization.d.ts +16 -0
  27. package/dist/docx/numericIdNormalization.js +174 -0
  28. package/dist/docx/paragraphPropertySource.js +1 -1
  29. package/dist/docx/paragraphTextBoxEnrichment.js +2 -1
  30. package/dist/docx/parser.js +17 -5
  31. package/dist/docx/rasterMime.d.ts +64 -0
  32. package/dist/docx/rasterMime.js +116 -0
  33. package/dist/docx/replyToComment.js +13 -6
  34. package/dist/docx/rezip.js +14 -7
  35. package/dist/docx/selectiveSave.js +10 -0
  36. package/dist/docx/serializer/partNamespaces.js +4 -2
  37. package/dist/docx/serializer/runSerializer.js +5 -4
  38. package/dist/docx/serializer/trackedChangeAttributes.js +7 -1
  39. package/dist/docx/server/build.js +14 -3
  40. package/dist/docx/server/createBilingualDocument.js +10 -9
  41. package/dist/docx/streamingXmlParser.d.ts +10 -1
  42. package/dist/docx/streamingXmlParser.js +88 -19
  43. package/dist/docx/unzip.d.ts +3 -1
  44. package/dist/docx/unzip.js +36 -16
  45. package/dist/docx/verticalMergeProjection.d.ts +17 -0
  46. package/dist/docx/verticalMergeProjection.js +120 -0
  47. package/dist/layout-bridge/convert/fixedTableColumnWidths.d.ts +6 -0
  48. package/dist/layout-bridge/convert/fixedTableColumnWidths.js +23 -0
  49. package/dist/layout-bridge/convert/tableConversion.js +4 -1
  50. package/dist/layout-painter/index.js +2 -5
  51. package/dist/paged-layout/editorScrollRoot.d.ts +15 -0
  52. package/dist/paged-layout/editorScrollRoot.js +72 -0
  53. package/dist/paged-layout/scrollToPmPosition.js +8 -22
  54. package/dist/prosemirror/commands/clearParagraphIndent.d.ts +6 -0
  55. package/dist/prosemirror/commands/clearParagraphIndent.js +25 -0
  56. package/dist/prosemirror/commands/comments.js +6 -1
  57. package/dist/prosemirror/commands/propertyChangeScope.js +4 -1
  58. package/dist/prosemirror/commands/resolveAllTableChanges.js +18 -24
  59. package/dist/prosemirror/commands/resolveParagraphProperties.js +13 -2
  60. package/dist/prosemirror/commands/tableCellMergeResolution.d.ts +8 -9
  61. package/dist/prosemirror/commands/tableCellMergeResolution.js +28 -25
  62. package/dist/prosemirror/commands/tableMergeFoldDecisions.d.ts +61 -0
  63. package/dist/prosemirror/commands/tableMergeFoldDecisions.js +79 -0
  64. package/dist/prosemirror/commentIdAllocator.d.ts +15 -22
  65. package/dist/prosemirror/commentIdAllocator.js +50 -25
  66. package/dist/prosemirror/conversion/toProseDoc.d.ts +1 -6
  67. package/dist/prosemirror/conversion/toProseDoc.js +2 -117
  68. package/dist/prosemirror/extensions/core/HistoryExtension.js +97 -5
  69. package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -1
  70. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +22 -28
  71. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +8 -6
  72. package/dist/prosemirror/listNumbering.js +14 -14
  73. package/dist/prosemirror/listRendering.d.ts +4 -2
  74. package/dist/prosemirror/listRendering.js +3 -4
  75. package/dist/prosemirror/paragraphPropertyCarry.js +2 -5
  76. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -19
  77. package/dist/prosemirror/plugins/revisionIds.js +110 -29
  78. package/dist/prosemirror/plugins/suggestionMode.d.ts +2 -2
  79. package/dist/prosemirror/plugins/suggestionMode.js +197 -45
  80. package/dist/prosemirror/plugins/templateDirectives.d.ts +6 -0
  81. package/dist/prosemirror/plugins/templateDirectives.js +21 -7
  82. package/dist/prosemirror/plugins/templatePreviewValues.js +14 -21
  83. package/dist/prosemirror/storyListNumbering.js +11 -9
  84. package/dist/prosemirror/textInput.d.ts +6 -1
  85. package/dist/prosemirror/textInput.js +9 -2
  86. package/dist/prosemirror/utils/visualLineNavigation.js +6 -8
  87. package/dist/utils/mergeDocumentContent.d.ts +2 -2
  88. package/dist/utils/mergeDocumentContent.js +11 -8
  89. package/package.json +3 -3
@@ -20,7 +20,7 @@ type FolioAIEditView = {
20
20
  /**
21
21
  * Fixed revision provenance, for callers whose output must be reproducible.
22
22
  * Both halves of the stamp travel together because either one left to the
23
- * ambient clock (`new Date()` for the date, a `Date.now()`-seeded cursor for
23
+ * ambient clock (`new Date()` for the date, the shared bounded allocator for
24
24
  * the ids) makes the produced package differ between two runs over the same
25
25
  * inputs. `idSeed` starts a contiguous range the batch allocates from, so it
26
26
  * must sit above every revision id the target document already carries.
@@ -21,6 +21,7 @@ import { carryParagraphProperties, paragraphLeftAfter, propertiesSetInBatch, rec
21
21
  import { directParagraphSpacing, paragraphSpacingAttrPatch, paragraphSpacingEqual, paragraphSpacingFromFormatting, withDirectParagraphSpacing } from "../prosemirror/paragraphSpacing.js";
22
22
  import { getDocumentNumbering, getDocumentNumberingInstanceIds } from "../prosemirror/plugins/documentNumbering.js";
23
23
  import { getDocumentStyleDefinitions, getDocumentStyleResolver } from "../prosemirror/plugins/documentStyleState.js";
24
+ import { claimRevisionIds, nextRevisionId, nextRevisionIdRange, reserveRevisionIds, seedRevisionIdsFromDoc } from "../prosemirror/plugins/revisionIds.js";
24
25
  import { rebaseParagraphRuns } from "../prosemirror/rebaseParagraphRuns.js";
25
26
  import { NON_INCLUSIVE_MARK_DISPOSITION, addCarriedMarks, annotatedReplacement, commentsAcrossBlockBoundary, commentsForTrackedInsertion, hasReplacedAnnotations, inheritedReplacementMarks, placeTrackedInsertion, surveyReplacedAnnotations } from "../prosemirror/replacedAnnotations.js";
26
27
  import { applyMarksToRunFormattingRepresentation, runFormattingInlineControlNodeName, selectRunFormattingCarrierRepresentations } from "../prosemirror/runFormattingInlineCarriers.js";
@@ -825,24 +826,6 @@ const applyTrackedInlineFormattingToRanges = ({ tr, schema, doc = tr.doc, ranges
825
826
  };
826
827
  };
827
828
  /**
828
- * Module-scoped monotonic counter for tracked-change revision ids.
829
- * Seeded once from `Date.now()` so ids are roughly time-ordered for
830
- * humans reading raw DOCX, then incremented per allocation. A bare
831
- * `Date.now()` seed per applyAIEditOperations call would collide
832
- * across batches that fire within the same millisecond (the panel's
833
- * Accept-all loop does exactly that — multiple calls in tight
834
- * succession).
835
- *
836
- * A batch without a revision stamp allocates from it as it writes and claims
837
- * each operation's ids once they are written, rather than reserving a guess
838
- * up front: how many revisions an operation records is known only once it has
839
- * recorded them (clearing a background records one run-property change per
840
- * carrier). The claim also comes before anything that may start another
841
- * batch, a comment-id callback or the dispatch, so batches in one realm never
842
- * share an id and follow one another without a gap.
843
- */
844
- let revisionIdCursor = Date.now() * 1e3;
845
- /**
846
829
  * Walk the live doc once and bucket every textblock by its
847
830
  * normalised text hash. Resolution then maps each snapshot anchor
848
831
  * to the live block at the same ordinal among same-hash siblings —
@@ -1486,6 +1469,13 @@ const finalParagraphPredecessor = (doc, at) => {
1486
1469
  for (let index = $emptied.index() - 1; index >= 0; index--) {
1487
1470
  const node = $emptied.parent.child(index);
1488
1471
  position -= node.nodeSize;
1472
+ if (node.type.spec["tableRole"] === "table" && node.childCount > 0) {
1473
+ let allRowsDeleted = true;
1474
+ node.forEach((row) => {
1475
+ allRowsDeleted &&= expectTableRowAttrs(row).trDel != null;
1476
+ });
1477
+ if (allRowsDeleted) continue;
1478
+ }
1489
1479
  if (node.type !== emptied.type) return null;
1490
1480
  if (isPlainDeletedPPrMark(node.attrs["pPrMark"]) && holdsOnlyDeletedContent(node)) continue;
1491
1481
  return {
@@ -1701,6 +1691,24 @@ const withRotatedAddedFinalBreaks = ({ tr, batchRevisionIds, revisionSeed, autho
1701
1691
  };
1702
1692
  collectSupersededDeletedBreaks(tr.doc, 0);
1703
1693
  for (const { preceding, inserted, deletedMark, precedingMark } of supersededDeletedBreaks) {
1694
+ const insertionRevisionId = addedBreakRevisionId(precedingMark);
1695
+ if (insertionRevisionId !== null) {
1696
+ const previous = tr.doc.nodeAt(preceding);
1697
+ const current = tr.doc.nodeAt(inserted);
1698
+ if (!previous || !current) panic("A shifted paragraph break lost its carrier");
1699
+ const previousFormatting = paragraphPropertiesOnReject(previous);
1700
+ const existing = expectParagraphAttrs(current)._propertyChanges;
1701
+ if (!hasSerializableParagraphPropertyChange(existing) && JSON.stringify(previousFormatting) !== JSON.stringify(paragraphPropertiesSnapshot(current))) tr.setNodeAttribute(inserted, "_propertyChanges", [...Array.isArray(existing) ? existing : [], {
1702
+ type: "paragraphPropertyChange",
1703
+ info: {
1704
+ id: insertionRevisionId,
1705
+ author,
1706
+ date,
1707
+ ...initials ? { initials } : {}
1708
+ },
1709
+ previousFormatting
1710
+ }]);
1711
+ }
1704
1712
  tr.setNodeAttribute(preceding, "pPrMark", precedingMark);
1705
1713
  tr.setNodeAttribute(inserted, "pPrMark", deletedMark);
1706
1714
  }
@@ -2035,6 +2043,7 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
2035
2043
  };
2036
2044
  };
2037
2045
  const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: requestedOperations, mode = "tracked-changes", author = "AI", initials, createCommentId, revisionStamp, revisionIdSeed, wordDiff, wordDiffMode = "bounded", tableTemplates, replacementBackground = "clear", undefinedStyles = "refuse" }) => {
2046
+ seedRevisionIdsFromDoc(view.state.doc);
2038
2047
  const applied = [];
2039
2048
  const skipped = [];
2040
2049
  const normalizations = [];
@@ -2091,7 +2100,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2091
2100
  id: operation.id,
2092
2101
  reason: "unsupportedBlock"
2093
2102
  })),
2094
- nextRevisionId: revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor
2103
+ nextRevisionId: revisionIdSeed ?? revisionStamp?.idSeed ?? nextRevisionId()
2095
2104
  };
2096
2105
  const liveBlocks = collectLiveBlocksByHash(view.state.doc);
2097
2106
  const liveBlocksByParaId = collectLiveBlocksByParaId(view.state.doc);
@@ -2201,14 +2210,14 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2201
2210
  item,
2202
2211
  operationId: item.operation.id,
2203
2212
  target: getTableMutationPlanTarget(item)
2204
- })));
2213
+ })), view.state.doc);
2205
2214
  skipped.push(...tablePlan.skipped);
2206
2215
  const executableResolved = tablePlan.executable;
2207
2216
  if (executableResolved.length === 0) return {
2208
2217
  applied,
2209
2218
  skipped,
2210
2219
  ...normalizations.length > 0 && { normalizations },
2211
- nextRevisionId: revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor
2220
+ nextRevisionId: revisionIdSeed ?? revisionStamp?.idSeed ?? nextRevisionId()
2212
2221
  };
2213
2222
  const moveSideCounts = /* @__PURE__ */ new Map();
2214
2223
  for (const { operation } of executableResolved) {
@@ -2243,17 +2252,20 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2243
2252
  const mergedPropertyTargets = [];
2244
2253
  const deferredNoopFinalDeletions = /* @__PURE__ */ new Set();
2245
2254
  const ownsSharedRevisionIdCursor = revisionIdSeed === void 0 && revisionStamp === void 0;
2246
- let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor;
2247
- /**
2248
- * Claim the shared ids below `nextRevisionId` for this batch. A seeded or
2249
- * stamped batch allocates from its own range and claims nothing.
2250
- */
2251
- const claimSharedRevisionIds = (nextRevisionId) => {
2252
- if (ownsSharedRevisionIdCursor) revisionIdCursor = Math.max(revisionIdCursor, nextRevisionId);
2255
+ let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? nextRevisionIdRange();
2256
+ let claimedThrough = revisionSeed;
2257
+ const claimSharedRevisionIds = (next) => {
2258
+ if (revisionIdSeed !== void 0) return;
2259
+ if (ownsSharedRevisionIdCursor) claimRevisionIds(claimedThrough, next);
2260
+ else reserveRevisionIds(claimedThrough, next);
2261
+ claimedThrough = next;
2253
2262
  };
2254
- /** Continue past any shared id a batch the host started meanwhile claimed. */
2263
+ /** Continue past ids a reentrant callback allocated in this realm. */
2255
2264
  const continueSharedRevisionIds = () => {
2256
- if (ownsSharedRevisionIdCursor) revisionSeed = Math.max(revisionSeed, revisionIdCursor);
2265
+ if (ownsSharedRevisionIdCursor && nextRevisionId() !== claimedThrough) {
2266
+ revisionSeed = nextRevisionIdRange();
2267
+ claimedThrough = revisionSeed;
2268
+ }
2257
2269
  };
2258
2270
  const date = revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
2259
2271
  const insertedColumnCounts = /* @__PURE__ */ new Map();
@@ -3713,7 +3725,7 @@ const previewFolioAIEditOperationsWithResult = (options) => {
3713
3725
  applied: result.applied.map(({ id }) => ({ id })),
3714
3726
  skipped: result.skipped,
3715
3727
  ...result.normalizations !== void 0 && { normalizations: result.normalizations },
3716
- nextRevisionId: options.revisionStamp?.idSeed ?? revisionIdCursor
3728
+ nextRevisionId: options.revisionStamp?.idSeed ?? nextRevisionId()
3717
3729
  },
3718
3730
  doc: previewView.state.doc,
3719
3731
  commentIds
@@ -179,7 +179,7 @@ type FolioDocxComparisonStoryProjection = {
179
179
  type FolioDocxComparisonProjection = {
180
180
  stories: readonly FolioDocxComparisonStoryProjection[];
181
181
  revisions: {
182
- highestId: number;
182
+ ids: readonly number[];
183
183
  present: boolean;
184
184
  };
185
185
  };
@@ -350,7 +350,6 @@ declare class FolioDocxReviewer {
350
350
  private readonly importedFooters;
351
351
  private readonly resolvedStoryExpectations;
352
352
  private readonly createdComments;
353
- private readonly usedCommentIds;
354
353
  /** Comments the body anchored when it was loaded, read as the editor projects it. */
355
354
  private readonly loadedMainCommentIds;
356
355
  private readonly documentOperationUndoEntries;
@@ -7,6 +7,7 @@ import { applyReplyThreadMarkers } from "../docx/commentReplyMarkers.js";
7
7
  import { rebindDrawingImageRelationship } from "../docx/drawingRelationships.js";
8
8
  import { getEndnoteText, getFootnoteText, isSeparatorEndnote, isSeparatorFootnote } from "../docx/footnoteParser.js";
9
9
  import { getHeaderFooterText } from "../docx/headerFooterParser.js";
10
+ import { createNumberingIdAllocator } from "../docx/numberingIds.js";
10
11
  import { getCachedNumberingMap } from "../docx/numberingParser.js";
11
12
  import { recreateProseNodeWithParagraphPropertySource, transferProseParagraphPropertySource } from "../docx/paragraphPropertySource.js";
12
13
  import { parseDocx } from "../docx/parser.js";
@@ -20,6 +21,7 @@ import { captureSectionReferenceInventory, resolvedSectionReferenceLosses, withS
20
21
  import { expectCharacterStyleMarkAttrs, expectFootnoteRefMarkAttrs } from "../prosemirror/attrs/index.js";
21
22
  import { acceptAIEditRevision, acceptSuggestion, getSuggestions, rejectAIEditRevision, rejectAllSuggestions, rejectSuggestion, resolveAllChangesInHeadlessState, suggestionIdOfRevision } from "../prosemirror/commands/comments.js";
22
23
  import { sectionRejectProperties } from "../prosemirror/commands/propertyChangeScope.js";
24
+ import { allocateCommentId, createCommentIdAllocator, seedCommentAllocator } from "../prosemirror/commentIdAllocator.js";
23
25
  import { proseDocToBlocks, updateDocumentContent } from "../prosemirror/conversion/fromProseDoc.js";
24
26
  import { footnoteToProseDoc, headerFooterToProseDoc, toProseDoc } from "../prosemirror/conversion/toProseDoc.js";
25
27
  import { ensureBaseDirectionInState } from "../prosemirror/extensions/features/AutoBidiDetectionExtension.js";
@@ -68,14 +70,6 @@ import JSZip from "jszip";
68
70
  *
69
71
  * Scope: main, header, footer, footnote, and endnote blocks.
70
72
  */
71
- /**
72
- * Standalone comment-id allocator for the reviewer. The React editor uses
73
- * `Date.now()`-seeded ids from `commentsHelpers`; that module is DOM-bound
74
- * (`EditorView`, `findBodyPmAnchors`) and unimportable here, so the reviewer
75
- * mints its own. Seeded once per realm and incremented so ids stay unique
76
- * across reviewers created within the same millisecond.
77
- */
78
- let commentIdCursor = Date.now();
79
73
  let undoHandleCursor = Date.now();
80
74
  const createReviewerComment = ({ id, text, author, date }) => ({
81
75
  id,
@@ -399,7 +393,6 @@ var FolioDocxReviewer = class FolioDocxReviewer {
399
393
  importedFooters = /* @__PURE__ */ new Map();
400
394
  resolvedStoryExpectations = /* @__PURE__ */ new Map();
401
395
  createdComments = [];
402
- usedCommentIds;
403
396
  /** Comments the body anchored when it was loaded, read as the editor projects it. */
404
397
  loadedMainCommentIds;
405
398
  documentOperationUndoEntries = [];
@@ -418,7 +411,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
418
411
  this.originalBuffer = args.originalBuffer;
419
412
  this.state = args.state;
420
413
  this.author = args.author;
421
- this.usedCommentIds = new Set((args.baseDocument.package.document.comments ?? []).map(({ id }) => id));
414
+ seedCommentAllocator(createCommentIdAllocator(), args.baseDocument.package.document.comments, { state: args.state });
422
415
  this.loadedMainCommentIds = anchoredCommentIdsInProseDoc(args.state.doc);
423
416
  comparisonAccessByReviewer.set(this, Object.freeze({
424
417
  stageTerminalTableReviewCarrier: (target) => this.stageTerminalTableReviewCarrier(target),
@@ -571,8 +564,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
571
564
  const targetAbstracts = new Map(target.abstractNums.map((entry) => [entry.abstractNumId, entry]));
572
565
  const levelsByNumId = referencedNumberingLevelsByNumId({ references });
573
566
  const importedAbstractIds = /* @__PURE__ */ new Map();
574
- let nextAbstractId = 0;
575
- for (const id of abstracts.keys()) nextAbstractId = Math.max(nextAbstractId, id + 1);
567
+ const abstractIds = createNumberingIdAllocator("abstract", [...abstracts.keys(), ...targetAbstracts.keys()]);
576
568
  for (const numId of levelsByNumId.keys()) {
577
569
  const targetNum = targetNums.get(numId);
578
570
  if (!targetNum) return "conflict";
@@ -592,8 +584,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
592
584
  if (abstractNumId === void 0) {
593
585
  abstractNumId = targetAbstract.abstractNumId;
594
586
  const collision = abstracts.get(abstractNumId);
595
- if (collision && canonicalJson(collision) !== canonicalJson(targetAbstract)) abstractNumId = nextAbstractId++;
596
- nextAbstractId = Math.max(nextAbstractId, abstractNumId + 1);
587
+ if (collision && canonicalJson(collision) !== canonicalJson(targetAbstract)) abstractNumId = abstractIds.next();
597
588
  importedAbstractIds.set(targetAbstract.abstractNumId, abstractNumId);
598
589
  abstracts.set(abstractNumId, {
599
590
  ...targetAbstract,
@@ -628,9 +619,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
628
619
  const targetNums = new Map(target.nums.map((entry) => [entry.numId, entry]));
629
620
  const targetAbstracts = new Map(target.abstractNums.map((entry) => [entry.abstractNumId, entry]));
630
621
  const levelsByNumId = referencedNumberingLevelsByNumId({ references });
631
- let nextNumId = 0;
632
- for (const id of nums.keys()) nextNumId = Math.max(nextNumId, id + 1);
633
- for (const id of targetNums.keys()) nextNumId = Math.max(nextNumId, id + 1);
622
+ const numberingIds = createNumberingIdAllocator("num", [...nums.keys(), ...targetNums.keys()]);
634
623
  const remapped = /* @__PURE__ */ new Map();
635
624
  for (const numId of levelsByNumId.keys()) {
636
625
  const targetNum = targetNums.get(numId);
@@ -642,7 +631,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
642
631
  target,
643
632
  numId,
644
633
  levelsByNumId
645
- })) remapped.set(numId, nextNumId++);
634
+ })) remapped.set(numId, numberingIds.next());
646
635
  }
647
636
  return remapped;
648
637
  }
@@ -759,18 +748,18 @@ var FolioDocxReviewer = class FolioDocxReviewer {
759
748
  }
760
749
  projectComparisonStoriesInternal(mode) {
761
750
  const handles = this.listStoryHandlesInternal();
762
- let highestId = 0;
751
+ const ids = /* @__PURE__ */ new Set();
763
752
  let present = false;
764
753
  if (mode === "with-revision-census") {
765
754
  for (const change of this.currentFinalSectionProperties()?.propertyChanges ?? []) {
766
- highestId = Math.max(highestId, change.info.id);
755
+ ids.add(change.info.id);
767
756
  present = true;
768
757
  }
769
758
  for (const handle of handles) {
770
759
  const state = this.getEditableStoryState(handle);
771
760
  if (!state) continue;
772
761
  const stats = getTrackedChangeStatsFromDoc(state.doc);
773
- highestId = Math.max(highestId, stats.highestId);
762
+ for (const id of stats.ids) ids.add(id);
774
763
  present ||= stats.present;
775
764
  }
776
765
  }
@@ -785,7 +774,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
785
774
  return {
786
775
  stories,
787
776
  revisions: {
788
- highestId,
777
+ ids: [...ids],
789
778
  present
790
779
  }
791
780
  };
@@ -1231,7 +1220,6 @@ var FolioDocxReviewer = class FolioDocxReviewer {
1231
1220
  });
1232
1221
  if (!reply) return null;
1233
1222
  this.createdComments.push(reply);
1234
- this.usedCommentIds.add(reply.id);
1235
1223
  this.anchorReplyInEditor({
1236
1224
  parentId: reply.parentId ?? parentId,
1237
1225
  replyId: reply.id
@@ -2186,10 +2174,7 @@ var FolioDocxReviewer = class FolioDocxReviewer {
2186
2174
  }
2187
2175
  /** Allocate outside every parsed or newly-created comment and reply id. */
2188
2176
  nextCommentId() {
2189
- while (this.usedCommentIds.has(commentIdCursor)) commentIdCursor += 1;
2190
- const id = commentIdCursor++;
2191
- this.usedCommentIds.add(id);
2192
- return id;
2177
+ return allocateCommentId();
2193
2178
  }
2194
2179
  /**
2195
2180
  * Drive a ProseMirror command against the reviewer's headless state via the
@@ -76,6 +76,7 @@ declare const getTrackedChangeGroupIdsFromDoc: (doc: Node, id: number) => number
76
76
  declare const getTrackedChangeStatsFromDoc: (doc: Node) => {
77
77
  highestId: number;
78
78
  present: boolean;
79
+ ids: readonly number[];
79
80
  };
80
81
  /**
81
82
  * The comment anchors present in the body, read from the `comment` mark the
@@ -313,11 +313,16 @@ const getTrackedChangeGroupIdsFromDoc = (doc, id) => getTrackedChangeGroupsFromP
313
313
  /** @internal Read revision statistics without paying for an unrelated block projection. */
314
314
  const getTrackedChangeStatsFromDoc = (doc) => {
315
315
  let highestId = 0;
316
+ const revisionIds = /* @__PURE__ */ new Set();
316
317
  const groups = getTrackedChangeGroupsFromProjectedDoc(doc, null);
317
- for (const { ids } of groups) for (const id of ids) highestId = Math.max(highestId, id);
318
+ for (const { ids } of groups) for (const id of ids) {
319
+ highestId = Math.max(highestId, id);
320
+ revisionIds.add(id);
321
+ }
318
322
  return {
319
323
  highestId,
320
- present: groups.length > 0
324
+ present: groups.length > 0,
325
+ ids: [...revisionIds]
321
326
  };
322
327
  };
323
328
  /**
@@ -1,3 +1,4 @@
1
+ import { Node } from "prosemirror-model";
1
2
  //#region src/ai-edits/table-mutation-plan.d.ts
2
3
  type TableRectangle = {
3
4
  left: number;
@@ -32,7 +33,7 @@ type TableMutationPlan<T> = {
32
33
  executable: T[];
33
34
  skipped: TableMutationPlanSkip[];
34
35
  };
35
- declare const planTableMutations: <T>(candidates: readonly TableMutationPlanCandidate<T>[]) => TableMutationPlan<T>;
36
+ declare const planTableMutations: <T>(candidates: readonly TableMutationPlanCandidate<T>[], doc: Node) => TableMutationPlan<T>;
36
37
  declare const tableRectanglesEqual: (left: TableRectangle, right: TableRectangle) => boolean;
37
38
  //#endregion
38
39
  export { TableMutationPlanTarget, TableRectangle, planTableMutations, tableRectanglesEqual };
@@ -1,5 +1,7 @@
1
+ import { panic } from "better-result";
2
+ import { TableMap } from "prosemirror-tables";
1
3
  //#region src/ai-edits/table-mutation-plan.ts
2
- const planTableMutations = (candidates) => {
4
+ const planTableMutations = (candidates, doc) => {
3
5
  const tableStructureMutations = /* @__PURE__ */ new Set();
4
6
  const mergeTables = /* @__PURE__ */ new Set();
5
7
  const splitTables = /* @__PURE__ */ new Set();
@@ -44,6 +46,17 @@ const planTableMutations = (candidates) => {
44
46
  });
45
47
  continue;
46
48
  }
49
+ if (claimedRectangles.length > 0 && mergesRemoveRow({
50
+ doc,
51
+ tablePosition: target.tablePosition,
52
+ rectangles: [...claimedRectangles, target.rectangle]
53
+ })) {
54
+ skipped.push({
55
+ id: candidate.operationId,
56
+ reason: "unsupportedBlock"
57
+ });
58
+ continue;
59
+ }
47
60
  claimedRectangles.push(target.rectangle);
48
61
  mergeRectanglesByTable.set(target.tablePosition, claimedRectangles);
49
62
  executable.push(candidate.item);
@@ -84,6 +97,31 @@ const planTableMutations = (candidates) => {
84
97
  skipped
85
98
  };
86
99
  };
100
+ /**
101
+ * Disjoint merges can still share a row's survival. Direct merging closes a
102
+ * row left without cells, invalidating other rectangles' original row indices;
103
+ * tracked merging cannot record that removal. Keep a row-removing merge alone
104
+ * and refuse the later merge when their combined folded cells empty a row.
105
+ */
106
+ const mergesRemoveRow = ({ doc, tablePosition, rectangles }) => {
107
+ const table = doc.nodeAt(tablePosition);
108
+ if (table?.type.spec["tableRole"] !== "table") return panic("A planned cell merge lost its table", { position: tablePosition });
109
+ const map = TableMap.get(table);
110
+ const folded = /* @__PURE__ */ new Set();
111
+ for (const rectangle of rectangles) {
112
+ const kept = map.map[rectangle.top * map.width + rectangle.left];
113
+ for (const position of map.cellsInRect(rectangle)) if (position !== kept) folded.add(position);
114
+ }
115
+ let removesRow = false;
116
+ table.forEach((row, rowOffset) => {
117
+ let remaining = row.childCount;
118
+ row.forEach((_cell, cellOffset) => {
119
+ if (folded.has(rowOffset + 1 + cellOffset)) remaining--;
120
+ });
121
+ if (row.childCount > 0 && remaining === 0) removesRow = true;
122
+ });
123
+ return removesRow;
124
+ };
87
125
  const tableRectanglesOverlap = (left, right) => left.left < right.right && right.left < left.right && left.top < right.bottom && right.top < left.bottom;
88
126
  const tableRectanglesEqual = (left, right) => left.left === right.left && left.top === right.top && left.right === right.right && left.bottom === right.bottom;
89
127
  //#endregion
@@ -6,7 +6,7 @@ type ApplyResult = {
6
6
  stale: string[];
7
7
  };
8
8
  type ApplyOptions = {
9
- view: EditorView;
9
+ view: Pick<EditorView, "state" | "dispatch">;
10
10
  suggestions: AISuggestion[];
11
11
  mode: AISuggestionApplyMode;
12
12
  author: string;
@@ -1,3 +1,4 @@
1
+ import { mintRevisionId, seedRevisionIdsFromDoc } from "../prosemirror/plugins/revisionIds.js";
1
2
  import { resolveSuggestionAnchor } from "./conflict.js";
2
3
  //#region src/ai-suggestions/apply.ts
3
4
  function applySuggestions(options) {
@@ -39,10 +40,10 @@ function applySuggestions(options) {
39
40
  stale: [...stale, ...resolved.map((r) => r.suggestion.id)]
40
41
  };
41
42
  const date = (/* @__PURE__ */ new Date()).toISOString();
42
- let revisionSeed = Date.now();
43
+ seedRevisionIdsFromDoc(view.state.doc);
43
44
  for (const { suggestion, from, to } of resolved) {
44
45
  const attrs = {
45
- revisionId: revisionSeed++,
46
+ revisionId: mintRevisionId(),
46
47
  author,
47
48
  date
48
49
  };
@@ -10,6 +10,7 @@ import { isAltChunkMarkup } from "../docx/altChunk.js";
10
10
  import { isOpaqueNestedRowMarkup, opaqueRevisionCarrierName } from "../docx/opaqueCarrier.js";
11
11
  import { sectionReferenceHistory } from "../docx/sectionReferenceHistory.js";
12
12
  import { sameCanonicalInlinePresentation } from "../internal/compare/inline-presentation.js";
13
+ import { revisionIdSeedAbove } from "../prosemirror/plugins/revisionIds.js";
13
14
  import { createStyleResolver } from "../prosemirror/styles/styleResolver.js";
14
15
  import { canonicalJson } from "../utils/canonicalJson.js";
15
16
  import { createContentComparisonWorkSession } from "./content.js";
@@ -386,7 +387,7 @@ const parseComparison = async (base, target, options) => {
386
387
  reviewer,
387
388
  revisionStamp: {
388
389
  date: options.timestamp,
389
- idSeed: baseProjection.revisions.highestId + 1
390
+ idSeed: revisionIdSeedAbove(baseProjection.revisions.ids)
390
391
  },
391
392
  packageDate,
392
393
  pairs: comparisonPairs,
@@ -0,0 +1,11 @@
1
+ import { Node } from "prosemirror-model";
2
+ //#region src/docx/bookmarkIds.d.ts
3
+ declare const reserveBookmarkIds: (ids: Iterable<number>) => void;
4
+ declare const mintBookmarkId: () => number;
5
+ declare const createBookmarkIdAllocator: (existingIds: Iterable<number>) => {
6
+ reserve: (ids: Iterable<number>) => void;
7
+ next: () => number;
8
+ };
9
+ declare const reserveProseBookmarkIds: (doc: Node) => void;
10
+ //#endregion
11
+ export { createBookmarkIdAllocator, mintBookmarkId, reserveBookmarkIds, reserveProseBookmarkIds };
@@ -0,0 +1,33 @@
1
+ import { createNumericIdAllocator } from "./numericIdAllocator.js";
2
+ //#region src/docx/bookmarkIds.ts
3
+ let allocator;
4
+ const getAllocator = () => allocator ??= createNumericIdAllocator({
5
+ space: "bookmark",
6
+ firstId: 0
7
+ });
8
+ const reserveBookmarkIds = (ids) => getAllocator().reserve(ids);
9
+ const mintBookmarkId = () => getAllocator().next();
10
+ const createBookmarkIdAllocator = (existingIds) => {
11
+ const scopedAllocator = createNumericIdAllocator({
12
+ space: "bookmark",
13
+ firstId: 0
14
+ });
15
+ scopedAllocator.reserve(existingIds);
16
+ return scopedAllocator;
17
+ };
18
+ const reserveProseBookmarkIds = (doc) => {
19
+ const ids = [];
20
+ doc.descendants((node) => {
21
+ const bookmarks = node.attrs["bookmarks"];
22
+ if (Array.isArray(bookmarks)) {
23
+ for (const bookmark of bookmarks) if (typeof bookmark === "object" && bookmark !== null && "id" in bookmark && typeof bookmark.id === "number") ids.push(bookmark.id);
24
+ }
25
+ if (node.type.name === "bookmarkBoundary" || node.type.name === "blockBookmarkBoundary") {
26
+ const id = node.attrs["id"];
27
+ if (typeof id === "number") ids.push(id);
28
+ }
29
+ });
30
+ reserveBookmarkIds(ids);
31
+ };
32
+ //#endregion
33
+ export { createBookmarkIdAllocator, mintBookmarkId, reserveBookmarkIds, reserveProseBookmarkIds };
@@ -1,6 +1,7 @@
1
1
  import { document_d_exports } from "../types/document.js";
2
2
  import { NumberingMap } from "./numberingParser.js";
3
3
  import { ParseContext } from "./parseContext.js";
4
+ import { XmlElement } from "./xmlParser.js";
4
5
  import { PreviewLedger } from "./previewBudget.js";
5
6
  import { StyleMap } from "./styleParser.js";
6
7
  //#region src/docx/documentParser.d.ts
@@ -33,6 +34,18 @@ declare function extractAllTemplateVariables(content: document_d_exports.BlockCo
33
34
  * @returns DocumentBody with content, sections, and template variables
34
35
  */
35
36
  declare function parseDocumentBody(xml: string, styles?: StyleMap | null, theme?: document_d_exports.Theme | null, numbering?: NumberingMap | null, rels?: document_d_exports.RelationshipMap | null, media?: Map<string, document_d_exports.MediaFile> | null, context?: ParseContext, previews?: PreviewLedger): document_d_exports.DocumentBody;
37
+ type ParsedDocumentBodyOptions = {
38
+ doc: XmlElement;
39
+ styles: StyleMap | null;
40
+ theme: document_d_exports.Theme | null;
41
+ numbering: NumberingMap | null;
42
+ rels: document_d_exports.RelationshipMap | null;
43
+ media: Map<string, document_d_exports.MediaFile> | null;
44
+ context?: ParseContext | undefined;
45
+ previews: PreviewLedger;
46
+ };
47
+ /** Consume the same tree the import identity pass repaired. */
48
+ declare const parseDocumentBodyTree: ({ doc, styles, theme, numbering, rels, media, context, previews }: ParsedDocumentBodyOptions) => document_d_exports.DocumentBody;
36
49
  /**
37
50
  * Get all paragraphs from document body (flattened)
38
51
  */
@@ -66,4 +79,4 @@ declare function getSectionCount(body: document_d_exports.DocumentBody): number;
66
79
  */
67
80
  declare function hasTemplateVariables(body: document_d_exports.DocumentBody): boolean;
68
81
  //#endregion
69
- export { extractAllTemplateVariables, extractTemplateVariables, getAllParagraphs, getAllTables, getCharacterCount, getDocumentText, getParagraphCount, getSectionCount, getWordCount, hasTemplateVariables, parseDocumentBody };
82
+ export { extractAllTemplateVariables, extractTemplateVariables, getAllParagraphs, getAllTables, getCharacterCount, getDocumentText, getParagraphCount, getSectionCount, getWordCount, hasTemplateVariables, parseDocumentBody, parseDocumentBodyTree };
@@ -151,9 +151,22 @@ function parseDocumentBackground(documentEl, context) {
151
151
  * @returns DocumentBody with content, sections, and template variables
152
152
  */
153
153
  function parseDocumentBody(xml, styles = null, theme = null, numbering = null, rels = null, media = null, context, previews = standalonePreviewLedger()) {
154
+ if (!xml) return { content: [] };
155
+ return parseDocumentBodyTree({
156
+ doc: parseXml(xml),
157
+ styles,
158
+ theme,
159
+ numbering,
160
+ rels,
161
+ media,
162
+ context,
163
+ previews
164
+ });
165
+ }
166
+ /** Consume the same tree the import identity pass repaired. */
167
+ const parseDocumentBodyTree = ({ doc, styles, theme, numbering, rels, media, context, previews }) => {
154
168
  const result = { content: [] };
155
- if (!xml) return result;
156
- const documentEl = (parseXml(xml).elements ?? []).find((el) => el.type === "element" && getLocalName(el.name ?? "") === "document");
169
+ const documentEl = (doc.elements ?? []).find((el) => el.type === "element" && getLocalName(el.name ?? "") === "document");
157
170
  if (!documentEl) return result;
158
171
  const background = parseDocumentBackground(documentEl, context);
159
172
  if (background !== void 0) result.background = background;
@@ -169,7 +182,7 @@ function parseDocumentBody(xml, styles = null, theme = null, numbering = null, r
169
182
  canonicalizeLeadingBodySectionProperties(bodyEl, result);
170
183
  result.sections = buildSections(result.content, result.finalSectionProperties);
171
184
  return result;
172
- }
185
+ };
173
186
  /**
174
187
  * Get all paragraphs from document body (flattened)
175
188
  */
@@ -290,4 +303,4 @@ function hasTemplateVariables(body) {
290
303
  return extractAllTemplateVariables(body.content).length > 0;
291
304
  }
292
305
  //#endregion
293
- export { extractAllTemplateVariables, extractTemplateVariables, getAllParagraphs, getAllTables, getCharacterCount, getDocumentText, getParagraphCount, getSectionCount, getWordCount, hasTemplateVariables, parseDocumentBody };
306
+ export { extractAllTemplateVariables, extractTemplateVariables, getAllParagraphs, getAllTables, getCharacterCount, getDocumentText, getParagraphCount, getSectionCount, getWordCount, hasTemplateVariables, parseDocumentBody, parseDocumentBodyTree };
@@ -4,6 +4,7 @@ import { toTransitionalNamespaceUri } from "./transitionalSpelling.js";
4
4
  import { captureVerbatimXml } from "./verbatimCapture.js";
5
5
  import { NAMESPACES, OOXML_NAMESPACE_SCOPE, getLocalName, getNamespaceUri, parseXml } from "./xmlParser.js";
6
6
  import { panic } from "better-result";
7
+ import { assertValidOoxmlNumericId, isValidOoxmlNumericId } from "@stll/docx-core";
7
8
  //#region src/docx/drawingIdNormalization.ts
8
9
  const GENERATED_DRAWING_ID_START = 1e5;
9
10
  const isDetachedRawDrawing = (drawing) => drawing?.rawXml !== void 0 && isNewDataUrlDrawing(drawing);
@@ -27,7 +28,7 @@ const drawingXmlWithoutIdentity = (xml) => reassignRawDrawingId({
27
28
  xml,
28
29
  id: "0"
29
30
  }) ?? xml;
30
- const needsGeneratedId = ({ id }) => id === void 0 || id === "" || id === "0";
31
+ const needsGeneratedId = ({ id }) => id === void 0 || !isValidOoxmlNumericId(id, "unsigned32");
31
32
  const normalizeDrawingIds = (surfaces) => {
32
33
  const entries = [];
33
34
  visitDocxParagraphs(surfaces, (paragraph) => {
@@ -48,22 +49,36 @@ const normalizeDrawingIds = (surfaces) => {
48
49
  }
49
50
  });
50
51
  });
51
- const usedIds = new Set(entries.flatMap(({ drawing, rawDrawing }) => needsGeneratedId(drawing) || isDetachedRawDrawing(rawDrawing) ? [] : [drawing.id]));
52
+ const usedIds = new Set(entries.flatMap(({ drawing, rawDrawing }) => needsGeneratedId(drawing) || isDetachedRawDrawing(rawDrawing) ? [] : [Number(drawing.id)]));
52
53
  let nextId = GENERATED_DRAWING_ID_START;
53
54
  for (const { drawing, generateWhenMissing } of entries) {
54
55
  if (!generateWhenMissing || !needsGeneratedId(drawing)) continue;
55
- while (usedIds.has(String(nextId))) nextId += 1;
56
+ while (usedIds.has(nextId)) nextId += 1;
57
+ assertValidOoxmlNumericId({
58
+ value: nextId,
59
+ domain: "unsigned32",
60
+ partPath: "drawing normalization",
61
+ elementName: "docPr",
62
+ attributeName: "id"
63
+ });
56
64
  drawing.id = String(nextId);
57
- usedIds.add(drawing.id);
65
+ usedIds.add(nextId);
58
66
  nextId += 1;
59
67
  }
60
68
  for (const { drawing, rawDrawing } of entries) {
61
69
  if (!isDetachedRawDrawing(rawDrawing)) continue;
62
- if (!needsGeneratedId(drawing) && !usedIds.has(drawing.id)) {
63
- usedIds.add(drawing.id);
70
+ if (!needsGeneratedId(drawing) && !usedIds.has(Number(drawing.id))) {
71
+ usedIds.add(Number(drawing.id));
64
72
  continue;
65
73
  }
66
- while (usedIds.has(String(nextId))) nextId += 1;
74
+ while (usedIds.has(nextId)) nextId += 1;
75
+ assertValidOoxmlNumericId({
76
+ value: nextId,
77
+ domain: "unsigned32",
78
+ partPath: "drawing normalization",
79
+ elementName: "docPr",
80
+ attributeName: "id"
81
+ });
67
82
  const sourceXml = rawDrawing.rawXml;
68
83
  if (sourceXml === void 0) panic("Detached raw drawing must retain its XML.");
69
84
  const rawXml = reassignRawDrawingId({
@@ -73,7 +88,7 @@ const normalizeDrawingIds = (surfaces) => {
73
88
  if (rawXml === null) panic("Detached raw drawing must contain wp:docPr.");
74
89
  drawing.id = String(nextId);
75
90
  rawDrawing.rawXml = rawXml;
76
- usedIds.add(drawing.id);
91
+ usedIds.add(nextId);
77
92
  nextId += 1;
78
93
  }
79
94
  };