@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
@@ -64,10 +64,25 @@
64
64
  return getWidgetEl()?.querySelector('img') ?? null;
65
65
  }
66
66
 
67
+ /**
68
+ * What the drag measures and previews on. A cropped image is a FRAME (the widget) with the
69
+ * picture panned inside it, so the `<img>` is neither the size being changed nor the box the
70
+ * user is dragging: previewing on it bulges the picture out of its frame, and at a zoom above
71
+ * 1 the drag would start from the painted image's width and jump.
72
+ */
73
+ function isCropped(): boolean {
74
+ return getWidgetEl()?.classList.contains('md-image-cropped') ?? false;
75
+ }
76
+
77
+ function previewEl(): HTMLElement | null {
78
+ return isCropped() ? getWidgetEl() : imgEl();
79
+ }
80
+
67
81
  function startDrag(e: PointerEvent) {
68
82
  const img = imgEl();
69
- if (!img) return;
70
- const { width: startWidth, height: startHeight } = img.getBoundingClientRect();
83
+ const preview = previewEl();
84
+ if (!img || !preview) return;
85
+ const { width: startWidth, height: startHeight } = preview.getBoundingClientRect();
71
86
  // Unmeasurable width makes every snap run against 0 and commit a tiny image
72
87
  // whichever way the drag goes; bail before pointer capture.
73
88
  if (startWidth < MIN_WIDTH || editorContentWidth < MIN_WIDTH) return;
@@ -79,7 +94,10 @@
79
94
  startWidth,
80
95
  startHeight,
81
96
  naturalWidth: img.naturalWidth,
82
- aspectLocked: !e.shiftKey,
97
+ // A crop's frame keeps its shape through a resize (`commitImageResize` derives the
98
+ // height from it), so Shift has nothing to unlock here; the crop session's corner
99
+ // brackets are where a frame's aspect changes.
100
+ aspectLocked: isCropped() || !e.shiftKey,
83
101
  currentWidth: startWidth
84
102
  };
85
103
  }
@@ -95,12 +113,13 @@
95
113
  const clamped = clampWidth(proposed, editorContentWidth);
96
114
  const snapped = snapWidth(clamped, editorContentWidth, SNAP_THRESHOLD_PX);
97
115
  dragState.currentWidth = snapped;
98
- const img = imgEl();
99
- if (!img) return;
100
- img.style.width = `${snapped}px`;
116
+ const preview = previewEl();
117
+ if (!preview) return;
118
+ preview.style.width = `${snapped}px`;
101
119
  // Shift unlocks the aspect: the height stays where the user found it and the image
102
- // distorts. Locked, the stylesheet's `height: auto` derives it from the new width.
103
- img.style.height = dragState.aspectLocked ? '' : `${dragState.startHeight}px`;
120
+ // distorts. Locked, the frame's own `aspect-ratio` (cropped) or the stylesheet's
121
+ // `height: auto` (plain) derives it from the new width.
122
+ preview.style.height = dragState.aspectLocked ? '' : `${dragState.startHeight}px`;
104
123
  }
105
124
 
106
125
  function endDrag(e: PointerEvent) {
@@ -126,26 +145,36 @@
126
145
  dx: e.clientX - startX,
127
146
  editorContentWidth,
128
147
  naturalWidth,
129
- imgRectWidth: imgEl()?.getBoundingClientRect().width,
148
+ imgRectWidth: previewEl()?.getBoundingClientRect().width,
130
149
  imgWidthAttr: imgEl()?.getAttribute('width')
131
150
  });
132
151
  }
133
152
  onCommit(Math.round(finalWidth), resolveDraggedHeight(aspectLocked, startHeight));
134
153
  }
135
154
 
