@moldable-ai/editor 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/components/markdown-editor.d.ts +12 -3
  2. package/dist/components/markdown-editor.d.ts.map +1 -1
  3. package/dist/components/markdown-editor.js +23 -9
  4. package/dist/index.d.ts +6 -2
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +6 -2
  7. package/dist/lib/lexical/editor-theme.d.ts +2 -0
  8. package/dist/lib/lexical/editor-theme.d.ts.map +1 -1
  9. package/dist/lib/lexical/editor-theme.js +2 -0
  10. package/dist/lib/lexical/file-reference-plugin.d.ts +9 -0
  11. package/dist/lib/lexical/file-reference-plugin.d.ts.map +1 -0
  12. package/dist/lib/lexical/file-reference-plugin.js +99 -0
  13. package/dist/lib/lexical/find-replace-plugin.d.ts +31 -0
  14. package/dist/lib/lexical/find-replace-plugin.d.ts.map +1 -0
  15. package/dist/lib/lexical/find-replace-plugin.js +205 -0
  16. package/dist/lib/lexical/headless-editor.d.ts.map +1 -1
  17. package/dist/lib/lexical/headless-editor.js +6 -0
  18. package/dist/lib/lexical/image-component.d.ts +8 -0
  19. package/dist/lib/lexical/image-component.d.ts.map +1 -0
  20. package/dist/lib/lexical/image-component.js +99 -0
  21. package/dist/lib/lexical/image-node.d.ts +36 -0
  22. package/dist/lib/lexical/image-node.d.ts.map +1 -0
  23. package/dist/lib/lexical/image-node.js +105 -0
  24. package/dist/lib/lexical/markdown-transformers.d.ts +4 -1
  25. package/dist/lib/lexical/markdown-transformers.d.ts.map +1 -1
  26. package/dist/lib/lexical/markdown-transformers.js +78 -0
  27. package/dist/lib/lexical/media-context.d.ts +7 -0
  28. package/dist/lib/lexical/media-context.d.ts.map +1 -0
  29. package/dist/lib/lexical/media-context.js +7 -0
  30. package/dist/lib/lexical/media-node.d.ts +32 -0
  31. package/dist/lib/lexical/media-node.d.ts.map +1 -0
  32. package/dist/lib/lexical/media-node.js +97 -0
  33. package/dist/lib/lexical/media-plugin.d.ts +12 -0
  34. package/dist/lib/lexical/media-plugin.d.ts.map +1 -0
  35. package/dist/lib/lexical/media-plugin.js +139 -0
  36. package/dist/lib/lexical/sync-plugin.d.ts +1 -0
  37. package/dist/lib/lexical/sync-plugin.d.ts.map +1 -1
  38. package/dist/lib/lexical/sync-plugin.js +13 -5
  39. package/package.json +19 -18
  40. package/src/styles/index.css +130 -0
  41. package/dist/lib/lexical/translation.test.d.ts +0 -2
  42. package/dist/lib/lexical/translation.test.d.ts.map +0 -1
  43. package/dist/lib/lexical/translation.test.js +0 -329
