@voithos-labs/aragonite 0.10.2 → 0.10.3

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 (255) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +18 -0
  4. package/dist/a11y-strings.js +18 -0
  5. package/dist/action-contracts.d.ts +7 -1
  6. package/dist/ambient/ambient-dom.js +5 -1
  7. package/dist/block-component.d.ts +14 -0
  8. package/dist/components/BlockDragHandle.svelte +35 -24
  9. package/dist/components/BlockHost.svelte +16 -9
  10. package/dist/components/Editor.svelte +381 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +17 -3
  13. package/dist/components/TailInsert.svelte +107 -0
  14. package/dist/components/TailInsert.svelte.d.ts +17 -0
  15. package/dist/components/block-content-selector.d.ts +6 -2
  16. package/dist/components/block-content-selector.js +6 -2
  17. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  18. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  19. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  20. package/dist/components/blocks/code/CodeBlockRail.svelte +686 -0
  21. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  22. package/dist/components/blocks/code/code-bootstrap.js +4 -0
  23. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  24. package/dist/components/blocks/code/code-context-actions.js +24 -0
  25. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  26. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  27. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  28. package/dist/components/blocks/code/code-languages.js +11 -0
  29. package/dist/components/blocks/code/code-renderer.js +11 -0
  30. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  31. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  32. package/dist/components/blocks/editable-leaf.js +243 -29
  33. package/dist/components/blocks/editable-surface.d.ts +9 -0
  34. package/dist/components/blocks/editable-surface.js +22 -3
  35. package/dist/components/blocks/list/ListItemBlock.svelte +3 -3
  36. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  37. package/dist/components/blocks/list/task-checkbox.js +11 -2
  38. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  39. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  40. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  41. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  42. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  43. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  44. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  45. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  46. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  47. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  48. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  49. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  50. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  51. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  52. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  53. package/dist/components/blocks/table/table-menu-model.js +36 -17
  54. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  55. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  56. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  57. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  58. package/dist/components/blocks/text/edge-policy-dispatch.js +56 -2
  59. package/dist/components/blocks/text/live-selection-edit.js +27 -0
  60. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  61. package/dist/components/blocks/text/text-keydown.js +11 -1
  62. package/dist/components/blocks/text/text-render.d.ts +1 -1
  63. package/dist/components/blocks/text/text-render.js +3 -1
  64. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  65. package/dist/components/blocks/text/widget-interaction.js +144 -22
  66. package/dist/components/drag-handle.d.ts +35 -0
  67. package/dist/components/drag-handle.js +126 -0
  68. package/dist/components/editor-root-focus.d.ts +19 -0
  69. package/dist/components/editor-root-focus.js +67 -0
  70. package/dist/components/editor-root-geometry.d.ts +39 -0
  71. package/dist/components/editor-root-geometry.js +91 -0
  72. package/dist/components/editor-root-keydown.d.ts +1 -1
  73. package/dist/components/editor-root-keydown.js +12 -3
  74. package/dist/components/editor-root-listeners.d.ts +10 -6
  75. package/dist/components/editor-root-listeners.js +23 -22
  76. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  77. package/dist/components/editor-root-mode-flip.js +92 -0
  78. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  79. package/dist/components/image/ImageProperties.svelte +512 -70
  80. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  81. package/dist/components/image/ImageResizeHandles.svelte +59 -34
  82. package/dist/components/image/image-crop.d.ts +39 -0
  83. package/dist/components/image/image-crop.js +74 -0
  84. package/dist/components/image/image-edit-commit.d.ts +1 -0
  85. package/dist/components/image/image-edit-commit.js +21 -5
  86. package/dist/components/image/image-source-bytes.js +10 -3
  87. package/dist/components/image/image-widget-editing.js +1 -0
  88. package/dist/components/image/widget-dom.js +5 -1
  89. package/dist/components/link-card/link-card-commit.js +1 -1
  90. package/dist/components/lrd-map-gate.js +1 -1
  91. package/dist/components/menu/BlockMenu.svelte +315 -0
  92. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  93. package/dist/components/menu/MenuIcon.svelte +153 -0
  94. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  95. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  96. package/dist/components/menu/clipboard-actions.js +42 -0
  97. package/dist/components/menu/default-context-actions.d.ts +15 -0
  98. package/dist/components/menu/default-context-actions.js +76 -0
  99. package/dist/components/menu/flyout-placement.d.ts +6 -0
  100. package/dist/components/menu/flyout-placement.js +25 -0
  101. package/dist/core/inline/format-toggle.d.ts +12 -4
  102. package/dist/core/inline/format-toggle.js +94 -40
  103. package/dist/core/inline/image-dimensions.d.ts +3 -0
  104. package/dist/core/inline/image-dimensions.js +46 -10
  105. package/dist/core/inline/inline-widgets.d.ts +11 -0
  106. package/dist/core/inline/scan/brackets.js +1 -0
  107. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  108. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  109. package/dist/core/inline-render.d.ts +6 -0
  110. package/dist/core/inline-render.js +32 -0
  111. package/dist/core/nodes.d.ts +14 -0
  112. package/dist/cursor/edge-affinity.js +2 -1
  113. package/dist/cursor/overlay-remeasure.js +8 -0
  114. package/dist/cursor/reveal-source.js +7 -2
  115. package/dist/cursor/widget-offset.d.ts +6 -0
  116. package/dist/cursor/widget-offset.js +61 -4
  117. package/dist/debug/interaction-trace.d.ts +4 -0
  118. package/dist/debug/interaction-trace.js +15 -0
  119. package/dist/decorations/decoration-state.svelte.js +1 -1
  120. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  121. package/dist/editor-actions/ancestry-folds.js +1 -1
  122. package/dist/editor-actions/block-edit-scope.js +1 -1
  123. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  124. package/dist/editor-actions/commit/text-batch.js +1 -1
  125. package/dist/editor-actions/commit/undo-controller.js +4 -2
  126. package/dist/editor-actions/container-edit.js +2 -1
  127. package/dist/editor-actions/enter-completion.d.ts +2 -0
  128. package/dist/editor-actions/enter-completion.js +23 -2
  129. package/dist/editor-actions/inline-range-commit.js +1 -1
  130. package/dist/editor-actions/reorder-action.js +19 -10
  131. package/dist/editor-actions/reorder-drag.js +29 -1
  132. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  133. package/dist/editor-actions/replacement-focus.js +1 -1
  134. package/dist/editor-actions/search-replace.js +1 -1
  135. package/dist/editor-actions/table-context.d.ts +4 -1
  136. package/dist/editor-actions/table-context.js +57 -1
  137. package/dist/editor-events.d.ts +3 -0
  138. package/dist/editor-keys.d.ts +33 -0
  139. package/dist/editor-props.d.ts +21 -12
  140. package/dist/index.d.ts +1 -1
  141. package/dist/plugin.d.ts +6 -0
  142. package/dist/plugin.js +11 -0
  143. package/dist/plugins/latex/BlockMath.svelte +264 -29
  144. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  145. package/dist/plugins/latex/index.d.ts +2 -1
  146. package/dist/plugins/latex/latex-kind.js +36 -3
  147. package/dist/plugins/latex/math-completion.js +4 -1
  148. package/dist/plugins/latex/math-layout.d.ts +15 -0
  149. package/dist/plugins/latex/math-layout.js +13 -0
  150. package/dist/plugins/latex/math-source.d.ts +19 -0
  151. package/dist/plugins/latex/math-source.js +97 -0
  152. package/dist/plugins/latex/register.d.ts +11 -2
  153. package/dist/plugins/latex/register.js +3 -1
  154. package/dist/plugins/latex/renderer.d.ts +3 -3
  155. package/dist/plugins/latex/renderer.js +13 -6
  156. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  157. package/dist/reactivity/list-windowing.svelte.js +49 -24
  158. package/dist/schema/block-completions.d.ts +8 -0
  159. package/dist/schema/block-completions.js +11 -0
  160. package/dist/schema/context-actions.d.ts +31 -0
  161. package/dist/schema/context-actions.js +23 -0
  162. package/dist/schema/fenced-code-raw.js +31 -1
  163. package/dist/schema/operations.d.ts +8 -1
  164. package/dist/schema/reserved-chords.js +25 -4
  165. package/dist/schema/table-cell-raw.d.ts +1 -1
  166. package/dist/schema/table-cell-raw.js +1 -1
  167. package/dist/selection/block-hit-test.js +3 -2
  168. package/dist/selection/char-endpoint-snap.js +1 -1
  169. package/dist/selection/clipboard-text.js +6 -1
  170. package/dist/selection/covered-block.d.ts +10 -0
  171. package/dist/selection/covered-block.js +24 -0
  172. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  173. package/dist/selection/cross-block/dispatch.js +13 -1
  174. package/dist/selection/cross-block/format-range.d.ts +1 -1
  175. package/dist/selection/cross-block/format-range.js +4 -15
  176. package/dist/selection/cross-block/format-toggle.js +1 -1
  177. package/dist/selection/cross-block/keydown.js +1 -1
  178. package/dist/selection/cross-block/ops.js +1 -1
  179. package/dist/selection/cross-block/paste.js +32 -30
  180. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  181. package/dist/selection/cross-block/type-replace.js +62 -13
  182. package/dist/selection/dead-space-caret.d.ts +10 -0
  183. package/dist/selection/dead-space-caret.js +47 -1
  184. package/dist/selection/double-click-trim.d.ts +17 -0
  185. package/dist/selection/double-click-trim.js +57 -0
  186. package/dist/selection/drag-pointer.d.ts +7 -2
  187. package/dist/selection/drag-pointer.js +61 -2
  188. package/dist/selection/gap-caret.js +1 -1
  189. package/dist/selection/keyboard-extend.js +1 -1
  190. package/dist/selection/path-lookup.js +1 -1
  191. package/dist/selection/range-delete-ceremony.js +4 -2
  192. package/dist/selection/range-delete-chrome.js +2 -1
  193. package/dist/selection/range-delete-table-coverage.js +2 -1
  194. package/dist/selection/range-delete-table.js +3 -2
  195. package/dist/selection/range-delete.js +30 -2
  196. package/dist/selection/selection-restore.js +1 -1
  197. package/dist/selection/selection-state.svelte.d.ts +6 -0
  198. package/dist/selection/selection-state.svelte.js +36 -1
  199. package/dist/selection/table-endpoint-snap.js +1 -1
  200. package/dist/selection/table-rect-extend.js +1 -1
  201. package/dist/styles/editor-theme.css +57 -28
  202. package/dist/styles/editor.css +217 -18
  203. package/dist/testing/container-conformance.js +2 -2
  204. package/dist/testing/inline-conformance.js +2 -1
  205. package/dist/tree-operations/blockquote.js +1 -1
  206. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  207. package/dist/tree-operations/chain-rebuild.js +142 -0
  208. package/dist/tree-operations/children.d.ts +1 -1
  209. package/dist/tree-operations/children.js +1 -1
  210. package/dist/tree-operations/cleanup.js +1 -1
  211. package/dist/tree-operations/content-write.d.ts +50 -0
  212. package/dist/tree-operations/content-write.js +263 -0
  213. package/dist/tree-operations/index.d.ts +8 -3
  214. package/dist/tree-operations/index.js +6 -2
  215. package/dist/tree-operations/list/exit-replacement.js +1 -1
  216. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  217. package/dist/tree-operations/node-ops.d.ts +17 -234
  218. package/dist/tree-operations/node-ops.js +47 -1113
  219. package/dist/tree-operations/node-primitives.d.ts +75 -0
  220. package/dist/tree-operations/node-primitives.js +117 -0
  221. package/dist/tree-operations/paste/apply.js +1 -1
  222. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  223. package/dist/tree-operations/paste/body-write.js +2 -2
  224. package/dist/tree-operations/paste/container-match.js +4 -2
  225. package/dist/tree-operations/paste/dispatch.js +2 -1
  226. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  227. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  228. package/dist/tree-operations/paste/list-absorb.js +1 -1
  229. package/dist/tree-operations/paste/list-break-out.js +1 -1
  230. package/dist/tree-operations/paste/parent-scope.js +1 -1
  231. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  232. package/dist/tree-operations/paste/replace-block-at-parent.js +1 -1
  233. package/dist/tree-operations/path-mutate.d.ts +1 -1
  234. package/dist/tree-operations/path-mutate.js +2 -1
  235. package/dist/tree-operations/reorder-unit.js +1 -1
  236. package/dist/tree-operations/reorder.d.ts +5 -2
  237. package/dist/tree-operations/reorder.js +55 -2
  238. package/dist/tree-operations/settle.d.ts +105 -0
  239. package/dist/tree-operations/settle.js +660 -0
  240. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  241. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  242. package/dist/tree-operations/unshare.d.ts +15 -77
  243. package/dist/tree-operations/unshare.js +15 -166
  244. package/docs/guide/consumer-guide.md +126 -96
  245. package/docs/guide/plugin-api.md +31 -3
  246. package/docs/guide/plugin-guide.md +26 -3
  247. package/package.json +4 -2
  248. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  249. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  250. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  251. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  252. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  253. package/dist/components/blocks/table/table-drop-target.js +0 -16
  254. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  255. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -33,19 +33,21 @@
