@inkbrick/react 0.0.0-stage → 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/dist/FormulaPalette-WFRPRHS5.js +320 -0
  2. package/dist/FormulaPalette-WFRPRHS5.js.map +1 -0
  3. package/dist/chunk-TPUX4C45.js +2309 -0
  4. package/dist/chunk-TPUX4C45.js.map +1 -0
  5. package/dist/index.d.ts +1431 -0
  6. package/dist/index.js +4880 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/mathCss-FLPE6QBM.js +361 -0
  9. package/dist/mathCss-FLPE6QBM.js.map +1 -0
  10. package/package.json +46 -3
  11. package/src/block-handle/BlockHandleController.ts +93 -0
  12. package/src/block-handle/BlockHandleHost.tsx +108 -0
  13. package/src/block-handle/DefaultBlockHandle.tsx +71 -0
  14. package/src/block-handle/DefaultBlockMenu.tsx +114 -0
  15. package/src/block-handle/DropIndicatorHost.tsx +58 -0
  16. package/src/block-handle/actions.tsx +146 -0
  17. package/src/block-handle/blockDrag.ts +215 -0
  18. package/src/block-handle/geometry.ts +64 -0
  19. package/src/block-handle/gripGesture.ts +108 -0
  20. package/src/block-handle/types.ts +65 -0
  21. package/src/block-handle/useBlockDrag.ts +21 -0
  22. package/src/block-handle/useBlockHandles.ts +125 -0
  23. package/src/blocks/BlockChildren.tsx +17 -0
  24. package/src/blocks/BlockProps.ts +5 -0
  25. package/src/blocks/CalloutBlock.tsx +61 -0
  26. package/src/blocks/CodeBlock.tsx +96 -0
  27. package/src/blocks/DividerBlock.tsx +15 -0
  28. package/src/blocks/Editable.tsx +84 -0
  29. package/src/blocks/FormulaBlock.tsx +173 -0
  30. package/src/blocks/HeadingBlock.tsx +22 -0
  31. package/src/blocks/ImageBlock.tsx +362 -0
  32. package/src/blocks/ListBlock.tsx +86 -0
  33. package/src/blocks/NoteBlock.tsx +10 -0
  34. package/src/blocks/PageBlock.tsx +10 -0
  35. package/src/blocks/ParagraphBlock.tsx +20 -0
  36. package/src/blocks/QuoteBlock.tsx +18 -0
  37. package/src/blocks/UnknownBlockView.tsx +27 -0
  38. package/src/blocks/defineBlock.ts +49 -0
  39. package/src/clipboard/html/blocks.ts +379 -0
  40. package/src/clipboard/html/inline.ts +92 -0
  41. package/src/clipboard/html/style.ts +92 -0
  42. package/src/clipboard/html/tags.ts +77 -0
  43. package/src/clipboard/htmlToFragment.ts +41 -0
  44. package/src/clipboard/useClipboard.ts +296 -0
  45. package/src/code/highlight.ts +56 -0
  46. package/src/code/languages.ts +236 -0
  47. package/src/code/scan/css.ts +164 -0
  48. package/src/code/scan/generic.ts +328 -0
  49. package/src/code/scan/html.ts +129 -0
  50. package/src/code/scan/markdown.ts +58 -0
  51. package/src/code/scan/shared.ts +81 -0
  52. package/src/code/scan/yaml.ts +34 -0
  53. package/src/code/types.ts +34 -0
  54. package/src/context/EditorContext.ts +9 -0
  55. package/src/context/EditorProvider.tsx +172 -0
  56. package/src/controllers/adapters.tsx +63 -0
  57. package/src/editing/BlockSelectionController.ts +27 -0
  58. package/src/editing/CaretController.ts +27 -0
  59. package/src/editing/EditorView.ts +108 -0
  60. package/src/editing/ViewportController.ts +63 -0
  61. package/src/editing/beforeInput.ts +276 -0
  62. package/src/editing/deleteAtBoundary.ts +16 -0
  63. package/src/editing/inputActions.ts +109 -0
  64. package/src/editing/renderCode.ts +69 -0
  65. package/src/editing/renderInline.ts +144 -0
  66. package/src/editing/selection.ts +262 -0
  67. package/src/editing/useEditorInput.ts +223 -0
  68. package/src/editing/useSelectionState.ts +46 -0
  69. package/src/formula/FormulaController.ts +51 -0
  70. package/src/formula/FormulaPalette.tsx +79 -0
  71. package/src/formula/FormulaPopover.tsx +181 -0
  72. package/src/formula/LazyFormulaPalette.tsx +42 -0
  73. package/src/formula/actions.ts +138 -0
  74. package/src/formula/snippet.ts +70 -0
  75. package/src/formula/symbols.ts +264 -0
  76. package/src/hooks/useBlock.ts +21 -0
  77. package/src/hooks/useBlockController.ts +30 -0
  78. package/src/hooks/useChildren.ts +16 -0
  79. package/src/hooks/useDocument.ts +16 -0
  80. package/src/hooks/useEditor.ts +13 -0
  81. package/src/hooks/useEditorView.ts +13 -0
  82. package/src/hooks/useHistory.ts +46 -0
  83. package/src/hooks/useReadOnly.ts +21 -0
  84. package/src/hooks/useT.ts +10 -0
  85. package/src/i18n/en.ts +182 -0
  86. package/src/i18n/ru.ts +174 -0
  87. package/src/i18n/translate.ts +77 -0
  88. package/src/icons/ICONS_LICENSE.md +50 -0
  89. package/src/icons/Icon.tsx +41 -0
  90. package/src/icons/lucide.ts +234 -0
  91. package/src/images/ImageError.ts +39 -0
  92. package/src/images/UploadTracker.ts +49 -0
  93. package/src/images/insertImages.ts +142 -0
  94. package/src/images/types.ts +22 -0
  95. package/src/images/upload.ts +62 -0
  96. package/src/index.ts +102 -0
  97. package/src/keymap/defaultKeymap.ts +189 -0
  98. package/src/keymap/keymap.ts +42 -0
  99. package/src/keymap/shortcut.ts +105 -0
  100. package/src/keymap/types.ts +40 -0
  101. package/src/math/FormulaView.tsx +62 -0
  102. package/src/math/mathCss.ts +4 -0
  103. package/src/math/renderer.ts +198 -0
  104. package/src/registry/defaultBlockComponents.ts +26 -0
  105. package/src/registry/resolveBlockComponent.ts +14 -0
  106. package/src/renderer/BlockRenderer.tsx +63 -0
  107. package/src/renderer/DocumentRenderer.tsx +33 -0
  108. package/src/renderer/DocumentView.tsx +94 -0
  109. package/src/renderer/virtual/VirtualChildList.tsx +355 -0
  110. package/src/renderer/virtual/estimate.ts +62 -0
  111. package/src/renderer/virtual/virtualRenderer.tsx +35 -0
  112. package/src/slash/DefaultSlashMenu.tsx +119 -0
  113. package/src/slash/SlashMenuController.ts +66 -0
  114. package/src/slash/SlashMenuHost.tsx +48 -0
  115. package/src/slash/applySlashItem.ts +57 -0
  116. package/src/slash/defaultSlashItems.tsx +186 -0
  117. package/src/slash/filter.ts +29 -0
  118. package/src/slash/items.ts +30 -0
  119. package/src/slash/syncSlashMenu.ts +67 -0
  120. package/src/slash/types.ts +21 -0
  121. package/src/toolbar/DefaultToolbar.tsx +281 -0
  122. package/src/toolbar/ToolbarController.ts +100 -0
  123. package/src/toolbar/ToolbarHost.tsx +81 -0
  124. package/src/toolbar/formatting.ts +75 -0
  125. package/src/toolbar/rich.ts +58 -0
  126. package/src/toolbar/syncToolbar.ts +39 -0
  127. package/src/toolbar/types.ts +21 -0
  128. package/src/ui/EditorUIContext.ts +26 -0
  129. package/src/ui/presentation.ts +24 -0
  130. package/src/ui/types.ts +70 -0
  131. package/README.md +0 -3
