@meowdown/react 0.5.0 → 0.6.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/README.md CHANGED
@@ -5,7 +5,7 @@ React components for Meowdown, a hybrid (live-preview) Markdown editor.
5
5
  ## Usage
6
6
 
7
7
  ```tsx
8
- import { Editor, type EditorHandle } from '@meowdown/react'
8
+ import { MeowdownEditor, type EditorHandle } from '@meowdown/react'
9
9
  import '@meowdown/react/style.css'
10
10
  import { useRef, useCallback } from 'react'
11
11
 
@@ -15,13 +15,20 @@ export function App() {
15
15
  console.log(ref.current?.getMarkdown())
16
16
  }, [])
17
17
 
18
- return <Editor ref={ref} mode="focus" initialMarkdown="# Hello" onDocChange={handleDocChange} />
18
+ return (
19
+ <MeowdownEditor
20
+ handleRef={ref}
21
+ mode="focus"
22
+ initialMarkdown="# Hello"
23
+ onDocChange={handleDocChange}
24
+ />
25
+ )
19
26
  }
20
27
  ```
21
28
 
22
29
  ## API
23
30
 
24
- ### `<Editor>`
31
+ ### `<MeowdownEditor>`
25
32
 
26
33
  The Markdown editor component. Renders inside a `div.meowdown` wrapper that fills a flex parent. In rich modes, typing `/` opens a slash menu for inserting blocks (headings, blockquote, lists, code block, table). Hovering a block shows a handle to its left: the plus button inserts an empty paragraph below the block, and the grip selects the block and can be dragged to move it, with a drop indicator line marking the target.
27
34
 
@@ -31,7 +38,7 @@ The Markdown editor component. Renders inside a `div.meowdown` wrapper that fill
31
38
  - `'hide'`: Markdown syntax is always hidden.
32
39
  - `'source'`: raw Markdown source with syntax highlighting.
33
40
  - `initialMarkdown?: string`: first render only.
34
- - `onDocChange?: VoidFunction`: called on every document change.
41
+ - `onDocChange?: VoidFunction`: called on every user-driven document change. Programmatic `setMarkdown` / `setState` on the handle do not fire it.
35
42
  - `onTagSearch?: (query: string) => TagItem[] | Promise<TagItem[]>`: enables the tag menu, which opens when typing `#` followed by text in a rich mode. Returns ranked rows `{ tag, label?, detail?, onSelect? }` (the menu does not re-sort). Selecting a row inserts `#tag ` then runs its `onSelect`. Omit to disable.
36
43
  - `onWikilinkSearch?: (query: string) => WikilinkItem[] | Promise<WikilinkItem[]>`: enables the wikilink menu, which opens as soon as `[[` is typed in a rich mode. Returns ranked rows `{ target, label?, detail?, onSelect? }` (the menu does not re-sort). Selecting a row inserts `[[target]]` then runs its `onSelect`. Omit to disable.
37
44
  - `onWikilinkClick?: (payload: { target: string; event: MouseEvent }) => void`: called when a rendered wiki link is clicked. A plain click inside a link the caret already sits in just places the caret; `Mod`-click always fires. Pass a stable function (e.g. from `useCallback`). Ignored in source mode.
@@ -43,7 +50,7 @@ The Markdown editor component. Renders inside a `div.meowdown` wrapper that fill
43
50
  - `spellCheck?: boolean`: toggles the browser's native spell checking in the rich modes. Defaults to the browser's behavior. Ignored in source mode.
44
51
  - `editorClassName?: string`: class on the editable root (the contenteditable). Rich modes only.
45
52
  - `wrapperClassName?: string`: class on the outer `div.meowdown` wrapper.
46
- - `ref?: Ref<EditorHandle>`
53
+ - `handleRef?: Ref<EditorHandle>`
47
54
  - `children?: ReactNode`: rendered inside the editor's ProseKit context, so children can call `useEditor()`. Only rendered in the rich modes; source mode ignores them.
48
55
 
49
56
  ### `useEditor`
