@railway/inkwell 0.1.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,1628 @@
1
+ 'use strict';
2
+
3
+ var core = require('@slate-yjs/core');
4
+ var react = require('react');
5
+ var slate = require('slate');
6
+ var slateHistory = require('slate-history');
7
+ var slateReact = require('slate-react');
8
+ var jsxRuntime = require('react/jsx-runtime');
9
+ var rehypeHighlight = require('rehype-highlight');
10
+ var rehypeSanitize = require('rehype-sanitize');
11
+ var rehypeStringify = require('rehype-stringify');
12
+ var remarkGfm = require('remark-gfm');
13
+ var remarkParse = require('remark-parse');
14
+ var remarkRehype = require('remark-rehype');
15
+ var unified = require('unified');
16
+ var mdastUtilToString = require('mdast-util-to-string');
17
+ var unistUtilVisit = require('unist-util-visit');
18
+ var rehypeParse = require('rehype-parse');
19
+ var rehypeRemark = require('rehype-remark');
20
+ var remarkStringify = require('remark-stringify');
21
+ var rehypeReact = require('rehype-react');
22
+
23
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
24
+
25
+ var rehypeHighlight__default = /*#__PURE__*/_interopDefault(rehypeHighlight);
26
+ var rehypeSanitize__default = /*#__PURE__*/_interopDefault(rehypeSanitize);
27
+ var rehypeStringify__default = /*#__PURE__*/_interopDefault(rehypeStringify);
28
+ var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
29
+ var remarkParse__default = /*#__PURE__*/_interopDefault(remarkParse);
30
+ var remarkRehype__default = /*#__PURE__*/_interopDefault(remarkRehype);
31
+ var rehypeParse__default = /*#__PURE__*/_interopDefault(rehypeParse);
32
+ var rehypeRemark__default = /*#__PURE__*/_interopDefault(rehypeRemark);
33
+ var remarkStringify__default = /*#__PURE__*/_interopDefault(remarkStringify);
34
+ var rehypeReact__default = /*#__PURE__*/_interopDefault(rehypeReact);
35
+
36
+ // src/editor/inkwell-editor.tsx
37
+
38
+ // src/lib/class-names.ts
39
+ function editorClass(component) {
40
+ return `inkwell-editor-${component}`;
41
+ }
42
+ function pluginClass(pluginName) {
43
+ return (component) => `inkwell-plugin-${pluginName}-${component}`;
44
+ }
45
+ var cls = pluginClass("bubble-menu");
46
+ function BoldButton({ wrapSelection }) {
47
+ return /* @__PURE__ */ jsxRuntime.jsx(
48
+ "button",
49
+ {
50
+ type: "button",
51
+ onClick: () => wrapSelection("**", "**"),
52
+ className: cls("btn"),
53
+ "aria-label": "Bold",
54
+ title: "Bold",
55
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: cls("item-bold"), children: "B" })
56
+ }
57
+ );
58
+ }
59
+ function ItalicButton({ wrapSelection }) {
60
+ return /* @__PURE__ */ jsxRuntime.jsx(
61
+ "button",
62
+ {
63
+ type: "button",
64
+ onClick: () => wrapSelection("_", "_"),
65
+ className: cls("btn"),
66
+ "aria-label": "Italic",
67
+ title: "Italic",
68
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: cls("item-italic"), children: "I" })
69
+ }
70
+ );
71
+ }
72
+ function StrikethroughButton({ wrapSelection }) {
73
+ return /* @__PURE__ */ jsxRuntime.jsx(
74
+ "button",
75
+ {
76
+ type: "button",
77
+ onClick: () => wrapSelection("~~", "~~"),
78
+ className: cls("btn"),
79
+ "aria-label": "Strikethrough",
80
+ title: "Strikethrough",
81
+ children: /* @__PURE__ */ jsxRuntime.jsx("span", { className: cls("item-strike"), children: "S" })
82
+ }
83
+ );
84
+ }
85
+ var defaultBubbleMenuItems = [
86
+ {
87
+ key: "bold",
88
+ shortcut: "b",
89
+ onShortcut: (wrap) => wrap("**", "**"),
90
+ render: (props) => /* @__PURE__ */ jsxRuntime.jsx(BoldButton, { ...props })
91
+ },
92
+ {
93
+ key: "italic",
94
+ shortcut: "i",
95
+ onShortcut: (wrap) => wrap("_", "_"),
96
+ render: (props) => /* @__PURE__ */ jsxRuntime.jsx(ItalicButton, { ...props })
97
+ },
98
+ {
99
+ key: "strikethrough",
100
+ shortcut: "d",
101
+ onShortcut: (wrap) => wrap("~~", "~~"),
102
+ render: (props) => /* @__PURE__ */ jsxRuntime.jsx(StrikethroughButton, { ...props })
103
+ }
104
+ ];
105
+ function BubbleMenuWidget({
106
+ editorRef,
107
+ wrapSelection,
108
+ items
109
+ }) {
110
+ const [position, setPosition] = react.useState(null);
111
+ const handleSelectionChange = react.useCallback(() => {
112
+ const active = document.activeElement;
113
+ if (active?.closest(`.${cls("container")}`)) return;
114
+ const sel = window.getSelection();
115
+ if (!sel || !editorRef.current || !editorRef.current.contains(sel.anchorNode) || sel.isCollapsed || !sel.toString().trim()) {
116
+ setPosition(null);
117
+ return;
118
+ }
119
+ const rect = sel.getRangeAt(0).getBoundingClientRect();
120
+ const editorRect = editorRef.current.getBoundingClientRect();
121
+ setPosition({
122
+ top: rect.top - editorRect.top,
123
+ left: rect.left - editorRect.left + rect.width / 2
124
+ });
125
+ }, [editorRef]);
126
+ react.useEffect(() => {
127
+ document.addEventListener("selectionchange", handleSelectionChange);
128
+ const handleMouseUp = () => {
129
+ requestAnimationFrame(handleSelectionChange);
130
+ };
131
+ editorRef.current?.addEventListener("mouseup", handleMouseUp);
132
+ const el = editorRef.current;
133
+ return () => {
134
+ document.removeEventListener("selectionchange", handleSelectionChange);
135
+ el?.removeEventListener("mouseup", handleMouseUp);
136
+ };
137
+ }, [handleSelectionChange, editorRef]);
138
+ if (!position) return null;
139
+ return /* @__PURE__ */ jsxRuntime.jsx(
140
+ "div",
141
+ {
142
+ className: cls("container"),
143
+ style: {
144
+ position: "absolute",
145
+ top: position.top,
146
+ left: position.left,
147
+ transform: "translateX(-50%) translateY(-100%)",
148
+ marginTop: -8,
149
+ zIndex: 1e3
150
+ },
151
+ onMouseDown: (e) => e.preventDefault(),
152
+ children: /* @__PURE__ */ jsxRuntime.jsx("div", { className: cls("inner"), children: items.map((item) => /* @__PURE__ */ jsxRuntime.jsx(item.render, { wrapSelection }, item.key)) })
153
+ }
154
+ );
155
+ }
156
+ function createBubbleMenuPlugin(options) {
157
+ const items = options?.items ?? defaultBubbleMenuItems;
158
+ return {
159
+ name: "bubble-menu",
160
+ render: (props) => /* @__PURE__ */ jsxRuntime.jsx(BubbleMenuWidget, { ...props, items }),
161
+ onKeyDown: (event, { wrapSelection }) => {
162
+ if (!event.metaKey && !event.ctrlKey) return;
163
+ const item = items.find((i) => i.shortcut === event.key);
164
+ if (item?.onShortcut) {
165
+ event.preventDefault();
166
+ item.onShortcut(wrapSelection);
167
+ }
168
+ }
169
+ };
170
+ }
171
+ function remarkFlattenBlockquotes() {
172
+ return (tree) => {
173
+ unistUtilVisit.visit(tree, "blockquote", (node) => {
174
+ const newChildren = [];
175
+ for (const child of node.children) {
176
+ if (child.type === "blockquote") {
177
+ const text = mdastUtilToString.toString(child);
178
+ const paragraph = {
179
+ type: "paragraph",
180
+ children: [{ type: "text", value: "> " + text }]
181
+ };
182
+ newChildren.push(paragraph);
183
+ } else {
184
+ newChildren.push(child);
185
+ }
186
+ }
187
+ node.children = newChildren;
188
+ });
189
+ };
190
+ }
191
+
192
+ // src/lib/remark-no-tables.ts
193
+ function rowToText(row) {
194
+ return "| " + row.children.map(
195
+ (cell) => cell.children.map((child) => "value" in child ? child.value : "").join("")
196
+ ).join(" | ") + " |";
197
+ }
198
+ function remarkNoTables() {
199
+ return (tree) => {
200
+ tree.children = tree.children.map((node) => {
201
+ if (node.type !== "table") return node;
202
+ const table = node;
203
+ const lines = table.children.map(rowToText);
204
+ if (lines.length > 0) {
205
+ const colCount = table.children[0].children.length;
206
+ const sep = "| " + Array.from({ length: colCount }, () => "---").join(" | ") + " |";
207
+ lines.splice(1, 0, sep);
208
+ }
209
+ const paragraph = {
210
+ type: "paragraph",
211
+ children: [{ type: "text", value: lines.join("\n") }]
212
+ };
213
+ return paragraph;
214
+ });
215
+ };
216
+ }
217
+
218
+ // src/lib/render-html.ts
219
+ var processorCache = /* @__PURE__ */ new Map();
220
+ function getProcessor(rehypePlugins) {
221
+ const key = rehypePlugins ? JSON.stringify(
222
+ rehypePlugins.map((p) => Array.isArray(p) ? p[1] : "default")
223
+ ) : "default";
224
+ const cached = processorCache.get(key);
225
+ if (cached) return cached;
226
+ const processor2 = unified.unified().use(remarkParse__default.default).use(remarkGfm__default.default).use(remarkNoTables).use(remarkFlattenBlockquotes).use(remarkRehype__default.default);
227
+ const plugins = rehypePlugins ?? [[rehypeHighlight__default.default, { detect: true }]];
228
+ for (const plugin of plugins) {
229
+ if (Array.isArray(plugin)) {
230
+ processor2.use(plugin[0], plugin[1]);
231
+ } else {
232
+ processor2.use(plugin);
233
+ }
234
+ }
235
+ processor2.use(rehypeSanitize__default.default, {
236
+ ...rehypeSanitize.defaultSchema,
237
+ tagNames: [...rehypeSanitize.defaultSchema.tagNames ?? [], "span"],
238
+ attributes: {
239
+ ...rehypeSanitize.defaultSchema.attributes,
240
+ code: ["className"],
241
+ span: ["className"]
242
+ }
243
+ });
244
+ processor2.use(rehypeStringify__default.default);
245
+ processorCache.set(key, processor2);
246
+ return processor2;
247
+ }
248
+ function escapeBareBq(markdown) {
249
+ return markdown.replace(/^>(?=\S)/gm, "\\>");
250
+ }
251
+ function renderMarkdownToHtml(markdown, rehypePlugins) {
252
+ const processor2 = getProcessor(rehypePlugins);
253
+ return String(processor2.processSync(escapeBareBq(markdown)));
254
+ }
255
+
256
+ // src/editor/slate/decorations.ts
257
+ function computeDecorations(entry, editor, rehypePlugins) {
258
+ const [node, _path] = entry;
259
+ if (!slate.Element.isElement(node)) return [];
260
+ const element = node;
261
+ if (element.type === "code-line") {
262
+ return computeCodeDecorations(entry, editor, rehypePlugins);
263
+ }
264
+ if (element.type === "paragraph" || element.type === "blockquote" || element.type === "list-item" || element.type === "heading") {
265
+ return computeInlineDecorations(entry);
266
+ }
267
+ if (element.type === "code-fence") {
268
+ return computeFenceDecorations(entry);
269
+ }
270
+ return [];
271
+ }
272
+ function computeInlineDecorations(entry) {
273
+ const [node, path] = entry;
274
+ const text = slate.Node.string(node);
275
+ if (!text) return [];
276
+ const ranges = [];
277
+ const codeRanges = [];
278
+ const codeRegex = /`([^`]+)`/g;
279
+ let match;
280
+ while ((match = codeRegex.exec(text)) !== null) {
281
+ const start = match.index;
282
+ const end = start + match[0].length;
283
+ codeRanges.push({ start, end });
284
+ ranges.push({
285
+ anchor: { path: [...path, 0], offset: start },
286
+ focus: { path: [...path, 0], offset: start + 1 },
287
+ codeMarker: true
288
+ });
289
+ ranges.push({
290
+ anchor: { path: [...path, 0], offset: start + 1 },
291
+ focus: { path: [...path, 0], offset: end - 1 },
292
+ inlineCode: true
293
+ });
294
+ ranges.push({
295
+ anchor: { path: [...path, 0], offset: end - 1 },
296
+ focus: { path: [...path, 0], offset: end },
297
+ codeMarker: true
298
+ });
299
+ }
300
+ const isInCode = (offset) => codeRanges.some((r) => offset >= r.start && offset < r.end);
301
+ const boldRegex = /\*\*(.+?)\*\*/g;
302
+ while ((match = boldRegex.exec(text)) !== null) {
303
+ if (isInCode(match.index)) continue;
304
+ const start = match.index;
305
+ const end = start + match[0].length;
306
+ ranges.push({
307
+ anchor: { path: [...path, 0], offset: start },
308
+ focus: { path: [...path, 0], offset: start + 2 },
309
+ boldMarker: true
310
+ });
311
+ ranges.push({
312
+ anchor: { path: [...path, 0], offset: start + 2 },
313
+ focus: { path: [...path, 0], offset: end - 2 },
314
+ bold: true
315
+ });
316
+ ranges.push({
317
+ anchor: { path: [...path, 0], offset: end - 2 },
318
+ focus: { path: [...path, 0], offset: end },
319
+ boldMarker: true
320
+ });
321
+ }
322
+ const italicPatterns = [/_(.+?)_/g, /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)/g];
323
+ for (const regex of italicPatterns) {
324
+ while ((match = regex.exec(text)) !== null) {
325
+ if (isInCode(match.index)) continue;
326
+ const start = match.index;
327
+ const end = start + match[0].length;
328
+ ranges.push({
329
+ anchor: { path: [...path, 0], offset: start },
330
+ focus: { path: [...path, 0], offset: start + 1 },
331
+ italicMarker: true
332
+ });
333
+ ranges.push({
334
+ anchor: { path: [...path, 0], offset: start + 1 },
335
+ focus: { path: [...path, 0], offset: end - 1 },
336
+ italic: true
337
+ });
338
+ ranges.push({
339
+ anchor: { path: [...path, 0], offset: end - 1 },
340
+ focus: { path: [...path, 0], offset: end },
341
+ italicMarker: true
342
+ });
343
+ }
344
+ }
345
+ const strikeRegex = /~~(.+?)~~/g;
346
+ while ((match = strikeRegex.exec(text)) !== null) {
347
+ if (isInCode(match.index)) continue;
348
+ const start = match.index;
349
+ const end = start + match[0].length;
350
+ ranges.push({
351
+ anchor: { path: [...path, 0], offset: start },
352
+ focus: { path: [...path, 0], offset: start + 2 },
353
+ strikeMarker: true
354
+ });
355
+ ranges.push({
356
+ anchor: { path: [...path, 0], offset: start + 2 },
357
+ focus: { path: [...path, 0], offset: end - 2 },
358
+ strikethrough: true
359
+ });
360
+ ranges.push({
361
+ anchor: { path: [...path, 0], offset: end - 2 },
362
+ focus: { path: [...path, 0], offset: end },
363
+ strikeMarker: true
364
+ });
365
+ }
366
+ return ranges;
367
+ }
368
+ function computeFenceDecorations(entry) {
369
+ const [node, _path] = entry;
370
+ const text = slate.Node.string(node);
371
+ if (!text) return [];
372
+ return [];
373
+ }
374
+ function computeCodeDecorations(entry, editor, rehypePlugins) {
375
+ const [node, path] = entry;
376
+ const lineText = slate.Node.string(node);
377
+ if (!lineText) return [];
378
+ const lineIndex = path[0];
379
+ let lang = "";
380
+ for (let i = lineIndex - 1; i >= 0; i--) {
381
+ const sibling = editor.children[i];
382
+ if (sibling.type === "code-fence") {
383
+ lang = slate.Node.string(sibling).replace(/^`+/, "").trim();
384
+ break;
385
+ }
386
+ }
387
+ const codeLines = [];
388
+ let blockStartIdx = -1;
389
+ for (let i = lineIndex - 1; i >= 0; i--) {
390
+ const sibling = editor.children[i];
391
+ if (sibling.type === "code-fence") {
392
+ blockStartIdx = i + 1;
393
+ break;
394
+ }
395
+ if (sibling.type !== "code-line") break;
396
+ }
397
+ if (blockStartIdx === -1) blockStartIdx = lineIndex;
398
+ for (let i = blockStartIdx; i < editor.children.length; i++) {
399
+ const sibling = editor.children[i];
400
+ if (sibling.type === "code-fence") break;
401
+ if (sibling.type !== "code-line") break;
402
+ codeLines.push(slate.Node.string(sibling));
403
+ }
404
+ const thisLineIdx = lineIndex - blockStartIdx;
405
+ const fullCode = codeLines.join("\n");
406
+ if (!fullCode.trim()) return [];
407
+ const highlighted = highlightAndSplit(fullCode, lang, rehypePlugins);
408
+ const lineHtml = highlighted[thisLineIdx];
409
+ if (!lineHtml) return [];
410
+ return parseHljsRanges(lineHtml, path);
411
+ }
412
+ function highlightAndSplit(code, lang, rehypePlugins) {
413
+ const md = "```" + lang + "\n" + code + "\n```";
414
+ const html = renderMarkdownToHtml(md, rehypePlugins);
415
+ const match = html.match(/<code[^>]*>([\s\S]*?)<\/code>/);
416
+ const raw = match?.[1]?.replace(/^\n|\n$/g, "") || "";
417
+ if (!raw) return [];
418
+ const lines = [];
419
+ let currentLine = "";
420
+ const openSpans = [];
421
+ for (let i = 0; i < raw.length; i++) {
422
+ if (raw[i] === "\n") {
423
+ for (let j = openSpans.length - 1; j >= 0; j--) currentLine += "</span>";
424
+ lines.push(currentLine);
425
+ currentLine = openSpans.join("");
426
+ } else if (raw[i] === "<") {
427
+ const closeIdx = raw.indexOf(">", i);
428
+ if (closeIdx === -1) {
429
+ currentLine += raw[i];
430
+ continue;
431
+ }
432
+ const tag = raw.slice(i, closeIdx + 1);
433
+ currentLine += tag;
434
+ if (tag.startsWith("<span")) {
435
+ openSpans.push(tag);
436
+ } else if (tag === "</span>") {
437
+ openSpans.pop();
438
+ }
439
+ i = closeIdx;
440
+ } else {
441
+ currentLine += raw[i];
442
+ }
443
+ }
444
+ for (let j = openSpans.length - 1; j >= 0; j--) currentLine += "</span>";
445
+ if (currentLine) lines.push(currentLine);
446
+ return lines;
447
+ }
448
+ function parseHljsRanges(html, elementPath) {
449
+ const ranges = [];
450
+ let textOffset = 0;
451
+ const classStack = [];
452
+ let i = 0;
453
+ while (i < html.length) {
454
+ if (html[i] === "<") {
455
+ const closeIdx = html.indexOf(">", i);
456
+ if (closeIdx === -1) break;
457
+ const tag = html.slice(i, closeIdx + 1);
458
+ if (tag.startsWith("<span")) {
459
+ const classMatch = tag.match(/class="([^"]+)"/);
460
+ classStack.push(classMatch?.[1] || "");
461
+ } else if (tag === "</span>") {
462
+ classStack.pop();
463
+ }
464
+ i = closeIdx + 1;
465
+ } else if (html[i] === "&") {
466
+ const semiIdx = html.indexOf(";", i);
467
+ if (semiIdx !== -1) {
468
+ const entity = html.slice(i, semiIdx + 1);
469
+ let decoded;
470
+ if (entity === "&amp;") decoded = "&";
471
+ else if (entity === "&lt;") decoded = "<";
472
+ else if (entity === "&gt;") decoded = ">";
473
+ else if (entity === "&quot;") decoded = '"';
474
+ else if (entity.startsWith("&#x"))
475
+ decoded = String.fromCodePoint(parseInt(entity.slice(3, -1), 16));
476
+ else if (entity.startsWith("&#"))
477
+ decoded = String.fromCodePoint(parseInt(entity.slice(2, -1), 10));
478
+ else decoded = entity;
479
+ if (classStack.length > 0 && classStack[classStack.length - 1]) {
480
+ ranges.push({
481
+ anchor: { path: [...elementPath, 0], offset: textOffset },
482
+ focus: {
483
+ path: [...elementPath, 0],
484
+ offset: textOffset + decoded.length
485
+ },
486
+ hljs: classStack[classStack.length - 1]
487
+ });
488
+ }
489
+ textOffset += decoded.length;
490
+ i = semiIdx + 1;
491
+ } else {
492
+ textOffset++;
493
+ i++;
494
+ }
495
+ } else {
496
+ const nextTag = html.indexOf("<", i);
497
+ const nextEntity = html.indexOf("&", i);
498
+ let end = html.length;
499
+ if (nextTag !== -1) end = Math.min(end, nextTag);
500
+ if (nextEntity !== -1) end = Math.min(end, nextEntity);
501
+ const chunk = html.slice(i, end);
502
+ if (classStack.length > 0 && classStack[classStack.length - 1] && chunk.length > 0) {
503
+ ranges.push({
504
+ anchor: { path: [...elementPath, 0], offset: textOffset },
505
+ focus: {
506
+ path: [...elementPath, 0],
507
+ offset: textOffset + chunk.length
508
+ },
509
+ hljs: classStack[classStack.length - 1]
510
+ });
511
+ }
512
+ textOffset += chunk.length;
513
+ i = end;
514
+ }
515
+ }
516
+ return ranges;
517
+ }
518
+ function generateId() {
519
+ return crypto.randomUUID();
520
+ }
521
+ function withNodeId(editor) {
522
+ const { apply } = editor;
523
+ editor.apply = (operation) => {
524
+ if (operation.type === "insert_node") {
525
+ const node = operation.node;
526
+ if (slate.Element.isElement(node)) {
527
+ const el = node;
528
+ if (!el.id) {
529
+ operation = {
530
+ ...operation,
531
+ node: { ...node, id: generateId() }
532
+ };
533
+ }
534
+ }
535
+ }
536
+ if (operation.type === "split_node") {
537
+ const props = operation.properties;
538
+ if ("type" in props) {
539
+ operation = {
540
+ ...operation,
541
+ properties: { ...props, id: generateId() }
542
+ };
543
+ }
544
+ }
545
+ apply(operation);
546
+ };
547
+ return editor;
548
+ }
549
+
550
+ // src/editor/slate/deserialize.ts
551
+ var HEADING_RE = /^(#{1,6}) /;
552
+ function deserialize(markdown, decorations) {
553
+ if (!markdown) {
554
+ return [{ type: "paragraph", id: generateId(), children: [{ text: "" }] }];
555
+ }
556
+ const headingEnabled = [
557
+ decorations?.heading1 ?? true,
558
+ decorations?.heading2 ?? true,
559
+ decorations?.heading3 ?? true,
560
+ decorations?.heading4 ?? true,
561
+ decorations?.heading5 ?? true,
562
+ decorations?.heading6 ?? true
563
+ ];
564
+ const cfg = {
565
+ lists: decorations?.lists ?? true,
566
+ blockquotes: decorations?.blockquotes ?? true,
567
+ codeBlocks: decorations?.codeBlocks ?? true
568
+ };
569
+ const lines = markdown.split("\n");
570
+ const result = [];
571
+ let inCodeBlock = false;
572
+ let paragraphLines = [];
573
+ const flushParagraph = () => {
574
+ if (paragraphLines.length === 0) return;
575
+ for (const line of paragraphLines) {
576
+ const hMatch = HEADING_RE.exec(line);
577
+ if (hMatch && headingEnabled[hMatch[1].length - 1]) {
578
+ result.push({
579
+ type: "heading",
580
+ id: generateId(),
581
+ level: hMatch[1].length,
582
+ children: [{ text: line.slice(hMatch[0].length) }]
583
+ });
584
+ continue;
585
+ }
586
+ if (cfg.blockquotes && /^> /.test(line)) {
587
+ result.push({
588
+ type: "blockquote",
589
+ id: generateId(),
590
+ children: [{ text: line.slice(2) }]
591
+ });
592
+ } else if (cfg.lists && /^[-*+] /.test(line)) {
593
+ result.push({
594
+ type: "list-item",
595
+ id: generateId(),
596
+ children: [{ text: line }]
597
+ });
598
+ } else {
599
+ result.push({
600
+ type: "paragraph",
601
+ id: generateId(),
602
+ children: [{ text: line }]
603
+ });
604
+ }
605
+ }
606
+ paragraphLines = [];
607
+ };
608
+ for (const line of lines) {
609
+ if (cfg.codeBlocks && !inCodeBlock && line.startsWith("```")) {
610
+ flushParagraph();
611
+ inCodeBlock = true;
612
+ result.push({
613
+ type: "code-fence",
614
+ id: generateId(),
615
+ children: [{ text: line }]
616
+ });
617
+ continue;
618
+ }
619
+ if (inCodeBlock && line.trim() === "```") {
620
+ result.push({
621
+ type: "code-fence",
622
+ id: generateId(),
623
+ children: [{ text: line }]
624
+ });
625
+ inCodeBlock = false;
626
+ continue;
627
+ }
628
+ if (inCodeBlock) {
629
+ result.push({
630
+ type: "code-line",
631
+ id: generateId(),
632
+ children: [{ text: line }]
633
+ });
634
+ continue;
635
+ }
636
+ if (line.trim() === "") {
637
+ flushParagraph();
638
+ result.push({
639
+ type: "paragraph",
640
+ id: generateId(),
641
+ children: [{ text: "" }]
642
+ });
643
+ continue;
644
+ }
645
+ paragraphLines.push(line);
646
+ }
647
+ flushParagraph();
648
+ return result.length > 0 ? result : [{ type: "paragraph", id: generateId(), children: [{ text: "" }] }];
649
+ }
650
+ function RenderElement({
651
+ attributes,
652
+ children,
653
+ element
654
+ }) {
655
+ const el = element;
656
+ switch (el.type) {
657
+ case "heading":
658
+ return /* @__PURE__ */ jsxRuntime.jsx(
659
+ "p",
660
+ {
661
+ ...attributes,
662
+ className: `${editorClass("heading")} ${editorClass(`heading-${el.level ?? 1}`)}`,
663
+ children
664
+ }
665
+ );
666
+ case "code-fence":
667
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { ...attributes, className: editorClass("code-fence"), children });
668
+ case "code-line":
669
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { ...attributes, className: editorClass("code-line"), children });
670
+ case "blockquote":
671
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { ...attributes, className: editorClass("blockquote"), children });
672
+ case "list-item":
673
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { ...attributes, className: editorClass("list-item"), "data-list": true, children });
674
+ default:
675
+ return /* @__PURE__ */ jsxRuntime.jsx("p", { ...attributes, children });
676
+ }
677
+ }
678
+ function RenderLeaf({ attributes, children, leaf }) {
679
+ const l = leaf;
680
+ if (l.boldMarker || l.italicMarker || l.strikeMarker) {
681
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { ...attributes, className: editorClass("marker"), children });
682
+ }
683
+ if (l.codeMarker) {
684
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { ...attributes, className: editorClass("backtick"), children });
685
+ }
686
+ let content = children;
687
+ if (l.bold) content = /* @__PURE__ */ jsxRuntime.jsx("strong", { children: content });
688
+ if (l.italic) content = /* @__PURE__ */ jsxRuntime.jsx("em", { children: content });
689
+ if (l.strikethrough) content = /* @__PURE__ */ jsxRuntime.jsx("del", { children: content });
690
+ if (l.inlineCode) content = /* @__PURE__ */ jsxRuntime.jsx("code", { children: content });
691
+ if (l.hljs) {
692
+ content = /* @__PURE__ */ jsxRuntime.jsx("span", { className: l.hljs, children: content });
693
+ }
694
+ if (l.remoteCursor) {
695
+ content = /* @__PURE__ */ jsxRuntime.jsx(
696
+ "span",
697
+ {
698
+ className: editorClass("remote-cursor"),
699
+ style: { backgroundColor: `${l.remoteCursor}30` },
700
+ children: content
701
+ }
702
+ );
703
+ }
704
+ if (l.remoteCursorCaret) {
705
+ content = /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
706
+ /* @__PURE__ */ jsxRuntime.jsx(
707
+ "span",
708
+ {
709
+ className: editorClass("remote-caret"),
710
+ style: { borderColor: l.remoteCursor },
711
+ contentEditable: false
712
+ }
713
+ ),
714
+ content
715
+ ] });
716
+ }
717
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { ...attributes, children: content });
718
+ }
719
+ function serialize(nodes) {
720
+ const entries = [];
721
+ for (const node of nodes) {
722
+ const text = slate.Node.string(node);
723
+ const type = node.type;
724
+ if (type === "heading") {
725
+ const level = node.level ?? 1;
726
+ const prefix = "#".repeat(level);
727
+ entries.push({ text: `${prefix} ${text}`, type });
728
+ continue;
729
+ }
730
+ if (type === "blockquote") {
731
+ const lines = text.split("\n").filter((l) => l.trim() !== "");
732
+ if (lines.length === 0) {
733
+ entries.push({ text: "> ", type });
734
+ } else {
735
+ const prefixed = lines.map((line) => {
736
+ const escaped = line.replace(/^(>+)/g, (m) => "\\>".repeat(m.length));
737
+ return "> " + escaped;
738
+ }).join("\n>\n");
739
+ entries.push({ text: prefixed, type });
740
+ }
741
+ continue;
742
+ }
743
+ if (type === "paragraph" && !text.trim()) {
744
+ entries.push({ text: "", type });
745
+ continue;
746
+ }
747
+ entries.push({ text, type });
748
+ }
749
+ const codeTypes = /* @__PURE__ */ new Set(["code-fence", "code-line"]);
750
+ let result = "";
751
+ for (let i = 0; i < entries.length; i++) {
752
+ if (i > 0) {
753
+ const prev = entries[i - 1].type;
754
+ const curr = entries[i].type;
755
+ const sameGroup = prev === "blockquote" && curr === "blockquote" || prev === "list-item" && curr === "list-item" || codeTypes.has(prev) && codeTypes.has(curr);
756
+ result += sameGroup ? "\n" : "\n\n";
757
+ }
758
+ result += entries[i].text;
759
+ }
760
+ return result.replace(/\n{3,}/g, "\n\n").trim();
761
+ }
762
+ var HEADING_RE2 = /^#{1,6}$/;
763
+ function withMarkdown(editor, decorationsRef) {
764
+ const { insertBreak, insertData, insertText } = editor;
765
+ editor.insertBreak = () => {
766
+ const { selection } = editor;
767
+ if (!selection) return insertBreak();
768
+ const [match] = slate.Editor.nodes(editor, {
769
+ match: (n) => slate.Element.isElement(n)
770
+ });
771
+ if (!match) return insertBreak();
772
+ const [node, path] = match;
773
+ const element = node;
774
+ const text = slate.Node.string(node);
775
+ const deco = decorationsRef.current;
776
+ if (deco.codeBlocks && element.type === "paragraph" && text.startsWith("```")) {
777
+ slate.Transforms.setNodes(editor, {
778
+ type: "code-fence"
779
+ });
780
+ const newLine = {
781
+ type: "code-line",
782
+ id: generateId(),
783
+ children: [{ text: "" }]
784
+ };
785
+ slate.Transforms.insertNodes(editor, newLine, { at: slate.Path.next(path) });
786
+ slate.Transforms.select(editor, slate.Editor.start(editor, slate.Path.next(path)));
787
+ return;
788
+ }
789
+ if (element.type === "code-line" && text.trim() === "```") {
790
+ slate.Transforms.setNodes(editor, {
791
+ type: "code-fence"
792
+ });
793
+ const newParagraph = {
794
+ type: "paragraph",
795
+ id: generateId(),
796
+ children: [{ text: "" }]
797
+ };
798
+ slate.Transforms.insertNodes(editor, newParagraph, { at: slate.Path.next(path) });
799
+ slate.Transforms.select(editor, slate.Editor.start(editor, slate.Path.next(path)));
800
+ return;
801
+ }
802
+ if (element.type === "code-fence") {
803
+ const prevIdx = path[0] - 1;
804
+ const isClosing = prevIdx >= 0 && editor.children[prevIdx].type === "code-line";
805
+ const newNode = isClosing ? { type: "paragraph", id: generateId(), children: [{ text: "" }] } : { type: "code-line", id: generateId(), children: [{ text: "" }] };
806
+ slate.Transforms.insertNodes(editor, newNode, { at: slate.Path.next(path) });
807
+ slate.Transforms.select(editor, slate.Editor.start(editor, slate.Path.next(path)));
808
+ return;
809
+ }
810
+ if (element.type === "blockquote") {
811
+ const text2 = slate.Node.string(node);
812
+ if (!text2.trim()) {
813
+ slate.Transforms.setNodes(editor, {
814
+ type: "paragraph"
815
+ });
816
+ return;
817
+ }
818
+ const newParagraph = {
819
+ type: "paragraph",
820
+ id: generateId(),
821
+ children: [{ text: "" }]
822
+ };
823
+ slate.Transforms.insertNodes(editor, newParagraph, {
824
+ at: slate.Path.next(path)
825
+ });
826
+ slate.Transforms.select(editor, slate.Editor.start(editor, slate.Path.next(path)));
827
+ return;
828
+ }
829
+ if (element.type === "heading") {
830
+ if (!text.trim()) {
831
+ slate.Transforms.setNodes(editor, {
832
+ type: "paragraph"
833
+ });
834
+ slate.Transforms.unsetNodes(editor, "level");
835
+ return;
836
+ }
837
+ const newParagraph = {
838
+ type: "paragraph",
839
+ id: generateId(),
840
+ children: [{ text: "" }]
841
+ };
842
+ slate.Transforms.insertNodes(editor, newParagraph, { at: slate.Path.next(path) });
843
+ slate.Transforms.select(editor, slate.Editor.start(editor, slate.Path.next(path)));
844
+ return;
845
+ }
846
+ if (element.type === "code-line") {
847
+ const newLine = {
848
+ type: "code-line",
849
+ id: generateId(),
850
+ children: [{ text: "" }]
851
+ };
852
+ slate.Transforms.insertNodes(editor, newLine, { at: slate.Path.next(path) });
853
+ slate.Transforms.select(editor, slate.Editor.start(editor, slate.Path.next(path)));
854
+ return;
855
+ }
856
+ if (element.type === "list-item") {
857
+ const text2 = slate.Node.string(node);
858
+ if (text2.trim() === "-" || text2.trim() === "*" || text2.trim() === "+" || text2 === "- " || text2 === "* " || text2 === "+ ") {
859
+ slate.Transforms.delete(editor, {
860
+ at: {
861
+ anchor: slate.Editor.start(editor, path),
862
+ focus: slate.Editor.end(editor, path)
863
+ }
864
+ });
865
+ slate.Transforms.setNodes(editor, {
866
+ type: "paragraph"
867
+ });
868
+ return;
869
+ }
870
+ const marker = text2.match(/^([-*+] )/)?.[1] || "- ";
871
+ const newItem = {
872
+ type: "list-item",
873
+ id: generateId(),
874
+ children: [{ text: marker }]
875
+ };
876
+ slate.Transforms.insertNodes(editor, newItem, { at: slate.Path.next(path) });
877
+ slate.Transforms.select(editor, slate.Editor.end(editor, slate.Path.next(path)));
878
+ return;
879
+ }
880
+ insertBreak();
881
+ slate.Transforms.setNodes(editor, {
882
+ type: "paragraph"
883
+ });
884
+ };
885
+ editor.insertSoftBreak = () => {
886
+ const [match] = slate.Editor.nodes(editor, {
887
+ match: (n) => slate.Element.isElement(n)
888
+ });
889
+ if (match) {
890
+ const [node, path] = match;
891
+ const element = node;
892
+ if (element.type === "blockquote") {
893
+ const newBq = {
894
+ type: "blockquote",
895
+ id: generateId(),
896
+ children: [{ text: "" }]
897
+ };
898
+ slate.Transforms.insertNodes(editor, newBq, { at: slate.Path.next(path) });
899
+ slate.Transforms.select(editor, slate.Editor.start(editor, slate.Path.next(path)));
900
+ return;
901
+ }
902
+ if (element.type === "code-line") {
903
+ const newLine = {
904
+ type: "code-line",
905
+ id: generateId(),
906
+ children: [{ text: "" }]
907
+ };
908
+ slate.Transforms.insertNodes(editor, newLine, { at: slate.Path.next(path) });
909
+ slate.Transforms.select(editor, slate.Editor.start(editor, slate.Path.next(path)));
910
+ return;
911
+ }
912
+ }
913
+ editor.insertBreak();
914
+ };
915
+ editor.insertText = (text) => {
916
+ const { selection } = editor;
917
+ if (!selection) return insertText(text);
918
+ const [match] = slate.Editor.nodes(editor, {
919
+ match: (n) => slate.Element.isElement(n)
920
+ });
921
+ if (!match) return insertText(text);
922
+ const [node, path] = match;
923
+ const element = node;
924
+ const currentText = slate.Node.string(node);
925
+ const deco = decorationsRef.current;
926
+ if (element.type === "code-line" && currentText === "```" && text !== "" && text !== "\n") {
927
+ slate.Transforms.setNodes(editor, {
928
+ type: "code-fence"
929
+ });
930
+ const newParagraph = {
931
+ type: "paragraph",
932
+ id: generateId(),
933
+ children: [{ text }]
934
+ };
935
+ slate.Transforms.insertNodes(editor, newParagraph, { at: slate.Path.next(path) });
936
+ slate.Transforms.select(editor, slate.Editor.end(editor, slate.Path.next(path)));
937
+ return;
938
+ }
939
+ if (deco.blockquotes && element.type === "paragraph" && text === " " && currentText === ">") {
940
+ slate.Transforms.delete(editor, {
941
+ at: {
942
+ anchor: slate.Editor.start(editor, path),
943
+ focus: slate.Editor.end(editor, path)
944
+ }
945
+ });
946
+ slate.Transforms.setNodes(editor, {
947
+ type: "blockquote"
948
+ });
949
+ return;
950
+ }
951
+ const headingLevel = currentText.length;
952
+ const headingKey = `heading${headingLevel}`;
953
+ if (element.type === "paragraph" && text === " " && HEADING_RE2.test(currentText) && deco[headingKey]) {
954
+ const level = headingLevel;
955
+ slate.Transforms.delete(editor, {
956
+ at: {
957
+ anchor: slate.Editor.start(editor, path),
958
+ focus: slate.Editor.end(editor, path)
959
+ }
960
+ });
961
+ slate.Transforms.setNodes(editor, {
962
+ type: "heading",
963
+ level
964
+ });
965
+ return;
966
+ }
967
+ if (deco.lists && element.type === "paragraph" && text === " " && (currentText === "-" || currentText === "*" || currentText === "+")) {
968
+ insertText(text);
969
+ slate.Transforms.setNodes(editor, {
970
+ type: "list-item"
971
+ });
972
+ return;
973
+ }
974
+ if (element.type === "code-fence") {
975
+ const prevIdx = path[0] - 1;
976
+ if (prevIdx >= 0) {
977
+ const prev = editor.children[prevIdx];
978
+ if (prev.type === "code-line" && currentText === "```") {
979
+ const newParagraph = {
980
+ type: "paragraph",
981
+ id: generateId(),
982
+ children: [{ text }]
983
+ };
984
+ slate.Transforms.insertNodes(editor, newParagraph, {
985
+ at: slate.Path.next(path)
986
+ });
987
+ slate.Transforms.select(editor, slate.Editor.end(editor, slate.Path.next(path)));
988
+ return;
989
+ }
990
+ }
991
+ }
992
+ insertText(text);
993
+ };
994
+ editor.insertData = (data) => {
995
+ const text = data.getData("text/plain");
996
+ if (text) {
997
+ const nodes = deserialize(text, decorationsRef.current);
998
+ slate.Transforms.insertNodes(editor, nodes);
999
+ return;
1000
+ }
1001
+ insertData(data);
1002
+ };
1003
+ return editor;
1004
+ }
1005
+ var IS_SERVER = typeof window === "undefined";
1006
+ var DEFAULT_DECORATIONS = {
1007
+ heading1: true,
1008
+ heading2: true,
1009
+ heading3: true,
1010
+ heading4: true,
1011
+ heading5: true,
1012
+ heading6: true,
1013
+ lists: true,
1014
+ blockquotes: true,
1015
+ codeBlocks: true
1016
+ };
1017
+ function InkwellEditor({
1018
+ content,
1019
+ onChange,
1020
+ className,
1021
+ placeholder,
1022
+ plugins: userPlugins = [],
1023
+ rehypePlugins,
1024
+ decorations,
1025
+ collaboration,
1026
+ bubbleMenu = true
1027
+ }) {
1028
+ const resolvedDecorations = react.useMemo(
1029
+ () => ({ ...DEFAULT_DECORATIONS, ...decorations }),
1030
+ [decorations]
1031
+ );
1032
+ const decorationsRef = react.useRef(resolvedDecorations);
1033
+ decorationsRef.current = resolvedDecorations;
1034
+ const plugins = react.useMemo(() => {
1035
+ const builtIn = bubbleMenu ? [createBubbleMenuPlugin()] : [];
1036
+ return [...builtIn, ...userPlugins];
1037
+ }, [userPlugins, bubbleMenu]);
1038
+ const editor = react.useMemo(() => {
1039
+ if (IS_SERVER) return null;
1040
+ const base = withNodeId(slateReact.withReact(slate.createEditor()));
1041
+ if (collaboration) {
1042
+ const { sharedType, awareness, user } = collaboration;
1043
+ const yjsEditor = core.withYjs(base, sharedType, { autoConnect: false });
1044
+ const cursorEditor = core.withCursors(yjsEditor, awareness, {
1045
+ data: user
1046
+ });
1047
+ const historyEditor = core.withYHistory(cursorEditor);
1048
+ return withMarkdown(historyEditor, decorationsRef);
1049
+ }
1050
+ return withMarkdown(slateHistory.withHistory(base), decorationsRef);
1051
+ }, []);
1052
+ if (!editor) return null;
1053
+ react.useEffect(() => {
1054
+ if (!collaboration || !core.YjsEditor.isYjsEditor(editor)) return;
1055
+ core.YjsEditor.connect(editor);
1056
+ return () => {
1057
+ core.YjsEditor.disconnect(editor);
1058
+ };
1059
+ }, [editor, collaboration]);
1060
+ const [cursorVersion, setCursorVersion] = react.useState(0);
1061
+ react.useEffect(() => {
1062
+ if (!collaboration || !core.CursorEditor.isCursorEditor(editor)) return;
1063
+ const handleChange2 = () => setCursorVersion((v) => v + 1);
1064
+ core.CursorEditor.on(editor, "change", handleChange2);
1065
+ return () => {
1066
+ core.CursorEditor.off(editor, "change", handleChange2);
1067
+ };
1068
+ }, [editor, collaboration]);
1069
+ const remoteCursorRanges = react.useMemo(() => {
1070
+ if (!collaboration || !core.CursorEditor.isCursorEditor(editor)) return [];
1071
+ const ranges = [];
1072
+ const states = core.CursorEditor.cursorStates(editor);
1073
+ for (const [, state] of Object.entries(states)) {
1074
+ if (!state.relativeSelection) continue;
1075
+ const data = state.data;
1076
+ if (!data) continue;
1077
+ try {
1078
+ const { anchor, focus } = state.relativeSelection;
1079
+ const anchorPoint = core.relativePositionToSlatePoint(
1080
+ collaboration.sharedType,
1081
+ editor,
1082
+ anchor
1083
+ );
1084
+ const focusPoint = core.relativePositionToSlatePoint(
1085
+ collaboration.sharedType,
1086
+ editor,
1087
+ focus
1088
+ );
1089
+ if (!anchorPoint || !focusPoint) continue;
1090
+ const range = { anchor: anchorPoint, focus: focusPoint };
1091
+ if (slate.Range.isCollapsed(range)) {
1092
+ ranges.push({
1093
+ ...range,
1094
+ remoteCursor: data.color,
1095
+ remoteCursorCaret: true
1096
+ });
1097
+ } else {
1098
+ ranges.push({
1099
+ ...range,
1100
+ remoteCursor: data.color
1101
+ });
1102
+ }
1103
+ } catch {
1104
+ }
1105
+ }
1106
+ return ranges;
1107
+ }, [editor, collaboration, cursorVersion]);
1108
+ const initialValue = react.useMemo(
1109
+ () => collaboration ? [
1110
+ {
1111
+ type: "paragraph",
1112
+ id: generateId(),
1113
+ children: [{ text: "" }]
1114
+ }
1115
+ ] : deserialize(content, resolvedDecorations),
1116
+ []
1117
+ );
1118
+ const lastContent = react.useRef(content);
1119
+ const isInternalChange = react.useRef(false);
1120
+ react.useEffect(() => {
1121
+ if (collaboration) return;
1122
+ if (isInternalChange.current) {
1123
+ isInternalChange.current = false;
1124
+ return;
1125
+ }
1126
+ if (content === lastContent.current) return;
1127
+ const newValue = deserialize(content, resolvedDecorations);
1128
+ editor.children = newValue;
1129
+ slate.Transforms.select(editor, slate.Editor.start(editor, []));
1130
+ editor.onChange();
1131
+ lastContent.current = content;
1132
+ }, [content, editor, collaboration]);
1133
+ const handleChange = react.useCallback(
1134
+ (value) => {
1135
+ const isAstChange = editor.operations.some(
1136
+ (op) => op.type !== "set_selection"
1137
+ );
1138
+ if (!isAstChange) return;
1139
+ if (!onChange) return;
1140
+ const md = serialize(value);
1141
+ if (collaboration) {
1142
+ onChange(md);
1143
+ } else {
1144
+ if (md !== lastContent.current) {
1145
+ lastContent.current = md;
1146
+ isInternalChange.current = true;
1147
+ onChange(md);
1148
+ }
1149
+ }
1150
+ },
1151
+ [editor, onChange, collaboration]
1152
+ );
1153
+ const decorate = react.useCallback(
1154
+ (entry) => {
1155
+ const ranges = computeDecorations(entry, editor, rehypePlugins);
1156
+ if (remoteCursorRanges.length > 0) {
1157
+ const [, path] = entry;
1158
+ for (const cursorRange of remoteCursorRanges) {
1159
+ try {
1160
+ const intersection = slate.Range.intersection(
1161
+ cursorRange,
1162
+ slate.Editor.range(editor, path)
1163
+ );
1164
+ if (intersection) {
1165
+ ranges.push({
1166
+ ...intersection,
1167
+ remoteCursor: cursorRange.remoteCursor,
1168
+ remoteCursorCaret: cursorRange.remoteCursorCaret
1169
+ });
1170
+ }
1171
+ } catch {
1172
+ }
1173
+ }
1174
+ }
1175
+ return ranges;
1176
+ },
1177
+ [editor, rehypePlugins, remoteCursorRanges]
1178
+ );
1179
+ const [activePlugin, setActivePlugin] = react.useState(null);
1180
+ const pluginPositionRef = react.useRef({
1181
+ top: 0,
1182
+ left: 0
1183
+ });
1184
+ const wrapperRef = react.useRef(null);
1185
+ const editorElRef = react.useRef(null);
1186
+ const getCursorPosition = react.useCallback(() => {
1187
+ try {
1188
+ const domSelection = window.getSelection();
1189
+ if (!domSelection || domSelection.rangeCount === 0)
1190
+ return { top: 0, left: 0 };
1191
+ const range = domSelection.getRangeAt(0);
1192
+ let rect = range.getBoundingClientRect();
1193
+ if (rect.width === 0 && rect.height === 0 && domSelection.anchorNode) {
1194
+ const node = domSelection.anchorNode instanceof HTMLElement ? domSelection.anchorNode : domSelection.anchorNode.parentElement;
1195
+ if (node) rect = node.getBoundingClientRect();
1196
+ }
1197
+ const wrapperEl = wrapperRef.current;
1198
+ if (!wrapperEl) return { top: 0, left: 0 };
1199
+ const wrapperRect = wrapperEl.getBoundingClientRect();
1200
+ return {
1201
+ top: rect.bottom - wrapperRect.top + 4,
1202
+ left: rect.left - wrapperRect.left
1203
+ };
1204
+ } catch {
1205
+ return { top: 0, left: 0 };
1206
+ }
1207
+ }, []);
1208
+ const wrapSelection = react.useCallback(
1209
+ (before, after) => {
1210
+ const { selection } = editor;
1211
+ if (!selection) return;
1212
+ const selectedText = slate.Editor.string(editor, selection);
1213
+ if (selectedText.startsWith(before) && selectedText.endsWith(after) && selectedText.length >= before.length + after.length) {
1214
+ slate.Transforms.delete(editor);
1215
+ slate.Transforms.insertText(
1216
+ editor,
1217
+ selectedText.slice(before.length, -after.length || void 0)
1218
+ );
1219
+ return;
1220
+ }
1221
+ const { anchor, focus } = selection;
1222
+ const [start, end] = slate.Range.isForward(selection) ? [anchor, focus] : [focus, anchor];
1223
+ const beforeStart = {
1224
+ path: start.path,
1225
+ offset: Math.max(0, start.offset - before.length)
1226
+ };
1227
+ const afterEnd = { path: end.path, offset: end.offset + after.length };
1228
+ try {
1229
+ const textBefore = slate.Editor.string(editor, {
1230
+ anchor: beforeStart,
1231
+ focus: start
1232
+ });
1233
+ const textAfter = slate.Editor.string(editor, {
1234
+ anchor: end,
1235
+ focus: afterEnd
1236
+ });
1237
+ if (textBefore === before && textAfter === after) {
1238
+ const expandedRange = { anchor: beforeStart, focus: afterEnd };
1239
+ slate.Transforms.select(editor, expandedRange);
1240
+ slate.Transforms.delete(editor);
1241
+ slate.Transforms.insertText(editor, selectedText);
1242
+ return;
1243
+ }
1244
+ } catch {
1245
+ }
1246
+ slate.Transforms.delete(editor);
1247
+ slate.Transforms.insertText(editor, `${before}${selectedText}${after}`);
1248
+ },
1249
+ [editor]
1250
+ );
1251
+ const insertTextAtCursor = react.useCallback(
1252
+ (text) => {
1253
+ slateReact.ReactEditor.focus(editor);
1254
+ const nodes = deserialize(text);
1255
+ slate.Transforms.insertFragment(editor, nodes);
1256
+ },
1257
+ [editor]
1258
+ );
1259
+ const dismissPlugin = react.useCallback(() => {
1260
+ setActivePlugin(null);
1261
+ slateReact.ReactEditor.focus(editor);
1262
+ }, [editor]);
1263
+ const handlePluginSelect = react.useCallback(
1264
+ (text) => {
1265
+ const triggerKey = activePlugin?.trigger?.key;
1266
+ const isCharTrigger = triggerKey && !triggerKey.includes("+");
1267
+ dismissPlugin();
1268
+ requestAnimationFrame(() => {
1269
+ slateReact.ReactEditor.focus(editor);
1270
+ if (isCharTrigger) {
1271
+ slate.Transforms.delete(editor, {
1272
+ distance: 1,
1273
+ unit: "character",
1274
+ reverse: true
1275
+ });
1276
+ }
1277
+ insertTextAtCursor(text);
1278
+ });
1279
+ },
1280
+ [dismissPlugin, insertTextAtCursor, activePlugin, editor]
1281
+ );
1282
+ const makePluginProps = (plugin) => ({
1283
+ active: plugin.trigger ? activePlugin === plugin : true,
1284
+ query: "",
1285
+ onSelect: handlePluginSelect,
1286
+ onDismiss: dismissPlugin,
1287
+ position: pluginPositionRef.current,
1288
+ editorRef: editorElRef,
1289
+ wrapSelection
1290
+ });
1291
+ const handleKeyDown = react.useCallback(
1292
+ (event) => {
1293
+ if (activePlugin) {
1294
+ if (event.key === "Escape") {
1295
+ event.preventDefault();
1296
+ dismissPlugin();
1297
+ }
1298
+ return;
1299
+ }
1300
+ for (const plugin of plugins) {
1301
+ plugin.onKeyDown?.(event, { wrapSelection });
1302
+ if (event.defaultPrevented) return;
1303
+ }
1304
+ for (const plugin of plugins) {
1305
+ const t = plugin.trigger;
1306
+ if (!t) continue;
1307
+ const parts = t.key.toLowerCase().split("+").map((s) => s.trim());
1308
+ const key = parts[parts.length - 1];
1309
+ const mods = new Set(parts.slice(0, -1));
1310
+ const hasModifiers = mods.size > 0;
1311
+ const needCtrl = mods.has("control") || mods.has("ctrl");
1312
+ const needMeta = mods.has("meta") || mods.has("cmd") || mods.has("command");
1313
+ const needAlt = mods.has("alt");
1314
+ const needShift = mods.has("shift");
1315
+ const keyMatch = event.key.toLowerCase() === key;
1316
+ const modMatch = hasModifiers ? event.ctrlKey === needCtrl && event.metaKey === needMeta && event.altKey === needAlt && event.shiftKey === needShift : !event.ctrlKey && !event.metaKey;
1317
+ if (keyMatch && modMatch) {
1318
+ if (hasModifiers) event.preventDefault();
1319
+ pluginPositionRef.current = getCursorPosition();
1320
+ setActivePlugin(plugin);
1321
+ return;
1322
+ }
1323
+ }
1324
+ if (event.key === "a" && (event.metaKey || event.ctrlKey) && !slate.Node.string(editor).trim()) {
1325
+ event.preventDefault();
1326
+ return;
1327
+ }
1328
+ },
1329
+ [
1330
+ plugins,
1331
+ activePlugin,
1332
+ editor,
1333
+ getCursorPosition,
1334
+ dismissPlugin,
1335
+ wrapSelection
1336
+ ]
1337
+ );
1338
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1339
+ "div",
1340
+ {
1341
+ ref: wrapperRef,
1342
+ className: `inkwell-editor-wrapper ${className ?? ""}`,
1343
+ children: [
1344
+ activePlugin && /* @__PURE__ */ jsxRuntime.jsx(
1345
+ "div",
1346
+ {
1347
+ className: "inkwell-plugin-backdrop",
1348
+ style: {
1349
+ position: "fixed",
1350
+ inset: 0,
1351
+ zIndex: 999,
1352
+ background: "transparent"
1353
+ },
1354
+ onMouseDown: dismissPlugin
1355
+ }
1356
+ ),
1357
+ plugins.map((plugin) => {
1358
+ const props = makePluginProps(plugin);
1359
+ if (!props.active) return null;
1360
+ return /* @__PURE__ */ jsxRuntime.jsx(react.Fragment, { children: plugin.render(props) }, plugin.name);
1361
+ }),
1362
+ /* @__PURE__ */ jsxRuntime.jsx(
1363
+ slateReact.Slate,
1364
+ {
1365
+ editor,
1366
+ initialValue,
1367
+ onChange: handleChange,
1368
+ children: /* @__PURE__ */ jsxRuntime.jsx(
1369
+ slateReact.Editable,
1370
+ {
1371
+ ref: editorElRef,
1372
+ className: "inkwell-editor",
1373
+ renderElement: RenderElement,
1374
+ renderLeaf: RenderLeaf,
1375
+ decorate,
1376
+ placeholder: placeholder ?? "Start writing...",
1377
+ spellCheck: true,
1378
+ role: "textbox",
1379
+ "aria-multiline": true,
1380
+ "aria-placeholder": placeholder,
1381
+ "data-placeholder": placeholder ?? "Start writing...",
1382
+ onKeyDown: handleKeyDown
1383
+ }
1384
+ )
1385
+ }
1386
+ )
1387
+ ]
1388
+ }
1389
+ );
1390
+ }
1391
+ var cls2 = pluginClass("snippets");
1392
+ function SnippetPicker({
1393
+ snippets,
1394
+ onSelect,
1395
+ onDismiss
1396
+ }) {
1397
+ const [selectedIndex, setSelectedIndex] = react.useState(0);
1398
+ const [search, setSearch] = react.useState("");
1399
+ const filtered = snippets.filter(
1400
+ (s) => s.title.toLowerCase().includes(search.toLowerCase())
1401
+ );
1402
+ const focusRef = react.useCallback((el) => {
1403
+ if (el) requestAnimationFrame(() => el.focus());
1404
+ }, []);
1405
+ const activeItemRef = react.useCallback((el) => {
1406
+ if (el && typeof el.scrollIntoView === "function") {
1407
+ el.scrollIntoView({ block: "nearest" });
1408
+ }
1409
+ }, []);
1410
+ const handleSearchKeyDown = react.useCallback(
1411
+ (e) => {
1412
+ switch (e.key) {
1413
+ case "ArrowDown":
1414
+ e.preventDefault();
1415
+ setSelectedIndex((prev) => prev < filtered.length - 1 ? prev + 1 : 0);
1416
+ break;
1417
+ case "ArrowUp":
1418
+ e.preventDefault();
1419
+ setSelectedIndex((prev) => prev > 0 ? prev - 1 : filtered.length - 1);
1420
+ break;
1421
+ case "Enter":
1422
+ e.preventDefault();
1423
+ if (filtered[selectedIndex]) {
1424
+ onSelect(filtered[selectedIndex].content);
1425
+ }
1426
+ break;
1427
+ case "Escape":
1428
+ e.preventDefault();
1429
+ onDismiss();
1430
+ break;
1431
+ }
1432
+ },
1433
+ [filtered, selectedIndex, onSelect, onDismiss]
1434
+ );
1435
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: cls2("picker"), children: [
1436
+ /* @__PURE__ */ jsxRuntime.jsx(
1437
+ "input",
1438
+ {
1439
+ ref: focusRef,
1440
+ type: "text",
1441
+ placeholder: "Search snippets...",
1442
+ value: search,
1443
+ onChange: (e) => {
1444
+ setSearch(e.target.value);
1445
+ setSelectedIndex(0);
1446
+ },
1447
+ onKeyDown: handleSearchKeyDown,
1448
+ className: cls2("search")
1449
+ }
1450
+ ),
1451
+ filtered.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("div", { className: cls2("empty"), children: "No snippets found" }) : /* @__PURE__ */ jsxRuntime.jsx("div", { children: filtered.map((snippet, i) => /* @__PURE__ */ jsxRuntime.jsxs(
1452
+ "div",
1453
+ {
1454
+ ref: i === selectedIndex ? activeItemRef : void 0,
1455
+ "data-snippet-item": true,
1456
+ className: `${cls2("item")} ${i === selectedIndex ? cls2("item-active") : ""}`,
1457
+ onMouseEnter: () => setSelectedIndex(i),
1458
+ onClick: () => onSelect(snippet.content),
1459
+ children: [
1460
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cls2("title"), children: snippet.title }),
1461
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: cls2("preview"), children: snippet.content.length > 80 ? `${snippet.content.slice(0, 80)}...` : snippet.content })
1462
+ ]
1463
+ },
1464
+ snippet.title
1465
+ )) })
1466
+ ] });
1467
+ }
1468
+ function createSnippetsPlugin(options) {
1469
+ const { snippets, key = "[" } = options;
1470
+ return {
1471
+ name: "snippets",
1472
+ trigger: { key },
1473
+ render: (props) => /* @__PURE__ */ jsxRuntime.jsx(
1474
+ "div",
1475
+ {
1476
+ className: cls2("popup"),
1477
+ style: {
1478
+ position: "absolute",
1479
+ top: props.position.top,
1480
+ left: props.position.left,
1481
+ zIndex: 1001
1482
+ },
1483
+ onMouseDown: (e) => e.preventDefault(),
1484
+ children: /* @__PURE__ */ jsxRuntime.jsx(SnippetPicker, { snippets, ...props })
1485
+ }
1486
+ )
1487
+ };
1488
+ }
1489
+ var processor = unified.unified().use(rehypeParse__default.default, { fragment: true }).use(rehypeRemark__default.default).use(remarkGfm__default.default).use(remarkNoTables).use(remarkStringify__default.default, {
1490
+ bullet: "-",
1491
+ emphasis: "_",
1492
+ strong: "*",
1493
+ fences: true,
1494
+ handlers: {
1495
+ // Don't escape markdown syntax in text nodes — this allows users to
1496
+ // type markdown directly in the editor (e.g. _foo_ for italic,
1497
+ // # for headings) and have it rendered via the markdown pipeline.
1498
+ text(node) {
1499
+ return node.value;
1500
+ }
1501
+ }
1502
+ });
1503
+ function serializeToMarkdown(html) {
1504
+ return String(processor.processSync(html)).trim();
1505
+ }
1506
+ function CopyCodeBlock({
1507
+ children,
1508
+ ...props
1509
+ }) {
1510
+ const preRef = react.useRef(null);
1511
+ const [copied, setCopied] = react.useState(false);
1512
+ const handleCopy = react.useCallback(() => {
1513
+ const text = preRef.current?.textContent ?? "";
1514
+ navigator.clipboard.writeText(text).catch(() => {
1515
+ });
1516
+ setCopied(true);
1517
+ setTimeout(() => setCopied(false), 2e3);
1518
+ }, []);
1519
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inkwell-renderer-code-block", children: [
1520
+ /* @__PURE__ */ jsxRuntime.jsx(
1521
+ "button",
1522
+ {
1523
+ type: "button",
1524
+ className: "inkwell-renderer-copy-btn",
1525
+ onClick: handleCopy,
1526
+ "aria-label": "Copy code",
1527
+ children: copied ? /* @__PURE__ */ jsxRuntime.jsx(
1528
+ "svg",
1529
+ {
1530
+ xmlns: "http://www.w3.org/2000/svg",
1531
+ width: "14",
1532
+ height: "14",
1533
+ viewBox: "0 0 24 24",
1534
+ fill: "none",
1535
+ stroke: "currentColor",
1536
+ strokeWidth: "2",
1537
+ strokeLinecap: "round",
1538
+ strokeLinejoin: "round",
1539
+ children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "20 6 9 17 4 12" })
1540
+ }
1541
+ ) : /* @__PURE__ */ jsxRuntime.jsxs(
1542
+ "svg",
1543
+ {
1544
+ xmlns: "http://www.w3.org/2000/svg",
1545
+ width: "14",
1546
+ height: "14",
1547
+ viewBox: "0 0 24 24",
1548
+ fill: "none",
1549
+ stroke: "currentColor",
1550
+ strokeWidth: "2",
1551
+ strokeLinecap: "round",
1552
+ strokeLinejoin: "round",
1553
+ children: [
1554
+ /* @__PURE__ */ jsxRuntime.jsx("rect", { width: "14", height: "14", x: "8", y: "8", rx: "2", ry: "2" }),
1555
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" })
1556
+ ]
1557
+ }
1558
+ )
1559
+ }
1560
+ ),
1561
+ /* @__PURE__ */ jsxRuntime.jsx("pre", { ref: preRef, ...props, children })
1562
+ ] });
1563
+ }
1564
+ function createProcessor(options = {}) {
1565
+ const proc = unified.unified().use(remarkParse__default.default).use(remarkGfm__default.default).use(remarkNoTables).use(remarkFlattenBlockquotes).use(remarkRehype__default.default);
1566
+ const plugins = options.rehypePlugins ?? [
1567
+ [rehypeHighlight__default.default, { detect: true }]
1568
+ ];
1569
+ for (const plugin of plugins) {
1570
+ if (Array.isArray(plugin)) {
1571
+ proc.use(plugin[0], plugin[1]);
1572
+ } else {
1573
+ proc.use(plugin);
1574
+ }
1575
+ }
1576
+ proc.use(rehypeSanitize__default.default, {
1577
+ ...rehypeSanitize.defaultSchema,
1578
+ tagNames: [...rehypeSanitize.defaultSchema.tagNames ?? [], "span"],
1579
+ attributes: {
1580
+ ...rehypeSanitize.defaultSchema.attributes,
1581
+ code: ["className"],
1582
+ span: ["className"]
1583
+ }
1584
+ });
1585
+ proc.use(rehypeReact__default.default, {
1586
+ createElement: react.createElement,
1587
+ Fragment: react.Fragment,
1588
+ jsx: jsxRuntime.jsx,
1589
+ jsxs: jsxRuntime.jsxs,
1590
+ components: options.components ?? {}
1591
+ });
1592
+ return proc;
1593
+ }
1594
+ function escapeBareBq2(markdown) {
1595
+ return markdown.replace(/^>(?=\S)/gm, "\\>");
1596
+ }
1597
+ function parseMarkdown(markdown, components, rehypePlugins) {
1598
+ const processor2 = createProcessor({ components, rehypePlugins });
1599
+ const file = processor2.processSync(escapeBareBq2(markdown));
1600
+ return file.result;
1601
+ }
1602
+ function InkwellRenderer({
1603
+ content,
1604
+ className,
1605
+ components,
1606
+ rehypePlugins,
1607
+ copyButton = true
1608
+ }) {
1609
+ const mergedComponents = react.useMemo(() => {
1610
+ if (!copyButton) return components;
1611
+ return { pre: CopyCodeBlock, ...components };
1612
+ }, [copyButton, components]);
1613
+ const rendered = react.useMemo(
1614
+ () => parseMarkdown(content, mergedComponents, rehypePlugins),
1615
+ [content, mergedComponents, rehypePlugins]
1616
+ );
1617
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: `inkwell-renderer ${className ?? ""}`, children: rendered });
1618
+ }
1619
+
1620
+ exports.InkwellEditor = InkwellEditor;
1621
+ exports.InkwellRenderer = InkwellRenderer;
1622
+ exports.createBubbleMenuPlugin = createBubbleMenuPlugin;
1623
+ exports.createSnippetsPlugin = createSnippetsPlugin;
1624
+ exports.defaultBubbleMenuItems = defaultBubbleMenuItems;
1625
+ exports.deserialize = deserialize;
1626
+ exports.parseMarkdown = parseMarkdown;
1627
+ exports.pluginClass = pluginClass;
1628
+ exports.serializeToMarkdown = serializeToMarkdown;