@@ -1,4 +1,7 @@
1
- interface MarkdownEditorProps {
1
+ import { type MarkdownEditorHandle } from '../lib/lexical/find-replace-plugin';
2
+ import { type MarkdownReferenceOption } from '../lib/lexical/file-reference-plugin';
3
+ import { type MarkdownMediaUpload } from '../lib/lexical/media-plugin';
4
+ export interface MarkdownEditorProps {
2
5
  value: string;
3
6
  onChange: (value: string) => void;
4
7
  placeholder?: string;
@@ -9,7 +12,13 @@ interface MarkdownEditorProps {
9
12
  minHeight?: string;
10
13
  maxHeight?: string;
11
14
  hideMarkdownHint?: boolean;
15
+ findQuery?: string;
16
+ currentFindMatch?: number;
17
+ onFindMatchCountChange?: (count: number) => void;
18
+ onMediaUpload?: MarkdownMediaUpload;
19
+ resolveMediaUrl?: (src: string) => string;
20
+ referenceOptions?: MarkdownReferenceOption[];
12
21
  }
13
- export declare function MarkdownEditor({ value, onChange, placeholder, disabled, autoFocus, className, contentClassName, minHeight, maxHeight, hideMarkdownHint, }: MarkdownEditorProps): import("react/jsx-runtime").JSX.Element;
14
- export {};
22
+ export declare const MarkdownEditor: import("react").ForwardRefExoticComponent<MarkdownEditorProps & import("react").RefAttributes<MarkdownEditorHandle>>;
23
+ export type { MarkdownEditorHandle };
15
24
  //# sourceMappingURL=markdown-editor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"markdown-editor.d.ts","sourceRoot":"","sources":["../../src/components/markdown-editor.tsx"],"names":[],"mappings":"AAsCA,UAAU,mBAAmB;IAC3B,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAC3B;AAED,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,QAAQ,EACR,WAAkC,EAClC,QAAgB,EAChB,SAAiB,EACjB,SAAS,EACT,gBAAgB,EAChB,SAAmB,EACnB,SAAkB,EAClB,gBAAwB,GACzB,EAAE,mBAAmB,2CAsGrB"}
1
+ {"version":3,"file":"markdown-editor.d.ts","sourceRoot":"","sources":["../../src/components/markdown-editor.tsx"],"names":[],"mappings":"AAoBA,OAAO,EAGL,KAAK,oBAAoB,EAC1B,MAAM,oCAAoC,CAAA;AAkB3C,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,sCAAsC,CAAA;AAG7C,OAAO,EAEL,KAAK,mBAAmB,EACzB,MAAM,6BAA6B,CAAA;AAOpC,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAA;IACb,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IACjC,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,OAAO,CAAA;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAA;IACzB,sBAAsB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAChD,aAAa,CAAC,EAAE,mBAAmB,CAAA;IACnC,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAA;IACzC,gBAAgB,CAAC,EAAE,uBAAuB,EAAE,CAAA;CAC7C;AAED,eAAO,MAAM,cAAc,sHAmJzB,CAAA;AAEF,YAAY,EAAE,oBAAoB,EAAE,CAAA"}
@@ -13,21 +13,28 @@ import { MarkdownShortcutPlugin } from '@lexical/react/LexicalMarkdownShortcutPl
13
13
  import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
14
14
  import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
15
15
  import { TabIndentationPlugin } from '@lexical/react/LexicalTabIndentationPlugin';
16
- import { useCallback, useRef } from 'react';
16
+ import { forwardRef, useCallback, useRef } from 'react';
17
17
  import { cn } from '@moldable-ai/ui';
18
18
  import { LINK_MATCHERS } from '../lib/lexical/auto-link-config';
19
19
  import { ClickableLinkPlugin } from '../lib/lexical/clickable-link-plugin';
20
20
  import { editorTheme } from '../lib/lexical/editor-theme';
21
+ import { FIND_REPLACE_UPDATE_TAG, FindReplacePlugin, } from '../lib/lexical/find-replace-plugin';
21
22
  import { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, } from '../lib/lexical/markdown-transformers';
22
- import { SyncPlugin } from '../lib/lexical/sync-plugin';
23
+ import { EXTERNAL_MARKDOWN_SYNC_TAG, SyncPlugin, } from '../lib/lexical/sync-plugin';
23
24
  import { FloatingToolbar } from './floating-toolbar';
24
25
  import { CodeHighlightNode, CodeNode } from '@lexical/code';
25
26
  import { HorizontalRuleNode } from '@lexical/extension';
26
27
  import { AutoLinkNode, LinkNode } from '@lexical/link';
27
28
  import { ListItemNode, ListNode } from '@lexical/list';
29
+ import { MarkNode } from '@lexical/mark';
28
30
  import { HeadingNode, QuoteNode } from '@lexical/rich-text';
29
31
  import { $createParagraphNode, $getRoot } from 'lexical';
30
- export function MarkdownEditor({ value, onChange, placeholder = 'Write something...', disabled = false, autoFocus = false, className, contentClassName, minHeight = '150px', maxHeight = '50vh', hideMarkdownHint = false, }) {
32
+ import { FileReferencePlugin, } from '../lib/lexical/file-reference-plugin';
33
+ import { ImageNode } from '../lib/lexical/image-node';
34
+ import { MediaNode } from '../lib/lexical/media-node';
35
+ import { MediaPasteDropPlugin, } from '../lib/lexical/media-plugin';
36
+ import { MediaProvider } from '../lib/lexical/media-context';
37
+ export const MarkdownEditor = forwardRef(function MarkdownEditor({ value, onChange, placeholder = 'Write something...', disabled = false, autoFocus = false, className, contentClassName, minHeight = '150px', maxHeight = '50vh', hideMarkdownHint = false, findQuery = '', currentFindMatch = 0, onFindMatchCountChange, onMediaUpload, resolveMediaUrl, referenceOptions, }, ref) {
31
38
  const initialValueRef = useRef(value);
32
39
  const initialConfig = {
33
40
  namespace: 'MarkdownEditor',
@@ -38,8 +45,9 @@ export function MarkdownEditor({ value, onChange, placeholder = 'Write something
38
45
  markdown: value,
39
46
  transformers: markdownTransformers,
40
47
  node: root,
41
- shouldPreserveNewLines: true,
48
+ shouldPreserveNewLines: false,
42
49
  });
50
+ root.selectEnd();
43
51
  }
44
52
  else {
45
53
  root.append($createParagraphNode());
@@ -54,7 +62,10 @@ export function MarkdownEditor({ value, onChange, placeholder = 'Write something
54
62
  CodeHighlightNode,
55
63
  AutoLinkNode,
56
64
  LinkNode,
65
+ MarkNode,
57
66
  HorizontalRuleNode,
67
+ ImageNode,
68
+ MediaNode,
58
69
  ],
59
70
  onError: (error) => {
60
71
  console.error('MarkdownEditor error:', error);
@@ -62,12 +73,15 @@ export function MarkdownEditor({ value, onChange, placeholder = 'Write something
62
73
  theme: editorTheme,
63
74
  editable: !disabled,
64
75
  };
65
- const handleChange = useCallback((editorState) => {
76
+ const handleChange = useCallback((editorState, _editor, tags) => {
77
+ if (tags.has(FIND_REPLACE_UPDATE_TAG))
78
+ return;
79
+ if (tags.has(EXTERNAL_MARKDOWN_SYNC_TAG))
80
+ return;
66
81
  editorState.read(() => {
67
- // Use shouldPreserveNewLines: true to preserve empty paragraphs as extra newlines
68
82
  const markdown = $convertToMarkdownString({
69
83
  transformers: markdownTransformers,
70
- shouldPreserveNewLines: true,
84
+ shouldPreserveNewLines: false,
71
85
  });
72
86
  // Only call onChange if the value actually changed
73
87
  if (markdown !== initialValueRef.current) {
@@ -76,5 +90,5 @@ export function MarkdownEditor({ value, onChange, placeholder = 'Write something
76
90
  }
77
91
  });
78
92
  }, [onChange]);
79
- return (_jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsxs("div", { className: cn('relative h-full', className), "data-lexical-editor-container": true, children: [_jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: cn('prose prose-sm caret-primary dark:prose-invert max-w-none resize-none overflow-auto bg-transparent outline-none', disabled && 'cursor-not-allowed opacity-50', contentClassName), style: { minHeight, maxHeight }, "aria-placeholder": placeholder, "data-lexical-editor": "true", placeholder: _jsx("div", { className: "text-muted-foreground pointer-events-none absolute left-0 top-1 text-sm opacity-75", children: placeholder }) }), ErrorBoundary: LexicalErrorBoundary }), _jsx(HistoryPlugin, {}), autoFocus && _jsx(AutoFocusPlugin, {}), _jsx(ListPlugin, {}), _jsx(CheckListPlugin, {}), _jsx(TabIndentationPlugin, {}), _jsx(LinkPlugin, {}), _jsx(AutoLinkPlugin, { matchers: LINK_MATCHERS }), _jsx(ClickableLinkPlugin, {}), _jsx(FloatingToolbar, {}), _jsx(MarkdownShortcutPlugin, { transformers: markdownTransformers }), _jsx(OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true }), _jsx(SyncPlugin, { value: value, initialValueRef: initialValueRef }), !hideMarkdownHint && (_jsx("div", { className: "mt-1 flex justify-end", children: _jsx("span", { className: "text-muted-foreground/60 text-xs", children: "Markdown supported" }) }))] }) }));
80
- }
93
+ return (_jsx(LexicalComposer, { initialConfig: initialConfig, children: _jsx(MediaProvider, { value: { resolveMediaUrl }, children: _jsxs("div", { className: cn('relative h-full', className), "data-lexical-editor-container": true, children: [_jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: cn('prose prose-sm caret-primary dark:prose-invert max-w-none resize-none overflow-auto bg-transparent outline-none', disabled && 'cursor-not-allowed opacity-50', contentClassName), style: { minHeight, maxHeight }, "aria-placeholder": placeholder, "data-lexical-editor": "true", placeholder: _jsx("div", { className: cn('pointer-events-none absolute inset-x-0 top-1 overflow-hidden whitespace-nowrap', contentClassName, 'text-muted-foreground text-sm opacity-75'), children: placeholder }) }), ErrorBoundary: LexicalErrorBoundary }), _jsx(HistoryPlugin, {}), autoFocus && _jsx(AutoFocusPlugin, {}), _jsx(ListPlugin, {}), _jsx(CheckListPlugin, {}), _jsx(TabIndentationPlugin, {}), _jsx(LinkPlugin, {}), _jsx(AutoLinkPlugin, { matchers: LINK_MATCHERS }), _jsx(ClickableLinkPlugin, {}), _jsx(FloatingToolbar, {}), _jsx(MarkdownShortcutPlugin, { transformers: markdownTransformers }), _jsx(FileReferencePlugin, { references: referenceOptions }), _jsx(MediaPasteDropPlugin, { onMediaUpload: onMediaUpload }), _jsx(OnChangePlugin, { onChange: handleChange, ignoreSelectionChange: true }), _jsx(FindReplacePlugin, { query: findQuery, currentIndex: currentFindMatch, onMatchCountChange: onFindMatchCountChange, editorRef: ref }), _jsx(SyncPlugin, { value: value, initialValueRef: initialValueRef }), !hideMarkdownHint && (_jsx("div", { className: "mt-1 flex justify-end", children: _jsx("span", { className: "text-muted-foreground/60 text-xs", children: "Markdown supported" }) }))] }) }) }));
94
+ });
package/dist/index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
- export { MarkdownEditor } from './components/markdown-editor';
1
+ export { MarkdownEditor, type MarkdownEditorHandle, type MarkdownEditorProps, } from './components/markdown-editor';
2
2
  export { FloatingToolbar } from './components/floating-toolbar';