33
33
  } from '../../../schema/inline-construct-policy';
34
34
  import {
35
35
  cycleHeading,
36
+ demoteEmptyAtxHeading,
36
37
  demoteToParagraph,
37
38
  insertHardBreak,
38
39
  insertLiteralTab,
39
40
  type TextEditResult
40
41
  } from './text-keydown';
41
42
  import { tryGetBlockKindDescriptor } from '../../../schema/block-kind-descriptor';
42
- import { blockNodeAt } from '../../../tree-operations/node-ops';
43
+ import { blockNodeAt } from '../../../tree-operations/node-primitives';
43
44
  import { createTextClipboard } from './text-clipboard';
44
45
  import { createTextRender } from './text-render';
45
46
  import { createWidgetInteraction } from './widget-interaction';
46
- import { createEdgePolicyDispatch } from './edge-policy-dispatch';
47
+ import { createEdgePolicyDispatch, keepsBlockKind } from './edge-policy-dispatch';
47
48
  import { hidesStructuralSuffix } from './hidden-suffix';
48
49
  import { applyLiveRangeEdit, resolveSelectionEdit } from './live-selection-edit';
50
+ import { applyDelimiterAutoPair } from './delimiter-autopair';
49
51
  import { createCompositionSeat } from './composition-seat';
