@stll/folio-core 0.49.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/ai-edits/headless.d.ts +37 -1
  2. package/dist/ai-edits/headless.js +47 -16
  3. package/dist/ai-edits/word-diff.js +134 -1
  4. package/dist/compare/content-alignment.js +220 -5
  5. package/dist/compare/content.d.ts +1 -0
  6. package/dist/compare/content.js +51 -20
  7. package/dist/display-list/dom/renderDisplayListToDom.d.ts +10 -1
  8. package/dist/display-list/dom/renderDisplayListToDom.js +8 -7
  9. package/dist/display-list/html/renderDisplayListToHtml.d.ts +21 -0
  10. package/dist/display-list/html/renderDisplayListToHtml.js +126 -0
  11. package/dist/display-list/selectDisplayPages.d.ts +6 -0
  12. package/dist/display-list/selectDisplayPages.js +44 -0
  13. package/dist/docx/selectiveSave.d.ts +1 -1
  14. package/dist/docx/selectiveSave.js +16 -6
  15. package/dist/docx/structuralXmlPatch.d.ts +14 -0
  16. package/dist/docx/structuralXmlPatch.js +209 -0
  17. package/dist/export-pdf.d.ts +35 -2
  18. package/dist/export-pdf.js +51 -20
  19. package/dist/fonts/fontsourceFaces.d.ts +47 -0
  20. package/dist/fonts/fontsourceFaces.js +230 -0
  21. package/dist/layout-bridge/convert/flowBlockPostprocessing.d.ts +8 -0
  22. package/dist/layout-bridge/convert/flowBlockPostprocessing.js +106 -0
  23. package/dist/layout-bridge/convert/flowBorders.d.ts +17 -0
  24. package/dist/layout-bridge/convert/flowBorders.js +31 -0
  25. package/dist/layout-bridge/convert/flowConversionShared.d.ts +111 -0
  26. package/dist/layout-bridge/convert/flowConversionShared.js +23 -0
  27. package/dist/layout-bridge/convert/footnoteLayout.d.ts +2 -1
  28. package/dist/layout-bridge/convert/footnoteLayout.js +2 -1
  29. package/dist/layout-bridge/convert/headerFooterLayout.d.ts +2 -1
  30. package/dist/layout-bridge/convert/imageConversion.d.ts +33 -0
  31. package/dist/layout-bridge/convert/imageConversion.js +118 -0
  32. package/dist/layout-bridge/convert/listMarkers.d.ts +28 -0
  33. package/dist/layout-bridge/convert/listMarkers.js +98 -0
  34. package/dist/layout-bridge/convert/noteReferences.d.ts +14 -0
  35. package/dist/layout-bridge/convert/noteReferences.js +29 -0
  36. package/dist/layout-bridge/convert/pageBreakSplitting.d.ts +16 -0
  37. package/dist/layout-bridge/convert/pageBreakSplitting.js +140 -0
  38. package/dist/layout-bridge/convert/paragraphAttrs.d.ts +20 -0
  39. package/dist/layout-bridge/convert/paragraphAttrs.js +234 -0
  40. package/dist/layout-bridge/convert/paragraphConversion.d.ts +12 -0
  41. package/dist/layout-bridge/convert/paragraphConversion.js +112 -0
  42. package/dist/layout-bridge/convert/paragraphRuns.d.ts +15 -0
  43. package/dist/layout-bridge/convert/paragraphRuns.js +281 -0
  44. package/dist/layout-bridge/convert/runFormattingMerge.d.ts +19 -0
  45. package/dist/layout-bridge/convert/runFormattingMerge.js +167 -0
  46. package/dist/layout-bridge/convert/runMarkFormatting.d.ts +11 -0
  47. package/dist/layout-bridge/convert/runMarkFormatting.js +291 -0
  48. package/dist/layout-bridge/convert/sectionBoundaries.d.ts +35 -0
  49. package/dist/layout-bridge/convert/sectionBoundaries.js +114 -0
  50. package/dist/layout-bridge/convert/tableConversion.d.ts +12 -0
  51. package/dist/layout-bridge/convert/tableConversion.js +303 -0
  52. package/dist/layout-bridge/convert/textBoxFill.d.ts +20 -0
  53. package/dist/layout-bridge/convert/textBoxFill.js +42 -0
  54. package/dist/layout-bridge/convert/textFormattingConversion.d.ts +22 -0
  55. package/dist/layout-bridge/convert/textFormattingConversion.js +120 -0
  56. package/dist/layout-bridge/convert/toFlowBlocks.d.ts +5 -113
  57. package/dist/layout-bridge/convert/toFlowBlocks.js +11 -2082
  58. package/dist/layout-bridge/engine/hitTest.js +1 -1
  59. package/dist/layout-bridge/engine/measuring/index.d.ts +2 -1
  60. package/dist/layout-bridge/engine/measuring/index.js +2 -1
  61. package/dist/layout-bridge/engine/selectionRects.js +1 -1
  62. package/dist/layout-engine/imageLayout.d.ts +9 -0
  63. package/dist/layout-engine/imageLayout.js +48 -0
  64. package/dist/layout-engine/index.d.ts +3 -18
  65. package/dist/layout-engine/index.js +12 -937
  66. package/dist/layout-engine/layoutFlowShared.d.ts +6 -0
  67. package/dist/layout-engine/layoutFlowShared.js +8 -0
  68. package/dist/layout-engine/measure/crossRunWords.d.ts +45 -0
  69. package/dist/layout-engine/measure/crossRunWords.js +172 -0
  70. package/dist/layout-engine/measure/floatingLineClearance.d.ts +24 -0
  71. package/dist/layout-engine/measure/floatingLineClearance.js +44 -0
  72. package/dist/layout-engine/measure/inlineObjectMetrics.d.ts +6 -0
  73. package/dist/layout-engine/measure/inlineObjectMetrics.js +52 -0
  74. package/dist/layout-engine/measure/justification.d.ts +32 -0
  75. package/dist/layout-engine/measure/justification.js +83 -0
  76. package/dist/layout-engine/measure/lineFitting.d.ts +48 -0
  77. package/dist/layout-engine/measure/lineFitting.js +145 -0
  78. package/dist/layout-engine/measure/lineTypography.d.ts +44 -0
  79. package/dist/layout-engine/measure/lineTypography.js +124 -0
  80. package/dist/layout-engine/measure/measureBlocks.js +2 -1
  81. package/dist/layout-engine/measure/measureParagraph.d.ts +1 -20
  82. package/dist/layout-engine/measure/measureParagraph.js +32 -734
  83. package/dist/layout-engine/measure/paragraphMeasureShared.d.ts +109 -0
  84. package/dist/layout-engine/measure/paragraphMeasureShared.js +82 -0
  85. package/dist/layout-engine/measure/paragraphTabs.d.ts +18 -0
  86. package/dist/layout-engine/measure/paragraphTabs.js +78 -0
  87. package/dist/layout-engine/paragraphLayout.d.ts +23 -0
  88. package/dist/layout-engine/paragraphLayout.js +204 -0
  89. package/dist/layout-engine/sectionLayout.d.ts +45 -0
  90. package/dist/layout-engine/sectionLayout.js +216 -0
  91. package/dist/layout-engine/tableLayout.d.ts +17 -0
  92. package/dist/layout-engine/tableLayout.js +369 -0
  93. package/dist/layout-engine/textBoxLayout.d.ts +15 -0
  94. package/dist/layout-engine/textBoxLayout.js +137 -0
  95. package/dist/layout-painter/renderPage.js +1 -0
  96. package/dist/managers/editorShortcuts.d.ts +16 -3
  97. package/dist/managers/editorShortcuts.js +6 -4
  98. package/dist/paged-layout/sectionBlockWidths.d.ts +2 -1
  99. package/dist/paged-layout/sectionBlockWidths.js +1 -1
  100. package/dist/prosemirror/extensions/StarterKit.d.ts +3 -0
  101. package/dist/prosemirror/extensions/StarterKit.js +2 -1
  102. package/dist/prosemirror/extensions/core/HistoryExtension.d.ts +9 -1
  103. package/dist/prosemirror/extensions/core/HistoryExtension.js +4 -3
  104. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts +12 -5
  105. package/dist/prosemirror/extensions/features/ParagraphChangeTrackerExtension.js +91 -33
  106. package/dist/server.d.ts +3 -2
  107. package/package.json +2 -2