@@ -54,12 +61,16 @@ Re-exported from `@prosekit/react`. Call it from a component passed as `children
54
61
 
55
62
  Re-exported from `@meowdown/core`. `checkRoundTrip(markdown)` returns `'exact' | 'normalizing' | 'lossy'`, for hosts that gate saving markdown files on whether the editor reproduces them faithfully.
56
63
 
64
+ ### `EDITOR_KEY_BINDINGS`
65
+
66
+ Re-exported from `@meowdown/core`. A literal (`as const`) object mapping each editor shortcut (e.g. `Mod-b`, `Mod-1`) to its description, for host settings UIs and keybinding-collision checks.
67
+
57
68
  ### `EditorHandle`
58
69
 
59
- Imperative handle for the editor, attached via `ref`.
70
+ Imperative handle for the editor, attached via `handleRef`.
60
71
 
61
72
  - `getMarkdown(): string`: serializes the current document to Markdown. Can be expensive on large documents; call it on demand (e.g. throttled) instead of on every change.
62
- - `setMarkdown(markdown: string): void`: replaces the whole document as a single undoable edit.
73
+ - `setMarkdown(markdown: string): void`: replaces the whole document as a single undoable edit. Does not fire `onDocChange`.
63
74
  - `getState(): EditorStateSnapshot`: returns `[markdown, selection]`, where `selection` is a `SelectionJSON` (`{ anchor: number, head: number, type: string }`).
64
75
  - `setState(markdown?: string, selection?: SelectionJSON | 'start' | 'end'): void`: replaces the document (if `markdown` is given) and restores `selection`: exactly when valid, otherwise clamped to the nearest text selection; out-of-range positions never throw. `'start'` and `'end'` jump to the document edges. Without a selection, the current one is mapped through the change. Restore a snapshot with `handle.setState(...handle.getState())`.
65
76
  - `getSelection(): SelectionJSON`: returns the current selection.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactNode, Ref } from "react";
2
- import { ImageOptions, MarkMode, MarkMode as MarkMode$1, PlaceholderOptions, RoundTripFidelity, TypedEditor, TypedEditor as TypedEditor$1, WikilinkClickHandler, checkRoundTrip } from "@meowdown/core";
2
+ import { EDITOR_KEY_BINDINGS, ImageOptions, MarkMode, MarkMode as MarkMode$1, PlaceholderOptions, RoundTripFidelity, TypedEditor, TypedEditor as TypedEditor$1, WikilinkClickHandler, checkRoundTrip } from "@meowdown/core";
3
3
  import { SelectionJSON, SelectionJSON as SelectionJSON$1 } from "@prosekit/core";
4
4
  import { useEditor } from "@prosekit/react";
5
5
 
@@ -91,7 +91,10 @@ interface EditorProps {
91
91
  * first render is used; later changes are ignored.
92
92
  */
93
93
  initialMarkdown?: string;
94
- /** Called on every document change. */
94
+ /**
95
+ * Called on every user-driven document change. Programmatic `setMarkdown` and
96
+ * `setState` on the handle do not fire it.
97
+ */
95
98
  onDocChange?: VoidFunction;
96
99
  /**
97
100
  * Searches tags for the tag menu, which opens when typing `#` followed by
@@ -144,11 +147,11 @@ interface EditorProps {
144
147
  /** Class on the outer `.meowdown` wrapper div. */
145
148
  wrapperClassName?: string;
146
149
  /** Imperative handle for the editor. */
147
- ref?: Ref<EditorHandle>;
150
+ handleRef?: Ref<EditorHandle>;
148
151
  /** Nodes rendered inside the editor's ProseKit context (rich modes only). */
149
152
  children?: ReactNode;
150
153
  }