50
52
  import { createConstructReveal } from './construct-reveal';
51
53
  import { assertInvariant } from '../../../assert';
@@ -56,7 +58,11 @@
56
58
  handleSharedKeydown,
57
59
  handleSharedBeforeInput
58
60
  } from '../../../selection/shared-keydown';
59
- import { createEditableSurface, consumePendingRestore } from '../editable-surface';
61
+ import {
62
+ createEditableSurface,
63
+ consumePendingRestore,
64
+ withKeydownVerdict
65
+ } from '../editable-surface';
60
66
  import { wireSurfaceContexts, useParkFocusOnUnmount } from '../surface-wiring.svelte';
61
67
  import {
62
68
  domTextOffsetAtNode,
@@ -76,6 +82,7 @@
76
82
  import { createAmbientCursorIO } from '../../../ambient/ambient-cursor';
77
83
  import { type CommandId } from '../../../schema/commands';
78
84
  import { reorderRunCommand } from '../../../editor-actions/reorder-action';
85
+ import { planTypedCompletion } from '../../../editor-actions/enter-completion';
79
86
  import {
80
87
  perfEnabled,
81
88
  recordBlockRender,
@@ -111,6 +118,12 @@
111
118
  const ambientPrefixText = $derived(
112
119
  typeof ambientPrefix === 'string' ? ambientPrefix : ambientPrefix.text
113
120
  );
121
+ // The hanging indent is the prefix's painted width: its text width by default, or what a
122
+ // prefix painting itself as chrome (the task box) declares.
123
+ const ambientIndent = $derived(
124
+ (typeof ambientPrefix === 'string' ? undefined : ambientPrefix.indent) ??
125
+ `${ambientPrefixText.length}ch`
126
+ );
114
127
 
115
128
  const wiring = wireSurfaceContexts();
116
129
  const {
@@ -361,6 +374,7 @@
361
374
  widgetInteraction.enterWidget(widget, fromTrailingEdge),
362
375
  isReading: () => readOnly,
363
376
  getEdgeAffinity: () => edgeAffinity.get(),
377
+ noteOutside: edgeAffinity.noteExtreme,
364
378
  pendingMarks,
365
379
  installedAs: 'block'
366
380
  });
@@ -668,9 +682,9 @@
668
682
  // Only while this block still owns focus: a blur-commit also arms a pending
669
683
  // offset, and restoring would yank the selection back into the blurred block.
670
684
  // The clear runs regardless, so a skipped restore is dropped, never re-armed.
671
- const applied = consumePendingRestore(el ?? null, pendingCursorOffset, (offset) =>
672
- cursor.setRaw(asRawOffset(offset))
673
- );
685
+ const applied = consumePendingRestore(el ?? null, pendingCursorOffset, (offset) => {
686
+ if (!widgetInteraction.revealInterior(offset)) cursor.setRaw(asRawOffset(offset));
687
+ });
674
688
  tracePendingCursorConsume(pendingCursorOffset, applied);
675
689
  pendingCursorOffset = null;
676
690
  }
@@ -815,6 +829,8 @@
815
829
  if (wiring.dispatchChord(e, { kind: node.kind, runCommand })) return;
816
830
  }
817
831
 