@@ -0,0 +1,137 @@
1
+ import { emuToPixels } from "../utils/units.js";
2
+ import { bandFragmentX, bandTopContentY, isPageFrameRelativeAnchor } from "./textBoxFlow.js";
3
+ import { floatingTextBoxReservesBand } from "./types.js";
4
+ //#region src/layout-engine/textBoxLayout.ts
5
+ /**
6
+ * Places text boxes onto pages: inline and anchored placement, page-pinned
7
+ * wrap bands, and host-paragraph line offsets.
8
+ */
9
+ const TEXT_BOX_ANCHOR_BLOCK_ID = Symbol.for("stll.textBoxAnchorBlockId");
10
+ const readTextBoxAnchorBlockId = (block) => Reflect.get(block, TEXT_BOX_ANCHOR_BLOCK_ID);
11
+ /**
12
+ * Layout a text box block onto pages.
13
+ */
14
+ function layoutTextBox(block, measure, { paginator, sectionMarginTop, sectionPageHeight, sectionMarginBottom }) {
15
+ if (isPagePinnedBandTextBox(block)) {
16
+ const state = paginator.getCurrentState();
17
+ const bandTop = bandTopContentY(block.position?.vertical, {
18
+ pageHeight: sectionPageHeight,
19
+ marginTop: sectionMarginTop,
20
+ marginBottom: sectionMarginBottom,
21
+ boxHeight: measure.height
22
+ });
23
+ const x = bandFragmentX(block.position?.horizontal, {
24
+ pageWidth: state.page.size.w,
25
+ marginLeft: state.page.margins.left,
26
+ marginRight: state.page.margins.right,
27
+ activeColumnLeft: paginator.getColumnX(state.columnIndex),
28
+ activeColumnWidth: paginator.columnWidth,
29
+ boxWidth: measure.width
30
+ });
31
+ const fragment = {
32
+ kind: "textBox",
33
+ blockId: block.id,
34
+ x,
35
+ y: block.position?.vertical?.relativeTo === "page" ? sectionMarginTop + bandTop : state.topMargin + bandTop,
36
+ width: measure.width,
37
+ height: measure.height,
38
+ isPositioned: true,
39
+ ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
40
+ ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
41
+ };
42
+ paginator.addUnflowedFragment(fragment);
43
+ return;
44
+ }
45
+ if (block.position !== void 0) {
46
+ const state = paginator.getCurrentState();
47
+ const x = bandFragmentX(block.position.horizontal, {
48
+ pageWidth: state.page.size.w,
49
+ marginLeft: state.page.margins.left,
50
+ marginRight: state.page.margins.right,
51
+ activeColumnLeft: paginator.getColumnX(state.columnIndex),
52
+ activeColumnWidth: paginator.columnWidth,
53
+ boxWidth: measure.width
54
+ });
55
+ const vertical = block.position.vertical;
56
+ const anchorBlockId = readTextBoxAnchorBlockId(block);
57
+ const anchorParagraph = vertical?.relativeTo === "paragraph" && typeof anchorBlockId === "string" ? state.page.fragments.find((fragment) => fragment.kind === "paragraph" && fragment.blockId === anchorBlockId) : void 0;
58
+ const bandGeometry = {
59
+ pageHeight: sectionPageHeight,
60
+ marginTop: sectionMarginTop,
61
+ marginBottom: sectionMarginBottom,
62
+ boxHeight: measure.height
63
+ };
64
+ let y;
65
+ if (vertical?.relativeTo === "page") y = sectionMarginTop + bandTopContentY(vertical, bandGeometry);
66
+ else if (isPageFrameRelativeAnchor(vertical?.relativeTo)) y = state.topMargin + bandTopContentY(vertical, {
67
+ pageHeight: sectionPageHeight,
68
+ marginTop: sectionMarginTop,
69
+ marginBottom: sectionMarginBottom,
70
+ boxHeight: measure.height
71
+ });
72
+ else y = (anchorParagraph?.y ?? state.cursorY) + emuToPixels(vertical?.posOffset ?? 0);
73
+ const fragment = {
74
+ kind: "textBox",
75
+ blockId: block.id,
76
+ x,
77
+ y,
78
+ width: measure.width,
79
+ height: measure.height,
80
+ isPositioned: true,
81
+ ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
82
+ ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
83
+ };
84
+ paginator.addUnflowedFragment(fragment);
85
+ return;
86
+ }
87
+ const effect = block.effectExtent;
88
+ const occupiedHeight = measure.height + (effect?.top ?? 0) + (effect?.bottom ?? 0);
89
+ const occupiedWidth = measure.width + (effect?.left ?? 0) + (effect?.right ?? 0);
90
+ const host = block.hostParagraph;
91
+ const fragment = {
92
+ kind: "textBox",
93
+ blockId: block.id,
94
+ x: 0,
95
+ y: 0,
96
+ width: measure.width,
97
+ height: measure.height,
98
+ ...block.pmStart !== void 0 ? { pmStart: block.pmStart } : {},
99
+ ...block.pmEnd !== void 0 ? { pmEnd: block.pmEnd } : {}
100
+ };
101
+ const result = paginator.addFragment(fragment, occupiedHeight, host?.spacing?.before ?? 0, host?.spacing?.after ?? 0);
102
+ fragment.x = result.x + hostParagraphLineOffset(host, paginator.columnWidth, occupiedWidth) + (effect?.left ?? 0);
103
+ fragment.y = result.y + (effect?.top ?? 0);
104
+ }
105
+ /**
106
+ * Where a line holding only an object of `width` starts within a column:
107
+ * after the host paragraph's start and first-line indents, then aligned in
108
+ * what remains before its end indent.
109
+ */
110
+ function hostParagraphLineOffset(host, columnWidth, width) {
111
+ if (!host) return 0;
112
+ const indent = host.indent;
113
+ const start = (indent?.left ?? 0) + (indent?.firstLine ?? 0) - (indent?.hanging ?? 0);
114
+ const slack = Math.max(0, columnWidth - start - (indent?.right ?? 0) - width);
115
+ switch (host.alignment) {
116
+ case "center": return start + slack / 2;
117
+ case "right": return start + slack;
118
+ case "left":
119
+ case "justify":
120
+ case void 0: return start;
121
+ default:
122
+ host.alignment;
123
+ return start;
124
+ }
125
+ }
126
+ /**
127
+ * A topAndBottom text box whose vertical anchor pins it to the page frame
128
+ * (page/margin/margin-strip) — it floats to a fixed page position rather than
129
+ * flowing in document order. Must agree with the measure pass's band extraction
130
+ * (extractFloatingZones), which uses the same predicate. eigenpal #694.
131
+ */
132
+ function isPagePinnedBandTextBox(block) {
133
+ if (!floatingTextBoxReservesBand(block)) return false;
134
+ return isPageFrameRelativeAnchor(block.position?.vertical?.relativeTo);
135
+ }
136
+ //#endregion
137
+ export { layoutTextBox };
@@ -1,3 +1,4 @@
1
+ import "../layout-engine/measure/floatingLineClearance.js";
1
2
  import { resolveFloatingTableX } from "../layout-engine/measure/floatingTablePosition.js";
