@voithos-labs/aragonite 0.10.2 → 0.10.4

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 (300) hide show
  1. package/README.md +6 -21
  2. package/THIRD-PARTY-NOTICES.md +25 -0
  3. package/dist/a11y-strings.d.ts +19 -0
  4. package/dist/a11y-strings.js +19 -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 +37 -24
  9. package/dist/components/BlockHost.svelte +18 -11
  10. package/dist/components/Editor.svelte +434 -198
  11. package/dist/components/Editor.svelte.d.ts +1 -1
  12. package/dist/components/SelectionOverlay.svelte +25 -17
  13. package/dist/components/SelectionOverlay.svelte.d.ts +1 -1
  14. package/dist/components/TailInsert.svelte +107 -0
  15. package/dist/components/TailInsert.svelte.d.ts +17 -0
  16. package/dist/components/block-content-selector.d.ts +6 -2
  17. package/dist/components/block-content-selector.js +6 -2
  18. package/dist/components/blocks/ThematicBreakBlock.svelte +19 -6
  19. package/dist/components/blocks/ThematicBreakBlock.svelte.d.ts +1 -0
  20. package/dist/components/blocks/code/CodeBlock.svelte +211 -30
  21. package/dist/components/blocks/code/CodeBlockRail.svelte +704 -0
  22. package/dist/components/blocks/code/CodeBlockRail.svelte.d.ts +26 -0
  23. package/dist/components/blocks/code/code-bootstrap.js +16 -0
  24. package/dist/components/blocks/code/code-context-actions.d.ts +1 -0
  25. package/dist/components/blocks/code/code-context-actions.js +24 -0
  26. package/dist/components/blocks/code/code-fence-exit.d.ts +15 -0
  27. package/dist/components/blocks/code/code-fence-exit.js +26 -0
  28. package/dist/components/blocks/code/code-languages.d.ts +6 -0
  29. package/dist/components/blocks/code/code-languages.js +22 -3
  30. package/dist/components/blocks/code/code-renderer.js +11 -0
  31. package/dist/components/blocks/directive/DirectiveContainerBlock.svelte +1 -1
  32. package/dist/components/blocks/editable-leaf.d.ts +37 -6
  33. package/dist/components/blocks/editable-leaf.js +248 -30
  34. package/dist/components/blocks/editable-surface.d.ts +9 -0
  35. package/dist/components/blocks/editable-surface.js +22 -3
  36. package/dist/components/blocks/list/ListBlock.svelte +1 -0
  37. package/dist/components/blocks/list/ListItemBlock.svelte +14 -5
  38. package/dist/components/blocks/list/ListItemBlock.svelte.d.ts +2 -0
  39. package/dist/components/blocks/list/task-checkbox.d.ts +2 -0
  40. package/dist/components/blocks/list/task-checkbox.js +11 -2
  41. package/dist/components/blocks/surface-wiring.svelte.d.ts +4 -0
  42. package/dist/components/blocks/surface-wiring.svelte.js +8 -1
  43. package/dist/components/blocks/table/TableActionMenu.svelte +210 -86
  44. package/dist/components/blocks/table/TableActionMenu.svelte.d.ts +5 -0
  45. package/dist/components/blocks/table/TableBlock.svelte +205 -186
  46. package/dist/components/blocks/table/TableBlock.svelte.d.ts +1 -0
  47. package/dist/components/blocks/table/TableCellBlock.svelte +87 -22
  48. package/dist/components/blocks/table/TableRowBlock.svelte +4 -18
  49. package/dist/components/blocks/table/TableRowBlock.svelte.d.ts +0 -5
  50. package/dist/components/blocks/table/cell-clipboard.d.ts +10 -0
  51. package/dist/components/blocks/table/cell-clipboard.js +34 -1
  52. package/dist/components/blocks/table/cell-keydown-plan.d.ts +1 -1
  53. package/dist/components/blocks/table/cell-keydown-plan.js +3 -1
  54. package/dist/components/blocks/table/table-cell-paste.js +2 -1
  55. package/dist/components/blocks/table/table-menu-model.d.ts +35 -8
  56. package/dist/components/blocks/table/table-menu-model.js +36 -17
  57. package/dist/components/blocks/text/TextEditableBlock.svelte +58 -11
  58. package/dist/components/blocks/text/click-snap-guard.d.ts +3 -0
  59. package/dist/components/blocks/text/click-snap-guard.js +8 -0
  60. package/dist/components/blocks/text/delimiter-autopair.d.ts +71 -0
  61. package/dist/components/blocks/text/delimiter-autopair.js +216 -0
  62. package/dist/components/blocks/text/edge-policy-dispatch.d.ts +4 -0
  63. package/dist/components/blocks/text/edge-policy-dispatch.js +89 -20
  64. package/dist/components/blocks/text/live-selection-edit.d.ts +5 -5
  65. package/dist/components/blocks/text/live-selection-edit.js +37 -7
  66. package/dist/components/blocks/text/text-clipboard.js +2 -2
  67. package/dist/components/blocks/text/text-keydown.d.ts +7 -1
  68. package/dist/components/blocks/text/text-keydown.js +11 -1
  69. package/dist/components/blocks/text/text-render.d.ts +1 -1
  70. package/dist/components/blocks/text/text-render.js +3 -1
  71. package/dist/components/blocks/text/widget-interaction.d.ts +3 -0
  72. package/dist/components/blocks/text/widget-interaction.js +181 -37
  73. package/dist/components/drag-handle.d.ts +41 -0
  74. package/dist/components/drag-handle.js +134 -0
  75. package/dist/components/editor-root-focus.d.ts +19 -0
  76. package/dist/components/editor-root-focus.js +67 -0
  77. package/dist/components/editor-root-geometry.d.ts +39 -0
  78. package/dist/components/editor-root-geometry.js +91 -0
  79. package/dist/components/editor-root-keydown.d.ts +1 -1
  80. package/dist/components/editor-root-keydown.js +12 -3
  81. package/dist/components/editor-root-listeners.d.ts +3 -6
  82. package/dist/components/editor-root-listeners.js +3 -24
  83. package/dist/components/editor-root-mode-flip.d.ts +36 -0
  84. package/dist/components/editor-root-mode-flip.js +92 -0
  85. package/dist/components/image/ImageOverlayHost.svelte +14 -6
  86. package/dist/components/image/ImageProperties.svelte +512 -70
  87. package/dist/components/image/ImageProperties.svelte.d.ts +6 -1
  88. package/dist/components/image/ImageResizeHandles.svelte +67 -34
  89. package/dist/components/image/image-crop.d.ts +39 -0
  90. package/dist/components/image/image-crop.js +74 -0
  91. package/dist/components/image/image-edit-commit.d.ts +1 -0
  92. package/dist/components/image/image-edit-commit.js +21 -5
  93. package/dist/components/image/image-source-bytes.js +10 -3
  94. package/dist/components/image/image-widget-editing.js +1 -0
  95. package/dist/components/image/widget-dom.js +5 -1
  96. package/dist/components/link-card/link-card-commit.js +1 -1
  97. package/dist/components/lrd-map-gate.js +1 -1
  98. package/dist/components/menu/BlockMenu.svelte +315 -0
  99. package/dist/components/menu/BlockMenu.svelte.d.ts +34 -0
  100. package/dist/components/menu/MenuIcon.svelte +153 -0
  101. package/dist/components/menu/MenuIcon.svelte.d.ts +51 -0
  102. package/dist/components/menu/SelectionToolbar.svelte +385 -0
  103. package/dist/components/menu/SelectionToolbar.svelte.d.ts +16 -0
  104. package/dist/components/menu/clipboard-actions.d.ts +12 -0
  105. package/dist/components/menu/clipboard-actions.js +42 -0
  106. package/dist/components/menu/default-context-actions.d.ts +15 -0
  107. package/dist/components/menu/default-context-actions.js +76 -0
  108. package/dist/components/menu/flyout-placement.d.ts +6 -0
  109. package/dist/components/menu/flyout-placement.js +25 -0
  110. package/dist/core/inline/format-toggle.d.ts +12 -4
  111. package/dist/core/inline/format-toggle.js +94 -40
  112. package/dist/core/inline/image-dimensions.d.ts +3 -0
  113. package/dist/core/inline/image-dimensions.js +46 -10
  114. package/dist/core/inline/inline-widgets.d.ts +19 -0
  115. package/dist/core/inline/inline-widgets.js +5 -0
  116. package/dist/core/inline/scan/brackets.js +1 -0
  117. package/dist/core/inline/scan/plugin-syntax.d.ts +8 -0
  118. package/dist/core/inline/scan/plugin-syntax.js +15 -1
  119. package/dist/core/inline/transparency.js +3 -3
  120. package/dist/core/inline-render.d.ts +6 -0
  121. package/dist/core/inline-render.js +32 -0
  122. package/dist/core/nodes.d.ts +14 -0
  123. package/dist/cursor/edge-affinity.js +2 -1
  124. package/dist/cursor/height-oracle.d.ts +2 -3
  125. package/dist/cursor/height-oracle.js +0 -1
  126. package/dist/cursor/overlay-remeasure.js +8 -0
  127. package/dist/cursor/reveal-source.js +7 -2
  128. package/dist/cursor/scroll-hold.d.ts +10 -0
  129. package/dist/cursor/scroll-hold.js +22 -0
  130. package/dist/cursor/scrollport.d.ts +9 -0
  131. package/dist/cursor/scrollport.js +30 -1
  132. package/dist/cursor/visual-lines.d.ts +5 -4
  133. package/dist/cursor/visual-lines.js +56 -11
  134. package/dist/cursor/widget-edge-snap.d.ts +28 -0
  135. package/dist/cursor/widget-edge-snap.js +38 -0
  136. package/dist/cursor/widget-offset.d.ts +9 -0
  137. package/dist/cursor/widget-offset.js +71 -4
  138. package/dist/debug/interaction-trace.d.ts +4 -0
  139. package/dist/debug/interaction-trace.js +15 -0
  140. package/dist/decorations/decoration-state.svelte.js +1 -1
  141. package/dist/decorations/reserved-attrs.js +1 -0
  142. package/dist/editor-actions/ancestry-folds.d.ts +2 -2
  143. package/dist/editor-actions/ancestry-folds.js +1 -1
  144. package/dist/editor-actions/block-edit-scope.js +1 -1
  145. package/dist/editor-actions/commit/text-batch.d.ts +3 -2
  146. package/dist/editor-actions/commit/text-batch.js +1 -1
  147. package/dist/editor-actions/commit/undo-controller.js +4 -2
  148. package/dist/editor-actions/container-edit.js +2 -1
  149. package/dist/editor-actions/enter-completion.d.ts +2 -0
  150. package/dist/editor-actions/enter-completion.js +23 -2
  151. package/dist/editor-actions/focus/focus-dispatch.js +7 -4
  152. package/dist/editor-actions/focus/focus-landing.d.ts +10 -1
  153. package/dist/editor-actions/focus/focus-landing.js +20 -6
  154. package/dist/editor-actions/inline-range-commit.js +1 -1
  155. package/dist/editor-actions/plugin/container.d.ts +7 -0
  156. package/dist/editor-actions/plugin/container.js +3 -1
  157. package/dist/editor-actions/reorder-action.js +19 -10
  158. package/dist/editor-actions/reorder-drag.js +29 -1
  159. package/dist/editor-actions/replacement-focus.d.ts +1 -1
  160. package/dist/editor-actions/replacement-focus.js +1 -1
  161. package/dist/editor-actions/search-replace.js +1 -1
  162. package/dist/editor-actions/table-context.d.ts +4 -1
  163. package/dist/editor-actions/table-context.js +57 -1
  164. package/dist/editor-events.d.ts +3 -0
  165. package/dist/editor-keys.d.ts +33 -0
  166. package/dist/editor-props.d.ts +24 -12
  167. package/dist/index.d.ts +1 -1
  168. package/dist/plugin.d.ts +7 -0
  169. package/dist/plugin.js +16 -0
  170. package/dist/plugins/latex/BlockMath.svelte +264 -29
  171. package/dist/plugins/latex/BlockMath.svelte.d.ts +2 -0
  172. package/dist/plugins/latex/index.d.ts +2 -1
  173. package/dist/plugins/latex/latex-kind.js +60 -10
  174. package/dist/plugins/latex/math-completion.js +4 -1
  175. package/dist/plugins/latex/math-layout.d.ts +15 -0
  176. package/dist/plugins/latex/math-layout.js +13 -0
  177. package/dist/plugins/latex/math-source.d.ts +19 -0
  178. package/dist/plugins/latex/math-source.js +103 -0
  179. package/dist/plugins/latex/register.d.ts +11 -2
  180. package/dist/plugins/latex/register.js +3 -1
  181. package/dist/plugins/latex/renderer.d.ts +3 -3
  182. package/dist/plugins/latex/renderer.js +13 -6
  183. package/dist/plugins/mermaid/MermaidBlock.svelte +19 -9
  184. package/dist/plugins/parrot/ParrotBlock.svelte +3 -1
  185. package/dist/reactivity/list-windowing.svelte.d.ts +8 -8
  186. package/dist/reactivity/list-windowing.svelte.js +75 -30
  187. package/dist/schema/block-completions.d.ts +8 -0
  188. package/dist/schema/block-completions.js +11 -0
  189. package/dist/schema/commands.d.ts +9 -5
  190. package/dist/schema/commands.js +12 -7
  191. package/dist/schema/context-actions.d.ts +31 -0
  192. package/dist/schema/context-actions.js +23 -0
  193. package/dist/schema/fenced-code-raw.js +31 -1
  194. package/dist/schema/operations.d.ts +8 -1
  195. package/dist/schema/reserved-chords.js +37 -4
  196. package/dist/schema/table-cell-raw.d.ts +1 -1
  197. package/dist/schema/table-cell-raw.js +1 -1
  198. package/dist/selection/block-hit-test.js +3 -2
  199. package/dist/selection/char-endpoint-snap.js +1 -1
  200. package/dist/selection/clipboard-text.js +6 -1
  201. package/dist/selection/covered-block.d.ts +10 -0
  202. package/dist/selection/covered-block.js +24 -0
  203. package/dist/selection/cross-block/dispatch.d.ts +3 -0
  204. package/dist/selection/cross-block/dispatch.js +13 -1
  205. package/dist/selection/cross-block/format-range.d.ts +1 -1
  206. package/dist/selection/cross-block/format-range.js +4 -15
  207. package/dist/selection/cross-block/format-toggle.js +1 -1
  208. package/dist/selection/cross-block/keydown.js +3 -29
  209. package/dist/selection/cross-block/ops.js +1 -1
  210. package/dist/selection/cross-block/paste.js +31 -35
  211. package/dist/selection/cross-block/type-replace.d.ts +3 -2
  212. package/dist/selection/cross-block/type-replace.js +52 -13
  213. package/dist/selection/dead-space-caret.d.ts +13 -0
  214. package/dist/selection/dead-space-caret.js +57 -1
  215. package/dist/selection/drag-pointer.d.ts +26 -2
  216. package/dist/selection/drag-pointer.js +103 -2
  217. package/dist/selection/gap-caret.js +1 -1
  218. package/dist/selection/keyboard-extend.d.ts +3 -2
  219. package/dist/selection/keyboard-extend.js +4 -3
  220. package/dist/selection/multi-click.d.ts +42 -0
  221. package/dist/selection/multi-click.js +203 -0
  222. package/dist/selection/native-bridge.d.ts +3 -0
  223. package/dist/selection/native-bridge.js +31 -2
  224. package/dist/selection/path-lookup.d.ts +10 -2
  225. package/dist/selection/path-lookup.js +11 -4
  226. package/dist/selection/pointer-gesture.d.ts +9 -0
  227. package/dist/selection/pointer-gesture.js +11 -0
  228. package/dist/selection/primitives.d.ts +7 -0
  229. package/dist/selection/primitives.js +19 -1
  230. package/dist/selection/range-delete-ceremony.js +4 -2
  231. package/dist/selection/range-delete-chrome.js +2 -1
  232. package/dist/selection/range-delete-table-coverage.js +2 -1
  233. package/dist/selection/range-delete-table.js +3 -2
  234. package/dist/selection/range-delete.d.ts +4 -0
  235. package/dist/selection/range-delete.js +31 -3
  236. package/dist/selection/selection-drop.d.ts +34 -0
  237. package/dist/selection/selection-drop.js +253 -0
  238. package/dist/selection/selection-restore.js +1 -1
  239. package/dist/selection/selection-state.svelte.d.ts +6 -0
  240. package/dist/selection/selection-state.svelte.js +36 -1
  241. package/dist/selection/table-endpoint-snap.js +1 -1
  242. package/dist/selection/table-rect-extend.js +1 -1
  243. package/dist/styles/editor-theme.css +75 -31
  244. package/dist/styles/editor.css +268 -18
  245. package/dist/testing/container-conformance.js +2 -2
  246. package/dist/testing/inline-conformance.js +2 -1
  247. package/dist/tree-operations/blockquote.js +1 -1
  248. package/dist/tree-operations/chain-rebuild.d.ts +63 -0
  249. package/dist/tree-operations/chain-rebuild.js +142 -0
  250. package/dist/tree-operations/children.d.ts +1 -1
  251. package/dist/tree-operations/children.js +1 -1
  252. package/dist/tree-operations/cleanup.js +1 -1
  253. package/dist/tree-operations/content-write.d.ts +50 -0
  254. package/dist/tree-operations/content-write.js +263 -0
  255. package/dist/tree-operations/index.d.ts +8 -3
  256. package/dist/tree-operations/index.js +6 -2
  257. package/dist/tree-operations/list/exit-replacement.js +1 -1
  258. package/dist/tree-operations/list/unwrap-merge.js +3 -3
  259. package/dist/tree-operations/node-ops.d.ts +17 -234
  260. package/dist/tree-operations/node-ops.js +47 -1113
  261. package/dist/tree-operations/node-primitives.d.ts +75 -0
  262. package/dist/tree-operations/node-primitives.js +117 -0
  263. package/dist/tree-operations/paste/apply.js +1 -1
  264. package/dist/tree-operations/paste/body-write.d.ts +1 -1
  265. package/dist/tree-operations/paste/body-write.js +2 -2
  266. package/dist/tree-operations/paste/container-match.js +4 -2
  267. package/dist/tree-operations/paste/dispatch.js +2 -1
  268. package/dist/tree-operations/paste/find-enclosing-list.js +1 -1
  269. package/dist/tree-operations/paste/focus-target.d.ts +1 -1
  270. package/dist/tree-operations/paste/list-absorb.js +1 -1
  271. package/dist/tree-operations/paste/list-break-out.js +1 -1
  272. package/dist/tree-operations/paste/parent-scope.js +1 -1
  273. package/dist/tree-operations/paste/paste-replacement.js +1 -1
  274. package/dist/tree-operations/paste/replace-block-at-parent.d.ts +3 -1
  275. package/dist/tree-operations/paste/replace-block-at-parent.js +5 -2
  276. package/dist/tree-operations/paste/replacement-parse.d.ts +17 -0
  277. package/dist/tree-operations/paste/replacement-parse.js +22 -0
  278. package/dist/tree-operations/path-mutate.d.ts +1 -1
  279. package/dist/tree-operations/path-mutate.js +2 -1
  280. package/dist/tree-operations/reorder-unit.js +1 -1
  281. package/dist/tree-operations/reorder.d.ts +5 -2
  282. package/dist/tree-operations/reorder.js +55 -2
  283. package/dist/tree-operations/settle.d.ts +105 -0
  284. package/dist/tree-operations/settle.js +660 -0
  285. package/dist/tree-operations/table-grid-clipboard.d.ts +21 -0
  286. package/dist/tree-operations/table-grid-clipboard.js +90 -0
  287. package/dist/tree-operations/unshare.d.ts +15 -77
  288. package/dist/tree-operations/unshare.js +15 -166
  289. package/docs/guide/consumer-guide.md +136 -104
  290. package/docs/guide/plugin-api.md +73 -29
  291. package/docs/guide/plugin-guide.md +55 -23
  292. package/package.json +9 -7
  293. package/dist/components/blocks/code/CodeLanguageChip.svelte +0 -127
  294. package/dist/components/blocks/code/CodeLanguageChip.svelte.d.ts +0 -14
  295. package/dist/components/blocks/table/TableGrip.svelte +0 -91
  296. package/dist/components/blocks/table/TableGrip.svelte.d.ts +0 -8
  297. package/dist/components/blocks/table/table-drop-target.d.ts +0 -1
  298. package/dist/components/blocks/table/table-drop-target.js +0 -16
  299. package/dist/components/blocks/table/table-reorder-drag.d.ts +0 -78
  300. package/dist/components/blocks/table/table-reorder-drag.js +0 -97