832
+ const onKeyDownTraced = withKeydownVerdict(onKeyDown);
833
+
818
834
  /**
819
835
  * A native ranged edit inside ONE block, in a mode that paints no delimiter: the engine would
820
836
  * write the runs the range crossed literally, so the edit goes through the join seam instead.
@@ -837,9 +853,33 @@
837
853
  );
838
854
  }
839
855
 
856
+ // The write takes the ranged edit's road above, so the surface repaints once with the caret
857
+ // inside the pair. During a reveal the caret indexes the DOM text, which has outrun `node.raw`.
858
+ function handleDelimiterAutoPair(e: InputEvent): boolean {
859
+ return applyDelimiterAutoPair(e, {
860
+ text: () => (widgetInteraction.isRevealing() ? readRawText() : getDisplayText()),
861
+ content: () => getContentRange(node),
862
+ caret: () => cursor.getRaw(),
863
+ hasSelection: () => cursor.getRawSelection() !== null,
864
+ isRevealing: widgetInteraction.isRevealing,
865
+ foldReveal: () => widgetInteraction.foldRevealBeforeMutation(),
866
+ markersPaint: () => paintsFocusedMarkers(presentationMode),
867
+ setCaret: (offset) => cursor.setRaw(asRawOffset(offset)),
868
+ seatOutside: edgeAffinity.noteExtreme,
869
+ completesLine: (caret) => planTypedCompletion(node, caret) !== null,
870
+ keepsBlockKind: (text) => keepsBlockKind(node, text),
871
+ write: (text, caretBefore, caretAfter) => {
872
+ const raw = text + trailingLineEnding(node.raw);
873
+ void blockEdit.updateBlockContent(index, raw, caretBefore, caretAfter);
874
+ setPendingCursorOffset(caretAfter, 'delimiter-autopair');
875
+ }
876
+ });
877
+ }
878
+
840
879
  async function onBeforeInput(e: InputEvent): Promise<void> {
841
880
  if (await handleSharedBeforeInput(e, sharedCtx)) return;
842
881
  if (handleLiveSelectionEdit(e)) return;
882
+ if (handleDelimiterAutoPair(e)) return;
843
883
  // Soft-keyboard/IME insertLineBreak slipped past onKeyDown — swallow; Shift+Enter there owns hard breaks.
844
884
  if (e.inputType === 'insertLineBreak') {
845
885
  e.preventDefault();
@@ -868,6 +908,13 @@
868
908
  // Persist a revealed source edit before the caret is gone.
869
909
  widgetInteraction.commitRevealOnBlur();
870
910
  lastSnapTargetOffset = null;
911
+ demoteEmptyHeadingOnBlur();
912
+ }
913
+
914
+ function demoteEmptyHeadingOnBlur(): void {
915
+ if (readOnly || node.kind !== 'heading' || editableSurface.isDetached()) return;
916
+ const demoted = demoteEmptyAtxHeading(node.raw, getContentRange(node));
917
+ if (demoted) void blockEdit.updateBlockContent(index, demoted.newRaw, 0);
871
918
  }
872
919
 
873
920
  function onClick(e: MouseEvent): void {
@@ -933,10 +980,10 @@
933
980
  contenteditable={readOnly ? 'false' : 'true'}
934
981
  aria-readonly={readOnly ? 'true' : undefined}
935
982
  role="textbox"
936
- style:text-indent={ambientPrefixText ? `-${ambientLength}ch` : null}
937
- style:padding-left={ambientPrefixText ? `${ambientLength}ch` : null}
983
+ style:text-indent={ambientPrefixText ? `-${ambientIndent}` : null}
984
+ style:padding-left={ambientPrefixText ? ambientIndent : null}
938
985
  oninput={onInput}
939
- onkeydown={onKeyDown}
986
+ onkeydown={onKeyDownTraced}
940
987
  onbeforeinput={onBeforeInput}
941
988
  oncopy={clipboardHandlers.onCopy}
942
989
  oncut={clipboardHandlers.onCut}
@@ -986,7 +1033,7 @@
986
1033
  }
987
1034
 
988
1035
  .text-editable-block.raw-block {
989
- font-family: var(--font-editor, ui-monospace, monospace);
1036
+ font-family: var(--font-code, ui-monospace, monospace);
990
1037
  font-size: 0.9em;
991
1038
  opacity: 0.85;
992
1039
  }
@@ -998,7 +1045,7 @@
998
1045
  }
999
1046
 
1000
1047
  .text-editable-block :global(.inline-code-content) {
1001
- font-family: var(--font-editor, ui-monospace, monospace);
1048
+ font-family: var(--font-code, ui-monospace, monospace);
1002
1049
  font-size: 0.9em;
1003
1050
  background: var(--color-bg-secondary, rgba(128, 128, 128, 0.12));
1004
1051
  border-radius: 3px;
@@ -0,0 +1,71 @@
1
+ /**
2
+ * A typed inline delimiter closes itself: a lone `$`, backtick, `*`, `_` or `~~` would pair with
3
+ * whatever matching run comes later on the line, so the keystroke lands its twin after the caret.
4
+ * Typing the closer over the twin steps past it, a closer typed by hand seats the next byte
5
+ * outside, and the empty pair drops its twin when the first body byte makes it no construct.
6
+ * `applyDelimiterAutoPair` is the one `beforeinput` arm every prose surface runs (G4.65).
7
+ */
8
+ import { type ContentRange } from '../../../core/inline';
9
+ export type AutoPairEdit = {
10
+ kind: 'write';
11
+ text: string;
12
+ caret: number;
13
+ }
14
+ /** The typed byte completed a construct's closer; what follows belongs outside it. */
15
+ | {
16
+ kind: 'close';
17
+ text: string;
18
+ caret: number;
19
+ }
20
+ /** Nothing written: the caret passes the twin. Over a construct's closer that run may be
21
+ * unpainted, and then only the SIDE moves, which the surface records as an edge affinity. */
22
+ | {
23
+ kind: 'step-over';
24
+ caret: number;
25
+ overConstruct: boolean;
26
+ };
27
+ /**
28
+ * What a single typed byte does at a collapsed caret, or null to leave the engine its insert.
29
+ * `content` bounds the inline scan (a heading's `# ` is not prose); the caret must lie inside it.
30
+ * `keepsBlockKind` says whether a written line still reloads as this block: a grown `****` is a
31
+ * thematic break and `~~~~` a fence, so such a pair steps past a twin instead, or declines.
32
+ */
33
+ export declare function resolveDelimiterAutoPair(text: string, content: ContentRange, caret: number, typed: string, keepsBlockKind?: (text: string) => boolean): AutoPairEdit | null;
34
+ /**
35
+ * Inside a revealed source (`$ab|$`) the twin is the live closer, and typing it means "done":
36
+ * the caret steps past it and the caller folds the reveal, so the formula renders at once.
37
+ */
38
+ export declare function stepsOverRevealedCloser(text: string, caret: number, typed: string): boolean;
39
+ /** Backspace at an empty pair takes both runs, between them (`**|**`) or right after them
40
+ * (`$$|`), as it does in any IDE. Exactly a pair of equal runs, one or two bytes each: a longer
41
+ * run is a fence or a literal the user built by hand. */
42
+ export declare function resolveEmptyPairBackspace(text: string, caret: number): AutoPairEdit | null;
43
+ /** What a prose surface lends the arm. Reactive reads are thunks, so nothing here goes stale. */
44
+ export interface AutoPairSurface {
45
+ /** The bytes the caret indexes: the display text, or the revealed DOM text during a reveal. */
46
+ text(): string;
47
+ content(): ContentRange;
48
+ caret(): number | null;
49
+ hasSelection(): boolean;
50
+ isRevealing(): boolean;
51
+ foldReveal(): {
52
+ settled: Promise<void>;
53
+ } | null;
54
+ /** Whether the closer a step-over passes is on screen; unpainted, only the side moves. */
55
+ markersPaint(): boolean;
56
+ setCaret(offset: number): void;
57
+ /** The arrival the seat reads next: past the construct's delimiters. */
58
+ seatOutside(): void;
59
+ /** One CST write plus the caret it parks. */
60
+ write(text: string, caretBefore: number, caretAfter: number): void;
61
+ /** A step-over that leaves the line one an on-type completer claims (`$$`), which only a
62
+ * content write consults. */
63
+ completesLine?(caret: number): boolean;
64
+ /** The resolver's block-kind guard, for a surface whose line can become another block. */
65
+ keepsBlockKind?(text: string): boolean;
66
+ }
67
+ /**
68
+ * The `beforeinput` arm. True when the press was this seam's: the event is cancelled and the
69
+ * surface has written, moved or re-seated the caret.
70
+ */
71
+ export declare function applyDelimiterAutoPair(e: InputEvent, surface: AutoPairSurface): boolean;
@@ -0,0 +1,216 @@
1
+ /**
2
+ * A typed inline delimiter closes itself: a lone `$`, backtick, `*`, `_` or `~~` would pair with
3
+ * whatever matching run comes later on the line, so the keystroke lands its twin after the caret.
4
+ * Typing the closer over the twin steps past it, a closer typed by hand seats the next byte
5
+ * outside, and the empty pair drops its twin when the first body byte makes it no construct.
6
+ * `applyDelimiterAutoPair` is the one `beforeinput` arm every prose surface runs (G4.65).
7
+ */
8
+ import { constructContentRange, inlineDescendants, parseInline } from '../../../core/inline';
9
+ import { isAutoPairTrigger } from '../../../core/inline/scan/plugin-syntax';
10
+ const STEP = { single: true, inside: 'step-over' };
11
+ const BUILTIN = {
12
+ '`': STEP,
13
+ '*': { single: true, inside: 'grow', notAfterWord: true },
14
+ _: { single: true, inside: 'grow', notAfterWord: true },
15
+ // `~5 minutes` would strike the 5 (GFM takes a single tilde), so only `~~` pairs.
16
+ '~': { single: false, inside: 'grow' }
17
+ };
18
+ const isWordByte = (ch) => ch !== undefined && /[\p{L}\p{N}]/u.test(ch);
19
+ function policyOf(ch) {
20
+ return BUILTIN[ch] ?? (isAutoPairTrigger(ch) ? STEP : null);
21
+ }
22
+ // ── The resolver ─────────────────────────────────────────────────────────────
23
+ /**
24
+ * What a single typed byte does at a collapsed caret, or null to leave the engine its insert.
25
+ * `content` bounds the inline scan (a heading's `# ` is not prose); the caret must lie inside it.
26
+ * `keepsBlockKind` says whether a written line still reloads as this block: a grown `****` is a
27
+ * thematic break and `~~~~` a fence, so such a pair steps past a twin instead, or declines.
28
+ */
29
+ export function resolveDelimiterAutoPair(text, content, caret, typed, keepsBlockKind = () => true) {
30
+ if (typed.length !== 1 || caret < content.start || caret > content.end)
31
+ return null;
32
+ const before = text[caret - 1];
33
+ const after = text[caret];
34
+ const policy = policyOf(typed);
35
+ if (!policy)
36
+ return collapseEmptyPair(text, content, caret, typed);
37
+ if (after === typed && closingRunAt(text, content, caret, typed)) {
38
+ return { kind: 'step-over', caret: caret + 1, overConstruct: true };
39
+ }
40
+ const paired = text.slice(0, caret) + typed + text.slice(caret);
41
+ if (closerEndsAt(paired, content, caret + 1, typed)) {
42
+ return { kind: 'close', text: paired, caret: caret + 1 };
43
+ }
44
+ const pair = (next) => {
45
+ if (keepsBlockKind(next))
46
+ return write(next, caret + 1);
47
+ return after === typed ? { kind: 'step-over', caret: caret + 1, overConstruct: false } : null;
48
+ };
49
+ if (after === typed) {
50
+ if (before === typed) {
51
+ if (policy.inside === 'step-over') {
52
+ return { kind: 'step-over', caret: caret + 1, overConstruct: false };
53
+ }
54
+ return pair(text.slice(0, caret) + typed + typed + text.slice(caret));
55
+ }
56
+ // A byte in front of another run's opener is spelling something out, not opening a span.
57
+ return null;
58
+ }
59
+ if (before === typed && !closerEndsAt(text, content, caret, typed)) {
60
+ // `~|` plus `~`: the double run this delimiter pairs on, as long as it IS a lone run.
61
+ const single = text[caret - 2] !== typed;
62
+ if (policy.inside === 'grow' && single) {
63
+ return pair(text.slice(0, caret) + typed.repeat(3) + text.slice(caret));
64
+ }
65
+ return null;
66
+ }
67
+ if (!policy.single || (policy.notAfterWord && isWordByte(before)))
68
+ return null;
69
+ return pair(text.slice(0, caret) + typed + typed + text.slice(caret));
70
+ }
71
+ /**
72
+ * Inside a revealed source (`$ab|$`) the twin is the live closer, and typing it means "done":
73
+ * the caret steps past it and the caller folds the reveal, so the formula renders at once.
74
+ */
75
+ export function stepsOverRevealedCloser(text, caret, typed) {
76
+ return typed.length === 1 && policyOf(typed) !== null && text[caret] === typed;
77
+ }
78
+ /** Backspace at an empty pair takes both runs, between them (`**|**`) or right after them
79
+ * (`$$|`), as it does in any IDE. Exactly a pair of equal runs, one or two bytes each: a longer
80
+ * run is a fence or a literal the user built by hand. */
81
+ export function resolveEmptyPairBackspace(text, caret) {
82
+ const pair = emptyPairEnding(text, caret) ?? emptyPairAround(text, caret);
83
+ if (!pair)
84
+ return null;
85
+ return write(text.slice(0, pair.start) + text.slice(pair.end), pair.start);
86
+ }
87
+ const write = (text, caret) => ({ kind: 'write', text, caret });
88
+ /**
89
+ * The `beforeinput` arm. True when the press was this seam's: the event is cancelled and the
90
+ * surface has written, moved or re-seated the caret.
91
+ */
92
+ export function applyDelimiterAutoPair(e, surface) {
93
+ const typing = e.inputType === 'insertText';
94
+ if (!typing && e.inputType !== 'deleteContentBackward')
95
+ return false;
96
+ if (e.isComposing || surface.hasSelection())
97
+ return false;
98
+ const caret = surface.caret();
99
+ if (caret === null)
100
+ return false;
101
+ const text = surface.text();
102
+ if (surface.isRevealing()) {
103
+ if (!typing || !stepsOverRevealedCloser(text, caret, e.data ?? ''))
104
+ return false;
105
+ e.preventDefault();
106
+ surface.setCaret(caret + 1);
107
+ void surface.foldReveal()?.settled;
108
+ return true;
109
+ }
110
+ const edit = typing
111
+ ? resolveDelimiterAutoPair(text, surface.content(), caret, e.data ?? '', surface.keepsBlockKind)
112
+ : resolveEmptyPairBackspace(text, caret);
113
+ if (!edit)
114
+ return false;
115
+ e.preventDefault();
116
+ switch (edit.kind) {
117
+ case 'step-over':
118
+ if (edit.overConstruct && !surface.markersPaint())
119
+ surface.seatOutside();
120
+ else if (surface.completesLine?.(edit.caret))
121
+ surface.write(text, caret, edit.caret);
122
+ else
123
+ surface.setCaret(edit.caret);
124
+ return true;
125
+ case 'close':
126
+ surface.write(edit.text, caret, edit.caret);
127
+ surface.seatOutside();
128
+ return true;
129
+ case 'write':
130
+ surface.write(edit.text, caret, edit.caret);
131
+ return true;
132
+ }
133
+ }
134
+ // ── Runs ─────────────────────────────────────────────────────────────────────
135
+ /** Length of the run of `ch` ending just before `at` (0 when `text[at - 1]` is not `ch`). */
136
+ function runBefore(text, at, ch) {
137
+ let n = 0;
138
+ while (text[at - 1 - n] === ch)
139
+ n++;
140
+ return n;
141
+ }
142
+ function runAfter(text, at, ch) {
143
+ let n = 0;
144
+ while (text[at + n] === ch)
145
+ n++;
146
+ return n;
147
+ }
148
+ // `X..X|X..X`: equal runs of a pair delimiter, one or two bytes, with nothing of it either side.
149
+ function emptyPairAround(text, caret) {
150
+ const d = text[caret - 1];
151
+ if (d === undefined || !policyOf(d))
152
+ return null;
153
+ const k = runBefore(text, caret, d);
154
+ if (k > 2 || runAfter(text, caret, d) !== k)
155
+ return null;
156
+ return { start: caret - k, end: caret + k };
157
+ }
158
+ // `XX|`: a stepped-over empty pair with the caret after it. Only the delimiters that step (the
159
+ // emphasis family grows instead, and its `**|` is a double opener with content ahead).
160
+ function emptyPairEnding(text, caret) {
161
+ const d = text[caret - 1];
162
+ if (d === undefined || policyOf(d)?.inside !== 'step-over' || text[caret] === d)
163
+ return null;
164
+ if (runBefore(text, caret, d) !== 2)
165
+ return null;
166
+ return { start: caret - 2, end: caret };
167
+ }
168
+ // ── Constructs ───────────────────────────────────────────────────────────────
169
+ // `X|X` plus the typed byte: keep the twin run only if the pair then parses as a construct.
170
+ function collapseEmptyPair(text, content, caret, typed) {
171
+ const d = text[caret - 1];
172
+ if (d === undefined || !policyOf(d))
173
+ return null;
174
+ const k = runBefore(text, caret, d);
175
+ if (k > 2 || runAfter(text, caret, d) !== k)
176
+ return null;
177
+ const paired = text.slice(0, caret) + typed + text.slice(caret);
178
+ const shifted = { start: content.start, end: content.end + 1 };
179
+ if (constructAt(paired, shifted, caret - k, caret + 1 + k))
180
+ return null;
181
+ return write(text.slice(0, caret) + typed + text.slice(caret + k), caret + 1);
182
+ }
183
+ // The caret sits inside or at the start of the closing run of a construct this delimiter both
184
+ // opens and closes, so the byte under it is that construct's own closer.
185
+ function closingRunAt(text, content, caret, typed) {
186
+ for (const node of inlineDescendants(parseInline(text, content.start, content.end))) {
187
+ if (node.kind === 'text' || text[node.start] !== typed || node.end <= caret)
188
+ continue;
189
+ let closer = node.end;
190
+ while (closer > caret && text[closer - 1] === typed)
191
+ closer--;
192
+ if (closer <= caret)
193
+ return true;
194
+ }
195
+ return false;
196
+ }
197
+ // A construct of `typed`'s family ends exactly at `end`, its last byte in the closing run. The
198
+ // scan's bound grows past the content by the byte a caller has just inserted.
199
+ function closerEndsAt(text, content, end, typed) {
200
+ const bound = Math.max(content.end, end);
201
+ for (const node of inlineDescendants(parseInline(text, content.start, bound))) {
202
+ if (node.kind === 'text' || node.end !== end || text[end - 1] !== typed)
203
+ continue;
204
+ const inner = constructContentRange(node);
205
+ if (!inner || inner.end < end)
206
+ return true;
207
+ }
208
+ return false;
209
+ }
210
+ function constructAt(text, content, start, end) {
211
+ for (const node of inlineDescendants(parseInline(text, content.start, content.end))) {
212
+ if (node.kind !== 'text' && node.start === start && node.end === end)
213
+ return true;
214
+ }
215
+ return false;
216
+ }
@@ -13,6 +13,8 @@ import { type RawOffset } from '../../../cursor/coordinate-spaces';
13
13
  import type { EdgeAffinity } from '../../../cursor/edge-affinity';