2
3
  import { rectsToFloatingZones } from "../layout-engine/measure/floatingZones.js";
3
4
  import { DEFAULT_FONT_FAMILY } from "../layout-engine/measure/measureHelpers.js";
@@ -1,3 +1,4 @@
1
+ import { HistoryShortcutOwner } from "../prosemirror/extensions/core/HistoryExtension.js";
1
2
  import { EditorState, Transaction } from "prosemirror-state";
2
3
  //#region src/managers/editorShortcuts.d.ts
3
4
  declare function isMacPlatform(): boolean;
@@ -17,19 +18,31 @@ type EditorKeydownIntent = {
17
18
  } | {
18
19
  type: "none";
19
20
  };
21
+ /**
22
+ * An editor shortcut the host binds itself. The editor leaves its keys alone,
23
+ * so the host's binding (an IDE's undo stack, its own print command) is the
24
+ * only one that runs.
25
+ * - `"history"`: undo and redo (Mod-z, Mod-y, Mod-Shift-z).
26
+ * - `"print"`: Cmd/Ctrl+P.
27
+ */
28
+ type HostShortcut = "history" | "print";
29
+ /** Who answers the undo and redo keys when the host owns `hostShortcuts`. */
30
+ declare const historyShortcutOwner: (hostShortcuts: readonly HostShortcut[]) => HistoryShortcutOwner;
20
31
  type ClassifyEditorKeydownOptions = {
21
32
  /** Whether the platform uses Cmd (Mac) rather than Ctrl as the primary modifier. */
22
33
  isMac: boolean;
23
34
  /** Whether focus is in a non-editor input/textarea/contenteditable. */
24
35
  isInputLike: boolean;
36
+ /** Shortcuts the host binds itself; their keys map to `none`. */
37
+ hostShortcuts: readonly HostShortcut[];
25
38
  };
