@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
@@ -5,17 +5,16 @@ import { Transaction } from "prosemirror-state";
5
5
  //#region src/prosemirror/commands/tableCellMergeResolution.d.ts
6
6
  declare const tableCellContinuationPayload: (node: Node) => DecodedTableCellParagraphSourcePayload | null;
7
7
  declare const hasMatchingCollapsedTableCellMerge: (node: Node, revisionSet: Set<number> | null) => boolean;
8
- /**
9
- * Whether accepting a pending vertical merge folds the cell into the one above.
10
- * The saved `w:vMerge` continuation is read back as its own cell when it carries
11
- * content, so accepting such a merge keeps the cell standing; folding it would
12
- * hide text that reappears when the document is reopened.
13
- */
14
- declare const acceptedMergeFoldsIntoCellAbove: (cell: Node) => boolean;
15
- declare const resolveVisibleTableCellMerge: (tr: Transaction, cellPos: number, mode: "accept" | "reject") => boolean;
8
+ type ResolveVisibleTableCellMergeOptions = {
9
+ tr: Transaction;
10
+ cellPos: number;
11
+ mode: "accept" | "reject";
12
+ revisionSet: ReadonlySet<number> | null;
13
+ };
14
+ declare const resolveVisibleTableCellMerge: ({ tr, cellPos, mode, revisionSet }: ResolveVisibleTableCellMergeOptions) => boolean;
16
15
  declare const tableCellContinuationCells: (cell: Node, rowspan: number) => document_d_exports.TableCell[];
17
16
  declare const tableCellContinuationFromNode: (cell: Node) => document_d_exports.TableCell;
18
17
  declare const resolveCollapsedTableCellMerge: (tr: Transaction, cellPos: number, mode: "accept" | "reject", revisionSet: Set<number> | null) => boolean;
19
18
  declare const createRestoredTableCell: (origin: Node, source: document_d_exports.TableCell) => Node | null;
20
19
  //#endregion
21
- export { acceptedMergeFoldsIntoCellAbove, createRestoredTableCell, hasMatchingCollapsedTableCellMerge, resolveCollapsedTableCellMerge, resolveVisibleTableCellMerge, tableCellContinuationCells, tableCellContinuationFromNode, tableCellContinuationPayload };
20
+ export { createRestoredTableCell, hasMatchingCollapsedTableCellMerge, resolveCollapsedTableCellMerge, resolveVisibleTableCellMerge, tableCellContinuationCells, tableCellContinuationFromNode, tableCellContinuationPayload };
@@ -1,9 +1,10 @@
1
1
  import { decodeTableCellParagraphSourcePayload, restoreTableCellsWithParagraphPropertySources, transportTableCellsWithParagraphPropertySources } from "../../docx/paragraphPropertySource.js";
2
2
  import { isTableCellMergeRevisionContinuation } from "../../docx/tableParser.js";
3
3
  import { standaloneTableCellFromProseMirror } from "../conversion/fromProseDoc.js";
4
- import { standaloneTableCellToProseMirror, tableCellHasMeaningfulContent } from "../conversion/toProseDoc.js";
4
+ import { standaloneTableCellToProseMirror } from "../conversion/toProseDoc.js";
5
5
  import { getTableCellMergeChange } from "../tableCellMergeRevision.js";
6
6
  import { removeRowsWithoutCellsInRange } from "../tableGridMutation.js";
7
+ import { resolvedTableMergeOriginAttrs, resolvedVisibleTableCellMergeAttrs, tableMergeFoldDecisions } from "./tableMergeFoldDecisions.js";
7
8
  import { Result, panic } from "better-result";
8
9
  import { TableMap } from "prosemirror-tables";
9
10
  //#region src/prosemirror/commands/tableCellMergeResolution.ts
@@ -37,33 +38,35 @@ const tableCellContext = (doc, cellPos) => {
37
38
  }
38
39
  return null;
39
40
  };
