@meowdown/react 0.58.3 → 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, defineViewAttributes, 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, editorClassName }) {
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]));
@@ -908,7 +913,7 @@ function SelectionMenu({ onSelectionMenuSearch, context, onOpen, onClose, afford
908
913
  useExtension$1(useMemo(() => {
909
914
  return defineUpdateHandler((view) => {
910
915
  const { from, to, empty } = view.state.selection;
911
- 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;
912
917
  setSelection((previous) => {
913
918
  if (previous?.from === from && previous?.to === to && previous?.anchorable === anchorable) return previous;
914
919
  return {
@@ -1681,7 +1686,7 @@ function findHeadingPosition(doc, fragment) {
1681
1686
  });
1682
1687
  return match;
1683
1688
  }
1684
- 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 }) {
1685
1690
  const [editor] = useState(() => {
1686
1691
  const editor = createEditor({ extension: union(defineEditorExtension({
1687
1692
  resolveFileLink,
@@ -1791,6 +1796,12 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1791
1796
  function discardPendingReplacement() {
1792
1797
  editor.commands.discardPendingReplacement();
1793
1798
  }
1799
+ function findNext() {
1800
+ editor.commands.findNext();
1801
+ }
1802
+ function findPrevious() {
1803
+ editor.commands.findPrev();
1804
+ }
1794
1805
  return {
1795
1806
  getMarkdown,
1796
1807
  setMarkdown,
@@ -1809,6 +1820,8 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1809
1820
  appendPendingReplacementText,
1810
1821
  acceptPendingReplacement,
1811
1822
  discardPendingReplacement,
1823
+ findNext,
1824
+ findPrevious,
1812
1825
  editor
1813
1826
  };
1814
1827
  }, [
@@ -1849,6 +1862,8 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1849
1862
  readOnly,
1850
1863
  wikilinkEnabled: !!onWikilinkSearch,
1851
1864
  spellCheck,
1865
+ searchQuery,
1866
+ onSearchChange,
1852
1867
  editorClassName
1853
1868
  }),
1854
1869
  blockHandle && !readOnly && /* @__PURE__ */ jsx(BlockHandle, {}),
@@ -1891,7 +1906,7 @@ const CARET_GLIDE_OFF = { "--meowdown-caret-glide": "0ms" };
1891
1906
  *
1892
1907
  * Callbacks and resolvers should be stable; pass them via `useCallback`.
1893
1908
  */
1894
- 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 }) {
1895
1910
  const childRef = useRef(null);
1896
1911
  useImperativeHandle(handleRef, () => {
1897
1912
  function getMarkdown() {
@@ -1953,6 +1968,12 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1953
1968
  function discardPendingReplacement() {
1954
1969
  childRef.current?.discardPendingReplacement();
1955
1970
  }
1971
+ function findNext() {
1972
+ childRef.current?.findNext();
1973
+ }
1974
+ function findPrevious() {
1975
+ childRef.current?.findPrevious();
1976
+ }
1956
1977
  return {
1957
1978
  getMarkdown,
1958
1979
  setMarkdown,
@@ -1971,6 +1992,8 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1971
1992
  appendPendingReplacementText,
1972
1993
  acceptPendingReplacement,
1973
1994
  discardPendingReplacement,
1995
+ findNext,
1996
+ findPrevious,
1974
1997
  get editor() {
1975
1998
  return childRef.current?.editor;
1976
1999
  }
@@ -2013,6 +2036,8 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
2013
2036
  placeholder,
2014
2037
  readOnly,
2015
2038
  spellCheck,
2039
+ searchQuery,
2040
+ onSearchChange,
2016
2041
  timeFormat,
2017
2042
  editorClassName,
2018
2043
  children
@@ -2478,48 +2503,54 @@ function renderInline(node, context) {
2478
2503
  marks: Mark.setFrom(marks)
2479
2504
  })), 0, context);
2480
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
+ }
2481
2544
  function renderBlock(node, context) {
2482
2545
  if (context.referenceDefinitionNodes.has(node)) return null;
2483
2546
  const key = context.keyCounter.value++;
2484
2547
  const typeName = node.type.name;
2485
2548
  let handleTaskClick;
2486
2549
  if (typeName === "list") {
2487
- let attrs = node.attrs;
2488
- if (context.expandCollapsed && attrs.collapsed) {
2489
- attrs = {
2490
- ...attrs,
2491
- collapsed: false
2492
- };
2493
- node = node.type.create(attrs, node.content, node.marks);
2494
- }
2495
- const { onTaskClick } = context;
2496
- if (attrs.kind === "task" && onTaskClick) {
2497
- const index = context.taskCounter.value++;
2498
- const checked = attrs.checked === true;
2499
- const marker = attrs.marker ?? null;
2500
- const text = node.firstChild?.isTextblock ? node.firstChild.textContent.split("\n", 1)[0] ?? "" : "";
2501
- handleTaskClick = (event) => {
2502
- event.preventDefault();
2503
- onTaskClick({
2504
- index,
2505
- checked,
2506
- marker,
2507
- text,
2508
- event: event.nativeEvent
2509
- });
2510
- };
2511
- }
2512
- }
2513
- if (typeName === "codeBlock") {
2514
- const attrs = node.attrs;
2515
- const language = typeof attrs.language === "string" ? attrs.language : "";
2516
- if (language === "math") return /* @__PURE__ */ jsx(MathCodeBlock, { code: node.textContent }, key);
2517
- if (language === "mermaid") return /* @__PURE__ */ jsx(MermaidCodeBlock, { code: node.textContent }, key);
2518
- return /* @__PURE__ */ jsx(CodeBlock, {
2519
- code: node.textContent,
2520
- language
2521
- }, key);
2550
+ if (context.expandCollapsed) node = expandCollapsedList(node);
2551
+ handleTaskClick = createTaskClickHandler(node, context);
2522
2552
  }
2553
+ if (typeName === "codeBlock") return renderCodeBlock(node, key);
2523
2554
  const toDOM = node.type.spec.toDOM;
2524
2555
  if (node.isTextblock) {
2525
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.3",
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.3"
34
+ "@meowdown/core": "0.59.0"
35
35
  },
36
36
  "peerDependencies": {
37
37
  "react": "^19.0.0",