26
39
  /**
27
40
  * Map a keydown to an editor intent:
28
41
  * - Cmd/Ctrl+F or Cmd/Ctrl+H → open find
29
- * - Cmd/Ctrl+P (no auto-repeat) → custom print
42
+ * - Cmd/Ctrl+P (no auto-repeat) → custom print, unless the host owns print
30
43
  * - Delete/Backspace (no modifiers, focus not in an input) → delete selected table
31
44
  */
32
- declare function classifyEditorKeydown(e: KeyboardEvent, { isMac, isInputLike }: ClassifyEditorKeydownOptions): EditorKeydownIntent;
45
+ declare function classifyEditorKeydown(e: Pick<KeyboardEvent, "key" | "metaKey" | "ctrlKey" | "shiftKey" | "altKey" | "repeat">, { isMac, isInputLike, hostShortcuts }: ClassifyEditorKeydownOptions): EditorKeydownIntent;
33
46
  /**
34
47
  * Which key presses the editor's page-level shortcuts answer.
35
48
  * - `"document"`: every press on the page.
@@ -69,4 +82,4 @@ declare function isKeydownInShortcutScope(event: {
69
82
  */
70
83
  declare function deleteSelectedTable(state: EditorState, dispatch: (tr: Transaction) => void): boolean;
71
84
  //#endregion
72
- export { ClassifyEditorKeydownOptions, EditorKeydownIntent, IsKeydownInShortcutScopeOptions, KeyboardShortcutScope, ShortcutScopeRoot, classifyEditorKeydown, deleteSelectedTable, isFocusInInputLike, isKeydownInShortcutScope, isMacPlatform };
85
+ export { ClassifyEditorKeydownOptions, EditorKeydownIntent, HostShortcut, IsKeydownInShortcutScopeOptions, KeyboardShortcutScope, ShortcutScopeRoot, classifyEditorKeydown, deleteSelectedTable, historyShortcutOwner, isFocusInInputLike, isKeydownInShortcutScope, isMacPlatform };
@@ -16,19 +16,21 @@ function isFocusInInputLike(target, editorDom) {
16
16
  if (target.isContentEditable && target !== editorDom) return true;
17
17
  return false;
18
18
  }
19
+ /** Who answers the undo and redo keys when the host owns `hostShortcuts`. */
20
+ const historyShortcutOwner = (hostShortcuts) => hostShortcuts.includes("history") ? "host" : "editor";
19
21
  /**
20
22
  * Map a keydown to an editor intent:
21
23
  * - Cmd/Ctrl+F or Cmd/Ctrl+H → open find
22
- * - Cmd/Ctrl+P (no auto-repeat) → custom print
24
+ * - Cmd/Ctrl+P (no auto-repeat) → custom print, unless the host owns print
23
25
  * - Delete/Backspace (no modifiers, focus not in an input) → delete selected table
24
26
  */