3
- export { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, HR, } from './lib/lexical/markdown-transformers';
3
+ export { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, HR, IMAGE, OBSIDIAN_IMAGE_EMBED, MEDIA, } from './lib/lexical/markdown-transformers';
4
+ export { ImageNode, $createImageNode, $isImageNode, type ImagePayload, } from './lib/lexical/image-node';
5
+ export { MediaNode, $createMediaNode, $isMediaNode, type MediaKind, type MediaPayload, } from './lib/lexical/media-node';
6
+ export { MediaPasteDropPlugin, type MarkdownMediaKind, type MarkdownMediaUpload, type MarkdownMediaUploadResult, } from './lib/lexical/media-plugin';
7
+ export { FileReferencePlugin, type MarkdownReferenceOption, } from './lib/lexical/file-reference-plugin';
4
8
  export { editorTheme } from './lib/lexical/editor-theme';
5
9
  export { LINK_MATCHERS, URL_REGEX, EMAIL_REGEX, } from './lib/lexical/auto-link-config';
6
10
  export { useFloatingToolbar, type FloatingToolbarState, } from './lib/lexical/floating-toolbar-plugin';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,8BAA8B,CAAA;AAC7D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAG/D,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,oBAAoB,EACpB,EAAE,GACH,MAAM,qCAAqC,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AACxD,OAAO,EACL,aAAa,EACb,SAAS,EACT,WAAW,GACZ,MAAM,gCAAgC,CAAA;AACvC,OAAO,EACL,kBAAkB,EAClB,KAAK,oBAAoB,GAC1B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AACtD,OAAO,EACL,mBAAmB,EACnB,SAAS,GACV,MAAM,qCAAqC,CAAA;AAG5C,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EACL,cAAc,EACd,KAAK,oBAAoB,EACzB,KAAK,mBAAmB,GACzB,MAAM,8BAA8B,CAAA;AACrC,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAA;AAG/D,OAAO,EACL,0BAA0B,EAC1B,wBAAwB,EACxB,oBAAoB,EACpB,EAAE,EACF,KAAK,EACL,oBAAoB,EACpB,KAAK,GACN,MAAM,qCAAqC,CAAA;AAC5C,OAAO,EACL,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,KAAK,YAAY,GAClB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,SAAS,EACT,gBAAgB,EAChB,YAAY,EACZ,KAAK,SAAS,EACd,KAAK,YAAY,GAClB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EACL,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,yBAAyB,GAC/B,MAAM,4BAA4B,CAAA;AACnC,OAAO,EACL,mBAAmB,EACnB,KAAK,uBAAuB,GAC7B,MAAM,qCAAqC,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAA;AACxD,OAAO,EACL,aAAa,EACb,SAAS,EACT,WAAW,GACZ,MAAM,gCAAgC,CAAA;AACvC,OAAO,EACL,kBAAkB,EAClB,KAAK,oBAAoB,GAC1B,MAAM,uCAAuC,CAAA;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAA;AACtD,OAAO,EACL,mBAAmB,EACnB,SAAS,GACV,MAAM,qCAAqC,CAAA;AAG5C,OAAO,EAAE,4BAA4B,EAAE,MAAM,+BAA+B,CAAA"}
