@railway/inkwell 0.1.4 → 1.0.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.js CHANGED
@@ -1,8 +1,7 @@
1
- import { withYjs, withCursors, withYHistory, YjsEditor, CursorEditor, relativePositionToSlatePoint } from '@slate-yjs/core';
2
- import { useMemo, useRef, useEffect, useState, useCallback, Fragment, createElement } from 'react';
3
- import { createEditor, Range, Transforms, Editor, Node, Element, Path } from 'slate';
4
- import { withHistory } from 'slate-history';
5
- import { withReact, ReactEditor, Slate, Editable } from 'slate-react';
1
+ import { forwardRef, useMemo, useRef, useState, useCallback, useEffect, useImperativeHandle, useLayoutEffect, Fragment, useId, createElement } from 'react';
2
+ import { createEditor, Transforms, Editor, Node, Range, Element, Path } from 'slate';
3
+ import { withHistory, HistoryEditor } from 'slate-history';
4
+ import { withReact, ReactEditor, Slate, Editable, useSelected } from 'slate-react';
6
5
  import { jsx, jsxs, Fragment as Fragment$1 } from 'react/jsx-runtime';
7
6
  import rehypeHighlight from 'rehype-highlight';
8
7
  import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
@@ -12,7 +11,7 @@ import remarkParse from 'remark-parse';
12
11
  import remarkRehype from 'remark-rehype';
13
12
  import { unified } from 'unified';
14
13
  import { toString } from 'mdast-util-to-string';
15
- import { visit } from 'unist-util-visit';
14
+ import { visit, SKIP } from 'unist-util-visit';
16
15
  import rehypeParse from 'rehype-parse';
17
16
  import rehypeRemark from 'rehype-remark';
18
17
  import remarkStringify from 'remark-stringify';
@@ -103,9 +102,12 @@ function BubbleMenuWidget({
103
102
  }
104
103
  const rect = sel.getRangeAt(0).getBoundingClientRect();
105
104
  const editorRect = editorRef.current.getBoundingClientRect();
105
+ const topInEditor = rect.top - editorRect.top;
106
+ const hasRoomAbove = topInEditor >= 48;
106
107
  setPosition({
107
- top: rect.top - editorRect.top,
108
- left: rect.left - editorRect.left + rect.width / 2
108
+ top: hasRoomAbove ? topInEditor : rect.bottom - editorRect.top,
109
+ left: rect.left - editorRect.left + rect.width / 2,
110
+ placement: hasRoomAbove ? "above" : "below"
109
111
  });
110
112
  }, [editorRef]);