@@ -1,10 +1,11 @@
1
1
  /**
2
2
  * Toggle an inline format inside a prose block. Over a SELECTION the direction is parse coverage,
3
3
  * not edge adjacency: a covered range unapplies (aligned strip, else split, over every covering run
4
- * in turn), overlapping or abutting runs apply over their union, a bare range wraps. Where the mode
5
- * PAINTS delimiters the strip and the wrap write literally; every other candidate declines unless it
6
- * verifies. At a COLLAPSED CARET: unwrap the span, else drop the empty pair, else insert one
7
- * (live-mode.md § 4.3). Every write clamps to the CONTENT range: a marker in `# ` changes the kind.
4
+ * in turn), overlapping or abutting runs apply over their union, a bare range wraps the core its
5
+ * boundary whitespace leaves. Where the mode PAINTS delimiters the strip and the wrap write
6
+ * unverified; every other candidate declines unless it verifies. At a COLLAPSED CARET: unwrap the
7
+ * span, else drop the empty pair, else insert one (live-mode.md § 4.3). Every write clamps to the
8
+ * CONTENT range: a marker in `# ` changes the kind.
8
9
  */
9
10
  import { recordFormatCoverageRead } from '../../perf/instruments';
10
11
  import { paintsFocusedMarkers } from '../../presentation-mode';