155
+ /** The frame's committed width, from the `|WxH` hint the widget was built with. */
156
+ function restoreFrameWidth(widget: HTMLElement): void {
157
+ const width = imgEl()?.getAttribute('width');
158
+ if (width) widget.style.width = `${width}px`;
159
+ }
160
+
136
161
  function cancelDrag(e: PointerEvent) {
137
162
  if (!dragState) return;
138
- // Fall back to the widget's committed width/height attributes.
139
- const img = imgEl();
140
- if (img) {
141
- img.style.width = '';
142
- img.style.height = '';
163
+ // Fall back to the committed geometry: the widget's width/height attributes, or for a
164
+ // cropped image the frame width `applyCropToWidget` wrote.
165
+ const preview = previewEl();
166
+ if (preview) {
167
+ preview.style.height = '';
168
+ if (preview === imgEl()) preview.style.width = '';
169
+ else restoreFrameWidth(preview);
143
170
  }
144
171
  dragState = null;
145
172
  (e.target as HTMLElement).releasePointerCapture(e.pointerId);
146
173
  }
147
174
  </script>
148
175
 
176
+ <!-- One grip, on the right edge: width is the only thing a drag sets (Shift unlocks the
177
+ aspect), so a second corner grip was the same gesture twice. -->
149
178
  {#if !isBroken}
150
179
  <div
151
180
  class="md-resize-handle md-resize-handle-right"
@@ -155,34 +184,30 @@
155
184
  onpointerup={endDrag}
156
185
  onpointercancel={cancelDrag}
157
186
  ></div>
158
- <div
159
- class="md-resize-handle md-resize-handle-corner"
160
- role="presentation"
161
- onpointerdown={startDrag}
162
- onpointermove={moveDrag}
163
- onpointerup={endDrag}
164
- onpointercancel={cancelDrag}
165
- ></div>
166
187
  {/if}
167
188
 
168
189
  <style>
190
+ /* An accent pill straddling the edge with a white rim, so it reads on any picture. The hit
191
+ strip is wider than the paint. */
169
192
  .md-resize-handle {
170
193
  position: absolute;
171
- width: 8px;
172
- height: 8px;
194
+ right: -3px;
195
+ top: 50%;
196
+ width: 6px;
197
+ height: 44px;
198
+ max-height: 60%;
199
+ transform: translateY(-50%);
200
+ border-radius: 3px;
173
201
  background: var(--color-accent, #567b67);
174
- border: 1px solid #fff;
202
+ box-shadow:
203
+ 0 0 0 1.5px rgba(255, 255, 255, 0.9),
204
+ 0 1px 3px rgba(0, 0, 0, 0.3);
175
205
  z-index: 10;
176
206
  cursor: ew-resize;
177
207
  }
178
- .md-resize-handle-right {
179
- right: -4px;
180
- top: 50%;
181
- transform: translateY(-50%);
182
- }
183
- .md-resize-handle-corner {
184
- right: -4px;
185
- bottom: -4px;
186
- cursor: nwse-resize;
208
+ .md-resize-handle::before {
209
+ content: '';
210
+ position: absolute;
211
+ inset: -6px -8px;
187
212
  }
188
213
  </style>
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Pure math for the image crop: how a `{x, y, z}` window maps to the styles that paint it, and
3
+ * how a pan or a wheel turn moves it. No DOM here beyond a style bag; the properties surface
4
+ * owns the pointer events and `widget-dom.ts` paints a committed crop from the same styles.
5
+ *
6
+ * The model is CSS `background-size: cover` at `z` times the fill scale, with the image point
7
+ * (`x`%, `y`%) pinned to that point of the frame, so `{50, 50, 1}` is a centred cover fit.
8
+ */
9
+ import type { ImageCrop } from '../../core/nodes';
10
+ export declare const DEFAULT_CROP: ImageCrop;
11
+ export interface Size {
12
+ width: number;
13
+ height: number;
14
+ }
15
+ export declare function clampCrop(crop: ImageCrop): ImageCrop;
16
+ /**
17
+ * The styles an `<img>` inside an `overflow: hidden` frame takes. The image box is `z` times
18
+ * the frame with `object-fit: cover`, offset so the pinned point stays put: at `z` 1 the offset
19
+ * is zero and `object-position` alone pans the cover overflow.
20
+ */
21
+ export declare function cropImageStyle(crop: ImageCrop): {
22
+ width: string;
23
+ height: string;
24
+ left: string;
25
+ top: string;
26
+ objectPosition: string;
27
+ };
28
+ /** The painted image size at this crop: cover-scaled to the frame, then times `z`. */
29
+ export declare function paintedSize(frame: Size, natural: Size, z: number): Size;
30
+ /**
31
+ * A drag of (`dx`, `dy`) px moves the window the other way, by the share of the overflow the
32
+ * pointer covered; an axis with nothing to pan on ignores its component.
33
+ */
34
+ export declare function panCrop(crop: ImageCrop, dx: number, dy: number, frame: Size, natural: Size): ImageCrop;
35
+ /** A wheel turn zooms about the pinned point; positive `deltaY` (scrolling down) zooms out. */
36
+ export declare function zoomCrop(crop: ImageCrop, deltaY: number): ImageCrop;
37
+ export declare function isDefaultCrop(crop: ImageCrop): boolean;
38
+ /** Paint a crop onto a widget and its image, the way a committed one renders. */
39
+ export declare function applyCropToWidget(widget: HTMLElement, img: HTMLImageElement, frame: Size, crop: ImageCrop): void;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Pure math for the image crop: how a `{x, y, z}` window maps to the styles that paint it, and
3
+ * how a pan or a wheel turn moves it. No DOM here beyond a style bag; the properties surface
4
+ * owns the pointer events and `widget-dom.ts` paints a committed crop from the same styles.
5
+ *
6
+ * The model is CSS `background-size: cover` at `z` times the fill scale, with the image point
7
+ * (`x`%, `y`%) pinned to that point of the frame, so `{50, 50, 1}` is a centred cover fit.
8
+ */
9
+ import { MAX_CROP_ZOOM } from '../../core/inline/image-dimensions';
10
+ export const DEFAULT_CROP = { x: 50, y: 50, z: 1 };
11
+ export function clampCrop(crop) {
12
+ return {
13
+ x: Math.min(100, Math.max(0, crop.x)),
14
+ y: Math.min(100, Math.max(0, crop.y)),
15
+ z: Math.min(MAX_CROP_ZOOM, Math.max(1, crop.z))
16
+ };
17
+ }
18
+ /**
19
+ * The styles an `<img>` inside an `overflow: hidden` frame takes. The image box is `z` times
20
+ * the frame with `object-fit: cover`, offset so the pinned point stays put: at `z` 1 the offset
21
+ * is zero and `object-position` alone pans the cover overflow.
22
+ */
23
+ export function cropImageStyle(crop) {
24
+ const c = clampCrop(crop);
25
+ const pct = (n) => `${Math.round(n * 1000) / 1000}%`;
26
+ return {
27
+ width: pct(c.z * 100),
28
+ height: pct(c.z * 100),
29
+ left: pct(-(c.z - 1) * c.x),
30
+ top: pct(-(c.z - 1) * c.y),
31
+ objectPosition: `${pct(c.x)} ${pct(c.y)}`
32
+ };
33
+ }
34
+ /** The painted image size at this crop: cover-scaled to the frame, then times `z`. */
35
+ export function paintedSize(frame, natural, z) {
36
+ if (natural.width <= 0 || natural.height <= 0)
37
+ return { width: frame.width * z, height: frame.height * z };
38
+ const scale = Math.max(frame.width / natural.width, frame.height / natural.height) * z;
39
+ return { width: natural.width * scale, height: natural.height * scale };
40
+ }
41
+ /**
42
+ * A drag of (`dx`, `dy`) px moves the window the other way, by the share of the overflow the
43
+ * pointer covered; an axis with nothing to pan on ignores its component.
44
+ */
45
+ export function panCrop(crop, dx, dy, frame, natural) {
46
+ const painted = paintedSize(frame, natural, crop.z);
47
+ const overX = painted.width - frame.width;
48
+ const overY = painted.height - frame.height;
49
+ return clampCrop({
50
+ x: overX > 0.5 ? crop.x - (dx / overX) * 100 : crop.x,
51
+ y: overY > 0.5 ? crop.y - (dy / overY) * 100 : crop.y,
52
+ z: crop.z
53
+ });
54
+ }
55
+ /** A wheel turn zooms about the pinned point; positive `deltaY` (scrolling down) zooms out. */
56
+ export function zoomCrop(crop, deltaY) {
57
+ return clampCrop({ ...crop, z: crop.z - deltaY * 0.002 });
58
+ }
59
+ export function isDefaultCrop(crop) {
60
+ return crop.x === 50 && crop.y === 50 && crop.z === 1;
61
+ }
62
+ /** Paint a crop onto a widget and its image, the way a committed one renders. */
63
+ export function applyCropToWidget(widget, img, frame, crop) {
64
+ widget.classList.add('md-image-cropped');
65
+ widget.style.width = `${Math.round(frame.width)}px`;
66
+ widget.style.aspectRatio = `${Math.round(frame.width)} / ${Math.round(frame.height)}`;
67
+ const style = cropImageStyle(crop);
68
+ img.style.width = style.width;
69
+ img.style.height = style.height;
70
+ img.style.left = style.left;
71
+ img.style.top = style.top;
72
+ img.style.objectPosition = style.objectPosition;
73
+ img.style.aspectRatio = '';
74
+ }
@@ -30,6 +30,7 @@ export interface ImageEditCommitter {
30
30
  * popover's dirty check compares against these. */
31
31
  buildEditBytes(target: WidgetTarget, newFields: ImageFields): string | null;
32
32
  commitImageResize(newWidth: number, newHeight: number | undefined): void;
33
+ removeImage(target: WidgetTarget): void;
33
34
  dismissImagePopover(): void;
34
35
  getEditorContentWidth(): number;
35
36
  attachWidgetSelectListener(): () => void;
@@ -2,7 +2,7 @@ import { resolvedInlineContent } from '../../core/inline/inline-cache';
2
2
  import { flattenInlineWidgets } from '../../core/inline/inline-widgets';
3
3
  import { createInlineRangeCommit } from '../../editor-actions/inline-range-commit';
4
4
  import { FALLBACK_CONTENT_WIDTH } from '../../cursor/typography-estimates';
5
- import { blockNodeAt } from '../../tree-operations/node-ops';
5
+ import { blockNodeAt } from '../../tree-operations/node-primitives';
6
6
  import { buildImageEditBytes } from './image-source-bytes';
7
7
  export function createImageEditCommitter(deps) {
8
8
  const { getDoc, getEditorEl, widgetSelection, controller, events } = deps;
@@ -71,15 +71,30 @@ export function createImageEditCommitter(deps) {
71
71
  const ctx = getSelectedImageFields();
72
72
  if (!ctx)
73
73
  return;
74
+ const { image } = ctx;
75
+ // A cropped frame keeps its shape through a resize: the height follows the width.
76
+ const framedHeight = image.crop && image.width !== undefined && image.height !== undefined
77
+ ? Math.round((image.height * newWidth) / image.width)
78
+ : newHeight;
74
79
  const newFields = {
75
- alt: ctx.image.alt ?? '',
76
- url: ctx.image.url ?? '',
77
- ...(ctx.image.title !== undefined ? { title: ctx.image.title } : {}),
80
+ alt: image.alt ?? '',
81
+ url: image.url ?? '',
82
+ ...(image.title !== undefined ? { title: image.title } : {}),
78
83
  width: newWidth,
79
- ...(newHeight !== undefined ? { height: newHeight } : {})
84
+ ...(framedHeight !== undefined ? { height: framedHeight } : {}),
85
+ ...(image.crop !== undefined && framedHeight !== undefined ? { crop: image.crop } : {})
80
86
  };
81
87
  commitImageEdit(sel, newFields);
82
88
  }
89
+ /** The whole `![...](...)` span goes; the caret lands where the image began. */
90
+ function removeImage(target) {
91
+ const paragraph = blockNodeAt(getDoc(), target.paragraphPath);
92
+ const image = paragraph && findImageInParagraph(paragraph, target.sourceStart);
93
+ if (!image)
94
+ return;
95
+ widgetSelection.clear();
96
+ void inlineRange.commitInlineRange(target.paragraphPath, image.start, image.end, '', 0);
97
+ }
83
98
  function dismissImagePopover() {
84
99
  widgetSelection.clear();
85
100
  }
@@ -151,6 +166,7 @@ export function createImageEditCommitter(deps) {
151
166
  commitImageEdit,
152
167
  buildEditBytes,
153
168
  commitImageResize,
169
+ removeImage,
154
170
  dismissImagePopover,
155
171
  getEditorContentWidth,
156
172
  attachWidgetSelectListener,
@@ -31,6 +31,7 @@ export function imageFieldsFromInline(image) {
31
31
  ...(image.title !== undefined ? { title: image.title } : {}),
32
32
  ...(image.width !== undefined ? { width: image.width } : {}),
33
33
  ...(image.height !== undefined ? { height: image.height } : {}),
34
+ ...(image.crop !== undefined ? { crop: image.crop } : {}),
34
35
  ...(image.label !== undefined ? { label: image.label } : {})
35
36
  };
36
37
  }
@@ -38,7 +39,7 @@ export function imageFieldsFromInline(image) {
38
39
  /** The built-in grammar's inverse. Reach it through `buildImageEditBytes`, the only
39
40
  * caller entitled to decide these bytes are GFM's to write. */
40
41
  export function buildImageSourceBytes(fields) {
41
- const dimSuffix = buildDimSuffix(fields.width, fields.height);
42
+ const dimSuffix = buildDimSuffix(fields.width, fields.height, fields.crop);
42
43
  const altSegment = escapeAlt(fields.alt) + dimSuffix;
43
44
  // Reference form: the dimension hint rides in the alt, and url/title are not
44
45
  // written — they belong to the LRD.
@@ -48,12 +49,18 @@ export function buildImageSourceBytes(fields) {
48
49
  const titleSegment = fields.title !== undefined ? ` "${escapeTitle(fields.title)}"` : '';
49
50
  return `![${altSegment}](${encodeDestination(fields.url)}${titleSegment})`;
50
51
  }
51
- function buildDimSuffix(width, height) {
52
+ function buildDimSuffix(width, height, crop) {
52
53
  if (width === undefined)
53
54
  return '';
54
55
  if (height === undefined)
55
56
  return `|${width}`;
56
- return `|${width}x${height}`;
57
+ return `|${width}x${height}${crop ? buildCropTail(crop) : ''}`;
58
+ }
59
+ /** Whole percents; the zoom only when it is one worth writing, trimmed to what it needs. */
60
+ function buildCropTail(crop) {
61
+ const z = Math.round(crop.z * 100) / 100;
62
+ const zoom = z === 1 ? '' : `,${String(z)}`;
63
+ return `@${Math.round(crop.x)},${Math.round(crop.y)}${zoom}`;
57
64
  }
58
65
  // Alt sits inside `[...]`, where an unescaped bracket closes the scan early. Unlike
59
66
  // `title` and `url` it carries RAW label bytes, so a blanket escape doubles every
@@ -28,6 +28,7 @@ export function imageWidgetOnSelectedKey(e, ctx) {
28
28
  ...(inline.height !== undefined
29
29
  ? { height: Math.round((newWidth / currentWidth) * inline.height) }
30
30
  : {}),
31
+ ...(inline.crop !== undefined ? { crop: inline.crop } : {}),
31
32
  ...(inline.label !== undefined ? { label: inline.label } : {})
32
33
  };
33
34
  const newBytes = buildImageEditBytes(inline, node.raw, newFields);
@@ -4,6 +4,7 @@
4
4
  import { isAllowedImageSrcScheme } from '../../core/url-policy';
5
5
  import { findSurfacePathForElement } from '../../selection/path-lookup';
6
6
  import { devWarn } from '../../dev-warn';
7
+ import { applyCropToWidget } from './image-crop';
7
8
  export function buildImageWidget(node, _raw, opts) {
8
9
  const widget = document.createElement('span');
9
10
  widget.className = 'md-image-widget';
@@ -63,9 +64,12 @@ export function buildImageWidget(node, _raw, opts) {
63
64
  img.setAttribute('height', String(node.height));
64
65
  // A declared `|WxH` box belongs to the author, so it both reserves space before the bytes
65
66
  // arrive and survives the decode: the attribute pair alone loses to the natural ratio the
66
- // moment `height: auto` has one to read.
67
+ // moment `height: auto` has one to read. With a crop the box is a frame the image pans in.
67
68
  if (node.width !== undefined && node.height !== undefined) {
68
69
  img.style.aspectRatio = `${node.width} / ${node.height}`;
70
+ if (node.crop) {
71
+ applyCropToWidget(widget, img, { width: node.width, height: node.height }, node.crop);
72
+ }
69
73
  }
70
74
  const markBroken = () => {
71
75
  opts.brokenUrlCache.add(resolvedUrl);
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { wireOverlayRemeasure } from '../../cursor/overlay-remeasure';
7
7
  import { createInlineRangeCommit } from '../../editor-actions/inline-range-commit';
8
- import { isBlockNode, nodeAt } from '../../tree-operations/node-ops';
8
+ import { isBlockNode, nodeAt } from '../../tree-operations/node-primitives';
9
9
  import { linkConstructAt } from '../blocks/text/link-at-point';
10
10
  import { buildLinkEditBytes, buildLinkUnwrapBytes, buildLinkWrapBytes, linkFieldsFromInline } from '../blocks/text/link-source-bytes';
11
11
  export function createLinkCardCommitter(deps) {
@@ -1,4 +1,4 @@
1
- import { nodeAt } from '../tree-operations/node-ops';
1
+ import { nodeAt } from '../tree-operations/node-primitives';
2
2
  /**
3
3
  * Whether a commit could change the LRD set, keeping the O(nodes) map rebuild off the keystroke
4
4
  * hot path. Any op that is not a kind-stable `input`/`metadataUpdate` could add or remove a