@stll/folio-core 0.54.1 → 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 (69) hide show
  1. package/dist/ai-edits/apply.d.ts +1 -1
  2. package/dist/ai-edits/apply.js +18 -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/paragraphTextBoxEnrichment.js +2 -1
  29. package/dist/docx/parser.js +17 -5
  30. package/dist/docx/rasterMime.d.ts +64 -0
  31. package/dist/docx/rasterMime.js +116 -0
  32. package/dist/docx/replyToComment.js +13 -6
  33. package/dist/docx/rezip.js +14 -7
  34. package/dist/docx/selectiveSave.js +10 -0
  35. package/dist/docx/serializer/partNamespaces.js +4 -2
  36. package/dist/docx/serializer/runSerializer.js +5 -4
  37. package/dist/docx/serializer/trackedChangeAttributes.js +7 -1
  38. package/dist/docx/server/build.js +14 -3
  39. package/dist/docx/server/createBilingualDocument.js +10 -9
  40. package/dist/docx/streamingXmlParser.d.ts +10 -1
  41. package/dist/docx/streamingXmlParser.js +88 -19
  42. package/dist/docx/unzip.d.ts +3 -1
  43. package/dist/docx/unzip.js +36 -16
  44. package/dist/layout-bridge/convert/fixedTableColumnWidths.d.ts +6 -0
  45. package/dist/layout-bridge/convert/fixedTableColumnWidths.js +23 -0
  46. package/dist/layout-bridge/convert/tableConversion.js +4 -1
  47. package/dist/layout-painter/index.js +2 -5
  48. package/dist/paged-layout/editorScrollRoot.d.ts +15 -0
  49. package/dist/paged-layout/editorScrollRoot.js +72 -0
  50. package/dist/paged-layout/scrollToPmPosition.js +8 -22
  51. package/dist/prosemirror/commands/clearParagraphIndent.d.ts +6 -0
  52. package/dist/prosemirror/commands/clearParagraphIndent.js +25 -0
  53. package/dist/prosemirror/commentIdAllocator.d.ts +15 -22
  54. package/dist/prosemirror/commentIdAllocator.js +50 -25
  55. package/dist/prosemirror/extensions/core/HistoryExtension.js +97 -5
  56. package/dist/prosemirror/extensions/core/ParagraphExtension.js +3 -1
  57. package/dist/prosemirror/extensions/features/BaseKeymapExtension.js +22 -28
  58. package/dist/prosemirror/extensions/nodes/ShapeExtension.js +8 -6
  59. package/dist/prosemirror/plugins/revisionIds.d.ts +23 -19
  60. package/dist/prosemirror/plugins/revisionIds.js +110 -29
  61. package/dist/prosemirror/plugins/suggestionMode.d.ts +2 -2
  62. package/dist/prosemirror/plugins/suggestionMode.js +196 -40
  63. package/dist/prosemirror/storyListNumbering.js +11 -9
  64. package/dist/prosemirror/textInput.d.ts +6 -1
  65. package/dist/prosemirror/textInput.js +9 -2
  66. package/dist/prosemirror/utils/visualLineNavigation.js +6 -8
  67. package/dist/utils/mergeDocumentContent.d.ts +2 -2
  68. package/dist/utils/mergeDocumentContent.js +11 -8
  69. package/package.json +2 -2
@@ -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 —
@@ -2060,6 +2043,7 @@ const buildInsertedParagraphs = ({ item, schema, formattingFromStyle, mode, auth
2060
2043
  };
2061
2044
  };
2062
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);
2063
2047
  const applied = [];
2064
2048
  const skipped = [];
2065
2049
  const normalizations = [];
@@ -2116,7 +2100,7 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2116
2100
  id: operation.id,
2117
2101
  reason: "unsupportedBlock"
2118
2102
  })),
2119
- nextRevisionId: revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor
2103
+ nextRevisionId: revisionIdSeed ?? revisionStamp?.idSeed ?? nextRevisionId()
2120
2104
  };