@@ -29,29 +30,31 @@ export function toggleInlineFormat(edit, format, mode) {
29
30
  // Painted delimiters are the mode's own answer, so those modes write their candidate unverified.
30
31
  // The preview rungs paint: this seam only writes into the block those rungs reveal.
31
32
  const paints = paintsFocusedMarkers(mode ?? 'source');
32
- const covering = coveringSpansOf(inlines, start, end, format);
33
+ const { from, to, covering } = coveredReading(display, inlines, start, end, format);
33
34
  // A strip sheds ONE run, so where a second of the same kind covers the selection too, the answer
34
35
  // is the covering arm's split: stripping there leaves the press's own read still active.
35
- const sole = covering.length > 1 ? null : soleStripCandidate(display, inlines, start, end, format);
36
+ const sole = covering.length > 1 ? null : soleStripCandidate(display, inlines, from, to, format);
36
37
  if (sole)
37
38
  return paints || preservesScreen(sole, edit, screenOf(display, content)) ? sole : null;
38
39
  // The flank strip rewrites bytes OUTSIDE the selection, and byte equality can mistake a nested
39
40
  // run's delimiters for the enclosing one's, so its candidate verifies like the split's.
40
- const enclosing = enclosingSpanOf(inlines, start, end, format);
41
- if (enclosing && flanksAreItsMarkers(display, start, end, enclosing)) {
42
- const flank = firstFlipVerified([flankStrip(display, start, end, enclosing)], edit, format, 'unapply');
41
+ const enclosing = enclosingSpanOf(inlines, from, to, format);
42
+ if (enclosing && flanksAreItsMarkers(display, from, to, enclosing)) {
43
+ const flank = firstFlipVerified([flankStrip(display, from, to, enclosing)], edit, format, 'unapply');
43
44
  if (flank)
44
45
  return flank;
45
46
  }
46
47
  if (covering.length > 0)
47
- return firstFlipVerified(covering.flatMap((span) => splitCandidates(display, inlines, span, start, end, format)), edit, format, 'unapply');
48
+ return firstFlipVerified(covering.flatMap((span) => splitCandidates(display, inlines, span, from, to, format)), edit, format, 'unapply');
48
49
  const union = formatUnionOf(inlines, start, end, format);
49
50
  if (union)
50
- return firstFlipVerified(absorbCandidates(display, inlines, union, format, mark), edit, format, 'apply');
51
+ return firstFlipVerified(absorbCandidates(display, inlines, union, { start, end }, format, mark), edit, format, 'apply');
51
52
  // A wrap whose markers do not paint owes split and absorb's coverage check: bytes that re-pair
52
53
  // against a neighbouring run leave the range unformatted with nothing on screen to say so.
53
- const wraps = wrapCandidates(display, inlines, start, end, mark);
54
- return paints ? (wraps[0] ?? null) : firstFlipVerified(wraps, edit, format, 'apply');
54
+ const wrap = wrapCandidate(display, inlines, start, end, mark);
55
+ if (!wrap)
56
+ return null;
57
+ return paints ? wrap : firstFlipVerified([wrap], edit, format, 'apply');
55
58
  }
56
59
  /** Whether a toggle right now would UNAPPLY — the pressed-state a toolbar paints. The same arms
57
60
  * the toggle routes by, asked without emitting: a caret inside the construct, a selection
@@ -137,7 +140,24 @@ function coverageCarries({ display, start, end, inlines, sliceNodes }, format) {
137
140
  const enclosing = enclosingSpanOf(inlines, start, end, format);
138
141
  if (enclosing && flanksAreItsMarkers(display, start, end, enclosing))
139
142
  return true;
140
- return coveringSpansOf(inlines, start, end, format).length > 0;
143
+ return coveredReading(display, inlines, start, end, format).covering.length > 0;
144
+ }
145
+ /**
146
+ * The range an unapply reads, with the runs covering it. A run closes against a word and never
147
+ * whitespace, so the wrap leaves a boundary space OUTSIDE the delimiters: a selection reaching
148
+ * past a run by whitespace alone is still that run's own, or it could not take its own mark off.
149
+ */
150
+ function coveredReading(display, inlines, start, end, format) {
151
+ const covering = coveringSpansOf(inlines, start, end, format);
152
+ if (covering.length > 0)
153
+ return { from: start, to: end, covering };
154
+ const trimmed = withoutBoundaryWhitespace(display, start, end);
155
+ if (!trimmed || (trimmed.start === start && trimmed.end === end))
156
+ return { from: start, to: end, covering };
157
+ const inner = coveringSpansOf(inlines, trimmed.start, trimmed.end, format);
158
+ return inner.length > 0
159
+ ? { from: trimmed.start, to: trimmed.end, covering: inner }
160
+ : { from: start, to: end, covering };
141
161
  }
142
162
  // ── Aligned unapply ──────────────────────────────────────────────────────────
143
163
  /** The selection carries its own flanking markers (the user selected `**word**`): exactly one
@@ -247,18 +267,37 @@ function formatUnionOf(inlines, start, end, format) {
247
267
  }
248
268
  return touched ? { start: from, end: to } : null;
249
269
  }
250
- function absorbCandidates(display, inlines, union, format, mark) {
270
+ /** The run grows to the union, the SELECTION does not: each endpoint is carried through the
271
+ * strip and re-wrap, so a press over half a run leaves that half selected. An endpoint on the
272
+ * union's edge keeps the new marker inside the range, as a bare wrap's selection does. */
273
+ function absorbCandidates(display, inlines, union, selected, format, mark) {
251
274
  if (!cutsLandCleanly(inlines, union, union))
252
275
  return [];
253
- const stripped = stripKindMarkers(display, inlines, format, union.start, union.end);
276
+ const cuts = kindMarkerCuts(inlines, format, union.start, union.end);
277
+ const stripped = spliceOutCuts(display, cuts, union.start, union.end);
254
278
  const prefix = display.slice(0, union.start);
255
279
  const suffix = display.slice(union.end);
256
280
  const wrapAt = (lead, core, trail) => {
257
281
  const wrapped = wrapSlice(core, mark);
282
+ // The kind may pad its fence, so the marker widths are read off the wrap, not the row.
283
+ const found = core ? wrapped.indexOf(core) : -1;
284
+ const open = found >= 0 ? found : (wrapped.length - core.length) / 2;
285
+ const close = wrapped.length - core.length - open;
286
+ const coreStart = lead.length;
287
+ const coreEnd = lead.length + core.length;
288
+ const startAt = strippedOffset(cuts, union.start, selected.start);
289
+ const endAt = strippedOffset(cuts, union.start, selected.end);
290
+ const shift = (at, edgeInclusive) => {
291
+ if (edgeInclusive === 'start' ? at <= coreStart : at < coreStart)
292
+ return at;
293
+ if (edgeInclusive === 'start' ? at <= coreEnd : at < coreEnd)
294
+ return at + open;
295
+ return at + open + close;
296
+ };
258
297
  return {
259
298
  newDisplay: prefix + lead + wrapped + trail + suffix,
260
- newSelStart: prefix.length + lead.length,
261
- newSelEnd: prefix.length + lead.length + wrapped.length
299
+ newSelStart: prefix.length + shift(startAt, 'start'),
300
+ newSelEnd: prefix.length + shift(endAt, 'end')
262
301
  };
263
302
  };
264
303
  const out = [wrapAt('', stripped, '')];
@@ -288,6 +327,10 @@ function cutsLandCleanly(inlines, cuts, within) {
288
327
  /** The bytes of `[from, to)` with the delimiter runs of every same-format construct lying wholly
289
328
  * inside removed; one straddling the range keeps its markers, for the verifier to judge. */
290
329
  function stripKindMarkers(display, inlines, format, from, to) {
330
+ return spliceOutCuts(display, kindMarkerCuts(inlines, format, from, to), from, to);
331
+ }
332
+ /** The marker byte ranges of every `format` run lying wholly inside [from, to), in order. */
333
+ function kindMarkerCuts(inlines, format, from, to) {
291
334
  const cuts = [];
292
335
  for (const node of inlineDescendants(inlines)) {
293
336
  if (node.kind !== format)
@@ -297,7 +340,9 @@ function stripKindMarkers(display, inlines, format, from, to) {
297
340
  continue;
298
341
  cuts.push([span.start, span.contentStart], [span.contentEnd, span.end]);
299
342
  }
300
- cuts.sort((a, b) => a[0] - b[0]);
343
+ return cuts.sort((a, b) => a[0] - b[0]);
344
+ }
345
+ function spliceOutCuts(display, cuts, from, to) {
301
346
  let out = '';
302
347
  let at = from;
303
348
  for (const [cutFrom, cutTo] of cuts) {
@@ -306,30 +351,27 @@ function stripKindMarkers(display, inlines, format, from, to) {
306
351
  }
307
352
  return out + display.slice(at, to);
308
353
  }
354
+ /** Where a display offset lands in the stripped slice: the cut bytes before it fall away, and an
355
+ * offset inside a marker rides to that marker's start. */
356
+ function strippedOffset(cuts, from, offset) {
357
+ let removed = 0;
358
+ for (const [cutFrom, cutTo] of cuts)
359
+ if (cutFrom < offset)
360
+ removed += Math.min(cutTo, offset) - cutFrom;
361
+ return Math.max(0, offset - from - removed);
362
+ }
309
363
  // ── Wrap ─────────────────────────────────────────────────────────────────────
310
364
  /**
311
- * What a bare wrap could mean, what it literally says first. Only the wrap has a second reading:
312
- * markdown opens and closes a run against a word, never whitespace, so a boundary space goes to
313
- * the plain text beside the run, as `live-split-rebalance` reads the same problem. Either reading
314
- * needs both its endpoints to be legal cuts: markers spliced into a construct's bytes re-pair.
365
+ * The bare wrap, over the selection's trimmed core: markdown opens and closes a run against a word,
366
+ * never whitespace, so a boundary space goes to the plain text beside the run, as
367
+ * `live-split-rebalance` reads the same problem. Null where nothing survives the trim, or where an
368
+ * endpoint is no legal cut: markers spliced into a construct's bytes re-pair.
315
369
  */
316
- function wrapCandidates(display, inlines, start, end, mark) {
317
- const core = trimmedRange(display, start, end);
318
- const readings = core === null ? [{ start, end }] : [{ start, end }, core];
319
- return readings
320
- .filter((range) => cutsLandCleanly(inlines, range, range))
321
- .map((range) => wrapRange(display, range.start, range.end, mark));
322
- }
323
- /** The selection minus its boundary whitespace, or null when there is none to trim or nothing
324
- * left once it goes. */
325
- function trimmedRange(display, start, end) {
326
- let from = start;
327
- let to = end;
328
- while (from < to && /\s/.test(display[from]))
329
- from++;
330
- while (to > from && /\s/.test(display[to - 1]))
331
- to--;
332
- return (from === start && to === end) || from === to ? null : { start: from, end: to };
370
+ function wrapCandidate(display, inlines, start, end, mark) {
371
+ const core = withoutBoundaryWhitespace(display, start, end);
372
+ if (!core || !cutsLandCleanly(inlines, core, core))
373
+ return null;
374
+ return wrapRange(display, core.start, core.end, mark);
333
375
  }
334
376
  function wrapRange(display, start, end, mark) {
335
377
  const wrapped = wrapSlice(display.slice(start, end), mark);
@@ -527,6 +569,18 @@ function leadingWs(text) {
527
569
  function trailingWs(text) {
528
570
  return /\s*$/.exec(text)[0];
529
571
  }
572
+ /** A range minus its boundary whitespace, null once nothing is left. One home for the trim, so the
573
+ * wrap here and the cross-block decomposition that trims before it ever asks
574
+ * (`selection/cross-block/format-range.ts`) cannot disagree about where a run may close. */
575
+ export function withoutBoundaryWhitespace(display, from, to) {
576
+ let start = from;
577
+ let end = to;
578
+ while (start < end && /\s/.test(display[start]))
579
+ start++;
580
+ while (end > start && /\s/.test(display[end - 1]))
581
+ end--;
582
+ return start === end ? null : { start, end };
583
+ }
530
584
  // ── Content clamp ────────────────────────────────────────────────────────────
531
585
  function clampToContent(offset, content) {
532
586
  return Math.min(Math.max(offset, content.start), content.end);
@@ -1,6 +1,9 @@
1
+ import type { ImageCrop } from '../nodes';
2
+ export declare const MAX_CROP_ZOOM = 4;
1
3
  export interface ParsedImageAlt {
2
4
  displayAlt: string;
3
5
  width: number | undefined;
4
6
  height: number | undefined;
7
+ crop: ImageCrop | undefined;
5
8
  }
6
9
  export declare function parseImageDimensions(alt: string): ParsedImageAlt;
@@ -1,14 +1,17 @@
1
- // `|N` / `|NxM` is an Obsidian extension, not part of GFM.
1
+ // `|N` / `|NxM` is an Obsidian extension, not part of GFM. The `@X,Y[,Z]` tail on the `NxM`
2
+ // form is this editor's own: the pan and zoom of the image inside that frame.
2
3
  const MAX_DIMENSION = 10000;
3
- // The longest zero-free decodable suffix, `|10000x10000`. Bounding the pipe search to that tail
4
- // keeps nested-label floods linear, at the deliberate cost of not decoding zero-padded runs.
5
- const MAX_SUFFIX_SEARCH = 12;
4
+ export const MAX_CROP_ZOOM = 4;
5
+ // The longest decodable suffix, `|10000x10000@100,100,4.00`. Bounding the pipe search to that
6
+ // tail keeps nested-label floods linear.
7
+ const MAX_SUFFIX_SEARCH = 26;
8
+ const NO_HINT = { width: undefined, height: undefined, crop: undefined };
6
9
  export function parseImageDimensions(alt) {
7
10
  const lastPipe = boundedLastPipe(alt);
8
11
  const dims = lastPipe === -1 ? null : parseDimensionSuffix(alt.slice(lastPipe + 1));
9
12
  if (dims === null)
10
- return { displayAlt: alt, width: undefined, height: undefined };
11
- return { displayAlt: alt.slice(0, lastPipe), width: dims.width, height: dims.height };
13
+ return { displayAlt: alt, ...NO_HINT };
14
+ return { displayAlt: alt.slice(0, lastPipe), ...dims };
12
15
  }
13
16
  function boundedLastPipe(alt) {
14
17
  const floor = alt.length > MAX_SUFFIX_SEARCH ? alt.length - MAX_SUFFIX_SEARCH : 0;
@@ -24,16 +27,49 @@ function parseDimensionSuffix(s) {
24
27
  const w = parseStrictInt(s);
25
28
  if (w === null)
26
29
  return null;
27
- return { width: w, height: undefined };
30
+ return { width: w, height: undefined, crop: undefined };
28
31
  }
32
+ const atIdx = s.indexOf('@');
33
+ const heightEnd = atIdx === -1 ? s.length : atIdx;
29
34
  const w = parseStrictInt(s.slice(0, xIdx));
30
- const h = parseStrictInt(s.slice(xIdx + 1));
35
+ const h = parseStrictInt(s.slice(xIdx + 1, heightEnd));
31
36
  if (w === null || h === null)
32
37
  return null;
33
- return { width: w, height: h };
38
+ if (atIdx === -1)
39
+ return { width: w, height: h, crop: undefined };
40
+ const crop = parseCropTail(s.slice(atIdx + 1));
41
+ // A malformed tail is not a hint at all: the whole suffix stays alt text.
42
+ if (crop === null)
43
+ return null;
44
+ return { width: w, height: h, crop };
45
+ }
46
+ /** `X,Y` or `X,Y,Z`: whole percents 0–100 and a zoom of 1–4 with up to two decimals. */
47
+ function parseCropTail(s) {
48
+ const parts = s.split(',');
49
+ if (parts.length < 2 || parts.length > 3)
50
+ return null;
51
+ const x = parsePercent(parts[0]);
52
+ const y = parsePercent(parts[1]);
53
+ if (x === null || y === null)
54
+ return null;
55
+ if (parts.length === 2)
56
+ return { x, y, z: 1 };
57
+ if (!/^[1-4](?:\.\d{1,2})?$/.test(parts[2]))
58
+ return null;
59
+ const z = Number(parts[2]);
60
+ if (z > MAX_CROP_ZOOM)
61
+ return null;
62
+ return { x, y, z };
63
+ }
64
+ function parsePercent(s) {
65
+ if (!/^\d{1,3}$/.test(s))
66
+ return null;
67
+ const n = Number(s);
68
+ return n > 100 ? null : n;
34
69
  }
70
+ /** No leading zeros: a padded run is alt text, not a hint. */
35
71
  function parseStrictInt(s) {
36
- if (s.length === 0 || !/^\d+$/.test(s))
72
+ if (!/^[1-9]\d*$/.test(s))
37
73
  return null;
38
74
  const n = Number(s);
39
75
  if (n <= 0 || n > MAX_DIMENSION)
@@ -67,6 +67,22 @@ export declare const ON_EDGE_POLICIES: readonly ["select", "step-over"];
67
67
  */
68
68
  export interface InlineWidgetEditingPolicy {
69
69
  revealSource?: boolean;
70
+ /**
71
+ * Where this kind's editable CONTENT sits inside its source span, as offsets relative to
72
+ * that span — `$x$` answers `{ start: 1, end: 2 }`. It bounds a caret entering the source,
73
+ * and `end` is where a revealing click lands when the kind maps no point of its own. Only
74
+ * the kind knows its own delimiters; absent, the caret keeps the leading edge.
75
+ */
76
+ revealContentSpan?: (source: string) => {
77
+ start: number;
78
+ end: number;
79
+ } | null;
80
+ /**
81
+ * The offset in `source` a press on the RENDERED widget names, so a click seats the caret
82
+ * where it landed rather than at one edge. Only the kind can map its render back to bytes (a
83
+ * KaTeX island paints glyphs, not source); null declines this point and keeps the fallback.
84
+ */
85
+ revealOffsetAtPoint?: (widgetEl: HTMLElement, source: string, clientX: number, clientY: number) => number | null;
70
86
  deleteGranularity?: (typeof DELETE_GRANULARITIES)[number];
71
87
  onEdge?: (typeof ON_EDGE_POLICIES)[number];
72
88
  onSelectedKey?: (e: KeyboardEvent, ctx: InlineWidgetEditingContext) => boolean;
@@ -107,6 +123,9 @@ export declare function augmentInlineWidgetKind(kind: AnyInlineKind, editing: Pa
107
123
  /** Kind-level recognition, independent of per-block render policy (renderImagesAsWidgets). */
108
124
  export declare function isInlineWidget(node: InlineNode, raw: string): boolean;
109
125
  export declare function getInlineWidgetEditing(kind: AnyInlineKind): InlineWidgetEditingPolicy | undefined;
126
+ /** A kind the caret reads as one character: it steps over in one press, carries a column of its
127
+ * own, and a press ON its glyph names an edge rather than selecting the island whole. */
128
+ export declare function isCharacterLikeWidget(kind: AnyInlineKind): boolean;
110
129
  export declare function getInlineWidgetComponent(kind: AnyInlineKind): Component<InlineWidgetComponentProps> | undefined;
111
130
  /**
112
131
  * Every live widget reachable from `nodes`, in document order. Descends so a widget nested in a
@@ -74,6 +74,11 @@ export function isInlineWidget(node, raw) {
74
74
  export function getInlineWidgetEditing(kind) {
75
75
  return registry.get(kind)?.editing;
76
76
  }
77
+ /** A kind the caret reads as one character: it steps over in one press, carries a column of its
78
+ * own, and a press ON its glyph names an edge rather than selecting the island whole. */
79
+ export function isCharacterLikeWidget(kind) {
80
+ return getInlineWidgetEditing(kind)?.onEdge === 'step-over';
81
+ }
77
82
  export function getInlineWidgetComponent(kind) {
78
83
  return registry.get(kind)?.component;
79
84
  }
@@ -112,6 +112,7 @@ function buildImage(raw, opener, labelEnd, target, children) {
112
112
  ...(target.title !== undefined ? { title: target.title } : {}),
113
113
  ...(dims.width !== undefined ? { width: dims.width } : {}),
114
114
  ...(dims.height !== undefined ? { height: dims.height } : {}),
115
+ ...(dims.crop !== undefined ? { crop: dims.crop } : {}),
115
116
  ...(target.label !== undefined ? { label: target.label } : {})
116
117
  };
117
118
  }
@@ -38,6 +38,12 @@ export interface InlineSyntaxOptions {
38
38
  * commit's equality guard. See the plugin guide's inline section.
39
39
  */
40
40
  rewriteImage?: ImageSyntaxRewriter;
41
+ /**
42
+ * The trigger is a symmetric one-byte delimiter (`$…$`) a keystroke should close at once:
43
+ * typing it lands its twin after the caret, so the new opener pairs with that twin rather
44
+ * than with a later formula's delimiter (see `delimiter-autopair.ts`). Bare triggers only.
45
+ */
46
+ autoPair?: boolean;
41
47
  }
42
48
  export interface InlineRung extends InlineSyntaxClaim {
43
49
  recognizer: InlineSyntaxRecognizer;
@@ -57,6 +63,8 @@ export declare function hasInlineSyntax(): boolean;
57
63
  /** Read once per scan, so an empty set leaves `needsScan` at its pre-plugin cost. */
58
64
  export declare function hasScanProbeRungs(): boolean;
59
65
  export declare function isScanProbeTrigger(char: string): boolean;
66
+ /** Whether a plugin asked for `char` to close itself as it is typed. */
67
+ export declare function isAutoPairTrigger(char: string): boolean;
60
68
  /** False costs the scan loop nothing. */
61
69
  export declare function hasPrefixRungs(): boolean;
62
70
  export declare function __resetInlineSyntaxForTests(): void;
@@ -36,6 +36,9 @@ const NO_RUNGS = [];
36
36
  // (`default` arm) so each dispatch path reads one map and its empty gate is one `size` check.
37
37
  const reservedRegistry = new Map();
38
38
  const unreservedRegistry = new Map();
39
+ // Triggers whose keystroke closes itself (`autoPair`); the built-in backtick is not registered
40
+ // here, the typing seam knows it on its own.
41
+ const autoPairTriggers = new Set();
39
42
  // Triggers the fast bail (`needsScan`, scan/index.ts) must visit while a rung lives on them.
40
43
  // Maintained at registration, so a rung on a trigger `SPECIAL_CHARS` already visits costs nothing.
41
44
  const scanProbeTriggers = new Set();
@@ -44,7 +47,7 @@ export function registerInlineSyntax(trigger, recognizer, options) {
44
47
  if (trigger.length !== 1) {
45
48
  throw new Error('registerInlineSyntax: trigger must be a single character');
46
49
  }
47
- const { prefix, priority = INLINE_PRIORITIES.plugin, rewriteImage } = options ?? {};
50
+ const { prefix, priority = INLINE_PRIORITIES.plugin, rewriteImage, autoPair } = options ?? {};
48
51
  if (prefix !== undefined && (prefix.length < 2 || !prefix.startsWith(trigger))) {
49
52
  throw new Error(`registerInlineSyntax: prefix ${JSON.stringify(prefix)} must begin with the trigger ` +
50
53
  `${JSON.stringify(trigger)} and be at least two characters`);
@@ -69,6 +72,10 @@ export function registerInlineSyntax(trigger, recognizer, options) {
69
72
  `the built-in scanner; got ${priority}`);
70
73
  }
71
74
  }
75
+ if (autoPair && prefix !== undefined) {
76
+ throw new Error(`registerInlineSyntax: autoPair pairs the bare trigger ${JSON.stringify(trigger)} with ` +
77
+ `itself, so it cannot be combined with a prefix`);
78
+ }
72
79
  const effectivePrefix = prefix ?? trigger;
73
80
  const registry = reserved ? reservedRegistry : unreservedRegistry;
74
81
  const existing = registry.get(trigger);
@@ -84,6 +91,8 @@ export function registerInlineSyntax(trigger, recognizer, options) {
84
91
  // or the recognizer is the silent no-op this seam refuses to accept.
85
92
  if (!reserved || SCAN_PROBED_RESERVED.has(trigger))
86
93
  scanProbeTriggers.add(trigger);
94
+ if (autoPair)
95
+ autoPairTriggers.add(trigger);
87
96
  }, `registerInlineSyntax: ${JSON.stringify(trigger)} already registered at prefix ` +
88
97
  `${JSON.stringify(effectivePrefix)}, priority ${priority}`);
89
98
  }
@@ -139,6 +148,10 @@ export function hasScanProbeRungs() {
139
148
  export function isScanProbeTrigger(char) {
140
149
  return scanProbeTriggers.has(char);
141
150
  }
151
+ /** Whether a plugin asked for `char` to close itself as it is typed. */
152
+ export function isAutoPairTrigger(char) {
153
+ return autoPairTriggers.has(char);
154
+ }
142
155
  /** False costs the scan loop nothing. */
143
156
  export function hasPrefixRungs() {
144
157
  return reservedRegistry.size > 0;
@@ -147,4 +160,5 @@ export function __resetInlineSyntaxForTests() {
147
160
  reservedRegistry.clear();
148
161
  unreservedRegistry.clear();
149
162
  scanProbeTriggers.clear();
163
+ autoPairTriggers.clear();
150
164
  }
@@ -5,7 +5,7 @@
5
5
  * an unparsed block degrades to "land on it".
6
6
  */
7
7
  import { getInlineContent } from './inline-cache';
8
- import { isInlineWidget, getInlineWidgetEditing } from './inline-widgets';
8
+ import { isInlineWidget, isCharacterLikeWidget } from './inline-widgets';
9
9
  export function isVerticallyTransparentNode(node) {
10
10
  if (!node)
11
11
  return false;
@@ -26,8 +26,8 @@ export function isVerticallyTransparentNode(node) {
26
26
  return false;
27
27
  for (const inline of inlines) {
28
28
  if (isInlineWidget(inline, node.raw)) {
29
- // A step-over widget is character-like: it carries a column, so it reads as text.
30
- if (getInlineWidgetEditing(inline.kind)?.onEdge === 'step-over')
29
+ // A character-like widget carries a column, so it reads as text.
30
+ if (isCharacterLikeWidget(inline.kind))
31
31
  return false;
32
32
  continue;
33
33
  }
@@ -25,6 +25,12 @@ export interface RenderInlineOptions {
25
25
  * as `buildImageWidget`; absent or null falls the widget back to its raw source.
26
26
  */
27
27
  buildPortalWidget?: (node: InlineNode, raw: string) => HTMLElement | null;
28
+ /**
29
+ * Paint a lone backslash ending the display as the hard break it is about to become: the
30
+ * byte as a (hidden) marker plus two `br` anchors so the caret has a second line to sit on.
31
+ * Only for a mode that hides markers; the DOM stays byte-identical elsewhere.
32
+ */
33
+ pendingBreakSeat?: boolean;
28
34
  /**
29
35
  * Stamp marker spans with the construct's raw range, so preview-inline's reveal trigger can
30
36
  * address them. Attributes only, leaving textContent and the offset walk untouched. Off by
@@ -288,8 +288,40 @@ export function renderInlineNodes(nodes, raw, opts = {}) {
288
288
  if (child !== null)
289
289
  stack.push(child);
290
290
  }
291
+ if (opts.pendingBreakSeat)
292
+ paintPendingBreak(nodes, raw, root.content);
291
293
  return root.content;
292
294
  }
295
+ /**
296
+ * `insertHardBreak` at the end of a block writes `\` whose line ending IS the block's trailing
297
+ * one, so until the next key supplies the following line CommonMark (and the scanner) read the
298
+ * byte as literal text. Left as text it shows as a stray backslash and the caret stays on the
299
+ * first line; painted like this the user sees the new line they asked for. A `br` adds no
300
+ * textContent, so G1.28 holds; the walk seats offset `length` after the first anchor.
301
+ */
302
+ function paintPendingBreak(nodes, raw, frag) {
303
+ const last = nodes[nodes.length - 1];
304
+ if (!last || last.kind !== 'text' || raw[last.end - 1] !== '\\')
305
+ return;
306
+ const rest = raw.slice(last.end);
307
+ if (rest !== '' && rest !== '\n' && rest !== '\r\n')
308
+ return;
309
+ const tail = frag.lastChild;
310
+ if (!tail || tail.nodeType !== Node.TEXT_NODE || !tail.textContent?.endsWith('\\'))
311
+ return;
312
+ if (tail.textContent.length === 1)
313
+ tail.remove();
314
+ else
315
+ tail.textContent = tail.textContent.slice(0, -1);
316
+ const marker = markerSpan('\\');
317
+ marker.classList.add('md-break-pending');
318
+ frag.appendChild(marker);
319
+ for (let i = 0; i < 2; i++) {
320
+ const anchor = document.createElement('br');
321
+ anchor.dataset.caretAnchor = 'break';
322
+ frag.appendChild(anchor);
323
+ }
324
+ }
293
325
  /**
294
326
  * The leaf containing `offset`, preferring the right node at a boundary; `offset === end` only
295
327
  * matches the last node. Model-layer, touching no DOM: the DOM counterpart is
@@ -259,6 +259,8 @@ export interface InlineNode {
259
259
  decoded?: string;
260
260
  width?: number;
261
261
  height?: number;
262
+ /** Pan/zoom inside the `|WxH` frame, read from a `@X,Y[,Z]` tail on the hint. */
263
+ crop?: ImageCrop;
262
264
  /** Discriminator for `unresolvedReference` nodes: which form they would have been. */
263
265
  refKind?: 'link' | 'image';
264
266
  /**
@@ -271,12 +273,24 @@ export interface InlineNode {
271
273
  * Optional keys are omitted rather than set to `undefined`, so a serializer can tell "no title"
272
274
  * from "empty title" and reproduce a node that never carried one.
273
275
  */
276
+ /**
277
+ * How an image sits inside its `|WxH` frame: the image point (`x`%, `y`%) pinned to the same
278
+ * point of the frame, at `z` times the smallest scale that fills it. `{50, 50, 1}` is a plain
279
+ * centred cover fit; absent, the frame stretches the image the way `<img width height>` does.
280
+ */
281
+ export interface ImageCrop {
282
+ x: number;
283
+ y: number;
284
+ z: number;
285
+ }
274
286
  export interface ImageFields {
275
287
  alt: string;
276
288
  url: string;
277
289
  title?: string;
278
290
  width?: number;
279
291
  height?: number;
292
+ /** Needs both dimensions: a crop is a window into a fixed frame. */
293
+ crop?: ImageCrop;
280
294
  /**
281
295
  * Reference-style images only (`![alt][label]`). When set, the serializer emits the reference
282
296
  * form and writes no url/title: those live in the LRD, and inlining them would orphan it.
@@ -39,7 +39,8 @@ export function classifyArrivalKey(key, metaKey = false) {
39
39
  if (metaKey && (key === 'ArrowLeft' || key === 'ArrowRight'))
40
40
  return 'outside';
41
41
  // A step stops on the side of the run it approached from, so one press never changes which
42
- // construct the caret is in: forward keys reach the near side, backward keys the far one.
42
+ // construct the caret is in (live-mode.md § 4.2); leaving a construct is a typed closer's job
43
+ // (delimiter-autopair.ts) or a toggle's, not the arrow's.
43
44
  if (key === 'ArrowRight' || key === 'ArrowDown' || key === 'PageDown')
44
45
  return 'near';
45
46
  if (key === 'ArrowLeft' || key === 'ArrowUp' || key === 'PageUp')
@@ -16,9 +16,8 @@ export interface HeightOracle {
16
16
  estimate(node: NodeView, width: number): number;
17
17
  measured(id: string): number | undefined;
18
18
  recordMeasured(id: string, height: number): void;
19
- /** measured(id) ?? estimate(node, width). */
20
- height(id: string, node: NodeView, width: number): number;
21
- /** Drop every measured height; estimates carry the model until each block re-measures. */
19
+ /** Drop every measured height. A live scope carries its own across a rebuild (VR-15), so
20
+ * estimates seed only a scope built after the drop, and any block with a fresh id. */
22
21
  dropMeasured(): void;
23
22
  }
24
23
  export declare function createHeightOracle(opts: HeightOracleOptions): HeightOracle;
@@ -88,7 +88,6 @@ export function createHeightOracle(opts) {
88
88
  recordMeasured: (id, height) => {
89
89
  measuredById.set(id, height);
90
90
  },
91
- height: (id, node, width) => measuredById.get(id) ?? estimate(node, width),
92
91
  dropMeasured: () => measuredById.clear()
93
92
  };
94
93
  }
@@ -27,5 +27,13 @@ export function wireOverlayRemeasure(opts) {
27
27
  editorRoot.addEventListener('scroll', measure, { passive: true });
28
28
  disposers.push(() => editorRoot.removeEventListener('scroll', measure));
29
29
  }
30
+ // The block's own box changing under a live range — a paragraph set to a heading, a font
31
+ // load, a resize — moves the text the rects were measured against. Layout-driven, so it
32
+ // fires after the change has painted; absent (jsdom) the scroll paths above still hold.
33
+ if (typeof ResizeObserver === 'function') {
34
+ const observer = new ResizeObserver(() => measure());
35
+ observer.observe(el);
36
+ disposers.push(() => observer.disconnect());
37
+ }
30
38
  return () => disposers.forEach((dispose) => dispose());
31
39
  }
@@ -8,13 +8,18 @@
8
8
  import { tick } from 'svelte';
9
9
  import { asRawOffset, toDomTextOffset } from './coordinate-spaces';
10
10
  import { restoreCaretAtWalkOffset } from './focused-caret';
11
+ import { clampToLandableRaw } from './widget-offset';
11
12
  import { assertInvariant } from '../assert';
12
13
  import { checkRevealSourceLength } from '../invariants/inline-transitions';
13
14
  export function createSourceReveal(deps) {
14
15
  /** Places the caret at a BLOCK-source offset, converting to ambient-included walk space. */
15
16
  function placeCaret(container, blockSourceOffset) {
16
- const walkOffset = toDomTextOffset(asRawOffset(blockSourceOffset), deps.getAmbientLength());
17
- restoreCaretAtWalkOffset(container, walkOffset);
17
+ // Offset 0 of a source that opens with hidden chrome (a leaf painting `$$` fence lines) is
18
+ // BEFORE that chrome: typing there would land outside the fence. The seat is the nearest
19
+ // landable position, which is a no-op wherever nothing hides.
20
+ const ambient = deps.getAmbientLength();
21
+ const seat = clampToLandableRaw(container, blockSourceOffset, ambient);
22
+ restoreCaretAtWalkOffset(container, toDomTextOffset(asRawOffset(seat), ambient));
18
23
  }
19
24
  async function reveal(atSourceOffset = 0) {
20
25
  assertInvariant('reveal-transition', () => checkRevealSourceLength(deps.source.length, deps.sourceStart, deps.sourceEnd));
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Scroll-position hold across a block's render swap. A swap that shortens a block clamps the
3
+ * scrollport against the transient layout on the way to its settled height, and the clamp is
4
+ * never given back. A reveal in flight outranks the hold and owns the position; host mode does
5
+ * not, since native anchoring cannot undo a max-scroll clamp. Only a measure-then-fit surface
6
+ * needs this, and today that is one: an editable leaf's source is content-sized at mount.
7
+ */
8
+ /** Capture the position of whatever scrolls `el`; await the result AFTER the state flip that
9
+ * changes the block's height. */
10
+ export declare function captureScrollPosition(el: HTMLElement | null | undefined, isRevealInFlight: () => boolean): () => Promise<void>;