package/dist/index.js CHANGED
@@ -1,8 +1,12 @@
1
1
  // Components
2
- export { MarkdownEditor } from './components/markdown-editor';
2
+ export { MarkdownEditor, } from './components/markdown-editor';
3
3
  export { FloatingToolbar } from './components/floating-toolbar';
4
4
  // Lexical utilities (for advanced use cases)
5
- export { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, HR, } from './lib/lexical/markdown-transformers';
5
+ export { $convertFromMarkdownString, $convertToMarkdownString, markdownTransformers, HR, IMAGE, OBSIDIAN_IMAGE_EMBED, MEDIA, } from './lib/lexical/markdown-transformers';
6
+ export { ImageNode, $createImageNode, $isImageNode, } from './lib/lexical/image-node';
7
+ export { MediaNode, $createMediaNode, $isMediaNode, } from './lib/lexical/media-node';
8
+ export { MediaPasteDropPlugin, } from './lib/lexical/media-plugin';
9
+ export { FileReferencePlugin, } from './lib/lexical/file-reference-plugin';
6
10
  export { editorTheme } from './lib/lexical/editor-theme';
7
11
  export { LINK_MATCHERS, URL_REGEX, EMAIL_REGEX, } from './lib/lexical/auto-link-config';
8
12
  export { useFloatingToolbar, } from './lib/lexical/floating-toolbar-plugin';
@@ -32,6 +32,8 @@ export declare const editorTheme: {
32
32
  listitemUnchecked: string;
33
33
  };
34
34
  link: string;
35
+ mark: string;
36
+ markOverlap: string;
35
37
  quote: string;
36
38
  code: string;
37
39
  };
@@ -1 +1 @@
1
- {"version":3,"file":"editor-theme.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/editor-theme.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCvB,CAAA"}
1
+ {"version":3,"file":"editor-theme.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/editor-theme.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkCvB,CAAA"}
@@ -32,6 +32,8 @@ export const editorTheme = {
32
32
  listitemUnchecked: 'lexical-listitem-unchecked',
33
33
  },
34
34
  link: 'text-primary underline hover:text-primary/80 cursor-pointer',
35
+ mark: 'lexical-mark',
36
+ markOverlap: 'lexical-mark-overlap',
35
37
  quote: 'border-l-4 border-muted px-4 py-2 my-2 text-muted-foreground',
36
38
  code: 'bg-muted p-3 my-4 rounded-md overflow-x-auto font-mono text-sm leading-loose text-muted-foreground',
37
39
  };