2121
2105
  const liveBlocks = collectLiveBlocksByHash(view.state.doc);
2122
2106
  const liveBlocksByParaId = collectLiveBlocksByParaId(view.state.doc);
@@ -2226,14 +2210,14 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2226
2210
  item,
2227
2211
  operationId: item.operation.id,
2228
2212
  target: getTableMutationPlanTarget(item)
2229
- })));
2213
+ })), view.state.doc);
2230
2214
  skipped.push(...tablePlan.skipped);
2231
2215
  const executableResolved = tablePlan.executable;
2232
2216
  if (executableResolved.length === 0) return {
2233
2217
  applied,
2234
2218
  skipped,
2235
2219
  ...normalizations.length > 0 && { normalizations },
2236
- nextRevisionId: revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor
2220
+ nextRevisionId: revisionIdSeed ?? revisionStamp?.idSeed ?? nextRevisionId()
2237
2221
  };
2238
2222
  const moveSideCounts = /* @__PURE__ */ new Map();
2239
2223
  for (const { operation } of executableResolved) {
@@ -2268,17 +2252,20 @@ const applyFolioAIEditOperationsInternal = ({ view, snapshot, operations: reques
2268
2252
  const mergedPropertyTargets = [];
2269
2253
  const deferredNoopFinalDeletions = /* @__PURE__ */ new Set();
2270
2254
  const ownsSharedRevisionIdCursor = revisionIdSeed === void 0 && revisionStamp === void 0;
2271
- let revisionSeed = revisionIdSeed ?? revisionStamp?.idSeed ?? revisionIdCursor;
2272
- /**
2273
- * Claim the shared ids below `nextRevisionId` for this batch. A seeded or
2274
- * stamped batch allocates from its own range and claims nothing.
2275
- */
2276
- const claimSharedRevisionIds = (nextRevisionId) => {
2277
- 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;
2278
2262
  };
2279
- /** Continue past any shared id a batch the host started meanwhile claimed. */
2263
+ /** Continue past ids a reentrant callback allocated in this realm. */
2280
2264
  const continueSharedRevisionIds = () => {
2281
- if (ownsSharedRevisionIdCursor) revisionSeed = Math.max(revisionSeed, revisionIdCursor);
2265
+ if (ownsSharedRevisionIdCursor && nextRevisionId() !== claimedThrough) {
2266
+ revisionSeed = nextRevisionIdRange();
2267
+ claimedThrough = revisionSeed;
2268
+ }
2282
2269
  };
2283
2270
  const date = revisionStamp?.date ?? (/* @__PURE__ */ new Date()).toISOString();
2284
2271
  const insertedColumnCounts = /* @__PURE__ */ new Map();
@@ -3738,7 +3725,7 @@ const previewFolioAIEditOperationsWithResult = (options) => {
3738
3725
  applied: result.applied.map(({ id }) => ({ id })),
3739
3726
  skipped: result.skipped,
3740
3727
  ...result.normalizations !== void 0 && { normalizations: result.normalizations },
3741
- nextRevisionId: options.revisionStamp?.idSeed ?? revisionIdCursor
3728
+ nextRevisionId: options.revisionStamp?.idSeed ?? nextRevisionId()
3742
3729
  },
3743
3730
  doc: previewView.state.doc,
3744
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
  };
@@ -15,6 +15,11 @@ type EnsureParaIdsResult = {
15
15
  deduplicated: number;
16
16
  /** True when the input already had full, unique coverage. */
17
17
  alreadyComplete: boolean;
18
+ /**
19
+ * Every id this pass wrote (assigned or deduplicated), in scan order. An id
20
+ * absent from this list was already the package's own.
21
+ */
22
+ mintedParaIds: readonly string[];
18
23
  };
19
24
  /** Controls mutation of package metadata that has security implications. */
20
25
  type EnsureParaIdsOptions = {