25
- function classifyEditorKeydown(e, { isMac, isInputLike }) {
27
+ function classifyEditorKeydown(e, { isMac, isInputLike, hostShortcuts }) {
26
28
  const cmdOrCtrl = isMac ? e.metaKey : e.ctrlKey;
27
29
  if (!cmdOrCtrl && !e.shiftKey && !e.altKey && (e.key === "Delete" || e.key === "Backspace") && !isInputLike) return { type: "deleteSelectedTable" };
28
30
  if (cmdOrCtrl && !e.shiftKey && !e.altKey) {
29
31
  const key = e.key.toLowerCase();
30
32
  if (key === "f" || key === "h") return { type: "openFind" };
31
- if (key === "p" && !e.repeat) return { type: "print" };
33
+ if (key === "p" && !e.repeat && !hostShortcuts.includes("print")) return { type: "print" };
32
34
  }
33
35
  return { type: "none" };
34
36
  }
@@ -76,4 +78,4 @@ function deleteSelectedTable(state, dispatch) {
76
78
  return false;
77
79
  }
78
80
  //#endregion
79
- export { classifyEditorKeydown, deleteSelectedTable, isFocusInInputLike, isKeydownInShortcutScope, isMacPlatform };
81
+ export { classifyEditorKeydown, deleteSelectedTable, historyShortcutOwner, isFocusInInputLike, isKeydownInShortcutScope, isMacPlatform };
@@ -1,6 +1,7 @@
1
1
  import { FlowBlock } from "../layout-engine/types.js";
2
2
  import { SectionStartAdvance } from "../layout-engine/section-breaks.js";
3
- import { SectionLayoutConfig } from "../layout-engine/index.js";
3
+ import { SectionLayoutConfig } from "../layout-engine/sectionLayout.js";
4
+ import "../layout-engine/index.js";
4
5
  //#region src/paged-layout/sectionBlockWidths.d.ts
5
6
  type ComputePerBlockMeasureInput = {
6
7
  blocks: FlowBlock[];
@@ -1,7 +1,7 @@
1
- import { collectSectionConfigs } from "../layout-engine/index.js";
2
1
  import { hasPageBreakBefore } from "../layout-engine/keep-together.js";
3
2
  import { calculateColumnLefts, calculateColumnWidths } from "../layout-engine/paginator.js";
4
3
  import { physicalColumnRegionIsShared, sectionStartAdvanceOf } from "../layout-engine/section-breaks.js";
4
+ import { collectSectionConfigs } from "../layout-engine/sectionLayout.js";
5
5
  //#region src/paged-layout/sectionBlockWidths.ts
6
6
  function resolvePreBlockPageTransition(block, config) {
7
7
  if (!hasPageBreakBefore(block)) return { type: "none" };
@@ -1,4 +1,5 @@
1
1
  import { AnyExtension } from "./types.js";
2
+ import { HistoryShortcutOwner } from "./core/HistoryExtension.js";
2
3
  import { SelectionChangeCallback } from "../plugins/selectionTracker.js";
3
4
  //#region src/prosemirror/extensions/StarterKit.d.ts
4
5
  type StarterKitOptions = {
@@ -8,6 +9,8 @@ type StarterKitOptions = {
8
9
  historyDepth?: number;
9
10
  /** History new group delay (default: 500) */
10
11
  historyNewGroupDelay?: number;
12
+ /** Who answers the undo and redo keys (default: `"editor"`). */
13
+ historyShortcuts?: HistoryShortcutOwner;
11
14
  /** Selection change callback */
12
15
  onSelectionChange?: SelectionChangeCallback;
13
16
  };
@@ -61,7 +61,8 @@ function createStarterKit(options = {}) {
61
61
  add("paragraph", ParagraphExtension());
62
62
  add("history", HistoryExtension({
63
63
  ...options.historyDepth !== void 0 ? { depth: options.historyDepth } : {},
64
- ...options.historyNewGroupDelay !== void 0 ? { newGroupDelay: options.historyNewGroupDelay } : {}
64
+ ...options.historyNewGroupDelay !== void 0 ? { newGroupDelay: options.historyNewGroupDelay } : {},
65
+ ...options.historyShortcuts !== void 0 ? { shortcuts: options.historyShortcuts } : {}
65
66
  }));
66
67
  for (const name of MARK_NESTING_ORDER) add(name, MARK_EXTENSIONS[name]());
67
68
  add("bookmarkBoundary", BookmarkBoundaryExtension({ getInternalClipboardToken }));
@@ -3,10 +3,18 @@ import { Extension } from "../types.js";
3
3
  /**
4
4
  * History Extension — undo/redo via prosemirror-history
5
5
  */
6
+ /**
7
+ * Who answers the undo and redo keys (Mod-z, Mod-y, Mod-Shift-z).
8
+ * - `"editor"`: the extension binds them. Default.
9
+ * - `"host"`: nothing binds them. The host keeps its own undo stack and runs
10
+ * the `undo` / `redo` commands itself, so one press never undoes twice.
11
+ */
12
+ type HistoryShortcutOwner = "editor" | "host";
6
13
  type HistoryOptions = {
7
14
  depth: number;
8
15
  newGroupDelay: number;
16
+ shortcuts: HistoryShortcutOwner;
9
17
  };
10
18
  declare const HistoryExtension: (options?: Partial<HistoryOptions> | undefined) => Extension;
11
19
  //#endregion
12
- export { HistoryExtension };
20
+ export { HistoryExtension, HistoryShortcutOwner };
@@ -4,7 +4,8 @@ const HistoryExtension = createExtension({
4
4
  name: "history",
5
5
  defaultOptions: {
6
6
  depth: 100,
7
- newGroupDelay: 500
7
+ newGroupDelay: 500,
8
+ shortcuts: "editor"
8
9
  },
9
10
  onSchemaReady(_ctx, options) {
10
11
  return {
@@ -16,11 +17,11 @@ const HistoryExtension = createExtension({
16
17
  undo: () => undo,
17
18
  redo: () => redo
18
19
  },
19
- keyboardShortcuts: {
20
+ ...options.shortcuts === "editor" && { keyboardShortcuts: {
20
21
  "Mod-z": undo,
21
22
  "Mod-y": redo,
22
23
  "Mod-Shift-z": redo
23
- }
24
+ } }
24
25
  };
25
26
  }
26
27
  });
@@ -3,15 +3,15 @@ import { RemovedSectionReference, TrackedSectionEndpointRemoval } from "../../..
3
3
  import { EditorState, PluginKey, Transaction } from "prosemirror-state";
4
4
  import { Node } from "prosemirror-model";
5
5
  //#region src/prosemirror/extensions/features/ParagraphChangeTrackerExtension.d.ts
6
- declare const paragraphChangeTrackerKey: PluginKey<ParagraphChangeTrackerState>;
6
+ declare const paragraphChangeTrackerKey: PluginKey<InternalParagraphChangeTrackerState>;
7
7
  type ParagraphChangeTrackerState = {
8
8
  /** Set of paraIds that were modified since last clear */
9
9
  changedParaIds: Set<string>;
10
- /** Whether paragraphs were added or deleted (structural change) */
10
+ /** Whether paragraph order, membership, or block structure changed. */
11
11
  structuralChange: boolean;
12
- /** Whether any edited paragraph lacked a paraId */
12
+ /** Whether edited paragraphs still lack IDs or source identity was unavailable. */
13
13
  hasUntrackedChanges: boolean;
14
- /** Cached paragraph count to avoid full doc traversal on every transaction */
14
+ /** Paragraph count in the current tracked document. */
15
15
  paragraphCount: number;
16
16
  /** Cached section-endpoint count used to invalidate stale save authorization. */
17
17
  sectionEndpointCount: number;
@@ -20,6 +20,13 @@ type ParagraphChangeTrackerState = {
20
20
  /** Exact tracked-resolution transition that may reduce the saved section count. */
21
21
  sectionEndpointRemoval: TrackedSectionEndpointRemoval | null;
22
22
  };
23
+ type InternalParagraphChangeTrackerState = ParagraphChangeTrackerState & {
24
+ /** Edited paragraph positions retained across allocator-only transactions. */
25
+ affectedParagraphPositions: Set<number>;
26
+ /** Editing an already unidentified source paragraph cannot become selective. */
27
+ hasUntrackedSourceChanges: boolean;
28
+ blockStructureFingerprint: string;
29
+ };
23
30
  /**
24
31
  * Get the change tracker state from an EditorState
25
32
  */
@@ -29,7 +36,7 @@ declare function getChangeTrackerState(state: EditorState): ParagraphChangeTrack
29
36
  */
30
37
  declare function getChangedParagraphIds(state: EditorState): Set<string>;
31
38
  /**
32
- * Check if structural changes (paragraph add/delete) occurred
39
+ * Check if paragraph membership, order, or block structure changed
33
40
  */
34
41
  declare function hasStructuralChanges(state: EditorState): boolean;
35
42
  /**
@@ -18,9 +18,20 @@ const isTrackedSectionEndpointRemovalMeta = (value) => typeof value === "object"
18
18
  function countDocumentStructure(doc) {
19
19
  let paragraphs = 0;
20
20
  let sectionEndpoints = 0;
21
+ const blockRecords = [];
21
22
  const endpointRecords = [];
22
23
  const visit = (parent, parentPath) => {
23
24
  parent.forEach((node, _offset, index) => {
25
+ const path = parentPath.length === 0 ? `${index}` : `${parentPath}.${index}`;
26
+ blockRecords.push(node.type.name === "paragraph" ? [
27
+ path,
28
+ node.type.name,
29
+ node.attrs["paraId"] ?? null
30
+ ] : [
31
+ path,
32
+ node.type.name,
33
+ canonicalJson(node.attrs)
34
+ ]);
24
35
  if (node.type.name === "paragraph") {
25
36
  paragraphs++;
26
37
  const sectionProperties = sectionPropertiesOf(node);
@@ -34,34 +45,36 @@ function countDocumentStructure(doc) {
34
45
  }
35
46
  return;
36
47
  }
37
- if (node.childCount > 0) {
38
- const path = parentPath.length === 0 ? `${index}` : `${parentPath}.${index}`;
39
- visit(node, path);
40
- }
48
+ if (node.childCount > 0) visit(node, path);
41
49
  });
42
50
  };
43
51
  visit(doc, "");
44
52
  return {
53
+ blockStructureFingerprint: JSON.stringify(blockRecords),
45
54
  paragraphs,
46
55
  sectionEndpoints,
47
56
  sectionEndpointFingerprint: canonicalJson(endpointRecords)
48
57
  };
49
58
  }
59
+ const isUsableParaId = (value) => typeof value === "string" && value.length > 0 && value !== "00000000";
50
60
  /**
51
61
  * Collect paraIds of all paragraphs that overlap with the given range
52
62
  */
53
63
  function collectAffectedParaIds(doc, from, to) {
54
64
  const ids = /* @__PURE__ */ new Set();
65
+ const positions = /* @__PURE__ */ new Set();
55
66
  let hasUntracked = false;
56
- doc.nodesBetween(from, to, (node) => {
67
+ doc.nodesBetween(from, to, (node, pos) => {
57
68
  if (node.type.name === "paragraph") {
69
+ positions.add(pos);
58
70
  const paraId = node.attrs["paraId"];
59
- if (paraId) ids.add(paraId);
71
+ if (isUsableParaId(paraId)) ids.add(paraId);
60
72
  else hasUntracked = true;
61
73
  }
62
74
  });
63
75
  return {
64
76
  ids,
77
+ positions,
65
78
  hasUntracked
66
79
  };
67
80
  }
@@ -75,28 +88,40 @@ function collectAffectedParaIdsFromMarkLikeStep(doc, from, to) {
75
88
  const hi = Math.max(from, to);
76
89
  const primary = collectAffectedParaIds(doc, lo, hi > lo ? hi : lo + 1);
77
90
  if (primary.ids.size > 0 || primary.hasUntracked) return primary;
78
- try {
79
- const $p = doc.resolve(lo);
80
- for (let d = $p.depth; d >= 0; d--) {
81
- const n = $p.node(d);
82
- if (n.type.name === "paragraph") {
83
- const paraId = n.attrs["paraId"];
84
- if (paraId) return {
85
- ids: /* @__PURE__ */ new Set([paraId]),
86
- hasUntracked: false
87
- };
88
- return {
89
- ids: /* @__PURE__ */ new Set(),
90
- hasUntracked: true
91
- };
92
- }
91
+ const $p = doc.resolve(lo);
92
+ for (let depth = $p.depth; depth > 0; depth--) {
93
+ const node = $p.node(depth);
94
+ if (node.type.name === "paragraph") {
95
+ const paraId = node.attrs["paraId"];
96
+ return {
97
+ ids: new Set(isUsableParaId(paraId) ? [paraId] : []),
98
+ positions: /* @__PURE__ */ new Set([$p.before(depth)]),
99
+ hasUntracked: !isUsableParaId(paraId)
100
+ };
93
101
  }
94
- } catch {}
102
+ }
95
103
  return {
96
104
  ids: /* @__PURE__ */ new Set(),
105
+ positions: /* @__PURE__ */ new Set(),
97
106
  hasUntracked: false
98
107
  };
99
108
  }
109
+ /** Map an edited paragraph's interior so markup replacement retains its ownership. */
110
+ const mapParagraphPosition = (tr, position) => {
111
+ const mapped = tr.mapping.mapResult(position + 1, 1);
112
+ if (mapped.deletedAcross) return null;
113
+ const $position = tr.doc.resolve(mapped.pos);
114
+ for (let depth = $position.depth; depth > 0; depth--) if ($position.node(depth).type.name === "paragraph") return $position.before(depth);
115
+ return null;
116
+ };
117
+ const mapAffectedParagraphPositions = (tr, positions) => {
118
+ const mappedPositions = /* @__PURE__ */ new Set();
119
+ for (const position of positions) {
120
+ const mapped = mapParagraphPosition(tr, position);
121
+ if (mapped !== null) mappedPositions.add(mapped);
122
+ }
123
+ return mappedPositions;
124
+ };
100
125
  function mapStepPosition(remap, pos, assoc) {
101
126
  return remap.map(pos, assoc);
102
127
  }
@@ -107,6 +132,9 @@ function createParagraphChangeTrackerPlugin() {
107
132
  init(_config, state) {
108
133
  const counts = countDocumentStructure(state.doc);
109
134
  return {
135
+ affectedParagraphPositions: /* @__PURE__ */ new Set(),
136
+ hasUntrackedSourceChanges: false,
137
+ blockStructureFingerprint: counts.blockStructureFingerprint,
110
138
  changedParaIds: /* @__PURE__ */ new Set(),
111
139
  structuralChange: false,
112
140
  hasUntrackedChanges: false,
@@ -123,6 +151,9 @@ function createParagraphChangeTrackerPlugin() {
123
151
  if (meta === CLEAR_META) {
124
152
  const counts = countDocumentStructure(tr.doc);
125
153
  return {
154
+ affectedParagraphPositions: /* @__PURE__ */ new Set(),
155
+ hasUntrackedSourceChanges: false,
156
+ blockStructureFingerprint: counts.blockStructureFingerprint,
126
157
  changedParaIds: /* @__PURE__ */ new Set(),
127
158
  structuralChange: false,
128
159
  hasUntrackedChanges: false,
@@ -133,13 +164,22 @@ function createParagraphChangeTrackerPlugin() {
133
164
  };
134
165
  }
135
166
  if (meta === IGNORE_META) {
136
- const counts = tr.docChanged ? countDocumentStructure(tr.doc) : {
137
- paragraphs: prevState.paragraphCount,
138
- sectionEndpoints: prevState.sectionEndpointCount,
139
- sectionEndpointFingerprint: prevState.sectionEndpointFingerprint
140
- };
167
+ if (!tr.docChanged) return prevState;
168
+ const counts = countDocumentStructure(tr.doc);
169
+ const affectedParagraphPositions = mapAffectedParagraphPositions(tr, prevState.affectedParagraphPositions);
170
+ const changedParaIds = /* @__PURE__ */ new Set();
171
+ let unresolvedChanges = prevState.hasUntrackedSourceChanges;
172
+ for (const position of affectedParagraphPositions) {
173
+ const paraId = tr.doc.nodeAt(position)?.attrs["paraId"];
174
+ if (isUsableParaId(paraId)) changedParaIds.add(paraId);
175
+ else unresolvedChanges = true;
176
+ }
141
177
  return {
142
178
  ...prevState,
179
+ affectedParagraphPositions,
180
+ changedParaIds,
181
+ hasUntrackedChanges: unresolvedChanges,
182
+ blockStructureFingerprint: counts.blockStructureFingerprint,
143
183
  paragraphCount: counts.paragraphs,
144
184
  sectionEndpointCount: counts.sectionEndpoints,
145
185
  sectionEndpointRemoval: counts.sectionEndpointFingerprint === prevState.sectionEndpointFingerprint ? prevState.sectionEndpointRemoval : null,
@@ -162,6 +202,9 @@ function createParagraphChangeTrackerPlugin() {
162
202
  };
163
203
  } else sectionEndpointRemoval = null;
164
204
  const newState = {
205
+ affectedParagraphPositions: mapAffectedParagraphPositions(tr, prevState.affectedParagraphPositions),
206
+ hasUntrackedSourceChanges: prevState.hasUntrackedSourceChanges,
207
+ blockStructureFingerprint: counts.blockStructureFingerprint,
165
208
  changedParaIds: new Set(prevState.changedParaIds),
166
209
  structuralChange: prevState.structuralChange || meta === STRUCTURAL_META,
167
210
  hasUntrackedChanges: prevState.hasUntrackedChanges,
@@ -176,12 +219,13 @@ function createParagraphChangeTrackerPlugin() {
176
219
  const from = mapStepPosition(remap, range.from, 1);
177
220
  const to = mapStepPosition(remap, range.to, -1);
178
221
  if (to <= from) continue;
179
- const { ids, hasUntracked } = collectAffectedParaIdsFromMarkLikeStep(tr.doc, from, to);
222
+ const { ids, positions, hasUntracked } = collectAffectedParaIdsFromMarkLikeStep(tr.doc, from, to);
223
+ for (const position of positions) newState.affectedParagraphPositions.add(position);
180
224
  for (const id of ids) newState.changedParaIds.add(id);
181
225
  if (hasUntracked) newState.hasUntrackedChanges = true;
182
226
  }
183
227
  }
184
- if (prevState.paragraphCount !== newCount) newState.structuralChange = true;
228
+ if (prevState.blockStructureFingerprint !== counts.blockStructureFingerprint) newState.structuralChange = true;
185
229
  for (let stepIndex = 0; stepIndex < tr.steps.length; stepIndex++) {
186
230
  const step = tr.steps[stepIndex];
187
231
  const remap = tr.mapping.slice(stepIndex + 1);
@@ -189,7 +233,8 @@ function createParagraphChangeTrackerPlugin() {
189
233
  const from = mapStepPosition(remap, step.from, 1);
190
234
  const to = mapStepPosition(remap, step.to, -1);
191
235
  if (to <= from) continue;
192
- const { ids, hasUntracked } = collectAffectedParaIdsFromMarkLikeStep(tr.doc, from, to);
236
+ const { ids, positions, hasUntracked } = collectAffectedParaIdsFromMarkLikeStep(tr.doc, from, to);
237
+ for (const position of positions) newState.affectedParagraphPositions.add(position);
193
238
  for (const id of ids) newState.changedParaIds.add(id);
194
239
  if (hasUntracked) newState.hasUntrackedChanges = true;
195
240
  continue;
@@ -199,7 +244,8 @@ function createParagraphChangeTrackerPlugin() {
199
244
  const node = tr.doc.nodeAt(pos);
200
245
  if (!node) continue;
201
246
  const end = pos + node.nodeSize;
202
- const { ids, hasUntracked } = collectAffectedParaIds(tr.doc, pos, end);
247
+ const { ids, positions, hasUntracked } = collectAffectedParaIds(tr.doc, pos, end);
248
+ for (const position of positions) newState.affectedParagraphPositions.add(position);
203
249
  for (const id of ids) newState.changedParaIds.add(id);
204
250
  if (hasUntracked) newState.hasUntrackedChanges = true;
205
251
  continue;
@@ -208,11 +254,23 @@ function createParagraphChangeTrackerPlugin() {
208
254
  const from = mapStepPosition(remap, newStart, 1);
209
255
  const to = mapStepPosition(remap, newEnd, -1);
210
256
  if (to < from) return;
211
- const { ids, hasUntracked } = collectAffectedParaIds(tr.doc, from, to);
257
+ const { ids, positions, hasUntracked } = collectAffectedParaIds(tr.doc, from, to);
258
+ for (const position of positions) newState.affectedParagraphPositions.add(position);
212
259
  for (const id of ids) newState.changedParaIds.add(id);
213
260
  if (hasUntracked) newState.hasUntrackedChanges = true;
214
261
  });
215
262
  }
263
+ if (newState.hasUntrackedChanges || prevState.blockStructureFingerprint !== counts.blockStructureFingerprint) tr.before.descendants((node, position) => {
264
+ if (node.type.name !== "paragraph") return true;
265
+ if (!isUsableParaId(node.attrs["paraId"])) {
266
+ const mapped = mapParagraphPosition(tr, position);
267
+ if (mapped === null || newState.affectedParagraphPositions.has(mapped)) {
268
+ newState.hasUntrackedSourceChanges = true;
269
+ newState.hasUntrackedChanges = true;
270
+ }
271
+ }
272
+ return false;
273
+ });
216
274
  return newState;
217
275
  }
218
276
  }
@@ -231,7 +289,7 @@ function getChangedParagraphIds(state) {
231
289
  return getChangeTrackerState(state)?.changedParaIds ?? /* @__PURE__ */ new Set();
232
290
  }
233
291
  /**
234
- * Check if structural changes (paragraph add/delete) occurred
292
+ * Check if paragraph membership, order, or block structure changed
235
293
  */
236
294
  function hasStructuralChanges(state) {
237
295
  return getChangeTrackerState(state)?.structuralChange ?? false;