@@ -0,0 +1,9 @@
1
+ export interface MarkdownReferenceOption {
2
+ path: string;
3
+ title?: string;
4
+ kind?: 'note' | 'file';
5
+ }
6
+ export declare function FileReferencePlugin({ references, }: {
7
+ references?: MarkdownReferenceOption[];
8
+ }): import("react/jsx-runtime").JSX.Element;
9
+ //# sourceMappingURL=file-reference-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"file-reference-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/file-reference-plugin.tsx"],"names":[],"mappings":"AAYA,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CACvB;AAqDD,wBAAgB,mBAAmB,CAAC,EAClC,UAAU,GACX,EAAE;IACD,UAAU,CAAC,EAAE,uBAAuB,EAAE,CAAA;CACvC,2CAmGA"}
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { LexicalTypeaheadMenuPlugin, MenuOption, useBasicTypeaheadTriggerMatch, } from '@lexical/react/LexicalTypeaheadMenuPlugin';
4
+ import { $createTextNode } from 'lexical';
5
+ import { useMemo, useState } from 'react';
6
+ import { createPortal } from 'react-dom';
7
+ import { cn } from '@moldable-ai/ui';
8
+ class FileReferenceOption extends MenuOption {
9
+ path;
10
+ title;
11
+ isCreate;
12
+ constructor(path, title, isCreate = false) {
13
+ super(`${isCreate ? 'create' : 'reference'}:${path}`);
14
+ this.path = path;
15
+ this.title = title;
16
+ this.isCreate = isCreate;
17
+ }
18
+ }
19
+ function referenceTarget(path) {
20
+ return path.replace(/\.md$/, '');
21
+ }
22
+ function titleFromPath(path) {
23
+ return path.split('/').pop()?.replace(/\.md$/, '') ?? path;
24
+ }
25
+ function slugifyReference(value) {
26
+ const segments = value
27
+ .split('/')
28
+ .map((segment) => segment
29
+ .trim()
30
+ .toLowerCase()
31
+ .replace(/[^a-z0-9 -]/g, '')
32
+ .replace(/\s+/g, '-')
33
+ .replace(/^-+|-+$/g, ''))
34
+ .filter(Boolean);
35
+ return segments.join('/') || 'untitled';
36
+ }
37
+ function referenceScore(option, query) {
38
+ const needle = query.trim().toLowerCase();
39
+ if (!needle)
40
+ return 1;
41
+ const title = (option.title ?? titleFromPath(option.path)).toLowerCase();
42
+ const path = option.path.toLowerCase();
43
+ if (title === needle || path === needle || path === `${needle}.md`)
44
+ return 100;
45
+ if (title.startsWith(needle))
46
+ return 80;
47
+ if (path.startsWith(needle))
48
+ return 70;
49
+ if (title.includes(needle))
50
+ return 50;
51
+ if (path.includes(needle))
52
+ return 40;
53
+ return 0;
54
+ }
55
+ export function FileReferencePlugin({ references, }) {
56
+ const [query, setQuery] = useState(null);
57
+ const triggerFn = useBasicTypeaheadTriggerMatch('@', {
58
+ allowWhitespace: true,
59
+ maxLength: 80,
60
+ minLength: 0,
61
+ });
62
+ const options = useMemo(() => {
63
+ const matching = (references ?? [])
64
+ .map((reference) => ({
65
+ reference,
66
+ score: referenceScore(reference, query ?? ''),
67
+ }))
68
+ .filter(({ score }) => score > 0)
69
+ .sort((a, b) => b.score - a.score || a.reference.path.localeCompare(b.reference.path))
70
+ .slice(0, 8)
71
+ .map(({ reference }) => new FileReferenceOption(reference.path, reference.title ?? titleFromPath(reference.path)));
72
+ const trimmed = query?.trim();
73
+ if (!trimmed)
74
+ return matching;
75
+ const createPath = `${slugifyReference(trimmed)}.md`;
76
+ const hasExact = (references ?? []).some((reference) => reference.path.toLowerCase() === createPath.toLowerCase() ||
77
+ reference.path.replace(/\.md$/, '').toLowerCase() ===
78
+ createPath.replace(/\.md$/, '').toLowerCase());
79
+ return hasExact
80
+ ? matching
81
+ : [
82
+ ...matching,
83
+ new FileReferenceOption(createPath, `Create ${trimmed}`, true),
84
+ ].slice(0, 9);
85
+ }, [query, references]);
86
+ return (_jsx(LexicalTypeaheadMenuPlugin, { triggerFn: triggerFn, options: options, onQueryChange: setQuery, preselectFirstItem: true, onSelectOption: (option, textNodeContainingQuery, closeMenu) => {
87
+ if (!textNodeContainingQuery)
88
+ return;
89
+ const node = $createTextNode(`[[${referenceTarget(option.path)}]]`);
90
+ textNodeContainingQuery.replace(node);
91
+ node.selectEnd();
92
+ closeMenu();
93
+ }, menuRenderFn: (anchorElementRef, { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }) => {
94
+ if (!anchorElementRef.current || options.length === 0)
95
+ return null;
96
+ return createPortal(_jsx("div", { className: "moldable-editor-reference-menu", children: options.map((option, index) => (_jsxs("button", { ref: option.setRefElement, type: "button", role: "option", "aria-selected": selectedIndex === index, className: cn('moldable-editor-reference-option', selectedIndex === index &&
97
+ 'moldable-editor-reference-option-selected'), onMouseEnter: () => setHighlightedIndex(index), onMouseDown: (event) => event.preventDefault(), onClick: () => selectOptionAndCleanUp(option), children: [_jsx("span", { className: "moldable-editor-reference-title", children: option.title }), _jsx("span", { className: "moldable-editor-reference-path", children: option.isCreate ? 'New note' : option.path })] }, option.key))) }), anchorElementRef.current);
98
+ } }));
99
+ }
@@ -0,0 +1,31 @@
1
+ import { type ForwardedRef } from 'react';
2
+ import { type LexicalEditor } from 'lexical';
3
+ export interface MarkdownEditorHandle {
4
+ replaceCurrentFindMatch: (replacement: string) => boolean;
5
+ replaceAllFindMatches: (replacement: string) => number;
6
+ focus: () => void;
7
+ }
8
+ interface FindReplacePluginProps {
9
+ query: string;
10
+ currentIndex: number;
11
+ onMatchCountChange?: (count: number) => void;
12
+ editorRef: ForwardedRef<MarkdownEditorHandle>;
13
+ }
14
+ export interface FindMatch {
15
+ start: number;
16
+ end: number;
17
+ }
18
+ export declare const FIND_MATCH_MARK_ID = "moldable-find-match";
19
+ export declare const FIND_CURRENT_MARK_ID = "moldable-find-current";
20
+ export declare const FIND_REPLACE_UPDATE_TAG = "moldable-find-replace";
21
+ export declare function getTextMatches(text: string, query: string): FindMatch[];
22
+ export declare function $clearFindMarks(): void;
23
+ export declare function $getFindMatches(query: string): FindMatch[];
24
+ export declare function $applyFindHighlights(query: string, currentIndex: number): number;
25
+ export declare function updateFindHighlights(editor: LexicalEditor, query: string, currentIndex: number): number;
26
+ export declare function registerFindMarkResolver(editor: LexicalEditor): () => void;
27
+ export declare function replaceFindMatch(editor: LexicalEditor, query: string, replacement: string, currentIndex: number): boolean;
28
+ export declare function replaceAllFindMatches(editor: LexicalEditor, query: string, replacement: string): number;
29
+ export declare function FindReplacePlugin({ query, currentIndex, onMatchCountChange, editorRef, }: FindReplacePluginProps): null;
30
+ export {};
31
+ //# sourceMappingURL=find-replace-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"find-replace-plugin.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/find-replace-plugin.tsx"],"names":[],"mappings":"AAGA,OAAO,EACL,KAAK,YAAY,EAKlB,MAAM,OAAO,CAAA;AASd,OAAO,EAML,KAAK,aAAa,EAGnB,MAAM,SAAS,CAAA;AAEhB,MAAM,WAAW,oBAAoB;IACnC,uBAAuB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,OAAO,CAAA;IACzD,qBAAqB,EAAE,CAAC,WAAW,EAAE,MAAM,KAAK,MAAM,CAAA;IACtD,KAAK,EAAE,MAAM,IAAI,CAAA;CAClB;AAED,UAAU,sBAAsB;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,YAAY,EAAE,MAAM,CAAA;IACpB,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAA;IAC5C,SAAS,EAAE,YAAY,CAAC,oBAAoB,CAAC,CAAA;CAC9C;AAaD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;CACZ;AAED,eAAO,MAAM,kBAAkB,wBAAwB,CAAA;AACvD,eAAO,MAAM,oBAAoB,0BAA0B,CAAA;AAC3D,eAAO,MAAM,uBAAuB,0BAA0B,CAAA;AAM9D,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,CAiBvE;AAwDD,wBAAgB,eAAe,SAgB9B;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,eAG5C;AAED,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,UAoBvE;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,EACb,YAAY,EAAE,MAAM,UAcrB;AAeD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,aAAa,cAa7D;AAED,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,WAsBrB;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,aAAa,EACrB,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,UAsBpB;AAED,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,YAAY,EACZ,kBAAkB,EAClB,SAAS,GACV,EAAE,sBAAsB,QAqDxB"}
@@ -0,0 +1,205 @@
1
+ 'use client';
2
+ import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
3
+ import { useCallback, useEffect, useImperativeHandle, useRef, } from 'react';
4
+ import { $createMarkNode, $isMarkNode, $unwrapMarkNode, $wrapSelectionInMarkNode, MarkNode, } from '@lexical/mark';
5
+ import { mergeRegister, registerNestedElementResolver } from '@lexical/utils';
6
+ import { $createRangeSelection, $getRoot, $getSelection, $nodesOfType, $setSelection, } from 'lexical';
7
+ export const FIND_MATCH_MARK_ID = 'moldable-find-match';
8
+ export const FIND_CURRENT_MARK_ID = 'moldable-find-current';
9
+ export const FIND_REPLACE_UPDATE_TAG = 'moldable-find-replace';
10
+ function getNormalizedQuery(query) {
11
+ return query.trim().toLocaleLowerCase();
12
+ }
13
+ export function getTextMatches(text, query) {
14
+ const normalizedQuery = getNormalizedQuery(query);
15
+ if (!normalizedQuery)
16
+ return [];
17
+ const normalizedText = text.toLocaleLowerCase();
18
+ const matches = [];
19
+ let searchFrom = 0;
20
+ while (searchFrom <= normalizedText.length - normalizedQuery.length) {
21
+ const start = normalizedText.indexOf(normalizedQuery, searchFrom);
22
+ if (start === -1)
23
+ break;
24
+ matches.push({ start, end: start + normalizedQuery.length });
25
+ searchFrom = start + normalizedQuery.length;
26
+ }
27
+ return matches;
28
+ }
29
+ function $collectTextEntries() {
30
+ const entries = [];
31
+ let text = '';
32
+ for (const node of $getRoot().getAllTextNodes()) {
33
+ const value = node.getTextContent();
34
+ const start = text.length;
35
+ text += value;
36
+ entries.push({ node, start, end: text.length });
37
+ }
38
+ return { entries, text };
39
+ }
40
+ function getTextPosition(entries, offset, bias) {
41
+ if (bias === 'next') {
42
+ for (const entry of entries) {
43
+ if (offset >= entry.start && offset < entry.end) {
44
+ return { node: entry.node, offset: offset - entry.start };
45
+ }
46
+ }
47
+ }
48
+ else {
49
+ for (const entry of entries) {
50
+ if (offset > entry.start && offset <= entry.end) {
51
+ return { node: entry.node, offset: offset - entry.start };
52
+ }
53
+ }
54
+ }
55
+ const fallback = bias === 'next' ? entries[0] : entries.at(-1);
56
+ if (!fallback)
57
+ return null;
58
+ return {
59
+ node: fallback.node,
60
+ offset: bias === 'next' ? 0 : fallback.node.getTextContentSize(),
61
+ };
62
+ }
63
+ function $createSelectionForMatch(match) {
64
+ const { entries } = $collectTextEntries();
65
+ const start = getTextPosition(entries, match.start, 'next');
66
+ const end = getTextPosition(entries, match.end, 'previous');
67
+ if (!start || !end)
68
+ return null;
69
+ const selection = $createRangeSelection();
70
+ selection.setTextNodeRange(start.node, start.offset, end.node, end.offset);
71
+ return selection;
72
+ }
73
+ export function $clearFindMarks() {
74
+ for (const markNode of $nodesOfType(MarkNode)) {
75
+ let nextNode = markNode;
76
+ if (nextNode.hasID(FIND_MATCH_MARK_ID)) {
77
+ nextNode = nextNode.deleteID(FIND_MATCH_MARK_ID);
78
+ }
79
+ if (nextNode.hasID(FIND_CURRENT_MARK_ID)) {
80
+ nextNode = nextNode.deleteID(FIND_CURRENT_MARK_ID);
81
+ }
82
+ if (nextNode.getIDs().length === 0) {
83
+ $unwrapMarkNode(nextNode);
84
+ }
85
+ }
86
+ }
87
+ export function $getFindMatches(query) {
88
+ const { text } = $collectTextEntries();
89
+ return getTextMatches(text, query);
90
+ }
91
+ export function $applyFindHighlights(query, currentIndex) {
92
+ $clearFindMarks();
93
+ const matches = $getFindMatches(query);
94
+ for (let index = matches.length - 1; index >= 0; index -= 1) {
95
+ const selection = $createSelectionForMatch(matches[index]);
96
+ if (selection) {
97
+ $wrapSelectionInMarkNode(selection, false, FIND_MATCH_MARK_ID);
98
+ }
99
+ }
100
+ const currentMatch = matches[currentIndex] ?? matches[0];
101
+ if (currentMatch) {
102
+ const selection = $createSelectionForMatch(currentMatch);
103
+ if (selection) {
104
+ $wrapSelectionInMarkNode(selection, false, FIND_CURRENT_MARK_ID);
105
+ }
106
+ }
107
+ return matches.length;
108
+ }
109
+ export function updateFindHighlights(editor, query, currentIndex) {
110
+ let matchCount = 0;
111
+ editor.update(() => {
112
+ const previousSelection = $getSelection()?.clone() ?? null;
113
+ matchCount = $applyFindHighlights(query, currentIndex);
114
+ $setSelection(previousSelection);
115
+ }, { discrete: true, tag: FIND_REPLACE_UPDATE_TAG });
116
+ return matchCount;
117
+ }
118
+ function scrollCurrentMatchIntoView(editor) {
119
+ const editorRoot = editor.getRootElement();
120
+ const currentMatch = editorRoot?.querySelector('.lexical-mark-overlap');
121
+ currentMatch?.scrollIntoView({
122
+ behavior: 'smooth',
123
+ block: 'center',
124
+ inline: 'nearest',
125
+ });
126
+ }
127
+ export function registerFindMarkResolver(editor) {
128
+ return registerNestedElementResolver(editor, MarkNode, (from) => $createMarkNode(from.getIDs()), (from, to) => {
129
+ if (!$isMarkNode(to))
130
+ return;
131
+ for (const id of from.getIDs()) {
132
+ to.addID(id);
133
+ }
134
+ });
135
+ }
136
+ export function replaceFindMatch(editor, query, replacement, currentIndex) {
137
+ let replaced = false;
138
+ editor.update(() => {
139
+ $clearFindMarks();
140
+ const matches = $getFindMatches(query);
141
+ const match = matches[currentIndex] ?? matches[0];
142
+ if (!match)
143
+ return;
144
+ const selection = $createSelectionForMatch(match);
145
+ if (!selection)
146
+ return;
147
+ $setSelection(selection);
148
+ selection.insertText(replacement);
149
+ replaced = true;
150
+ }, { discrete: true });
151
+ return replaced;
152
+ }
153
+ export function replaceAllFindMatches(editor, query, replacement) {
154
+ let replacedCount = 0;
155
+ editor.update(() => {
156
+ $clearFindMarks();
157
+ const matches = $getFindMatches(query);
158
+ for (let index = matches.length - 1; index >= 0; index -= 1) {
159
+ const selection = $createSelectionForMatch(matches[index]);
160
+ if (!selection)
161
+ continue;
162
+ $setSelection(selection);
163
+ selection.insertText(replacement);
164
+ replacedCount += 1;
165
+ }
166
+ }, { discrete: true });
167
+ return replacedCount;
168
+ }
169
+ export function FindReplacePlugin({ query, currentIndex, onMatchCountChange, editorRef, }) {
170
+ const [editor] = useLexicalComposerContext();
171
+ const onMatchCountChangeRef = useRef(onMatchCountChange);
172
+ useEffect(() => {
173
+ onMatchCountChangeRef.current = onMatchCountChange;
174
+ }, [onMatchCountChange]);
175
+ useImperativeHandle(editorRef, () => ({
176
+ replaceCurrentFindMatch: (replacement) => replaceFindMatch(editor, query, replacement, currentIndex),
177
+ replaceAllFindMatches: (replacement) => replaceAllFindMatches(editor, query, replacement),
178
+ focus: () => editor.focus(),
179
+ }), [currentIndex, editor, query]);
180
+ const applyHighlights = useCallback(() => {
181
+ const matchCount = updateFindHighlights(editor, query, currentIndex);
182
+ onMatchCountChangeRef.current?.(matchCount);
183
+ if (getNormalizedQuery(query) && matchCount > 0) {
184
+ window.requestAnimationFrame(() => scrollCurrentMatchIntoView(editor));
185
+ }
186
+ }, [currentIndex, editor, query]);
187
+ useEffect(() => {
188
+ return registerFindMarkResolver(editor);
189
+ }, [editor]);
190
+ useEffect(() => {
191
+ applyHighlights();
192
+ const unregister = mergeRegister(editor.registerUpdateListener(({ tags }) => {
193
+ if (tags.has(FIND_REPLACE_UPDATE_TAG))
194
+ return;
195
+ applyHighlights();
196
+ }), () => {
197
+ editor.update(() => $clearFindMarks(), {
198
+ discrete: true,
199
+ tag: FIND_REPLACE_UPDATE_TAG,
200
+ });
201
+ });
202
+ return unregister;
203
+ }, [applyHighlights, editor]);
204
+ return null;
205
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"headless-editor.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/headless-editor.ts"],"names":[],"mappings":"AAQA;;;GAGG;AACH,wBAAgB,4BAA4B,oCAkB3C"}
1
+ {"version":3,"file":"headless-editor.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/headless-editor.ts"],"names":[],"mappings":"AAWA;;;GAGG;AACH,wBAAgB,4BAA4B,oCAqB3C"}
@@ -4,7 +4,10 @@ import { HorizontalRuleNode } from '@lexical/extension';
4
4
  import { createHeadlessEditor } from '@lexical/headless';