111
113
  useEffect(() => {
@@ -129,9 +131,9 @@ function BubbleMenuWidget({
129
131
  position: "absolute",
130
132
  top: position.top,
131
133
  left: position.left,
132
- transform: "translateX(-50%) translateY(-100%)",
133
- marginTop: -8,
134
- zIndex: 1e3
134
+ transform: position.placement === "above" ? "translateX(-50%) translateY(-100%)" : "translateX(-50%)",
135
+ marginTop: position.placement === "above" ? -8 : 8,
136
+ zIndex: 1100
135
137
  },
136
138
  onMouseDown: (e) => e.preventDefault(),
137
139
  children: /* @__PURE__ */ jsx("div", { className: cls("inner"), children: items.map((item) => /* @__PURE__ */ jsx(item.render, { wrapSelection }, item.key)) })
@@ -202,17 +204,13 @@ function remarkNoTables() {
202
204
 
203
205
  // src/lib/render-html.ts
204
206
  var processorCache = /* @__PURE__ */ new Map();
205
- function getProcessor(rehypePlugins) {
206
- const key = rehypePlugins ? JSON.stringify(
207
- rehypePlugins.map((p) => Array.isArray(p) ? p[1] : "default")
208
- ) : "default";
209
- const cached = processorCache.get(key);
210
- if (cached) return cached;
207
+ function createProcessor(rehypePlugins) {
211
208
  const processor2 = unified().use(remarkParse).use(remarkGfm).use(remarkNoTables).use(remarkFlattenBlockquotes).use(remarkRehype);
212
209
  const plugins = rehypePlugins ?? [[rehypeHighlight, { detect: true }]];
213
210
  for (const plugin of plugins) {
214
211
  if (Array.isArray(plugin)) {
215
- processor2.use(plugin[0], plugin[1]);
212
+ const [rehypePlugin, ...options] = plugin;
213
+ processor2.use(rehypePlugin, ...options);
216
214
  } else {
217
215
  processor2.use(plugin);
218
216
  }
@@ -227,7 +225,14 @@ function getProcessor(rehypePlugins) {
227
225
  }
228
226
  });
229
227
  processor2.use(rehypeStringify);
230
- processorCache.set(key, processor2);
228
+ return processor2;
229
+ }
230
+ function getProcessor(rehypePlugins) {
231
+ if (rehypePlugins) return createProcessor(rehypePlugins);
232
+ const cached = processorCache.get("default");
233
+ if (cached) return cached;
234
+ const processor2 = createProcessor();
235
+ processorCache.set("default", processor2);
231
236
  return processor2;
232
237
  }
233
238
  function escapeBareBq(markdown) {
@@ -246,7 +251,7 @@ function computeDecorations(entry, editor, rehypePlugins) {
246
251
  if (element.type === "code-line") {
247
252
  return computeCodeDecorations(entry, editor, rehypePlugins);
248
253
  }
249
- if (element.type === "paragraph" || element.type === "blockquote" || element.type === "list-item" || element.type === "heading") {
254
+ if (element.type === "paragraph" || element.type === "blockquote" || element.type === "heading") {
250
255
  return computeInlineDecorations(entry);
251
256
  }
252
257
  if (element.type === "code-fence") {
@@ -500,6 +505,37 @@ function parseHljsRanges(html, elementPath) {
500
505
  }
501
506
  return ranges;
502
507
  }
508
+
509
+ // src/editor/slate/features.ts
510
+ var DEFAULT_FEATURES = {
511
+ heading1: true,
512
+ heading2: true,
513
+ heading3: true,
514
+ heading4: true,
515
+ heading5: true,
516
+ heading6: true,
517
+ blockquotes: true,
518
+ codeBlocks: true,
519
+ images: true
520
+ };
521
+ var resolveFeatures = (features) => {
522
+ if (!features) return DEFAULT_FEATURES;
523
+ const maybeResolved = features;
524
+ const headings = features.headings;
525
+ const headingOverrides = typeof headings === "object" && headings !== null ? headings : null;
526
+ const allHeadings = typeof headings === "boolean" ? headings : true;
527
+ return {
528
+ heading1: maybeResolved.heading1 ?? headingOverrides?.h1 ?? allHeadings,
529
+ heading2: maybeResolved.heading2 ?? headingOverrides?.h2 ?? allHeadings,
530
+ heading3: maybeResolved.heading3 ?? headingOverrides?.h3 ?? allHeadings,
531
+ heading4: maybeResolved.heading4 ?? headingOverrides?.h4 ?? allHeadings,
532
+ heading5: maybeResolved.heading5 ?? headingOverrides?.h5 ?? allHeadings,
533
+ heading6: maybeResolved.heading6 ?? headingOverrides?.h6 ?? allHeadings,
534
+ blockquotes: features.blockquotes ?? true,
535
+ codeBlocks: features.codeBlocks ?? true,
536
+ images: features.images ?? true
537
+ };
538
+ };
503
539
  function generateId() {
504
540
  return crypto.randomUUID();
505
541
  }
@@ -534,24 +570,21 @@ function withNodeId(editor) {
534
570
 
535
571
  // src/editor/slate/deserialize.ts
536
572
  var HEADING_RE = /^(#{1,6}) /;
537
- function deserialize(markdown, decorations) {
538
- if (!markdown) {
573
+ var IMAGE_RE = /^!\[([^\]]*)\]\(([^)\s]+)\)$/;
574
+ function deserialize(content, features) {
575
+ if (!content) {
539
576
  return [{ type: "paragraph", id: generateId(), children: [{ text: "" }] }];
540
577
  }
578
+ const cfg = resolveFeatures(features);
541
579
  const headingEnabled = [
542
- decorations?.heading1 ?? true,
543
- decorations?.heading2 ?? true,
544
- decorations?.heading3 ?? true,
545
- decorations?.heading4 ?? true,
546
- decorations?.heading5 ?? true,
547
- decorations?.heading6 ?? true
580
+ cfg.heading1,
581
+ cfg.heading2,
582
+ cfg.heading3,
583
+ cfg.heading4,
584
+ cfg.heading5,
585
+ cfg.heading6
548
586
  ];
549
- const cfg = {
550
- lists: decorations?.lists ?? true,
551
- blockquotes: decorations?.blockquotes ?? true,
552
- codeBlocks: decorations?.codeBlocks ?? true
553
- };
554
- const lines = markdown.split("\n");
587
+ const lines = content.split("\n");
555
588
  const result = [];
556
589
  let inCodeBlock = false;
557
590
  let paragraphLines = [];
@@ -564,19 +597,22 @@ function deserialize(markdown, decorations) {
564
597
  type: "heading",
565
598
  id: generateId(),
566
599
  level: hMatch[1].length,
567
- children: [{ text: line.slice(hMatch[0].length) }]
600
+ children: [{ text: line }]
568
601
  });
569
602
  continue;
570
603
  }
571
- if (cfg.blockquotes && /^> /.test(line)) {
604
+ const imageMatch = cfg.images ? IMAGE_RE.exec(line) : null;
605
+ if (imageMatch) {
572
606
  result.push({
573
- type: "blockquote",
607
+ type: "image",
574
608
  id: generateId(),
575
- children: [{ text: line.slice(2) }]
609
+ alt: imageMatch[1],
610
+ url: imageMatch[2],
611
+ children: [{ text: line }]
576
612
  });
577
- } else if (cfg.lists && /^[-*+] /.test(line)) {
613
+ } else if (cfg.blockquotes && /^> /.test(line)) {
578
614
  result.push({
579
- type: "list-item",
615
+ type: "blockquote",
580
616
  id: generateId(),
581
617
  children: [{ text: line }]
582
618
  });
@@ -632,6 +668,41 @@ function deserialize(markdown, decorations) {
632
668
  flushParagraph();
633
669
  return result.length > 0 ? result : [{ type: "paragraph", id: generateId(), children: [{ text: "" }] }];
634
670
  }
671
+
672
+ // src/lib/safe-url.ts
673
+ function isSafeImageUrl(rawUrl) {
674
+ if (typeof rawUrl !== "string") return false;
675
+ if (Array.from(rawUrl).some((char) => {
676
+ const code = char.charCodeAt(0);
677
+ return code <= 31 || code === 127;
678
+ })) {
679
+ return false;
680
+ }
681
+ const url = rawUrl.trim();
682
+ if (url.length === 0) return false;
683
+ if (url.startsWith("/") || url.startsWith("./") || url.startsWith("../")) {
684
+ return true;
685
+ }
686
+ if (!/^[a-z][a-z0-9+.-]*:/i.test(url)) {
687
+ return true;
688
+ }
689
+ const lower = url.toLowerCase();
690
+ if (lower.startsWith("http://") || lower.startsWith("https://")) {
691
+ return true;
692
+ }
693
+ if (lower.startsWith("blob:")) {
694
+ return true;
695
+ }
696
+ if (/^data:image\/(png|jpeg|jpg|gif|webp);/.test(lower)) {
697
+ return true;
698
+ }
699
+ return false;
700
+ }
701
+ function sanitizeImageUrl(rawUrl) {
702
+ if (!rawUrl) return void 0;
703
+ const url = rawUrl.trim();
704
+ return isSafeImageUrl(rawUrl) ? url : void 0;
705
+ }
635
706
  function RenderElement({
636
707
  attributes,
637
708
  children,
@@ -654,12 +725,39 @@ function RenderElement({
654
725
  return /* @__PURE__ */ jsx("p", { ...attributes, className: editorClass("code-line"), children });
655
726
  case "blockquote":
656
727
  return /* @__PURE__ */ jsx("p", { ...attributes, className: editorClass("blockquote"), children });
657
- case "list-item":
658
- return /* @__PURE__ */ jsx("p", { ...attributes, className: editorClass("list-item"), "data-list": true, children });
728
+ case "image":
729
+ return /* @__PURE__ */ jsx(ImageElement, { attributes, element: el, children });
659
730
  default:
660
731
  return /* @__PURE__ */ jsx("p", { ...attributes, children });
661
732
  }
662
733
  }
734
+ function ImageElement({
735
+ attributes,
736
+ element,
737
+ children
738
+ }) {
739
+ const selected = useSelected();
740
+ return /* @__PURE__ */ jsxs(
741
+ "div",
742
+ {
743
+ ...attributes,
744
+ className: editorClass("image"),
745
+ "data-selected": selected || void 0,
746
+ children: [
747
+ /* @__PURE__ */ jsx(
748
+ "img",
749
+ {
750
+ src: sanitizeImageUrl(element.url),
751
+ alt: element.alt ?? "",
752
+ contentEditable: false,
753
+ draggable: false
754
+ }
755
+ ),
756
+ children
757
+ ]
758
+ }
759
+ );
760
+ }
663
761
  function RenderLeaf({ attributes, children, leaf }) {
664
762
  const l = leaf;
665
763
  if (l.boldMarker || l.italicMarker || l.strikeMarker) {
@@ -676,53 +774,19 @@ function RenderLeaf({ attributes, children, leaf }) {
676
774
  if (l.hljs) {
677
775
  content = /* @__PURE__ */ jsx("span", { className: l.hljs, children: content });
678
776
  }
679
- if (l.remoteCursor) {
680
- content = /* @__PURE__ */ jsx(
681
- "span",
682
- {
683
- className: editorClass("remote-cursor"),
684
- style: { backgroundColor: `${l.remoteCursor}30` },
685
- children: content
686
- }
687
- );
688
- }
689
- if (l.remoteCursorCaret) {
690
- content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
691
- /* @__PURE__ */ jsx(
692
- "span",
693
- {
694
- className: editorClass("remote-caret"),
695
- style: { borderColor: l.remoteCursor },
696
- contentEditable: false
697
- }
698
- ),
699
- content
700
- ] });
701
- }
702
777
  return /* @__PURE__ */ jsx("span", { ...attributes, children: content });
703
778
  }
779
+ var LIST_LIKE_PARAGRAPH_RE = /^\s*(?:[-*+]|\d+\.)(?:\s|$)/;
780
+ var isListLikeParagraph = (entry) => entry.type === "paragraph" && LIST_LIKE_PARAGRAPH_RE.test(entry.text);
704
781
  function serialize(nodes) {
705
782
  const entries = [];
706
783
  for (const node of nodes) {
707
784
  const text = Node.string(node);
708
785
  const type = node.type;
709
- if (type === "heading") {
710
- const level = node.level ?? 1;
711
- const prefix = "#".repeat(level);
712
- entries.push({ text: `${prefix} ${text}`, type });
713
- continue;
714
- }
715
- if (type === "blockquote") {
716
- const lines = text.split("\n").filter((l) => l.trim() !== "");
717
- if (lines.length === 0) {
718
- entries.push({ text: "> ", type });
719
- } else {
720
- const prefixed = lines.map((line) => {
721
- const escaped = line.replace(/^(>+)/g, (m) => "\\>".repeat(m.length));
722
- return "> " + escaped;
723
- }).join("\n>\n");
724
- entries.push({ text: prefixed, type });
725
- }
786
+ if (type === "image" && !text) {
787
+ const url = node.url ?? "";
788
+ const alt = node.alt ?? "";
789
+ entries.push({ text: `![${alt}](${url})`, type });
726
790
  continue;
727
791
  }
728
792
  if (type === "paragraph" && !text.trim()) {
@@ -735,18 +799,44 @@ function serialize(nodes) {
735
799
  let result = "";
736
800
  for (let i = 0; i < entries.length; i++) {
737
801
  if (i > 0) {
738
- const prev = entries[i - 1].type;
739
- const curr = entries[i].type;
740
- const sameGroup = prev === "blockquote" && curr === "blockquote" || prev === "list-item" && curr === "list-item" || codeTypes.has(prev) && codeTypes.has(curr);
802
+ const prev = entries[i - 1];
803
+ const curr = entries[i];
804
+ const sameGroup = prev.type === "blockquote" && curr.type === "blockquote" || codeTypes.has(prev.type) && codeTypes.has(curr.type) || isListLikeParagraph(prev) && isListLikeParagraph(curr);
741
805
  result += sameGroup ? "\n" : "\n\n";
742
806
  }
743
807
  result += entries[i].text;
744
808
  }
745
- return result.replace(/\n{3,}/g, "\n\n").trim();
809
+ return result.replace(/\n{3,}/g, "\n\n").replace(/^\n+|\n+$/g, "");
746
810
  }
747
811
  var HEADING_RE2 = /^#{1,6}$/;
748
- function withMarkdown(editor, decorationsRef) {
749
- const { insertBreak, insertData, insertText } = editor;
812
+ var UNORDERED_LIST_CONTINUE_RE = /^(\s*)([-*+]) \S/;
813
+ var UNORDERED_LIST_EMPTY_RE = /^(\s*)([-*+]) ?$/;
814
+ var HEADING_LINE_RE = /^(#{1,6})\s/;
815
+ function classifyLine(text, deco) {
816
+ const headingMatch = HEADING_LINE_RE.exec(text);
817
+ if (headingMatch) {
818
+ const level = headingMatch[1].length;
819
+ const key = `heading${level}`;
820
+ if (deco[key]) return { type: "heading", level };
821
+ }
822
+ if (deco.blockquotes && /^>\s/.test(text)) {
823
+ return { type: "blockquote" };
824
+ }
825
+ return { type: "paragraph" };
826
+ }
827
+ function withMarkdown(editor, featuresRef) {
828
+ const {
829
+ insertBreak,
830
+ insertData,
831
+ insertText,
832
+ isVoid,
833
+ normalizeNode,
834
+ setFragmentData
835
+ } = editor;
836
+ editor.isVoid = (element) => {
837
+ if (element.type === "image") return true;
838
+ return isVoid(element);
839
+ };
750
840
  editor.insertBreak = () => {
751
841
  const { selection } = editor;
752
842
  if (!selection) return insertBreak();
@@ -757,7 +847,7 @@ function withMarkdown(editor, decorationsRef) {
757
847
  const [node, path] = match;
758
848
  const element = node;
759
849
  const text = Node.string(node);
760
- const deco = decorationsRef.current;
850
+ const deco = featuresRef.current;
761
851
  if (deco.codeBlocks && element.type === "paragraph" && text.startsWith("```")) {
762
852
  Transforms.setNodes(editor, {
763
853
  type: "code-fence"
@@ -794,7 +884,13 @@ function withMarkdown(editor, decorationsRef) {
794
884
  }
795
885
  if (element.type === "blockquote") {
796
886
  const text2 = Node.string(node);
797
- if (!text2.trim()) {
887
+ if (/^>\s*$/.test(text2)) {
888
+ Transforms.delete(editor, {
889
+ at: {
890
+ anchor: Editor.start(editor, path),
891
+ focus: Editor.end(editor, path)
892
+ }
893
+ });
798
894
  Transforms.setNodes(editor, {
799
895
  type: "paragraph"
800
896
  });
@@ -812,22 +908,124 @@ function withMarkdown(editor, decorationsRef) {
812
908
  return;
813
909
  }
814
910
  if (element.type === "heading") {
815
- if (!text.trim()) {
911
+ if (!text.trim() || /^#{1,6}\s*$/.test(text)) {
912
+ Transforms.delete(editor, {
913
+ at: {
914
+ anchor: Editor.start(editor, path),
915
+ focus: Editor.end(editor, path)
916
+ }
917
+ });
816
918
  Transforms.setNodes(editor, {
817
919
  type: "paragraph"
818
920
  });
819
921
  Transforms.unsetNodes(editor, "level");
820
922
  return;
821
923
  }
822
- const newParagraph = {
924
+ if (!Range.isCollapsed(selection)) {
925
+ Transforms.delete(editor);
926
+ }
927
+ const point = editor.selection?.anchor;
928
+ const cursorOffset = point?.offset ?? text.length;
929
+ const endPoint = Editor.end(editor, path);
930
+ const tail = point ? Editor.string(editor, { anchor: point, focus: endPoint }) : "";
931
+ if (point && tail.length > 0) {
932
+ Transforms.delete(editor, { at: { anchor: point, focus: endPoint } });
933
+ }
934
+ const head = text.slice(0, cursorOffset);
935
+ const headClass = classifyLine(head, deco);
936
+ if (headClass.type === "heading" && headClass.level !== void 0) {
937
+ Transforms.setNodes(editor, {
938
+ type: "heading",
939
+ level: headClass.level
940
+ });
941
+ } else {
942
+ Transforms.setNodes(editor, {
943
+ type: headClass.type
944
+ });
945
+ Transforms.unsetNodes(editor, "level");
946
+ }
947
+ const tailClass = classifyLine(tail, deco);
948
+ const newNode = tailClass.type === "heading" && tailClass.level !== void 0 ? {
949
+ type: "heading",
950
+ id: generateId(),
951
+ level: tailClass.level,
952
+ children: [{ text: tail }]
953
+ } : {
823
954
  type: "paragraph",
824
955
  id: generateId(),
825
- children: [{ text: "" }]
956
+ children: [{ text: tail }]
826
957
  };
827
- Transforms.insertNodes(editor, newParagraph, { at: Path.next(path) });
958
+ Transforms.insertNodes(editor, newNode, { at: Path.next(path) });
828
959
  Transforms.select(editor, Editor.start(editor, Path.next(path)));
829
960
  return;
830
961
  }
962
+ if (element.type === "paragraph") {
963
+ const emptyMatch = UNORDERED_LIST_EMPTY_RE.exec(text);
964
+ if (emptyMatch) {
965
+ const [, indent, marker] = emptyMatch;
966
+ Transforms.delete(editor, {
967
+ at: {
968
+ anchor: Editor.start(editor, path),
969
+ focus: Editor.end(editor, path)
970
+ }
971
+ });
972
+ if (indent.length >= 2) {
973
+ editor.insertText(`${indent.slice(2)}${marker} `);
974
+ }
975
+ return;
976
+ }
977
+ const continueMatch = UNORDERED_LIST_CONTINUE_RE.exec(text);
978
+ if (continueMatch) {
979
+ const [, indent, marker] = continueMatch;
980
+ const prefix = `${indent}${marker} `;
981
+ if (!Range.isCollapsed(selection)) {
982
+ Transforms.delete(editor);
983
+ }
984
+ const point = editor.selection?.anchor;
985
+ const cursorOffset = point?.offset ?? text.length;
986
+ if (cursorOffset < prefix.length) {
987
+ const newParagraph2 = {
988
+ type: "paragraph",
989
+ id: generateId(),
990
+ children: [{ text: prefix }]
991
+ };
992
+ Transforms.insertNodes(editor, newParagraph2, {
993
+ at: Path.next(path)
994
+ });
995
+ Transforms.select(editor, Editor.end(editor, Path.next(path)));
996
+ return;
997
+ }
998
+ if (cursorOffset === prefix.length) {
999
+ const newParagraph2 = {
1000
+ type: "paragraph",
1001
+ id: generateId(),
1002
+ children: [{ text: prefix }]
1003
+ };
1004
+ Transforms.insertNodes(editor, newParagraph2, { at: path });
1005
+ Transforms.select(editor, {
1006
+ path: [...Path.next(path), 0],
1007
+ offset: prefix.length
1008
+ });
1009
+ return;
1010
+ }
1011
+ const endPoint = Editor.end(editor, path);
1012
+ const tail = point ? Editor.string(editor, { anchor: point, focus: endPoint }) : "";
1013
+ if (point && tail.length > 0) {
1014
+ Transforms.delete(editor, { at: { anchor: point, focus: endPoint } });
1015
+ }
1016
+ const newParagraph = {
1017
+ type: "paragraph",
1018
+ id: generateId(),
1019
+ children: [{ text: `${prefix}${tail}` }]
1020
+ };
1021
+ Transforms.insertNodes(editor, newParagraph, { at: Path.next(path) });
1022
+ Transforms.select(editor, {
1023
+ path: [...Path.next(path), 0],
1024
+ offset: prefix.length
1025
+ });
1026
+ return;
1027
+ }
1028
+ }
831
1029
  if (element.type === "code-line") {
832
1030
  const newLine = {
833
1031
  type: "code-line",
@@ -838,28 +1036,14 @@ function withMarkdown(editor, decorationsRef) {
838
1036
  Transforms.select(editor, Editor.start(editor, Path.next(path)));
839
1037
  return;
840
1038
  }
841
- if (element.type === "list-item") {
842
- const text2 = Node.string(node);
843
- if (text2.trim() === "-" || text2.trim() === "*" || text2.trim() === "+" || text2 === "- " || text2 === "* " || text2 === "+ ") {
844
- Transforms.delete(editor, {
845
- at: {
846
- anchor: Editor.start(editor, path),
847
- focus: Editor.end(editor, path)
848
- }
849
- });
850
- Transforms.setNodes(editor, {
851
- type: "paragraph"
852
- });
853
- return;
854
- }
855
- const marker = text2.match(/^([-*+] )/)?.[1] || "- ";
856
- const newItem = {
857
- type: "list-item",
1039
+ if (element.type === "image") {
1040
+ const newParagraph = {
1041
+ type: "paragraph",
858
1042
  id: generateId(),
859
- children: [{ text: marker }]
1043
+ children: [{ text: "" }]
860
1044
  };
861
- Transforms.insertNodes(editor, newItem, { at: Path.next(path) });
862
- Transforms.select(editor, Editor.end(editor, Path.next(path)));
1045
+ Transforms.insertNodes(editor, newParagraph, { at: Path.next(path) });
1046
+ Transforms.select(editor, Editor.start(editor, Path.next(path)));
863
1047
  return;
864
1048
  }
865
1049
  insertBreak();
@@ -878,7 +1062,7 @@ function withMarkdown(editor, decorationsRef) {
878
1062
  const newBq = {
879
1063
  type: "blockquote",
880
1064
  id: generateId(),
881
- children: [{ text: "" }]
1065
+ children: [{ text: "> " }]
882
1066
  };
883
1067
  Transforms.insertNodes(editor, newBq, { at: Path.next(path) });
884
1068
  Transforms.select(editor, Editor.start(editor, Path.next(path)));
@@ -907,7 +1091,7 @@ function withMarkdown(editor, decorationsRef) {
907
1091
  const [node, path] = match;
908
1092
  const element = node;
909
1093
  const currentText = Node.string(node);
910
- const deco = decorationsRef.current;
1094
+ const deco = featuresRef.current;
911
1095
  if (element.type === "code-line" && currentText === "```" && text !== "" && text !== "\n") {
912
1096
  Transforms.setNodes(editor, {
913
1097
  type: "code-fence"
@@ -922,12 +1106,7 @@ function withMarkdown(editor, decorationsRef) {
922
1106
  return;
923
1107
  }
924
1108
  if (deco.blockquotes && element.type === "paragraph" && text === " " && currentText === ">") {
925
- Transforms.delete(editor, {
926
- at: {
927
- anchor: Editor.start(editor, path),
928
- focus: Editor.end(editor, path)
929
- }
930
- });
1109
+ insertText(text);
931
1110
  Transforms.setNodes(editor, {
932
1111
  type: "blockquote"
933
1112
  });
@@ -937,25 +1116,13 @@ function withMarkdown(editor, decorationsRef) {
937
1116
  const headingKey = `heading${headingLevel}`;
938
1117
  if (element.type === "paragraph" && text === " " && HEADING_RE2.test(currentText) && deco[headingKey]) {
939
1118
  const level = headingLevel;
940
- Transforms.delete(editor, {
941
- at: {
942
- anchor: Editor.start(editor, path),
943
- focus: Editor.end(editor, path)
944
- }
945
- });
1119
+ insertText(text);
946
1120
  Transforms.setNodes(editor, {
947
1121
  type: "heading",
948
1122
  level
949
1123
  });
950
1124
  return;
951
1125
  }
952
- if (deco.lists && element.type === "paragraph" && text === " " && (currentText === "-" || currentText === "*" || currentText === "+")) {
953
- insertText(text);
954
- Transforms.setNodes(editor, {
955
- type: "list-item"
956
- });
957
- return;
958
- }
959
1126
  if (element.type === "code-fence") {
960
1127
  const prevIdx = path[0] - 1;
961
1128
  if (prevIdx >= 0) {
@@ -979,494 +1146,1816 @@ function withMarkdown(editor, decorationsRef) {
979
1146
  editor.insertData = (data) => {
980
1147
  const text = data.getData("text/plain");
981
1148
  if (text) {
982
- const nodes = deserialize(text, decorationsRef.current);
1149
+ const nodes = deserialize(text, featuresRef.current);
983
1150
  Transforms.insertNodes(editor, nodes);
984
1151
  return;
985
1152
  }
986
1153
  insertData(data);
987
1154
  };
1155
+ editor.setFragmentData = (data, originEvent) => {
1156
+ try {
1157
+ setFragmentData(data, originEvent);
1158
+ } catch {
1159
+ }
1160
+ const fragment = editor.getFragment();
1161
+ if (fragment.length > 0) {
1162
+ data.setData("text/plain", serialize(fragment));
1163
+ }
1164
+ };
1165
+ editor.normalizeNode = (entry) => {
1166
+ const [node, path] = entry;
1167
+ if (Element.isElement(node)) {
1168
+ const element = node;
1169
+ const textDriven = element.type === "paragraph" || element.type === "heading" || element.type === "blockquote";
1170
+ if (textDriven) {
1171
+ const text = Node.string(node);
1172
+ const cls2 = classifyLine(text, featuresRef.current);
1173
+ if (cls2.type === "heading" && cls2.level !== void 0) {
1174
+ if (element.type !== "heading" || element.level !== cls2.level) {
1175
+ Transforms.setNodes(
1176
+ editor,
1177
+ {
1178
+ type: "heading",
1179
+ level: cls2.level
1180
+ },
1181
+ { at: path }
1182
+ );
1183
+ return;
1184
+ }
1185
+ } else if (cls2.type === "blockquote") {
1186
+ if (element.type !== "blockquote") {
1187
+ Transforms.setNodes(
1188
+ editor,
1189
+ { type: "blockquote" },
1190
+ { at: path }
1191
+ );
1192
+ if (element.level !== void 0) {
1193
+ Transforms.unsetNodes(editor, "level", { at: path });
1194
+ }
1195
+ return;
1196
+ }
1197
+ } else {
1198
+ if (element.type !== "paragraph") {
1199
+ Transforms.setNodes(
1200
+ editor,
1201
+ { type: "paragraph" },
1202
+ { at: path }
1203
+ );
1204
+ if (element.level !== void 0) {
1205
+ Transforms.unsetNodes(editor, "level", { at: path });
1206
+ }
1207
+ return;
1208
+ }
1209
+ }
1210
+ }
1211
+ }
1212
+ normalizeNode(entry);
1213
+ };
988
1214
  return editor;
989
1215
  }
990
1216
  var IS_SERVER = typeof window === "undefined";
991
- var DEFAULT_DECORATIONS = {
992
- heading1: true,
993
- heading2: true,
994
- heading3: true,
995
- heading4: true,
996
- heading5: true,
997
- heading6: true,
998
- lists: true,
999
- blockquotes: true,
1000
- codeBlocks: true
1001
- };
1002
- function InkwellEditor({
1003
- content,
1004
- onChange,
1005
- className,
1006
- placeholder,
1007
- plugins: userPlugins = [],
1008
- rehypePlugins,
1009
- decorations,
1010
- collaboration,
1011
- bubbleMenu = true
1217
+ var EMPTY_PLUGINS = [];
1218
+ function CharacterCount({
1219
+ count,
1220
+ limit,
1221
+ over
1012
1222
  }) {
1013
- const resolvedDecorations = useMemo(
1014
- () => ({ ...DEFAULT_DECORATIONS, ...decorations }),
1015
- [decorations]
1016
- );
1017
- const decorationsRef = useRef(resolvedDecorations);
1018
- decorationsRef.current = resolvedDecorations;
1019
- const plugins = useMemo(() => {
1020
- const builtIn = bubbleMenu ? [createBubbleMenuPlugin()] : [];
1021
- return [...builtIn, ...userPlugins];
1022
- }, [userPlugins, bubbleMenu]);
1023
- const editor = useMemo(() => {
1024
- if (IS_SERVER) return null;
1025
- const base = withNodeId(withReact(createEditor()));
1026
- if (collaboration) {
1027
- const { sharedType, awareness, user } = collaboration;
1028
- const yjsEditor = withYjs(base, sharedType, { autoConnect: false });
1029
- const cursorEditor = withCursors(yjsEditor, awareness, {
1030
- data: user
1031
- });
1032
- const historyEditor = withYHistory(cursorEditor);
1033
- return withMarkdown(historyEditor, decorationsRef);
1223
+ const label = `${count} of ${limit} characters${over ? ", over limit" : ""}`;
1224
+ return /* @__PURE__ */ jsxs(
1225
+ "div",
1226
+ {
1227
+ className: `inkwell-editor-character-count${over ? " inkwell-editor-character-count-over" : ""}`,
1228
+ role: over ? "status" : void 0,
1229
+ "aria-live": over ? "polite" : "off",
1230
+ "aria-atomic": over ? true : void 0,
1231
+ "aria-label": label,
1232
+ children: [
1233
+ count,
1234
+ " / ",
1235
+ limit
1236
+ ]
1034
1237
  }
1035
- return withMarkdown(withHistory(base), decorationsRef);
1036
- }, []);
1037
- if (!editor) return null;
1038
- useEffect(() => {
1039
- if (!collaboration || !YjsEditor.isYjsEditor(editor)) return;
1040
- YjsEditor.connect(editor);
1041
- return () => {
1042
- YjsEditor.disconnect(editor);
1043
- };
1044
- }, [editor, collaboration]);
1045
- const [cursorVersion, setCursorVersion] = useState(0);
1046
- useEffect(() => {
1047
- if (!collaboration || !CursorEditor.isCursorEditor(editor)) return;
1048
- const handleChange2 = () => setCursorVersion((v) => v + 1);
1049
- CursorEditor.on(editor, "change", handleChange2);
1050
- return () => {
1051
- CursorEditor.off(editor, "change", handleChange2);
1052
- };
1053
- }, [editor, collaboration]);
1054
- const remoteCursorRanges = useMemo(() => {
1055
- if (!collaboration || !CursorEditor.isCursorEditor(editor)) return [];
1056
- const ranges = [];
1057
- const states = CursorEditor.cursorStates(editor);
1058
- for (const [, state] of Object.entries(states)) {
1059
- if (!state.relativeSelection) continue;
1060
- const data = state.data;
1061
- if (!data) continue;
1062
- try {
1063
- const { anchor, focus } = state.relativeSelection;
1064
- const anchorPoint = relativePositionToSlatePoint(
1065
- collaboration.sharedType,
1066
- editor,
1067
- anchor
1068
- );
1069
- const focusPoint = relativePositionToSlatePoint(
1070
- collaboration.sharedType,
1071
- editor,
1072
- focus
1073
- );
1074
- if (!anchorPoint || !focusPoint) continue;
1075
- const range = { anchor: anchorPoint, focus: focusPoint };
1076
- if (Range.isCollapsed(range)) {
1077
- ranges.push({
1078
- ...range,
1079
- remoteCursor: data.color,
1080
- remoteCursorCaret: true
1081
- });
1082
- } else {
1083
- ranges.push({
1084
- ...range,
1085
- remoteCursor: data.color
1086
- });
1087
- }
1088
- } catch {
1238
+ );
1239
+ }
1240
+ var ORDERED_LIST_MARKER_RE = /^\s*\d+\.(?:\s|$)/;
1241
+ var UNORDERED_LIST_MARKER_RE = /^(\s*)([-*+])(?:\s|$)/;
1242
+ function replaceEditorChildren(editor, nodes, withoutSaving) {
1243
+ const replace = () => {
1244
+ Editor.withoutNormalizing(editor, () => {
1245
+ for (let index = editor.children.length - 1; index >= 0; index--) {
1246
+ Transforms.removeNodes(editor, { at: [index] });
1089
1247
  }
1090
- }
1091
- return ranges;
1092
- }, [editor, collaboration, cursorVersion]);
1093
- const initialValue = useMemo(
1094
- () => collaboration ? [
1095
- {
1096
- type: "paragraph",
1097
- id: generateId(),
1098
- children: [{ text: "" }]
1248
+ Transforms.insertNodes(editor, nodes, { at: [0] });
1249
+ });
1250
+ };
1251
+ if (HistoryEditor.isHistoryEditor(editor)) {
1252
+ HistoryEditor.withoutSaving(editor, replace);
1253
+ return;
1254
+ }
1255
+ replace();
1256
+ }
1257
+ var InkwellEditor = forwardRef(function InkwellEditor2(props, ref) {
1258
+ if (IS_SERVER) return null;
1259
+ return /* @__PURE__ */ jsx(InkwellEditorClient, { ...props, ref });
1260
+ });
1261
+ var InkwellEditorClient = forwardRef(
1262
+ function InkwellEditorClient2({
1263
+ content = "",
1264
+ onChange,
1265
+ onStateChange,
1266
+ className,
1267
+ classNames,
1268
+ styles,
1269
+ placeholder,
1270
+ editable = true,
1271
+ plugins: userPlugins = EMPTY_PLUGINS,
1272
+ rehypePlugins,
1273
+ features,
1274
+ bubbleMenu = true,
1275
+ characterLimit,
1276
+ onCharacterCount,
1277
+ submitOnEnter = false,
1278
+ onSubmit
1279
+ }, ref) {
1280
+ const resolvedFeatures = useMemo(
1281
+ () => resolveFeatures(features),
1282
+ [features]
1283
+ );
1284
+ const featuresRef = useRef(resolvedFeatures);
1285
+ featuresRef.current = resolvedFeatures;
1286
+ const plugins = useMemo(() => {
1287
+ const builtIn = bubbleMenu ? [createBubbleMenuPlugin()] : [];
1288
+ const userNames = new Set(userPlugins.map((p) => p.name));
1289
+ const survivingBuiltIns = builtIn.filter((p) => !userNames.has(p.name));
1290
+ return [...survivingBuiltIns, ...userPlugins];
1291
+ }, [userPlugins, bubbleMenu]);
1292
+ const editor = useMemo(() => {
1293
+ const base = withNodeId(withReact(createEditor()));
1294
+ return withMarkdown(withHistory(base), featuresRef);
1295
+ }, []);
1296
+ const initialValue = useMemo(
1297
+ () => deserialize(content, resolvedFeatures),
1298
+ []
1299
+ );
1300
+ const lastContent = useRef(content);
1301
+ const isInternalChange = useRef(false);
1302
+ const suppressImperativeOnChange = useRef(false);
1303
+ const [characterCount, setCharacterCount] = useState(
1304
+ () => serialize(initialValue).length
1305
+ );
1306
+ const [isFocused, setIsFocused] = useState(false);
1307
+ const focusStateFrameRef = useRef(null);
1308
+ const [stateVersion, setStateVersion] = useState(0);
1309
+ const bumpStateVersion = useCallback(() => {
1310
+ setStateVersion((version) => version + 1);
1311
+ }, []);
1312
+ const scheduleFocusedState = useCallback((nextFocused) => {
1313
+ if (focusStateFrameRef.current !== null) {
1314
+ cancelAnimationFrame(focusStateFrameRef.current);
1099
1315
  }
1100
- ] : deserialize(content, resolvedDecorations),
1101
- []
1102
- );
1103
- const lastContent = useRef(content);
1104
- const isInternalChange = useRef(false);
1105
- useEffect(() => {
1106
- if (collaboration) return;
1107
- if (isInternalChange.current) {
1108
- isInternalChange.current = false;
1109
- return;
1110
- }
1111
- if (content === lastContent.current) return;
1112
- const newValue = deserialize(content, resolvedDecorations);
1113
- editor.children = newValue;
1114
- Transforms.select(editor, Editor.start(editor, []));
1115
- editor.onChange();
1116
- lastContent.current = content;
1117
- }, [content, editor, collaboration]);
1118
- const handleChange = useCallback(
1119
- (value) => {
1120
- const isAstChange = editor.operations.some(
1121
- (op) => op.type !== "set_selection"
1122
- );
1123
- if (!isAstChange) return;
1124
- if (!onChange) return;
1125
- const md = serialize(value);
1126
- if (collaboration) {
1127
- onChange(md);
1128
- } else {
1129
- if (md !== lastContent.current) {
1130
- lastContent.current = md;
1131
- isInternalChange.current = true;
1132
- onChange(md);
1316
+ focusStateFrameRef.current = requestAnimationFrame(() => {
1317
+ focusStateFrameRef.current = null;
1318
+ setIsFocused(nextFocused);
1319
+ });
1320
+ }, []);
1321
+ useEffect(
1322
+ () => () => {
1323
+ if (focusStateFrameRef.current !== null) {
1324
+ cancelAnimationFrame(focusStateFrameRef.current);
1133
1325
  }
1134
- }
1135
- },
1136
- [editor, onChange, collaboration]
1137
- );
1138
- const decorate = useCallback(
1139
- (entry) => {
1140
- const ranges = computeDecorations(entry, editor, rehypePlugins);
1141
- if (remoteCursorRanges.length > 0) {
1142
- const [, path] = entry;
1143
- for (const cursorRange of remoteCursorRanges) {
1144
- try {
1145
- const intersection = Range.intersection(
1146
- cursorRange,
1147
- Editor.range(editor, path)
1148
- );
1149
- if (intersection) {
1150
- ranges.push({
1151
- ...intersection,
1152
- remoteCursor: cursorRange.remoteCursor,
1153
- remoteCursorCaret: cursorRange.remoteCursorCaret
1154
- });
1155
- }
1156
- } catch {
1326
+ },
1327
+ []
1328
+ );
1329
+ const updateCharacterCount = useCallback(() => {
1330
+ const length = serialize(editor.children).length;
1331
+ setCharacterCount(length);
1332
+ onCharacterCount?.(length, characterLimit);
1333
+ return length;
1334
+ }, [editor, onCharacterCount, characterLimit]);
1335
+ const serializeContent = useCallback(
1336
+ () => serialize(editor.children),
1337
+ [editor]
1338
+ );
1339
+ const pluginEditorRef = useRef(null);
1340
+ const handleChange = useCallback(
1341
+ (value) => {
1342
+ const isAstChange = editor.operations.some(
1343
+ (op) => op.type !== "set_selection"
1344
+ );
1345
+ if (!isAstChange) return;
1346
+ updateCharacterCount();
1347
+ bumpStateVersion();
1348
+ const currentPluginEditor = pluginEditorRef.current;
1349
+ if (currentPluginEditor) {
1350
+ for (const plugin of plugins) {
1351
+ plugin.onEditorChange?.(currentPluginEditor);
1157
1352
  }
1158
1353
  }
1159
- }
1160
- return ranges;
1161
- },
1162
- [editor, rehypePlugins, remoteCursorRanges]
1163
- );
1164
- const [activePlugin, setActivePlugin] = useState(null);
1165
- const pluginPositionRef = useRef({
1166
- top: 0,
1167
- left: 0
1168
- });
1169
- const wrapperRef = useRef(null);
1170
- const editorElRef = useRef(null);
1171
- const getCursorPosition = useCallback(() => {
1172
- try {
1173
- const domSelection = window.getSelection();
1174
- if (!domSelection || domSelection.rangeCount === 0)
1175
- return { top: 0, left: 0 };
1176
- const range = domSelection.getRangeAt(0);
1177
- let rect = range.getBoundingClientRect();
1178
- if (rect.width === 0 && rect.height === 0 && domSelection.anchorNode) {
1179
- const node = domSelection.anchorNode instanceof HTMLElement ? domSelection.anchorNode : domSelection.anchorNode.parentElement;
1180
- if (node) rect = node.getBoundingClientRect();
1181
- }
1182
- const wrapperEl = wrapperRef.current;
1183
- if (!wrapperEl) return { top: 0, left: 0 };
1184
- const wrapperRect = wrapperEl.getBoundingClientRect();
1354
+ const nextContent = serialize(value);
1355
+ if (nextContent !== lastContent.current) {
1356
+ lastContent.current = nextContent;
1357
+ isInternalChange.current = true;
1358
+ onChange?.(nextContent);
1359
+ }
1360
+ },
1361
+ [bumpStateVersion, editor, onChange, plugins, updateCharacterCount]
1362
+ );
1363
+ const overLimit = characterLimit !== void 0 && characterCount > characterLimit;
1364
+ const hasCharacterLimit = characterLimit !== void 0;
1365
+ const getEditorState = useCallback(() => {
1366
+ const content2 = serializeContent();
1185
1367
  return {
1186
- top: rect.bottom - wrapperRect.top + 4,
1187
- left: rect.left - wrapperRect.left
1368
+ content: content2,
1369
+ isEmpty: content2.trim().length === 0,
1370
+ isFocused,
1371
+ isEditable: editable,
1372
+ characterCount,
1373
+ characterLimit,
1374
+ overLimit
1188
1375
  };
1189
- } catch {
1190
- return { top: 0, left: 0 };
1191
- }
1192
- }, []);
1193
- const wrapSelection = useCallback(
1194
- (before, after) => {
1195
- const { selection } = editor;
1196
- if (!selection) return;
1197
- const selectedText = Editor.string(editor, selection);
1198
- if (selectedText.startsWith(before) && selectedText.endsWith(after) && selectedText.length >= before.length + after.length) {
1199
- Transforms.delete(editor);
1200
- Transforms.insertText(
1201
- editor,
1202
- selectedText.slice(before.length, -after.length || void 0)
1203
- );
1376
+ }, [
1377
+ characterCount,
1378
+ characterLimit,
1379
+ editable,
1380
+ editor,
1381
+ isFocused,
1382
+ overLimit,
1383
+ serializeContent
1384
+ ]);
1385
+ useEffect(() => {
1386
+ if (isInternalChange.current) {
1387
+ isInternalChange.current = false;
1204
1388
  return;
1205
1389
  }
1206
- const { anchor, focus } = selection;
1207
- const [start, end] = Range.isForward(selection) ? [anchor, focus] : [focus, anchor];
1208
- const beforeStart = {
1209
- path: start.path,
1210
- offset: Math.max(0, start.offset - before.length)
1211
- };
1212
- const afterEnd = { path: end.path, offset: end.offset + after.length };
1213
- try {
1214
- const textBefore = Editor.string(editor, {
1215
- anchor: beforeStart,
1216
- focus: start
1390
+ if (content === lastContent.current) return;
1391
+ const newValue = deserialize(content, resolvedFeatures);
1392
+ replaceEditorChildren(editor, newValue);
1393
+ Transforms.select(editor, Editor.start(editor, []));
1394
+ lastContent.current = content;
1395
+ updateCharacterCount();
1396
+ bumpStateVersion();
1397
+ editor.onChange();
1398
+ }, [
1399
+ bumpStateVersion,
1400
+ content,
1401
+ editor,
1402
+ resolvedFeatures,
1403
+ updateCharacterCount
1404
+ ]);
1405
+ useEffect(() => {
1406
+ onStateChange?.(getEditorState());
1407
+ }, [getEditorState, onStateChange, stateVersion]);
1408
+ const decorate = useCallback(
1409
+ (entry) => {
1410
+ const ranges = computeDecorations(entry, editor, rehypePlugins);
1411
+ return ranges;
1412
+ },
1413
+ [editor, rehypePlugins]
1414
+ );
1415
+ const [activePlugin, setActivePluginState] = useState(
1416
+ null
1417
+ );
1418
+ const [activePluginQuery, setActivePluginQuery] = useState("");
1419
+ const activePluginQueryRef = useRef("");
1420
+ const activePluginRef = useRef(null);
1421
+ activePluginRef.current = activePlugin;
1422
+ const pluginPositionRef = useRef({
1423
+ top: 0,
1424
+ left: 0
1425
+ });
1426
+ const forwardedKeyListenersRef = useRef(/* @__PURE__ */ new Map());
1427
+ const wrapperRef = useRef(null);
1428
+ const editorElRef = useRef(null);
1429
+ const subscribeForwardedKeyFor = useCallback(
1430
+ (pluginName) => (listener) => {
1431
+ const map = forwardedKeyListenersRef.current;
1432
+ let listeners = map.get(pluginName);
1433
+ if (!listeners) {
1434
+ listeners = /* @__PURE__ */ new Set();
1435
+ map.set(pluginName, listeners);
1436
+ }
1437
+ listeners.add(listener);
1438
+ return () => {
1439
+ listeners?.delete(listener);
1440
+ if (listeners && listeners.size === 0) map.delete(pluginName);
1441
+ };
1442
+ },
1443
+ []
1444
+ );
1445
+ const emitForwardedKey = useCallback((pluginName, key) => {
1446
+ const listeners = forwardedKeyListenersRef.current.get(pluginName);
1447
+ if (!listeners || listeners.size === 0) return;
1448
+ for (const listener of listeners) listener(key);
1449
+ }, []);
1450
+ const canonicalizeEmptyEditor = useCallback(() => {
1451
+ if (Node.string(editor).trim().length !== 0) return;
1452
+ const onlyChild = editor.children[0];
1453
+ const isCanonicalEmptyParagraph = editor.children.length === 1 && onlyChild?.type === "paragraph" && Node.string(onlyChild).length === 0;
1454
+ if (isCanonicalEmptyParagraph) return;
1455
+ HistoryEditor.withoutSaving(editor, () => {
1456
+ Editor.withoutNormalizing(editor, () => {
1457
+ for (let index = editor.children.length - 1; index >= 0; index--) {
1458
+ Transforms.removeNodes(editor, { at: [index] });
1459
+ }
1460
+ Transforms.insertNodes(editor, {
1461
+ type: "paragraph",
1462
+ id: generateId(),
1463
+ children: [{ text: "" }]
1464
+ });
1217
1465
  });
1218
- const textAfter = Editor.string(editor, {
1219
- anchor: end,
1220
- focus: afterEnd
1466
+ });
1467
+ }, [editor]);
1468
+ const selectEditor = useCallback(
1469
+ (at = "end") => {
1470
+ try {
1471
+ Transforms.select(
1472
+ editor,
1473
+ at === "start" ? Editor.start(editor, []) : Editor.end(editor, [])
1474
+ );
1475
+ } catch {
1476
+ }
1477
+ },
1478
+ [editor]
1479
+ );
1480
+ const focusEditor = useCallback(
1481
+ (options) => {
1482
+ ReactEditor.focus(editor);
1483
+ if (options?.at) selectEditor(options.at);
1484
+ },
1485
+ [editor, selectEditor]
1486
+ );
1487
+ const replaceContent = useCallback(
1488
+ (content2, options) => {
1489
+ const select = options?.select ?? "start";
1490
+ const newValue = deserialize(content2, resolvedFeatures);
1491
+ suppressImperativeOnChange.current = true;
1492
+ replaceEditorChildren(editor, newValue);
1493
+ updateCharacterCount();
1494
+ bumpStateVersion();
1495
+ if (select !== "preserve") selectEditor(select);
1496
+ const nextContent = serializeContent();
1497
+ lastContent.current = nextContent;
1498
+ editor.onChange();
1499
+ queueMicrotask(() => {
1500
+ suppressImperativeOnChange.current = false;
1221
1501
  });
1222
- if (textBefore === before && textAfter === after) {
1223
- const expandedRange = { anchor: beforeStart, focus: afterEnd };
1224
- Transforms.select(editor, expandedRange);
1502
+ },
1503
+ [
1504
+ bumpStateVersion,
1505
+ editor,
1506
+ resolvedFeatures,
1507
+ selectEditor,
1508
+ serializeContent,
1509
+ updateCharacterCount
1510
+ ]
1511
+ );
1512
+ useImperativeHandle(
1513
+ ref,
1514
+ () => ({
1515
+ getState: getEditorState,
1516
+ focus: focusEditor,
1517
+ clear: (options) => replaceContent("", options),
1518
+ setContent: replaceContent,
1519
+ insertContent: (content2) => {
1520
+ focusEditor();
1521
+ const nodes = deserialize(content2, resolvedFeatures);
1522
+ Transforms.insertFragment(editor, nodes);
1523
+ }
1524
+ }),
1525
+ [
1526
+ editor,
1527
+ focusEditor,
1528
+ getEditorState,
1529
+ replaceContent,
1530
+ resolvedFeatures,
1531
+ serializeContent
1532
+ ]
1533
+ );
1534
+ const getCursorPosition = useCallback(() => {
1535
+ try {
1536
+ const domSelection = window.getSelection();
1537
+ if (!domSelection || domSelection.rangeCount === 0)
1538
+ return { top: 0, left: 0 };
1539
+ const range = domSelection.getRangeAt(0);
1540
+ let rect = range.getBoundingClientRect();
1541
+ if (rect.width === 0 && rect.height === 0 && domSelection.anchorNode) {
1542
+ const node = domSelection.anchorNode instanceof HTMLElement ? domSelection.anchorNode : domSelection.anchorNode.parentElement;
1543
+ if (node) rect = node.getBoundingClientRect();
1544
+ }
1545
+ const wrapperEl = wrapperRef.current;
1546
+ if (!wrapperEl) return { top: 0, left: 0 };
1547
+ const wrapperRect = wrapperEl.getBoundingClientRect();
1548
+ return {
1549
+ top: rect.bottom - wrapperRect.top + 4,
1550
+ left: rect.left - wrapperRect.left
1551
+ };
1552
+ } catch {
1553
+ return { top: 0, left: 0 };
1554
+ }
1555
+ }, []);
1556
+ const wrapSelection = useCallback(
1557
+ (before, after) => {
1558
+ const { selection } = editor;
1559
+ if (!selection) return;
1560
+ const selectedText = Editor.string(editor, selection);
1561
+ if (selectedText.startsWith(before) && selectedText.endsWith(after) && selectedText.length >= before.length + after.length) {
1225
1562
  Transforms.delete(editor);
1226
- Transforms.insertText(editor, selectedText);
1563
+ Transforms.insertText(
1564
+ editor,
1565
+ selectedText.slice(before.length, -after.length || void 0)
1566
+ );
1227
1567
  return;
1228
1568
  }
1569
+ const { anchor, focus } = selection;
1570
+ const [start, end] = Range.isForward(selection) ? [anchor, focus] : [focus, anchor];
1571
+ const beforeStart = {
1572
+ path: start.path,
1573
+ offset: Math.max(0, start.offset - before.length)
1574
+ };
1575
+ const afterEnd = { path: end.path, offset: end.offset + after.length };
1576
+ try {
1577
+ const textBefore = Editor.string(editor, {
1578
+ anchor: beforeStart,
1579
+ focus: start
1580
+ });
1581
+ const textAfter = Editor.string(editor, {
1582
+ anchor: end,
1583
+ focus: afterEnd
1584
+ });
1585
+ if (textBefore === before && textAfter === after) {
1586
+ const expandedRange = { anchor: beforeStart, focus: afterEnd };
1587
+ Transforms.select(editor, expandedRange);
1588
+ Transforms.delete(editor);
1589
+ Transforms.insertText(editor, selectedText);
1590
+ return;
1591
+ }
1592
+ } catch {
1593
+ }
1594
+ Transforms.delete(editor);
1595
+ Transforms.insertText(editor, `${before}${selectedText}${after}`);
1596
+ },
1597
+ [editor]
1598
+ );
1599
+ const insertTextAtCursor = useCallback(
1600
+ (text) => {
1601
+ ReactEditor.focus(editor);
1602
+ const nodes = deserialize(text, resolvedFeatures);
1603
+ Transforms.insertFragment(editor, nodes);
1604
+ },
1605
+ [editor, resolvedFeatures]
1606
+ );
1607
+ const getCurrentBlockPath = useCallback(() => {
1608
+ const { selection } = editor;
1609
+ if (!selection || !Range.isCollapsed(selection)) return null;
1610
+ return selection.anchor.path.slice(0, 1);
1611
+ }, [editor]);
1612
+ const getRangeContent = useCallback(
1613
+ (range) => {
1614
+ try {
1615
+ return Editor.string(editor, range);
1616
+ } catch {
1617
+ return null;
1618
+ }
1619
+ },
1620
+ [editor]
1621
+ );
1622
+ const getCurrentBlockContent = useCallback(() => {
1623
+ const path = getCurrentBlockPath();
1624
+ if (!path) return null;
1625
+ try {
1626
+ return Editor.string(editor, path);
1229
1627
  } catch {
1628
+ return null;
1230
1629
  }
1231
- Transforms.delete(editor);
1232
- Transforms.insertText(editor, `${before}${selectedText}${after}`);
1233
- },
1234
- [editor]
1235
- );
1236
- const insertTextAtCursor = useCallback(
1237
- (text) => {
1630
+ }, [editor, getCurrentBlockPath]);
1631
+ const getCurrentBlockContentBeforeCursor = useCallback(() => {
1632
+ const { selection } = editor;
1633
+ if (!selection || !Range.isCollapsed(selection)) return null;
1634
+ const anchor = selection.anchor;
1635
+ return getRangeContent({
1636
+ anchor: { path: anchor.path, offset: 0 },
1637
+ focus: anchor
1638
+ });
1639
+ }, [editor, getRangeContent]);
1640
+ const replaceCurrentBlockContent = useCallback(
1641
+ (nextContent) => {
1642
+ const path = getCurrentBlockPath();
1643
+ if (!path) return;
1644
+ const start = Editor.start(editor, path);
1645
+ const end = Editor.end(editor, path);
1646
+ Transforms.select(editor, { anchor: start, focus: end });
1647
+ Transforms.insertText(editor, nextContent);
1648
+ Transforms.select(editor, Editor.end(editor, path));
1649
+ },
1650
+ [editor, getCurrentBlockPath]
1651
+ );
1652
+ const clearCurrentBlock = useCallback(() => {
1653
+ const path = getCurrentBlockPath();
1654
+ if (!path) return;
1655
+ try {
1656
+ const start = Editor.start(editor, path);
1657
+ const end = Editor.end(editor, path);
1658
+ Transforms.select(editor, { anchor: start, focus: end });
1659
+ Transforms.delete(editor);
1660
+ editor.onChange();
1661
+ } catch {
1662
+ }
1663
+ }, [editor, getCurrentBlockPath]);
1664
+ const insertImage = useCallback(
1665
+ (image) => {
1666
+ const id = image.id ?? generateId();
1667
+ Transforms.insertNodes(editor, {
1668
+ type: "image",
1669
+ id,
1670
+ url: image.url,
1671
+ alt: image.alt,
1672
+ children: [{ text: "" }]
1673
+ });
1674
+ return id;
1675
+ },
1676
+ [editor]
1677
+ );
1678
+ const updateImage = useCallback(
1679
+ (id, image) => {
1680
+ for (const [node, path] of Node.nodes(editor)) {
1681
+ if (Editor.isEditor(node) || !("type" in node) || node.type !== "image" || node.id !== id) {
1682
+ continue;
1683
+ }
1684
+ Transforms.setNodes(editor, image, { at: path });
1685
+ break;
1686
+ }
1687
+ },
1688
+ [editor]
1689
+ );
1690
+ const removeImage = useCallback(
1691
+ (id) => {
1692
+ for (const [node, path] of Node.nodes(editor)) {
1693
+ if (Editor.isEditor(node) || !("type" in node) || node.type !== "image" || node.id !== id) {
1694
+ continue;
1695
+ }
1696
+ Transforms.removeNodes(editor, { at: path });
1697
+ break;
1698
+ }
1699
+ },
1700
+ [editor]
1701
+ );
1702
+ const pluginEditorImplRef = useRef(null);
1703
+ const getPluginEditorImpl = useCallback(() => {
1704
+ const impl = pluginEditorImplRef.current;
1705
+ if (!impl) throw new Error("Inkwell plugin editor is not ready");
1706
+ return impl;
1707
+ }, []);
1708
+ const pluginEditor = useMemo(
1709
+ () => ({
1710
+ getState: () => getPluginEditorImpl().getState(),
1711
+ isEmpty: () => getPluginEditorImpl().isEmpty(),
1712
+ focus: (options) => getPluginEditorImpl().focus(options),
1713
+ clear: (options) => getPluginEditorImpl().clear(options),
1714
+ setContent: (content2, options) => getPluginEditorImpl().setContent(content2, options),
1715
+ insertContent: (content2) => getPluginEditorImpl().insertContent(content2),
1716
+ getContentBeforeCursor: () => getPluginEditorImpl().getContentBeforeCursor(),
1717
+ getCurrentBlockContent: () => getPluginEditorImpl().getCurrentBlockContent(),
1718
+ getCurrentBlockContentBeforeCursor: () => getPluginEditorImpl().getCurrentBlockContentBeforeCursor(),
1719
+ replaceCurrentBlockContent: (content2) => getPluginEditorImpl().replaceCurrentBlockContent(content2),
1720
+ clearCurrentBlock: () => getPluginEditorImpl().clearCurrentBlock(),
1721
+ wrapSelection: (before, after) => getPluginEditorImpl().wrapSelection(before, after),
1722
+ insertImage: (image) => getPluginEditorImpl().insertImage(image),
1723
+ updateImage: (id, image) => getPluginEditorImpl().updateImage(id, image),
1724
+ removeImage: (id) => getPluginEditorImpl().removeImage(id)
1725
+ }),
1726
+ [getPluginEditorImpl]
1727
+ );
1728
+ pluginEditorImplRef.current = {
1729
+ getState: getEditorState,
1730
+ isEmpty: () => serializeContent().trim().length === 0,
1731
+ focus: focusEditor,
1732
+ clear: (options) => replaceContent("", options),
1733
+ setContent: replaceContent,
1734
+ insertContent: insertTextAtCursor,
1735
+ getContentBeforeCursor: () => {
1736
+ const { selection } = editor;
1737
+ if (!selection || !Range.isCollapsed(selection)) return null;
1738
+ return getRangeContent({
1739
+ anchor: Editor.start(editor, []),
1740
+ focus: selection.anchor
1741
+ });
1742
+ },
1743
+ getCurrentBlockContent,
1744
+ getCurrentBlockContentBeforeCursor,
1745
+ replaceCurrentBlockContent,
1746
+ clearCurrentBlock,
1747
+ wrapSelection,
1748
+ insertImage,
1749
+ updateImage,
1750
+ removeImage
1751
+ };
1752
+ useEffect(() => {
1753
+ const { insertData } = editor;
1754
+ editor.insertData = (data) => {
1755
+ const baseContext = {
1756
+ editor: pluginEditor,
1757
+ insertData
1758
+ };
1759
+ for (const plugin of plugins) {
1760
+ if (plugin.onInsertData?.(data, baseContext)) return;
1761
+ }
1762
+ insertData(data);
1763
+ };
1764
+ const cleanups = [];
1765
+ for (const plugin of plugins) {
1766
+ if (!plugin.setup) continue;
1767
+ const cleanup = plugin.setup(pluginEditor);
1768
+ if (typeof cleanup === "function") cleanups.push(cleanup);
1769
+ }
1770
+ return () => {
1771
+ editor.insertData = insertData;
1772
+ for (let i = cleanups.length - 1; i >= 0; i--) cleanups[i]();
1773
+ };
1774
+ }, [editor, plugins, pluginEditor, wrapSelection]);
1775
+ pluginEditorRef.current = pluginEditor;
1776
+ const dismissPlugin = useCallback(() => {
1777
+ activePluginRef.current = null;
1778
+ setActivePluginState(null);
1779
+ activePluginQueryRef.current = "";
1780
+ setActivePluginQuery("");
1238
1781
  ReactEditor.focus(editor);
1239
- const nodes = deserialize(text);
1240
- Transforms.insertFragment(editor, nodes);
1241
- },
1242
- [editor]
1243
- );
1244
- const dismissPlugin = useCallback(() => {
1245
- setActivePlugin(null);
1246
- ReactEditor.focus(editor);
1247
- }, [editor]);
1248
- const handlePluginSelect = useCallback(
1249
- (text) => {
1250
- const triggerKey = activePlugin?.trigger?.key;
1251
- const isCharTrigger = triggerKey && !triggerKey.includes("+");
1252
- dismissPlugin();
1253
- requestAnimationFrame(() => {
1254
- ReactEditor.focus(editor);
1255
- if (isCharTrigger) {
1256
- Transforms.delete(editor, {
1257
- distance: 1,
1258
- unit: "character",
1259
- reverse: true
1260
- });
1782
+ }, [editor]);
1783
+ const activatePlugin = useCallback(
1784
+ (plugin, options) => {
1785
+ const initialQuery = options?.query ?? "";
1786
+ activePluginQueryRef.current = initialQuery;
1787
+ setActivePluginQuery(initialQuery);
1788
+ pluginPositionRef.current = getCursorPosition();
1789
+ activePluginRef.current = plugin;
1790
+ setActivePluginState(plugin);
1791
+ },
1792
+ [getCursorPosition]
1793
+ );
1794
+ const handlePluginSelect = useCallback(
1795
+ (text) => {
1796
+ const activation = activePlugin?.activation;
1797
+ const triggerKey = activation?.type === "trigger" ? activation.key : void 0;
1798
+ const isCharTrigger = triggerKey && !triggerKey.includes("+");
1799
+ const queryLength = activePluginQueryRef.current.length;
1800
+ dismissPlugin();
1801
+ requestAnimationFrame(() => {
1802
+ ReactEditor.focus(editor);
1803
+ if (isCharTrigger) {
1804
+ Transforms.delete(editor, {
1805
+ distance: 1 + queryLength,
1806
+ unit: "character",
1807
+ reverse: true
1808
+ });
1809
+ }
1810
+ insertTextAtCursor(text);
1811
+ });
1812
+ },
1813
+ [dismissPlugin, insertTextAtCursor, activePlugin, editor]
1814
+ );
1815
+ const isActivatablePlugin = (plugin) => (plugin.activation?.type ?? "always") !== "always";
1816
+ const makePluginProps = (plugin) => ({
1817
+ active: isActivatablePlugin(plugin) ? activePlugin === plugin : true,
1818
+ query: activePlugin === plugin ? activePluginQuery : "",
1819
+ onSelect: handlePluginSelect,
1820
+ onDismiss: dismissPlugin,
1821
+ position: pluginPositionRef.current,
1822
+ editorRef: editorElRef,
1823
+ editor: pluginEditor,
1824
+ wrapSelection,
1825
+ subscribeForwardedKey: subscribeForwardedKeyFor(plugin.name)
1826
+ });
1827
+ const makeKeyDownContext = useCallback(
1828
+ (plugin) => ({
1829
+ editor: pluginEditor,
1830
+ wrapSelection,
1831
+ activate: (options) => activatePlugin(plugin, options),
1832
+ dismiss: dismissPlugin
1833
+ }),
1834
+ [activatePlugin, dismissPlugin, pluginEditor, wrapSelection]
1835
+ );
1836
+ const handleKeyDown = useCallback(
1837
+ (event) => {
1838
+ if (activePlugin) {
1839
+ const activeResult = activePlugin.onActiveKeyDown?.(
1840
+ event,
1841
+ makeKeyDownContext(activePlugin)
1842
+ );
1843
+ if (activeResult === false) {
1844
+ dismissPlugin();
1845
+ } else {
1846
+ if (event.defaultPrevented) return;
1847
+ if (event.key === "Escape") {
1848
+ event.preventDefault();
1849
+ dismissPlugin();
1850
+ return;
1851
+ }
1852
+ const isPrintable = !event.metaKey && !event.ctrlKey && !event.altKey && event.key.length === 1;
1853
+ const shouldForward = event.key === "ArrowDown" || event.key === "ArrowUp" || event.key === "Enter" || event.key === "Backspace" || isPrintable;
1854
+ if (shouldForward) {
1855
+ if (event.key === "Enter") {
1856
+ event.preventDefault();
1857
+ } else if (event.key === "ArrowDown" || event.key === "ArrowUp") {
1858
+ event.preventDefault();
1859
+ } else if (event.key === "Backspace") {
1860
+ if (activePluginQueryRef.current.length === 0) {
1861
+ dismissPlugin();
1862
+ return;
1863
+ }
1864
+ const nextQuery = activePluginQueryRef.current.slice(0, -1);
1865
+ activePluginQueryRef.current = nextQuery;
1866
+ setActivePluginQuery(nextQuery);
1867
+ } else if (isPrintable) {
1868
+ const nextQuery = `${activePluginQueryRef.current}${event.key}`;
1869
+ activePluginQueryRef.current = nextQuery;
1870
+ setActivePluginQuery(nextQuery);
1871
+ }
1872
+ emitForwardedKey(activePlugin.name, event.key);
1873
+ }
1874
+ return;
1875
+ }
1261
1876
  }
1262
- insertTextAtCursor(text);
1263
- });
1877
+ for (const plugin of plugins) {
1878
+ plugin.onKeyDown?.(event, makeKeyDownContext(plugin));
1879
+ if (event.defaultPrevented) return;
1880
+ if (activePluginRef.current) return;
1881
+ }
1882
+ if (submitOnEnter && event.key === "Enter" && !event.shiftKey) {
1883
+ event.preventDefault();
1884
+ onSubmit?.(serializeContent());
1885
+ return;
1886
+ }
1887
+ for (const plugin of plugins) {
1888
+ const activation = plugin.activation;
1889
+ if (activation?.type !== "trigger") continue;
1890
+ const parts = activation.key.toLowerCase().split("+").map((s) => s.trim());
1891
+ const key = parts[parts.length - 1];
1892
+ const mods = new Set(parts.slice(0, -1));
1893
+ const hasModifiers = mods.size > 0;
1894
+ const needCtrl = mods.has("control") || mods.has("ctrl");
1895
+ const needMeta = mods.has("meta") || mods.has("cmd") || mods.has("command");
1896
+ const needAlt = mods.has("alt");
1897
+ const needShift = mods.has("shift");
1898
+ const keyMatch = event.key.toLowerCase() === key;
1899
+ const modMatch = hasModifiers ? event.ctrlKey === needCtrl && event.metaKey === needMeta && event.altKey === needAlt && event.shiftKey === needShift : !event.ctrlKey && !event.metaKey;
1900
+ if (keyMatch && modMatch) {
1901
+ if (plugin.shouldTrigger && !plugin.shouldTrigger(event, makeKeyDownContext(plugin))) {
1902
+ continue;
1903
+ }
1904
+ if (hasModifiers) event.preventDefault();
1905
+ activatePlugin(plugin);
1906
+ return;
1907
+ }
1908
+ }
1909
+ if (event.key === "Tab" && !event.shiftKey && !event.metaKey && !event.ctrlKey && !event.altKey) {
1910
+ const { selection } = editor;
1911
+ if (selection) {
1912
+ const [match] = Editor.nodes(editor, {
1913
+ match: (n) => Element.isElement(n)
1914
+ });
1915
+ if (match) {
1916
+ const [node, path] = match;
1917
+ const element = node;
1918
+ if (element.type === "paragraph") {
1919
+ const text = Node.string(node);
1920
+ if (ORDERED_LIST_MARKER_RE.test(text)) {
1921
+ event.preventDefault();
1922
+ return;
1923
+ }
1924
+ if (UNORDERED_LIST_MARKER_RE.test(text)) {
1925
+ event.preventDefault();
1926
+ const savedSelection = editor.selection;
1927
+ Transforms.insertText(editor, " ", {
1928
+ at: { path: [...path, 0], offset: 0 }
1929
+ });
1930
+ if (savedSelection) {
1931
+ Transforms.select(editor, {
1932
+ anchor: {
1933
+ path: savedSelection.anchor.path,
1934
+ offset: savedSelection.anchor.offset + 2
1935
+ },
1936
+ focus: {
1937
+ path: savedSelection.focus.path,
1938
+ offset: savedSelection.focus.offset + 2
1939
+ }
1940
+ });
1941
+ }
1942
+ return;
1943
+ }
1944
+ }
1945
+ }
1946
+ }
1947
+ }
1948
+ if (event.key === "a" && (event.metaKey || event.ctrlKey) && !Node.string(editor).trim()) {
1949
+ event.preventDefault();
1950
+ return;
1951
+ }
1952
+ },
1953
+ [
1954
+ plugins,
1955
+ activePlugin,
1956
+ editor,
1957
+ getCursorPosition,
1958
+ dismissPlugin,
1959
+ makeKeyDownContext,
1960
+ activatePlugin,
1961
+ emitForwardedKey,
1962
+ submitOnEnter,
1963
+ onSubmit,
1964
+ serializeContent
1965
+ ]
1966
+ );
1967
+ const pluginPlaceholder = plugins.reduce(
1968
+ (value, plugin) => {
1969
+ if (value) return value;
1970
+ const nextPlaceholder = plugin.getPlaceholder?.(pluginEditor) ?? null;
1971
+ if (!nextPlaceholder) return null;
1972
+ return typeof nextPlaceholder === "string" ? { text: nextPlaceholder } : nextPlaceholder;
1973
+ },
1974
+ null
1975
+ );
1976
+ const basePlaceholder = pluginPlaceholder?.text ?? placeholder ?? "Start writing...";
1977
+ const resolvedPlaceholder = pluginPlaceholder?.hint ? `${pluginPlaceholder.hint} ${basePlaceholder}` : basePlaceholder;
1978
+ useLayoutEffect(() => {
1979
+ if (!pluginPlaceholder) return;
1980
+ if (Node.string(editor).trim().length !== 0) return;
1981
+ canonicalizeEmptyEditor();
1982
+ selectEditor("start");
1983
+ }, [
1984
+ canonicalizeEmptyEditor,
1985
+ editor,
1986
+ pluginPlaceholder,
1987
+ selectEditor,
1988
+ stateVersion
1989
+ ]);
1990
+ return /* @__PURE__ */ jsxs(
1991
+ "div",
1992
+ {
1993
+ ref: wrapperRef,
1994
+ className: `inkwell-editor-wrapper${hasCharacterLimit ? " inkwell-editor-has-character-limit" : ""}${overLimit ? " inkwell-editor-over-limit" : ""}${className ? ` ${className}` : ""}${classNames?.root ? ` ${classNames.root}` : ""}`,
1995
+ style: styles?.root,
1996
+ children: [
1997
+ hasCharacterLimit && /* @__PURE__ */ jsx(
1998
+ CharacterCount,
1999
+ {
2000
+ count: characterCount,
2001
+ limit: characterLimit,
2002
+ over: overLimit
2003
+ }
2004
+ ),
2005
+ activePlugin && /* @__PURE__ */ jsx(
2006
+ "div",
2007
+ {
2008
+ className: "inkwell-plugin-backdrop",
2009
+ style: {
2010
+ position: "fixed",
2011
+ inset: 0,
2012
+ zIndex: 999,
2013
+ background: "transparent"
2014
+ },
2015
+ onMouseDown: dismissPlugin
2016
+ }
2017
+ ),
2018
+ plugins.map((plugin) => {
2019
+ const props = makePluginProps(plugin);
2020
+ if (!props.active || !plugin.render) return null;
2021
+ return /* @__PURE__ */ jsx(Fragment, { children: plugin.render(props) }, plugin.name);
2022
+ }),
2023
+ /* @__PURE__ */ jsx(
2024
+ Slate,
2025
+ {
2026
+ editor,
2027
+ initialValue,
2028
+ onChange: handleChange,
2029
+ children: /* @__PURE__ */ jsx(
2030
+ Editable,
2031
+ {
2032
+ ref: editorElRef,
2033
+ className: `inkwell-editor${classNames?.editor ? ` ${classNames.editor}` : ""}`,
2034
+ style: styles?.editor,
2035
+ renderElement: RenderElement,
2036
+ renderLeaf: RenderLeaf,
2037
+ decorate,
2038
+ placeholder: resolvedPlaceholder,
2039
+ spellCheck: true,
2040
+ role: "textbox",
2041
+ "aria-multiline": true,
2042
+ "aria-placeholder": resolvedPlaceholder,
2043
+ "data-placeholder": resolvedPlaceholder,
2044
+ readOnly: !editable,
2045
+ onFocus: () => scheduleFocusedState(true),
2046
+ onBlur: () => scheduleFocusedState(false),
2047
+ onKeyDown: handleKeyDown
2048
+ }
2049
+ )
2050
+ }
2051
+ )
2052
+ ]
2053
+ }
2054
+ );
2055
+ }
2056
+ );
2057
+
2058
+ // src/plugins/attachments/index.tsx
2059
+ function mimeMatches(mime, accept) {
2060
+ if (!mime) return false;
2061
+ const patterns = accept.split(",").map((p) => p.trim()).filter(Boolean);
2062
+ return patterns.some((pattern) => {
2063
+ if (pattern.endsWith("/*")) {
2064
+ const prefix = pattern.slice(0, -1);
2065
+ return mime.startsWith(prefix);
2066
+ }
2067
+ return mime === pattern;
2068
+ });
2069
+ }
2070
+ function isImageFile(file) {
2071
+ return file.type.startsWith("image/");
2072
+ }
2073
+ function extractFiles(data) {
2074
+ if (data.files && data.files.length > 0) return Array.from(data.files);
2075
+ if (!data.items) return [];
2076
+ const files = [];
2077
+ for (const item of Array.from(data.items)) {
2078
+ if (item.kind !== "file") continue;
2079
+ const file = item.getAsFile();
2080
+ if (file) files.push(file);
2081
+ }
2082
+ return files;
2083
+ }
2084
+ function filesOnlyDataTransfer(files) {
2085
+ return {
2086
+ types: files.length > 0 ? ["Files"] : [],
2087
+ files,
2088
+ items: void 0,
2089
+ getData: () => "",
2090
+ setData: () => {
1264
2091
  },
1265
- [dismissPlugin, insertTextAtCursor, activePlugin, editor]
1266
- );
1267
- const makePluginProps = (plugin) => ({
1268
- active: plugin.trigger ? activePlugin === plugin : true,
1269
- query: "",
1270
- onSelect: handlePluginSelect,
1271
- onDismiss: dismissPlugin,
1272
- position: pluginPositionRef.current,
1273
- editorRef: editorElRef,
1274
- wrapSelection
2092
+ clearData: () => {
2093
+ },
2094
+ setDragImage: () => {
2095
+ },
2096
+ dropEffect: "none",
2097
+ effectAllowed: "all"
2098
+ };
2099
+ }
2100
+ function extractHtmlImages(data) {
2101
+ const html = data.getData("text/html");
2102
+ if (!html) return [];
2103
+ const template = document.createElement("template");
2104
+ template.innerHTML = html;
2105
+ const images = [];
2106
+ for (const img of Array.from(template.content.querySelectorAll("img"))) {
2107
+ const url = sanitizeImageUrl(img.getAttribute("src"));
2108
+ if (!url) continue;
2109
+ images.push({ url, alt: img.getAttribute("alt") ?? "" });
2110
+ }
2111
+ return images;
2112
+ }
2113
+ var insertUploadedImage = (editor, file, options) => {
2114
+ const placeholder = options.uploadingPlaceholder?.(file) ?? "Uploading\u2026";
2115
+ const id = editor.insertImage({ url: "", alt: placeholder });
2116
+ Promise.resolve().then(() => options.onUpload(file)).then((result) => {
2117
+ const url = typeof result === "string" ? result : result.url;
2118
+ const safeUrl = sanitizeImageUrl(url);
2119
+ if (!safeUrl) {
2120
+ editor.removeImage(id);
2121
+ options.onError?.(new Error("Unsafe upload URL"), file);
2122
+ return;
2123
+ }
2124
+ const alt = typeof result === "string" ? file.name : result.alt ?? file.name;
2125
+ editor.updateImage(id, { url: safeUrl, alt });
2126
+ }).catch((err) => {
2127
+ editor.removeImage(id);
2128
+ options.onError?.(err, file);
1275
2129
  });
1276
- const handleKeyDown = useCallback(
1277
- (event) => {
1278
- if (activePlugin) {
1279
- if (event.key === "Escape") {
1280
- event.preventDefault();
1281
- dismissPlugin();
2130
+ };
2131
+ var insertUploadedAttachment = (file, options) => {
2132
+ const onAttachmentAdd = options.onAttachmentAdd;
2133
+ if (!onAttachmentAdd) return;
2134
+ Promise.resolve().then(() => options.onUpload(file)).then((result) => {
2135
+ const url = typeof result === "string" ? result : result.url;
2136
+ if (!isSafeImageUrl(url)) {
2137
+ options.onError?.(new Error("Unsafe upload URL"), file);
2138
+ return;
2139
+ }
2140
+ const extra = typeof result === "string" ? {} : Object.fromEntries(
2141
+ Object.entries(result).filter(
2142
+ ([k]) => k !== "url" && k !== "alt"
2143
+ )
2144
+ );
2145
+ onAttachmentAdd({
2146
+ ...extra,
2147
+ url: url.trim(),
2148
+ filename: file.name || "attachment",
2149
+ mime: file.type,
2150
+ size: file.size
2151
+ });
2152
+ }).catch((err) => {
2153
+ options.onError?.(err, file);
2154
+ });
2155
+ };
2156
+ function createAttachmentsPlugin(options) {
2157
+ const { accept } = options;
2158
+ return {
2159
+ name: "attachments",
2160
+ onInsertData(data, { editor, insertData }) {
2161
+ const files = extractFiles(data);
2162
+ const matching = accept ? files.filter((f) => mimeMatches(f.type, accept)) : files;
2163
+ const handled = matching.filter(
2164
+ (f) => isImageFile(f) || options.onAttachmentAdd !== void 0
2165
+ );
2166
+ if (handled.length === 0) {
2167
+ const htmlImages = extractHtmlImages(data);
2168
+ if (htmlImages.length === 0) return false;
2169
+ for (const image of htmlImages) {
2170
+ editor.insertImage(image);
1282
2171
  }
1283
- return;
2172
+ return true;
1284
2173
  }
1285
- for (const plugin of plugins) {
1286
- plugin.onKeyDown?.(event, { wrapSelection });
1287
- if (event.defaultPrevented) return;
2174
+ const unhandled = files.filter((f) => !handled.includes(f));
2175
+ if (unhandled.length > 0) {
2176
+ insertData(filesOnlyDataTransfer(unhandled));
1288
2177
  }
1289
- for (const plugin of plugins) {
1290
- const t = plugin.trigger;
1291
- if (!t) continue;
1292
- const parts = t.key.toLowerCase().split("+").map((s) => s.trim());
1293
- const key = parts[parts.length - 1];
1294
- const mods = new Set(parts.slice(0, -1));
1295
- const hasModifiers = mods.size > 0;
1296
- const needCtrl = mods.has("control") || mods.has("ctrl");
1297
- const needMeta = mods.has("meta") || mods.has("cmd") || mods.has("command");
1298
- const needAlt = mods.has("alt");
1299
- const needShift = mods.has("shift");
1300
- const keyMatch = event.key.toLowerCase() === key;
1301
- const modMatch = hasModifiers ? event.ctrlKey === needCtrl && event.metaKey === needMeta && event.altKey === needAlt && event.shiftKey === needShift : !event.ctrlKey && !event.metaKey;
1302
- if (keyMatch && modMatch) {
1303
- if (hasModifiers) event.preventDefault();
1304
- pluginPositionRef.current = getCursorPosition();
1305
- setActivePlugin(plugin);
1306
- return;
2178
+ for (const file of handled) {
2179
+ if (isImageFile(file)) {
2180
+ insertUploadedImage(editor, file, options);
2181
+ } else {
2182
+ insertUploadedAttachment(file, options);
1307
2183
  }
1308
2184
  }
1309
- if (event.key === "a" && (event.metaKey || event.ctrlKey) && !Node.string(editor).trim()) {
2185
+ return true;
2186
+ }
2187
+ };
2188
+ }
2189
+
2190
+ // src/plugins/completions/index.tsx
2191
+ var isPlainTypingKey = (event) => {
2192
+ return event.key.length === 1 && !event.metaKey && !event.ctrlKey && !event.altKey;
2193
+ };
2194
+ var lastAcceptedCompletionByEditor = /* @__PURE__ */ new WeakMap();
2195
+ var setLastAcceptedCompletion = (editor, pluginName, completion) => {
2196
+ const completions = lastAcceptedCompletionByEditor.get(editor) ?? /* @__PURE__ */ new Map();
2197
+ completions.set(pluginName, completion);
2198
+ lastAcceptedCompletionByEditor.set(editor, completions);
2199
+ };
2200
+ var takeLastAcceptedCompletion = (editor, pluginName) => {
2201
+ const completions = lastAcceptedCompletionByEditor.get(editor);
2202
+ if (!completions) return null;
2203
+ const completion = completions.get(pluginName) ?? null;
2204
+ completions.delete(pluginName);
2205
+ if (completions.size === 0) {
2206
+ lastAcceptedCompletionByEditor.delete(editor);
2207
+ }
2208
+ return completion;
2209
+ };
2210
+ function createCompletionsPlugin({
2211
+ name = "completions",
2212
+ getCompletion,
2213
+ isLoading,
2214
+ loadingText = "Loading suggestion\u2026",
2215
+ acceptHint = "[tab \u21B9]",
2216
+ onAccept,
2217
+ onDismiss,
2218
+ onRestore,
2219
+ restoreOnUndo = true
2220
+ }) {
2221
+ return {
2222
+ name,
2223
+ getPlaceholder: (editor) => {
2224
+ if (!editor.isEmpty()) return null;
2225
+ const text = getCompletion() ?? (isLoading?.() ? loadingText : null);
2226
+ if (!text) return null;
2227
+ return { text, hint: acceptHint };
2228
+ },
2229
+ onKeyDown: (event, { editor }) => {
2230
+ const completion = getCompletion();
2231
+ if (!completion) return;
2232
+ if (!editor.isEmpty()) return;
2233
+ if (event.key === "Tab") {
1310
2234
  event.preventDefault();
2235
+ setLastAcceptedCompletion(editor, name, completion);
2236
+ editor.insertContent(completion);
2237
+ onAccept?.(completion);
1311
2238
  return;
1312
2239
  }
2240
+ if (event.key === "Escape") {
2241
+ event.preventDefault();
2242
+ onDismiss?.(completion);
2243
+ return;
2244
+ }
2245
+ if (isPlainTypingKey(event)) {
2246
+ onDismiss?.(completion);
2247
+ }
1313
2248
  },
1314
- [
1315
- plugins,
1316
- activePlugin,
1317
- editor,
1318
- getCursorPosition,
1319
- dismissPlugin,
1320
- wrapSelection
1321
- ]
1322
- );
1323
- return /* @__PURE__ */ jsxs(
1324
- "div",
1325
- {
1326
- ref: wrapperRef,
1327
- className: `inkwell-editor-wrapper ${className ?? ""}`,
1328
- children: [
1329
- activePlugin && /* @__PURE__ */ jsx(
1330
- "div",
1331
- {
1332
- className: "inkwell-plugin-backdrop",
1333
- style: {
1334
- position: "fixed",
1335
- inset: 0,
1336
- zIndex: 999,
1337
- background: "transparent"
1338
- },
1339
- onMouseDown: dismissPlugin
1340
- }
1341
- ),
1342
- plugins.map((plugin) => {
1343
- const props = makePluginProps(plugin);
1344
- if (!props.active) return null;
1345
- return /* @__PURE__ */ jsx(Fragment, { children: plugin.render(props) }, plugin.name);
1346
- }),
1347
- /* @__PURE__ */ jsx(
1348
- Slate,
1349
- {
1350
- editor,
1351
- initialValue,
1352
- onChange: handleChange,
1353
- children: /* @__PURE__ */ jsx(
1354
- Editable,
1355
- {
1356
- ref: editorElRef,
1357
- className: "inkwell-editor",
1358
- renderElement: RenderElement,
1359
- renderLeaf: RenderLeaf,
1360
- decorate,
1361
- placeholder: placeholder ?? "Start writing...",
1362
- spellCheck: true,
1363
- role: "textbox",
1364
- "aria-multiline": true,
1365
- "aria-placeholder": placeholder,
1366
- "data-placeholder": placeholder ?? "Start writing...",
1367
- onKeyDown: handleKeyDown
1368
- }
1369
- )
1370
- }
1371
- )
1372
- ]
2249
+ onEditorChange: (editor) => {
2250
+ if (!restoreOnUndo) return;
2251
+ if (!editor.isEmpty()) return;
2252
+ const lastAcceptedCompletion = takeLastAcceptedCompletion(editor, name);
2253
+ if (!lastAcceptedCompletion) return;
2254
+ onRestore?.(lastAcceptedCompletion);
1373
2255
  }
1374
- );
2256
+ };
1375
2257
  }
1376
- var cls2 = pluginClass("snippets");
1377
- function SnippetPicker({
1378
- snippets,
2258
+ var BASE = "inkwell-plugin-picker";
2259
+ var pluginPickerClass = {
2260
+ popup: `${BASE}-popup`,
2261
+ picker: `${BASE}`,
2262
+ search: `${BASE}-search`,
2263
+ item: `${BASE}-item`,
2264
+ itemActive: `${BASE}-item-active`,
2265
+ empty: `${BASE}-empty`,
2266
+ title: `${BASE}-title`,
2267
+ subtitle: `${BASE}-subtitle`,
2268
+ preview: `${BASE}-preview`
2269
+ };
2270
+ function PluginMenuPrimitive({
2271
+ items,
2272
+ search,
2273
+ getKey,
2274
+ renderItem,
2275
+ itemToText,
2276
+ placeholder,
2277
+ emptyMessage = "No results",
1379
2278
  onSelect,
1380
- onDismiss
2279
+ onDismiss,
2280
+ position,
2281
+ subscribeForwardedKey
1381
2282
  }) {
1382
2283
  const [selectedIndex, setSelectedIndex] = useState(0);
1383
- const [search, setSearch] = useState("");
1384
- const filtered = snippets.filter(
1385
- (s) => s.title.toLowerCase().includes(search.toLowerCase())
1386
- );
1387
- const focusRef = useCallback((el) => {
1388
- if (el) requestAnimationFrame(() => el.focus());
2284
+ const [query, setQuery] = useState("");
2285
+ const [asyncResults, setAsyncResults] = useState([]);
2286
+ const selectedIndexRef = useRef(0);
2287
+ const resultsRef = useRef(items ?? []);
2288
+ const updateSelectedIndex = useCallback((next) => {
2289
+ selectedIndexRef.current = next;
2290
+ setSelectedIndex(next);
1389
2291
  }, []);
1390
- const activeItemRef = useCallback((el) => {
1391
- if (el && typeof el.scrollIntoView === "function") {
1392
- el.scrollIntoView({ block: "nearest" });
2292
+ const syncResults = useMemo(() => {
2293
+ if (search) return null;
2294
+ const all = items ?? [];
2295
+ return all.filter(
2296
+ (item) => getKey(item).toLowerCase().includes(query.toLowerCase())
2297
+ );
2298
+ }, [getKey, items, query, search]);
2299
+ const results = syncResults ?? asyncResults;
2300
+ useEffect(() => {
2301
+ if (!syncResults) return;
2302
+ resultsRef.current = syncResults;
2303
+ if (selectedIndexRef.current >= syncResults.length) {
2304
+ updateSelectedIndex(0);
1393
2305
  }
1394
- }, []);
1395
- const handleSearchKeyDown = useCallback(
1396
- (e) => {
1397
- switch (e.key) {
1398
- case "ArrowDown":
1399
- e.preventDefault();
1400
- setSelectedIndex((prev) => prev < filtered.length - 1 ? prev + 1 : 0);
2306
+ }, [syncResults, updateSelectedIndex]);
2307
+ useEffect(() => {
2308
+ if (!search) return;
2309
+ let cancelled = false;
2310
+ Promise.resolve(search(query)).then((next) => {
2311
+ if (cancelled) return;
2312
+ resultsRef.current = next;
2313
+ setAsyncResults(next);
2314
+ updateSelectedIndex(0);
2315
+ });
2316
+ return () => {
2317
+ cancelled = true;
2318
+ };
2319
+ }, [query, search, updateSelectedIndex]);
2320
+ const commitItem = useCallback(
2321
+ (item) => onSelect(itemToText(item)),
2322
+ [itemToText, onSelect]
2323
+ );
2324
+ const commitSelected = useCallback(() => {
2325
+ const item = resultsRef.current[selectedIndexRef.current];
2326
+ if (item) commitItem(item);
2327
+ }, [commitItem]);
2328
+ const handlePluginKey = useCallback(
2329
+ (key) => {
2330
+ switch (key) {
2331
+ case "Backspace":
2332
+ if (query.length === 0) {
2333
+ onSelect("");
2334
+ } else {
2335
+ setQuery((prev) => prev.slice(0, -1));
2336
+ }
1401
2337
  break;
1402
- case "ArrowUp":
1403
- e.preventDefault();
1404
- setSelectedIndex((prev) => prev > 0 ? prev - 1 : filtered.length - 1);
2338
+ case "ArrowDown": {
2339
+ const length = resultsRef.current.length;
2340
+ if (length === 0) break;
2341
+ updateSelectedIndex(
2342
+ selectedIndexRef.current < length - 1 ? selectedIndexRef.current + 1 : 0
2343
+ );
1405
2344
  break;
2345
+ }
2346
+ case "ArrowUp": {
2347
+ const length = resultsRef.current.length;
2348
+ if (length === 0) break;
2349
+ updateSelectedIndex(
2350
+ selectedIndexRef.current > 0 ? selectedIndexRef.current - 1 : length - 1
2351
+ );
2352
+ break;
2353
+ }
1406
2354
  case "Enter":
1407
- e.preventDefault();
1408
- if (filtered[selectedIndex]) {
1409
- onSelect(filtered[selectedIndex].content);
1410
- }
2355
+ commitSelected();
2356
+ break;
2357
+ default:
2358
+ if (key.length === 1) setQuery((prev) => `${prev}${key}`);
2359
+ break;
2360
+ }
2361
+ },
2362
+ [commitSelected, onSelect, query.length, updateSelectedIndex]
2363
+ );
2364
+ useEffect(
2365
+ () => subscribeForwardedKey(handlePluginKey),
2366
+ [handlePluginKey, subscribeForwardedKey]
2367
+ );
2368
+ const handleKeyDown = useCallback(
2369
+ (event) => {
2370
+ event.stopPropagation();
2371
+ switch (event.key) {
2372
+ case "ArrowDown":
2373
+ case "ArrowUp":
2374
+ case "Enter":
2375
+ case "Backspace":
2376
+ event.preventDefault();
2377
+ handlePluginKey(event.key);
1411
2378
  break;
1412
2379
  case "Escape":
1413
- e.preventDefault();
2380
+ event.preventDefault();
1414
2381
  onDismiss();
1415
2382
  break;
1416
2383
  }
1417
2384
  },
1418
- [filtered, selectedIndex, onSelect, onDismiss]
2385
+ [handlePluginKey, onDismiss]
1419
2386
  );
1420
- return /* @__PURE__ */ jsxs("div", { className: cls2("picker"), children: [
1421
- /* @__PURE__ */ jsx(
1422
- "input",
1423
- {
1424
- ref: focusRef,
1425
- type: "text",
1426
- placeholder: "Search snippets...",
1427
- value: search,
1428
- onChange: (e) => {
1429
- setSearch(e.target.value);
1430
- setSelectedIndex(0);
2387
+ const activeItemRef = useCallback((el) => {
2388
+ if (el && typeof el.scrollIntoView === "function") {
2389
+ el.scrollIntoView({ block: "nearest" });
2390
+ }
2391
+ }, []);
2392
+ const reactId = useId().replace(/:/g, "");
2393
+ const listboxId = `${pluginPickerClass.picker}-${reactId}-listbox`;
2394
+ const activeOptionId = `${listboxId}-option-${selectedIndex}`;
2395
+ const renderedResults = useMemo(
2396
+ () => results.map((item, index) => {
2397
+ const active = index === selectedIndex;
2398
+ return /* @__PURE__ */ jsx(
2399
+ "div",
2400
+ {
2401
+ ref: active ? activeItemRef : void 0,
2402
+ id: `${listboxId}-option-${index}`,
2403
+ role: "option",
2404
+ "aria-selected": active,
2405
+ className: `${pluginPickerClass.item} ${active ? pluginPickerClass.itemActive : ""}`,
2406
+ onMouseDown: (event) => event.preventDefault(),
2407
+ onMouseEnter: () => updateSelectedIndex(index),
2408
+ onClick: () => commitItem(item),
2409
+ children: renderItem(item, active)
1431
2410
  },
1432
- onKeyDown: handleSearchKeyDown,
1433
- className: cls2("search")
1434
- }
1435
- ),
1436
- filtered.length === 0 ? /* @__PURE__ */ jsx("div", { className: cls2("empty"), children: "No snippets found" }) : /* @__PURE__ */ jsx("div", { children: filtered.map((snippet, i) => /* @__PURE__ */ jsxs(
1437
- "div",
1438
- {
1439
- ref: i === selectedIndex ? activeItemRef : void 0,
1440
- "data-snippet-item": true,
1441
- className: `${cls2("item")} ${i === selectedIndex ? cls2("item-active") : ""}`,
1442
- onMouseEnter: () => setSelectedIndex(i),
1443
- onClick: () => onSelect(snippet.content),
1444
- children: [
1445
- /* @__PURE__ */ jsx("div", { className: cls2("title"), children: snippet.title }),
1446
- /* @__PURE__ */ jsx("div", { className: cls2("preview"), children: snippet.content.length > 80 ? `${snippet.content.slice(0, 80)}...` : snippet.content })
1447
- ]
2411
+ getKey(item)
2412
+ );
2413
+ }),
2414
+ [
2415
+ activeItemRef,
2416
+ commitItem,
2417
+ getKey,
2418
+ listboxId,
2419
+ renderItem,
2420
+ results,
2421
+ selectedIndex,
2422
+ updateSelectedIndex
2423
+ ]
2424
+ );
2425
+ return /* @__PURE__ */ jsx(
2426
+ "div",
2427
+ {
2428
+ className: pluginPickerClass.popup,
2429
+ style: {
2430
+ position: "absolute",
2431
+ top: position.top,
2432
+ left: position.left,
2433
+ zIndex: 1001
1448
2434
  },
1449
- snippet.title
1450
- )) })
1451
- ] });
2435
+ onMouseDown: (event) => event.preventDefault(),
2436
+ children: /* @__PURE__ */ jsxs(
2437
+ "div",
2438
+ {
2439
+ className: pluginPickerClass.picker,
2440
+ onKeyDown: handleKeyDown,
2441
+ role: "combobox",
2442
+ "aria-expanded": "true",
2443
+ "aria-haspopup": "listbox",
2444
+ "aria-controls": listboxId,
2445
+ "aria-activedescendant": results.length > 0 ? activeOptionId : void 0,
2446
+ children: [
2447
+ /* @__PURE__ */ jsx("div", { className: pluginPickerClass.search, "aria-label": placeholder, children: query || placeholder }),
2448
+ results.length === 0 ? /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, role: "status", children: emptyMessage }) : /* @__PURE__ */ jsx("div", { id: listboxId, role: "listbox", children: renderedResults })
2449
+ ]
2450
+ }
2451
+ )
2452
+ }
2453
+ );
2454
+ }
2455
+ var defaultEmojis = [
2456
+ { emoji: "\u{1F600}", name: "grinning", shortcodes: ["smile"], tags: ["happy"] },
2457
+ { emoji: "\u{1F604}", name: "smile", shortcodes: ["smiley"], tags: ["happy"] },
2458
+ { emoji: "\u{1F602}", name: "joy", shortcodes: ["laugh"], tags: ["tears"] },
2459
+ { emoji: "\u{1F923}", name: "rofl", shortcodes: ["rolling_on_the_floor_laughing"] },
2460
+ { emoji: "\u{1F60A}", name: "blush", shortcodes: ["smiling"] },
2461
+ { emoji: "\u{1F642}", name: "slight_smile", shortcodes: ["slightly_smiling_face"] },
2462
+ { emoji: "\u{1F609}", name: "wink" },
2463
+ { emoji: "\u{1F60D}", name: "heart_eyes", tags: ["love"] },
2464
+ { emoji: "\u{1F618}", name: "kissing_heart" },
2465
+ { emoji: "\u{1F60E}", name: "sunglasses", shortcodes: ["cool"] },
2466
+ { emoji: "\u{1F914}", name: "thinking", shortcodes: ["thinking_face"] },
2467
+ { emoji: "\u{1F615}", name: "confused", shortcodes: ["slash"] },
2468
+ { emoji: "\u{1F62D}", name: "sob", tags: ["cry"] },
2469
+ { emoji: "\u{1F621}", name: "rage", shortcodes: ["angry"] },
2470
+ { emoji: "\u{1F44D}", name: "thumbsup", shortcodes: ["+1", "like"] },
2471
+ { emoji: "\u{1F44E}", name: "thumbsdown", shortcodes: ["-1", "dislike"] },
2472
+ { emoji: "\u{1F44F}", name: "clap" },
2473
+ { emoji: "\u{1F64C}", name: "raised_hands" },
2474
+ { emoji: "\u{1F64F}", name: "pray", shortcodes: ["thanks"] },
2475
+ { emoji: "\u{1F4AA}", name: "muscle", shortcodes: ["strong"] },
2476
+ { emoji: "\u{1F440}", name: "eyes" },
2477
+ { emoji: "\u{1F4AF}", name: "100" },
2478
+ { emoji: "\u{1F525}", name: "fire" },
2479
+ { emoji: "\u2728", name: "sparkles" },
2480
+ { emoji: "\u{1F389}", name: "tada", shortcodes: ["party"] },
2481
+ { emoji: "\u{1F680}", name: "rocket", tags: ["ship", "launch"] },
2482
+ { emoji: "\u2705", name: "white_check_mark", shortcodes: ["check", "done"] },
2483
+ { emoji: "\u274C", name: "x", shortcodes: ["cross"] },
2484
+ { emoji: "\u26A0\uFE0F", name: "warning" },
2485
+ { emoji: "\u{1F6A8}", name: "rotating_light", shortcodes: ["alert"] },
2486
+ { emoji: "\u{1F41B}", name: "bug" },
2487
+ { emoji: "\u{1F4A1}", name: "bulb", shortcodes: ["idea"] },
2488
+ { emoji: "\u{1F9F5}", name: "yarn" },
2489
+ { emoji: "\u{1F6E0}\uFE0F", name: "tools", shortcodes: ["wrench"] },
2490
+ { emoji: "\u{1F4CC}", name: "pushpin", shortcodes: ["pin"] },
2491
+ { emoji: "\u{1F4DD}", name: "memo", shortcodes: ["note"] },
2492
+ { emoji: "\u{1F4CE}", name: "paperclip", shortcodes: ["attachment"] },
2493
+ { emoji: "\u{1F512}", name: "lock" },
2494
+ { emoji: "\u{1F513}", name: "unlock" },
2495
+ { emoji: "\u{1F4AC}", name: "speech_balloon", shortcodes: ["comment"] },
2496
+ { emoji: "\u2764\uFE0F", name: "heart", shortcodes: ["love"] },
2497
+ { emoji: "\u{1F494}", name: "broken_heart" },
2498
+ { emoji: "\u2601\uFE0F", name: "cloud" },
2499
+ { emoji: "\u{1F682}", name: "train", tags: ["railway"] }
2500
+ ];
2501
+ var itemText = (emoji) => [emoji.name, ...emoji.shortcodes ?? [], ...emoji.tags ?? []].join(" ");
2502
+ var defaultSearch = (emojis, query) => {
2503
+ const q = query.toLowerCase();
2504
+ const slashQuery = q === "/" ? "confused" : q;
2505
+ return emojis.filter(
2506
+ (emoji) => !slashQuery || itemText(emoji).toLowerCase().includes(slashQuery)
2507
+ ).slice(0, 20);
2508
+ };
2509
+ var DefaultEmojiItem = ({ item }) => /* @__PURE__ */ jsxs(Fragment$1, { children: [
2510
+ /* @__PURE__ */ jsx("span", { "aria-hidden": "true", children: item.emoji }),
2511
+ /* @__PURE__ */ jsxs("span", { className: pluginPickerClass.title, children: [
2512
+ ":",
2513
+ item.name,
2514
+ ":"
2515
+ ] }),
2516
+ item.shortcodes?.[0] ? /* @__PURE__ */ jsxs("span", { className: pluginPickerClass.subtitle, children: [
2517
+ ":",
2518
+ item.shortcodes[0],
2519
+ ":"
2520
+ ] }) : null
2521
+ ] });
2522
+ function createEmojiPlugin({
2523
+ name = "emoji",
2524
+ trigger = ":",
2525
+ emojis,
2526
+ search,
2527
+ renderItem,
2528
+ emptyMessage = "No emoji found"
2529
+ } = {}) {
2530
+ const resolvedEmojis = emojis ?? defaultEmojis;
2531
+ return {
2532
+ name,
2533
+ shouldTrigger: (event, { editor }) => {
2534
+ if (event.key !== trigger || event.ctrlKey || event.metaKey || event.altKey) {
2535
+ return false;
2536
+ }
2537
+ const beforeCursor = editor.getContentBeforeCursor();
2538
+ if (beforeCursor === null) return false;
2539
+ const previous = beforeCursor.at(-1) ?? "";
2540
+ return previous === "" || /\s|[([{]/.test(previous);
2541
+ },
2542
+ activation: { type: "trigger", key: trigger },
2543
+ onActiveKeyDown: (event) => {
2544
+ if (event.key.length !== 1) return;
2545
+ if (/[\p{L}\p{N}_+-]/u.test(event.key)) return;
2546
+ return false;
2547
+ },
2548
+ render: (props) => {
2549
+ if (!props.active) return null;
2550
+ return /* @__PURE__ */ jsx(
2551
+ PluginMenuPrimitive,
2552
+ {
2553
+ ...props,
2554
+ pluginName: name,
2555
+ items: search ? void 0 : emojis,
2556
+ search: search ?? ((query) => defaultSearch(resolvedEmojis, query)),
2557
+ getKey: (item) => item.name,
2558
+ renderItem: renderItem ?? ((item, _active) => /* @__PURE__ */ jsx(DefaultEmojiItem, { item })),
2559
+ itemToText: (item) => item.emoji,
2560
+ placeholder: "Search emoji...",
2561
+ emptyMessage
2562
+ }
2563
+ );
2564
+ }
2565
+ };
1452
2566
  }
1453
- function createSnippetsPlugin(options) {
1454
- const { snippets, key = "[" } = options;
2567
+ function createMentionsPlugin(options) {
2568
+ const name = options.name ?? "mentions";
2569
+ const trigger = options.trigger ?? "@";
2570
+ const marker = options.marker ?? "mention";
2571
+ const itemToText = (item) => options.onSelect ? options.onSelect(item) : `@${marker}[${item.id}]`;
1455
2572
  return {
1456
- name: "snippets",
1457
- trigger: { key },
2573
+ name,
2574
+ activation: { type: "trigger", key: trigger },
2575
+ // Dismiss the picker when the user types whitespace or punctuation —
2576
+ // matches the emoji plugin so `@john<space>` flows back into the
2577
+ // document instead of growing the query indefinitely.
2578
+ onActiveKeyDown: (event) => {
2579
+ if (event.key.length !== 1) return;
2580
+ if (/[\p{L}\p{N}_-]/u.test(event.key)) return;
2581
+ return false;
2582
+ },
1458
2583
  render: (props) => /* @__PURE__ */ jsx(
2584
+ PluginMenuPrimitive,
2585
+ {
2586
+ pluginName: name,
2587
+ placeholder: "Search...",
2588
+ search: options.search,
2589
+ getKey: (item) => item.id,
2590
+ renderItem: options.renderItem,
2591
+ itemToText,
2592
+ emptyMessage: options.emptyMessage ?? "No results",
2593
+ ...props
2594
+ }
2595
+ )
2596
+ };
2597
+ }
2598
+ var fuzzyMatch = (query, text) => {
2599
+ const q = query.toLowerCase();
2600
+ const t = text.toLowerCase();
2601
+ return t.includes(q) || t.startsWith(q);
2602
+ };
2603
+ var SlashCommandMenuInner = forwardRef(function SlashCommandMenuInner2({
2604
+ commands,
2605
+ emptyMessage,
2606
+ onReadyChange,
2607
+ onExecute,
2608
+ onDismiss,
2609
+ position,
2610
+ getEditor
2611
+ }, ref) {
2612
+ const [mode, setMode] = useState("commands");
2613
+ const [query, setQuery] = useState("");
2614
+ const [selectedIndex, setSelectedIndex] = useState(0);
2615
+ const [selectedCommand, setSelectedCommand] = useState(null);
2616
+ const [selectedArg, setSelectedArg] = useState(null);
2617
+ const [argChoices, setArgChoices] = useState([]);
2618
+ const [loadingArgs, setLoadingArgs] = useState(false);
2619
+ useEffect(() => {
2620
+ onReadyChange?.(mode === "ready");
2621
+ }, [mode, onReadyChange]);
2622
+ useEffect(() => {
2623
+ let cancelled = false;
2624
+ const loadChoices = async () => {
2625
+ if (mode !== "args" || !selectedCommand) {
2626
+ setArgChoices([]);
2627
+ return;
2628
+ }
2629
+ const firstArg = selectedCommand.arg;
2630
+ if (!firstArg) {
2631
+ setArgChoices([]);
2632
+ return;
2633
+ }
2634
+ if (firstArg.choices) {
2635
+ setArgChoices(firstArg.choices);
2636
+ return;
2637
+ }
2638
+ if (!firstArg.fetchChoices) {
2639
+ setArgChoices([]);
2640
+ return;
2641
+ }
2642
+ setLoadingArgs(true);
2643
+ try {
2644
+ const choices = await firstArg.fetchChoices();
2645
+ if (!cancelled) setArgChoices(choices);
2646
+ } finally {
2647
+ if (!cancelled) setLoadingArgs(false);
2648
+ }
2649
+ };
2650
+ void loadChoices();
2651
+ return () => {
2652
+ cancelled = true;
2653
+ };
2654
+ }, [mode, selectedCommand]);
2655
+ const commandItems = useMemo(
2656
+ () => commands.filter((command) => {
2657
+ if (!query) return true;
2658
+ return fuzzyMatch(query, command.name) || command.aliases?.some((alias) => fuzzyMatch(query, alias));
2659
+ }).map((command) => {
2660
+ const disabledReason = command.disabled?.();
2661
+ return {
2662
+ type: "command",
2663
+ value: command.name,
2664
+ label: `/${command.name}`,
2665
+ description: command.description,
2666
+ disabled: !!disabledReason,
2667
+ disabledReason: disabledReason || void 0,
2668
+ command
2669
+ };
2670
+ }),
2671
+ [commands, query]
2672
+ );
2673
+ const argItems = useMemo(
2674
+ () => argChoices.filter((choice) => !query || fuzzyMatch(query, choice.label)).map((choice) => ({
2675
+ type: "arg",
2676
+ value: choice.value,
2677
+ label: choice.label,
2678
+ description: choice.disabled ? "(current)" : selectedCommand?.arg?.description ?? "",
2679
+ disabled: choice.disabled
2680
+ })),
2681
+ [argChoices, query, selectedCommand]
2682
+ );
2683
+ const items = mode === "args" ? argItems : commandItems;
2684
+ const reactId = useId().replace(/:/g, "");
2685
+ const listboxId = `${pluginPickerClass.picker}-${reactId}-slash-listbox`;
2686
+ const activeOptionId = `${listboxId}-option-${selectedIndex}`;
2687
+ useEffect(() => {
2688
+ const firstEnabled = items.findIndex((item) => !item.disabled);
2689
+ setSelectedIndex(firstEnabled >= 0 ? firstEnabled : 0);
2690
+ }, [items.length, mode, query]);
2691
+ const writeSlashLine = useCallback(
2692
+ (line) => {
2693
+ const editor = getEditor();
2694
+ if (!editor) return;
2695
+ editor.replaceCurrentBlockContent(line);
2696
+ },
2697
+ [getEditor]
2698
+ );
2699
+ const handleSelect = useCallback(
2700
+ (item) => {
2701
+ if (item.disabled) return;
2702
+ if (item.type === "command") {
2703
+ const command = item.command;
2704
+ if (!command) return;
2705
+ const hasArg = !!command.arg;
2706
+ const nextLine = `/${command.name}${hasArg ? " " : ""}`;
2707
+ writeSlashLine(nextLine);
2708
+ setSelectedCommand(command);
2709
+ setSelectedArg(null);
2710
+ setQuery("");
2711
+ setMode(hasArg ? "args" : "ready");
2712
+ return;
2713
+ }
2714
+ if (!selectedCommand) return;
2715
+ const firstArg = selectedCommand.arg;
2716
+ writeSlashLine(`/${selectedCommand.name} ${item.label}`);
2717
+ setSelectedArg(
2718
+ firstArg ? { name: firstArg.name, value: item.value } : null
2719
+ );
2720
+ setQuery("");
2721
+ setMode("ready");
2722
+ },
2723
+ [selectedCommand, writeSlashLine]
2724
+ );
2725
+ useImperativeHandle(
2726
+ ref,
2727
+ () => ({
2728
+ isReady: () => mode === "ready",
2729
+ appendQuery: (value) => setQuery((current) => `${current}${value}`),
2730
+ removeQueryChar: () => setQuery((current) => current.length > 0 ? current.slice(0, -1) : ""),
2731
+ move: (direction) => setSelectedIndex((current) => {
2732
+ if (items.length === 0) return current;
2733
+ for (let step = 1; step <= items.length; step++) {
2734
+ const next = (current + direction * step + items.length) % items.length;
2735
+ if (!items[next]?.disabled) return next;
2736
+ }
2737
+ return current;
2738
+ }),
2739
+ selectActive: () => {
2740
+ const item = items[selectedIndex];
2741
+ if (item) handleSelect(item);
2742
+ },
2743
+ execute: () => {
2744
+ if (!selectedCommand) return;
2745
+ const editor = getEditor();
2746
+ const raw = editor?.getCurrentBlockContent() ?? `/${selectedCommand.name}`;
2747
+ onExecute?.({
2748
+ name: selectedCommand.name,
2749
+ args: selectedArg ? { [selectedArg.name]: selectedArg.value } : {},
2750
+ raw
2751
+ });
2752
+ },
2753
+ reset: () => {
2754
+ setMode("commands");
2755
+ setQuery("");
2756
+ setSelectedCommand(null);
2757
+ setSelectedArg(null);
2758
+ setSelectedIndex(0);
2759
+ setArgChoices([]);
2760
+ }
2761
+ }),
2762
+ [
2763
+ getEditor,
2764
+ handleSelect,
2765
+ items,
2766
+ mode,
2767
+ onExecute,
2768
+ selectedArg,
2769
+ selectedCommand,
2770
+ selectedIndex
2771
+ ]
2772
+ );
2773
+ if (mode === "ready" && selectedCommand) {
2774
+ return /* @__PURE__ */ jsx(
1459
2775
  "div",
1460
2776
  {
1461
- className: cls2("popup"),
2777
+ className: pluginPickerClass.popup,
1462
2778
  style: {
1463
2779
  position: "absolute",
1464
- top: props.position.top,
1465
- left: props.position.left,
2780
+ top: position.top,
2781
+ left: position.left,
1466
2782
  zIndex: 1001
1467
2783
  },
1468
- onMouseDown: (e) => e.preventDefault(),
1469
- children: /* @__PURE__ */ jsx(SnippetPicker, { snippets, ...props })
2784
+ children: /* @__PURE__ */ jsx("div", { className: pluginPickerClass.picker, children: /* @__PURE__ */ jsx("div", { className: "inkwell-plugin-slash-commands-execute", children: "Enter to execute \xB7 Esc to cancel" }) })
2785
+ }
2786
+ );
2787
+ }
2788
+ return /* @__PURE__ */ jsx(
2789
+ "div",
2790
+ {
2791
+ className: pluginPickerClass.popup,
2792
+ style: {
2793
+ position: "absolute",
2794
+ top: position.top,
2795
+ left: position.left,
2796
+ zIndex: 1001
2797
+ },
2798
+ children: /* @__PURE__ */ jsxs("div", { className: pluginPickerClass.picker, children: [
2799
+ /* @__PURE__ */ jsx("div", { className: pluginPickerClass.search, children: mode === "commands" ? `/${query}` : `${selectedCommand ? `/${selectedCommand.name} ` : ""}${query}` }),
2800
+ loadingArgs && items.length === 0 ? /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, children: "Loading..." }) : items.length === 0 ? /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, children: emptyMessage }) : /* @__PURE__ */ jsx(
2801
+ "div",
2802
+ {
2803
+ id: listboxId,
2804
+ role: "listbox",
2805
+ "aria-activedescendant": activeOptionId,
2806
+ children: items.map((item, index) => {
2807
+ const active = index === selectedIndex;
2808
+ return /* @__PURE__ */ jsxs(
2809
+ "div",
2810
+ {
2811
+ id: `${listboxId}-option-${index}`,
2812
+ role: "option",
2813
+ "aria-selected": active,
2814
+ className: `${pluginPickerClass.item} ${active ? pluginPickerClass.itemActive : ""}`,
2815
+ onMouseDown: (event) => event.preventDefault(),
2816
+ onMouseEnter: () => {
2817
+ if (!item.disabled) setSelectedIndex(index);
2818
+ },
2819
+ onClick: () => handleSelect(item),
2820
+ "aria-disabled": item.disabled,
2821
+ children: [
2822
+ /* @__PURE__ */ jsx("span", { className: pluginPickerClass.title, children: item.label }),
2823
+ /* @__PURE__ */ jsx("span", { className: pluginPickerClass.subtitle, children: item.disabledReason ?? item.description })
2824
+ ]
2825
+ },
2826
+ `${item.type}-${item.value}`
2827
+ );
2828
+ })
2829
+ }
2830
+ ),
2831
+ /* @__PURE__ */ jsx("div", { className: pluginPickerClass.empty, children: "\u2191\u2193 navigate \xB7 Tab/Enter select \xB7 Esc close" })
2832
+ ] })
2833
+ }
2834
+ );
2835
+ });
2836
+ var createSlashCommandsPlugin = ({
2837
+ name = "slash-commands",
2838
+ commands,
2839
+ onReadyChange,
2840
+ onExecute,
2841
+ emptyMessage = "No commands found"
2842
+ }) => {
2843
+ let editorRef = null;
2844
+ const menuRef = { current: null };
2845
+ const enterReadyOrExecuteCleanup = (editor, ctx, action) => {
2846
+ action();
2847
+ ctx.dismiss();
2848
+ requestAnimationFrame(() => editor.clearCurrentBlock());
2849
+ };
2850
+ return {
2851
+ name,
2852
+ activation: { type: "manual" },
2853
+ render: (props) => {
2854
+ if (!props.active) return null;
2855
+ return /* @__PURE__ */ jsx(
2856
+ SlashCommandMenuInner,
2857
+ {
2858
+ ...props,
2859
+ ref: menuRef,
2860
+ commands,
2861
+ emptyMessage,
2862
+ onReadyChange,
2863
+ onExecute,
2864
+ getEditor: () => editorRef
2865
+ }
2866
+ );
2867
+ },
2868
+ onKeyDown: (event, ctx) => {
2869
+ editorRef = ctx.editor;
2870
+ if (event.key === "/" && !event.metaKey && !event.ctrlKey && !event.altKey) {
2871
+ const beforeCursor = ctx.editor.getCurrentBlockContentBeforeCursor();
2872
+ const blockText = ctx.editor.getCurrentBlockContent();
2873
+ if (beforeCursor !== null && beforeCursor.trim() === "" && blockText !== null && blockText.trim() === "") {
2874
+ event.preventDefault();
2875
+ ctx.editor.insertContent("/");
2876
+ ctx.activate();
2877
+ menuRef.current?.reset();
2878
+ }
2879
+ }
2880
+ },
2881
+ onActiveKeyDown: (event, ctx) => {
2882
+ editorRef = ctx.editor;
2883
+ const menu = menuRef.current;
2884
+ if (!menu) return;
2885
+ if (event.key === "Escape") {
2886
+ event.preventDefault();
2887
+ if (menu.isReady()) {
2888
+ enterReadyOrExecuteCleanup(ctx.editor, ctx, () => {
2889
+ });
2890
+ } else {
2891
+ ctx.dismiss();
2892
+ menu.reset();
2893
+ }
2894
+ return;
2895
+ }
2896
+ if (menu.isReady() && event.key === "Enter") {
2897
+ event.preventDefault();
2898
+ enterReadyOrExecuteCleanup(ctx.editor, ctx, () => {
2899
+ menu.execute();
2900
+ });
2901
+ return;
2902
+ }
2903
+ if (event.key === "ArrowDown") {
2904
+ event.preventDefault();
2905
+ menu.move(1);
2906
+ return;
2907
+ }
2908
+ if (event.key === "ArrowUp") {
2909
+ event.preventDefault();
2910
+ menu.move(-1);
2911
+ return;
2912
+ }
2913
+ if (event.key === "Tab" || event.key === "Enter") {
2914
+ event.preventDefault();
2915
+ menu.selectActive();
2916
+ return;
2917
+ }
2918
+ if (event.key === "Backspace") {
2919
+ const beforeCursor = ctx.editor.getCurrentBlockContentBeforeCursor();
2920
+ if (beforeCursor === "/") {
2921
+ ctx.dismiss();
2922
+ menu.reset();
2923
+ return;
2924
+ }
2925
+ menu.removeQueryChar();
2926
+ return;
2927
+ }
2928
+ if (!event.metaKey && !event.ctrlKey && !event.altKey && event.key.length === 1) {
2929
+ menu.appendQuery(event.key);
2930
+ }
2931
+ }
2932
+ };
2933
+ };
2934
+ function renderSnippet(snippet) {
2935
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [
2936
+ /* @__PURE__ */ jsx("div", { className: pluginPickerClass.title, children: snippet.title }),
2937
+ /* @__PURE__ */ jsx("div", { className: pluginPickerClass.preview, children: snippet.content.length > 80 ? `${snippet.content.slice(0, 80)}...` : snippet.content })
2938
+ ] });
2939
+ }
2940
+ function createSnippetsPlugin({
2941
+ snippets,
2942
+ name = "snippets",
2943
+ trigger = "["
2944
+ }) {
2945
+ return {
2946
+ name,
2947
+ activation: { type: "trigger", key: trigger },
2948
+ render: (props) => /* @__PURE__ */ jsx(
2949
+ PluginMenuPrimitive,
2950
+ {
2951
+ pluginName: name,
2952
+ items: snippets,
2953
+ getKey: (snippet) => snippet.title,
2954
+ renderItem: renderSnippet,
2955
+ itemToText: (snippet) => snippet.content,
2956
+ placeholder: "Search snippets...",
2957
+ emptyMessage: "No snippets found",
2958
+ ...props
1470
2959
  }
1471
2960
  )
1472
2961
  };
@@ -1485,7 +2974,7 @@ var processor = unified().use(rehypeParse, { fragment: true }).use(rehypeRemark)
1485
2974
  }
1486
2975
  }
1487
2976
  });
1488
- function serializeToMarkdown(html) {
2977
+ function htmlToMarkdown(html) {
1489
2978
  return String(processor.processSync(html)).trim();
1490
2979
  }
1491
2980
  function CopyCodeBlock({
@@ -1546,14 +3035,76 @@ function CopyCodeBlock({
1546
3035
  /* @__PURE__ */ jsx("pre", { ref: preRef, ...props, children })
1547
3036
  ] });
1548
3037
  }
1549
- function createProcessor(options = {}) {
3038
+ var MENTION_TAG_PREFIX = "inkwell-mention-";
3039
+ function rehypeMentions(mentions) {
3040
+ return () => (tree) => {
3041
+ if (mentions.length === 0) return;
3042
+ visit(tree, "text", (node, index, parent) => {
3043
+ if (typeof node.value !== "string" || !parent || index == null) return;
3044
+ const text = node.value;
3045
+ const hits = [];
3046
+ for (let i = 0; i < mentions.length; i++) {
3047
+ const re = new RegExp(
3048
+ mentions[i].pattern.source,
3049
+ mentions[i].pattern.flags.includes("g") ? mentions[i].pattern.flags : `${mentions[i].pattern.flags}g`
3050
+ );
3051
+ let m;
3052
+ while ((m = re.exec(text)) !== null) {
3053
+ hits.push({
3054
+ start: m.index,
3055
+ end: m.index + m[0].length,
3056
+ mentionIdx: i,
3057
+ matchText: m[0]
3058
+ });
3059
+ if (m[0].length === 0) re.lastIndex++;
3060
+ }
3061
+ }
3062
+ if (hits.length === 0) return;
3063
+ hits.sort((a, b) => a.start - b.start || a.mentionIdx - b.mentionIdx);
3064
+ const nonOverlapping = [];
3065
+ let cursor = 0;
3066
+ for (const hit of hits) {
3067
+ if (hit.start < cursor) continue;
3068
+ nonOverlapping.push(hit);
3069
+ cursor = hit.end;
3070
+ }
3071
+ const replacements = [];
3072
+ let offset = 0;
3073
+ for (const hit of nonOverlapping) {
3074
+ if (hit.start > offset) {
3075
+ replacements.push({
3076
+ type: "text",
3077
+ value: text.slice(offset, hit.start)
3078
+ });
3079
+ }
3080
+ replacements.push({
3081
+ type: "element",
3082
+ tagName: `${MENTION_TAG_PREFIX}${hit.mentionIdx}`,
3083
+ properties: { "data-match": hit.matchText },
3084
+ children: []
3085
+ });
3086
+ offset = hit.end;
3087
+ }
3088
+ if (offset < text.length) {
3089
+ replacements.push({
3090
+ type: "text",
3091
+ value: text.slice(offset)
3092
+ });
3093
+ }
3094
+ parent.children.splice(index, 1, ...replacements);
3095
+ return [SKIP, index + replacements.length];
3096
+ });
3097
+ };
3098
+ }
3099
+ function createProcessor2(options = {}) {
1550
3100
  const proc = unified().use(remarkParse).use(remarkGfm).use(remarkNoTables).use(remarkFlattenBlockquotes).use(remarkRehype);
1551
3101
  const plugins = options.rehypePlugins ?? [
1552
3102
  [rehypeHighlight, { detect: true }]
1553
3103
  ];
1554
3104
  for (const plugin of plugins) {
1555
3105
  if (Array.isArray(plugin)) {
1556
- proc.use(plugin[0], plugin[1]);
3106
+ const [rehypePlugin, ...options2] = plugin;
3107
+ proc.use(rehypePlugin, ...options2);
1557
3108
  } else {
1558
3109
  proc.use(plugin);
1559
3110
  }
@@ -1567,21 +3118,37 @@ function createProcessor(options = {}) {
1567
3118
  span: ["className"]
1568
3119
  }
1569
3120
  });
3121
+ const mentionConfigs = options.mentions ?? [];
3122
+ if (mentionConfigs.length > 0) {
3123
+ proc.use(rehypeMentions(mentionConfigs));
3124
+ }
3125
+ const mentionComponents = {};
3126
+ mentionConfigs.forEach((cfg, i) => {
3127
+ const tag = `${MENTION_TAG_PREFIX}${i}`;
3128
+ mentionComponents[tag] = (props) => {
3129
+ const matchText = props["data-match"] ?? "";
3130
+ const exec = new RegExp(cfg.pattern.source, cfg.pattern.flags).exec(
3131
+ matchText
3132
+ );
3133
+ if (!exec) return matchText;
3134
+ return cfg.resolve(exec);
3135
+ };
3136
+ });
1570
3137
  proc.use(rehypeReact, {
1571
3138
  createElement,
1572
3139
  Fragment: Fragment,
1573
3140
  jsx: jsx,
1574
3141
  jsxs: jsxs,
1575
- components: options.components ?? {}
3142
+ components: { ...mentionComponents, ...options.components ?? {} }
1576
3143
  });
1577
3144
  return proc;
1578
3145
  }
1579
3146
  function escapeBareBq2(markdown) {
1580
3147
  return markdown.replace(/^>(?=\S)/gm, "\\>");
1581
3148
  }
1582
- function parseMarkdown(markdown, components, rehypePlugins) {
1583
- const processor2 = createProcessor({ components, rehypePlugins });
1584
- const file = processor2.processSync(escapeBareBq2(markdown));
3149
+ function parseMarkdown(content, options = {}) {
3150
+ const processor2 = createProcessor2(options);
3151
+ const file = processor2.processSync(escapeBareBq2(content));
1585
3152
  return file.result;
1586
3153
  }
1587
3154
  function InkwellRenderer({
@@ -1589,17 +3156,21 @@ function InkwellRenderer({
1589
3156
  className,
1590
3157
  components,
1591
3158
  rehypePlugins,
1592
- copyButton = true
3159
+ mentions
1593
3160
  }) {
1594
- const mergedComponents = useMemo(() => {
1595
- if (!copyButton) return components;
1596
- return { pre: CopyCodeBlock, ...components };
1597
- }, [copyButton, components]);
3161
+ const mergedComponents = useMemo(
3162
+ () => ({ pre: CopyCodeBlock, ...components }),
3163
+ [components]
3164
+ );
1598
3165
  const rendered = useMemo(
1599
- () => parseMarkdown(content, mergedComponents, rehypePlugins),
1600
- [content, mergedComponents, rehypePlugins]
3166
+ () => parseMarkdown(content, {
3167
+ components: mergedComponents,
3168
+ rehypePlugins,
3169
+ mentions
3170
+ }),
3171
+ [content, mergedComponents, rehypePlugins, mentions]
1601
3172
  );
1602
3173
  return /* @__PURE__ */ jsx("div", { className: `inkwell-renderer ${className ?? ""}`, children: rendered });
1603
3174
  }
1604
3175
 
1605
- export { InkwellEditor, InkwellRenderer, createBubbleMenuPlugin, createSnippetsPlugin, defaultBubbleMenuItems, deserialize, parseMarkdown, pluginClass, serializeToMarkdown };
3176
+ export { InkwellEditor, InkwellRenderer, createAttachmentsPlugin, createBubbleMenuPlugin, createCompletionsPlugin, createEmojiPlugin, createMentionsPlugin, createSlashCommandsPlugin, createSnippetsPlugin, defaultBubbleMenuItems, defaultEmojis, htmlToMarkdown, parseMarkdown };