@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
@@ -1,3 +1,4 @@
1
+ import { mintBookmarkId, reserveProseBookmarkIds } from "../../../docx/bookmarkIds.js";
1
2
  import { BUILT_IN_STYLE_NAME } from "../../../docx/builtInStyles.js";
2
3
  import { PROSE_PARAGRAPH_SOURCE_TOKEN_ATTR } from "../../../docx/paragraphPropertySource.js";
3
4
  import { isStyleSourcedParagraphNumbering } from "../../../internal/paragraphFormattingSerialization.js";
@@ -671,6 +672,7 @@ const ParagraphExtension = createNodeExtension({
671
672
  if (!dispatch) return true;
672
673
  const { schema: s } = state;
673
674
  const tr = state.tr;
675
+ reserveProseBookmarkIds(state.doc);
674
676
  const bookmarkEntries = [];
675
677
  for (const h of headings) {
676
678
  const bookmarkName = `_Toc${Math.floor(1e8 + Math.random() * 9e8)}`;
@@ -683,7 +685,7 @@ const ParagraphExtension = createNodeExtension({
683
685
  const paragraphNode = tr.doc.resolve(mappedPos).nodeAfter;
684
686
  if (paragraphNode && paragraphNode.type.name === "paragraph") {
685
687
  const newBookmarks = [...(paragraphNode.attrs["bookmarks"] ?? []).filter((b) => !b.name.startsWith("_Toc")), {
686
- id: Math.floor(Math.random() * 2147483647),
688
+ id: mintBookmarkId(),
687
689
  name: bookmarkName
688
690
  }];
689
691
  tr.setNodeMarkup(mappedPos, void 0, {
@@ -1,5 +1,6 @@
1
1
  import { mergeTextFormatting } from "../../../utils/textFormattingMerge.js";
2
2
  import { expectCharacterStyleMarkAttrs, expectRunFormattingOverrideMarkAttrs } from "../../attrs/index.js";
3
+ import { clearIndentOnBackspace } from "../../commands/clearParagraphIndent.js";
3
4
  import { keepSectionBreaksOnSurvivingMarks } from "../../commands/sectionBreak.js";
4
5
  import { getDocumentStyleResolver } from "../../plugins/documentStyleState.js";
5
6
  import { RUN_FORMATTING_MARK_NAMES } from "../../runFormattingMarkNames.js";
@@ -8,6 +9,7 @@ import { paragraphAttrsFromResolvedStyle } from "../../styles/resolvedStyleAttrs
8
9
  import { createExtension } from "../create.js";
9
10
  import { marksToTextFormatting, textFormattingToMarks } from "../marks/markUtils.js";
10
11
  import { Priority } from "../types.js";
12
+ import { EditorState, TextSelection } from "prosemirror-state";
11
13
  import { baseKeymap, deleteSelection, joinBackward, joinForward, selectAll, selectParentNode, splitBlock } from "prosemirror-commands";
12
14
  //#region src/prosemirror/extensions/features/BaseKeymapExtension.ts
13
15
  /**
@@ -22,31 +24,6 @@ function chainCommands(...commands) {
22
24
  };
23
25
  }
24
26
  /**
25
- * Backspace at the start of a paragraph clears first-line indent / hanging indent
26
- * before joining with the previous paragraph (matches Word behavior).
27
- */
28
- const clearIndentOnBackspace = (state, dispatch) => {
29
- const { $cursor } = state.selection;
30
- if (!$cursor) return false;
31
- if ($cursor.parentOffset !== 0) return false;
32
- if ($cursor.parent.type.name !== "paragraph") return false;
33
- const attrs = $cursor.parent.attrs;
34
- const hasFirstLine = attrs["indentFirstLine"] !== null && attrs["indentFirstLine"] > 0;
35
- const hasHanging = !!attrs["hangingIndent"];
36
- const hasIndentLeft = attrs["indentLeft"] !== null && attrs["indentLeft"] > 0;
37
- if (!hasFirstLine && !hasHanging && !hasIndentLeft) return false;
38
- if (dispatch) {
39
- const pos = $cursor.before();
40
- dispatch(state.tr.setNodeMarkup(pos, void 0, {
41
- ...attrs,
42
- indentFirstLine: null,
43
- hangingIndent: null,
44
- indentLeft: null
45
- }).scrollIntoView());
46
- }
47
- return true;
48
- };
49
- /**
50
27
  * Custom Enter handler: splits the block, inherits style-related attrs,
51
28
  * clears paragraph borders, and preserves run formatting on the new paragraph.
52
29
  *
@@ -117,16 +94,33 @@ function applyNextParagraphStyle(tr, sourcePara, newPara, resolver) {
117
94
  tr.setStoredMarks(resolved.runFormatting ? textFormattingToMarks(resolved.runFormatting, tr.doc.type.schema) : []);
118
95
  return true;
119
96
  }
97
+ const blockBoundaryDeletion = (state) => {
98
+ if (!(state.selection instanceof TextSelection) || state.selection.empty) return null;
99
+ const tr = state.tr.deleteSelection();
100
+ return tr.doc.resolve(tr.mapping.map(state.selection.from)).parent.isTextblock ? null : tr;
101
+ };
120
102
  const splitBlockClearBorders = (state, dispatch, view) => {
121
- const { $from: preSplitFrom } = state.selection;
103
+ const deletion = blockBoundaryDeletion(state);
104
+ const splitState = deletion ? EditorState.create({
105
+ doc: deletion.doc,
106
+ selection: deletion.selection,
107
+ storedMarks: deletion.storedMarks
108
+ }) : state;
109
+ const { $from: preSplitFrom } = splitState.selection;
122
110
  const sourcePara = preSplitFrom.parent.type.name === "paragraph" ? preSplitFrom.parent : null;
123
- const caretFormattingMarks = (state.storedMarks || preSplitFrom.marks()).filter((mark) => RUN_FORMATTING_MARK_NAMES.has(mark.type.name));
111
+ const caretFormattingMarks = (splitState.storedMarks || preSplitFrom.marks()).filter((mark) => RUN_FORMATTING_MARK_NAMES.has(mark.type.name));
124
112
  const resolver = getDocumentStyleResolver(state);
125
113
  const splitResult = { tr: null };
126
- if (!splitBlock(state, dispatch ? (tr) => {
114
+ if (!splitBlock(splitState, dispatch ? (tr) => {
127
115
  splitResult.tr = tr;
128
116
  } : void 0, view)) return false;
129
117
  if (dispatch && splitResult.tr !== null) {
118
+ if (deletion) {
119
+ for (const step of splitResult.tr.steps) deletion.step(step);
120
+ deletion.setSelection(splitResult.tr.selection.getBookmark().resolve(deletion.doc));
121
+ deletion.setStoredMarks(splitResult.tr.storedMarks);
122
+ splitResult.tr = deletion;
123
+ }
130
124
  const tr = splitResult.tr;
131
125
  const { $from } = tr.selection;
132
126
  const newPara = $from.parent;
@@ -268,9 +268,9 @@ function createShapeElement(type, w, h, adjustments) {
268
268
  case "straightConnector1": {
269
269
  const el = document.createElementNS(SVG_NS, "line");
270
270
  setNum(el, "x1", 0);
271
- setNum(el, "y1", h / 2);
271
+ setNum(el, "y1", 0);
272
272
  setNum(el, "x2", w);
273
- setNum(el, "y2", h / 2);
273
+ setNum(el, "y2", h);
274
274
  return el;
275
275
  }
276
276
  case "rightBrace": {
@@ -537,9 +537,11 @@ const ShapeExtension = createNodeExtension({
537
537
  const strokeWidth = finiteNumber(attrs.outlineWidth) ?? 1;
538
538
  const svg = document.createElementNS(SVG_NS, "svg");
539
539
  svg.setAttribute("xmlns", SVG_NS);
540
- setNum(svg, "width", w);
541
- setNum(svg, "height", h);
542
- svg.setAttribute("viewBox", `0 0 ${w} ${h}`);
540
+ const viewportWidth = w === 0 ? 1 : w;
541
+ const viewportHeight = h === 0 ? 1 : h;
542
+ setNum(svg, "width", viewportWidth);
543
+ setNum(svg, "height", viewportHeight);
544
+ svg.setAttribute("viewBox", `0 0 ${viewportWidth} ${viewportHeight}`);
543
545
  let gradient = null;
544
546
  let fillAttr;
545
547
  if (attrs.fillType === "gradient" && attrs.gradientStops) {
@@ -548,7 +550,7 @@ const ShapeExtension = createNodeExtension({
548
550
  fillAttr = gradient ? `url(#${gradId})` : safeFillColor;
549
551
  } else fillAttr = attrs.fillType === "none" ? "none" : safeFillColor;
550
552
  const strokeAttr = attrs.outlineStyle === "none" ? "none" : safeStrokeColor;
551
- svg.setAttribute("style", `fill:${fillAttr};stroke:${strokeAttr};stroke-width:${strokeWidth}`);
553
+ svg.setAttribute("style", `fill:${fillAttr};stroke:${strokeAttr};stroke-width:${strokeWidth};overflow:visible`);
552
554
  if (gradient) {
553
555
  const defs = document.createElementNS(SVG_NS, "defs");
554
556
  defs.append(gradient);
@@ -7,6 +7,7 @@ import { listRenderingFor, recordsListRendering } from "./listRendering.js";
7
7
  import { LIST_RENDERING_ATTR_KEYS } from "./listRenderingAttrs.js";
8
8
  import { directParagraphIndentation } from "./paragraphIndentation.js";
9
9
  import { getDocumentNumbering } from "./plugins/documentNumbering.js";
10
+ import { getDocumentStyleResolver } from "./plugins/documentStyleState.js";
10
11
  import { SUGGESTION_META, makeRevisionInfo } from "./plugins/suggestionMode.js";
11
12
  import { listAttrsFromNumbering, listLevelIndentAttrPatch } from "./styles/resolvedStyleAttrs.js";
12
13
  //#region src/prosemirror/listNumbering.ts
@@ -20,22 +21,15 @@ const PARAGRAPH_NODE = "paragraph";
20
21
  */
21
22
  const listChangeSnapshot = (node, attrs) => {
22
23
  const previousFormatting = paragraphPropertiesSnapshot(node.type.create(attrs, node.content, node.marks));
23
- previousFormatting["numPr"] = attrs["numPr"] ?? null;
24
+ previousFormatting["numPr"] ??= null;
24
25
  for (const key of LIST_RENDERING_ATTR_KEYS) previousFormatting[key] = attrs[key] ?? null;
25
26
  return previousFormatting;
26
27
  };
27
- /**
28
- * The original state a pending record describes, completed with the list
29
- * attrs it may not state. A record written by a list command states them all.
30
- * One written for another property (or read from a file) states the scope
31
- * only: its numbering is the original's, and an absent one means the original
32
- * had none, so the rendering is recomputed from it rather than copied from
33
- * the live paragraph, whose list may since have changed.
34
- */
35
- const originalListFormatting = (record, numbering) => {
28
+ const originalListFormatting = ({ record, numbering, styleResolver }) => {
36
29
  const original = { ...record };
37
30
  if (recordsListRendering(record)) return original;
38
- Object.assign(original, listRenderingFor(record.numPr, numbering));
31
+ const inherited = styleResolver?.resolveParagraphStyle(record.styleId ?? void 0).paragraphFormatting?.numPr;
32
+ Object.assign(original, listRenderingFor(record.numPr ?? inherited, numbering));
39
33
  original["numPr"] = record.numPr ?? null;
40
34
  return original;
41
35
  };
@@ -54,12 +48,16 @@ const canonicalJson = (value) => JSON.stringify(value, (_key, nested) => nested
54
48
  * the file) keeps its author, id and date: the change joins that revision
55
49
  * rather than re-attributing it, and it is never dropped.
56
50
  */
57
- const trackListChange = ({ current, next, rev, numbering }) => {
51
+ const trackListChange = ({ current, next, rev, numbering, styleResolver }) => {
58
52
  const currentAttrs = expectParagraphAttrs(current);
59
53
  const existing = currentAttrs._propertyChanges ?? [];
60
54
  const pending = existing.find(({ info }) => info.provenance !== "suggested");
61
55
  const retained = existing.filter((change) => change !== pending);
62
- const previousFormatting = pending?.previousFormatting ? originalListFormatting(pending.previousFormatting, numbering) : listChangeSnapshot(current, currentAttrs);
56
+ const previousFormatting = pending?.previousFormatting ? originalListFormatting({
57
+ record: pending.previousFormatting,
58
+ numbering,
59
+ styleResolver
60
+ }) : listChangeSnapshot(current, currentAttrs);
63
61
  const ownPending = pending !== void 0 && pending.info.author === rev.author;
64
62
  if (ownPending && canonicalJson(previousFormatting) === canonicalJson(listChangeSnapshot(current, next))) return {
65
63
  ...next,
@@ -198,12 +196,14 @@ const listItemAttrs = (attrs, { numId, ilvl }, numbering) => ({
198
196
  const applyParagraphUpdates = ({ tr, state, updates }) => {
199
197
  const rev = makeRevisionInfo(state);
200
198
  const numbering = getDocumentNumbering(state);
199
+ const styleResolver = getDocumentStyleResolver(state);
201
200
  for (const { pos, node, next } of updates) {
202
201
  const attrs = rev ? trackListChange({
203
202
  current: node,
204
203
  next,
205
204
  rev,
206
- numbering
205
+ numbering,
206
+ styleResolver
207
207
  }) : next;
208
208
  tr.setNodeMarkup(pos, void 0, attrs);
209
209
  }
@@ -1,11 +1,12 @@
1
1
  import { ParagraphAttrs, ParagraphPropertyChangeAttrs } from "./schema/nodes.js";
2
2
  import { NumberingMap } from "../docx/numberingParser.js";
3
+ import { ParagraphNumberingOverride } from "../docx/numberingReference.js";
3
4
  //#region src/prosemirror/listRendering.d.ts
4
5
  type PreviousFormatting = NonNullable<ParagraphPropertyChangeAttrs["previousFormatting"]>;
5
6
  /** Whether a `w:pPrChange` record states the list rendering it had (a list command's does). */
6
7
  declare const recordsListRendering: (record: PreviousFormatting) => boolean;
7
8
  /** Every list-rendering attr for `numPr`: its level's, or all cleared when it numbers nothing. */
8
- declare const listRenderingFor: (numPr: PreviousFormatting["numPr"], numbering: NumberingMap | null) => Record<string, unknown>;
9
+ declare const listRenderingFor: (numPr: ParagraphNumberingOverride | null | undefined, numbering: NumberingMap | null) => Record<string, unknown>;
9
10
  /**
10
11
  * The list rendering a paragraph takes when a `w:pPrChange` is rejected. A
11
12
  * reject restores the recorded numbering, but the rendering attrs sit outside
@@ -21,7 +22,8 @@ type RejectedListRenderingOptions = {
21
22
  /** The previous state the rejected record restores. */
22
23
  previousFormatting: PreviousFormatting | null | undefined;
23
24
  numbering: NumberingMap | null;
25
+ restoredNumbering: ParagraphAttrs["numPr"] | null;
24
26
  };
25
- declare const rejectedListRenderingPatch: ({ current, previousFormatting, numbering }: RejectedListRenderingOptions) => Record<string, unknown>;
27
+ declare const rejectedListRenderingPatch: ({ current, previousFormatting, numbering, restoredNumbering }: RejectedListRenderingOptions) => Record<string, unknown>;
26
28
  //#endregion
27
29
  export { RejectedListRenderingOptions, listRenderingFor, recordsListRendering, rejectedListRenderingPatch };
@@ -16,10 +16,9 @@ const listRenderingFor = (numPr, numbering) => {
16
16
  for (const key of LIST_RENDERING_ATTR_KEYS) attrs[key] = rendering[key] ?? null;
17
17
  return attrs;
18
18
  };
19
- const rejectedListRenderingPatch = ({ current, previousFormatting, numbering }) => {
20
- const record = previousFormatting ?? {};
21
- if (recordsListRendering(record)) return {};
22
- const restored = record.numPr ?? void 0;
19
+ const rejectedListRenderingPatch = ({ current, previousFormatting, numbering, restoredNumbering }) => {
20
+ if (recordsListRendering(previousFormatting ?? {})) return {};
21
+ const restored = restoredNumbering ?? void 0;
23
22
  if (sameStatedParagraphNumbering(restored, current.numPr ?? void 0)) return {};
24
23
  return listRenderingFor(restored, numbering);
25
24
  };
@@ -86,7 +86,7 @@ const carryParagraphProperties = ({ tr, position, source, styleResolver, numberi
86
86
  };
87
87
  /**
88
88
  * Whether the carried numbering is the paragraph's own or its style's: the
89
- * record holds `numPr` as the paragraph reads it, and a style's numbering
89
+ * record holds only stated numbering, and a style's numbering
90
90
  * stays the style's rather than becoming the paragraph's own `w:numPr`.
91
91
  */
92
92
  const carriedNumberingProvenance = (source, target, keep) => ({ numPrFromStyle: expectParagraphAttrs(keep?.has("numPr") ? target : source).numPrFromStyle });
@@ -112,10 +112,7 @@ const paragraphLeftAfter = ({ doc, paragraphPos, removedBreakPositions }) => {
112
112
  node = next;
113
113
  }
114
114
  };
115
- const recordedFormatting = (node) => ({
116
- ...paragraphPropertiesSnapshot(node),
117
- numPr: expectParagraphAttrs(node).numPr ?? null
118
- });
115
+ const recordedFormatting = paragraphPropertiesSnapshot;
119
116
  /**
120
117
  * A paragraph now holds the break another one had (a split, or a paste into
121
118
  * it, leaves that break on the last part) but reads with other properties:
@@ -1,31 +1,35 @@
1
1
  import { Node } from "prosemirror-model";
2
2
  //#region src/prosemirror/plugins/revisionIds.d.ts
3
+ declare const RevisionIdAllocationError_base: import("better-result").TaggedErrorClass<"RevisionIdAllocationError">;
4
+ declare class RevisionIdAllocationError extends RevisionIdAllocationError_base<{
5
+ message: string;
6
+ first: number;
7
+ next: number;
8
+ }> {}
9
+ /** Deterministic package-local batches prefer ids above loaded revisions, then a free interval. */
10
+ declare const revisionIdSeedAbove: (existingIds: Iterable<number>) => number;
11
+ /** The next free id, including after the signed 32-bit boundary wraps. */
12
+ declare const nextRevisionId: () => number;
3
13
  /**
4
- * Mint the next tracked-revision id (`w:id`). Strictly monotonic per realm,
5
- * and always inside the range serialized `w:id` may occupy.
14
+ * A shared batch increments its seed locally, so a single free id is not
15
+ * enough: start in the largest free interval. Every contiguous batch that
16
+ * can fit anywhere fits here, including after loaded ids shorten the tail.
17
+ * Continue within this interval until another producer changes the cursor.
6
18
  */
19
+ declare const nextRevisionIdRange: () => number;
20
+ /** Reserve a deterministic batch without changing its caller-supplied ids. */
21
+ declare const reserveRevisionIds: (first: number, next: number) => void;
22
+ /** Shared producers must never cross a loaded or already issued id on wrap. */
23
+ declare const claimRevisionIds: (first: number, next: number) => void;
24
+ /** Mint a bounded id without reusing loaded or previously issued ids. */
7
25
  declare function mintRevisionId(): number;
8
- /**
9
- * Raise the counter above `maxId` so freshly minted ids cannot collide with
10
- * ids already present in a loaded document. Only ever raises.
11
- *
12
- * `maxId` is derived from file content, so it is untrusted: a malformed or
13
- * out-of-range value is ignored outright rather than folded into range.
14
- */
26
+ /** Reserve a valid loaded id and prefer the range above it. */
15
27
  declare function seedRevisionIdsAbove(maxId: number): void;
16
- /**
17
- * The highest id in `doc` from any part of the annotation space.
18
- *
19
- * A revision, a comment and a bookmark draw their `w:id` from one space, so a
20
- * counter seeded above its own kind alone still mints over another kind's id.
21
- * Both allocators seed from this, and the save-time normalization avoids the
22
- * same space, so the three agree on what is taken.
23
- */
24
- declare function maxAnnotationIdInDoc(doc: Node): number;
28
+ declare const maxAnnotationIdInDoc: (doc: Node) => number;
25
29
  /**
26
30
  * Raise the counter above every annotation id already present in `doc`.
27
31
  * Called from the suggestion-mode plugin's `state.init`.
28
32
  */
29
33
  declare function seedRevisionIdsFromDoc(doc: Node): void;
30
34
  //#endregion
31
- export { maxAnnotationIdInDoc, mintRevisionId, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
35
+ export { RevisionIdAllocationError, claimRevisionIds, maxAnnotationIdInDoc, mintRevisionId, nextRevisionId, nextRevisionIdRange, reserveRevisionIds, revisionIdSeedAbove, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
@@ -1,3 +1,4 @@
1
+ import { TaggedError } from "better-result";
1
2
  import { MAX_REVISION_ID } from "@stll/docx-core/model";
2
3
  //#region src/prosemirror/plugins/revisionIds.ts
3
4
  /**
@@ -9,23 +10,98 @@ import { MAX_REVISION_ID } from "@stll/docx-core/model";
9
10
  * int consumers read `w:id` into.
10
11
  */
11
12
  let counter = 1;
13
+ const occupiedIds = /* @__PURE__ */ new Set();
14
+ var RevisionIdAllocationError = class extends TaggedError("RevisionIdAllocationError") {};
15
+ /** Batches use contiguous ids; on rollover prefer the largest unused interval. */
16
+ const largestFreeIntervalStart = (reservedIds) => {
17
+ let first = 1;
18
+ let bestFirst = 1;
19
+ let bestSize = 0;
20
+ const boundaries = [...reservedIds].filter((id) => id > 0).toSorted((a, b) => a - b);
21
+ boundaries.push(MAX_REVISION_ID + 1);
22
+ for (const boundary of boundaries) {
23
+ const size = boundary - first;
24
+ if (size > bestSize) {
25
+ bestFirst = first;
26
+ bestSize = size;
27
+ }
28
+ first = boundary + 1;
29
+ }
30
+ if (bestSize === 0) throw new RevisionIdAllocationError({
31
+ message: "Revision id space exhausted",
32
+ first: 1,
33
+ next: 1
34
+ });
35
+ return bestFirst;
36
+ };
37
+ /** Deterministic package-local batches prefer ids above loaded revisions, then a free interval. */
38
+ const revisionIdSeedAbove = (existingIds) => {
39
+ const reservedIds = /* @__PURE__ */ new Set();
40
+ let maxId = 0;
41
+ for (const id of existingIds) {
42
+ if (!Number.isInteger(id) || id < 0 || id > MAX_REVISION_ID) continue;
43
+ reservedIds.add(id);
44
+ maxId = Math.max(maxId, id);
45
+ }
46
+ return maxId < MAX_REVISION_ID ? maxId + 1 : largestFreeIntervalStart(reservedIds);
47
+ };
48
+ /** The next free id, including after the signed 32-bit boundary wraps. */
49
+ const nextRevisionId = () => {
50
+ if (counter > MAX_REVISION_ID) counter = largestFreeIntervalStart(occupiedIds);
51
+ while (occupiedIds.has(counter)) {
52
+ counter += 1;
53
+ if (counter > MAX_REVISION_ID) counter = largestFreeIntervalStart(occupiedIds);
54
+ if (occupiedIds.size >= MAX_REVISION_ID) throw new RevisionIdAllocationError({
55
+ message: "Revision id space exhausted",
56
+ first: counter,
57
+ next: counter
58
+ });
59
+ }
60
+ return counter;
61
+ };
12
62
  /**
13
- * Mint the next tracked-revision id (`w:id`). Strictly monotonic per realm,
14
- * and always inside the range serialized `w:id` may occupy.
63
+ * A shared batch increments its seed locally, so a single free id is not
64
+ * enough: start in the largest free interval. Every contiguous batch that
65
+ * can fit anywhere fits here, including after loaded ids shorten the tail.
66
+ * Continue within this interval until another producer changes the cursor.
15
67
  */
68
+ const nextRevisionIdRange = () => {
69
+ counter = largestFreeIntervalStart(occupiedIds);
70
+ return counter;
71
+ };
72
+ const validateRevisionIdRange = (first, next) => {
73
+ if (!Number.isInteger(first) || !Number.isInteger(next) || first < 0 || next < first || next > MAX_REVISION_ID + 1) throw new RevisionIdAllocationError({
74
+ message: "Revision id range exceeds the OOXML limit",
75
+ first,
76
+ next
77
+ });
78
+ };
79
+ /** Reserve a deterministic batch without changing its caller-supplied ids. */
80
+ const reserveRevisionIds = (first, next) => {
81
+ validateRevisionIdRange(first, next);
82
+ for (let id = first; id < next; id += 1) occupiedIds.add(id);
83
+ if (next > counter) counter = next;
84
+ };
85
+ /** Shared producers must never cross a loaded or already issued id on wrap. */
86
+ const claimRevisionIds = (first, next) => {
87
+ validateRevisionIdRange(first, next);
88
+ for (let id = first; id < next && id <= MAX_REVISION_ID; id += 1) if (occupiedIds.has(id)) throw new RevisionIdAllocationError({
89
+ message: "Revision id range overlaps an occupied id",
90
+ first,
91
+ next
92
+ });
93
+ reserveRevisionIds(first, next);
94
+ };
95
+ /** Mint a bounded id without reusing loaded or previously issued ids. */
16
96
  function mintRevisionId() {
17
- if (counter > MAX_REVISION_ID) counter = 1;
18
- return counter++;
97
+ const id = nextRevisionId();
98
+ claimRevisionIds(id, id + 1);
99
+ return id;
19
100
  }
20
- /**
21
- * Raise the counter above `maxId` so freshly minted ids cannot collide with
22
- * ids already present in a loaded document. Only ever raises.
23
- *
24
- * `maxId` is derived from file content, so it is untrusted: a malformed or
25
- * out-of-range value is ignored outright rather than folded into range.
26
- */
101
+ /** Reserve a valid loaded id and prefer the range above it. */
27
102
  function seedRevisionIdsAbove(maxId) {
28
- if (!Number.isInteger(maxId) || maxId < 0 || maxId >= MAX_REVISION_ID) return;
103
+ if (!Number.isInteger(maxId) || maxId < 0 || maxId > MAX_REVISION_ID) return;
104
+ occupiedIds.add(maxId);
29
105
  if (maxId >= counter) counter = maxId + 1;
30
106
  }
31
107
  /** Node attrs that carry a revision id directly (`revisionId` field). */
@@ -33,40 +109,45 @@ const DIRECT_REVISION_ATTR_KEYS = ["trIns", "trDel"];
33
109
  /**
34
110
  * The highest id in `doc` from any part of the annotation space.
35
111
  *
36
- * A revision, a comment and a bookmark draw their `w:id` from one space, so a
37
- * counter seeded above its own kind alone still mints over another kind's id.
38
- * Both allocators seed from this, and the save-time normalization avoids the
39
- * same space, so the three agree on what is taken.
112
+ * Preserve the editor's conservative annotation seeding: revisions reserve
113
+ * comment and bookmark ids too, although OOXML defines separate id spaces.
40
114
  */
41
- function maxAnnotationIdInDoc(doc) {
115
+ const visitAnnotationIds = (doc, visit) => {
42
116
  let max = 0;
43
117
  const consider = (id) => {
44
- if (typeof id === "number" && Number.isInteger(id) && id > max && id <= MAX_REVISION_ID) max = id;
118
+ if (typeof id === "number" && Number.isInteger(id) && id >= 0 && id <= MAX_REVISION_ID) {
119
+ visit(id);
120
+ max = Math.max(max, id);
121
+ }
122
+ };
123
+ const considerMetadata = (value) => {
124
+ if (typeof value !== "object" || value === null) return;
125
+ for (const [key, child] of Object.entries(value)) if (key === "id" || key === "revisionId" || key === "commentId") consider(child);
126
+ else considerMetadata(child);
45
127
  };
46
128
  doc.descendants((node) => {
47
129
  for (const mark of node.marks) {
48
130
  consider(mark.attrs["revisionId"]);
49
131
  consider(mark.attrs["commentId"]);
132
+ considerMetadata(mark.attrs["_docxRevisionAncestors"]);
133
+ considerMetadata(mark.attrs["changes"]);
50
134
  }
51
135
  if (node.type.name === "bookmarkBoundary") consider(node.attrs["id"]);
52
136
  const attrs = node.attrs;
53
- const pPrMark = attrs["pPrMark"];
54
- if (pPrMark?.info) consider(pPrMark.info.id);
55
- for (const key of DIRECT_REVISION_ATTR_KEYS) {
56
- const value = attrs[key];
57
- if (value) consider(value.revisionId);
58
- }
59
- const cellMarker = attrs["cellMarker"];
60
- if (cellMarker?.info) consider(cellMarker.info.revisionId);
137
+ considerMetadata(attrs["_propertyChanges"]);
138
+ considerMetadata(attrs["pPrMark"]);
139
+ for (const key of DIRECT_REVISION_ATTR_KEYS) considerMetadata(attrs[key]);
140
+ considerMetadata(attrs["cellMarker"]);
61
141
  });
62
142
  return max;
63
- }
143
+ };
144
+ const maxAnnotationIdInDoc = (doc) => visitAnnotationIds(doc, () => {});
64
145
  /**
65
146
  * Raise the counter above every annotation id already present in `doc`.
66
147
  * Called from the suggestion-mode plugin's `state.init`.
67
148
  */
68
149
  function seedRevisionIdsFromDoc(doc) {
69
- seedRevisionIdsAbove(maxAnnotationIdInDoc(doc));
150
+ visitAnnotationIds(doc, seedRevisionIdsAbove);
70
151
  }
71
152
  //#endregion
72
- export { maxAnnotationIdInDoc, mintRevisionId, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
153
+ export { RevisionIdAllocationError, claimRevisionIds, maxAnnotationIdInDoc, mintRevisionId, nextRevisionId, nextRevisionIdRange, reserveRevisionIds, revisionIdSeedAbove, seedRevisionIdsAbove, seedRevisionIdsFromDoc };
@@ -66,8 +66,8 @@ declare function handleSuggestionEnter(view: EditorView, pluginState: Suggestion
66
66
  * - Delete-at-paragraph-end → current paragraph.
67
67
  *
68
68
  * Returns `null` when there is no adjacent sibling paragraph (the join would
69
- * not produce paragraph-mark merging — e.g. doc start, doc end, or sibling
70
- * is a table). The default merge path then runs unchanged.
69
+ * not produce paragraph-mark merging, e.g. doc start, doc end, or a table).
70
+ * Bookmark markers at an edge do not hide the adjacent paragraph break.
71
71
  */
72
72
  declare function paragraphBoundaryTarget(state: EditorState, direction: "backward" | "forward"): number | null;
73
73
  /** Apply the editor's selected-content deletion path for a host cut action. */