5
5
  import { AutoLinkNode, LinkNode } from '@lexical/link';
6
6
  import { ListItemNode, ListNode } from '@lexical/list';
7
+ import { MarkNode } from '@lexical/mark';
7
8
  import { HeadingNode, QuoteNode } from '@lexical/rich-text';
9
+ import { ImageNode } from './image-node';
10
+ import { MediaNode } from './media-node';
8
11
  /**
9
12
  * Creates a headless Lexical editor for server-side operations.
10
13
  * Use this for translation, content extraction, etc.
@@ -20,7 +23,10 @@ export function createMoldableHeadlessEditor() {
20
23
  CodeHighlightNode,
21
24
  AutoLinkNode,
22
25
  LinkNode,
26
+ MarkNode,
23
27
  HorizontalRuleNode,
28
+ ImageNode,
29
+ MediaNode,
24
30
  ],
25
31
  theme: editorTheme,
26
32
  onError: (error) => {
@@ -0,0 +1,8 @@
1
+ import { type NodeKey } from 'lexical';
2
+ export declare function ImageComponent({ altText, nodeKey, src, width, }: {
3
+ altText: string;
4
+ nodeKey: NodeKey;
5
+ src: string;
6
+ width?: number;
7
+ }): import("react/jsx-runtime").JSX.Element;
8
+ //# sourceMappingURL=image-component.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"image-component.d.ts","sourceRoot":"","sources":["../../../src/lib/lexical/image-component.tsx"],"names":[],"mappings":"AAKA,OAAO,EAQL,KAAK,OAAO,EACb,MAAM,SAAS,CAAA;AAiBhB,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,OAAO,EACP,GAAG,EACH,KAAK,GACN,EAAE;IACD,OAAO,EAAE,MAAM,CAAA;IACf,OAAO,EAAE,OAAO,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,2CAoIA"}