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