40
- /**
41
- * Whether accepting a pending vertical merge folds the cell into the one above.
42
- * The saved `w:vMerge` continuation is read back as its own cell when it carries
43
- * content, so accepting such a merge keeps the cell standing; folding it would
44
- * hide text that reappears when the document is reopened.
45
- */
46
- const acceptedMergeFoldsIntoCellAbove = (cell) => !tableCellHasMeaningfulContent(standaloneTableCellFromProseMirror(cell));
47
- const resolveVisibleTableCellMerge = (tr, cellPos, mode) => {
41
+ const resolveVisibleTableCellMerge = ({ tr, cellPos, mode, revisionSet }) => {
48
42
  const cell = tr.doc.nodeAt(cellPos);
49
43
  const marker = cell?.attrs["cellMarker"];
50
44
  if (!cell || !isTableCellMergeRevisionAttr(marker)) return false;
51
- if (mode === "accept" ? isTableCellMergeRevisionContinuation(marker.verticalMerge) && acceptedMergeFoldsIntoCellAbove(cell) : isTableCellMergeRevisionContinuation(marker.verticalMergeOriginal)) return mergeTableCellWithCellAbove(tr, cellPos);
52
- if (mode === "accept") {
53
- tr.setNodeAttribute(cellPos, "cellMarker", null);
54
- return true;
55
- }
56
- const originalFormatting = cell.attrs["_originalFormatting"];
57
- let nextOriginalFormatting = originalFormatting;
58
- if (typeof originalFormatting === "object" && originalFormatting !== null) {
59
- nextOriginalFormatting = { ...originalFormatting };
60
- delete nextOriginalFormatting.vMerge;
61
- }
62
- tr.setNodeMarkup(cellPos, void 0, {
63
- ...cell.attrs,
64
- cellMarker: null,
65
- _originalFormatting: nextOriginalFormatting
45
+ const context = tableCellContext(tr.doc, cellPos);
46
+ if (!context) return false;
47
+ const rectangle = TableMap.get(context.table).findCell(context.relativeCellPos);
48
+ if (mode === "reject" && isTableCellMergeRevisionContinuation(marker.verticalMergeOriginal) && rectangle.top === 0) return false;
49
+ const decisions = tableMergeFoldDecisions({
50
+ table: context.table,
51
+ mode,
52
+ revisionSet
66
53
  });
54
+ if (decisions.invalid.has(context.relativeCellPos)) return false;
55
+ const folds = decisions.folds.get(context.relativeCellPos);
56
+ if (folds === void 0) return panic("Merge resolution lost its cell");
57
+ for (const origin of decisions.origins) {
58
+ if (origin === context.relativeCellPos) continue;
59
+ const originPos = context.tableStart + origin;
60
+ const originCell = tr.doc.nodeAt(originPos);
61
+ if (!originCell) return panic("Merge resolution lost its origin");
62
+ tr.setNodeMarkup(originPos, void 0, resolvedTableMergeOriginAttrs(originCell));
63
+ }
64
+ if (folds) return mergeTableCellWithCellAbove(tr, cellPos);
65
+ tr.setNodeMarkup(cellPos, void 0, resolvedVisibleTableCellMergeAttrs({
66
+ cell,
67
+ mode,
68
+ origin: decisions.origins.has(context.relativeCellPos)
69
+ }));
67
70
  return true;
68
71
  };
69
72
  const mergeTableCellWithCellAbove = (tr, cellPos) => {
@@ -201,4 +204,4 @@ const createRestoredTableCell = (origin, source) => {
201
204
  return Result.try(() => origin.type.schema.nodeFromJSON(restored.toJSON())).unwrapOr(null);
202
205
  };
203
206
  //#endregion
204
- export { acceptedMergeFoldsIntoCellAbove, createRestoredTableCell, hasMatchingCollapsedTableCellMerge, resolveCollapsedTableCellMerge, resolveVisibleTableCellMerge, tableCellContinuationCells, tableCellContinuationFromNode, tableCellContinuationPayload };
207
+ export { createRestoredTableCell, hasMatchingCollapsedTableCellMerge, resolveCollapsedTableCellMerge, resolveVisibleTableCellMerge, tableCellContinuationCells, tableCellContinuationFromNode, tableCellContinuationPayload };
@@ -0,0 +1,61 @@
1
+ import { Node } from "prosemirror-model";
2
+ //#region src/prosemirror/commands/tableMergeFoldDecisions.d.ts
3
+ type TableMergeFoldDecisionsOptions = {
4
+ table: Node;
5
+ mode: "accept" | "reject";
6
+ revisionSet: ReadonlySet<number> | null;
7
+ };
8
+ /** Resolve raw merge states without converting unrelated table revisions. */
9
+ declare const tableMergeFoldDecisions: ({ table, mode, revisionSet }: TableMergeFoldDecisionsOptions) => {
10
+ folds: Map<number, boolean>;
11
+ origins: Set<number>;
12
+ invalid: Set<number>;
13
+ };
14
+ /** Persist an implicit merge origin even when reader rules retain its continuation. */
15
+ declare const resolvedTableMergeOriginAttrs: (cell: Node) => {
16
+ _originalFormatting: {
17
+ vMerge: string;
18
+ width?: import("@stll/docx-core/model").TableMeasurement;
19
+ borders?: import("@stll/docx-core/model").TableCellBorders;
20
+ margins?: import("@stll/docx-core/model").CellMargins;
21
+ shading?: import("@stll/docx-core/model").ShadingProperties;
22
+ verticalAlign?: "top" | "center" | "bottom";
23
+ textDirection?: import("@stll/docx-core/model").TextDirection;
24
+ gridSpan?: number;
25
+ fitText?: boolean;
26
+ noWrap?: boolean;
27
+ hideMark?: boolean;
28
+ conditionalFormat?: import("@stll/docx-core/model").ConditionalFormatStyle;
29
+ preserved?: import("@stll/docx-core/model").PreservedMarkup;
30
+ sourceXml?: string;
31
+ };
32
+ _preserveVMergeRestart: boolean;
33
+ };
34
+ type ResolvedVisibleTableCellMergeOptions = {
35
+ cell: Node;
36
+ mode: "accept" | "reject";
37
+ origin: boolean;
38
+ };
39
+ /** A retained continuation must save the merge state the reviewer resolved. */
40
+ declare const resolvedVisibleTableCellMergeAttrs: ({ cell, mode, origin }: ResolvedVisibleTableCellMergeOptions) => {
41
+ cellMarker: null;
42
+ _originalFormatting: {
43
+ width?: import("@stll/docx-core/model").TableMeasurement;
44
+ borders?: import("@stll/docx-core/model").TableCellBorders;
45
+ margins?: import("@stll/docx-core/model").CellMargins;
46
+ shading?: import("@stll/docx-core/model").ShadingProperties;
47
+ verticalAlign?: "top" | "center" | "bottom";
48
+ textDirection?: import("@stll/docx-core/model").TextDirection;
49
+ gridSpan?: number;
50
+ vMerge?: "restart" | "continue";
51
+ fitText?: boolean;
52
+ noWrap?: boolean;
53
+ hideMark?: boolean;
54
+ conditionalFormat?: import("@stll/docx-core/model").ConditionalFormatStyle;
55
+ preserved?: import("@stll/docx-core/model").PreservedMarkup;
56
+ sourceXml?: string;
57
+ };
58
+ _preserveVMergeRestart: boolean;
59
+ };
60
+ //#endregion
61
+ export { resolvedTableMergeOriginAttrs, resolvedVisibleTableCellMergeAttrs, tableMergeFoldDecisions };
@@ -0,0 +1,79 @@
1
+ import { isCellMergeContinuation, isCellMergeStart, isTableCellMergeRevisionContinuation } from "../../docx/tableParser.js";
2
+ import { tableCellHasMeaningfulContent } from "../../docx/verticalMergeProjection.js";
3
+ import { expectTableCellAttrs } from "../attrs/index.js";
4
+ import { standaloneTableCellFromProseMirror } from "../conversion/fromProseDoc.js";
5
+ import { panic } from "better-result";
6
+ import { TableMap } from "prosemirror-tables";
7
+ //#region src/prosemirror/commands/tableMergeFoldDecisions.ts
8
+ /** Resolve raw merge states without converting unrelated table revisions. */
9
+ const tableMergeFoldDecisions = ({ table, mode, revisionSet }) => {
10
+ const decisions = /* @__PURE__ */ new Map();
11
+ const origins = /* @__PURE__ */ new Set();
12
+ const invalid = /* @__PURE__ */ new Set();
13
+ const map = TableMap.get(table);
14
+ const visited = /* @__PURE__ */ new Set();
15
+ for (const [index, position] of map.map.entries()) {
16
+ if (visited.has(position)) continue;
17
+ visited.add(position);
18
+ const cell = table.nodeAt(position);
19
+ if (cell?.attrs["cellMarker"]?.kind !== "merge") continue;
20
+ const marker = expectTableCellAttrs(cell).cellMarker;
21
+ if (marker?.kind !== "merge" || revisionSet !== null && !revisionSet.has(marker.info.revisionId)) continue;
22
+ const continues = isTableCellMergeRevisionContinuation(mode === "accept" ? marker.verticalMerge : marker.verticalMergeOriginal);
23
+ decisions.set(position, continues && (mode === "reject" || !tableCellHasMeaningfulContent(standaloneTableCellFromProseMirror(cell))));
24
+ if (!continues) continue;
25
+ if (Math.floor(index / map.width) === 0) {
26
+ invalid.add(position);
27
+ continue;
28
+ }
29
+ const abovePosition = map.map[index - map.width];
30
+ if (abovePosition === void 0) return panic("Merge resolution lost its origin");
31
+ const above = table.nodeAt(abovePosition);
32
+ if (!above) return panic("Merge resolution lost its origin");
33
+ const rectangle = map.findCell(position);
34
+ const aboveRectangle = map.findCell(abovePosition);
35
+ if (rectangle.left !== aboveRectangle.left || rectangle.right !== aboveRectangle.right || rectangle.top !== aboveRectangle.bottom) {
36
+ invalid.add(position);
37
+ continue;
38
+ }
39
+ const aboveMarker = expectTableCellAttrs(above).cellMarker;
40
+ const aboveContinues = aboveMarker?.kind === "merge" && (revisionSet === null || revisionSet.has(aboveMarker.info.revisionId)) && isTableCellMergeRevisionContinuation(mode === "accept" ? aboveMarker.verticalMerge : aboveMarker.verticalMergeOriginal);
41
+ const aboveModel = standaloneTableCellFromProseMirror(above);
42
+ if (mode === "accept" && (aboveContinues && decisions.get(abovePosition) === false || isCellMergeContinuation(aboveModel) && tableCellHasMeaningfulContent(aboveModel))) decisions.set(position, false);
43
+ if (aboveContinues || isCellMergeContinuation(aboveModel) || isCellMergeStart(aboveModel)) continue;
44
+ origins.add(abovePosition);
45
+ }
46
+ return {
47
+ folds: decisions,
48
+ origins,
49
+ invalid
50
+ };
51
+ };
52
+ /** Persist an implicit merge origin even when reader rules retain its continuation. */
53
+ const resolvedTableMergeOriginAttrs = (cell) => ({
54
+ ...cell.attrs,
55
+ _originalFormatting: {
56
+ ...expectTableCellAttrs(cell)._originalFormatting,
57
+ vMerge: "restart"
58
+ },
59
+ _preserveVMergeRestart: true
60
+ });
61
+ /** A retained continuation must save the merge state the reviewer resolved. */
62
+ const resolvedVisibleTableCellMergeAttrs = ({ cell, mode, origin }) => {
63
+ const attrs = expectTableCellAttrs(cell);
64
+ const marker = attrs.cellMarker;
65
+ if (marker?.kind !== "merge") return panic("Cell has no merge revision");
66
+ const continuation = mode === "accept" ? marker.verticalMerge : marker.verticalMergeOriginal;
67
+ const formatting = { ...attrs._originalFormatting };
68
+ if (origin) formatting.vMerge = "restart";
69
+ else if (isTableCellMergeRevisionContinuation(continuation)) formatting.vMerge = "continue";
70
+ else delete formatting.vMerge;
71
+ return {
72
+ ...cell.attrs,
73
+ cellMarker: null,
74
+ _originalFormatting: formatting,
75
+ _preserveVMergeRestart: origin
76
+ };
77
+ };
78
+ //#endregion
79
+ export { resolvedTableMergeOriginAttrs, resolvedVisibleTableCellMergeAttrs, tableMergeFoldDecisions };
@@ -3,30 +3,23 @@ import { EditorView } from "prosemirror-view";
3
3
  //#region src/prosemirror/commentIdAllocator.d.ts
4
4
  /** Sentinel ID for a comment that hasn't been persisted yet (anchored to selection). */
5
5
  declare const PENDING_COMMENT_ID = -1;
6
+ declare const CommentIdSpaceExhaustedError_base: import("better-result").TaggedErrorClass<"CommentIdSpaceExhaustedError">;
7
+ declare class CommentIdSpaceExhaustedError extends CommentIdSpaceExhaustedError_base<{
8
+ message: string;
9
+ }> {}
10
+ /** Allocate a valid, unused comment ID in this realm. */
11
+ declare function allocateCommentId(): number;
12
+ /** Reserve a loaded ID and advance past it when the upper bound permits. */
13
+ declare function seedCommentIdAbove(id: number): void;
6
14
  type CommentIdAllocator = {
7
- /** Allocate the next ID and advance the counter. */
15
+ /** Allocate the next unused ID. */
8
16
  next(): number;
9
- /**
10
- * On document load, bump the counter above the highest ID found in the
11
- * loaded comments and tracked-change marks so subsequent allocations don't
12
- * collide with already-present IDs.
13
- */
14
- seedAbove(maxId: number): void;
17
+ /** Reserve a loaded ID and seed above it when possible. */
18
+ seedAbove(id: number): void;
15
19
  };
16
- /**
17
- * Create an instance-scoped monotonic comment/revision ID allocator. IDs are
18
- * never reused (deleting a comment does not free its ID), and the counter is
19
- * private to this allocator — multiple editors get independent ID spaces.
20
- */
20
+ /** Every handle uses the same counter, including separate editor instances. */
21
21
  declare function createCommentIdAllocator(): CommentIdAllocator;
22
- /**
23
- * Seed an allocator above every comment/revision ID currently in the document
24
- * — comment objects (including replies, which carry no mark) plus
25
- * tracked-change `revisionId` marks. Because `seedAbove` only ever raises the
26
- * counter, this is safe to call on load (React) or before each allocation
27
- * (Vue): new IDs never collide with or reuse an existing one, and the comment
28
- * and revision ID spaces stay unified.
29
- */
30
- declare function seedCommentAllocator(allocator: CommentIdAllocator, comments: Comment[] | undefined, view: EditorView | null): void;
22
+ /** Seed from comments, replies, and the document's conservative annotation maximum. */
23
+ declare function seedCommentAllocator(allocator: CommentIdAllocator, comments: Comment[] | undefined, view: Pick<EditorView, "state"> | null): void;
31
24
  //#endregion
32
- export { CommentIdAllocator, PENDING_COMMENT_ID, createCommentIdAllocator, seedCommentAllocator };
25
+ export { CommentIdAllocator, CommentIdSpaceExhaustedError, PENDING_COMMENT_ID, allocateCommentId, createCommentIdAllocator, seedCommentAllocator, seedCommentIdAbove };
@@ -1,40 +1,65 @@
1
1
  import { maxAnnotationIdInDoc, seedRevisionIdsAbove } from "./plugins/revisionIds.js";
2
+ import { readRangeAnchorAttrs } from "./rangeAnchorAttrs.js";
3
+ import { TaggedError } from "better-result";
2
4
  import { MAX_REVISION_ID } from "@stll/docx-core/model";
3
5
  //#region src/prosemirror/commentIdAllocator.ts
4
- /** Sentinel ID for a comment that hasn't been persisted yet (anchored to selection). */
5
- const PENDING_COMMENT_ID = -1;
6
6
  /**
7
- * Create an instance-scoped monotonic comment/revision ID allocator. IDs are
8
- * never reused (deleting a comment does not free its ID), and the counter is
9
- * private to this allocator — multiple editors get independent ID spaces.
7
+ * Realm-wide comment ID allocation, shared by live editors and headless reviewers.
8
+ * Loaded and issued IDs stay reserved even when a comment is deleted.
10
9
  */
10
+ /** Sentinel ID for a comment that hasn't been persisted yet (anchored to selection). */
11
+ const PENDING_COMMENT_ID = -1;
12
+ var CommentIdSpaceExhaustedError = class extends TaggedError("CommentIdSpaceExhaustedError") {};
13
+ let nextId = 1;
14
+ const reservedIds = /* @__PURE__ */ new Set();
15
+ /** Allocate a valid, unused comment ID in this realm. */
16
+ function allocateCommentId() {
17
+ if (reservedIds.size >= MAX_REVISION_ID) throw new CommentIdSpaceExhaustedError({ message: "The OOXML comment ID space is exhausted." });
18
+ while (reservedIds.has(nextId)) nextId = nextId === MAX_REVISION_ID ? 1 : nextId + 1;
19
+ const id = nextId;
20
+ reservedIds.add(id);
21
+ nextId = id === MAX_REVISION_ID ? 1 : id + 1;
22
+ return id;
23
+ }
24
+ /** Reserve a loaded ID and advance past it when the upper bound permits. */
25
+ function seedCommentIdAbove(id) {
26
+ if (!Number.isInteger(id) || id < 0 || id > MAX_REVISION_ID) return;
27
+ if (id > 0) reservedIds.add(id);
28
+ if (id >= nextId) nextId = id === MAX_REVISION_ID ? 1 : id + 1;
29
+ }
30
+ /** Every handle uses the same counter, including separate editor instances. */
11
31
  function createCommentIdAllocator() {
12
- let nextId = 1;
13
32
  return {
14
- next: () => {
15
- if (nextId > MAX_REVISION_ID) nextId = 1;
16
- return nextId++;
17
- },
18
- seedAbove(maxId) {
19
- if (!Number.isInteger(maxId) || maxId < 0 || maxId >= MAX_REVISION_ID) return;
20
- if (maxId >= nextId) nextId = maxId + 1;
21
- }
33
+ next: allocateCommentId,
34
+ seedAbove: seedCommentIdAbove
22
35
  };
23
36
  }
24
- /**
25
- * Seed an allocator above every comment/revision ID currently in the document
26
- * — comment objects (including replies, which carry no mark) plus
27
- * tracked-change `revisionId` marks. Because `seedAbove` only ever raises the
28
- * counter, this is safe to call on load (React) or before each allocation
29
- * (Vue): new IDs never collide with or reuse an existing one, and the comment
30
- * and revision ID spaces stay unified.
31
- */
37
+ /** Seed from comments, replies, and the document's conservative annotation maximum. */
32
38
  function seedCommentAllocator(allocator, comments, view) {
33
39
  let max = 0;
34
- for (const comment of comments ?? []) if (Number.isInteger(comment.id) && comment.id > max && comment.id <= MAX_REVISION_ID) max = comment.id;
35
- if (view) max = Math.max(max, maxAnnotationIdInDoc(view.state.doc));
40
+ for (const comment of comments ?? []) {
41
+ allocator.seedAbove(comment.id);
42
+ if (Number.isInteger(comment.id) && comment.id > max && comment.id <= MAX_REVISION_ID) max = comment.id;
43
+ }
44
+ if (view) {
45
+ view.state.doc.descendants((node) => {
46
+ for (const mark of node.marks) {
47
+ const id = mark.attrs["commentId"];
48
+ if (typeof id === "number") allocator.seedAbove(id);
49
+ }
50
+ if (node.type.name === "commentReference") {
51
+ const id = node.attrs["commentId"];
52
+ if (typeof id === "number") allocator.seedAbove(id);
53
+ }
54
+ if (node.type.name === "rangeAnchor") {
55
+ const attrs = readRangeAnchorAttrs(node);
56
+ if (attrs.ok && attrs.value.start.type === "commentRangeStart") allocator.seedAbove(attrs.value.start.id);
57
+ }
58
+ });
59
+ max = Math.max(max, maxAnnotationIdInDoc(view.state.doc));
60
+ }
36
61
  allocator.seedAbove(max);
37
62
  seedRevisionIdsAbove(max);
38
63
  }
39
64
  //#endregion
40
- export { PENDING_COMMENT_ID, createCommentIdAllocator, seedCommentAllocator };
65
+ export { CommentIdSpaceExhaustedError, PENDING_COMMENT_ID, allocateCommentId, createCommentIdAllocator, seedCommentAllocator, seedCommentIdAbove };
@@ -23,11 +23,6 @@ type ToProseDocOptions = {
23
23
  * @param options - Conversion options including style definitions
24
24
  */
25
25
  declare function toProseDoc(document: document_d_exports.Document, options?: ToProseDocOptions): Node;
26
- /**
27
- * Whether a cell carries content a vertical-merge continuation must not hide.
28
- * The reader keeps such a continuation as a cell of its own.
29
- */
30
- declare function tableCellHasMeaningfulContent(cell: document_d_exports.TableCell): boolean;
31
26
  declare function standaloneTableCellToProseMirror(cell: document_d_exports.TableCell, nodeType: "tableCell" | "tableHeader"): Node;
32
27
  /**
33
28
  * Convert HeaderFooter content (array of Paragraph/Table blocks) to a ProseMirror document.
@@ -44,4 +39,4 @@ declare function footnoteToProseDoc(content: document_d_exports.BlockContent[],
44
39
  */
45
40
  declare function createEmptyDoc(): Node;
46
41
  //#endregion
47
- export { ToProseDocOptions, createEmptyDoc, footnoteToProseDoc, headerFooterToProseDoc, standaloneTableCellToProseMirror, tableCellHasMeaningfulContent, toProseDoc };
42
+ export { ToProseDocOptions, createEmptyDoc, footnoteToProseDoc, headerFooterToProseDoc, standaloneTableCellToProseMirror, toProseDoc };
@@ -6,6 +6,7 @@ import { isBaselineVertAlign } from "../../docx/runParser.js";
6
6
  import { replayableShapeAlternateContent } from "../../docx/shapeAlternateContent.js";
7
7
  import { resolveTableLook } from "../../docx/tableLook.js";
8
8
  import { isCellMergeContinuation } from "../../docx/tableParser.js";
9
+ import { calculateRowSpans } from "../../docx/verticalMergeProjection.js";
9
10
  import { copiedWrapPolygon } from "../../docx/wrapPolygon.js";
10
11
  import { buildPageBreakRunSourceDescendantIndex } from "../../internal/pageBreakRunSourceDescendantIndex.js";
11
12
  import { createStyleEngine } from "../../style-engine/styleEngine.js";
@@ -893,122 +894,6 @@ function resolveTextFormatting(formatting, styleResolver) {
893
894
  if (!styleResolver) return formatting;
894
895
  return mergeTextFormatting(styleResolver.resolveRunStyle(formatting.styleId), formatting);
895
896
  }
896
- function calculateRowSpans(table) {
897
- const result = /* @__PURE__ */ new Map();
898
- const numRows = table.rows.length;
899
- const activeMerges = /* @__PURE__ */ new Map();
900
- for (let rowIndex = 0; rowIndex < numRows; rowIndex++) {
901
- const row = table.rows[rowIndex];
902
- if (row.cells.length === 0) {
903
- clearActiveVerticalMerges(activeMerges, result);
904
- continue;
905
- }
906
- let colIndex = row.formatting?.gridBefore ?? 0;
907
- const rowCells = row.cells.map((cell) => {
908
- const colspan = cell.formatting?.gridSpan ?? 1;
909
- const vMerge = cell.formatting?.vMerge;
910
- const isMergeContinuation = isCellMergeContinuation(cell);
911
- const startRow = isMergeContinuation ? activeMerges.get(colIndex) : void 0;
912
- const info = {
913
- cell,
914
- colIndex,
915
- colspan,
916
- vMerge,
917
- isMergeContinuation,
918
- startRow,
919
- hasMeaningfulContent: tableCellHasMeaningfulContent(cell),
920
- shouldSkip: isMergeContinuation && startRow !== void 0
921
- };
922
- colIndex += colspan;
923
- return info;
924
- });
925
- const rowWouldBeEmpty = rowCells.length > 0 && rowCells.every((cell) => cell.shouldSkip);
926
- for (const cellInfo of rowCells) {
927
- const { colIndex: cellColIndex, vMerge, isMergeContinuation, startRow, hasMeaningfulContent } = cellInfo;
928
- const key = `${rowIndex}-${cellColIndex}`;
929
- if (vMerge === "restart") {
930
- closeVerticalMerge(activeMerges, result, cellColIndex);
931
- activeMerges.set(cellColIndex, rowIndex);
932
- result.set(key, {
933
- rowSpan: 1,
934
- skip: false
935
- });
936
- } else if (isMergeContinuation) {
937
- if (startRow === void 0 || rowWouldBeEmpty || hasMeaningfulContent) {
938
- result.set(key, {
939
- rowSpan: 1,
940
- skip: false
941
- });
942
- if ((rowWouldBeEmpty || hasMeaningfulContent) && startRow !== void 0) {
943
- const restartCell = result.get(`${startRow}-${cellColIndex}`);
944
- if (restartCell) restartCell.preserveVMergeRestart = true;
945
- activeMerges.delete(cellColIndex);
946
- }
947
- continue;
948
- }
949
- const startKey = `${startRow}-${cellColIndex}`;
950
- const startCell = result.get(startKey);
951
- if (startCell) {
952
- startCell.rowSpan++;
953
- startCell.continuationCells ??= [];
954
- startCell.continuationCells.push(cellInfo.cell);
955
- }
956
- result.set(key, {
957
- rowSpan: 1,
958
- skip: true
959
- });
960
- } else {
961
- closeVerticalMerge(activeMerges, result, cellColIndex);
962
- result.set(key, {
963
- rowSpan: 1,
964
- skip: false
965
- });
966
- }
967
- }
968
- }
969
- clearActiveVerticalMerges(activeMerges, result);
970
- return result;
971
- }
972
- /**
973
- * End the vertical merge active in one column.
974
- *
975
- * A `w:vMerge="restart"` is carried through the editor by its cell's rowspan,
976
- * which only exists once a continuation joins it. A merge that closes with a
977
- * rowspan of 1 — a restart with no continuation, one interrupted by a plain
978
- * cell, one whose continuation is in a row a revision removed — has nothing
979
- * but this flag to say the cell was a merge origin, and dropping it changes
980
- * the table's visible structure.
981
- */
982
- function closeVerticalMerge(activeMerges, result, colIndex) {
983
- const startRow = activeMerges.get(colIndex);
984
- if (startRow === void 0) return;
985
- const restartCell = result.get(`${startRow}-${colIndex}`);
986
- if (restartCell && restartCell.rowSpan === 1) restartCell.preserveVMergeRestart = true;
987
- activeMerges.delete(colIndex);
988
- }
989
- function clearActiveVerticalMerges(activeMerges, result) {
990
- for (const colIndex of [...activeMerges.keys()]) closeVerticalMerge(activeMerges, result, colIndex);
991
- }
992
- /**
993
- * Whether a cell carries content a vertical-merge continuation must not hide.
994
- * The reader keeps such a continuation as a cell of its own.
995
- */
996
- function tableCellHasMeaningfulContent(cell) {
997
- return cell.content.some(blockHasMeaningfulContent);
998
- }
999
- function blockHasMeaningfulContent(block) {
1000
- if (block.type === "preservedBlock") return true;
1001
- if (block.type === "bookmarkStart" || block.type === "bookmarkEnd") return true;
1002
- if (block.type === "table") return block.rows.some((row) => row.cells.some((cell) => tableCellHasMeaningfulContent(cell)));
1003
- if (block.type === "blockSdt" || block.type === "blockCustomXml") return true;
1004
- return block.content.some(paragraphContentHasMeaningfulContent);
1005
- }
1006
- function paragraphContentHasMeaningfulContent(content) {
1007
- if (content.type === "run") return content.content.length > 0;
1008
- if (content.type === "hyperlink") return content.children.some(paragraphContentHasMeaningfulContent);
1009
- if (content.type === "insertion" || content.type === "deletion" || content.type === "moveFrom" || content.type === "moveTo") return content.content.some(paragraphContentHasMeaningfulContent);
1010
- return true;
1011
- }
1012
897
  function convertTable(table, styleResolver, context) {
1013
898
  for (const row of table.rows) for (const cell of row.cells) {
1014
899
  if (isCellMergeContinuation(cell) && context.pageBreakRunSourceDescendants.containsPageBreakRun(cell.content)) {
@@ -2840,4 +2725,4 @@ function createEmptyDoc() {
2840
2725
  return schema.node("doc", null, [schema.node("paragraph", {}, [])]);
2841
2726
  }
2842
2727
  //#endregion
2843
- export { createEmptyDoc, footnoteToProseDoc, headerFooterToProseDoc, standaloneTableCellToProseMirror, tableCellHasMeaningfulContent, toProseDoc };
2728
+ export { createEmptyDoc, footnoteToProseDoc, headerFooterToProseDoc, standaloneTableCellToProseMirror, toProseDoc };
@@ -1,5 +1,97 @@
1
1
  import { createExtension } from "../create.js";
2
- import { history, redo, undo } from "prosemirror-history";
2
+ import { Plugin } from "prosemirror-state";
3
+ import { closeHistory, history, isHistoryTransaction, redo, undo } from "prosemirror-history";
4
+ //#region src/prosemirror/extensions/core/HistoryExtension.ts
5
+ /**
6
+ * History Extension — undo/redo via prosemirror-history
7
+ */
8
+ /**
9
+ * History adjacency is based on step maps, which differ for physical deletions
10
+ * and suggestion marks. Group continuous typing and same-direction deletions explicitly; each
11
+ * structural edit, paste, drop, or composition starts its own undo event.
12
+ * Capture DOM input before the shared input router or a keymap handles it.
13
+ */
14
+ const inputBoundaryPlugin = (newGroupDelay) => {
15
+ let previousKind;
16
+ let deletionGroup = 0;
17
+ let previousInputTime = 0;
18
+ let pendingDeletionGroup = null;
19
+ return new Plugin({
20
+ filterTransaction(tr) {
21
+ const pendingGroup = pendingDeletionGroup;
22
+ pendingDeletionGroup = null;
23
+ if (isHistoryTransaction(tr) || !tr.docChanged && tr.selectionSet) {
24
+ previousKind = void 0;
25
+ return true;
26
+ }
27
+ if (tr.docChanged && (previousKind === "deleteBackward" || previousKind === "deleteForward" || previousKind === "deletion")) {
28
+ if (pendingGroup !== null) tr.setMeta("composition", pendingGroup);
29
+ else if (!tr.getMeta("appendedTransaction")) previousKind = void 0;
30
+ }
31
+ return true;
32
+ },
33
+ view(view) {
34
+ const startInput = (kind) => {
35
+ const now = Date.now();
36
+ if (kind !== previousKind || kind === "structure" || kind === "paste" || kind === "drop" || now - previousInputTime > newGroupDelay) {
37
+ deletionGroup--;
38
+ view.dispatch(closeHistory(view.state.tr));
39
+ }
40
+ previousInputTime = now;
41
+ previousKind = kind;
42
+ pendingDeletionGroup = kind === "deleteBackward" || kind === "deleteForward" || kind === "deletion" ? deletionGroup : null;
43
+ };
44
+ const keydown = (event) => {
45
+ if (event.isComposing) return;
46
+ if (event.key === "Backspace" || event.key === "Delete") {
47
+ startInput(event.key === "Backspace" ? "deleteBackward" : "deleteForward");
48
+ return;
49
+ }
50
+ if (event.key === "Enter") {
51
+ startInput("structure");
52
+ return;
53
+ }
54
+ if (event.key.length === 1 && !event.metaKey && !event.ctrlKey) startInput("typing");
55
+ };
56
+ const beforeinput = (event) => {
57
+ if (event.inputType === "insertCompositionText" || event.isComposing) {
58
+ startInput("composition");
59
+ return;
60
+ }
61
+ if (event.inputType === "insertText" || event.inputType === "insertReplacementText") {
62
+ startInput("typing");
63
+ return;
64
+ }
65
+ if (event.inputType.startsWith("delete")) if (event.inputType.endsWith("Backward")) startInput("deleteBackward");
66
+ else if (event.inputType.endsWith("Forward")) startInput("deleteForward");
67
+ else startInput("deletion");
68
+ else if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") startInput("structure");
69
+ };
70
+ const paste = () => startInput("paste");
71
+ const drop = () => startInput("drop");
72
+ const compositionstart = () => {
73
+ view.dispatch(closeHistory(view.state.tr));
74
+ previousKind = "composition";
75
+ };
76
+ view.dom.addEventListener("keydown", keydown, true);
77
+ view.dom.addEventListener("beforeinput", beforeinput, true);
78
+ view.dom.addEventListener("paste", paste, true);
79
+ view.dom.addEventListener("drop", drop, true);
80
+ view.dom.addEventListener("compositionstart", compositionstart, true);
81
+ return { destroy() {
82
+ view.dom.removeEventListener("keydown", keydown, true);
83
+ view.dom.removeEventListener("beforeinput", beforeinput, true);
84
+ view.dom.removeEventListener("paste", paste, true);
85
+ view.dom.removeEventListener("drop", drop, true);
86
+ view.dom.removeEventListener("compositionstart", compositionstart, true);
87
+ } };
88
+ }
89
+ });
90
+ };
91
+ const consumeHistoryShortcut = (command) => (state, dispatch, view) => {
92
+ command(state, dispatch, view);
93
+ return true;
94
+ };
3
95
  const HistoryExtension = createExtension({
4
96
  name: "history",
5
97
  defaultOptions: {
@@ -12,15 +104,15 @@ const HistoryExtension = createExtension({
12
104
  plugins: [history({
13
105
  depth: options.depth,
14
106
  newGroupDelay: options.newGroupDelay
15
- })],
107
+ }), inputBoundaryPlugin(options.newGroupDelay)],
16
108
  commands: {
17
109
  undo: () => undo,
18
110
  redo: () => redo
19
111
  },
20
112
  ...options.shortcuts === "editor" && { keyboardShortcuts: {
21
- "Mod-z": undo,
22
- "Mod-y": redo,
23
- "Mod-Shift-z": redo
113
+ "Mod-z": consumeHistoryShortcut(undo),
114
+ "Mod-y": consumeHistoryShortcut(redo),
115
+ "Mod-Shift-z": consumeHistoryShortcut(redo)
24
116
  } }
25
117
  };
26
118
  }