14
14
  import type { PendingMarksState } from '../../../cursor/pending-marks';
15
15
  import { type EdgeDeletionSurface } from './construct-edge-delete';
16
+ /** Whether `line` still reloads as `node`'s kind, the auto-pair resolver's guard. */
17
+ export declare function keepsBlockKind(node: NodeView, line: string): boolean;
16
18
  /** The subset of the inline-widget vocabulary the internal island policies reuse,
17
19
  * expressed in the same terms without leaking into the public API. */
18
20
  export type EdgePolicy = Pick<InlineWidgetEditingPolicy, 'onEdge' | 'deleteGranularity'>;
@@ -70,6 +72,8 @@ export interface EdgePolicyDispatchDeps {
70
72
  isReading: () => boolean;
71
73
  /** The arrival side the typing seat consults; null when no arrival claimed one. */
72
74
  getEdgeAffinity: () => EdgeAffinity | null;
75
+ /** A seated delimiter that completed a construct leaves the caret meaning outside it. */
76
+ noteOutside?: () => void;
73
77
  /** The constructs a collapsed-caret toggle promised the next insertion. Read AND spent
74
78
  * here: the first byte after the chord is the one insertion they were pending for. */
75
79
  pendingMarks: PendingMarksState;
@@ -21,6 +21,12 @@ import { resolveEdgeSeat } from './edge-seat';
21
21
  import { deleteRangeRaw } from './live-selection-edit';
22
22
  import { resolveMarkedInsertion } from './pending-mark-insert';
23
23
  import { widgetAtCursor } from './widget-adjacency';
24
+ import { resolveDelimiterAutoPair } from './delimiter-autopair';
25
+ import { soleProseReparse } from './screen-diff';
26
+ /** Whether `line` still reloads as `node`'s kind, the auto-pair resolver's guard. */
27
+ export function keepsBlockKind(node, line) {
28
+ return soleProseReparse(line + trailingLineEnding(node.raw))?.block.kind === node.kind;
29
+ }
24
30
  const REPLACE_ISLAND_POLICY = {
25
31
  onEdge: 'select',
26
32
  deleteGranularity: 'select-then-delete'
@@ -34,6 +40,11 @@ export function createEdgePolicyDispatch(deps) {
34
40
  reason: 'an explicit instruction about the very next byte, so it outranks every classification below, which decide by where the caret happens to be',
35
41
  claims: handlePendingMarks
36
42
  },
43
+ {
44
+ id: 'transitional-hard-break',
45
+ reason: 'a hard break at the end of a block has no following line yet, so its own position is where the next byte starts that line',
46
+ claims: handleTransitionalHardBreak
47
+ },
37
48
  {
38
49
  id: 'cst-widget',
39
50
  reason: 'a key aimed at an atomic construct is the widget branch’s before any byte rule',
@@ -389,6 +400,35 @@ export function createEdgePolicyDispatch(deps) {
389
400
  return true;
390
401
  }
391
402
  // ── Pending marks (the toggle seat) ────────────────────────────────────────
403
+ /**
404
+ * The byte that completes a hard break made at the END of a block. `insertHardBreak` writes
405
+ * `\\` plus the block's ending there, but that ending IS the block's trailing one, so the
406
+ * display stops at the backslash and the caret has nowhere to sit past it. The next
407
+ * printable key is what supplies the following line: it lands AFTER the break rather than
408
+ * between the backslash and its newline, where it would read as content and undo the break.
409
+ */
410
+ function handleTransitionalHardBreak(e, caretOffset) {
411
+ if (deps.isReading())
412
+ return false;
413
+ if (!isPlainTypingKey(e) || caretOffset === null || hasSelectionHelper())
414
+ return false;
415
+ const d = display();
416
+ // Only at the very end, and only when the block's last byte is the break's backslash.
417
+ if (caretOffset !== d.length || !d.endsWith('\\'))
418
+ return false;
419
+ // An escaped backslash (`\\\\`) is content, not a break.
420
+ if (d.endsWith('\\\\'))
421
+ return false;
422
+ // Backslash before ASCII punctuation is an escape (`\|`, `\*`), never a break's backslash.
423
+ if (/^[!-/:-@[-`{-~]$/.test(e.key))
424
+ return false;
425
+ const ending = trailingLineEnding(deps.node.raw);
426
+ e.preventDefault();
427
+ deps.setSnapTarget(null);
428
+ const next = d + ending + e.key;
429
+ writeDisplay(next, next.length, 'transitional-hard-break', caretOffset);
430
+ return true;
431
+ }
392
432
  /**
393
433
  * A printable key while a collapsed-caret toggle has marks pending. Marks are the newer
394
434
  * instruction about the same bytes, so they outrank the arrival side the seat below reads
@@ -442,14 +482,28 @@ export function createEdgePolicyDispatch(deps) {
442
482
  function handleConstructSeat(e, caretOffset) {
443
483
  if (!isPlainTypingKey(e) || caretOffset === null || hasSelectionHelper())
444
484
  return false;
485
+ // A delimiter typed over its own closer is the auto-pair's step-over, which the beforeinput
486
+ // arm owns (delimiter-autopair.ts); seated outside the run it would be typed instead.
487
+ const content = getContentRange(deps.node);
488
+ const autoPair = resolveDelimiterAutoPair(display(), content, caretOffset, e.key);
489
+ if (autoPair?.kind === 'step-over')
490
+ return false;
445
491
  const el = deps.getEl();
446
492
  const seat = el && typingSeatAt(el, caretOffset, e.key);
447
493
  if (!seat)
448
494
  return false;
449
495
  e.preventDefault();
450
496
  deps.setSnapTarget(null);
451
- // The kind rides the trace: it names the construct whose hidden edge the caret sat at, the
452
- // question this seam answered, so a trace line without it cannot be read.
497
+ // The seat decides WHERE the byte lands; what a delimiter keystroke writes there is still
498
+ // the auto-pair's answer, or a byte seated past a run would arrive without its twin. The
499
+ // kind rides the trace: it names the construct whose hidden edge the caret sat at.
500
+ const paired = resolveDelimiterAutoPair(display(), content, seat.offset, e.key, (line) => keepsBlockKind(deps.node, line));
501
+ if (paired && paired.kind !== 'step-over') {
502
+ writeDisplay(paired.text, paired.caret, `seat:${seat.kind}`, caretOffset);
503
+ if (paired.kind === 'close')
504
+ deps.noteOutside?.();
505
+ return true;
506
+ }
453
507
  editDisplay(seat.offset, seat.offset, e.key, `seat:${seat.kind}`, caretOffset);
454
508
  return true;
455
509
  }
@@ -19,6 +19,8 @@ export function resolveLiveRangeEdit(e, node, cursor, presentationMode, linkRef,
19
19
  if (!range)
20
20
  return null;
21
21
  const insert = replacementText(e);
22
+ if (range.start === range.end)
23
+ return parkedCaretInsertion(node, cursor, range.start, insert);
22
24
  const edit = resolveSelectionEdit(node, range, insert ?? '', presentationMode, linkRef, ambientPrefix);
23
25
  if (!edit)
24
26
  return null;
@@ -26,6 +28,31 @@ export function resolveLiveRangeEdit(e, node, cursor, presentationMode, linkRef,
26
28
  ? { kind: 'swallow' }
27
29
  : { kind: 'rewrite', range, raw: edit.raw, caret: edit.caret };
28
30
  }
31
+ /**
32
+ * A collapsed insertion has no seam to clean, but Chromium inserts at its canonical spelling of
33
+ * the caret's pixel, UPSTREAM across a hidden run, while the DOM caret may sit where a commit
34
+ * parked it, past that run (after a `)` just typed). A target behind the caret is that
35
+ * canonicalization, and the byte belongs where the caret is, the side the keydown seat settled. A
36
+ * target at or past the caret is the engine landing the byte itself, which a split's reopened run
37
+ * relies on: parked at its start, the byte goes inside.
38
+ */
39
+ function parkedCaretInsertion(node, cursor, engineTarget, insert) {
40
+ if (!insert)
41
+ return null;
42
+ const selection = window.getSelection();
43
+ if (!selection || selection.rangeCount === 0 || !selection.isCollapsed)
44
+ return null;
45
+ const caret = cursor.rawRangeOf(selection.getRangeAt(0))?.start ?? null;
46
+ if (caret === null || engineTarget >= caret)
47
+ return null;
48
+ const display = trimTrailingLineEnding(node.raw);
49
+ return {
50
+ kind: 'rewrite',
51
+ range: { start: caret, end: caret },
52
+ raw: display.slice(0, caret) + insert + display.slice(caret) + trailingLineEnding(node.raw),
53
+ caret: caret + insert.length
54
+ };
55
+ }
29
56
  /**
30
57
  * The whole surface arm around {@link resolveLiveRangeEdit}: stand down while a source reveal has
31
58
  * outrun the CST, resolve, preventDefault what the seam owns, then swallow or hand the rewrite to
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pure raw/caret transforms for TextEditableBlock keydown branches — heading-level
2
+ * Pure raw/caret transforms for TextEditableBlock's structural gestures — heading-level
3
3
  * swap, demotion to prose, hard-break insertion, literal-tab insertion. The component
4
4
  * owns the plumbing; these own the string math.
5
5
  */
@@ -21,6 +21,12 @@ export declare function dropStructuralPrefix(raw: string, contentStart: number,
21
21
  /** Drop everything past `contentEnd` but the block's own trailing line ending — the setext
22
22
  * underline, which sits after every caret the content range admits. */
23
23
  export declare function dropStructuralSuffix(raw: string, contentEnd: number, preEditOffset: number): TextEditResult;
24
+ /**
25
+ * An ATX heading left with no text is a marker standing over nothing: unfocused it paints
26
+ * nothing (the chrome-only stamp is focus-scoped), so the block would survive invisibly and
27
+ * resurface as a `#` on the next click. On blur it becomes the empty paragraph it looks like.
28
+ */
29
+ export declare function demoteEmptyAtxHeading(raw: string, content: ContentRange): TextEditResult | null;
24
30
  /**
25
31
  * Re-mark the block's CONTENT with an ATX prefix for `level`, replacing whatever structural bytes
26
32
  * the current kind keeps — the same content range {@link demoteToParagraph} reads, so an indented
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Pure raw/caret transforms for TextEditableBlock keydown branches — heading-level
2
+ * Pure raw/caret transforms for TextEditableBlock's structural gestures — heading-level
3
3
  * swap, demotion to prose, hard-break insertion, literal-tab insertion. The component
4
4
  * owns the plumbing; these own the string math.
5
5
  */
@@ -33,6 +33,16 @@ export function dropStructuralSuffix(raw, contentEnd, preEditOffset) {
33
33
  caretOffset: Math.min(preEditOffset, contentEnd)
34
34
  };
35
35
  }
36
+ /**
37
+ * An ATX heading left with no text is a marker standing over nothing: unfocused it paints
38
+ * nothing (the chrome-only stamp is focus-scoped), so the block would survive invisibly and
39
+ * resurface as a `#` on the next click. On blur it becomes the empty paragraph it looks like.
40
+ */
41
+ export function demoteEmptyAtxHeading(raw, content) {
42
+ if (content.start === 0 || content.end > content.start)
43
+ return null;
44
+ return demoteToParagraph(raw, content, 0);
45
+ }
36
46
  /**
37
47
  * Re-mark the block's CONTENT with an ATX prefix for `level`, replacing whatever structural bytes
38
48
  * the current kind keeps — the same content range {@link demoteToParagraph} reads, so an indented
@@ -6,7 +6,7 @@
6
6
  */
7
7
  import type { AmbientPrefix } from '../../../block-component';
8
8
  import type { DocumentView, NodeView } from '../../../core/node-views';
9
- import type { PresentationMode } from '../../../presentation-mode';
9
+ import { type PresentationMode } from '../../../presentation-mode';
10
10
  import type { ResolveImageUrl, ResolveLinkUrl } from '../../../editor-keys';
11
11
  import type { LinkReferenceResolver } from '../../../core/inline/link-reference-resolver';
12
12
  import { type ImageLoadPolicy } from '../../../core/inline-render';