@meowdown/react 0.58.2 → 0.59.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
@@ -101,6 +101,31 @@ file embeds; return `false` to keep it as a normal link.
101
101
  />
102
102
  ```
103
103
 
104
+ ### Find in document
105
+
106
+ `searchQuery` highlights every match and selects the first one at or after the
107
+ caret; changing it re-anchors from wherever the selection is, so refining a
108
+ query keeps the match the user is looking at. An empty string (the default)
109
+ clears the highlights and leaves the selection alone. `onSearchChange` reports
110
+ `{ total, active }` for a match counter (`active` is one-based; `0` means the
111
+ selection is not on a match), and `EditorHandle.findNext()` /
112
+ `EditorHandle.findPrevious()` walk the matches and wrap at the document edges.
113
+ The query is applied through `useDeferredValue`, so fast typing may skip
114
+ intermediate values on a slow device.
115
+
116
+ ```tsx
117
+ const [query, setQuery] = useState('')
118
+ const [status, setStatus] = useState({ total: 0, active: 0 })
119
+
120
+ <input value={query} onChange={(event) => setQuery(event.target.value)} />
121
+ <span>{status.active} / {status.total}</span>
122
+ <MeowdownEditor
123
+ handleRef={editorRef}
124
+ searchQuery={query}
125
+ onSearchChange={setStatus}
126
+ />
127
+ ```
128
+
104
129
  ### Wiki-link hover cards
105
130
 
106
131
  Mount `WikilinkHoverCard` inside `MeowdownEditor` and render host-owned preview
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactElement, ReactNode, Ref } from "react";
2
- import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileInfoResolver, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikiEmbedResolver, WikilinkClickHandler, WikilinkHoverHit } from "@meowdown/core";
2
+ import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileInfoResolver, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, SearchStatusHandler, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikiEmbedResolver, WikilinkClickHandler, WikilinkHoverHit } from "@meowdown/core";
3
3
  import { SelectionJSON, SelectionJSON as SelectionJSON$1 } from "@prosekit/core";
4
4
  import { useEditor, useExtension, useKeymap } from "@prosekit/react";
5
5
  //#region src/utils/date-format.d.ts
@@ -97,6 +97,10 @@ interface EditorHandle {
97
97
  acceptPendingReplacement: (options?: AcceptPendingReplacementOptions) => void;
98
98
  /** Clears the staged replacement without touching the document. */
99
99
  discardPendingReplacement: () => void;
100
+ /** Selects the next match of the current search query, wrapping at the document end. */
101
+ findNext: () => void;
102
+ /** Selects the previous match of the current search query, wrapping at the document start. */
103
+ findPrevious: () => void;
100
104
  /**
101
105
  * Escape hatch: the underlying ProseKit editor, or `undefined` when the
102
106
  * handle does not wrap one.
@@ -389,6 +393,18 @@ interface EditorProps {
389
393
  * behavior.
390
394
  */
391
395
  spellCheck?: boolean;
396
+ /**
397
+ * The search query. Every match is highlighted, and the first match at or
398
+ * after the caret is selected whenever the query changes. An empty string
399
+ * (the default) clears the highlights and leaves the selection alone. A match
400
+ * that the current mode hides reveals itself while it is the selected one.
401
+ */
402
+ searchQuery?: string;
403
+ /**
404
+ * Called when the number of matches or the selected match changes. Pass a
405
+ * stable function (e.g. from `useCallback`).
406
+ */
407
+ onSearchChange?: SearchStatusHandler;
392
408
  /**
393
409
  * Clock format the `/now` slash command inserts: '12' for "3:45pm" or '24'
394
410
  * for "15:45". Defaults to '12'.
@@ -409,7 +425,7 @@ interface EditorProps {
409
425
  *
410
426
  * Callbacks and resolvers should be stable; pass them via `useCallback`.
411
427
  */
412
- declare function MeowdownEditor({ mode, initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, frontmatter, blockHandle, caretGlide, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }: EditorProps): ReactElement;
428
+ declare function MeowdownEditor({ mode, initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, frontmatter, blockHandle, caretGlide, placeholder, readOnly, spellCheck, searchQuery, onSearchChange, timeFormat, editorClassName, wrapperClassName, handleRef, children }: EditorProps): ReactElement;
413
429
  //#endregion
414
430
  //#region src/components/markdown-view.d.ts
415
431
  /** Payload for {@link TaskClickHandler}. */
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { clsx } from "clsx/lite";
2
2
  import { Fragment, cloneElement, createElement, useCallback, useDeferredValue, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
- import { buildFileMarkdown, codeBlockLanguages, collectReferenceDefinitions, defaultResolveImageUrl, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSpellCheckPlugin, defineSubstitution, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, docToMarkdown, formatFileSize, getCodeTokens, getFileKind, getMarkBuilders, getPendingReplacement, getSelectedText, getTableColumnAlign, getTextblockDisplayText, getVirtualElementFromRange, inlineTextToMarkChunksWithContext, isCodeBlockPreviewHiddenDecoration, isNodeOfType, isSelectionInTableCell, listenForTweetHeight, loadKaTeX, markdownToDoc, matchEmbed, renderMathInto } from "@meowdown/core";
3
+ import { buildFileMarkdown, codeBlockLanguages, collectReferenceDefinitions, defaultResolveImageUrl, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSearchStatusHandler, defineSpellCheckPlugin, defineSubstitution, defineTagClickHandler, defineViewAttributes, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, docToMarkdown, formatFileSize, getCodeTokens, getFileKind, getMarkBuilders, getPendingReplacement, getSearchStatus, getSelectedText, getTableColumnAlign, getTextblockDisplayText, getVirtualElementFromRange, inlineTextToMarkChunksWithContext, isCodeBlockPreviewHiddenDecoration, isNodeOfType, isSelectionInTableCell, listenForTweetHeight, loadKaTeX, markdownToDoc, matchEmbed, renderMathInto } from "@meowdown/core";
4
4
  import { clamp } from "@ocavue/utils";
5
5
  import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, defineUpdateHandler, isTextSelection, union } from "@prosekit/core";
6
6
  import { Selection, TextSelection } from "@prosekit/pm/state";
@@ -391,11 +391,16 @@ function DropIndicator$1() {
391
391
 
392
392
  //#endregion
393
393
  //#region src/components/editor-extensions.tsx
394
- function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, placeholder, readOnly, wikilinkEnabled, spellCheck }) {
394
+ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, placeholder, readOnly, wikilinkEnabled, spellCheck, searchQuery, onSearchChange, editorClassName }) {
395
395
  const editor = useEditor$1();
396
396
  useEffect(() => {
397
397
  editor.commands.setMarkMode(markMode);
398
398
  }, [editor, markMode]);
399
+ const deferredSearchQuery = useDeferredValue(searchQuery);
400
+ useEffect(() => {
401
+ editor.commands.setSearchQuery(deferredSearchQuery);
402
+ }, [editor, deferredSearchQuery]);
403
+ useExtension$1(useMemo(() => onSearchChange ? defineSearchStatusHandler(onSearchChange) : null, [onSearchChange]));
399
404
  useExtension$1(useMemo(() => {
400
405
  return readOnly ? defineReadonly() : null;
401
406
  }, [readOnly]));
@@ -469,6 +474,9 @@ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick,
469
474
  useExtension$1(useMemo(() => {
470
475
  return spellCheck == null ? null : defineSpellCheckPlugin(spellCheck);
471
476
  }, [spellCheck]));
477
+ useExtension$1(useMemo(() => {
478
+ return editorClassName ? defineViewAttributes({ class: editorClassName }) : null;
479
+ }, [editorClassName]));
472
480
  return null;
473
481
  }
474
482
 
@@ -905,7 +913,7 @@ function SelectionMenu({ onSelectionMenuSearch, context, onOpen, onClose, afford
905
913
  useExtension$1(useMemo(() => {
906
914
  return defineUpdateHandler((view) => {
907
915
  const { from, to, empty } = view.state.selection;
908
- const anchorable = !empty && isTextSelection(view.state.selection) && !getPendingReplacement(view.state);
916
+ const anchorable = !empty && isTextSelection(view.state.selection) && !getPendingReplacement(view.state) && getSearchStatus(view.state).active === 0;
909
917
  setSelection((previous) => {
910
918
  if (previous?.from === from && previous?.to === to && previous?.anchorable === anchorable) return previous;
911
919
  return {
@@ -1678,7 +1686,7 @@ function findHeadingPosition(doc, fragment) {
1678
1686
  });
1679
1687
  return match;
1680
1688
  }
1681
- function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution = true, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, ref, children }) {
1689
+ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution = true, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, searchQuery = "", onSearchChange, timeFormat, editorClassName, ref, children }) {
1682
1690
  const [editor] = useState(() => {
1683
1691
  const editor = createEditor({ extension: union(defineEditorExtension({
1684
1692
  resolveFileLink,
@@ -1788,6 +1796,12 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1788
1796
  function discardPendingReplacement() {
1789
1797
  editor.commands.discardPendingReplacement();
1790
1798
  }
1799
+ function findNext() {
1800
+ editor.commands.findNext();
1801
+ }
1802
+ function findPrevious() {
1803
+ editor.commands.findPrev();
1804
+ }
1791
1805
  return {
1792
1806
  getMarkdown,
1793
1807
  setMarkdown,
@@ -1806,6 +1820,8 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1806
1820
  appendPendingReplacementText,
1807
1821
  acceptPendingReplacement,
1808
1822
  discardPendingReplacement,
1823
+ findNext,
1824
+ findPrevious,
1809
1825
  editor
1810
1826
  };
1811
1827
  }, [
@@ -1824,10 +1840,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1824
1840
  return /* @__PURE__ */ jsxs(ProseKit, {
1825
1841
  editor,
1826
1842
  children: [
1827
- /* @__PURE__ */ jsx("div", {
1828
- ref: editor.mount,
1829
- className: clsx("meowdown-content", editorClassName)
1830
- }),
1843
+ /* @__PURE__ */ jsx("div", { ref: editor.mount }),
1831
1844
  /* @__PURE__ */ jsx(EditorExtensions, {
1832
1845
  markMode,
1833
1846
  onDocChange: handleDocChange,
@@ -1848,7 +1861,10 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1848
1861
  placeholder,
1849
1862
  readOnly,
1850
1863
  wikilinkEnabled: !!onWikilinkSearch,
1851
- spellCheck
1864
+ spellCheck,
1865
+ searchQuery,
1866
+ onSearchChange,
1867
+ editorClassName
1852
1868
  }),
1853
1869
  blockHandle && !readOnly && /* @__PURE__ */ jsx(BlockHandle, {}),
1854
1870
  !readOnly && /* @__PURE__ */ jsx(TableHandle, {}),
@@ -1890,7 +1906,7 @@ const CARET_GLIDE_OFF = { "--meowdown-caret-glide": "0ms" };
1890
1906
  *
1891
1907
  * Callbacks and resolvers should be stable; pass them via `useCallback`.
1892
1908
  */
1893
- function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste = true, linkPaste = true, bulletAfterHeading = false, substitution = true, frontmatter = false, blockHandle = true, caretGlide = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }) {
1909
+ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste = true, linkPaste = true, bulletAfterHeading = false, substitution = true, frontmatter = false, blockHandle = true, caretGlide = true, placeholder, readOnly, spellCheck, searchQuery, onSearchChange, timeFormat, editorClassName, wrapperClassName, handleRef, children }) {
1894
1910
  const childRef = useRef(null);
1895
1911
  useImperativeHandle(handleRef, () => {
1896
1912
  function getMarkdown() {
@@ -1952,6 +1968,12 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1952
1968
  function discardPendingReplacement() {
1953
1969
  childRef.current?.discardPendingReplacement();
1954
1970
  }
1971
+ function findNext() {
1972
+ childRef.current?.findNext();
1973
+ }
1974
+ function findPrevious() {
1975
+ childRef.current?.findPrevious();
1976
+ }
1955
1977
  return {
1956
1978
  getMarkdown,
1957
1979
  setMarkdown,
@@ -1970,6 +1992,8 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1970
1992
  appendPendingReplacementText,
1971
1993
  acceptPendingReplacement,
1972
1994
  discardPendingReplacement,
1995
+ findNext,
1996
+ findPrevious,
1973
1997
  get editor() {
1974
1998
  return childRef.current?.editor;
1975
1999
  }
@@ -2012,6 +2036,8 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
2012
2036
  placeholder,
2013
2037
  readOnly,
2014
2038
  spellCheck,
2039
+ searchQuery,
2040
+ onSearchChange,
2015
2041
  timeFormat,
2016
2042
  editorClassName,
2017
2043
  children
@@ -2477,48 +2503,54 @@ function renderInline(node, context) {
2477
2503
  marks: Mark.setFrom(marks)
2478
2504
  })), 0, context);
2479
2505
  }
2506
+ /** A collapsed list renders as an expanded one */
2507
+ function expandCollapsedList(node) {
2508
+ const attrs = node.attrs;
2509
+ if (!attrs.collapsed) return node;
2510
+ return node.type.create({
2511
+ ...attrs,
2512
+ collapsed: false
2513
+ }, node.content, node.marks);
2514
+ }
2515
+ function createTaskClickHandler(node, context) {
2516
+ const attrs = node.attrs;
2517
+ const { onTaskClick } = context;
2518
+ if (attrs.kind !== "task" || !onTaskClick) return void 0;
2519
+ const index = context.taskCounter.value++;
2520
+ const checked = attrs.checked === true;
2521
+ const marker = attrs.marker ?? null;
2522
+ const text = node.firstChild?.isTextblock ? node.firstChild.textContent.split("\n", 1)[0] ?? "" : "";
2523
+ return (event) => {
2524
+ event.preventDefault();
2525
+ onTaskClick({
2526
+ index,
2527
+ checked,
2528
+ marker,
2529
+ text,
2530
+ event: event.nativeEvent
2531
+ });
2532
+ };
2533
+ }
2534
+ function renderCodeBlock(node, key) {
2535
+ const attrs = node.attrs;
2536
+ const language = typeof attrs.language === "string" ? attrs.language : "";
2537
+ if (language === "math") return /* @__PURE__ */ jsx(MathCodeBlock, { code: node.textContent }, key);
2538
+ if (language === "mermaid") return /* @__PURE__ */ jsx(MermaidCodeBlock, { code: node.textContent }, key);
2539
+ return /* @__PURE__ */ jsx(CodeBlock, {
2540
+ code: node.textContent,
2541
+ language
2542
+ }, key);
2543
+ }
2480
2544
  function renderBlock(node, context) {
2481
2545
  if (context.referenceDefinitionNodes.has(node)) return null;
2482
2546
  const key = context.keyCounter.value++;
2483
2547
  const typeName = node.type.name;
2484
2548
  let handleTaskClick;
2485
2549
  if (typeName === "list") {
2486
- let attrs = node.attrs;
2487
- if (context.expandCollapsed && attrs.collapsed) {
2488
- attrs = {
2489
- ...attrs,
2490
- collapsed: false
2491
- };
2492
- node = node.type.create(attrs, node.content, node.marks);
2493
- }
2494
- const { onTaskClick } = context;
2495
- if (attrs.kind === "task" && onTaskClick) {
2496
- const index = context.taskCounter.value++;
2497
- const checked = attrs.checked === true;
2498
- const marker = attrs.marker ?? null;
2499
- const text = node.firstChild?.isTextblock ? node.firstChild.textContent.split("\n", 1)[0] ?? "" : "";
2500
- handleTaskClick = (event) => {
2501
- event.preventDefault();
2502
- onTaskClick({
2503
- index,
2504
- checked,
2505
- marker,
2506
- text,
2507
- event: event.nativeEvent
2508
- });
2509
- };
2510
- }
2511
- }
2512
- if (typeName === "codeBlock") {
2513
- const attrs = node.attrs;
2514
- const language = typeof attrs.language === "string" ? attrs.language : "";
2515
- if (language === "math") return /* @__PURE__ */ jsx(MathCodeBlock, { code: node.textContent }, key);
2516
- if (language === "mermaid") return /* @__PURE__ */ jsx(MermaidCodeBlock, { code: node.textContent }, key);
2517
- return /* @__PURE__ */ jsx(CodeBlock, {
2518
- code: node.textContent,
2519
- language
2520
- }, key);
2550
+ if (context.expandCollapsed) node = expandCollapsedList(node);
2551
+ handleTaskClick = createTaskClickHandler(node, context);
2521
2552
  }
2553
+ if (typeName === "codeBlock") return renderCodeBlock(node, key);
2522
2554
  const toDOM = node.type.spec.toDOM;
2523
2555
  if (node.isTextblock) {
2524
2556
  const inline = renderInline(node, context);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.58.2",
4
+ "version": "0.59.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  "github-slugger": "^2.0.0",
32
32
  "lucide-react": "^1.24.0",
33
33
  "react-property": "^2.0.2",
34
- "@meowdown/core": "0.58.2"
34
+ "@meowdown/core": "0.59.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": "^19.0.0",