151
- declare function Editor({
154
+ declare function MeowdownEditor({
152
155
  mode,
153
156
  initialMarkdown,
154
157
  onDocChange,
@@ -163,8 +166,8 @@ declare function Editor({
163
166
  spellCheck,
164
167
  editorClassName,
165
168
  wrapperClassName,
166
- ref,
169
+ handleRef,
167
170
  children
168
171
  }: EditorProps): import("react").JSX.Element;
169
172
  //#endregion
170
- export { Editor, type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, type MarkMode, type RoundTripFidelity, type SelectionHint, type SelectionJSON, type TagItem, type TagSearchHandler, type TypedEditor, type WikilinkItem, type WikilinkSearchHandler, checkRoundTrip, useEditor };
173
+ export { EDITOR_KEY_BINDINGS, type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, type MarkMode, MeowdownEditor, type RoundTripFidelity, type SelectionHint, type SelectionJSON, type TagItem, type TagSearchHandler, type TypedEditor, type WikilinkItem, type WikilinkSearchHandler, checkRoundTrip, useEditor };
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { Compartment, EditorSelection, EditorState } from "@codemirror/state";
7
7
  import { EditorView, keymap } from "@codemirror/view";
8
8
  import { clamp } from "@ocavue/utils";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
- import { checkRoundTrip, codeBlockLanguages, defineEditorExtension, defineImages, defineMarkMode, definePlaceholder, defineReadonly, defineWikilinkClickHandler, docToMarkdown, markdownToDoc } from "@meowdown/core";
10
+ import { EDITOR_KEY_BINDINGS, checkRoundTrip, codeBlockLanguages, defineEditorExtension, defineImages, defineMarkMode, definePlaceholder, defineReadonly, defineWikilinkClickHandler, docToMarkdown, markdownToDoc } from "@meowdown/core";
11
11
  import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, union } from "@prosekit/core";
12
12
  import { Selection, TextSelection } from "@prosekit/pm/state";
13
13
  import { ProseKit, defineReactNodeView, useEditor, useEditor as useEditor$1, useExtension } from "@prosekit/react";
@@ -26,6 +26,7 @@ function CodeMirrorEditor({ initialMarkdown, onDocChange, readOnly, ref }) {
26
26
  const containerRef = useRef(null);
27
27
  const viewRef = useRef(null);
28
28
  const readOnlyCompartmentRef = useRef(new Compartment());
29
+ const suppressDocChangeRef = useRef(false);
29
30
  const onDocChangeRef = useRef(onDocChange);
30
31
  useLayoutEffect(() => {
31
32
  onDocChangeRef.current = onDocChange;
@@ -55,15 +56,20 @@ function CodeMirrorEditor({ initialMarkdown, onDocChange, readOnly, ref }) {
55
56
  }
56
57
  if (markdown == null && !selection) return;
57
58
  const docLength = markdown == null ? view.state.doc.length : markdown.length;
58
- view.dispatch({
59
- changes: markdown == null ? void 0 : {
60
- from: 0,
61
- to: view.state.doc.length,
62
- insert: markdown
63
- },
64
- selection: selection ? resolveSelection$1(selection, docLength) : void 0,
65
- scrollIntoView: true
66
- });
59
+ suppressDocChangeRef.current = true;
60
+ try {
61
+ view.dispatch({
62
+ changes: markdown == null ? void 0 : {
63
+ from: 0,
64
+ to: view.state.doc.length,
65
+ insert: markdown
66
+ },
67
+ selection: selection ? resolveSelection$1(selection, docLength) : void 0,
68
+ scrollIntoView: true
69
+ });
70
+ } finally {
71
+ suppressDocChangeRef.current = false;
72
+ }
67
73
  }
68
74
  function setMarkdown(markdown) {
69
75
  setState(markdown);
@@ -104,7 +110,7 @@ function CodeMirrorEditor({ initialMarkdown, onDocChange, readOnly, ref }) {
104
110
  EditorView.lineWrapping,
105
111
  readOnlyCompartmentRef.current.of(EditorState.readOnly.of(initialReadOnlyRef.current)),
106
112
  EditorView.updateListener.of((update) => {
107
- if (!update.docChanged) return;
113
+ if (!update.docChanged || suppressDocChangeRef.current) return;
108
114
  onDocChangeRef.current?.();
109
115
  })
110
116
  ]
@@ -429,22 +435,31 @@ function DropIndicator$1() {
429
435
  //#endregion
430
436
  //#region src/components/editor-extensions.tsx
431
437
  function EditorExtensions({ markMode, onDocChange, onWikilinkClick, resolveImageUrl, onImagePaste, onImageSaveError, placeholder, readOnly }) {
432
- useExtension(useMemo(() => defineMarkMode(markMode), [markMode]));
433
- useExtension(useMemo(() => readOnly ? defineReadonly() : null, [readOnly]));
434
- useExtension(useMemo(() => onDocChange ? defineDocChangeHandler(onDocChange) : null, [onDocChange]));
435
- useExtension(useMemo(() => onWikilinkClick ? defineWikilinkClickHandler(onWikilinkClick) : null, [onWikilinkClick]));
436
- useExtension(useMemo(() => resolveImageUrl ? defineImages({
437
- resolveImageUrl,
438
- onImagePaste,
439
- onImageSaveError
440
- }) : null, [
438
+ useExtension(useMemo(() => {
439
+ return defineMarkMode(markMode);
440
+ }, [markMode]));
441
+ useExtension(useMemo(() => {
442
+ return readOnly ? defineReadonly() : null;
443
+ }, [readOnly]));
444
+ useExtension(useMemo(() => {
445
+ return onDocChange ? defineDocChangeHandler(onDocChange) : null;
446
+ }, [onDocChange]));
447
+ useExtension(useMemo(() => {
448
+ return onWikilinkClick ? defineWikilinkClickHandler(onWikilinkClick) : null;
449
+ }, [onWikilinkClick]));
450
+ useExtension(useMemo(() => {
451
+ return resolveImageUrl ? defineImages({
452
+ resolveImageUrl,
453
+ onImagePaste,
454
+ onImageSaveError
455
+ }) : null;
456
+ }, [
441
457
  resolveImageUrl,
442
458
  onImagePaste,
443
459
  onImageSaveError
444
460
  ]));
445
461
  useExtension(useMemo(() => {
446
- if (!placeholder) return null;
447
- return definePlaceholder({ placeholder });
462
+ return placeholder ? definePlaceholder({ placeholder }) : null;
448
463
  }, [placeholder]));
449
464
  return null;
450
465
  }
@@ -686,6 +701,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
686
701
  if (initialMarkdown) editor.setContent(markdownToDoc(editor, initialMarkdown));
687
702
  return editor;
688
703
  });
704
+ const suppressDocChangeRef = useRef(false);
689
705
  useImperativeHandle(ref, () => {
690
706
  function getMarkdown() {
691
707
  return docToMarkdown(editor.state.doc);
@@ -704,7 +720,12 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
704
720
  transaction.replaceWith(0, transaction.doc.content.size, doc.content);
705
721
  }
706
722
  if (selection) transaction.setSelection(resolveSelection(transaction.doc, selection)).scrollIntoView();
707
- editor.view.dispatch(transaction);
723
+ suppressDocChangeRef.current = true;
724
+ try {
725
+ editor.view.dispatch(transaction);
726
+ } finally {
727
+ suppressDocChangeRef.current = false;
728
+ }
708
729
  }
709
730
  function setMarkdown(markdown) {
710
731
  setState(markdown);
@@ -730,6 +751,13 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
730
751
  editor
731
752
  };
732
753
  }, [editor]);
754
+ const handleDocChange = useMemo(() => {
755
+ if (!onDocChange) return void 0;
756
+ return () => {
757
+ if (suppressDocChangeRef.current) return;
758
+ onDocChange();
759
+ };
760
+ }, [onDocChange]);
733
761
  return /* @__PURE__ */ jsxs(ProseKit, {
734
762
  editor,
735
763
  children: [
@@ -740,7 +768,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
740
768
  }),
741
769
  /* @__PURE__ */ jsx(EditorExtensions, {
742
770
  markMode,
743
- onDocChange,
771
+ onDocChange: handleDocChange,
744
772
  onWikilinkClick,
745
773
  resolveImageUrl,
746
774
  onImagePaste,
@@ -760,9 +788,9 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
760
788
 
761
789
  //#endregion
762
790
  //#region src/components/editor.tsx
763
- function Editor({ mode = "focus", initialMarkdown, onDocChange, onTagSearch, onWikilinkSearch, onWikilinkClick, resolveImageUrl, onImagePaste, onImageSaveError, placeholder, readOnly, spellCheck, editorClassName, wrapperClassName, ref, children }) {
791
+ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSearch, onWikilinkSearch, onWikilinkClick, resolveImageUrl, onImagePaste, onImageSaveError, placeholder, readOnly, spellCheck, editorClassName, wrapperClassName, handleRef, children }) {
764
792
  const childRef = useRef(null);
765
- useImperativeHandle(ref, () => {
793
+ useImperativeHandle(handleRef, () => {
766
794
  function getMarkdown() {
767
795
  return childRef.current?.getMarkdown() ?? "";
768
796
  }
@@ -838,4 +866,4 @@ function Editor({ mode = "focus", initialMarkdown, onDocChange, onTagSearch, onW
838
866
  }
839
867
 
840
868
  //#endregion
841
- export { Editor, checkRoundTrip, useEditor };
869
+ export { EDITOR_KEY_BINDINGS, MeowdownEditor, checkRoundTrip, useEditor };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.5.0",
4
+ "version": "0.6.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -28,7 +28,7 @@
28
28
  "@prosekit/pm": "^0.1.18",
29
29
  "@prosekit/react": "^0.8.0-beta.0",
30
30
  "clsx": "^2.1.1",
31
- "@meowdown/core": "0.4.0"
31
+ "@meowdown/core": "0.5.0"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "react": "^19.0.0",
@@ -43,7 +43,7 @@
43
43
  }
44
44
  },
45
45
  "devDependencies": {
46
- "@css-modules-kit/codegen": "^1.2.0",
46
+ "@css-modules-kit/codegen": "^1.3.0",
47
47
  "@ocavue/tsconfig": "^0.7.1",
48
48
  "@tsdown/css": "^0.22.2",
49
49
  "@types/react": "^19.2.17",