package/dist/index.js ADDED
@@ -0,0 +1,4880 @@
1
+ import {
2
+ EditorUIContext,
3
+ HIGHLIGHT_LIMIT,
4
+ ICON_SHAPES,
5
+ Icon,
6
+ applyInlineFormula,
7
+ applySlashItem,
8
+ availableSlashItems,
9
+ createTranslator,
10
+ defaultKeymap,
11
+ defaultSlashItems,
12
+ deleteForwardAtEnd,
13
+ en,
14
+ ensureMathStyles,
15
+ filterSlashItems,
16
+ getLocales,
17
+ highlightCode,
18
+ matchInlineFormula,
19
+ mountFormula,
20
+ normalizeLanguage,
21
+ openFormulaAt,
22
+ openInlineFormula,
23
+ readDocSelection,
24
+ registerLocale,
25
+ resolveMessages,
26
+ resolveSlashItems,
27
+ ru,
28
+ setAlignInSelection,
29
+ setColorInSelection,
30
+ setDomSelection,
31
+ setLinkInSelection,
32
+ syncToolbar,
33
+ temmlRenderer,
34
+ toggleFormatInSelection,
35
+ useEditorUI,
36
+ useLocale,
37
+ useSnippetInsert,
38
+ useT,
39
+ visibleSlashItems
40
+ } from "./chunk-TPUX4C45.js";
41
+
42
+ // src/context/EditorContext.ts
43
+ import { createContext } from "react";
44
+ var EditorContext = createContext(null);
45
+ var EditorViewContext = createContext(null);
46
+
47
+ // src/context/EditorProvider.tsx
48
+ import { useLayoutEffect, useMemo } from "react";
49
+
50
+ // src/block-handle/BlockHandleController.ts
51
+ var IDLE = Object.freeze({ hover: null, menu: null, drag: null });
52
+ var sameAnchor = (a, b) => a === b || !!a && !!b && a.blockId === b.blockId && a.x === b.x && a.y === b.y;
53
+ var BlockHandleController = class {
54
+ state = IDLE;
55
+ listeners = /* @__PURE__ */ new Set();
56
+ getState = () => this.state;
57
+ subscribe = (listener) => {
58
+ this.listeners.add(listener);
59
+ return () => {
60
+ this.listeners.delete(listener);
61
+ };
62
+ };
63
+ setHover(hover) {
64
+ if (!sameAnchor(this.state.hover, hover)) this.set({ ...this.state, hover });
65
+ }
66
+ openMenu(menu) {
67
+ this.set({ ...this.state, menu });
68
+ }
69
+ closeMenu() {
70
+ if (this.state.menu) this.set({ ...this.state, menu: null });
71
+ }
72
+ startDrag(blockId) {
73
+ this.set({
74
+ ...this.state,
75
+ menu: null,
76
+ drag: { blockId, targetId: null, position: null, indicator: null }
77
+ });
78
+ }
79
+ updateDrag(patch) {
80
+ const drag = this.state.drag;
81
+ if (!drag) return;
82
+ const next = { ...drag, ...patch };
83
+ const same = next.targetId === drag.targetId && next.position === drag.position && next.indicator?.left === drag.indicator?.left && next.indicator?.top === drag.indicator?.top && next.indicator?.width === drag.indicator?.width;
84
+ if (!same) this.set({ ...this.state, drag: next });
85
+ }
86
+ endDrag() {
87
+ if (this.state.drag) this.set({ ...this.state, drag: null });
88
+ }
89
+ set(state) {
90
+ this.state = state;
91
+ for (const listener of [...this.listeners]) listener();
92
+ }
93
+ };
94
+
95
+ // src/images/UploadTracker.ts
96
+ var UploadTracker = class {
97
+ active = /* @__PURE__ */ new Set();
98
+ errors = /* @__PURE__ */ new Map();
99
+ listeners = /* @__PURE__ */ new Set();
100
+ has = (blockId) => this.active.has(blockId);
101
+ errorOf = (blockId) => this.errors.get(blockId)?.message;
102
+ errorDetailsOf = (blockId) => this.errors.get(blockId);
103
+ subscribe = (listener) => {
104
+ this.listeners.add(listener);
105
+ return () => {
106
+ this.listeners.delete(listener);
107
+ };
108
+ };
109
+ start(blockId) {
110
+ this.errors.delete(blockId);
111
+ this.active.add(blockId);
112
+ this.notify();
113
+ }
114
+ finish(blockId, error) {
115
+ const changed = this.active.delete(blockId);
116
+ if (error) this.errors.set(blockId, typeof error === "string" ? { message: error } : error);
117
+ if (changed || error) this.notify();
118
+ }
119
+ clearError(blockId) {
120
+ if (this.errors.delete(blockId)) this.notify();
121
+ }
122
+ notify() {
123
+ for (const listener of [...this.listeners]) listener();
124
+ }
125
+ };
126
+
127
+ // src/editing/BlockSelectionController.ts
128
+ var BlockSelectionController = class {
129
+ selected = /* @__PURE__ */ new Set();
130
+ listeners = /* @__PURE__ */ new Set();
131
+ isSelected = (blockId) => this.selected.has(blockId);
132
+ subscribe = (listener) => {
133
+ this.listeners.add(listener);
134
+ return () => {
135
+ this.listeners.delete(listener);
136
+ };
137
+ };
138
+ set(ids) {
139
+ if (ids.length === this.selected.size && ids.every((id) => this.selected.has(id))) return;
140
+ this.selected = new Set(ids);
141
+ for (const listener of [...this.listeners]) listener();
142
+ }
143
+ };
144
+
145
+ // src/formula/FormulaController.ts
146
+ var CLOSED = Object.freeze({
147
+ open: false,
148
+ mode: "insert",
149
+ blockId: "",
150
+ from: 0,
151
+ to: 0,
152
+ latex: "",
153
+ position: { x: 0, y: 0, top: 0 }
154
+ });
155
+ var FormulaController = class {
156
+ state = CLOSED;
157
+ listeners = /* @__PURE__ */ new Set();
158
+ getState = () => this.state;
159
+ subscribe = (listener) => {
160
+ this.listeners.add(listener);
161
+ return () => {
162
+ this.listeners.delete(listener);
163
+ };
164
+ };
165
+ open(state) {
166
+ this.set({ ...state, open: true });
167
+ }
168
+ close() {
169
+ if (this.state.open) this.set(CLOSED);
170
+ }
171
+ set(state) {
172
+ this.state = state;
173
+ for (const listener of [...this.listeners]) listener();
174
+ }
175
+ };
176
+
177
+ // src/editing/CaretController.ts
178
+ var CaretController = class {
179
+ blockId = null;
180
+ listeners = /* @__PURE__ */ new Set();
181
+ has = (blockId) => this.blockId === blockId;
182
+ subscribe = (listener) => {
183
+ this.listeners.add(listener);
184
+ return () => {
185
+ this.listeners.delete(listener);
186
+ };
187
+ };
188
+ set(blockId) {
189
+ if (this.blockId === blockId) return;
190
+ this.blockId = blockId;
191
+ for (const listener of [...this.listeners]) listener();
192
+ }
193
+ };
194
+
195
+ // src/slash/SlashMenuController.ts
196
+ var CLOSED2 = Object.freeze({
197
+ open: false,
198
+ blockId: null,
199
+ slashOffset: 0,
200
+ query: "",
201
+ activeIndex: 0,
202
+ position: null
203
+ });
204
+ var SlashMenuController = class {
205
+ state = CLOSED2;
206
+ listeners = /* @__PURE__ */ new Set();
207
+ getState = () => this.state;
208
+ subscribe = (listener) => {
209
+ this.listeners.add(listener);
210
+ return () => {
211
+ this.listeners.delete(listener);
212
+ };
213
+ };
214
+ open(blockId, slashOffset) {
215
+ this.set({ ...CLOSED2, open: true, blockId, slashOffset });
216
+ }
217
+ close() {
218
+ if (this.state.open) this.set(CLOSED2);
219
+ }
220
+ update(patch) {
221
+ if (!this.state.open) return;
222
+ const next = { ...this.state, ...patch };
223
+ const same = next.query === this.state.query && next.activeIndex === this.state.activeIndex && next.position?.x === this.state.position?.x && next.position?.y === this.state.position?.y;
224
+ if (!same) this.set(next);
225
+ }
226
+ move(delta, count) {
227
+ if (!this.state.open || count === 0) return;
228
+ this.update({ activeIndex: (this.state.activeIndex + delta + count) % count });
229
+ }
230
+ set(state) {
231
+ this.state = state;
232
+ for (const listener of [...this.listeners]) listener();
233
+ }
234
+ };
235
+
236
+ // src/toolbar/ToolbarController.ts
237
+ var HIDDEN = Object.freeze({
238
+ visible: false,
239
+ position: null,
240
+ formats: null,
241
+ range: null,
242
+ linkOpen: false,
243
+ panel: null
244
+ });
245
+ function sameFormats(a, b) {
246
+ return a === b || !!a && !!b && a.bold === b.bold && a.italic === b.italic && a.underline === b.underline && a.strike === b.strike && a.code === b.code && a.link === b.link && a.color === b.color && a.backgroundColor === b.backgroundColor && a.align === b.align;
247
+ }
248
+ var ToolbarController = class {
249
+ state = HIDDEN;
250
+ listeners = /* @__PURE__ */ new Set();
251
+ getState = () => this.state;
252
+ subscribe = (listener) => {
253
+ this.listeners.add(listener);
254
+ return () => {
255
+ this.listeners.delete(listener);
256
+ };
257
+ };
258
+ show(range, formats, position) {
259
+ const prev = this.state;
260
+ const same = prev.visible && sameFormats(prev.formats, formats) && prev.position?.x === position.x && prev.position?.y === position.y && prev.range?.from.blockId === range.from.blockId && prev.range?.from.offset === range.from.offset && prev.range?.to.blockId === range.to.blockId && prev.range?.to.offset === range.to.offset;
261
+ if (!same) this.set({ ...prev, visible: true, position, formats, range });
262
+ }
263
+ hide() {
264
+ if (this.state.visible) this.set(HIDDEN);
265
+ }
266
+ openLink() {
267
+ if (this.state.visible && !this.state.linkOpen) {
268
+ this.set({ ...this.state, linkOpen: true, panel: null });
269
+ }
270
+ }
271
+ togglePanel(panel) {
272
+ if (!this.state.visible || this.state.linkOpen) return;
273
+ this.set({ ...this.state, panel: this.state.panel === panel ? null : panel });
274
+ }
275
+ closePanel() {
276
+ if (this.state.panel) this.set({ ...this.state, panel: null });
277
+ }
278
+ closeLink() {
279
+ if (this.state.linkOpen) this.set({ ...this.state, linkOpen: false });
280
+ }
281
+ set(state) {
282
+ this.state = state;
283
+ for (const listener of [...this.listeners]) listener();
284
+ }
285
+ };
286
+
287
+ // src/editing/ViewportController.ts
288
+ var ViewportController = class {
289
+ edges = [];
290
+ revealed = [];
291
+ lists = 0;
292
+ version = 0;
293
+ listeners = /* @__PURE__ */ new Set();
294
+ getVersion = () => this.version;
295
+ subscribe = (listener) => {
296
+ this.listeners.add(listener);
297
+ return () => {
298
+ this.listeners.delete(listener);
299
+ };
300
+ };
301
+ getPinned() {
302
+ return this.revealed.length === 0 ? this.edges : [...this.edges, ...this.revealed];
303
+ }
304
+ get virtual() {
305
+ return this.lists > 0;
306
+ }
307
+ attach() {
308
+ this.lists += 1;
309
+ return () => {
310
+ this.lists -= 1;
311
+ };
312
+ }
313
+ setEdges(ids) {
314
+ if (ids.length === this.edges.length && ids.every((id, i) => this.edges[i] === id)) return;
315
+ this.edges = ids;
316
+ this.notify();
317
+ }
318
+ reveal(ids) {
319
+ if (!this.virtual) return false;
320
+ this.revealed = ids;
321
+ this.notify();
322
+ return true;
323
+ }
324
+ notify() {
325
+ this.version += 1;
326
+ for (const listener of [...this.listeners]) listener();
327
+ }
328
+ };
329
+
330
+ // src/editing/EditorView.ts
331
+ var EditorView = class {
332
+ elements = /* @__PURE__ */ new Map();
333
+ root = null;
334
+ slash = new SlashMenuController();
335
+ uploads = new UploadTracker();
336
+ selection = new BlockSelectionController();
337
+ caret = new CaretController();
338
+ blocks = new BlockHandleController();
339
+ formula = new FormulaController();
340
+ toolbar = new ToolbarController();
341
+ viewport = new ViewportController();
342
+ pointerDown = false;
343
+ pending = null;
344
+ setRoot(element) {
345
+ this.root = element;
346
+ return () => {
347
+ if (this.root === element) this.root = null;
348
+ };
349
+ }
350
+ getRoot() {
351
+ return this.root;
352
+ }
353
+ register(id, element) {
354
+ this.elements.set(id, element);
355
+ const pending = this.pending;
356
+ if (pending && this.elements.has(pending.from.blockId) && this.elements.has(pending.to.blockId)) {
357
+ queueMicrotask(() => {
358
+ if (this.pending === pending) this.placeSelection(pending.from, pending.to);
359
+ });
360
+ }
361
+ return () => {
362
+ if (this.elements.get(id) === element) {
363
+ this.elements.delete(id);
364
+ }
365
+ };
366
+ }
367
+ getElement(id) {
368
+ return this.elements.get(id);
369
+ }
370
+ placeCaret(id, offset) {
371
+ this.placeSelection({ blockId: id, offset }, { blockId: id, offset });
372
+ }
373
+ placeSelection(from, to) {
374
+ const start = this.elements.get(from.blockId);
375
+ const end = this.elements.get(to.blockId);
376
+ if (!start || !end) {
377
+ this.pending = this.viewport.reveal([from.blockId, to.blockId]) ? { from, to } : null;
378
+ return;
379
+ }
380
+ this.pending = null;
381
+ if (this.root && globalThis.document.activeElement !== this.root) {
382
+ this.root.focus({ preventScroll: true });
383
+ }
384
+ setDomSelection(start, from.offset, end, to.offset);
385
+ end.scrollIntoView?.({ block: "nearest" });
386
+ }
387
+ };
388
+
389
+ // src/keymap/shortcut.ts
390
+ var MODIFIER = /^(mod|ctrl|control|meta|cmd|alt|option|shift)-/i;
391
+ var KEY_ALIASES = {
392
+ esc: "escape",
393
+ space: " ",
394
+ up: "arrowup",
395
+ down: "arrowdown",
396
+ left: "arrowleft",
397
+ right: "arrowright",
398
+ del: "delete",
399
+ return: "enter"
400
+ };
401
+ var cache = /* @__PURE__ */ new Map();
402
+ function parse(spec) {
403
+ const cached = cache.get(spec);
404
+ if (cached) return cached;
405
+ const shortcut = {
406
+ mod: false,
407
+ ctrl: false,
408
+ meta: false,
409
+ alt: false,
410
+ shift: false,
411
+ key: ""
412
+ };
413
+ let rest = spec.trim();
414
+ for (let match = MODIFIER.exec(rest); match; match = MODIFIER.exec(rest)) {
415
+ switch (match[1].toLowerCase()) {
416
+ case "mod":
417
+ shortcut.mod = true;
418
+ break;
419
+ case "ctrl":
420
+ case "control":
421
+ shortcut.ctrl = true;
422
+ break;
423
+ case "meta":
424
+ case "cmd":
425
+ shortcut.meta = true;
426
+ break;
427
+ case "alt":
428
+ case "option":
429
+ shortcut.alt = true;
430
+ break;
431
+ default:
432
+ shortcut.shift = true;
433
+ }
434
+ rest = rest.slice(match[0].length);
435
+ }
436
+ const key = rest.toLowerCase();
437
+ if (key === "") throw new Error(`Invalid shortcut "${spec}": no key`);
438
+ shortcut.key = KEY_ALIASES[key] ?? key;
439
+ cache.set(spec, shortcut);
440
+ return shortcut;
441
+ }
442
+ function matchesKey(event, key) {
443
+ const pressed = event.key.toLowerCase();
444
+ if (pressed === key) return true;
445
+ return /^[a-z]$/.test(key) && !/^[a-z]$/.test(pressed) && event.code === `Key${key.toUpperCase()}`;
446
+ }
447
+ function matchesShortcut(event, spec) {
448
+ const shortcut = parse(spec);
449
+ if (event.shiftKey !== shortcut.shift || event.altKey !== shortcut.alt) return false;
450
+ if (shortcut.mod) {
451
+ if (!(event.ctrlKey || event.metaKey)) return false;
452
+ } else if (event.ctrlKey !== shortcut.ctrl || event.metaKey !== shortcut.meta) {
453
+ return false;
454
+ }
455
+ return matchesKey(event, shortcut.key);
456
+ }
457
+
458
+ // src/keymap/keymap.ts
459
+ function resolveKeymap(custom = []) {
460
+ if (custom.length === 0) return defaultKeymap;
461
+ const overrides = new Map(custom.map((binding) => [binding.id, binding]));
462
+ const defaultIds = new Set(defaultKeymap.map((binding) => binding.id));
463
+ return [
464
+ ...custom.filter((binding) => !defaultIds.has(binding.id)),
465
+ ...defaultKeymap.map((binding) => overrides.get(binding.id) ?? binding)
466
+ ];
467
+ }
468
+ function unbind(id) {
469
+ return { id, keys: [], run: () => false };
470
+ }
471
+ function handleKeyDown(bindings, context) {
472
+ const { event } = context;
473
+ for (const binding of bindings) {
474
+ const keys = typeof binding.keys === "string" ? [binding.keys] : binding.keys;
475
+ if (!keys.some((spec) => matchesShortcut(event, spec))) continue;
476
+ if (binding.when && !binding.when(context)) continue;
477
+ if (binding.run(context) === false) continue;
478
+ event.preventDefault();
479
+ return true;
480
+ }
481
+ return false;
482
+ }
483
+
484
+ // src/blocks/defineBlock.ts
485
+ function defineBlock(definition) {
486
+ return definition;
487
+ }
488
+ function blockComponentsOf(blocks) {
489
+ const registry = {};
490
+ for (const block2 of blocks ?? []) {
491
+ registry[block2.schema.flavour] = block2.component;
492
+ }
493
+ return registry;
494
+ }
495
+
496
+ // src/controllers/adapters.tsx
497
+ import { jsx } from "react/jsx-runtime";
498
+ function iconByName(name) {
499
+ return name && Object.hasOwn(ICON_SHAPES, name) ? /* @__PURE__ */ jsx(Icon, { name }) : void 0;
500
+ }
501
+ function controllerSlashItems(editor) {
502
+ return editor.controllers.entries().flatMap(
503
+ ([flavour, controller]) => (controller.slashItems ?? []).map((item) => ({
504
+ id: item.id,
505
+ title: item.title,
506
+ description: item.description,
507
+ keywords: item.keywords,
508
+ icon: iconByName(item.icon),
509
+ flavour,
510
+ props: item.props
511
+ }))
512
+ );
513
+ }
514
+ function controlledBlockAt(context, flavour) {
515
+ const selection = context.selection();
516
+ const block2 = selection && context.editor.store.getBlock(selection.from.blockId);
517
+ return block2?.flavour === flavour ? block2 : void 0;
518
+ }
519
+ function controllerKeymap(editor) {
520
+ return editor.controllers.entries().flatMap(
521
+ ([flavour, controller]) => (controller.keymap ?? []).map((binding) => ({
522
+ id: binding.id,
523
+ keys: binding.keys,
524
+ when: (context) => controlledBlockAt(context, flavour) !== void 0,
525
+ run: (context) => {
526
+ const block2 = controlledBlockAt(context, flavour);
527
+ return block2 ? binding.run(context.editor.controllers.contextOf(block2)) : false;
528
+ }
529
+ }))
530
+ );
531
+ }
532
+ function mergeById(base, preferred, order = "base-first") {
533
+ const ids = new Set(preferred.map((item) => item.id));
534
+ const rest = base.filter((item) => !ids.has(item.id));
535
+ return order === "base-first" ? [...rest, ...preferred] : [...preferred, ...rest];
536
+ }
537
+
538
+ // src/context/EditorProvider.tsx
539
+ import { jsx as jsx2 } from "react/jsx-runtime";
540
+ function EditorProvider({
541
+ editor,
542
+ children,
543
+ presentation,
544
+ blocks,
545
+ components,
546
+ slots,
547
+ slashItems,
548
+ keymap,
549
+ highlighter,
550
+ formulaRenderer,
551
+ locale,
552
+ locales,
553
+ messages,
554
+ readOnly,
555
+ images
556
+ }) {
557
+ const view = useMemo(() => new EditorView(), [editor]);
558
+ useLayoutEffect(() => {
559
+ if (readOnly === void 0) return;
560
+ const previous = editor.readOnly;
561
+ editor.setReadOnly(readOnly);
562
+ return () => editor.setReadOnly(previous);
563
+ }, [editor, readOnly]);
564
+ const t = useMemo(
565
+ () => createTranslator(resolveMessages({ locale, locales, messages })),
566
+ [locale, locales, messages]
567
+ );
568
+ const ui = useMemo(() => {
569
+ const visual = presentation ?? {};
570
+ const code = highlighter ?? visual.highlighter;
571
+ const math = formulaRenderer ?? visual.formulaRenderer;
572
+ const customSlash = slashItems ?? visual.slashItems;
573
+ const customKeys = keymap ?? [];
574
+ return {
575
+ components: { ...blockComponentsOf(blocks), ...visual.components, ...components },
576
+ slots: { ...visual.slots, ...slots },
577
+ // Пункты контроллеров идут после стандартных; пункт приложения с тем же id важнее.
578
+ slashItems: customSlash === false ? [] : resolveSlashItems(mergeById(controllerSlashItems(editor), customSlash ?? []), t),
579
+ highlighter: code === false ? null : code ?? highlightCode,
580
+ formulaRenderer: math === false ? null : math ?? temmlRenderer,
581
+ locale: locale ?? "en",
582
+ t,
583
+ keymap: resolveKeymap(mergeById(controllerKeymap(editor), customKeys, "preferred-first")),
584
+ images: images ?? {},
585
+ readOnly
586
+ };
587
+ }, [
588
+ editor,
589
+ presentation,
590
+ blocks,
591
+ components,
592
+ slots,
593
+ slashItems,
594
+ keymap,
595
+ highlighter,
596
+ formulaRenderer,
597
+ locale,
598
+ t,
599
+ readOnly,
600
+ images
601
+ ]);
602
+ return /* @__PURE__ */ jsx2(EditorContext.Provider, { value: editor, children: /* @__PURE__ */ jsx2(EditorViewContext.Provider, { value: view, children: /* @__PURE__ */ jsx2(EditorUIContext.Provider, { value: ui, children }) }) });
603
+ }
604
+
605
+ // src/ui/presentation.ts
606
+ function definePresentation(presentation) {
607
+ return presentation;
608
+ }
609
+
610
+ // src/hooks/useEditor.ts
611
+ import { useContext } from "react";
612
+ function useEditor() {
613
+ const editor = useContext(EditorContext);
614
+ if (!editor) {
615
+ throw new Error("useEditor must be used inside <EditorProvider>");
616
+ }
617
+ return editor;
618
+ }
619
+
620
+ // src/hooks/useEditorView.ts
621
+ import { useContext as useContext2 } from "react";
622
+ function useEditorView() {
623
+ const view = useContext2(EditorViewContext);
624
+ if (!view) {
625
+ throw new Error("useEditorView must be used inside <EditorProvider>");
626
+ }
627
+ return view;
628
+ }
629
+
630
+ // src/hooks/useDocument.ts
631
+ import { useSyncExternalStore } from "react";
632
+ function useDocument() {
633
+ const editor = useEditor();
634
+ const { store } = editor;
635
+ useSyncExternalStore(
636
+ (listener) => store.subscribe(listener),
637
+ () => store.getVersion(),
638
+ () => store.getVersion()
639
+ );
640
+ return editor.document;
641
+ }
642
+
643
+ // src/hooks/useBlock.ts
644
+ import { useCallback, useSyncExternalStore as useSyncExternalStore2 } from "react";
645
+ function useBlock(id) {
646
+ const { store } = useEditor();
647
+ const subscribe = useCallback(
648
+ (listener) => store.subscribeBlock(id, listener),
649
+ [store, id]
650
+ );
651
+ const getSnapshot = useCallback(() => store.getBlock(id), [store, id]);
652
+ return useSyncExternalStore2(subscribe, getSnapshot, getSnapshot);
653
+ }
654
+
655
+ // src/hooks/useChildren.ts
656
+ var NO_CHILDREN = [];
657
+ function useChildren(parentId) {
658
+ const parent = useBlock(parentId);
659
+ return parent?.children ?? NO_CHILDREN;
660
+ }
661
+
662
+ // src/hooks/useHistory.ts
663
+ import { useCallback as useCallback2, useSyncExternalStore as useSyncExternalStore3 } from "react";
664
+ import { flushSync } from "react-dom";
665
+ function useHistory() {
666
+ const editor = useEditor();
667
+ const view = useEditorView();
668
+ const { store } = editor;
669
+ useSyncExternalStore3(
670
+ (listener) => store.subscribe(listener),
671
+ () => store.getHistoryVersion(),
672
+ () => store.getHistoryVersion()
673
+ );
674
+ const apply = useCallback2(
675
+ (action) => {
676
+ let result;
677
+ flushSync(() => {
678
+ result = action();
679
+ });
680
+ if (result?.caret) view.placeCaret(result.caret.blockId, result.caret.offset);
681
+ return result !== void 0;
682
+ },
683
+ [view]
684
+ );
685
+ const undo = useCallback2(() => apply(() => editor.undo()), [apply, editor]);
686
+ const redo = useCallback2(() => apply(() => editor.redo()), [apply, editor]);
687
+ return { canUndo: editor.canUndo, canRedo: editor.canRedo, undo, redo };
688
+ }
689
+
690
+ // src/hooks/useReadOnly.ts
691
+ import { useSyncExternalStore as useSyncExternalStore4 } from "react";
692
+ function useReadOnly() {
693
+ const editor = useEditor();
694
+ const { readOnly } = useEditorUI();
695
+ const state = useSyncExternalStore4(
696
+ (listener) => editor.subscribeReadOnly(listener),
697
+ () => editor.readOnly,
698
+ () => editor.readOnly
699
+ );
700
+ return readOnly ?? state;
701
+ }
702
+
703
+ // src/hooks/useBlockController.ts
704
+ import { useMemo as useMemo2 } from "react";
705
+ function useBlockController(block2) {
706
+ const editor = useEditor();
707
+ return useMemo2(() => {
708
+ const context = editor.controllers.contextOf(block2);
709
+ return Object.assign(Object.create(context), {
710
+ controller: editor.controllers.get(block2.flavour)
711
+ });
712
+ }, [editor, block2]);
713
+ }
714
+
715
+ // src/clipboard/htmlToFragment.ts
716
+ import { sanitizeFragment } from "@inkbrick/engine";
717
+
718
+ // src/clipboard/html/blocks.ts
719
+ import { isSafeImageUrl, normalizeInlineNodes as normalizeInlineNodes2 } from "@inkbrick/document";
720
+
721
+ // src/clipboard/html/inline.ts
722
+ import { normalizeInlineNodes } from "@inkbrick/document";
723
+
724
+ // src/clipboard/html/style.ts
725
+ import { isSafeUrl, normalizeLinkUrl } from "@inkbrick/document";
726
+
727
+ // src/clipboard/html/tags.ts
728
+ var MAX_DEPTH = 60;
729
+ var SKIP_TAGS = /* @__PURE__ */ new Set([
730
+ "MATH",
731
+ "SCRIPT",
732
+ "STYLE",
733
+ "HEAD",
734
+ "TEMPLATE",
735
+ "NOSCRIPT",
736
+ "IFRAME",
737
+ "OBJECT",
738
+ "EMBED",
739
+ "SVG",
740
+ "CANVAS",
741
+ "IMG",
742
+ "VIDEO",
743
+ "AUDIO",
744
+ "TITLE",
745
+ "META",
746
+ "LINK",
747
+ "INPUT",
748
+ "BUTTON",
749
+ "SELECT",
750
+ "TEXTAREA",
751
+ "FORM"
752
+ ]);
753
+ var BLOCK_TAGS = /* @__PURE__ */ new Set([
754
+ "P",
755
+ "DIV",
756
+ "H1",
757
+ "H2",
758
+ "H3",
759
+ "H4",
760
+ "H5",
761
+ "H6",
762
+ "UL",
763
+ "OL",
764
+ "LI",
765
+ "BLOCKQUOTE",
766
+ "PRE",
767
+ "HR",
768
+ "TABLE",
769
+ "TR",
770
+ "SECTION",
771
+ "ARTICLE",
772
+ "HEADER",
773
+ "FOOTER",
774
+ "MAIN",
775
+ "ASIDE",
776
+ "NAV",
777
+ "FIGURE",
778
+ "FIGCAPTION",
779
+ "DL",
780
+ "DT",
781
+ "DD",
782
+ "ADDRESS",
783
+ "FIELDSET",
784
+ "BODY",
785
+ "HTML",
786
+ "TBODY",
787
+ "THEAD",
788
+ "TFOOT",
789
+ "CAPTION"
790
+ ]);
791
+ var tagOf = (element) => element.localName.toUpperCase();
792
+
793
+ // src/clipboard/html/style.ts
794
+ function weight(value) {
795
+ const v = value.trim().toLowerCase();
796
+ if (v === "") return void 0;
797
+ if (v === "bold" || v === "bolder") return true;
798
+ if (v === "normal" || v === "lighter") return false;
799
+ const n = Number(v);
800
+ return Number.isNaN(n) ? void 0 : n >= 600;
801
+ }
802
+ function styleOf(element, base) {
803
+ const style = { ...base };
804
+ switch (tagOf(element)) {
805
+ case "B":
806
+ case "STRONG":
807
+ style.bold = true;
808
+ break;
809
+ case "I":
810
+ case "EM":
811
+ case "CITE":
812
+ case "DFN":
813
+ style.italic = true;
814
+ break;
815
+ case "U":
816
+ case "INS":
817
+ style.underline = true;
818
+ break;
819
+ case "S":
820
+ case "DEL":
821
+ case "STRIKE":
822
+ style.strike = true;
823
+ break;
824
+ case "CODE":
825
+ case "KBD":
826
+ case "SAMP":
827
+ case "TT":
828
+ style.code = true;
829
+ break;
830
+ case "A": {
831
+ const href = element.getAttribute("href");
832
+ const link = href ? normalizeLinkUrl(href) : null;
833
+ if (link) style.link = link;
834
+ break;
835
+ }
836
+ }
837
+ const css = element.style;
838
+ if (css) {
839
+ const bold = weight(css.fontWeight ?? "");
840
+ if (bold !== void 0) style.bold = bold;
841
+ const italic = (css.fontStyle ?? "").toLowerCase();
842
+ if (italic === "italic" || italic === "oblique") style.italic = true;
843
+ else if (italic === "normal") style.italic = false;
844
+ const decoration = `${css.textDecorationLine ?? ""} ${css.textDecoration ?? ""}`.toLowerCase();
845
+ if (decoration.includes("underline")) style.underline = true;
846
+ if (decoration.includes("line-through")) style.strike = true;
847
+ }
848
+ return style;
849
+ }
850
+ function attributesOf(style) {
851
+ const attributes = {};
852
+ if (style.bold) attributes.bold = true;
853
+ if (style.italic) attributes.italic = true;
854
+ if (style.underline) attributes.underline = true;
855
+ if (style.strike) attributes.strike = true;
856
+ if (style.code) attributes.code = true;
857
+ if (style.link && isSafeUrl(style.link)) attributes.link = style.link;
858
+ return Object.keys(attributes).length > 0 ? attributes : void 0;
859
+ }
860
+ function isHidden(element) {
861
+ return element.hasAttribute("hidden") || element.style?.display === "none" || element.style?.visibility === "hidden";
862
+ }
863
+
864
+ // src/clipboard/html/inline.ts
865
+ function cleanInline(nodes, preserve = false) {
866
+ if (preserve) return normalizeInlineNodes(nodes.filter((n) => n.insert.length > 0));
867
+ const cleaned = [];
868
+ let previousSpace = true;
869
+ for (const node of nodes) {
870
+ let text = "";
871
+ for (const ch of node.insert.replace(/\u00a0/g, " ")) {
872
+ if (ch === "\n") {
873
+ text = text.replace(/ $/, "") + ch;
874
+ previousSpace = true;
875
+ } else if (/\s/.test(ch)) {
876
+ if (!previousSpace) text += " ";
877
+ previousSpace = true;
878
+ } else {
879
+ text += ch;
880
+ previousSpace = false;
881
+ }
882
+ }
883
+ if (text)
884
+ cleaned.push({ insert: text, ...node.attributes ? { attributes: node.attributes } : {} });
885
+ }
886
+ while (cleaned.length > 0) {
887
+ const last = cleaned[cleaned.length - 1];
888
+ const trimmed = last.insert.replace(/[ \n]+$/, "");
889
+ if (trimmed === "") cleaned.pop();
890
+ else {
891
+ last.insert = trimmed;
892
+ break;
893
+ }
894
+ }
895
+ while (cleaned.length > 0 && /^\n+/.test(cleaned[0].insert)) {
896
+ cleaned[0].insert = cleaned[0].insert.replace(/^\n+/, "");
897
+ if (cleaned[0].insert === "") cleaned.shift();
898
+ }
899
+ return normalizeInlineNodes(cleaned);
900
+ }
901
+ function inlineOf(node, style, out, depth, preserve = false) {
902
+ if (depth > MAX_DEPTH) return;
903
+ if (node.nodeType === Node.TEXT_NODE) {
904
+ const raw = node.textContent ?? "";
905
+ const text = preserve ? raw : raw.replace(/[\t\n\r\f ]+/g, " ");
906
+ const attributes = attributesOf(style);
907
+ if (text) out.push({ insert: text, ...attributes ? { attributes } : {} });
908
+ return;
909
+ }
910
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
911
+ const element = node;
912
+ if (SKIP_TAGS.has(tagOf(element)) || isHidden(element)) return;
913
+ if (/mso-list:\s*ignore/i.test(element.getAttribute("style") ?? "")) return;
914
+ if (tagOf(element) === "BR") {
915
+ out.push({ insert: "\n" });
916
+ return;
917
+ }
918
+ const next = styleOf(element, style);
919
+ const isBlock = BLOCK_TAGS.has(tagOf(element));
920
+ if (isBlock && out.length > 0) out.push({ insert: "\n" });
921
+ element.childNodes.forEach((child) => inlineOf(child, next, out, depth + 1, preserve));
922
+ if (isBlock) out.push({ insert: "\n" });
923
+ }
924
+
925
+ // src/clipboard/html/blocks.ts
926
+ function pastedImageSrc(src) {
927
+ const value = (src ?? "").trim();
928
+ return value !== "" && !value.startsWith("/") && isSafeImageUrl(value) ? value : null;
929
+ }
930
+ function imageBlock(img, caption = "") {
931
+ const src = pastedImageSrc(img.getAttribute("src"));
932
+ if (!src) return null;
933
+ return block("block:image", [], {
934
+ src,
935
+ alt: (img.getAttribute("alt") ?? "").slice(0, 1e3),
936
+ caption: caption.slice(0, 2e3)
937
+ });
938
+ }
939
+ var block = (flavour, content, props = {}) => ({
940
+ flavour,
941
+ props,
942
+ content,
943
+ children: []
944
+ });
945
+ function hasBlockDescendant(element) {
946
+ const stack = [];
947
+ for (let child = element.lastElementChild; child; child = child.previousElementSibling)
948
+ stack.push(child);
949
+ while (stack.length > 0) {
950
+ const current = stack.pop();
951
+ const tag = tagOf(current);
952
+ if (BLOCK_TAGS.has(tag) && !SKIP_TAGS.has(tag)) return true;
953
+ for (let child = current.lastElementChild; child; child = child.previousElementSibling)
954
+ stack.push(child);
955
+ }
956
+ return false;
957
+ }
958
+ function wordListLevel(element) {
959
+ const style = element.getAttribute("style") ?? "";
960
+ const match = /mso-list:[^;]*level(\d+)/i.exec(style);
961
+ if (match) return Number(match[1]);
962
+ return /^MsoListParagraph/i.test(element.className) ? 1 : null;
963
+ }
964
+ function wordListMarker(element) {
965
+ for (const span of element.querySelectorAll("span")) {
966
+ if (/mso-list:\s*ignore/i.test(span.getAttribute("style") ?? "")) return span.textContent ?? "";
967
+ }
968
+ return "";
969
+ }
970
+ function listItems(list, ordered, style, depth) {
971
+ const result = [];
972
+ for (const child of Array.from(list.children)) {
973
+ if (SKIP_TAGS.has(tagOf(child)) || isHidden(child)) continue;
974
+ if (tagOf(child) === "UL" || tagOf(child) === "OL") {
975
+ const nested = listItems(child, tagOf(child) === "OL", style, depth + 1);
976
+ const parent = result[result.length - 1];
977
+ if (parent) parent.children.push(...nested);
978
+ else result.push(...nested);
979
+ continue;
980
+ }
981
+ if (tagOf(child) !== "LI") continue;
982
+ const content = [];
983
+ const children = [];
984
+ const itemStyle = styleOf(child, style);
985
+ const checkbox = child.querySelector(
986
+ ':scope > input[type="checkbox"], :scope > p > input[type="checkbox"]'
987
+ );
988
+ const todo = !!checkbox || /task-list-item/.test(child.className);
989
+ child.childNodes.forEach((node) => {
990
+ if (node.nodeType === Node.ELEMENT_NODE) {
991
+ const element = node;
992
+ if (tagOf(element) === "UL" || tagOf(element) === "OL") {
993
+ children.push(...listItems(element, tagOf(element) === "OL", itemStyle, depth + 1));
994
+ return;
995
+ }
996
+ }
997
+ inlineOf(node, itemStyle, content, depth + 1);
998
+ });
999
+ const props = { type: todo ? "todo" : ordered ? "ordered" : "bullet" };
1000
+ if (todo) props.checked = !!checkbox?.checked || checkbox?.hasAttribute("checked") === true;
1001
+ const item = block("block:list", cleanInline(content), props);
1002
+ item.children = children;
1003
+ result.push(item);
1004
+ }
1005
+ return result;
1006
+ }
1007
+ function pushWordItem(out, stack, item, level) {
1008
+ while (stack.length > 0 && stack[stack.length - 1].level >= level) stack.pop();
1009
+ const parent = stack[stack.length - 1];
1010
+ if (parent) parent.block.children.push(item);
1011
+ else out.push(item);
1012
+ stack.push({ block: item, level });
1013
+ }
1014
+ function collect(container, style, out, depth) {
1015
+ if (depth > MAX_DEPTH) return;
1016
+ let run = [];
1017
+ let wordStack = [];
1018
+ const flush = () => {
1019
+ const content = cleanInline(run);
1020
+ if (content.length > 0) out.push(block("block:paragraph", content));
1021
+ run = [];
1022
+ };
1023
+ const endWordList = () => {
1024
+ wordStack = [];
1025
+ };
1026
+ container.childNodes.forEach((node) => {
1027
+ if (node.nodeType === Node.TEXT_NODE) {
1028
+ inlineOf(node, style, run, depth + 1);
1029
+ return;
1030
+ }
1031
+ if (node.nodeType !== Node.ELEMENT_NODE) return;
1032
+ const element = node;
1033
+ if (SKIP_TAGS.has(tagOf(element)) || isHidden(element)) return;
1034
+ const tag = tagOf(element);
1035
+ const next = styleOf(element, style);
1036
+ const level = tag === "P" ? wordListLevel(element) : null;
1037
+ if (level !== null) {
1038
+ flush();
1039
+ const marker = wordListMarker(element).trim();
1040
+ const content = [];
1041
+ element.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
1042
+ pushWordItem(
1043
+ out,
1044
+ wordStack,
1045
+ block("block:list", cleanInline(content), {
1046
+ type: /^(\d+|[a-z]|[ivxlc]+)[.)]/i.test(marker) ? "ordered" : "bullet"
1047
+ }),
1048
+ level
1049
+ );
1050
+ return;
1051
+ }
1052
+ endWordList();
1053
+ if (/^H[1-6]$/.test(tag)) {
1054
+ flush();
1055
+ const content = [];
1056
+ element.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
1057
+ const cleaned = cleanInline(content);
1058
+ if (cleaned.length > 0) out.push(block("block:heading", cleaned, { level: Number(tag[1]) }));
1059
+ return;
1060
+ }
1061
+ if (tag === "IMG") {
1062
+ flush();
1063
+ const image = imageBlock(element);
1064
+ if (image) out.push(image);
1065
+ return;
1066
+ }
1067
+ switch (tag) {
1068
+ case "FIGURE": {
1069
+ const img = element.querySelector("img");
1070
+ if (img) {
1071
+ flush();
1072
+ const caption = (element.querySelector("figcaption")?.textContent ?? "").replace(/\s+/g, " ").trim();
1073
+ const image = imageBlock(img, caption);
1074
+ if (image) out.push(image);
1075
+ return;
1076
+ }
1077
+ break;
1078
+ }
1079
+ case "ASIDE": {
1080
+ const tone = element.getAttribute("data-callout");
1081
+ if (tone !== null) {
1082
+ flush();
1083
+ const content = [];
1084
+ element.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
1085
+ const cleaned = cleanInline(content);
1086
+ if (cleaned.length > 0) {
1087
+ out.push(
1088
+ block(
1089
+ "block:callout",
1090
+ cleaned,
1091
+ /^(info|warning|success|danger)$/.test(tone) ? { tone } : {}
1092
+ )
1093
+ );
1094
+ }
1095
+ return;
1096
+ }
1097
+ break;
1098
+ }
1099
+ case "UL":
1100
+ case "OL":
1101
+ flush();
1102
+ out.push(...listItems(element, tag === "OL", next, depth + 1));
1103
+ return;
1104
+ case "LI":
1105
+ flush();
1106
+ out.push(
1107
+ ...listItems(element.parentElement ?? element, false, next, depth + 1).slice(0, 1)
1108
+ );
1109
+ return;
1110
+ case "PRE": {
1111
+ flush();
1112
+ const code = element.querySelector("code");
1113
+ const language = /language-([\w+#-]+)/.exec(code?.className ?? element.className)?.[1] ?? element.getAttribute("data-language") ?? "plain";
1114
+ const text = (element.textContent ?? "").replace(/\n$/, "");
1115
+ out.push(block("block:code", text ? [{ insert: text }] : [], { language }));
1116
+ return;
1117
+ }
1118
+ case "HR":
1119
+ flush();
1120
+ out.push(block("block:divider", []));
1121
+ return;
1122
+ case "BLOCKQUOTE": {
1123
+ flush();
1124
+ if (hasBlockDescendant(element)) {
1125
+ const inner = [];
1126
+ collect(element, next, inner, depth + 1);
1127
+ out.push(
1128
+ ...inner.map(
1129
+ (b) => b.flavour === "block:paragraph" ? { ...b, flavour: "block:quote" } : b
1130
+ )
1131
+ );
1132
+ } else {
1133
+ const content = [];
1134
+ element.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
1135
+ const cleaned = cleanInline(content);
1136
+ if (cleaned.length > 0) out.push(block("block:quote", cleaned));
1137
+ }
1138
+ return;
1139
+ }
1140
+ case "TABLE": {
1141
+ flush();
1142
+ element.querySelectorAll("tr").forEach((row) => {
1143
+ const cells = [];
1144
+ row.querySelectorAll(":scope > td, :scope > th").forEach((cell, index) => {
1145
+ const content = [];
1146
+ if (index > 0) cells.push({ insert: " " });
1147
+ cell.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
1148
+ cells.push(...cleanInline(content));
1149
+ });
1150
+ const cleaned = normalizeInlineNodes2(cells);
1151
+ if (cleaned.length > 0) out.push(block("block:paragraph", cleaned));
1152
+ });
1153
+ return;
1154
+ }
1155
+ }
1156
+ if (hasBlockDescendant(element)) {
1157
+ flush();
1158
+ collect(element, next, out, depth + 1);
1159
+ return;
1160
+ }
1161
+ if (BLOCK_TAGS.has(tag)) {
1162
+ flush();
1163
+ const content = [];
1164
+ element.childNodes.forEach((child) => inlineOf(child, next, content, depth + 1));
1165
+ const cleaned = cleanInline(content);
1166
+ if (cleaned.length > 0) out.push(block("block:paragraph", cleaned));
1167
+ element.querySelectorAll("img").forEach((img) => {
1168
+ const image = imageBlock(img);
1169
+ if (image) out.push(image);
1170
+ });
1171
+ return;
1172
+ }
1173
+ inlineOf(element, style, run, depth + 1);
1174
+ });
1175
+ flush();
1176
+ }
1177
+
1178
+ // src/clipboard/htmlToFragment.ts
1179
+ var MAX_HTML = 2e6;
1180
+ function embeddedFragment(html) {
1181
+ if (!html.includes("data-inkbrick-fragment")) return null;
1182
+ const doc = new DOMParser().parseFromString(html.slice(0, MAX_HTML), "text/html");
1183
+ const raw = doc.querySelector("[data-inkbrick-fragment]")?.getAttribute("data-inkbrick-fragment");
1184
+ if (!raw) return null;
1185
+ try {
1186
+ return sanitizeFragment(JSON.parse(decodeURIComponent(raw)));
1187
+ } catch {
1188
+ return null;
1189
+ }
1190
+ }
1191
+ function htmlToFragment(html) {
1192
+ if (!html || html.length > MAX_HTML) return null;
1193
+ const doc = new DOMParser().parseFromString(html, "text/html");
1194
+ const blocks = [];
1195
+ collect(doc.body, {}, blocks, 0);
1196
+ const sanitized = sanitizeFragment({ blocks: blocks.map((b) => ({ ...b, complete: true })) });
1197
+ return sanitized;
1198
+ }
1199
+
1200
+ // src/images/upload.ts
1201
+ import { isSafeImageUrl as isSafeImageUrl2 } from "@inkbrick/document";
1202
+
1203
+ // src/images/ImageError.ts
1204
+ var ImageError = class extends Error {
1205
+ constructor(code, message, params) {
1206
+ super(message);
1207
+ this.code = code;
1208
+ this.params = params;
1209
+ this.name = "ImageError";
1210
+ }
1211
+ };
1212
+ function toImageErrorInfo(error) {
1213
+ if (error instanceof ImageError) {
1214
+ return { message: error.message, code: error.code, params: error.params };
1215
+ }
1216
+ return { message: error instanceof Error ? error.message : "" };
1217
+ }
1218
+ function formatImageError(info, t) {
1219
+ if (info.code) return t(`image.error.${info.code}`, info.params);
1220
+ return info.message || t("image.error.generic");
1221
+ }
1222
+
1223
+ // src/images/types.ts
1224
+ var DEFAULT_IMAGE_TYPES = [
1225
+ "image/png",
1226
+ "image/jpeg",
1227
+ "image/gif",
1228
+ "image/webp",
1229
+ "image/avif"
1230
+ ];
1231
+ var DEFAULT_MAX_INLINE_BYTES = 15e5;
1232
+
1233
+ // src/images/upload.ts
1234
+ function isAcceptedImage(file, options) {
1235
+ return (options.acceptedTypes ?? DEFAULT_IMAGE_TYPES).includes(file.type.toLowerCase());
1236
+ }
1237
+ function readAsDataUrl(file) {
1238
+ return new Promise((resolve, reject) => {
1239
+ const reader = new FileReader();
1240
+ reader.onload = () => resolve(String(reader.result));
1241
+ reader.onerror = () => reject(new ImageError("read", "Could not read the file"));
1242
+ reader.readAsDataURL(file);
1243
+ });
1244
+ }
1245
+ async function uploadImage(file, options) {
1246
+ if (!isAcceptedImage(file, options)) {
1247
+ const type = file.type || "unknown";
1248
+ throw new ImageError("type", `Unsupported image type "${type}"`, { type });
1249
+ }
1250
+ let url;
1251
+ if (options.upload) {
1252
+ url = await options.upload(file);
1253
+ } else {
1254
+ const limit = options.maxInlineBytes ?? DEFAULT_MAX_INLINE_BYTES;
1255
+ if (file.size > limit) {
1256
+ const size = Math.round(limit / 1024);
1257
+ throw new ImageError(
1258
+ "size",
1259
+ `The image is larger than ${size} KB. Provide "images.upload" to store larger images.`,
1260
+ { size }
1261
+ );
1262
+ }
1263
+ url = await readAsDataUrl(file);
1264
+ }
1265
+ if (!isSafeImageUrl2(url))
1266
+ throw new ImageError("uploadAddress", "The upload returned an unsupported image address");
1267
+ return url;
1268
+ }
1269
+ function normalizeImageUrl(input) {
1270
+ const value = input.trim();
1271
+ if (value === "") return null;
1272
+ const candidate = /^[a-z][a-z0-9+.-]*:/i.test(value) || value.startsWith("/") ? value : `https://${value}`;
1273
+ return isSafeImageUrl2(candidate) ? candidate : null;
1274
+ }
1275
+
1276
+ // src/blocks/BlockChildren.tsx
1277
+ import { useContext as useContext3 } from "react";
1278
+
1279
+ // src/renderer/DocumentRenderer.tsx
1280
+ import { createContext as createContext2 } from "react";
1281
+
1282
+ // src/renderer/BlockRenderer.tsx
1283
+ import { memo, useSyncExternalStore as useSyncExternalStore7 } from "react";
1284
+
1285
+ // src/blocks/UnknownBlockView.tsx
1286
+ import { jsx as jsx3, jsxs } from "react/jsx-runtime";
1287
+ function UnknownBlockView({ block: block2, children }) {
1288
+ const t = useT();
1289
+ return /* @__PURE__ */ jsxs(
1290
+ "div",
1291
+ {
1292
+ className: "block block-unknown",
1293
+ "data-inkbrick-block": "unknown",
1294
+ role: "note",
1295
+ contentEditable: false,
1296
+ "data-block-id": block2.id,
1297
+ "data-unknown-flavour": block2.flavour,
1298
+ children: [
1299
+ /* @__PURE__ */ jsxs("div", { className: "block-unknown__message", "data-inkbrick-part": "message", children: [
1300
+ t("block.unknown.title"),
1301
+ " ",
1302
+ /* @__PURE__ */ jsx3("code", { children: block2.flavour }),
1303
+ ". ",
1304
+ t("block.unknown.hint"),
1305
+ " ",
1306
+ /* @__PURE__ */ jsx3("code", { children: "components" }),
1307
+ "."
1308
+ ] }),
1309
+ children
1310
+ ]
1311
+ }
1312
+ );
1313
+ }
1314
+
1315
+ // src/blocks/PageBlock.tsx
1316
+ import { jsx as jsx4 } from "react/jsx-runtime";
1317
+ function PageBlock({ block: block2 }) {
1318
+ return /* @__PURE__ */ jsx4("div", { className: "page", "data-inkbrick-block": "page", "data-block-id": block2.id, children: /* @__PURE__ */ jsx4(BlockChildren, { id: block2.id }) });
1319
+ }
1320
+
1321
+ // src/blocks/NoteBlock.tsx
1322
+ import { jsx as jsx5 } from "react/jsx-runtime";
1323
+ function NoteBlock({ block: block2 }) {
1324
+ return /* @__PURE__ */ jsx5("section", { className: "page-content", "data-inkbrick-block": "note", "data-block-id": block2.id, children: /* @__PURE__ */ jsx5(BlockChildren, { id: block2.id }) });
1325
+ }
1326
+
1327
+ // src/blocks/ParagraphBlock.tsx
1328
+ import { getBlockAlign } from "@inkbrick/document";
1329
+
1330
+ // src/blocks/Editable.tsx
1331
+ import { useLayoutEffect as useLayoutEffect2, useRef, useSyncExternalStore as useSyncExternalStore5 } from "react";
1332
+
1333
+ // src/editing/renderCode.ts
1334
+ var TOKEN_TYPE = /^[a-z][a-z0-9-]{0,31}$/;
1335
+ function normalizeTokens(tokens, length) {
1336
+ const valid = tokens.filter(
1337
+ (token) => Number.isInteger(token.from) && Number.isInteger(token.to) && token.from >= 0 && token.to <= length && token.from < token.to && TOKEN_TYPE.test(token.type)
1338
+ ).sort((a, b) => a.from - b.from || b.to - a.to);
1339
+ const result = [];
1340
+ let end = 0;
1341
+ for (const token of valid) {
1342
+ if (token.from >= end) {
1343
+ result.push(token);
1344
+ end = token.to;
1345
+ }
1346
+ }
1347
+ return result;
1348
+ }
1349
+ function renderCode(root, text, tokens) {
1350
+ const doc = globalThis.document;
1351
+ const fragment = doc.createDocumentFragment();
1352
+ let position = 0;
1353
+ for (const token of normalizeTokens(tokens, text.length)) {
1354
+ if (token.from > position)
1355
+ fragment.appendChild(doc.createTextNode(text.slice(position, token.from)));
1356
+ const span = doc.createElement("span");
1357
+ span.setAttribute("data-inkbrick-token", token.type);
1358
+ span.textContent = text.slice(token.from, token.to);
1359
+ fragment.appendChild(span);
1360
+ position = token.to;
1361
+ }
1362
+ if (position < text.length) fragment.appendChild(doc.createTextNode(text.slice(position)));
1363
+ if (text === "" || text.endsWith("\n")) fragment.appendChild(doc.createElement("br"));
1364
+ root.replaceChildren(fragment);
1365
+ }
1366
+
1367
+ // src/editing/renderInline.ts
1368
+ import {
1369
+ FORMULA_CHAR,
1370
+ isColorName,
1371
+ isSafeUrl as isSafeUrl2,
1372
+ safeCssColor
1373
+ } from "@inkbrick/document";
1374
+ function wrap(node, tag) {
1375
+ const element = globalThis.document.createElement(tag);
1376
+ element.appendChild(node);
1377
+ return element;
1378
+ }
1379
+ function renderFormulaAtom(latex, options) {
1380
+ const doc = globalThis.document;
1381
+ const host = doc.createElement("span");
1382
+ host.setAttribute("data-inkbrick-formula", "");
1383
+ host.setAttribute("data-inkbrick-part", "formula-inline");
1384
+ host.setAttribute("data-latex", latex);
1385
+ host.setAttribute("contenteditable", "false");
1386
+ host.textContent = FORMULA_CHAR;
1387
+ const view = doc.createElement("span");
1388
+ view.setAttribute("part", "math");
1389
+ try {
1390
+ const shadow = host.attachShadow({ mode: "open" });
1391
+ ensureMathStyles(shadow);
1392
+ shadow.appendChild(view);
1393
+ } catch {
1394
+ host.setAttribute("data-state", "fallback");
1395
+ return host;
1396
+ }
1397
+ mountFormula(view, latex, {
1398
+ display: false,
1399
+ renderer: options.formulaRenderer ?? null,
1400
+ onSettled: (state) => host.setAttribute("data-state", state)
1401
+ });
1402
+ host.setAttribute("data-state", view.getAttribute("data-state") ?? "loading");
1403
+ return host;
1404
+ }
1405
+ function renderNode(insert, attributes, options) {
1406
+ let node;
1407
+ if (typeof attributes?.formula === "string") {
1408
+ const fragment = globalThis.document.createDocumentFragment();
1409
+ for (let i = 0; i < insert.length; i++)
1410
+ fragment.appendChild(renderFormulaAtom(attributes.formula, options));
1411
+ node = fragment;
1412
+ } else {
1413
+ node = globalThis.document.createTextNode(insert);
1414
+ }
1415
+ if (!attributes) return node;
1416
+ if (attributes.code) node = wrap(node, "code");
1417
+ if (attributes.bold) node = wrap(node, "strong");
1418
+ if (attributes.italic) node = wrap(node, "em");
1419
+ if (attributes.underline) node = wrap(node, "u");
1420
+ if (attributes.strike) node = wrap(node, "s");
1421
+ if (typeof attributes.link === "string" && isSafeUrl2(attributes.link)) {
1422
+ const anchor = wrap(node, "a");
1423
+ anchor.href = attributes.link.trim();
1424
+ anchor.rel = "noopener noreferrer";
1425
+ anchor.target = "_blank";
1426
+ node = anchor;
1427
+ }
1428
+ const color = attributes.color;
1429
+ const background = attributes.backgroundColor;
1430
+ const safeColor = isColorName(color) ? null : safeCssColor(color);
1431
+ const safeBackground = isColorName(background) ? null : safeCssColor(background);
1432
+ if (isColorName(color) || isColorName(background) || safeColor || safeBackground) {
1433
+ const span = wrap(node, "span");
1434
+ if (isColorName(color)) span.setAttribute("data-inkbrick-color", color);
1435
+ else if (safeColor) span.style.color = safeColor;
1436
+ if (isColorName(background)) span.setAttribute("data-inkbrick-bg", background);
1437
+ else if (safeBackground) span.style.backgroundColor = safeBackground;
1438
+ node = span;
1439
+ }
1440
+ return node;
1441
+ }
1442
+ function renderInline(root, nodes, options = {}) {
1443
+ const fragment = globalThis.document.createDocumentFragment();
1444
+ for (const { insert, attributes } of nodes) {
1445
+ if (insert.length > 0) fragment.appendChild(renderNode(insert, attributes, options));
1446
+ }
1447
+ const text = nodes.map((node) => node.insert).join("");
1448
+ if (text === "" || text.endsWith("\n")) {
1449
+ fragment.appendChild(globalThis.document.createElement("br"));
1450
+ }
1451
+ root.replaceChildren(fragment);
1452
+ }
1453
+
1454
+ // src/blocks/Editable.tsx
1455
+ import { jsx as jsx6 } from "react/jsx-runtime";
1456
+ function Editable({ block: block2, placeholder }) {
1457
+ const ref = useRef(null);
1458
+ const view = useEditorView();
1459
+ const t = useT();
1460
+ const readOnly = useReadOnly();
1461
+ const { slashItems, highlighter, formulaRenderer } = useEditorUI();
1462
+ const focused = useSyncExternalStore5(
1463
+ view.caret.subscribe,
1464
+ () => view.caret.has(block2.id),
1465
+ () => false
1466
+ );
1467
+ useLayoutEffect2(() => view.register(block2.id, ref.current), [view, block2.id]);
1468
+ const isCode = block2.flavour === "block:code";
1469
+ const language = typeof block2.props.language === "string" ? block2.props.language : "plain";
1470
+ useLayoutEffect2(() => {
1471
+ const plain = block2.content.every((node) => !node.attributes);
1472
+ if (isCode && highlighter && plain) {
1473
+ const text = block2.content.map((node) => node.insert).join("");
1474
+ let tokens = [];
1475
+ try {
1476
+ tokens = highlighter(text, language);
1477
+ } catch (error) {
1478
+ console.error("[@inkbrick/react] The code highlighter failed:", error);
1479
+ }
1480
+ renderCode(ref.current, text, tokens);
1481
+ } else {
1482
+ renderInline(ref.current, block2.content, { formulaRenderer });
1483
+ }
1484
+ }, [block2.content, isCode, language, highlighter, formulaRenderer]);
1485
+ const isEmpty = block2.content.every((node) => node.insert.length === 0);
1486
+ const hinted = !readOnly && slashItems.length > 0 && block2.flavour !== "block:code";
1487
+ return /* @__PURE__ */ jsx6(
1488
+ "div",
1489
+ {
1490
+ ref,
1491
+ className: "editable",
1492
+ "data-inkbrick-part": "content",
1493
+ "data-id": block2.id,
1494
+ "data-empty": isEmpty,
1495
+ "data-focused": focused || void 0,
1496
+ "data-hint": hinted ? t("editor.hint") : void 0,
1497
+ "data-placeholder": placeholder
1498
+ }
1499
+ );
1500
+ }
1501
+
1502
+ // src/blocks/ParagraphBlock.tsx
1503
+ import { jsx as jsx7 } from "react/jsx-runtime";
1504
+ function ParagraphBlock({ block: block2, selected }) {
1505
+ const align = getBlockAlign(block2) ?? "left";
1506
+ return /* @__PURE__ */ jsx7(
1507
+ "div",
1508
+ {
1509
+ className: "block block-paragraph",
1510
+ "data-inkbrick-block": "paragraph",
1511
+ "data-block-id": block2.id,
1512
+ "data-selected": selected || void 0,
1513
+ "data-align": align,
1514
+ children: /* @__PURE__ */ jsx7(Editable, { block: block2 })
1515
+ }
1516
+ );
1517
+ }
1518
+
1519
+ // src/blocks/HeadingBlock.tsx
1520
+ import { getBlockAlign as getBlockAlign2 } from "@inkbrick/document";
1521
+ import { jsx as jsx8 } from "react/jsx-runtime";
1522
+ function HeadingBlock({ block: block2, selected }) {
1523
+ const level = typeof block2.props.level === "number" ? block2.props.level : 1;
1524
+ const align = getBlockAlign2(block2) ?? "left";
1525
+ return /* @__PURE__ */ jsx8(
1526
+ "div",
1527
+ {
1528
+ className: `block block-heading level-${level}`,
1529
+ "data-inkbrick-block": "heading",
1530
+ "data-level": level,
1531
+ "data-block-id": block2.id,
1532
+ "data-selected": selected || void 0,
1533
+ "data-align": align,
1534
+ children: /* @__PURE__ */ jsx8(Editable, { block: block2 })
1535
+ }
1536
+ );
1537
+ }
1538
+
1539
+ // src/blocks/ListBlock.tsx
1540
+ import { getBlockAlign as getBlockAlign3 } from "@inkbrick/document";
1541
+ import { jsx as jsx9, jsxs as jsxs2 } from "react/jsx-runtime";
1542
+ function useOrderedNumber(block2) {
1543
+ const { store } = useEditor();
1544
+ const parent = useBlock(block2.parentId ?? "");
1545
+ if (!parent) return 1;
1546
+ let number = 1;
1547
+ for (let i = parent.children.indexOf(block2.id) - 1; i >= 0; i--) {
1548
+ const sibling = store.getBlock(parent.children[i]);
1549
+ if (sibling?.flavour !== "block:list" || sibling.props.type !== "ordered") break;
1550
+ number++;
1551
+ }
1552
+ return number;
1553
+ }
1554
+ function ListBlock({ block: block2, selected, readOnly }) {
1555
+ const editor = useEditor();
1556
+ const t = useT();
1557
+ const children = useChildren(block2.id);
1558
+ const orderedNumber = useOrderedNumber(block2);
1559
+ const type = typeof block2.props.type === "string" ? block2.props.type : "bullet";
1560
+ return /* @__PURE__ */ jsxs2(
1561
+ "div",
1562
+ {
1563
+ className: "block-list-item",
1564
+ "data-inkbrick-block": "list",
1565
+ "data-align": getBlockAlign3(block2) ?? "left",
1566
+ "data-block-id": block2.id,
1567
+ "data-list-type": type,
1568
+ "data-checked": type === "todo" ? block2.props.checked === true : void 0,
1569
+ "data-selected": selected || void 0,
1570
+ children: [
1571
+ /* @__PURE__ */ jsxs2("div", { className: "block block-list", "data-inkbrick-part": "row", "data-list-type": type, children: [
1572
+ type === "todo" ? /* @__PURE__ */ jsx9(
1573
+ "input",
1574
+ {
1575
+ className: "list-marker list-checkbox",
1576
+ "data-inkbrick-part": "checkbox",
1577
+ type: "checkbox",
1578
+ "aria-label": t("block.list.done"),
1579
+ contentEditable: false,
1580
+ disabled: readOnly,
1581
+ checked: block2.props.checked === true,
1582
+ onChange: (event) => editor.dispatch({
1583
+ type: "setBlockProps",
1584
+ payload: { blockId: block2.id, props: { checked: event.target.checked } }
1585
+ })
1586
+ }
1587
+ ) : /* @__PURE__ */ jsx9(
1588
+ "span",
1589
+ {
1590
+ className: "list-marker",
1591
+ "data-inkbrick-part": "marker",
1592
+ contentEditable: false,
1593
+ "aria-hidden": "true",
1594
+ children: type === "ordered" ? `${orderedNumber}.` : "\u2022"
1595
+ }
1596
+ ),
1597
+ /* @__PURE__ */ jsx9(Editable, { block: block2 })
1598
+ ] }),
1599
+ children.length > 0 && /* @__PURE__ */ jsx9("div", { className: "block-nested-children", "data-inkbrick-part": "children", children: /* @__PURE__ */ jsx9(BlockChildren, { id: block2.id }) })
1600
+ ]
1601
+ }
1602
+ );
1603
+ }
1604
+
1605
+ // src/blocks/QuoteBlock.tsx
1606
+ import { getBlockAlign as getBlockAlign4 } from "@inkbrick/document";
1607
+ import { jsx as jsx10 } from "react/jsx-runtime";
1608
+ function QuoteBlock({ block: block2, selected }) {
1609
+ return /* @__PURE__ */ jsx10(
1610
+ "blockquote",
1611
+ {
1612
+ className: "block block-quote",
1613
+ "data-inkbrick-block": "quote",
1614
+ "data-align": getBlockAlign4(block2) ?? "left",
1615
+ "data-block-id": block2.id,
1616
+ "data-selected": selected || void 0,
1617
+ children: /* @__PURE__ */ jsx10(Editable, { block: block2 })
1618
+ }
1619
+ );
1620
+ }
1621
+
1622
+ // src/blocks/CodeBlock.tsx
1623
+ import { useEffect, useRef as useRef2, useState } from "react";
1624
+ import { jsx as jsx11, jsxs as jsxs3 } from "react/jsx-runtime";
1625
+ var CODE_LANGUAGES = [
1626
+ "plain",
1627
+ "typescript",
1628
+ "javascript",
1629
+ "json",
1630
+ "html",
1631
+ "css",
1632
+ "python",
1633
+ "bash",
1634
+ "sql",
1635
+ "go",
1636
+ "rust",
1637
+ "java",
1638
+ "c",
1639
+ "cpp",
1640
+ "markdown",
1641
+ "yaml"
1642
+ ];
1643
+ function CodeBlock({ block: block2, editor, selected, readOnly }) {
1644
+ const t = useT();
1645
+ const language = typeof block2.props.language === "string" ? block2.props.language : "plain";
1646
+ const languages = CODE_LANGUAGES.includes(language) ? CODE_LANGUAGES : [...CODE_LANGUAGES, language];
1647
+ const [copied, setCopied] = useState(false);
1648
+ const timer = useRef2(void 0);
1649
+ useEffect(() => () => clearTimeout(timer.current), []);
1650
+ const copy = () => {
1651
+ const text = block2.content.map((node) => node.insert).join("");
1652
+ void navigator.clipboard?.writeText(text).then(() => {
1653
+ setCopied(true);
1654
+ clearTimeout(timer.current);
1655
+ timer.current = setTimeout(() => setCopied(false), 1500);
1656
+ });
1657
+ };
1658
+ return /* @__PURE__ */ jsxs3(
1659
+ "div",
1660
+ {
1661
+ className: "block block-code",
1662
+ "data-inkbrick-block": "code",
1663
+ "data-block-id": block2.id,
1664
+ "data-language": language,
1665
+ "data-selected": selected || void 0,
1666
+ children: [
1667
+ /* @__PURE__ */ jsxs3("div", { className: "code-toolbar", "data-inkbrick-part": "toolbar", contentEditable: false, children: [
1668
+ /* @__PURE__ */ jsx11(
1669
+ "select",
1670
+ {
1671
+ className: "code-language",
1672
+ "data-inkbrick-part": "language",
1673
+ "aria-label": t("block.code.language"),
1674
+ disabled: readOnly,
1675
+ value: language,
1676
+ onChange: (event) => editor.dispatch({
1677
+ type: "setBlockProps",
1678
+ payload: { blockId: block2.id, props: { language: event.target.value } }
1679
+ }),
1680
+ children: languages.map((name) => /* @__PURE__ */ jsx11("option", { value: name, children: name }, name))
1681
+ }
1682
+ ),
1683
+ /* @__PURE__ */ jsx11(
1684
+ "button",
1685
+ {
1686
+ type: "button",
1687
+ className: "code-copy",
1688
+ "data-inkbrick-part": "copy",
1689
+ "data-state": copied ? "copied" : void 0,
1690
+ "aria-label": copied ? t("block.code.copied") : t("block.code.copy"),
1691
+ title: copied ? t("block.code.copied") : t("block.code.copy"),
1692
+ onClick: copy,
1693
+ children: /* @__PURE__ */ jsx11(Icon, { name: copied ? "check" : "copy" })
1694
+ }
1695
+ )
1696
+ ] }),
1697
+ /* @__PURE__ */ jsx11(Editable, { block: block2 })
1698
+ ]
1699
+ }
1700
+ );
1701
+ }
1702
+
1703
+ // src/blocks/DividerBlock.tsx
1704
+ import { jsx as jsx12 } from "react/jsx-runtime";
1705
+ function DividerBlock({ block: block2, selected }) {
1706
+ return /* @__PURE__ */ jsx12(
1707
+ "div",
1708
+ {
1709
+ className: "block block-divider",
1710
+ "data-inkbrick-block": "divider",
1711
+ "data-block-id": block2.id,
1712
+ "data-selected": selected || void 0,
1713
+ contentEditable: false,
1714
+ children: /* @__PURE__ */ jsx12("hr", { "data-inkbrick-part": "rule" })
1715
+ }
1716
+ );
1717
+ }
1718
+
1719
+ // src/blocks/CalloutBlock.tsx
1720
+ import { CALLOUT_TONES, getBlockAlign as getBlockAlign5 } from "@inkbrick/document";
1721
+ import { jsx as jsx13, jsxs as jsxs4 } from "react/jsx-runtime";
1722
+ var ICONS = {
1723
+ info: "info",
1724
+ warning: "warning",
1725
+ success: "success",
1726
+ danger: "danger"
1727
+ };
1728
+ var isTone = (value) => CALLOUT_TONES.includes(value);
1729
+ function CalloutBlock({ block: block2, editor, selected, readOnly }) {
1730
+ const t = useT();
1731
+ const tone = isTone(block2.props.tone) ? block2.props.tone : "info";
1732
+ const icon = typeof block2.props.icon === "string" && block2.props.icon !== "" ? block2.props.icon : /* @__PURE__ */ jsx13(Icon, { name: ICONS[tone] });
1733
+ const nextTone = CALLOUT_TONES[(CALLOUT_TONES.indexOf(tone) + 1) % CALLOUT_TONES.length];
1734
+ return /* @__PURE__ */ jsxs4(
1735
+ "div",
1736
+ {
1737
+ className: "block block-callout",
1738
+ "data-inkbrick-block": "callout",
1739
+ "data-align": getBlockAlign5(block2) ?? "left",
1740
+ "data-tone": tone,
1741
+ "data-block-id": block2.id,
1742
+ "data-selected": selected || void 0,
1743
+ children: [
1744
+ /* @__PURE__ */ jsx13(
1745
+ "button",
1746
+ {
1747
+ type: "button",
1748
+ className: "callout-icon",
1749
+ "data-inkbrick-part": "icon",
1750
+ contentEditable: false,
1751
+ disabled: readOnly,
1752
+ "aria-label": t("block.callout.label", { tone: t(`block.callout.tone.${tone}`) }),
1753
+ title: t("block.callout.change"),
1754
+ onClick: () => editor.dispatch({
1755
+ type: "setBlockProps",
1756
+ payload: { blockId: block2.id, props: { tone: nextTone } }
1757
+ }),
1758
+ children: icon
1759
+ }
1760
+ ),
1761
+ /* @__PURE__ */ jsx13(Editable, { block: block2 })
1762
+ ]
1763
+ }
1764
+ );
1765
+ }
1766
+
1767
+ // src/blocks/ImageBlock.tsx
1768
+ import { useEffect as useEffect2, useRef as useRef3, useState as useState2, useSyncExternalStore as useSyncExternalStore6 } from "react";
1769
+ import { isSafeImageUrl as isSafeImageUrl3 } from "@inkbrick/document";
1770
+
1771
+ // src/images/insertImages.ts
1772
+ import { flushSync as flushSync2 } from "react-dom";
1773
+ import { createId } from "@inkbrick/document";
1774
+ function topLevelBlockId(editor, blockId) {
1775
+ let current = editor.store.getBlock(blockId);
1776
+ while (current?.parentId) {
1777
+ const parent = editor.store.getBlock(current.parentId);
1778
+ if (!parent || parent.flavour === "block:note" || !parent.parentId) break;
1779
+ current = parent;
1780
+ }
1781
+ return current?.id ?? blockId;
1782
+ }
1783
+ var altFromName = (file) => file.name.replace(/\.[^.]+$/, "").slice(0, 200);
1784
+ function report(options, error, file) {
1785
+ if (options.onError) options.onError(error, file);
1786
+ else console.error("[@inkbrick/react] Could not add the image:", error);
1787
+ }
1788
+ async function fillImageBlock(editor, view, options, blockId, file, mergeKey) {
1789
+ view.uploads.start(blockId);
1790
+ try {
1791
+ const src = await uploadImage(file, options);
1792
+ flushSync2(() => {
1793
+ editor.dispatch({
1794
+ type: "setBlockProps",
1795
+ payload: { blockId, props: { src, alt: altFromName(file) }, mergeKey }
1796
+ });
1797
+ });
1798
+ view.uploads.finish(blockId);
1799
+ return true;
1800
+ } catch (error) {
1801
+ view.uploads.finish(blockId, toImageErrorInfo(error));
1802
+ report(options, error, file);
1803
+ return false;
1804
+ }
1805
+ }
1806
+ function insertImageFiles(editor, view, options, files, afterId) {
1807
+ const images = files.filter((file) => isAcceptedImage(file, options));
1808
+ if (images.length === 0 || editor.readOnly) return false;
1809
+ let anchor = topLevelBlockId(editor, afterId);
1810
+ let caret;
1811
+ const mergeKey = `images:${createId()}`;
1812
+ const created = [];
1813
+ flushSync2(() => {
1814
+ editor.batch(
1815
+ () => {
1816
+ for (const file of images) {
1817
+ const target = editor.store.getBlock(anchor);
1818
+ const alt = altFromName(file);
1819
+ const emptyParagraph = target?.flavour === "block:paragraph" && target.content.length === 0 && target.children.length === 0;
1820
+ if (emptyParagraph) {
1821
+ caret = editor.dispatch({
1822
+ type: "convertBlock",
1823
+ payload: { blockId: anchor, flavour: "block:image", props: { alt } }
1824
+ });
1825
+ created.push({ id: anchor, file });
1826
+ } else {
1827
+ const inserted = editor.dispatch({
1828
+ type: "insertBlock",
1829
+ payload: { flavour: "block:image", afterId: anchor, props: { alt } }
1830
+ });
1831
+ if (!inserted) continue;
1832
+ created.push({ id: inserted.blockId, file });
1833
+ anchor = inserted.blockId;
1834
+ if (!editor.document.getNextSibling(anchor)) {
1835
+ caret = editor.dispatch({
1836
+ type: "insertBlock",
1837
+ payload: { flavour: "block:paragraph", afterId: anchor }
1838
+ });
1839
+ } else {
1840
+ caret = { blockId: editor.document.getNextSibling(anchor).id, offset: 0 };
1841
+ }
1842
+ }
1843
+ }
1844
+ },
1845
+ { mergeKey }
1846
+ );
1847
+ });
1848
+ if (caret) view.placeCaret(caret.blockId, caret.offset);
1849
+ for (const { id, file } of created) {
1850
+ void fillImageBlock(editor, view, options, id, file, mergeKey).then((ok) => {
1851
+ if (!ok && (editor.store.getBlock(id)?.props.src ?? "") === "") {
1852
+ flushSync2(() => {
1853
+ editor.execute("delete-block", { id });
1854
+ });
1855
+ }
1856
+ });
1857
+ }
1858
+ return true;
1859
+ }
1860
+
1861
+ // src/blocks/ImageBlock.tsx
1862
+ import { jsx as jsx14, jsxs as jsxs5 } from "react/jsx-runtime";
1863
+ var WIDTHS = [25, 50, 75, 100];
1864
+ var ALIGNS = [
1865
+ { value: "left", label: "image.align.left", icon: "align-left" },
1866
+ { value: "center", label: "image.align.center", icon: "align-center" },
1867
+ { value: "right", label: "image.align.right", icon: "align-right" }
1868
+ ];
1869
+ var setProps = (editor, blockId, props) => editor.dispatch({ type: "setBlockProps", payload: { blockId, props } });
1870
+ function CommitInput({
1871
+ value,
1872
+ label,
1873
+ placeholder,
1874
+ className,
1875
+ part,
1876
+ onCommit
1877
+ }) {
1878
+ const [draft, setDraft] = useState2(value);
1879
+ const saved = useRef3(value);
1880
+ const cancelled = useRef3(false);
1881
+ useEffect2(() => {
1882
+ saved.current = value;
1883
+ setDraft(value);
1884
+ }, [value]);
1885
+ const commit = () => {
1886
+ if (cancelled.current) {
1887
+ cancelled.current = false;
1888
+ return;
1889
+ }
1890
+ if (draft !== saved.current) {
1891
+ saved.current = draft;
1892
+ onCommit(draft);
1893
+ }
1894
+ };
1895
+ return /* @__PURE__ */ jsx14(
1896
+ "input",
1897
+ {
1898
+ className,
1899
+ "data-inkbrick-part": part,
1900
+ type: "text",
1901
+ "aria-label": label,
1902
+ placeholder,
1903
+ value: draft,
1904
+ onChange: (event) => setDraft(event.target.value),
1905
+ onBlur: commit,
1906
+ onKeyDown: (event) => {
1907
+ if (event.key === "Enter") {
1908
+ event.preventDefault();
1909
+ commit();
1910
+ event.target.blur();
1911
+ } else if (event.key === "Escape") {
1912
+ cancelled.current = true;
1913
+ setDraft(saved.current);
1914
+ event.target.blur();
1915
+ }
1916
+ }
1917
+ }
1918
+ );
1919
+ }
1920
+ function ImagePicker({ blockId, editor }) {
1921
+ const view = useEditorView();
1922
+ const t = useT();
1923
+ const { images } = useEditorUI();
1924
+ const [url, setUrl] = useState2("");
1925
+ const [linkError, setLinkError] = useState2(null);
1926
+ const uploadError = useSyncExternalStore6(
1927
+ view.uploads.subscribe,
1928
+ () => view.uploads.errorDetailsOf(blockId),
1929
+ () => void 0
1930
+ );
1931
+ const error = linkError ? t(linkError) : uploadError ? formatImageError(uploadError, t) : null;
1932
+ const file = useRef3(null);
1933
+ const embed = () => {
1934
+ const normalized = normalizeImageUrl(url);
1935
+ if (!normalized) {
1936
+ setLinkError("image.error.invalidLink");
1937
+ return;
1938
+ }
1939
+ if (editor.dispatch({
1940
+ type: "setBlockProps",
1941
+ payload: { blockId, props: { src: normalized } }
1942
+ }) === void 0) {
1943
+ setLinkError("image.error.unsupportedAddress");
1944
+ }
1945
+ };
1946
+ const upload = async (selected) => {
1947
+ if (!selected) return;
1948
+ setLinkError(null);
1949
+ await fillImageBlock(editor, view, images, blockId, selected);
1950
+ };
1951
+ return /* @__PURE__ */ jsxs5("div", { className: "image-picker", "data-inkbrick-part": "picker", "data-testid": "image-picker", children: [
1952
+ /* @__PURE__ */ jsxs5("div", { className: "image-picker__row", "data-inkbrick-part": "picker-row", children: [
1953
+ /* @__PURE__ */ jsx14(
1954
+ "input",
1955
+ {
1956
+ className: "image-picker__input",
1957
+ "data-inkbrick-part": "picker-input",
1958
+ type: "text",
1959
+ "aria-label": t("image.url.label"),
1960
+ "aria-invalid": linkError !== null,
1961
+ placeholder: t("image.url.placeholder"),
1962
+ value: url,
1963
+ onChange: (event) => {
1964
+ setUrl(event.target.value);
1965
+ setLinkError(null);
1966
+ view.uploads.clearError(blockId);
1967
+ },
1968
+ onKeyDown: (event) => {
1969
+ if (event.key === "Enter") {
1970
+ event.preventDefault();
1971
+ embed();
1972
+ }
1973
+ }
1974
+ }
1975
+ ),
1976
+ /* @__PURE__ */ jsx14(
1977
+ "button",
1978
+ {
1979
+ type: "button",
1980
+ className: "image-picker__button",
1981
+ "data-inkbrick-part": "picker-button",
1982
+ onClick: embed,
1983
+ children: t("image.embed")
1984
+ }
1985
+ ),
1986
+ /* @__PURE__ */ jsx14(
1987
+ "button",
1988
+ {
1989
+ type: "button",
1990
+ className: "image-picker__button",
1991
+ onClick: () => file.current?.click(),
1992
+ children: t("image.upload")
1993
+ }
1994
+ ),
1995
+ /* @__PURE__ */ jsx14(
1996
+ "input",
1997
+ {
1998
+ ref: file,
1999
+ type: "file",
2000
+ hidden: true,
2001
+ "data-testid": "image-file",
2002
+ accept: (images.acceptedTypes ?? DEFAULT_IMAGE_TYPES).join(","),
2003
+ onChange: (event) => {
2004
+ void upload(event.target.files?.[0]);
2005
+ event.target.value = "";
2006
+ }
2007
+ }
2008
+ )
2009
+ ] }),
2010
+ error && /* @__PURE__ */ jsx14("div", { className: "image-picker__error", "data-inkbrick-part": "picker-error", role: "alert", children: error })
2011
+ ] });
2012
+ }
2013
+ function ImageBlock({ block: block2, editor, selected, readOnly }) {
2014
+ const view = useEditorView();
2015
+ const t = useT();
2016
+ const uploading = useSyncExternalStore6(
2017
+ view.uploads.subscribe,
2018
+ () => view.uploads.has(block2.id),
2019
+ () => false
2020
+ );
2021
+ const { src, alt, caption } = block2.props;
2022
+ const width = typeof block2.props.width === "number" ? block2.props.width : 100;
2023
+ const align = typeof block2.props.align === "string" ? block2.props.align : "center";
2024
+ const address = typeof src === "string" ? src : "";
2025
+ const [failed, setFailed] = useState2(false);
2026
+ useEffect2(() => setFailed(false), [address]);
2027
+ let content;
2028
+ if (uploading) {
2029
+ content = /* @__PURE__ */ jsx14("div", { className: "image-status", "data-inkbrick-part": "status", "data-state": "loading", role: "status", children: t("image.status.uploading") });
2030
+ } else if (address === "") {
2031
+ content = readOnly ? /* @__PURE__ */ jsx14("div", { className: "image-status", "data-inkbrick-part": "status", "data-state": "empty", children: t("image.status.none") }) : /* @__PURE__ */ jsx14(ImagePicker, { blockId: block2.id, editor });
2032
+ } else if (!isSafeImageUrl3(address)) {
2033
+ content = /* @__PURE__ */ jsx14(
2034
+ "div",
2035
+ {
2036
+ className: "image-status image-status--error",
2037
+ "data-inkbrick-part": "status",
2038
+ "data-state": "error",
2039
+ children: t("image.status.unsupported")
2040
+ }
2041
+ );
2042
+ } else if (failed) {
2043
+ content = /* @__PURE__ */ jsxs5(
2044
+ "div",
2045
+ {
2046
+ className: "image-status image-status--error",
2047
+ "data-inkbrick-part": "status",
2048
+ "data-state": "error",
2049
+ role: "alert",
2050
+ children: [
2051
+ t("image.status.failed"),
2052
+ !readOnly && /* @__PURE__ */ jsx14(
2053
+ "button",
2054
+ {
2055
+ type: "button",
2056
+ className: "image-picker__button",
2057
+ "data-inkbrick-part": "picker-button",
2058
+ onClick: () => setProps(editor, block2.id, { src: "" }),
2059
+ children: t("image.status.retry")
2060
+ }
2061
+ )
2062
+ ]
2063
+ }
2064
+ );
2065
+ } else {
2066
+ content = /* @__PURE__ */ jsxs5(
2067
+ "figure",
2068
+ {
2069
+ className: "image-figure",
2070
+ "data-inkbrick-part": "figure",
2071
+ "data-align": align,
2072
+ style: { width: `${width}%` },
2073
+ children: [
2074
+ /* @__PURE__ */ jsx14(
2075
+ "img",
2076
+ {
2077
+ src: address,
2078
+ alt: alt ?? "",
2079
+ loading: "lazy",
2080
+ decoding: "async",
2081
+ draggable: false,
2082
+ "data-inkbrick-part": "image",
2083
+ referrerPolicy: "no-referrer",
2084
+ onError: () => setFailed(true)
2085
+ }
2086
+ ),
2087
+ !readOnly && /* @__PURE__ */ jsxs5(
2088
+ "div",
2089
+ {
2090
+ className: "image-tools",
2091
+ "data-inkbrick-part": "tools",
2092
+ role: "toolbar",
2093
+ "aria-label": t("image.options"),
2094
+ children: [
2095
+ ALIGNS.map(({ value, label, icon }) => /* @__PURE__ */ jsx14(
2096
+ "button",
2097
+ {
2098
+ type: "button",
2099
+ className: "image-tools__button",
2100
+ "data-inkbrick-part": "tool",
2101
+ "data-pressed": align === value || void 0,
2102
+ "aria-label": t(label),
2103
+ "aria-pressed": align === value,
2104
+ title: t(label),
2105
+ onClick: () => setProps(editor, block2.id, { align: value }),
2106
+ children: /* @__PURE__ */ jsx14(Icon, { name: icon })
2107
+ },
2108
+ value
2109
+ )),
2110
+ /* @__PURE__ */ jsxs5(
2111
+ "select",
2112
+ {
2113
+ className: "image-tools__select",
2114
+ "data-inkbrick-part": "width",
2115
+ "aria-label": t("image.width"),
2116
+ value: WIDTHS.includes(width) ? width : "",
2117
+ onChange: (event) => setProps(editor, block2.id, { width: Number(event.target.value) }),
2118
+ children: [
2119
+ !WIDTHS.includes(width) && /* @__PURE__ */ jsxs5("option", { value: "", children: [
2120
+ width,
2121
+ "%"
2122
+ ] }),
2123
+ WIDTHS.map((w) => /* @__PURE__ */ jsxs5("option", { value: w, children: [
2124
+ w,
2125
+ "%"
2126
+ ] }, w))
2127
+ ]
2128
+ }
2129
+ ),
2130
+ /* @__PURE__ */ jsx14(
2131
+ CommitInput,
2132
+ {
2133
+ className: "image-tools__alt",
2134
+ part: "alt",
2135
+ label: t("image.alt"),
2136
+ placeholder: t("image.alt"),
2137
+ value: alt ?? "",
2138
+ onCommit: (value) => setProps(editor, block2.id, { alt: value })
2139
+ }
2140
+ ),
2141
+ /* @__PURE__ */ jsx14(
2142
+ "button",
2143
+ {
2144
+ type: "button",
2145
+ className: "image-tools__button",
2146
+ "data-inkbrick-part": "tool",
2147
+ "aria-label": t("image.replace"),
2148
+ title: t("image.replace"),
2149
+ onClick: () => setProps(editor, block2.id, { src: "" }),
2150
+ children: /* @__PURE__ */ jsx14(Icon, { name: "replace" })
2151
+ }
2152
+ )
2153
+ ]
2154
+ }
2155
+ ),
2156
+ readOnly ? caption ? /* @__PURE__ */ jsx14("figcaption", { className: "image-caption", "data-inkbrick-part": "caption", children: caption }) : null : /* @__PURE__ */ jsx14(
2157
+ CommitInput,
2158
+ {
2159
+ className: "image-caption image-caption--input",
2160
+ part: "caption",
2161
+ label: t("image.caption.label"),
2162
+ placeholder: t("image.caption.placeholder"),
2163
+ value: caption ?? "",
2164
+ onCommit: (value) => setProps(editor, block2.id, { caption: value })
2165
+ }
2166
+ )
2167
+ ]
2168
+ }
2169
+ );
2170
+ }
2171
+ return /* @__PURE__ */ jsx14(
2172
+ "div",
2173
+ {
2174
+ className: "block block-image",
2175
+ "data-inkbrick-block": "image",
2176
+ "data-block-id": block2.id,
2177
+ "data-selected": selected || void 0,
2178
+ contentEditable: false,
2179
+ children: content
2180
+ }
2181
+ );
2182
+ }
2183
+
2184
+ // src/blocks/FormulaBlock.tsx
2185
+ import { useCallback as useCallback3, useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
2186
+ import { MAX_FORMULA_LENGTH } from "@inkbrick/document";
2187
+
2188
+ // src/formula/LazyFormulaPalette.tsx
2189
+ import { useEffect as useEffect3, useState as useState3 } from "react";
2190
+ import { jsx as jsx15 } from "react/jsx-runtime";
2191
+ var loaded;
2192
+ var loading;
2193
+ function preloadFormulaPalette() {
2194
+ return loading ??= import("./FormulaPalette-WFRPRHS5.js").then(
2195
+ (module) => loaded = module.FormulaPalette
2196
+ );
2197
+ }
2198
+ function LazyFormulaPalette(props) {
2199
+ const [Palette, setPalette] = useState3(() => loaded);
2200
+ useEffect3(() => {
2201
+ if (Palette) return;
2202
+ let active = true;
2203
+ void preloadFormulaPalette().then((component) => {
2204
+ if (active) setPalette(() => component);
2205
+ });
2206
+ return () => {
2207
+ active = false;
2208
+ };
2209
+ }, [Palette]);
2210
+ return Palette ? /* @__PURE__ */ jsx15(Palette, { ...props }) : null;
2211
+ }
2212
+
2213
+ // src/math/FormulaView.tsx
2214
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
2215
+ import { Fragment, jsx as jsx16, jsxs as jsxs6 } from "react/jsx-runtime";
2216
+ function FormulaView({
2217
+ latex,
2218
+ display = false,
2219
+ showError = false,
2220
+ onSettled
2221
+ }) {
2222
+ const { formulaRenderer } = useEditorUI();
2223
+ const ref = useRef4(null);
2224
+ const [error, setError] = useState4(null);
2225
+ const Tag = display ? "div" : "span";
2226
+ useEffect4(() => {
2227
+ const target = ref.current;
2228
+ if (!target) return;
2229
+ ensureMathStyles(globalThis.document);
2230
+ setError(null);
2231
+ return mountFormula(target, latex, {
2232
+ display,
2233
+ renderer: formulaRenderer,
2234
+ onSettled: (state, message) => {
2235
+ setError(state === "error" ? message ?? "" : null);
2236
+ onSettled?.(state, message);
2237
+ }
2238
+ });
2239
+ }, [latex, display, formulaRenderer, onSettled]);
2240
+ return /* @__PURE__ */ jsxs6(Fragment, { children: [
2241
+ /* @__PURE__ */ jsx16(
2242
+ Tag,
2243
+ {
2244
+ ref: (node) => {
2245
+ ref.current = node;
2246
+ },
2247
+ "data-inkbrick-part": "formula-render",
2248
+ "data-display": display || void 0
2249
+ }
2250
+ ),
2251
+ showError && error !== null && /* @__PURE__ */ jsx16("span", { "data-inkbrick-part": "formula-error", role: "alert", children: error })
2252
+ ] });
2253
+ }
2254
+
2255
+ // src/blocks/FormulaBlock.tsx
2256
+ import { jsx as jsx17, jsxs as jsxs7 } from "react/jsx-runtime";
2257
+ function FormulaBlock({ block: block2, editor, selected, readOnly }) {
2258
+ const t = useT();
2259
+ const latex = typeof block2.props.latex === "string" ? block2.props.latex : "";
2260
+ const [editing, setEditing] = useState5(latex === "" && !readOnly);
2261
+ const [draft, setDraft] = useState5(latex);
2262
+ const [error, setError] = useState5(null);
2263
+ const field = useRef5(null);
2264
+ const container = useRef5(null);
2265
+ const insertSnippet = useSnippetInsert(field, draft, setDraft);
2266
+ const cancelled = useRef5(false);
2267
+ const born = useRef5(Date.now());
2268
+ useEffect5(() => {
2269
+ if (!editing) setDraft(latex);
2270
+ }, [latex, editing]);
2271
+ useEffect5(() => {
2272
+ if (!editing) return;
2273
+ cancelled.current = false;
2274
+ born.current = Date.now();
2275
+ field.current?.focus();
2276
+ const timer = setTimeout(() => {
2277
+ if (Date.now() - born.current < 300) field.current?.focus();
2278
+ }, 0);
2279
+ return () => clearTimeout(timer);
2280
+ }, [editing]);
2281
+ useEffect5(() => {
2282
+ if (readOnly) setEditing(false);
2283
+ }, [readOnly]);
2284
+ const settle = useCallback3((state, message) => {
2285
+ setError(state === "error" ? message ?? "" : null);
2286
+ }, []);
2287
+ const commit = () => {
2288
+ if (cancelled.current) {
2289
+ cancelled.current = false;
2290
+ return;
2291
+ }
2292
+ if (latex === "" && draft === "" && Date.now() - born.current < 300) {
2293
+ setTimeout(() => field.current?.focus(), 0);
2294
+ return;
2295
+ }
2296
+ if (draft !== latex) {
2297
+ editor.dispatch({
2298
+ type: "setBlockProps",
2299
+ payload: { blockId: block2.id, props: { latex: draft } }
2300
+ });
2301
+ }
2302
+ setEditing(false);
2303
+ };
2304
+ const cancel = () => {
2305
+ cancelled.current = true;
2306
+ setDraft(latex);
2307
+ setEditing(false);
2308
+ };
2309
+ return /* @__PURE__ */ jsx17(
2310
+ "div",
2311
+ {
2312
+ className: "block block-formula",
2313
+ "data-inkbrick-block": "formula",
2314
+ "data-block-id": block2.id,
2315
+ "data-selected": selected || void 0,
2316
+ contentEditable: false,
2317
+ children: editing ? /* @__PURE__ */ jsxs7(
2318
+ "div",
2319
+ {
2320
+ ref: container,
2321
+ className: "formula-editor",
2322
+ "data-inkbrick-part": "formula-editor",
2323
+ onBlur: (event) => {
2324
+ if (event.relatedTarget instanceof Node && container.current?.contains(event.relatedTarget))
2325
+ return;
2326
+ commit();
2327
+ },
2328
+ children: [
2329
+ /* @__PURE__ */ jsx17(
2330
+ "textarea",
2331
+ {
2332
+ ref: field,
2333
+ className: "formula-source",
2334
+ "data-inkbrick-part": "formula-source",
2335
+ "aria-label": t("formula.source"),
2336
+ "aria-invalid": error !== null || void 0,
2337
+ placeholder: t("formula.placeholder"),
2338
+ spellCheck: false,
2339
+ rows: Math.min(8, Math.max(2, draft.split("\n").length)),
2340
+ maxLength: MAX_FORMULA_LENGTH,
2341
+ value: draft,
2342
+ onChange: (event) => setDraft(event.target.value),
2343
+ onKeyDown: (event) => {
2344
+ if (event.key === "Escape") {
2345
+ event.preventDefault();
2346
+ cancel();
2347
+ event.target.blur();
2348
+ } else if (event.key === "Enter" && (event.ctrlKey || event.metaKey)) {
2349
+ event.preventDefault();
2350
+ event.target.blur();
2351
+ }
2352
+ }
2353
+ }
2354
+ ),
2355
+ /* @__PURE__ */ jsx17("div", { className: "formula-preview", "data-inkbrick-part": "formula-preview", children: /* @__PURE__ */ jsx17(FormulaView, { latex: draft, display: true, showError: true, onSettled: settle }) }),
2356
+ /* @__PURE__ */ jsx17(LazyFormulaPalette, { onInsert: insertSnippet }),
2357
+ /* @__PURE__ */ jsx17("div", { className: "formula-hint", "data-inkbrick-part": "formula-hint", children: t("formula.hint") })
2358
+ ]
2359
+ }
2360
+ ) : latex === "" ? /* @__PURE__ */ jsx17(
2361
+ "button",
2362
+ {
2363
+ type: "button",
2364
+ className: "formula-empty",
2365
+ "data-inkbrick-part": "formula-empty",
2366
+ disabled: readOnly,
2367
+ onClick: () => setEditing(true),
2368
+ children: t("formula.empty")
2369
+ }
2370
+ ) : /* @__PURE__ */ jsx17(
2371
+ "div",
2372
+ {
2373
+ className: "formula-display",
2374
+ "data-inkbrick-part": "formula-display",
2375
+ role: readOnly ? void 0 : "button",
2376
+ tabIndex: readOnly ? void 0 : 0,
2377
+ "aria-label": readOnly ? void 0 : t("formula.edit"),
2378
+ title: readOnly ? void 0 : t("formula.edit"),
2379
+ onClick: () => !readOnly && setEditing(true),
2380
+ onKeyDown: (event) => {
2381
+ if (!readOnly && (event.key === "Enter" || event.key === " ")) {
2382
+ event.preventDefault();
2383
+ setEditing(true);
2384
+ }
2385
+ },
2386
+ children: /* @__PURE__ */ jsx17(FormulaView, { latex, display: true, showError: true })
2387
+ }
2388
+ )
2389
+ }
2390
+ );
2391
+ }
2392
+
2393
+ // src/registry/defaultBlockComponents.ts
2394
+ var defaultBlockComponents = Object.freeze({
2395
+ "block:page": PageBlock,
2396
+ "block:note": NoteBlock,
2397
+ "block:paragraph": ParagraphBlock,
2398
+ "block:heading": HeadingBlock,
2399
+ "block:list": ListBlock,
2400
+ "block:quote": QuoteBlock,
2401
+ "block:code": CodeBlock,
2402
+ "block:divider": DividerBlock,
2403
+ "block:callout": CalloutBlock,
2404
+ "block:image": ImageBlock,
2405
+ "block:formula": FormulaBlock
2406
+ });
2407
+
2408
+ // src/registry/resolveBlockComponent.ts
2409
+ function lookup(registry, flavour) {
2410
+ return Object.hasOwn(registry, flavour) ? registry[flavour] : void 0;
2411
+ }
2412
+ function resolveBlockComponent(flavour, custom) {
2413
+ return lookup(custom, flavour) ?? lookup(defaultBlockComponents, flavour);
2414
+ }
2415
+
2416
+ // src/renderer/BlockRenderer.tsx
2417
+ import { jsx as jsx18 } from "react/jsx-runtime";
2418
+ var warned = /* @__PURE__ */ new Set();
2419
+ var BlockRenderer = memo(function BlockRenderer2({ id }) {
2420
+ const editor = useEditor();
2421
+ const view = useEditorView();
2422
+ const readOnly = useReadOnly();
2423
+ const { components, slots } = useEditorUI();
2424
+ const selected = useSyncExternalStore7(
2425
+ view.selection.subscribe,
2426
+ () => view.selection.isSelected(id),
2427
+ () => false
2428
+ );
2429
+ const block2 = useBlock(id);
2430
+ if (!block2) {
2431
+ return null;
2432
+ }
2433
+ const Component = resolveBlockComponent(block2.flavour, components);
2434
+ if (Component) {
2435
+ return /* @__PURE__ */ jsx18(
2436
+ Component,
2437
+ {
2438
+ block: block2,
2439
+ editor,
2440
+ selected,
2441
+ readOnly,
2442
+ controller: editor.controllers.get(block2.flavour)
2443
+ }
2444
+ );
2445
+ }
2446
+ if (!warned.has(block2.flavour)) {
2447
+ warned.add(block2.flavour);
2448
+ console.warn(`[@inkbrick/react] No component registered for block "${block2.flavour}"`);
2449
+ }
2450
+ const Fallback = slots.UnknownBlock ?? UnknownBlockView;
2451
+ return /* @__PURE__ */ jsx18(Fallback, { block: block2, editor, selected, readOnly, children: /* @__PURE__ */ jsx18(BlockChildren, { id: block2.id }) });
2452
+ });
2453
+
2454
+ // src/renderer/DocumentRenderer.tsx
2455
+ import { Fragment as Fragment2, jsx as jsx19 } from "react/jsx-runtime";
2456
+ function FullChildList({ ids }) {
2457
+ return /* @__PURE__ */ jsx19(Fragment2, { children: ids.map((id) => /* @__PURE__ */ jsx19(BlockRenderer, { id }, id)) });
2458
+ }
2459
+ var fullRenderer = { ChildList: FullChildList };
2460
+ var DocumentRendererContext = createContext2(fullRenderer);
2461
+
2462
+ // src/blocks/BlockChildren.tsx
2463
+ import { jsx as jsx20 } from "react/jsx-runtime";
2464
+ function BlockChildren({ id }) {
2465
+ const children = useChildren(id);
2466
+ const { ChildList } = useContext3(DocumentRendererContext);
2467
+ return /* @__PURE__ */ jsx20(ChildList, { parentId: id, ids: children });
2468
+ }
2469
+
2470
+ // src/renderer/DocumentView.tsx
2471
+ import { useEffect as useEffect16, useLayoutEffect as useLayoutEffect8, useRef as useRef12 } from "react";
2472
+
2473
+ // src/block-handle/BlockHandleHost.tsx
2474
+ import { useEffect as useEffect7, useRef as useRef7, useSyncExternalStore as useSyncExternalStore9 } from "react";
2475
+ import { createPortal } from "react-dom";
2476
+
2477
+ // src/block-handle/actions.tsx
2478
+ import { flushSync as flushSync3 } from "react-dom";
2479
+
2480
+ // src/slash/syncSlashMenu.ts
2481
+ function caretPosition(view, blockId) {
2482
+ const selection = globalThis.getSelection();
2483
+ const range = selection && selection.rangeCount > 0 ? selection.getRangeAt(0) : null;
2484
+ const rect = range?.getClientRects()[0];
2485
+ if (rect && (rect.width > 0 || rect.height > 0)) return { x: rect.left, y: rect.bottom };
2486
+ const element = view.getElement(blockId);
2487
+ if (!element) return null;
2488
+ const box = element.getBoundingClientRect();
2489
+ return { x: box.left, y: box.bottom };
2490
+ }
2491
+ function syncSlashMenu(editor, view, items) {
2492
+ const state = view.slash.getState();
2493
+ if (!state.open || !state.blockId) return;
2494
+ const root = view.getRoot();
2495
+ const selection = root ? readDocSelection(root) : null;
2496
+ const block2 = editor.store.getBlock(state.blockId);
2497
+ const text = block2 ? editor.engine.inline.toPlainText(block2.content) : "";
2498
+ if (!selection || !selection.collapsed || selection.from.blockId !== state.blockId || selection.from.offset <= state.slashOffset || text[state.slashOffset] !== "/") {
2499
+ view.slash.close();
2500
+ return;
2501
+ }
2502
+ const query = text.slice(state.slashOffset + 1, selection.from.offset);
2503
+ const visible = visibleSlashItems(editor, items, state.blockId, query);
2504
+ if (visible.length === 0 && /\s$/.test(query)) {
2505
+ view.slash.close();
2506
+ return;
2507
+ }
2508
+ view.slash.update({
2509
+ query,
2510
+ activeIndex: query !== state.query ? 0 : Math.min(state.activeIndex, Math.max(visible.length - 1, 0)),
2511
+ position: caretPosition(view, state.blockId)
2512
+ });
2513
+ }
2514
+
2515
+ // src/block-handle/actions.tsx
2516
+ import { jsx as jsx21 } from "react/jsx-runtime";
2517
+ var isCaret = (value) => typeof value === "object" && value !== null && "blockId" in value && "offset" in value;
2518
+ function performBlockAction(view, action) {
2519
+ let result;
2520
+ flushSync3(() => {
2521
+ result = action();
2522
+ });
2523
+ view.blocks.closeMenu();
2524
+ if (isCaret(result) && view.getElement(result.blockId)) {
2525
+ view.placeCaret(result.blockId, result.offset);
2526
+ } else {
2527
+ view.getRoot()?.focus({ preventScroll: true });
2528
+ }
2529
+ }
2530
+ function buildBlockActions(editor, view, blockId, slashItems, t) {
2531
+ const previous = editor.document.getPreviousSibling(blockId);
2532
+ const next = editor.document.getNextSibling(blockId);
2533
+ const perform = (fn) => () => performBlockAction(view, fn);
2534
+ const moveUp = previous && { blockId, targetId: previous.id, position: "before" };
2535
+ const moveDown = next && { blockId, targetId: next.id, position: "after" };
2536
+ const block2 = editor.store.getBlock(blockId);
2537
+ const controller = block2 && editor.controllers.get(block2.flavour);
2538
+ const own = (controller?.menuItems ?? []).map((item) => {
2539
+ const context = editor.controllers.contextOf(block2);
2540
+ return {
2541
+ id: item.id,
2542
+ title: item.title,
2543
+ icon: iconByName(item.icon),
2544
+ danger: item.danger,
2545
+ disabled: item.isDisabled ? item.isDisabled(context) : context.readOnly,
2546
+ run: perform(() => item.run(context))
2547
+ };
2548
+ });
2549
+ const actions = [
2550
+ ...own,
2551
+ {
2552
+ id: "duplicate",
2553
+ title: t("menu.duplicate"),
2554
+ icon: /* @__PURE__ */ jsx21(Icon, { name: "copy" }),
2555
+ disabled: !editor.can("duplicateBlock", { blockId }),
2556
+ run: perform(() => editor.dispatch({ type: "duplicateBlock", payload: { blockId } }))
2557
+ },
2558
+ {
2559
+ id: "move-up",
2560
+ title: t("menu.moveUp"),
2561
+ icon: /* @__PURE__ */ jsx21(Icon, { name: "arrow-up" }),
2562
+ disabled: !moveUp || !editor.can("moveBlock", moveUp),
2563
+ run: perform(() => moveUp && editor.dispatch({ type: "moveBlock", payload: moveUp }))
2564
+ },
2565
+ {
2566
+ id: "move-down",
2567
+ title: t("menu.moveDown"),
2568
+ icon: /* @__PURE__ */ jsx21(Icon, { name: "arrow-down" }),
2569
+ disabled: !moveDown || !editor.can("moveBlock", moveDown),
2570
+ run: perform(() => moveDown && editor.dispatch({ type: "moveBlock", payload: moveDown }))
2571
+ },
2572
+ {
2573
+ id: "delete",
2574
+ title: t("menu.delete"),
2575
+ icon: /* @__PURE__ */ jsx21(Icon, { name: "trash" }),
2576
+ danger: true,
2577
+ run: perform(() => editor.execute("delete-block", { id: blockId }))
2578
+ }
2579
+ ];
2580
+ const turnInto = availableSlashItems(editor, slashItems, blockId).filter(
2581
+ (item) => editor.can("convertBlock", { blockId, flavour: item.flavour, props: item.props })
2582
+ ).map((item) => ({
2583
+ id: item.id,
2584
+ title: item.title,
2585
+ icon: item.icon,
2586
+ run: perform(
2587
+ () => editor.dispatch({
2588
+ type: "convertBlock",
2589
+ payload: { blockId, flavour: item.flavour, props: item.props }
2590
+ })
2591
+ )
2592
+ }));
2593
+ return { actions, turnInto };
2594
+ }
2595
+ function addBlockBelow(editor, view, blockId, slashItems) {
2596
+ const block2 = editor.store.getBlock(blockId);
2597
+ const parent = block2?.parentId ? editor.store.getBlock(block2.parentId) : void 0;
2598
+ if (!block2 || !parent) return;
2599
+ const nested = parent.flavour !== "block:note";
2600
+ const payload = {
2601
+ flavour: nested ? block2.flavour : "block:paragraph",
2602
+ afterId: blockId,
2603
+ props: nested ? block2.props : void 0,
2604
+ content: [{ insert: "/" }]
2605
+ };
2606
+ let caret;
2607
+ flushSync3(() => {
2608
+ caret = editor.dispatch({ type: "insertBlock", payload });
2609
+ });
2610
+ view.blocks.setHover(null);
2611
+ if (!caret) return;
2612
+ view.placeCaret(caret.blockId, 1);
2613
+ view.slash.open(caret.blockId, 0);
2614
+ syncSlashMenu(editor, view, slashItems);
2615
+ }
2616
+
2617
+ // src/block-handle/DefaultBlockHandle.tsx
2618
+ import { jsx as jsx22, jsxs as jsxs8 } from "react/jsx-runtime";
2619
+ var HANDLE_WIDTH = 52;
2620
+ function DefaultBlockHandle({
2621
+ blockId,
2622
+ position,
2623
+ canAdd,
2624
+ dragging,
2625
+ menuOpen,
2626
+ onAdd,
2627
+ onOpenMenu,
2628
+ gripProps
2629
+ }) {
2630
+ const t = useT();
2631
+ const left = position.x - HANDLE_WIDTH;
2632
+ return /* @__PURE__ */ jsxs8(
2633
+ "div",
2634
+ {
2635
+ className: "inkbrick-block-handle",
2636
+ "data-inkbrick-ui": "block-handle",
2637
+ "data-testid": "block-handle",
2638
+ "data-block": blockId,
2639
+ "data-inkbrick-block-ui": true,
2640
+ "data-compact": left < 2 || void 0,
2641
+ style: {
2642
+ left: Math.max(2, left),
2643
+ top: position.y,
2644
+ pointerEvents: dragging ? "none" : void 0
2645
+ },
2646
+ onMouseDown: (event) => event.preventDefault(),
2647
+ children: [
2648
+ /* @__PURE__ */ jsx22(
2649
+ "button",
2650
+ {
2651
+ type: "button",
2652
+ className: "inkbrick-block-handle__button",
2653
+ "data-inkbrick-part": "button",
2654
+ "aria-label": t("handle.add"),
2655
+ title: t("handle.add"),
2656
+ "data-action": "add",
2657
+ disabled: !canAdd,
2658
+ onClick: onAdd,
2659
+ children: /* @__PURE__ */ jsx22(Icon, { name: "plus" })
2660
+ }
2661
+ ),
2662
+ /* @__PURE__ */ jsx22(
2663
+ "button",
2664
+ {
2665
+ type: "button",
2666
+ className: "inkbrick-block-handle__button inkbrick-block-handle__grip",
2667
+ "data-inkbrick-part": "grip",
2668
+ "data-active": menuOpen || void 0,
2669
+ "aria-label": t("handle.grip"),
2670
+ title: t("handle.grip"),
2671
+ "aria-haspopup": "menu",
2672
+ "aria-expanded": menuOpen,
2673
+ "data-action": "grip",
2674
+ ...gripProps,
2675
+ onKeyDown: (event) => {
2676
+ if (event.key === "Enter" || event.key === " ") {
2677
+ event.preventDefault();
2678
+ onOpenMenu();
2679
+ }
2680
+ },
2681
+ children: /* @__PURE__ */ jsx22(Icon, { name: "grip" })
2682
+ }
2683
+ )
2684
+ ]
2685
+ }
2686
+ );
2687
+ }
2688
+
2689
+ // src/block-handle/DefaultBlockMenu.tsx
2690
+ import { useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useRef as useRef6, useState as useState6 } from "react";
2691
+ import { Fragment as Fragment3, jsx as jsx23, jsxs as jsxs9 } from "react/jsx-runtime";
2692
+ function DefaultBlockMenu({ position, actions, turnInto, onClose }) {
2693
+ const t = useT();
2694
+ const ref = useRef6(null);
2695
+ const [flip, setFlip] = useState6(false);
2696
+ const entries = [...actions, ...turnInto].filter((entry) => !entry.disabled);
2697
+ useEffect6(() => {
2698
+ ref.current?.querySelector('[role="menuitem"]:not([aria-disabled="true"])')?.focus();
2699
+ }, []);
2700
+ useLayoutEffect3(() => {
2701
+ const menu = ref.current;
2702
+ if (menu)
2703
+ setFlip(
2704
+ position.y + menu.offsetHeight > window.innerHeight && position.y - menu.offsetHeight > 0
2705
+ );
2706
+ }, [position.y]);
2707
+ const move = (delta) => {
2708
+ const items = [
2709
+ ...ref.current?.querySelectorAll(
2710
+ '[role="menuitem"]:not([aria-disabled="true"])'
2711
+ ) ?? []
2712
+ ];
2713
+ const index = items.indexOf(document.activeElement);
2714
+ items[(index + delta + items.length) % items.length]?.focus();
2715
+ };
2716
+ const renderItem = (action) => /* @__PURE__ */ jsxs9(
2717
+ "div",
2718
+ {
2719
+ role: "menuitem",
2720
+ tabIndex: action.disabled ? -1 : 0,
2721
+ "aria-disabled": action.disabled || void 0,
2722
+ "data-action": action.id,
2723
+ "data-inkbrick-part": "item",
2724
+ "data-disabled": action.disabled || void 0,
2725
+ "data-danger": action.danger || void 0,
2726
+ className: `inkbrick-block-menu__item${action.danger ? " inkbrick-block-menu__item--danger" : ""}`,
2727
+ onClick: () => !action.disabled && action.run(),
2728
+ onKeyDown: (event) => {
2729
+ if (event.key === "Enter" || event.key === " ") {
2730
+ event.preventDefault();
2731
+ if (!action.disabled) action.run();
2732
+ }
2733
+ },
2734
+ children: [
2735
+ action.icon !== void 0 && /* @__PURE__ */ jsx23("span", { className: "inkbrick-block-menu__icon", "data-inkbrick-part": "icon", children: action.icon }),
2736
+ action.title
2737
+ ]
2738
+ },
2739
+ action.id
2740
+ );
2741
+ return /* @__PURE__ */ jsxs9(
2742
+ "div",
2743
+ {
2744
+ ref,
2745
+ className: "inkbrick-block-menu",
2746
+ "data-inkbrick-ui": "block-menu",
2747
+ role: "menu",
2748
+ "aria-label": t("menu.label"),
2749
+ "data-testid": "block-menu",
2750
+ "data-inkbrick-block-ui": true,
2751
+ style: {
2752
+ left: Math.max(
2753
+ 8,
2754
+ Math.min(position.x, window.innerWidth - (ref.current?.offsetWidth ?? 232) - 8)
2755
+ ),
2756
+ top: flip ? Math.max(8, position.y - (ref.current?.offsetHeight ?? 0) - 32) : position.y
2757
+ },
2758
+ onKeyDown: (event) => {
2759
+ if (event.key === "ArrowDown") {
2760
+ event.preventDefault();
2761
+ move(1);
2762
+ } else if (event.key === "ArrowUp") {
2763
+ event.preventDefault();
2764
+ move(-1);
2765
+ } else if (event.key === "Escape") {
2766
+ event.preventDefault();
2767
+ onClose();
2768
+ }
2769
+ },
2770
+ children: [
2771
+ actions.map(renderItem),
2772
+ turnInto.length > 0 && /* @__PURE__ */ jsxs9(Fragment3, { children: [
2773
+ /* @__PURE__ */ jsx23("div", { className: "inkbrick-block-menu__label", "data-inkbrick-part": "label", role: "separator", children: t("menu.turnInto") }),
2774
+ turnInto.map(renderItem)
2775
+ ] }),
2776
+ entries.length === 0 && /* @__PURE__ */ jsx23("div", { className: "inkbrick-block-menu__label", "data-inkbrick-part": "label", children: t("menu.empty") })
2777
+ ]
2778
+ }
2779
+ );
2780
+ }
2781
+
2782
+ // src/block-handle/gripGesture.ts
2783
+ var DRAG_THRESHOLD = 5;
2784
+ var LONG_PRESS_MS = 350;
2785
+ var TOUCH_SLOP = 10;
2786
+ function startGripGesture(drag, blockId, event, { onTap }) {
2787
+ const touch = event.pointerType === "touch" || event.pointerType === "pen";
2788
+ const pointerId = event.pointerId;
2789
+ const startX = event.clientX;
2790
+ const startY = event.clientY;
2791
+ let dragging = false;
2792
+ let aborted = false;
2793
+ let timer;
2794
+ const begin = () => {
2795
+ if (dragging || aborted) return;
2796
+ dragging = true;
2797
+ drag.start(blockId);
2798
+ if (touch) navigator.vibrate?.(10);
2799
+ };
2800
+ const finish = () => {
2801
+ clearTimeout(timer);
2802
+ document.removeEventListener("pointermove", onMove);
2803
+ document.removeEventListener("pointerup", onUp);
2804
+ document.removeEventListener("pointercancel", onCancel);
2805
+ document.removeEventListener("keydown", onKey);
2806
+ };
2807
+ const onMove = (move) => {
2808
+ if (move.pointerId !== pointerId) return;
2809
+ const distance = Math.hypot(move.clientX - startX, move.clientY - startY);
2810
+ if (!dragging) {
2811
+ if (touch) {
2812
+ if (distance > TOUCH_SLOP) {
2813
+ aborted = true;
2814
+ finish();
2815
+ }
2816
+ return;
2817
+ }
2818
+ if (distance > DRAG_THRESHOLD) begin();
2819
+ }
2820
+ if (dragging) {
2821
+ move.preventDefault();
2822
+ drag.update(move.clientX, move.clientY);
2823
+ }
2824
+ };
2825
+ const onUp = (up) => {
2826
+ if (up.pointerId !== pointerId) return;
2827
+ finish();
2828
+ if (dragging) drag.drop();
2829
+ else if (!aborted) onTap();
2830
+ };
2831
+ const onCancel = (cancel) => {
2832
+ if (cancel.pointerId !== pointerId) return;
2833
+ abort();
2834
+ };
2835
+ const onKey = (key) => {
2836
+ if (key.key === "Escape") abort();
2837
+ };
2838
+ const abort = () => {
2839
+ aborted = true;
2840
+ finish();
2841
+ if (dragging) drag.cancel();
2842
+ };
2843
+ if (touch) timer = setTimeout(begin, LONG_PRESS_MS);
2844
+ document.addEventListener("pointermove", onMove, { passive: false });
2845
+ document.addEventListener("pointerup", onUp);
2846
+ document.addEventListener("pointercancel", onCancel);
2847
+ document.addEventListener("keydown", onKey);
2848
+ return abort;
2849
+ }
2850
+
2851
+ // src/block-handle/useBlockDrag.ts
2852
+ import { useSyncExternalStore as useSyncExternalStore8 } from "react";
2853
+
2854
+ // src/block-handle/blockDrag.ts
2855
+ import { flushSync as flushSync4 } from "react-dom";
2856
+
2857
+ // src/block-handle/geometry.ts
2858
+ var GUTTER = 72;
2859
+ function hasHandle(editor, blockId) {
2860
+ const block2 = editor.store.getBlock(blockId);
2861
+ const parent = block2?.parentId ? editor.store.getBlock(block2.parentId) : void 0;
2862
+ return !!parent?.parentId;
2863
+ }
2864
+ function blockElement(view, blockId) {
2865
+ return view.getRoot()?.querySelector(`[data-block-id="${CSS.escape(blockId)}"]`) ?? null;
2866
+ }
2867
+ function blockAtPoint(editor, view, x, y) {
2868
+ const root = view.getRoot();
2869
+ const hit = root ? document.elementFromPoint(x, y) : null;
2870
+ if (!root || !hit || !root.contains(hit)) return null;
2871
+ let node = hit.closest("[data-block-id]");
2872
+ while (node && root.contains(node)) {
2873
+ const id = node.getAttribute("data-block-id");
2874
+ if (hasHandle(editor, id)) return id;
2875
+ node = node.parentElement?.closest("[data-block-id]") ?? null;
2876
+ }
2877
+ return null;
2878
+ }
2879
+ function probePoint(view, x, y) {
2880
+ const root = view.getRoot();
2881
+ if (!root) return null;
2882
+ const rect = root.getBoundingClientRect();
2883
+ if (x < rect.left - GUTTER || x > rect.right || y < rect.top || y > rect.bottom) return null;
2884
+ return { x: Math.min(Math.max(x, rect.left + 12), rect.right - 12), y };
2885
+ }
2886
+ function rowRect(view, blockId) {
2887
+ return (view.getElement(blockId) ?? blockElement(view, blockId))?.getBoundingClientRect() ?? null;
2888
+ }
2889
+
2890
+ // src/block-handle/blockDrag.ts
2891
+ var EDGE = 56;
2892
+ var cache2 = /* @__PURE__ */ new WeakMap();
2893
+ function getBlockDrag(editor, view) {
2894
+ let api = cache2.get(view);
2895
+ if (!api) {
2896
+ api = createBlockDrag(editor, view);
2897
+ cache2.set(view, api);
2898
+ }
2899
+ return api;
2900
+ }
2901
+ function createBlockDrag(editor, view) {
2902
+ const controller = view.blocks;
2903
+ const markBody = (on) => {
2904
+ if (typeof document === "undefined") return;
2905
+ document.body.classList.toggle("inkbrick-dragging", on);
2906
+ if (on) document.body.setAttribute("data-inkbrick-dragging", "");
2907
+ else document.body.removeAttribute("data-inkbrick-dragging");
2908
+ };
2909
+ const canMove = (blockId, targetId, position) => blockId !== targetId && editor.can("moveBlock", { blockId, targetId, position });
2910
+ const blockRects = (blockId) => {
2911
+ const wrapper = blockElement(view, blockId);
2912
+ const row = rowRect(view, blockId);
2913
+ return wrapper && row ? { row, box: wrapper.getBoundingClientRect() } : null;
2914
+ };
2915
+ const api = {
2916
+ blockAtPoint(x, y) {
2917
+ const probe = probePoint(view, x, y);
2918
+ return probe ? blockAtPoint(editor, view, probe.x, probe.y) : null;
2919
+ },
2920
+ blockRects,
2921
+ canMove,
2922
+ dropTargetAt(blockId, x, y) {
2923
+ const targetId = api.blockAtPoint(x, y);
2924
+ const rects = targetId ? blockRects(targetId) : null;
2925
+ if (!targetId || !rects) return null;
2926
+ const { row, box } = rects;
2927
+ const position = y < row.top + row.height / 2 ? "before" : "after";
2928
+ if (!canMove(blockId, targetId, position)) return null;
2929
+ return {
2930
+ targetId,
2931
+ position,
2932
+ // «После» — после всего поддерева цели (вместе с вложенными блоками).
2933
+ indicator: {
2934
+ left: box.left,
2935
+ width: box.width,
2936
+ top: position === "before" ? row.top - 1 : box.bottom + 1
2937
+ }
2938
+ };
2939
+ },
2940
+ start(blockId) {
2941
+ controller.startDrag(blockId);
2942
+ markBody(true);
2943
+ },
2944
+ setTarget(target) {
2945
+ const drag = controller.getState().drag;
2946
+ if (!drag) return;
2947
+ if (!target || !canMove(drag.blockId, target.targetId, target.position)) {
2948
+ controller.updateDrag({ targetId: null, position: null, indicator: null });
2949
+ return;
2950
+ }
2951
+ controller.updateDrag({
2952
+ targetId: target.targetId,
2953
+ position: target.position,
2954
+ indicator: target.indicator
2955
+ });
2956
+ },
2957
+ update(x, y) {
2958
+ const drag = controller.getState().drag;
2959
+ if (!drag) return null;
2960
+ api.autoScroll(y);
2961
+ const target = api.dropTargetAt(drag.blockId, x, y);
2962
+ api.setTarget(target);
2963
+ return target;
2964
+ },
2965
+ drop() {
2966
+ const drag = controller.getState().drag;
2967
+ controller.endDrag();
2968
+ markBody(false);
2969
+ if (!drag?.targetId || !drag.position) return false;
2970
+ return api.move(drag.blockId, drag.targetId, drag.position);
2971
+ },
2972
+ cancel() {
2973
+ controller.endDrag();
2974
+ markBody(false);
2975
+ },
2976
+ move(blockId, targetId, position) {
2977
+ if (!canMove(blockId, targetId, position)) return false;
2978
+ const payload = { blockId, targetId, position };
2979
+ flushSync4(() => {
2980
+ editor.dispatch({ type: "moveBlock", payload });
2981
+ });
2982
+ controller.setHover(null);
2983
+ return true;
2984
+ },
2985
+ autoScroll(clientY) {
2986
+ if (typeof window === "undefined") return;
2987
+ if (clientY < EDGE) window.scrollBy(0, -14);
2988
+ else if (clientY > window.innerHeight - EDGE) window.scrollBy(0, 14);
2989
+ },
2990
+ getState: () => controller.getState().drag,
2991
+ subscribe: (listener) => {
2992
+ let last = controller.getState().drag;
2993
+ return controller.subscribe(() => {
2994
+ const next = controller.getState().drag;
2995
+ if (next !== last) {
2996
+ last = next;
2997
+ listener();
2998
+ }
2999
+ });
3000
+ }
3001
+ };
3002
+ return api;
3003
+ }
3004
+
3005
+ // src/block-handle/useBlockDrag.ts
3006
+ function useBlockDrag() {
3007
+ return getBlockDrag(useEditor(), useEditorView());
3008
+ }
3009
+ function useBlockDragState() {
3010
+ const drag = useBlockDrag();
3011
+ return useSyncExternalStore8(drag.subscribe, drag.getState, drag.getState);
3012
+ }
3013
+
3014
+ // src/block-handle/BlockHandleHost.tsx
3015
+ import { Fragment as Fragment4, jsx as jsx24, jsxs as jsxs10 } from "react/jsx-runtime";
3016
+ var GRIP_STYLE = Object.freeze({ touchAction: "none" });
3017
+ function BlockHandleHost() {
3018
+ const editor = useEditor();
3019
+ const view = useEditorView();
3020
+ const drag = useBlockDrag();
3021
+ const { slots, slashItems, t } = useEditorUI();
3022
+ const readOnly = useReadOnly();
3023
+ const state = useSyncExternalStore9(
3024
+ view.blocks.subscribe,
3025
+ view.blocks.getState,
3026
+ view.blocks.getState
3027
+ );
3028
+ const cleanup = useRef7(null);
3029
+ useEffect7(() => () => cleanup.current?.(), []);
3030
+ if (slots.BlockHandle === null || readOnly || typeof document === "undefined") return null;
3031
+ const { hover, menu } = state;
3032
+ const dragging = state.drag;
3033
+ const openMenuFor = (blockId) => {
3034
+ const anchor = view.blocks.getState().hover ?? hover;
3035
+ if (anchor) view.blocks.openMenu({ blockId, x: anchor.x, y: anchor.y + 30 });
3036
+ };
3037
+ const gripPropsFor = (blockId) => ({
3038
+ onPointerDown: (event) => {
3039
+ if (event.button !== 0) return;
3040
+ event.preventDefault();
3041
+ cleanup.current?.();
3042
+ const anchor = hover;
3043
+ const cancel = startGripGesture(drag, blockId, event.nativeEvent, {
3044
+ onTap: () => {
3045
+ cleanup.current = null;
3046
+ if (anchor) view.blocks.openMenu({ blockId, x: anchor.x, y: anchor.y + 30 });
3047
+ }
3048
+ });
3049
+ cleanup.current = cancel;
3050
+ },
3051
+ onContextMenu: (event) => event.preventDefault(),
3052
+ style: GRIP_STYLE,
3053
+ "data-inkbrick-grip": ""
3054
+ });
3055
+ const Handle = slots.BlockHandle ?? DefaultBlockHandle;
3056
+ const Menu = slots.BlockMenu ?? DefaultBlockMenu;
3057
+ const activeId = dragging?.blockId ?? hover?.blockId;
3058
+ const menuBlock = menu ? editor.store.getBlock(menu.blockId) : void 0;
3059
+ const menuContent = menu && menuBlock ? buildBlockActions(editor, view, menu.blockId, slashItems, t) : null;
3060
+ return createPortal(
3061
+ /* @__PURE__ */ jsxs10(Fragment4, { children: [
3062
+ hover && activeId && /* @__PURE__ */ jsx24(
3063
+ Handle,
3064
+ {
3065
+ blockId: activeId,
3066
+ position: { x: hover.x, y: hover.y },
3067
+ canAdd: editor.can("insertBlock", { flavour: "block:paragraph", afterId: activeId }),
3068
+ dragging: !!dragging,
3069
+ menuOpen: !!menu,
3070
+ onAdd: () => addBlockBelow(editor, view, activeId, slashItems),
3071
+ onOpenMenu: () => openMenuFor(activeId),
3072
+ gripProps: gripPropsFor(activeId)
3073
+ }
3074
+ ),
3075
+ menu && menuBlock && menuContent && /* @__PURE__ */ jsx24(
3076
+ Menu,
3077
+ {
3078
+ block: menuBlock,
3079
+ position: { x: menu.x, y: menu.y },
3080
+ actions: menuContent.actions,
3081
+ turnInto: menuContent.turnInto,
3082
+ onClose: () => {
3083
+ view.blocks.closeMenu();
3084
+ view.getRoot()?.focus({ preventScroll: true });
3085
+ }
3086
+ }
3087
+ )
3088
+ ] }),
3089
+ document.body
3090
+ );
3091
+ }
3092
+
3093
+ // src/block-handle/DropIndicatorHost.tsx
3094
+ import { useEffect as useEffect8 } from "react";
3095
+ import { createPortal as createPortal2 } from "react-dom";
3096
+ import { jsx as jsx25 } from "react/jsx-runtime";
3097
+ function DefaultDropIndicator({ indicator, position }) {
3098
+ return /* @__PURE__ */ jsx25(
3099
+ "div",
3100
+ {
3101
+ className: "inkbrick-drop-indicator",
3102
+ "data-inkbrick-ui": "drop-indicator",
3103
+ "data-testid": "drop-indicator",
3104
+ "data-position": position,
3105
+ style: { left: indicator.left, top: indicator.top, width: indicator.width }
3106
+ }
3107
+ );
3108
+ }
3109
+ function DropIndicatorHost() {
3110
+ const view = useEditorView();
3111
+ const { slots } = useEditorUI();
3112
+ const readOnly = useReadOnly();
3113
+ const drag = useBlockDragState();
3114
+ const draggedId = drag?.blockId;
3115
+ useEffect8(() => {
3116
+ if (!draggedId) return;
3117
+ const element = blockElement(view, draggedId);
3118
+ element?.setAttribute("data-inkbrick-dragging", "true");
3119
+ return () => element?.removeAttribute("data-inkbrick-dragging");
3120
+ }, [draggedId, view]);
3121
+ if (readOnly || slots.DropIndicator === null || typeof document === "undefined") return null;
3122
+ if (!drag?.indicator || !drag.targetId || !drag.position) return null;
3123
+ const Indicator = slots.DropIndicator ?? DefaultDropIndicator;
3124
+ return createPortal2(
3125
+ /* @__PURE__ */ jsx25(
3126
+ Indicator,
3127
+ {
3128
+ blockId: drag.blockId,
3129
+ targetId: drag.targetId,
3130
+ position: drag.position,
3131
+ indicator: drag.indicator
3132
+ }
3133
+ ),
3134
+ document.body
3135
+ );
3136
+ }
3137
+
3138
+ // src/clipboard/useClipboard.ts
3139
+ import { useEffect as useEffect9 } from "react";
3140
+ import { normalizeLinkUrl as normalizeLinkUrl2 } from "@inkbrick/document";
3141
+ import {
3142
+ FRAGMENT_MIME,
3143
+ fragmentToHtml,
3144
+ fragmentToPlainText,
3145
+ sanitizeFragment as sanitizeFragment2
3146
+ } from "@inkbrick/engine";
3147
+
3148
+ // src/editing/inputActions.ts
3149
+ import { flushSync as flushSync5 } from "react-dom";
3150
+ function createInputActions(editor, view, root, slashItems) {
3151
+ const exec = (intent) => editor.dispatch(intent);
3152
+ const run = (action) => {
3153
+ let caret;
3154
+ flushSync5(() => {
3155
+ caret = action();
3156
+ });
3157
+ if (caret) view.placeCaret(caret.blockId, caret.offset);
3158
+ syncSlash();
3159
+ };
3160
+ const syncSlash = () => syncSlashMenu(editor, view, slashItems);
3161
+ const blockText = (blockId) => {
3162
+ const block2 = editor.store.getBlock(blockId);
3163
+ return block2 ? editor.engine.inline.toPlainText(block2.content) : "";
3164
+ };
3165
+ const isSingleBlock = (selection) => selection.from.blockId === selection.to.blockId;
3166
+ const deleteSelection = (selection) => isSingleBlock(selection) ? exec({
3167
+ type: "deleteText",
3168
+ payload: {
3169
+ blockId: selection.from.blockId,
3170
+ from: selection.from.offset,
3171
+ to: selection.to.offset
3172
+ }
3173
+ }) : exec({
3174
+ type: "deleteRange",
3175
+ payload: { from: selection.from, to: selection.to }
3176
+ });
3177
+ const insertAt = (blockId, offset, text) => exec({
3178
+ type: "replaceText",
3179
+ payload: { blockId, from: offset, to: offset, text }
3180
+ });
3181
+ const replaceSelection = (selection, text) => {
3182
+ if (selection.collapsed) {
3183
+ return insertAt(selection.from.blockId, selection.from.offset, text);
3184
+ }
3185
+ return editor.batch(() => {
3186
+ const caret = deleteSelection(selection);
3187
+ return caret ? insertAt(caret.blockId, caret.offset, text) : void 0;
3188
+ });
3189
+ };
3190
+ const toggleFormat = (format) => {
3191
+ const selection = readDocSelection(root);
3192
+ if (selection) toggleFormatInSelection(editor, view, selection, format);
3193
+ };
3194
+ const undo = () => run(() => editor.undo()?.caret);
3195
+ const redo = () => run(() => editor.redo()?.caret);
3196
+ return {
3197
+ exec,
3198
+ run,
3199
+ syncSlash,
3200
+ blockText,
3201
+ deleteSelection,
3202
+ insertAt,
3203
+ replaceSelection,
3204
+ toggleFormat,
3205
+ undo,
3206
+ redo
3207
+ };
3208
+ }
3209
+
3210
+ // src/clipboard/useClipboard.ts
3211
+ var URL_ONLY = /^(https?:\/\/|mailto:)\S+$/i;
3212
+ function isForeignControl(_target) {
3213
+ const active = document.activeElement;
3214
+ return active instanceof Element && active.closest("input, textarea, select, [data-inkbrick-ignore-input]") !== null;
3215
+ }
3216
+ function parseJson(text) {
3217
+ try {
3218
+ return JSON.parse(text);
3219
+ } catch {
3220
+ return null;
3221
+ }
3222
+ }
3223
+ function useClipboard(ref, readOnly) {
3224
+ const editor = useEditor();
3225
+ const view = useEditorView();
3226
+ const { images, slashItems } = useEditorUI();
3227
+ useEffect9(() => {
3228
+ const root = ref.current;
3229
+ if (!root) return;
3230
+ let plainNext = false;
3231
+ let plainTimer;
3232
+ const { exec, run, deleteSelection, insertAt } = createInputActions(
3233
+ editor,
3234
+ view,
3235
+ root,
3236
+ slashItems
3237
+ );
3238
+ const writeSelection = (event, cut) => {
3239
+ if (isForeignControl(event.target)) return;
3240
+ const selection = readDocSelection(root);
3241
+ if (!selection || selection.collapsed || !event.clipboardData) return;
3242
+ const fragment = editor.copyFragment(selection.from, selection.to);
3243
+ if (fragment.blocks.length === 0) return;
3244
+ const json = JSON.stringify(fragment);
3245
+ event.clipboardData.setData("text/plain", fragmentToPlainText(fragment));
3246
+ event.clipboardData.setData("text/html", fragmentToHtml(fragment, encodeURIComponent(json)));
3247
+ event.clipboardData.setData(FRAGMENT_MIME, json);
3248
+ event.preventDefault();
3249
+ if (cut && !readOnly) run(() => editor.batch(() => deleteSelection(selection)));
3250
+ };
3251
+ const pastePlain = (text, selection) => {
3252
+ const trimmed = text.trim();
3253
+ if (!selection.collapsed && selection.from.blockId === selection.to.blockId && URL_ONLY.test(trimmed) && normalizeLinkUrl2(trimmed) && editor.store.getBlock(selection.from.blockId)?.flavour !== "block:code" && setLinkInSelection(editor, view, selection, trimmed)) {
3254
+ return;
3255
+ }
3256
+ const lines = text.replace(/\r\n?/g, "\n").split("\n");
3257
+ run(
3258
+ () => editor.batch(() => {
3259
+ let caret = selection.collapsed ? { blockId: selection.from.blockId, offset: selection.from.offset } : deleteSelection(selection);
3260
+ if (!caret) return void 0;
3261
+ const flavour = editor.store.getBlock(caret.blockId)?.flavour ?? "";
3262
+ if (editor.schema.getBlockSchema(flavour)?.enter?.mode === "newline") {
3263
+ return insertAt(caret.blockId, caret.offset, lines.join("\n"));
3264
+ }
3265
+ caret = insertAt(caret.blockId, caret.offset, lines[0]);
3266
+ for (const line of lines.slice(1)) {
3267
+ if (!caret) return void 0;
3268
+ const next = exec({
3269
+ type: "splitBlock",
3270
+ payload: { blockId: caret.blockId, offset: caret.offset }
3271
+ });
3272
+ if (!next) return caret;
3273
+ caret = insertAt(next.blockId, next.offset, line);
3274
+ }
3275
+ return caret;
3276
+ })
3277
+ );
3278
+ };
3279
+ const pasteFragment = (fragment, selection) => {
3280
+ if (!editor.can("pasteFragment", { at: selection.from, fragment })) return false;
3281
+ run(
3282
+ () => editor.batch(() => {
3283
+ const caret = selection.collapsed ? { blockId: selection.from.blockId, offset: selection.from.offset } : deleteSelection(selection);
3284
+ return caret ? exec({ type: "pasteFragment", payload: { at: caret, fragment } }) : void 0;
3285
+ })
3286
+ );
3287
+ return true;
3288
+ };
3289
+ const onPaste = (event) => {
3290
+ if (isForeignControl(event.target)) return;
3291
+ event.preventDefault();
3292
+ const data = event.clipboardData;
3293
+ const selection = readDocSelection(root);
3294
+ if (readOnly || !data || !selection) return;
3295
+ const text = data.getData("text/plain");
3296
+ const files = [...data.files].filter((file) => isAcceptedImage(file, images));
3297
+ if (files.length > 0 && text.trim() === "") {
3298
+ plainNext = false;
3299
+ insertImageFiles(editor, view, images, files, selection.to.blockId);
3300
+ return;
3301
+ }
3302
+ const plainOnly = plainNext;
3303
+ plainNext = false;
3304
+ const inCode = editor.store.getBlock(selection.from.blockId)?.flavour === "block:code";
3305
+ if (!plainOnly && !inCode) {
3306
+ const json = data.getData(FRAGMENT_MIME);
3307
+ const html = data.getData("text/html");
3308
+ const fragment = (json ? sanitizeFragment2(parseJson(json)) : null) ?? (html ? embeddedFragment(html) ?? htmlToFragment(html) : null);
3309
+ if (fragment && pasteFragment(fragment, selection)) return;
3310
+ }
3311
+ if (text) pastePlain(text, selection);
3312
+ };
3313
+ const onKeyDown = (event) => {
3314
+ if (readOnly && event.key === "Backspace" && !isForeignControl(event.target)) {
3315
+ event.preventDefault();
3316
+ return;
3317
+ }
3318
+ if (readOnly && (event.ctrlKey || event.metaKey) && !event.shiftKey && event.key.toLowerCase() === "a") {
3319
+ const selection = globalThis.getSelection();
3320
+ event.preventDefault();
3321
+ selection?.removeAllRanges();
3322
+ const range = document.createRange();
3323
+ range.selectNodeContents(root);
3324
+ selection?.addRange(range);
3325
+ return;
3326
+ }
3327
+ if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.key.toLowerCase() === "v") {
3328
+ plainNext = true;
3329
+ clearTimeout(plainTimer);
3330
+ plainTimer = setTimeout(() => {
3331
+ plainNext = false;
3332
+ }, 1e3);
3333
+ }
3334
+ };
3335
+ const hasFiles = (event) => !!event.dataTransfer && [...event.dataTransfer.types].includes("Files");
3336
+ const onDragOver = (event) => {
3337
+ if (hasFiles(event)) event.preventDefault();
3338
+ };
3339
+ const onDrop = (event) => {
3340
+ if (!hasFiles(event)) return;
3341
+ event.preventDefault();
3342
+ const files = [...event.dataTransfer?.files ?? []].filter(
3343
+ (file) => isAcceptedImage(file, images)
3344
+ );
3345
+ if (readOnly || files.length === 0) return;
3346
+ const probe = probePoint(view, event.clientX, event.clientY);
3347
+ const target = (probe ? blockAtPoint(editor, view, probe.x, probe.y) : null) ?? readDocSelection(root)?.to.blockId;
3348
+ if (target) insertImageFiles(editor, view, images, files, target);
3349
+ };
3350
+ const onCopy = (event) => writeSelection(event, false);
3351
+ const onCut = (event) => writeSelection(event, true);
3352
+ root.addEventListener("copy", onCopy);
3353
+ root.addEventListener("cut", onCut);
3354
+ root.addEventListener("paste", onPaste);
3355
+ root.addEventListener("keydown", onKeyDown);
3356
+ root.addEventListener("dragover", onDragOver);
3357
+ root.addEventListener("drop", onDrop);
3358
+ return () => {
3359
+ clearTimeout(plainTimer);
3360
+ root.removeEventListener("copy", onCopy);
3361
+ root.removeEventListener("cut", onCut);
3362
+ root.removeEventListener("paste", onPaste);
3363
+ root.removeEventListener("keydown", onKeyDown);
3364
+ root.removeEventListener("dragover", onDragOver);
3365
+ root.removeEventListener("drop", onDrop);
3366
+ };
3367
+ }, [editor, view, ref, readOnly, images, slashItems]);
3368
+ }
3369
+
3370
+ // src/block-handle/useBlockHandles.ts
3371
+ import { useEffect as useEffect10 } from "react";
3372
+ function useBlockHandles(ref, enabled) {
3373
+ const editor = useEditor();
3374
+ const view = useEditorView();
3375
+ useEffect10(() => {
3376
+ const root = ref.current;
3377
+ if (!root || !enabled) return;
3378
+ const showAt = (event) => {
3379
+ const probe = probePoint(view, event.clientX, event.clientY);
3380
+ if (!probe) {
3381
+ view.blocks.setHover(null);
3382
+ return;
3383
+ }
3384
+ const blockId = blockAtPoint(editor, view, probe.x, probe.y);
3385
+ if (!blockId) return;
3386
+ const wrapper = blockElement(view, blockId);
3387
+ const row = rowRect(view, blockId);
3388
+ if (wrapper && row) {
3389
+ view.blocks.setHover({
3390
+ blockId,
3391
+ x: Math.round(wrapper.getBoundingClientRect().left),
3392
+ y: Math.round(row.top)
3393
+ });
3394
+ }
3395
+ };
3396
+ const onUiTarget = (event) => event.target instanceof Element && !!event.target.closest("[data-inkbrick-block-ui]");
3397
+ const onMove = (event) => {
3398
+ const { menu, drag } = view.blocks.getState();
3399
+ if (event.pointerType === "touch") return;
3400
+ if (drag || menu || view.pointerDown) return;
3401
+ if (onUiTarget(event)) return;
3402
+ showAt(event);
3403
+ };
3404
+ const onPointerUp = (event) => {
3405
+ if (event.pointerType === "mouse" || onUiTarget(event)) return;
3406
+ const { menu, drag } = view.blocks.getState();
3407
+ if (!drag && !menu) showAt(event);
3408
+ };
3409
+ const onPointerDown = (event) => {
3410
+ if (onUiTarget(event)) return;
3411
+ view.blocks.closeMenu();
3412
+ };
3413
+ const hide = (event) => {
3414
+ if (onUiTarget(event)) return;
3415
+ if (view.blocks.getState().drag) return;
3416
+ view.blocks.setHover(null);
3417
+ view.blocks.closeMenu();
3418
+ };
3419
+ const onKeyDown = (event) => {
3420
+ if (event.key === "Escape") view.blocks.closeMenu();
3421
+ if (view.blocks.getState().hover && !event.metaKey && !event.ctrlKey)
3422
+ view.blocks.setHover(null);
3423
+ };
3424
+ const onLeave = (event) => {
3425
+ if (event.pointerType === "touch") return;
3426
+ if (!event.relatedTarget) view.blocks.setHover(null);
3427
+ };
3428
+ const unsubscribe = editor.store.subscribe(() => {
3429
+ const { hover, menu } = view.blocks.getState();
3430
+ if (hover && !hasHandle(editor, hover.blockId)) view.blocks.setHover(null);
3431
+ if (menu && !hasHandle(editor, menu.blockId)) view.blocks.closeMenu();
3432
+ });
3433
+ document.addEventListener("pointermove", onMove);
3434
+ document.addEventListener("pointerdown", onPointerDown);
3435
+ document.addEventListener("pointerup", onPointerUp);
3436
+ document.addEventListener("pointerout", onLeave);
3437
+ document.addEventListener("keydown", onKeyDown);
3438
+ window.addEventListener("scroll", hide, true);
3439
+ return () => {
3440
+ unsubscribe();
3441
+ document.removeEventListener("pointermove", onMove);
3442
+ document.removeEventListener("pointerdown", onPointerDown);
3443
+ document.removeEventListener("pointerup", onPointerUp);
3444
+ document.removeEventListener("pointerout", onLeave);
3445
+ document.removeEventListener("keydown", onKeyDown);
3446
+ window.removeEventListener("scroll", hide, true);
3447
+ view.blocks.setHover(null);
3448
+ view.blocks.closeMenu();
3449
+ };
3450
+ }, [editor, view, ref, enabled]);
3451
+ }
3452
+
3453
+ // src/editing/useEditorInput.ts
3454
+ import { useEffect as useEffect11 } from "react";
3455
+
3456
+ // src/editing/beforeInput.ts
3457
+ var FORMAT_BY_INPUT_TYPE = {
3458
+ formatBold: "bold",
3459
+ formatItalic: "italic",
3460
+ formatUnderline: "underline",
3461
+ formatStrikeThrough: "strike"
3462
+ };
3463
+ function matchShortcut(prefix) {
3464
+ if (/^#{1,6}$/.test(prefix)) {
3465
+ return { flavour: "block:heading", props: { level: prefix.length } };
3466
+ }
3467
+ if (prefix === ">") return { flavour: "block:quote", props: {} };
3468
+ if (prefix === "```") return { flavour: "block:code", props: {} };
3469
+ if (prefix === "---") return { flavour: "block:divider", props: {} };
3470
+ if (prefix === "$$") return { flavour: "block:formula", props: {} };
3471
+ if (/^[-*+]$/.test(prefix)) return { flavour: "block:list", props: { type: "bullet" } };
3472
+ if (/^\d+\.$/.test(prefix)) return { flavour: "block:list", props: { type: "ordered" } };
3473
+ if (prefix === "[]") return { flavour: "block:list", props: { type: "todo", checked: false } };
3474
+ return null;
3475
+ }
3476
+ function applyBeforeInput(event, root, editor, view, slashItems, actions) {
3477
+ const {
3478
+ exec,
3479
+ run,
3480
+ syncSlash,
3481
+ blockText,
3482
+ deleteSelection,
3483
+ replaceSelection,
3484
+ toggleFormat,
3485
+ undo,
3486
+ redo
3487
+ } = actions;
3488
+ const selection = readDocSelection(root);
3489
+ if (!selection) return;
3490
+ const { from, collapsed } = selection;
3491
+ const type = event.inputType;
3492
+ const block2 = editor.store.getBlock(from.blockId);
3493
+ const text = blockText(from.blockId);
3494
+ switch (type) {
3495
+ case "insertText":
3496
+ case "insertReplacementText": {
3497
+ const data = event.data ?? event.dataTransfer?.getData("text/plain") ?? "";
3498
+ if (!data) return;
3499
+ if (data === "$" && collapsed && block2 && block2.flavour !== "block:code") {
3500
+ const formula = matchInlineFormula(text.slice(0, from.offset));
3501
+ if (formula && editor.can("insertFormula", {
3502
+ blockId: from.blockId,
3503
+ from: formula.from,
3504
+ to: from.offset,
3505
+ latex: formula.latex
3506
+ })) {
3507
+ run(
3508
+ () => exec({
3509
+ type: "insertFormula",
3510
+ payload: {
3511
+ blockId: from.blockId,
3512
+ from: formula.from,
3513
+ to: from.offset,
3514
+ latex: formula.latex
3515
+ }
3516
+ })
3517
+ );
3518
+ return;
3519
+ }
3520
+ }
3521
+ if (data === " " && collapsed && block2?.flavour === "block:paragraph") {
3522
+ const shortcut = matchShortcut(text.slice(0, from.offset));
3523
+ if (shortcut) {
3524
+ run(
3525
+ () => editor.batch(() => {
3526
+ exec({
3527
+ type: "deleteText",
3528
+ payload: { blockId: from.blockId, from: 0, to: from.offset }
3529
+ });
3530
+ return exec({
3531
+ type: "convertBlock",
3532
+ payload: { blockId: from.blockId, ...shortcut, offset: 0 }
3533
+ });
3534
+ })
3535
+ );
3536
+ return;
3537
+ }
3538
+ }
3539
+ const opensSlash = data === "/" && collapsed && !!block2 && block2.flavour !== "block:code" && editor.schema.getBlockSchema(block2.flavour)?.inline !== false && (from.offset === 0 || /\s/.test(text[from.offset - 1])) && availableSlashItems(editor, slashItems, from.blockId).length > 0;
3540
+ run(() => replaceSelection(selection, data));
3541
+ if (opensSlash) {
3542
+ view.slash.open(from.blockId, from.offset);
3543
+ syncSlash();
3544
+ }
3545
+ return;
3546
+ }
3547
+ case "insertParagraph":
3548
+ run(
3549
+ () => editor.batch(() => {
3550
+ const caret = collapsed ? { blockId: from.blockId, offset: from.offset } : deleteSelection(selection);
3551
+ return caret ? exec({
3552
+ type: "splitBlock",
3553
+ payload: { blockId: caret.blockId, offset: caret.offset }
3554
+ }) : void 0;
3555
+ })
3556
+ );
3557
+ return;
3558
+ case "insertLineBreak":
3559
+ run(() => replaceSelection(selection, "\n"));
3560
+ return;
3561
+ case "deleteContentBackward":
3562
+ run(
3563
+ () => collapsed ? exec({
3564
+ type: "deleteBackward",
3565
+ payload: { blockId: from.blockId, offset: from.offset }
3566
+ }) : deleteSelection(selection)
3567
+ );
3568
+ return;
3569
+ case "deleteContentForward":
3570
+ run(() => {
3571
+ if (!collapsed) return deleteSelection(selection);
3572
+ if (from.offset < text.length) {
3573
+ return exec({
3574
+ type: "deleteText",
3575
+ payload: { blockId: from.blockId, from: from.offset, to: from.offset + 1 }
3576
+ });
3577
+ }
3578
+ return deleteForwardAtEnd(editor, from.blockId);
3579
+ });
3580
+ return;
3581
+ case "deleteWordBackward":
3582
+ run(() => {
3583
+ if (!collapsed) return deleteSelection(selection);
3584
+ const start = text.slice(0, from.offset).replace(/(\S+\s*|\s+)$/, "").length;
3585
+ return start < from.offset ? exec({
3586
+ type: "deleteText",
3587
+ payload: { blockId: from.blockId, from: start, to: from.offset }
3588
+ }) : exec({
3589
+ type: "deleteBackward",
3590
+ payload: { blockId: from.blockId, offset: from.offset }
3591
+ });
3592
+ });
3593
+ return;
3594
+ case "deleteWordForward":
3595
+ run(() => {
3596
+ if (!collapsed) return deleteSelection(selection);
3597
+ const rest = text.slice(from.offset);
3598
+ const length = rest.length - rest.replace(/^(\s*[^\s]+|\s+)/, "").length;
3599
+ return length > 0 ? exec({
3600
+ type: "deleteText",
3601
+ payload: { blockId: from.blockId, from: from.offset, to: from.offset + length }
3602
+ }) : void 0;
3603
+ });
3604
+ return;
3605
+ case "deleteSoftLineBackward":
3606
+ case "deleteHardLineBackward":
3607
+ run(() => {
3608
+ if (!collapsed) return deleteSelection(selection);
3609
+ return from.offset > 0 ? exec({
3610
+ type: "deleteText",
3611
+ payload: { blockId: from.blockId, from: 0, to: from.offset }
3612
+ }) : void 0;
3613
+ });
3614
+ return;
3615
+ case "deleteSoftLineForward":
3616
+ case "deleteHardLineForward":
3617
+ run(() => {
3618
+ if (!collapsed) return deleteSelection(selection);
3619
+ return from.offset < text.length ? exec({
3620
+ type: "deleteText",
3621
+ payload: { blockId: from.blockId, from: from.offset, to: text.length }
3622
+ }) : void 0;
3623
+ });
3624
+ return;
3625
+ case "deleteByCut":
3626
+ case "deleteByDrag":
3627
+ case "deleteContent":
3628
+ if (!collapsed) run(() => deleteSelection(selection));
3629
+ return;
3630
+ case "historyUndo":
3631
+ undo();
3632
+ return;
3633
+ case "historyRedo":
3634
+ redo();
3635
+ return;
3636
+ case "formatBold":
3637
+ case "formatItalic":
3638
+ case "formatUnderline":
3639
+ case "formatStrikeThrough":
3640
+ toggleFormat(FORMAT_BY_INPUT_TYPE[type]);
3641
+ return;
3642
+ default:
3643
+ return;
3644
+ }
3645
+ }
3646
+
3647
+ // src/editing/useEditorInput.ts
3648
+ var NAVIGATION_KEYS = /* @__PURE__ */ new Set([
3649
+ "ArrowLeft",
3650
+ "ArrowRight",
3651
+ "ArrowUp",
3652
+ "ArrowDown",
3653
+ "Home",
3654
+ "End",
3655
+ "PageUp",
3656
+ "PageDown"
3657
+ ]);
3658
+ function diffText(before, after) {
3659
+ let start = 0;
3660
+ while (start < before.length && start < after.length && before[start] === after[start]) start++;
3661
+ let endBefore = before.length;
3662
+ let endAfter = after.length;
3663
+ while (endBefore > start && endAfter > start && before[endBefore - 1] === after[endAfter - 1]) {
3664
+ endBefore--;
3665
+ endAfter--;
3666
+ }
3667
+ return { from: start, to: endBefore, text: after.slice(start, endAfter) };
3668
+ }
3669
+ function isForeignControl2(target) {
3670
+ return target instanceof Element && target.closest("input, textarea, select, button, [data-inkbrick-ignore-input]") !== null;
3671
+ }
3672
+ function useEditorInput(ref, enabled = true) {
3673
+ const editor = useEditor();
3674
+ const view = useEditorView();
3675
+ const { slashItems, keymap } = useEditorUI();
3676
+ useEffect11(() => {
3677
+ const root = ref.current;
3678
+ if (!root || !enabled) return;
3679
+ let composing = false;
3680
+ const actions = createInputActions(editor, view, root, slashItems);
3681
+ const { exec, run, syncSlash, blockText, replaceSelection, toggleFormat, undo, redo } = actions;
3682
+ const onBeforeInput = (event) => {
3683
+ if (isForeignControl2(event.target)) return;
3684
+ if (composing || event.isComposing) return;
3685
+ event.preventDefault();
3686
+ applyBeforeInput(event, root, editor, view, slashItems, actions);
3687
+ };
3688
+ const onKeyDown = (event) => {
3689
+ if (isForeignControl2(event.target)) return;
3690
+ const handled = handleKeyDown(keymap, {
3691
+ editor,
3692
+ view,
3693
+ root,
3694
+ event,
3695
+ slashItems,
3696
+ selection: () => readDocSelection(root),
3697
+ exec,
3698
+ run,
3699
+ replaceSelection,
3700
+ undo,
3701
+ redo,
3702
+ toggleFormat
3703
+ });
3704
+ if (!handled && NAVIGATION_KEYS.has(event.key)) editor.sealHistory();
3705
+ };
3706
+ const editableAtCaret = () => {
3707
+ const node = document.getSelection()?.anchorNode ?? null;
3708
+ const element = node instanceof Element ? node : node?.parentElement;
3709
+ const editable = element?.closest(".editable[data-id]");
3710
+ return editable && root.contains(editable) ? editable : null;
3711
+ };
3712
+ let compositionBlock = null;
3713
+ const onCompositionStart = () => {
3714
+ composing = true;
3715
+ compositionBlock = editableAtCaret();
3716
+ compositionBlock?.setAttribute("data-composing", "");
3717
+ };
3718
+ const onCompositionEnd = () => {
3719
+ composing = false;
3720
+ const started = compositionBlock?.isConnected ? compositionBlock : null;
3721
+ const element = started ?? editableAtCaret();
3722
+ const blockId = element?.dataset.id;
3723
+ compositionBlock?.removeAttribute("data-composing");
3724
+ compositionBlock = null;
3725
+ if (!element || !blockId) return;
3726
+ const { from, to, text } = diffText(blockText(blockId), element.textContent ?? "");
3727
+ if (from === to && text === "") return;
3728
+ run(() => exec({ type: "replaceText", payload: { blockId, from, to, text } }));
3729
+ };
3730
+ const prevent = (event) => event.preventDefault();
3731
+ const sealHistory = () => editor.sealHistory();
3732
+ const closeSlash = () => view.slash.close();
3733
+ const onPointerDown = () => {
3734
+ view.pointerDown = true;
3735
+ view.toolbar.closeLink();
3736
+ view.toolbar.hide();
3737
+ };
3738
+ const onPointerUp = () => {
3739
+ if (!view.pointerDown) return;
3740
+ view.pointerDown = false;
3741
+ setTimeout(() => syncToolbar(editor, view), 0);
3742
+ };
3743
+ const onClick = (event) => {
3744
+ const formula = event.target instanceof Element ? event.target.closest("[data-inkbrick-formula]") : null;
3745
+ if (formula && root.contains(formula)) {
3746
+ event.preventDefault();
3747
+ openFormulaAt(view, formula);
3748
+ return;
3749
+ }
3750
+ if (!(event.ctrlKey || event.metaKey) || !(event.target instanceof Element)) return;
3751
+ const anchor = event.target.closest("a[href]");
3752
+ if (anchor && root.contains(anchor)) {
3753
+ event.preventDefault();
3754
+ window.open(anchor.href, "_blank", "noopener,noreferrer");
3755
+ }
3756
+ };
3757
+ const onSelectionChange = () => {
3758
+ if (view.slash.getState().open) syncSlash();
3759
+ syncToolbar(editor, view);
3760
+ };
3761
+ const syncIfOpen = () => {
3762
+ if (view.slash.getState().open) syncSlash();
3763
+ if (view.toolbar.getState().visible) syncToolbar(editor, view);
3764
+ };
3765
+ root.addEventListener("beforeinput", onBeforeInput);
3766
+ root.addEventListener("keydown", onKeyDown);
3767
+ root.addEventListener("compositionstart", onCompositionStart);
3768
+ root.addEventListener("compositionend", onCompositionEnd);
3769
+ root.addEventListener("drop", prevent);
3770
+ root.addEventListener("pointerdown", sealHistory);
3771
+ root.addEventListener("focusout", sealHistory);
3772
+ root.addEventListener("pointerdown", closeSlash);
3773
+ root.addEventListener("focusout", closeSlash);
3774
+ root.addEventListener("pointerdown", onPointerDown);
3775
+ root.addEventListener("click", onClick);
3776
+ document.addEventListener("pointerup", onPointerUp);
3777
+ document.addEventListener("pointercancel", onPointerUp);
3778
+ document.addEventListener("selectionchange", onSelectionChange);
3779
+ window.addEventListener("scroll", syncIfOpen, true);
3780
+ return () => {
3781
+ root.removeEventListener("beforeinput", onBeforeInput);
3782
+ root.removeEventListener("keydown", onKeyDown);
3783
+ root.removeEventListener("compositionstart", onCompositionStart);
3784
+ root.removeEventListener("compositionend", onCompositionEnd);
3785
+ root.removeEventListener("drop", prevent);
3786
+ root.removeEventListener("pointerdown", sealHistory);
3787
+ root.removeEventListener("focusout", sealHistory);
3788
+ root.removeEventListener("pointerdown", closeSlash);
3789
+ root.removeEventListener("focusout", closeSlash);
3790
+ root.removeEventListener("pointerdown", onPointerDown);
3791
+ root.removeEventListener("click", onClick);
3792
+ document.removeEventListener("pointerup", onPointerUp);
3793
+ document.removeEventListener("pointercancel", onPointerUp);
3794
+ document.removeEventListener("selectionchange", onSelectionChange);
3795
+ window.removeEventListener("scroll", syncIfOpen, true);
3796
+ compositionBlock?.removeAttribute("data-composing");
3797
+ view.slash.close();
3798
+ view.toolbar.hide();
3799
+ };
3800
+ }, [editor, view, ref, slashItems, keymap, enabled]);
3801
+ }
3802
+
3803
+ // src/editing/useSelectionState.ts
3804
+ import { useEffect as useEffect12 } from "react";
3805
+ function useSelectionState(ref) {
3806
+ const editor = useEditor();
3807
+ const view = useEditorView();
3808
+ useEffect12(() => {
3809
+ const root = ref.current;
3810
+ if (!root) return;
3811
+ const update = () => {
3812
+ const selection = readDocSelection(root);
3813
+ view.selection.set(
3814
+ selection && !selection.collapsed ? editor.blocksBetween(selection.from, selection.to) : []
3815
+ );
3816
+ const caret = selection?.collapsed ? selection.from.blockId : null;
3817
+ view.caret.set(caret);
3818
+ editor.controllers.setFocusedBlock(caret);
3819
+ view.viewport.setEdges(
3820
+ !selection ? [] : selection.collapsed ? [selection.from.blockId] : [selection.from.blockId, selection.to.blockId]
3821
+ );
3822
+ };
3823
+ document.addEventListener("selectionchange", update);
3824
+ update();
3825
+ return () => {
3826
+ document.removeEventListener("selectionchange", update);
3827
+ view.selection.set([]);
3828
+ view.caret.set(null);
3829
+ view.viewport.setEdges([]);
3830
+ editor.controllers.setFocusedBlock(null);
3831
+ };
3832
+ }, [editor, view, ref]);
3833
+ }
3834
+
3835
+ // src/formula/FormulaPopover.tsx
3836
+ import {
3837
+ useCallback as useCallback4,
3838
+ useEffect as useEffect13,
3839
+ useLayoutEffect as useLayoutEffect4,
3840
+ useRef as useRef8,
3841
+ useState as useState7,
3842
+ useSyncExternalStore as useSyncExternalStore10
3843
+ } from "react";
3844
+ import { createPortal as createPortal3 } from "react-dom";
3845
+ import { MAX_FORMULA_LENGTH as MAX_FORMULA_LENGTH2 } from "@inkbrick/document";
3846
+ import { jsx as jsx26, jsxs as jsxs11 } from "react/jsx-runtime";
3847
+ var WIDTH = 340;
3848
+ function close(view, state) {
3849
+ view.formula.close();
3850
+ view.placeSelection(
3851
+ { blockId: state.blockId, offset: state.mode === "edit" ? state.to : state.from },
3852
+ { blockId: state.blockId, offset: state.to }
3853
+ );
3854
+ }
3855
+ function Body({ state }) {
3856
+ const editor = useEditor();
3857
+ const view = useEditorView();
3858
+ const t = useT();
3859
+ const [draft, setDraft] = useState7(state.latex);
3860
+ const [error, setError] = useState7(false);
3861
+ const ref = useRef8(null);
3862
+ const field = useRef8(null);
3863
+ const [top, setTop] = useState7(null);
3864
+ const insertSnippet = useSnippetInsert(field, draft, setDraft);
3865
+ const settle = useCallback4((result) => setError(result === "error"), []);
3866
+ const width = Math.min(WIDTH, window.innerWidth - 16);
3867
+ useEffect13(() => {
3868
+ field.current?.focus();
3869
+ field.current?.select();
3870
+ }, []);
3871
+ useLayoutEffect4(() => {
3872
+ const height = ref.current?.offsetHeight ?? 0;
3873
+ const below = state.position.y + 8;
3874
+ setTop(
3875
+ below + height > window.innerHeight && state.position.top - height - 8 > 0 ? state.position.top - height - 8 : below
3876
+ );
3877
+ }, [state.position.y, state.position.top, draft]);
3878
+ useEffect13(() => {
3879
+ const onPointerDown = (event) => {
3880
+ if (event.target instanceof Node && ref.current?.contains(event.target)) return;
3881
+ close(view, state);
3882
+ };
3883
+ document.addEventListener("pointerdown", onPointerDown, true);
3884
+ return () => document.removeEventListener("pointerdown", onPointerDown, true);
3885
+ }, [view, state]);
3886
+ const apply = () => {
3887
+ if (applyInlineFormula(editor, view, state, draft)) view.formula.close();
3888
+ };
3889
+ const remove = () => {
3890
+ if (applyInlineFormula(editor, view, state, "")) view.formula.close();
3891
+ };
3892
+ return /* @__PURE__ */ jsxs11(
3893
+ "div",
3894
+ {
3895
+ ref,
3896
+ className: "formula-popover",
3897
+ "data-inkbrick-ui": "formula-popover",
3898
+ "data-testid": "formula-popover",
3899
+ role: "dialog",
3900
+ "aria-label": t("formula.inline.title"),
3901
+ style: {
3902
+ left: Math.max(8, Math.min(state.position.x - width / 2, window.innerWidth - width - 8)),
3903
+ top: top ?? state.position.y + 8,
3904
+ width
3905
+ },
3906
+ onMouseDown: (event) => {
3907
+ if (!(event.target instanceof HTMLTextAreaElement)) event.preventDefault();
3908
+ },
3909
+ children: [
3910
+ /* @__PURE__ */ jsx26(
3911
+ "textarea",
3912
+ {
3913
+ ref: field,
3914
+ className: "formula-popover__source",
3915
+ "data-inkbrick-part": "formula-source",
3916
+ "aria-label": t("formula.source"),
3917
+ "aria-invalid": error || void 0,
3918
+ placeholder: t("formula.placeholder"),
3919
+ spellCheck: false,
3920
+ rows: 2,
3921
+ maxLength: MAX_FORMULA_LENGTH2,
3922
+ value: draft,
3923
+ onChange: (event) => setDraft(event.target.value),
3924
+ onKeyDown: (event) => {
3925
+ if (event.key === "Escape") {
3926
+ event.preventDefault();
3927
+ close(view, state);
3928
+ } else if (event.key === "Enter" && !event.shiftKey) {
3929
+ event.preventDefault();
3930
+ apply();
3931
+ }
3932
+ }
3933
+ }
3934
+ ),
3935
+ /* @__PURE__ */ jsx26("div", { className: "formula-popover__preview", "data-inkbrick-part": "formula-preview", children: /* @__PURE__ */ jsx26(FormulaView, { latex: draft, showError: true, onSettled: settle }) }),
3936
+ /* @__PURE__ */ jsx26(LazyFormulaPalette, { onInsert: insertSnippet }),
3937
+ /* @__PURE__ */ jsxs11("div", { className: "formula-popover__actions", "data-inkbrick-part": "formula-actions", children: [
3938
+ state.mode === "edit" && /* @__PURE__ */ jsx26(
3939
+ "button",
3940
+ {
3941
+ type: "button",
3942
+ "data-inkbrick-part": "button",
3943
+ "data-danger": "",
3944
+ "data-action": "remove",
3945
+ "aria-label": t("formula.remove"),
3946
+ title: t("formula.remove"),
3947
+ onClick: remove,
3948
+ children: /* @__PURE__ */ jsx26(Icon, { name: "trash" })
3949
+ }
3950
+ ),
3951
+ /* @__PURE__ */ jsx26("span", { className: "formula-popover__spacer" }),
3952
+ /* @__PURE__ */ jsx26(
3953
+ "button",
3954
+ {
3955
+ type: "button",
3956
+ "data-inkbrick-part": "button",
3957
+ "data-action": "cancel",
3958
+ onClick: () => close(view, state),
3959
+ children: t("formula.cancel")
3960
+ }
3961
+ ),
3962
+ /* @__PURE__ */ jsx26(
3963
+ "button",
3964
+ {
3965
+ type: "button",
3966
+ "data-inkbrick-part": "button",
3967
+ "data-primary": "",
3968
+ "data-action": "apply",
3969
+ disabled: state.mode === "insert" && draft.trim() === "",
3970
+ onClick: apply,
3971
+ children: t("formula.apply")
3972
+ }
3973
+ )
3974
+ ] })
3975
+ ]
3976
+ }
3977
+ );
3978
+ }
3979
+ function FormulaPopover() {
3980
+ const view = useEditorView();
3981
+ const readOnly = useReadOnly();
3982
+ const state = useSyncExternalStore10(
3983
+ view.formula.subscribe,
3984
+ view.formula.getState,
3985
+ view.formula.getState
3986
+ );
3987
+ if (!state.open || readOnly || typeof document === "undefined") return null;
3988
+ return createPortal3(
3989
+ /* @__PURE__ */ jsx26(Body, { state }, `${state.mode}:${state.blockId}:${state.from}:${state.to}`),
3990
+ document.body
3991
+ );
3992
+ }
3993
+
3994
+ // src/slash/SlashMenuHost.tsx
3995
+ import { createPortal as createPortal4 } from "react-dom";
3996
+ import { useSyncExternalStore as useSyncExternalStore11 } from "react";
3997
+
3998
+ // src/slash/DefaultSlashMenu.tsx
3999
+ import { useId, useLayoutEffect as useLayoutEffect5, useRef as useRef9, useState as useState8 } from "react";
4000
+ import { jsx as jsx27, jsxs as jsxs12 } from "react/jsx-runtime";
4001
+ var ESTIMATED_LINE = 28;
4002
+ function DefaultSlashMenu({
4003
+ items,
4004
+ activeIndex,
4005
+ position,
4006
+ onSelect,
4007
+ onHover
4008
+ }) {
4009
+ const t = useT();
4010
+ const view = useEditorView();
4011
+ const ref = useRef9(null);
4012
+ const [flip, setFlip] = useState8(false);
4013
+ const menuId = useId();
4014
+ const optionId = (index) => `${menuId}-option-${index}`;
4015
+ useLayoutEffect5(() => {
4016
+ const menu = ref.current;
4017
+ if (!menu) return;
4018
+ setFlip(
4019
+ position.y + menu.offsetHeight > window.innerHeight && position.y - menu.offsetHeight - ESTIMATED_LINE > 0
4020
+ );
4021
+ }, [position.x, position.y, items.length]);
4022
+ useLayoutEffect5(() => {
4023
+ ref.current?.querySelector('[aria-selected="true"]')?.scrollIntoView?.({ block: "nearest" });
4024
+ }, [activeIndex, items]);
4025
+ useLayoutEffect5(() => {
4026
+ const root = view.getRoot();
4027
+ if (!root) return;
4028
+ root.setAttribute("aria-controls", menuId);
4029
+ if (items.length > 0) root.setAttribute("aria-activedescendant", optionId(activeIndex));
4030
+ else root.removeAttribute("aria-activedescendant");
4031
+ return () => {
4032
+ root.removeAttribute("aria-controls");
4033
+ root.removeAttribute("aria-activedescendant");
4034
+ };
4035
+ }, [view, menuId, activeIndex, items.length]);
4036
+ return /* @__PURE__ */ jsxs12(
4037
+ "div",
4038
+ {
4039
+ ref,
4040
+ id: menuId,
4041
+ className: "inkbrick-slash-menu",
4042
+ "data-inkbrick-ui": "slash-menu",
4043
+ tabIndex: 0,
4044
+ role: "listbox",
4045
+ "aria-label": t("slash.label"),
4046
+ "data-testid": "slash-menu",
4047
+ style: {
4048
+ left: Math.max(
4049
+ 8,
4050
+ Math.min(position.x, window.innerWidth - (ref.current?.offsetWidth ?? 320) - 8)
4051
+ ),
4052
+ top: flip ? position.y - (ref.current?.offsetHeight ?? 0) - ESTIMATED_LINE : position.y + 4
4053
+ },
4054
+ onMouseDown: (event) => event.preventDefault(),
4055
+ children: [
4056
+ items.length === 0 && /* @__PURE__ */ jsx27("div", { className: "inkbrick-slash-menu__description", "data-inkbrick-part": "empty", children: t("slash.empty") }),
4057
+ items.map((item, index) => /* @__PURE__ */ jsxs12(
4058
+ "div",
4059
+ {
4060
+ id: optionId(index),
4061
+ role: "option",
4062
+ "aria-selected": index === activeIndex,
4063
+ "data-slash-item": item.id,
4064
+ "data-inkbrick-part": "item",
4065
+ "data-active": index === activeIndex || void 0,
4066
+ className: `inkbrick-slash-menu__item${index === activeIndex ? " inkbrick-slash-menu__item--selected" : ""}`,
4067
+ onMouseEnter: () => onHover(index),
4068
+ onClick: () => onSelect(item),
4069
+ children: [
4070
+ item.icon !== void 0 && /* @__PURE__ */ jsx27("span", { className: "inkbrick-slash-menu__icon", "data-inkbrick-part": "icon", children: item.icon }),
4071
+ /* @__PURE__ */ jsxs12("div", { className: "inkbrick-slash-menu__body", "data-inkbrick-part": "body", children: [
4072
+ /* @__PURE__ */ jsx27("div", { className: "inkbrick-slash-menu__header", "data-inkbrick-part": "title", children: item.title }),
4073
+ item.description && /* @__PURE__ */ jsx27("div", { className: "inkbrick-slash-menu__description", "data-inkbrick-part": "description", children: item.description })
4074
+ ] })
4075
+ ]
4076
+ },
4077
+ item.id
4078
+ ))
4079
+ ]
4080
+ }
4081
+ );
4082
+ }
4083
+
4084
+ // src/slash/SlashMenuHost.tsx
4085
+ import { jsx as jsx28 } from "react/jsx-runtime";
4086
+ function SlashMenuHost() {
4087
+ const editor = useEditor();
4088
+ const view = useEditorView();
4089
+ const { slots, slashItems } = useEditorUI();
4090
+ const readOnly = useReadOnly();
4091
+ const state = useSyncExternalStore11(
4092
+ view.slash.subscribe,
4093
+ view.slash.getState,
4094
+ view.slash.getState
4095
+ );
4096
+ if (readOnly || !state.open || !state.blockId || !state.position || typeof document === "undefined") {
4097
+ return null;
4098
+ }
4099
+ const items = visibleSlashItems(editor, slashItems, state.blockId, state.query);
4100
+ const Menu = slots.SlashMenu ?? DefaultSlashMenu;
4101
+ return createPortal4(
4102
+ /* @__PURE__ */ jsx28(
4103
+ Menu,
4104
+ {
4105
+ items,
4106
+ query: state.query,
4107
+ activeIndex: Math.min(state.activeIndex, Math.max(items.length - 1, 0)),
4108
+ position: state.position,
4109
+ onSelect: (item) => applySlashItem(editor, view, item),
4110
+ onHover: (index) => view.slash.update({ activeIndex: index }),
4111
+ onClose: () => view.slash.close()
4112
+ }
4113
+ ),
4114
+ document.body
4115
+ );
4116
+ }
4117
+
4118
+ // src/toolbar/ToolbarHost.tsx
4119
+ import { createPortal as createPortal5 } from "react-dom";
4120
+ import { useSyncExternalStore as useSyncExternalStore12 } from "react";
4121
+
4122
+ // src/toolbar/DefaultToolbar.tsx
4123
+ import { useEffect as useEffect14, useLayoutEffect as useLayoutEffect6, useRef as useRef10, useState as useState9 } from "react";
4124
+ import { ALIGNMENTS, COLOR_NAMES, isColorName as isColorName2 } from "@inkbrick/document";
4125
+ import { Fragment as Fragment5, jsx as jsx29, jsxs as jsxs13 } from "react/jsx-runtime";
4126
+ var BUTTONS = [
4127
+ { format: "bold", icon: "bold" },
4128
+ { format: "italic", icon: "italic" },
4129
+ { format: "underline", icon: "underline" },
4130
+ { format: "strike", icon: "strike" },
4131
+ { format: "code", icon: "code" }
4132
+ ];
4133
+ var ALIGN_ICONS = {
4134
+ left: "align-left",
4135
+ center: "align-center",
4136
+ right: "align-right",
4137
+ justify: "align-justify"
4138
+ };
4139
+ var PANELS = [
4140
+ { panel: "color", icon: "baseline" },
4141
+ { panel: "highlight", icon: "highlighter" }
4142
+ ];
4143
+ function DefaultToolbar({
4144
+ formats,
4145
+ position,
4146
+ linkOpen,
4147
+ panel,
4148
+ onToggle,
4149
+ onOpenLink,
4150
+ onApplyLink,
4151
+ onCancelLink,
4152
+ onTogglePanel,
4153
+ onApplyColor,
4154
+ onAlign,
4155
+ onInsertFormula
4156
+ }) {
4157
+ const t = useT();
4158
+ const input = useRef10(null);
4159
+ const ref = useRef10(null);
4160
+ const [value, setValue] = useState9("");
4161
+ const [invalid, setInvalid] = useState9(false);
4162
+ const [below, setBelow] = useState9(false);
4163
+ const [width, setWidth] = useState9(0);
4164
+ useEffect14(() => {
4165
+ if (!linkOpen) return;
4166
+ setValue(formats.link ?? "");
4167
+ setInvalid(false);
4168
+ input.current?.focus();
4169
+ input.current?.select();
4170
+ }, [linkOpen]);
4171
+ useLayoutEffect6(() => {
4172
+ const coarse = window.matchMedia?.("(pointer: coarse)").matches ?? false;
4173
+ setBelow(coarse || position.y - (ref.current?.offsetHeight ?? 40) - 12 < 0);
4174
+ setWidth(ref.current?.offsetWidth ?? 0);
4175
+ }, [position.x, position.y, linkOpen, panel]);
4176
+ const left = Math.max(8, Math.min(position.x - width / 2, window.innerWidth - width - 8));
4177
+ const submit = (event) => {
4178
+ event.preventDefault();
4179
+ if (!onApplyLink(value)) setInvalid(true);
4180
+ };
4181
+ const colorKind = panel === "highlight" ? "backgroundColor" : "color";
4182
+ const currentColor = panel === "highlight" ? formats.backgroundColor : formats.color;
4183
+ return /* @__PURE__ */ jsx29(
4184
+ "div",
4185
+ {
4186
+ ref,
4187
+ className: "inkbrick-toolbar",
4188
+ role: "toolbar",
4189
+ "aria-label": t("toolbar.label"),
4190
+ "data-inkbrick-ui": "toolbar",
4191
+ "data-placement": below ? "below" : "above",
4192
+ "data-testid": "toolbar",
4193
+ style: {
4194
+ left,
4195
+ top: position.y,
4196
+ transform: below ? "translateY(28px)" : "translateY(calc(-100% - 8px))"
4197
+ },
4198
+ onMouseDown: (event) => {
4199
+ if (event.target !== input.current) event.preventDefault();
4200
+ },
4201
+ children: linkOpen ? /* @__PURE__ */ jsxs13("form", { className: "inkbrick-toolbar__link", "data-inkbrick-part": "link-form", onSubmit: submit, children: [
4202
+ /* @__PURE__ */ jsx29(
4203
+ "input",
4204
+ {
4205
+ ref: input,
4206
+ "data-inkbrick-part": "input",
4207
+ "data-invalid": invalid || void 0,
4208
+ type: "text",
4209
+ "aria-label": t("toolbar.link.url"),
4210
+ "aria-invalid": invalid,
4211
+ placeholder: t("toolbar.link.placeholder"),
4212
+ value,
4213
+ onChange: (event) => {
4214
+ setValue(event.target.value);
4215
+ setInvalid(false);
4216
+ },
4217
+ onKeyDown: (event) => {
4218
+ if (event.key === "Escape") {
4219
+ event.preventDefault();
4220
+ onCancelLink();
4221
+ }
4222
+ }
4223
+ }
4224
+ ),
4225
+ /* @__PURE__ */ jsx29(
4226
+ "button",
4227
+ {
4228
+ type: "submit",
4229
+ className: "inkbrick-toolbar__button",
4230
+ "data-inkbrick-part": "button",
4231
+ "aria-label": t("toolbar.link.apply"),
4232
+ title: t("toolbar.link.apply"),
4233
+ children: /* @__PURE__ */ jsx29(Icon, { name: "check" })
4234
+ }
4235
+ ),
4236
+ formats.link && /* @__PURE__ */ jsx29(
4237
+ "button",
4238
+ {
4239
+ type: "button",
4240
+ className: "inkbrick-toolbar__button",
4241
+ "data-inkbrick-part": "button",
4242
+ "aria-label": t("toolbar.link.remove"),
4243
+ title: t("toolbar.link.remove"),
4244
+ onClick: () => onApplyLink(null),
4245
+ children: /* @__PURE__ */ jsx29(Icon, { name: "unlink" })
4246
+ }
4247
+ )
4248
+ ] }) : /* @__PURE__ */ jsxs13(Fragment5, { children: [
4249
+ BUTTONS.map(({ format, icon }) => /* @__PURE__ */ jsx29(
4250
+ "button",
4251
+ {
4252
+ type: "button",
4253
+ className: "inkbrick-toolbar__button",
4254
+ "data-inkbrick-part": "button",
4255
+ "data-format": format,
4256
+ "data-pressed": formats[format] || void 0,
4257
+ "aria-label": t(`toolbar.${format}.label`),
4258
+ "aria-pressed": formats[format],
4259
+ title: t(`toolbar.${format}.title`),
4260
+ onClick: () => onToggle(format),
4261
+ children: /* @__PURE__ */ jsx29(Icon, { name: icon })
4262
+ },
4263
+ format
4264
+ )),
4265
+ /* @__PURE__ */ jsx29(
4266
+ "span",
4267
+ {
4268
+ className: "inkbrick-toolbar__separator",
4269
+ "data-inkbrick-part": "separator",
4270
+ role: "separator"
4271
+ }
4272
+ ),
4273
+ /* @__PURE__ */ jsx29(
4274
+ "button",
4275
+ {
4276
+ type: "button",
4277
+ className: "inkbrick-toolbar__button",
4278
+ "data-inkbrick-part": "button",
4279
+ "data-format": "link",
4280
+ "data-pressed": formats.link !== void 0 || void 0,
4281
+ "aria-label": t("toolbar.link.label"),
4282
+ "aria-pressed": formats.link !== void 0,
4283
+ title: t("toolbar.link.title"),
4284
+ onClick: onOpenLink,
4285
+ children: /* @__PURE__ */ jsx29(Icon, { name: "link" })
4286
+ }
4287
+ ),
4288
+ /* @__PURE__ */ jsx29(
4289
+ "button",
4290
+ {
4291
+ type: "button",
4292
+ className: "inkbrick-toolbar__button",
4293
+ "data-inkbrick-part": "button",
4294
+ "data-format": "formula",
4295
+ "aria-label": t("toolbar.formula.label"),
4296
+ title: t("toolbar.formula.title"),
4297
+ onClick: onInsertFormula,
4298
+ children: /* @__PURE__ */ jsx29(Icon, { name: "sigma" })
4299
+ }
4300
+ ),
4301
+ /* @__PURE__ */ jsx29(
4302
+ "span",
4303
+ {
4304
+ className: "inkbrick-toolbar__separator",
4305
+ "data-inkbrick-part": "separator",
4306
+ role: "separator"
4307
+ }
4308
+ ),
4309
+ PANELS.map(({ panel: name, icon }) => {
4310
+ const current = name === "color" ? formats.color : formats.backgroundColor;
4311
+ return /* @__PURE__ */ jsxs13(
4312
+ "button",
4313
+ {
4314
+ type: "button",
4315
+ className: "inkbrick-toolbar__button",
4316
+ "data-inkbrick-part": "button",
4317
+ "data-format": name,
4318
+ "data-pressed": panel === name || void 0,
4319
+ "data-current-color": isColorName2(current) ? current : void 0,
4320
+ "aria-label": t(`toolbar.${name}.label`),
4321
+ "aria-haspopup": "true",
4322
+ "aria-expanded": panel === name,
4323
+ title: t(`toolbar.${name}.title`),
4324
+ onClick: () => onTogglePanel(name),
4325
+ children: [
4326
+ /* @__PURE__ */ jsx29(Icon, { name: icon }),
4327
+ /* @__PURE__ */ jsx29("span", { className: "inkbrick-toolbar__indicator", "data-inkbrick-part": "indicator" })
4328
+ ]
4329
+ },
4330
+ name
4331
+ );
4332
+ }),
4333
+ /* @__PURE__ */ jsx29(
4334
+ "span",
4335
+ {
4336
+ className: "inkbrick-toolbar__separator",
4337
+ "data-inkbrick-part": "separator",
4338
+ role: "separator"
4339
+ }
4340
+ ),
4341
+ ALIGNMENTS.map((align) => /* @__PURE__ */ jsx29(
4342
+ "button",
4343
+ {
4344
+ type: "button",
4345
+ className: "inkbrick-toolbar__button",
4346
+ "data-inkbrick-part": "button",
4347
+ "data-format": `align-${align}`,
4348
+ "data-pressed": formats.align === align || void 0,
4349
+ "aria-label": t(`toolbar.align.${align}.label`),
4350
+ "aria-pressed": formats.align === align,
4351
+ title: t(`toolbar.align.${align}.title`),
4352
+ onClick: () => onAlign(align),
4353
+ children: /* @__PURE__ */ jsx29(Icon, { name: ALIGN_ICONS[align] })
4354
+ },
4355
+ align
4356
+ )),
4357
+ panel && /* @__PURE__ */ jsxs13(
4358
+ "div",
4359
+ {
4360
+ className: "inkbrick-toolbar__panel",
4361
+ "data-inkbrick-part": "panel",
4362
+ "data-kind": panel,
4363
+ role: "group",
4364
+ "aria-label": t(`toolbar.${panel}.label`),
4365
+ children: [
4366
+ /* @__PURE__ */ jsx29(
4367
+ "button",
4368
+ {
4369
+ type: "button",
4370
+ "data-inkbrick-part": "swatch",
4371
+ "data-color": "",
4372
+ "data-pressed": currentColor === void 0 || void 0,
4373
+ "aria-label": t(
4374
+ panel === "color" ? "toolbar.color.default" : "toolbar.highlight.none"
4375
+ ),
4376
+ title: t(panel === "color" ? "toolbar.color.default" : "toolbar.highlight.none"),
4377
+ onClick: () => onApplyColor(colorKind, null),
4378
+ children: /* @__PURE__ */ jsx29(Icon, { name: "x" })
4379
+ }
4380
+ ),
4381
+ COLOR_NAMES.map((name) => /* @__PURE__ */ jsx29(
4382
+ "button",
4383
+ {
4384
+ type: "button",
4385
+ "data-inkbrick-part": "swatch",
4386
+ "data-color": name,
4387
+ "data-pressed": currentColor === name || void 0,
4388
+ "aria-label": t(`color.${name}`),
4389
+ title: t(`color.${name}`),
4390
+ onClick: () => onApplyColor(colorKind, name),
4391
+ children: panel === "color" ? "A" : null
4392
+ },
4393
+ name
4394
+ ))
4395
+ ]
4396
+ }
4397
+ )
4398
+ ] })
4399
+ }
4400
+ );
4401
+ }
4402
+
4403
+ // src/toolbar/ToolbarHost.tsx
4404
+ import { jsx as jsx30 } from "react/jsx-runtime";
4405
+ function ToolbarHost() {
4406
+ const editor = useEditor();
4407
+ const view = useEditorView();
4408
+ const { slots } = useEditorUI();
4409
+ const readOnly = useReadOnly();
4410
+ const state = useSyncExternalStore12(
4411
+ view.toolbar.subscribe,
4412
+ view.toolbar.getState,
4413
+ view.toolbar.getState
4414
+ );
4415
+ if (slots.Toolbar === null || readOnly) return null;
4416
+ if (!state.visible || !state.position || !state.formats || !state.range || typeof document === "undefined") {
4417
+ return null;
4418
+ }
4419
+ const Toolbar = slots.Toolbar ?? DefaultToolbar;
4420
+ const range = state.range;
4421
+ return createPortal5(
4422
+ /* @__PURE__ */ jsx30(
4423
+ Toolbar,
4424
+ {
4425
+ formats: state.formats,
4426
+ position: state.position,
4427
+ linkOpen: state.linkOpen,
4428
+ panel: state.panel,
4429
+ onToggle: (format) => {
4430
+ toggleFormatInSelection(editor, view, range, format);
4431
+ syncToolbar(editor, view);
4432
+ },
4433
+ onOpenLink: () => view.toolbar.openLink(),
4434
+ onApplyLink: (url) => {
4435
+ if (!setLinkInSelection(editor, view, range, url)) return false;
4436
+ view.toolbar.closeLink();
4437
+ syncToolbar(editor, view);
4438
+ return true;
4439
+ },
4440
+ onTogglePanel: (panel) => view.toolbar.togglePanel(panel),
4441
+ onApplyColor: (kind, value) => {
4442
+ setColorInSelection(editor, view, range, kind, value);
4443
+ view.toolbar.closePanel();
4444
+ syncToolbar(editor, view);
4445
+ },
4446
+ onAlign: (align) => {
4447
+ setAlignInSelection(editor, view, range, align);
4448
+ syncToolbar(editor, view);
4449
+ },
4450
+ onInsertFormula: () => {
4451
+ view.placeSelection(range.from, range.to);
4452
+ openInlineFormula(editor, view);
4453
+ },
4454
+ onCancelLink: () => {
4455
+ view.toolbar.closeLink();
4456
+ view.placeSelection(range.from, range.to);
4457
+ syncToolbar(editor, view);
4458
+ }
4459
+ }
4460
+ ),
4461
+ document.body
4462
+ );
4463
+ }
4464
+
4465
+ // src/renderer/virtual/estimate.ts
4466
+ var CHARS_PER_LINE = 80;
4467
+ function textLength(block2) {
4468
+ let length = 0;
4469
+ for (const node of block2.content) length += node.insert.length;
4470
+ return length;
4471
+ }
4472
+ var estimateBlockHeight = (block2) => {
4473
+ const lines = Math.max(1, Math.ceil(textLength(block2) / CHARS_PER_LINE));
4474
+ switch (block2.flavour) {
4475
+ case "block:heading":
4476
+ return 20 + 34 * lines;
4477
+ case "block:code": {
4478
+ let breaks = 0;
4479
+ for (const node of block2.content)
4480
+ for (const char of node.insert) if (char === "\n") breaks += 1;
4481
+ return 40 + 21 * (breaks + 1);
4482
+ }
4483
+ case "block:image":
4484
+ return 260;
4485
+ case "block:divider":
4486
+ return 25;
4487
+ case "block:formula":
4488
+ return 64;
4489
+ case "block:callout":
4490
+ return 24 + 26 * lines;
4491
+ default:
4492
+ return 8 + 26 * lines;
4493
+ }
4494
+ };
4495
+ function estimateSubtree(getBlock, id, estimate) {
4496
+ let total = 0;
4497
+ const queue = [id];
4498
+ for (let i = 0; i < queue.length; i++) {
4499
+ const block2 = getBlock(queue[i]);
4500
+ if (!block2) continue;
4501
+ total += estimate(block2);
4502
+ for (const child of block2.children) queue.push(child);
4503
+ }
4504
+ return total;
4505
+ }
4506
+
4507
+ // src/renderer/virtual/VirtualChildList.tsx
4508
+ import {
4509
+ useEffect as useEffect15,
4510
+ useLayoutEffect as useLayoutEffect7,
4511
+ useMemo as useMemo3,
4512
+ useReducer,
4513
+ useRef as useRef11,
4514
+ useState as useState10,
4515
+ useSyncExternalStore as useSyncExternalStore13
4516
+ } from "react";
4517
+ import { Fragment as Fragment6, jsx as jsx31, jsxs as jsxs14 } from "react/jsx-runtime";
4518
+ var SPACER = "data-inkbrick-spacer";
4519
+ function upperBound(offsets, value) {
4520
+ let lo = 0;
4521
+ let hi = offsets.length;
4522
+ while (lo < hi) {
4523
+ const mid = lo + hi >>> 1;
4524
+ if (offsets[mid] <= value) lo = mid + 1;
4525
+ else hi = mid;
4526
+ }
4527
+ return lo;
4528
+ }
4529
+ function scrollParents(element) {
4530
+ const result = [];
4531
+ for (let node = element.parentElement; node && node !== document.body && node !== document.documentElement; node = node.parentElement) {
4532
+ if (/(auto|scroll|hidden|clip)/.test(getComputedStyle(node).overflowY)) result.push(node);
4533
+ }
4534
+ return result;
4535
+ }
4536
+ function visibleRange(marker, parents) {
4537
+ let top = 0;
4538
+ let bottom = window.innerHeight;
4539
+ for (const parent of parents) {
4540
+ const rect = parent.getBoundingClientRect();
4541
+ top = Math.max(top, rect.top);
4542
+ bottom = Math.min(bottom, rect.bottom);
4543
+ }
4544
+ const origin = marker.getBoundingClientRect().top;
4545
+ return { top: top - origin, bottom: Math.max(top, bottom) - origin };
4546
+ }
4547
+ function indexOfItem(editor, parentId, ids, id) {
4548
+ let block2 = editor.store.getBlock(id);
4549
+ while (block2 && block2.parentId !== parentId) {
4550
+ block2 = block2.parentId ? editor.store.getBlock(block2.parentId) : void 0;
4551
+ }
4552
+ return block2 ? ids.indexOf(block2.id) : -1;
4553
+ }
4554
+ function Spacer({ height }) {
4555
+ return /* @__PURE__ */ jsx31(
4556
+ "div",
4557
+ {
4558
+ className: "inkbrick-spacer",
4559
+ "data-inkbrick-spacer": "",
4560
+ "aria-hidden": "true",
4561
+ contentEditable: false,
4562
+ style: { height }
4563
+ }
4564
+ );
4565
+ }
4566
+ function VirtualChildList({
4567
+ parentId,
4568
+ ids,
4569
+ options
4570
+ }) {
4571
+ const editor = useEditor();
4572
+ const view = useEditorView();
4573
+ const windowed = ids.length >= options.threshold;
4574
+ const marker = useRef11(null);
4575
+ const parents = useRef11([]);
4576
+ const [heights] = useState10(() => /* @__PURE__ */ new Map());
4577
+ const [estimates] = useState10(() => /* @__PURE__ */ new Map());
4578
+ const [measured, remeasure] = useReducer((n) => n + 1, 0);
4579
+ const [range, setRange] = useState10(() => ({
4580
+ top: 0,
4581
+ bottom: typeof window === "undefined" ? 1e3 : window.innerHeight
4582
+ }));
4583
+ const pins = useSyncExternalStore13(
4584
+ view.viewport.subscribe,
4585
+ view.viewport.getVersion,
4586
+ view.viewport.getVersion
4587
+ );
4588
+ const dragId = useSyncExternalStore13(
4589
+ view.blocks.subscribe,
4590
+ () => view.blocks.getState().drag?.blockId ?? null,
4591
+ () => null
4592
+ );
4593
+ const dropId = useSyncExternalStore13(
4594
+ view.blocks.subscribe,
4595
+ () => view.blocks.getState().drag?.targetId ?? null,
4596
+ () => null
4597
+ );
4598
+ useEffect15(() => windowed ? view.viewport.attach() : void 0, [view, windowed]);
4599
+ const offsets = useMemo3(() => {
4600
+ if (!windowed) return null;
4601
+ const result = new Float64Array(ids.length + 1);
4602
+ const getBlock = (id) => editor.store.getBlock(id);
4603
+ for (let i = 0; i < ids.length; i++) {
4604
+ const id = ids[i];
4605
+ let height = heights.get(id) ?? estimates.get(id);
4606
+ if (height === void 0) {
4607
+ height = estimateSubtree(getBlock, id, options.estimateHeight);
4608
+ estimates.set(id, height);
4609
+ }
4610
+ result[i + 1] = result[i] + height;
4611
+ }
4612
+ return result;
4613
+ }, [ids, windowed, measured, heights, estimates, editor, options.estimateHeight]);
4614
+ const rendered = useMemo3(() => {
4615
+ if (!offsets) return null;
4616
+ const last = ids.length - 1;
4617
+ const from = Math.max(0, upperBound(offsets, range.top - options.overscan) - 1);
4618
+ const to = Math.min(
4619
+ last,
4620
+ upperBound(offsets, range.bottom + options.overscan) - 1,
4621
+ from + options.maxRendered - 1
4622
+ );
4623
+ const pinned = /* @__PURE__ */ new Set([0, last]);
4624
+ for (const id of [...view.viewport.getPinned(), dragId, dropId]) {
4625
+ if (id) pinned.add(indexOfItem(editor, parentId, ids, id));
4626
+ }
4627
+ pinned.delete(-1);
4628
+ const result = [];
4629
+ for (const i of [...pinned].sort((a, b) => a - b)) if (i < from) result.push(i);
4630
+ for (let i = from; i <= to; i++) result.push(i);
4631
+ for (const i of [...pinned].sort((a, b) => a - b)) if (i > to) result.push(i);
4632
+ return result;
4633
+ }, [offsets, ids, range, options, view, editor, parentId, dragId, dropId, pins]);
4634
+ const observer = useRef11(null);
4635
+ const observed = useRef11(/* @__PURE__ */ new Map());
4636
+ useLayoutEffect7(() => {
4637
+ if (!windowed || !rendered || !marker.current) return;
4638
+ const seen = /* @__PURE__ */ new Set();
4639
+ const expected = new Set(rendered.map((i) => ids[i]));
4640
+ let changed = false;
4641
+ let previous = null;
4642
+ let remaining = rendered.length;
4643
+ const settle = (height) => {
4644
+ if (!previous) return;
4645
+ if (height > 0 && Math.abs((heights.get(previous.id) ?? -1) - height) > 0.5) {
4646
+ heights.set(previous.id, height);
4647
+ changed = true;
4648
+ }
4649
+ previous = null;
4650
+ };
4651
+ for (let element = marker.current.nextElementSibling; element && remaining > 0; element = element.nextElementSibling) {
4652
+ const rect = element.getBoundingClientRect();
4653
+ if (previous) settle(rect.top - previous.top);
4654
+ if (element.hasAttribute(SPACER)) continue;
4655
+ remaining -= 1;
4656
+ const id = element.getAttribute("data-block-id");
4657
+ if (id && expected.has(id)) {
4658
+ previous = { id, top: rect.top, height: rect.height };
4659
+ seen.add(element);
4660
+ }
4661
+ }
4662
+ if (previous) settle(previous.height);
4663
+ if (typeof ResizeObserver !== "undefined") {
4664
+ const sizes = observed.current;
4665
+ observer.current ??= new ResizeObserver((entries) => {
4666
+ let resized = false;
4667
+ for (const entry of entries) {
4668
+ const size = entry.contentRect.height;
4669
+ const known = sizes.get(entry.target);
4670
+ if (known !== void 0 && Math.abs(known - size) > 0.5) resized = true;
4671
+ sizes.set(entry.target, size);
4672
+ }
4673
+ if (resized) remeasure();
4674
+ });
4675
+ for (const element of [...sizes.keys()]) {
4676
+ if (!seen.has(element)) {
4677
+ observer.current.unobserve(element);
4678
+ sizes.delete(element);
4679
+ }
4680
+ }
4681
+ for (const element of seen) {
4682
+ if (!sizes.has(element)) observer.current.observe(element);
4683
+ }
4684
+ }
4685
+ const next2 = visibleRange(marker.current, parents.current);
4686
+ const step = options.overscan / 4;
4687
+ setRange(
4688
+ (current) => Math.abs(current.top - next2.top) < step && Math.abs(current.bottom - next2.bottom) < step ? current : next2
4689
+ );
4690
+ if (changed) remeasure();
4691
+ });
4692
+ useEffect15(() => () => observer.current?.disconnect(), []);
4693
+ useEffect15(() => {
4694
+ if (!windowed || !marker.current) return;
4695
+ parents.current = scrollParents(marker.current);
4696
+ let frame = 0;
4697
+ const step = options.overscan / 4;
4698
+ const onScroll = () => {
4699
+ if (frame) return;
4700
+ frame = requestAnimationFrame(() => {
4701
+ frame = 0;
4702
+ if (!marker.current) return;
4703
+ const next2 = visibleRange(marker.current, parents.current);
4704
+ setRange(
4705
+ (current) => Math.abs(current.top - next2.top) < step && Math.abs(current.bottom - next2.bottom) < step ? current : next2
4706
+ );
4707
+ });
4708
+ };
4709
+ window.addEventListener("scroll", onScroll, { capture: true, passive: true });
4710
+ window.addEventListener("resize", onScroll);
4711
+ return () => {
4712
+ cancelAnimationFrame(frame);
4713
+ window.removeEventListener("scroll", onScroll, { capture: true });
4714
+ window.removeEventListener("resize", onScroll);
4715
+ };
4716
+ }, [windowed, options.overscan]);
4717
+ if (!rendered || !offsets) {
4718
+ return /* @__PURE__ */ jsxs14(Fragment6, { children: [
4719
+ null,
4720
+ ids.map((id) => /* @__PURE__ */ jsx31(BlockRenderer, { id }, id))
4721
+ ] });
4722
+ }
4723
+ const items = [];
4724
+ let next = 0;
4725
+ for (const i of rendered) {
4726
+ if (i > next) items.push(/* @__PURE__ */ jsx31(Spacer, { height: offsets[i] - offsets[next] }, `gap:${next}`));
4727
+ items.push(/* @__PURE__ */ jsx31(BlockRenderer, { id: ids[i] }, ids[i]));
4728
+ next = i + 1;
4729
+ }
4730
+ return /* @__PURE__ */ jsxs14(Fragment6, { children: [
4731
+ /* @__PURE__ */ jsx31(
4732
+ "div",
4733
+ {
4734
+ ref: marker,
4735
+ className: "inkbrick-spacer",
4736
+ "data-inkbrick-spacer": "start",
4737
+ "aria-hidden": "true",
4738
+ contentEditable: false
4739
+ }
4740
+ ),
4741
+ items
4742
+ ] });
4743
+ }
4744
+
4745
+ // src/renderer/virtual/virtualRenderer.tsx
4746
+ import { jsx as jsx32 } from "react/jsx-runtime";
4747
+ function virtualRenderer(options = {}) {
4748
+ const resolved = {
4749
+ threshold: Math.max(1, options.threshold ?? 100),
4750
+ overscan: Math.max(0, options.overscan ?? 1200),
4751
+ maxRendered: Math.max(1, options.maxRendered ?? 400),
4752
+ estimateHeight: options.estimateHeight ?? estimateBlockHeight
4753
+ };
4754
+ function VirtualChildren(props) {
4755
+ return /* @__PURE__ */ jsx32(VirtualChildList, { ...props, options: resolved });
4756
+ }
4757
+ return { ChildList: VirtualChildren };
4758
+ }
4759
+ var defaultVirtualRenderer = virtualRenderer();
4760
+
4761
+ // src/renderer/DocumentView.tsx
4762
+ import { Fragment as Fragment7, jsx as jsx33, jsxs as jsxs15 } from "react/jsx-runtime";
4763
+ function resolveRenderer(renderer) {
4764
+ if (renderer === "virtual") return defaultVirtualRenderer;
4765
+ if (!renderer || renderer === "full") return fullRenderer;
4766
+ return renderer;
4767
+ }
4768
+ function DocumentView({ label, renderer } = {}) {
4769
+ const editor = useEditor();
4770
+ const view = useEditorView();
4771
+ const ref = useRef12(null);
4772
+ const { slots } = useEditorUI();
4773
+ const readOnly = useReadOnly();
4774
+ const t = useT();
4775
+ useEditorInput(ref, !readOnly);
4776
+ useSelectionState(ref);
4777
+ useClipboard(ref, readOnly);
4778
+ useBlockHandles(ref, slots.BlockHandle !== null && !readOnly);
4779
+ useLayoutEffect8(() => view.setRoot(ref.current), [view]);
4780
+ useEffect16(() => {
4781
+ if (!readOnly) void preloadFormulaPalette();
4782
+ }, [readOnly]);
4783
+ return /* @__PURE__ */ jsxs15(Fragment7, { children: [
4784
+ /* @__PURE__ */ jsx33(
4785
+ "div",
4786
+ {
4787
+ ref,
4788
+ className: "inkbrick-editor",
4789
+ "data-inkbrick-root": "",
4790
+ contentEditable: !readOnly,
4791
+ suppressContentEditableWarning: true,
4792
+ spellCheck: false,
4793
+ role: "textbox",
4794
+ "aria-multiline": "true",
4795
+ "aria-label": label ?? t("editor.label"),
4796
+ tabIndex: readOnly ? 0 : void 0,
4797
+ "aria-readonly": readOnly || void 0,
4798
+ "data-readonly": readOnly || void 0,
4799
+ children: /* @__PURE__ */ jsx33(DocumentRendererContext.Provider, { value: resolveRenderer(renderer), children: /* @__PURE__ */ jsx33(BlockRenderer, { id: editor.document.rootId }) })
4800
+ }
4801
+ ),
4802
+ /* @__PURE__ */ jsx33(SlashMenuHost, {}),
4803
+ /* @__PURE__ */ jsx33(ToolbarHost, {}),
4804
+ /* @__PURE__ */ jsx33(FormulaPopover, {}),
4805
+ /* @__PURE__ */ jsx33(BlockHandleHost, {}),
4806
+ /* @__PURE__ */ jsx33(DropIndicatorHost, {})
4807
+ ] });
4808
+ }
4809
+ export {
4810
+ BlockChildren,
4811
+ BlockRenderer,
4812
+ CODE_LANGUAGES,
4813
+ CalloutBlock,
4814
+ CodeBlock,
4815
+ DefaultBlockHandle,
4816
+ DefaultBlockMenu,
4817
+ DefaultDropIndicator,
4818
+ DefaultSlashMenu,
4819
+ DefaultToolbar,
4820
+ DividerBlock,
4821
+ DocumentView,
4822
+ Editable,
4823
+ EditorContext,
4824
+ EditorProvider,
4825
+ EditorView,
4826
+ EditorViewContext,
4827
+ FormulaView,
4828
+ HIGHLIGHT_LIMIT,
4829
+ HeadingBlock,
4830
+ Icon,
4831
+ ImageBlock,
4832
+ ListBlock,
4833
+ NoteBlock,
4834
+ PageBlock,
4835
+ ParagraphBlock,
4836
+ QuoteBlock,
4837
+ UnknownBlockView,
4838
+ createTranslator,
4839
+ defaultBlockComponents,
4840
+ defaultKeymap,
4841
+ defaultSlashItems,
4842
+ defineBlock,
4843
+ definePresentation,
4844
+ embeddedFragment,
4845
+ en,
4846
+ ensureMathStyles,
4847
+ estimateBlockHeight,
4848
+ filterSlashItems,
4849
+ fullRenderer,
4850
+ getLocales,
4851
+ highlightCode,
4852
+ htmlToFragment,
4853
+ matchesShortcut,
4854
+ mountFormula,
4855
+ normalizeImageUrl,
4856
+ normalizeLanguage,
4857
+ registerLocale,
4858
+ resolveBlockComponent,
4859
+ resolveKeymap,
4860
+ resolveMessages,
4861
+ ru,
4862
+ temmlRenderer,
4863
+ unbind,
4864
+ uploadImage,
4865
+ useBlock,
4866
+ useBlockController,
4867
+ useBlockDrag,
4868
+ useBlockDragState,
4869
+ useChildren,
4870
+ useDocument,
4871
+ useEditor,
4872
+ useEditorUI,
4873
+ useEditorView,
4874
+ useHistory,
4875
+ useLocale,
4876
+ useReadOnly,
4877
+ useT,
4878
+ virtualRenderer
4879
+ };
4880
+ //# sourceMappingURL=index.js.map