@meowdown/react 0.47.0 → 0.49.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
@@ -56,6 +56,40 @@ See the full API reference [here](https://npmx.dev/package-docs/@meowdown%2Freac
56
56
 
57
57
  Slash menu host items can include `keywords` to match hidden terms without changing the displayed label.
58
58
 
59
+ ### Wiki embeds
60
+
61
+ Obsidian-style wiki embeds (`![[path]]`, with optional `|width` or
62
+ `|widthxheight`) stay literal and editable unless the host classifies them.
63
+ `resolveWikiEmbed` is a pure, creation-time resolver: return an image, file, or
64
+ note result to reuse the corresponding Meowdown atom and its existing click
65
+ hook; return `undefined` for missing or ambiguous targets.
66
+
67
+ ```tsx
68
+ <MeowdownEditor
69
+ initialMarkdown="![[assets/photo.png|320]]"
70
+ resolveWikiEmbed={({ target }) => (target.endsWith('.png') ? { kind: 'image' } : undefined)}
71
+ resolveImageUrl={resolveLocalImageUrl}
72
+ onImageClick={openLightbox}
73
+ />
74
+ ```
75
+
76
+ Use `EditorHandle.revealHeading('#Section%20name')` to move the selection to a
77
+ matching heading and scroll it into view after following a heading link.
78
+
79
+ Both `MeowdownEditor` and the read-only `MarkdownView` accept
80
+ `resolveFileLink`. Return `true` to render a standard `[label](path)` Markdown
81
+ link with the same file pill, metadata resolver, and click hook used by wiki
82
+ file embeds; return `false` to keep it as a normal link.
83
+
84
+ ```tsx
85
+ <MarkdownView
86
+ markdown="[Quarterly report](docs/report.pdf)"
87
+ resolveFileLink={({ href }) => href.startsWith('docs/')}
88
+ resolveFileInfo={resolveLocalFileInfo}
89
+ onFileClick={openLocalFile}
90
+ />
91
+ ```
92
+
59
93
  ### Wiki-link hover cards
60
94
 
61
95
  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, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikilinkClickHandler, WikilinkHoverHit } from "@meowdown/core";
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";
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
@@ -45,6 +45,12 @@ interface EditorHandle {
45
45
  * is mapped through the change.
46
46
  */
47
47
  setState: (markdown?: string, selection?: SelectionHint) => void;
48
+ /**
49
+ * Reparse the current Markdown with the editor's creation-time resolvers,
50
+ * preserving the selection without adding an undo-history entry. Hosts use
51
+ * this when data consulted by a stable resolver changes out of band.
52
+ */
53
+ refreshMarkdownRendering: () => void;
48
54
  /** Returns the current selection. */
49
55
  getSelection: () => SelectionJSON$1;
50
56
  /** Restores a selection with the same hint semantics as `setState`. */
@@ -53,6 +59,12 @@ interface EditorHandle {
53
59
  focus: () => void;
54
60
  /** Scrolls the selection into view. */
55
61
  scrollIntoView: () => void;
62
+ /**
63
+ * Moves the selection to the first heading matching a decoded `#fragment`
64
+ * (case-insensitive, with whitespace collapsed) and scrolls it into view.
65
+ * Returns `false` when no heading matches.
66
+ */
67
+ revealHeading: (fragment: string) => boolean;
56
68
  /**
57
69
  * The current selection as Markdown: block structure (list markers,
58
70
  * headings) is serialized, and inline syntax is already literal text. A
@@ -288,6 +300,12 @@ interface EditorProps {
288
300
  * changes are ignored, like `initialMarkdown`.
289
301
  */
290
302
  resolveFileLink?: FileLinkResolver;
303
+ /**
304
+ * Classifies `![[target]]` as an image, file, or note atom. Return
305
+ * `undefined` for missing or ambiguous targets to leave the source literal
306
+ * and editable. Must be pure and is read once when the editor is created.
307
+ */
308
+ resolveWikiEmbed?: WikiEmbedResolver;
291
309
  /**
292
310
  * Resolves the metadata (file size in bytes) shown on a file pill, directly
293
311
  * or as a promise; the pill renders immediately and fills the size in when
@@ -333,6 +351,11 @@ interface EditorProps {
333
351
  * Off by default.
334
352
  */
335
353
  bulletAfterHeading?: boolean;
354
+ /**
355
+ * Replaces typed character sequences with their typographic equivalents,
356
+ * e.g. `->` with `→` and `(c)` with `©`. On by default.
357
+ */
358
+ substitution?: boolean;
336
359
  /** Handles a leading `---` frontmatter block (off by default). */
337
360
  frontmatter?: boolean;
338
361
  /**
@@ -369,7 +392,7 @@ interface EditorProps {
369
392
  /** Nodes rendered inside the editor's ProseKit context. */
370
393
  children?: ReactNode;
371
394
  }
372
- declare function MeowdownEditor({ mode, initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, frontmatter, blockHandle, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }: EditorProps): import("react").JSX.Element;
395
+ 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, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }: EditorProps): import("react").JSX.Element;
373
396
  //#endregion
374
397
  //#region src/components/markdown-view.d.ts
375
398
  /** Payload for {@link TaskClickHandler}. */
@@ -409,7 +432,7 @@ interface MarkdownViewProps {
409
432
  /** Peel a leading YAML frontmatter block before rendering. Off by default. */
410
433
  frontmatter?: boolean;
411
434
  /**
412
- * Whether rendered links, images, and task checkboxes can be activated.
435
+ * Whether rendered links, images, file pills, and task checkboxes can be activated.
413
436
  * Defaults to `true`. When `false`, callbacks are ignored, the rendered tree
414
437
  * contains no anchors or focusable task controls, and recognized tweet and
415
438
  * YouTube embeds are omitted before any image resolver runs.
@@ -417,12 +440,23 @@ interface MarkdownViewProps {
417
440
  interactive?: boolean;
418
441
  /** Map an image `src` to a displayable URL, or `undefined` to skip it. */
419
442
  resolveImageUrl?: (src: string) => string | undefined;
443
+ /**
444
+ * Claim a `[label](url)` link as a file pill instead of a regular link.
445
+ * Must be pure; return `false` for links that should render normally.
446
+ */
447
+ resolveFileLink?: FileLinkResolver;
448
+ /** Classify `![[target]]` as an image, file, or note; unresolved source stays literal. */
449
+ resolveWikiEmbed?: WikiEmbedResolver;
450
+ /** Resolve metadata shown on a file pill. */
451
+ resolveFileInfo?: FileInfoResolver;
420
452
  /** Called when a rendered wikilink is clicked. Pass a stable function. */
421
453
  onWikilinkClick?: WikilinkClickHandler;
422
454
  /** Called when a rendered Markdown link is clicked. Pass a stable function. */
423
455
  onLinkClick?: LinkClickHandler;
424
456
  /** Called when a rendered image is clicked. Pass a stable function. */
425
457
  onImageClick?: ImageClickHandler;
458
+ /** Called when a rendered file pill is clicked. Pass a stable function. */
459
+ onFileClick?: FileClickHandler;
426
460
  /** Called when a rendered task checkbox is clicked. Pass a stable function. */
427
461
  onTaskClick?: TaskClickHandler;
428
462
  /** Extra class on the content root (alongside `ProseMirror meowdown-content`). */
@@ -436,10 +470,10 @@ interface MarkdownViewProps {
436
470
  * and CSS (the root carries `ProseMirror` + `data-mark-mode` so the existing
437
471
  * stylesheet applies). Requires a DOM environment.
438
472
  *
439
- * Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
473
+ * Callbacks (`onWikilinkClick`, etc.) and resolvers should be stable; pass them via
440
474
  * `useCallback` to avoid re-rendering the whole tree.
441
475
  */
442
- declare function MarkdownView({ markdown, markMode, frontmatter, interactive, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, onTaskClick, className }: MarkdownViewProps): ReactElement;
476
+ declare function MarkdownView({ markdown, markMode, frontmatter, interactive, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }: MarkdownViewProps): ReactElement;
443
477
  //#endregion
444
478
  //#region src/components/wikilink-hover-card.d.ts
445
479
  /** Props for {@link WikilinkHoverCard}. */
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { clsx } from "clsx/lite";
2
2
  import { Fragment, cloneElement, createElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
- import { buildFileMarkdown, codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSpellCheckPlugin, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, getPendingReplacement, getSelectedText, getTableColumnAlign, getVirtualElementFromRange, inlineTextToMarkChunks, isCodeBlockPreviewHiddenDecoration, isSelectionInTableCell, listenForTweetHeight, loadKaTeX, markdownToDoc, matchEmbed, renderMathInto } from "@meowdown/core";
3
+ import { buildFileMarkdown, codeBlockLanguages, 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, getVirtualElementFromRange, inlineTextToMarkChunks, isCodeBlockPreviewHiddenDecoration, 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";
@@ -295,7 +295,7 @@ function DropIndicator$1() {
295
295
 
296
296
  //#endregion
297
297
  //#region src/components/editor-extensions.tsx
298
- function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, placeholder, readOnly, wikilinkEnabled, spellCheck }) {
298
+ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, placeholder, readOnly, wikilinkEnabled, spellCheck }) {
299
299
  const editor = useEditor$1();
300
300
  useEffect(() => {
301
301
  editor.commands.setMarkMode(markMode);
@@ -358,6 +358,9 @@ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick,
358
358
  useExtension$1(useMemo(() => {
359
359
  return bulletAfterHeading ? defineBulletAfterHeading() : null;
360
360
  }, [bulletAfterHeading]));
361
+ useExtension$1(useMemo(() => {
362
+ return substitution ? defineSubstitution() : null;
363
+ }, [substitution]));
361
364
  useExtension$1(useMemo(() => {
362
365
  return placeholder ? definePlaceholder({
363
366
  placeholder,
@@ -1542,10 +1545,77 @@ function resolveSelection(doc, selection) {
1542
1545
  return TextSelection.between(doc.resolve(anchor), doc.resolve(head));
1543
1546
  }
1544
1547
  }
1545
- function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, ref, children }) {
1548
+ function decodeHeadingFragment(fragment) {
1549
+ const source = fragment.startsWith("#") ? fragment.slice(1) : fragment;
1550
+ try {
1551
+ return decodeURIComponent(source);
1552
+ } catch {
1553
+ return source;
1554
+ }
1555
+ }
1556
+ function headingLookupKey(value) {
1557
+ return value.normalize("NFKC").trim().replaceAll(/\s+/g, " ").toLowerCase();
1558
+ }
1559
+ const HEADING_HIDDEN_MARKS = /* @__PURE__ */ new Set([
1560
+ "mdMark",
1561
+ "mdLinkUri",
1562
+ "mdLinkTitle"
1563
+ ]);
1564
+ const HEADING_ATOM_MARKS = /* @__PURE__ */ new Set([
1565
+ "mdWikilink",
1566
+ "mdImage",
1567
+ "mdFile",
1568
+ "mdMath"
1569
+ ]);
1570
+ function headingAtomText(mark) {
1571
+ const attrs = mark.attrs;
1572
+ const stringAttr = (name) => {
1573
+ const value = attrs[name];
1574
+ return typeof value === "string" ? value : "";
1575
+ };
1576
+ if (mark.type.name === "mdWikilink") return stringAttr("display") || stringAttr("target");
1577
+ if (mark.type.name === "mdImage") return stringAttr("alt");
1578
+ if (mark.type.name === "mdFile") return stringAttr("name");
1579
+ if (mark.type.name === "mdMath") return stringAttr("formula");
1580
+ return "";
1581
+ }
1582
+ /** The heading as its live-preview marks display it, with syntax runs omitted. */
1583
+ function headingDisplayText(heading) {
1584
+ let output = "";
1585
+ let previousAtom;
1586
+ heading.forEach((child) => {
1587
+ if (!child.isText || !child.text) return;
1588
+ const atom = child.marks.find((mark) => HEADING_ATOM_MARKS.has(mark.type.name));
1589
+ if (atom) {
1590
+ if (!previousAtom?.eq(atom)) output += headingAtomText(atom);
1591
+ previousAtom = atom;
1592
+ return;
1593
+ }
1594
+ previousAtom = void 0;
1595
+ if (child.marks.some((mark) => HEADING_HIDDEN_MARKS.has(mark.type.name))) return;
1596
+ output += child.text;
1597
+ });
1598
+ return output;
1599
+ }
1600
+ function findHeadingPosition(doc, fragment) {
1601
+ const target = headingLookupKey(decodeHeadingFragment(fragment));
1602
+ if (!target) return;
1603
+ let match;
1604
+ doc.descendants((node, pos) => {
1605
+ if (match != null) return false;
1606
+ if (node.type.name === "heading" && (headingLookupKey(node.textContent) === target || headingLookupKey(headingDisplayText(node)) === target)) {
1607
+ match = pos + 1;
1608
+ return false;
1609
+ }
1610
+ return true;
1611
+ });
1612
+ return match;
1613
+ }
1614
+ 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 }) {
1546
1615
  const [editor] = useState(() => {
1547
1616
  const editor = createEditor({ extension: union(defineEditorExtension({
1548
1617
  resolveFileLink,
1618
+ resolveWikiEmbed,
1549
1619
  markMode
1550
1620
  }), defineCodeBlockView()) });
1551
1621
  if (initialMarkdown) editor.setContent(markdownToDoc(initialMarkdown, {
@@ -1581,7 +1651,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1581
1651
  function getState() {
1582
1652
  return [getMarkdown(), getSelection()];
1583
1653
  }
1584
- function setState(markdown, selection) {
1654
+ function replaceState(markdown, selection, addToHistory = true) {
1585
1655
  if (markdown == null && !selection) return;
1586
1656
  const transaction = editor.state.tr;
1587
1657
  if (markdown != null) {
@@ -1592,6 +1662,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1592
1662
  transaction.replaceWith(0, transaction.doc.content.size, doc.content);
1593
1663
  }
1594
1664
  if (selection) transaction.setSelection(resolveSelection(transaction.doc, selection)).scrollIntoView();
1665
+ if (!addToHistory) transaction.setMeta("addToHistory", false);
1595
1666
  suppressDocChangeRef.current = true;
1596
1667
  try {
1597
1668
  editor.view.dispatch(transaction);
@@ -1599,9 +1670,16 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1599
1670
  suppressDocChangeRef.current = false;
1600
1671
  }
1601
1672
  }
1673
+ function setState(markdown, selection) {
1674
+ replaceState(markdown, selection);
1675
+ }
1602
1676
  function setMarkdown(markdown) {
1603
1677
  setState(markdown);
1604
1678
  }
1679
+ function refreshMarkdownRendering() {
1680
+ const [markdown, selection] = getState();
1681
+ replaceState(markdown, selection, false);
1682
+ }
1605
1683
  function insertMarkdown(markdown) {
1606
1684
  editor.commands.insertMarkdown(markdown);
1607
1685
  }
@@ -1614,6 +1692,13 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1614
1692
  function scrollIntoView() {
1615
1693
  editor.commands.scrollIntoView();
1616
1694
  }
1695
+ function revealHeading(fragment) {
1696
+ const position = findHeadingPosition(editor.state.doc, fragment);
1697
+ if (position == null) return false;
1698
+ const selection = TextSelection.near(editor.state.doc.resolve(position));
1699
+ editor.view.dispatch(editor.state.tr.setSelection(selection).scrollIntoView());
1700
+ return true;
1701
+ }
1617
1702
  function getSelectedTextFromState() {
1618
1703
  return getSelectedText(editor.state);
1619
1704
  }
@@ -1639,10 +1724,12 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1639
1724
  insertMarkdown,
1640
1725
  getState,
1641
1726
  setState,
1727
+ refreshMarkdownRendering,
1642
1728
  getSelection,
1643
1729
  setSelection,
1644
1730
  focus,
1645
1731
  scrollIntoView,
1732
+ revealHeading,
1646
1733
  getSelectedText: getSelectedTextFromState,
1647
1734
  openSelectionMenu: openSelectionMenuFromHandle,
1648
1735
  startPendingReplacement,
@@ -1687,6 +1774,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1687
1774
  embedPaste,
1688
1775
  linkPaste,
1689
1776
  bulletAfterHeading,
1777
+ substitution,
1690
1778
  placeholder,
1691
1779
  readOnly,
1692
1780
  wikilinkEnabled: !!onWikilinkSearch,
@@ -1725,7 +1813,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1725
1813
 
1726
1814
  //#endregion
1727
1815
  //#region src/components/editor.tsx
1728
- function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste = true, linkPaste = true, bulletAfterHeading = false, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }) {
1816
+ 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, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }) {
1729
1817
  const childRef = useRef(null);
1730
1818
  useImperativeHandle(handleRef, () => {
1731
1819
  function getMarkdown() {
@@ -1747,6 +1835,9 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1747
1835
  function setState(markdown, selection) {
1748
1836
  childRef.current?.setState(markdown, selection);
1749
1837
  }
1838
+ function refreshMarkdownRendering() {
1839
+ childRef.current?.refreshMarkdownRendering();
1840
+ }
1750
1841
  function getSelection() {
1751
1842
  return childRef.current?.getSelection() ?? {
1752
1843
  type: "text",
@@ -1763,6 +1854,9 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1763
1854
  function scrollIntoView() {
1764
1855
  childRef.current?.scrollIntoView();
1765
1856
  }
1857
+ function revealHeading(fragment) {
1858
+ return childRef.current?.revealHeading(fragment) ?? false;
1859
+ }
1766
1860
  function getSelectedText() {
1767
1861
  return childRef.current?.getSelectedText() ?? "";
1768
1862
  }
@@ -1787,10 +1881,12 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1787
1881
  insertMarkdown,
1788
1882
  getState,
1789
1883
  setState,
1884
+ refreshMarkdownRendering,
1790
1885
  getSelection,
1791
1886
  setSelection,
1792
1887
  focus,
1793
1888
  scrollIntoView,
1889
+ revealHeading,
1794
1890
  getSelectedText,
1795
1891
  openSelectionMenu,
1796
1892
  startPendingReplacement,
@@ -1823,6 +1919,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1823
1919
  onExitBoundary,
1824
1920
  resolveImageUrl,
1825
1921
  resolveFileLink,
1922
+ resolveWikiEmbed,
1826
1923
  resolveFileInfo,
1827
1924
  onFileClick,
1828
1925
  onFilePaste,
@@ -1831,6 +1928,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1831
1928
  embedPaste,
1832
1929
  linkPaste,
1833
1930
  bulletAfterHeading,
1931
+ substitution,
1834
1932
  frontmatter,
1835
1933
  blockHandle,
1836
1934
  placeholder,
@@ -2034,6 +2132,74 @@ function ImageView(props) {
2034
2132
  })]
2035
2133
  });
2036
2134
  }
2135
+ function FileView(props) {
2136
+ const { href, name, context, children } = props;
2137
+ const resolveFileInfo = context.resolveFileInfo;
2138
+ const [resolvedSize, setResolvedSize] = useState();
2139
+ useEffect(() => {
2140
+ if (!resolveFileInfo) return;
2141
+ let active = true;
2142
+ const load = async () => {
2143
+ try {
2144
+ const info = await resolveFileInfo(href);
2145
+ if (!active || info?.size == null || !Number.isFinite(info.size) || info.size < 0) return;
2146
+ setResolvedSize({
2147
+ href,
2148
+ resolver: resolveFileInfo,
2149
+ text: formatFileSize(info.size)
2150
+ });
2151
+ } catch (error) {
2152
+ console.error("[meowdown] resolveFileInfo failed:", error);
2153
+ }
2154
+ };
2155
+ load();
2156
+ return () => {
2157
+ active = false;
2158
+ };
2159
+ }, [resolveFileInfo, href]);
2160
+ const size = resolvedSize?.href === href && resolvedSize.resolver === resolveFileInfo ? resolvedSize.text : "";
2161
+ const handleClick = context.onFileClick ? (event) => context.onFileClick?.({
2162
+ href,
2163
+ name,
2164
+ event: event.nativeEvent
2165
+ }) : void 0;
2166
+ return /* @__PURE__ */ jsxs("span", {
2167
+ className: "md-file-view md-atom-view",
2168
+ children: [/* @__PURE__ */ jsxs("span", {
2169
+ className: "md-file-view-preview md-atom-view-preview",
2170
+ "data-testid": "file-pill",
2171
+ "data-file-kind": getFileKind(href),
2172
+ contentEditable: false,
2173
+ title: name,
2174
+ onClick: handleClick,
2175
+ children: [
2176
+ /* @__PURE__ */ jsxs("svg", {
2177
+ className: "md-file-view-icon",
2178
+ viewBox: "0 0 24 24",
2179
+ "aria-hidden": "true",
2180
+ fill: "none",
2181
+ stroke: "currentColor",
2182
+ strokeWidth: "2",
2183
+ strokeLinecap: "round",
2184
+ strokeLinejoin: "round",
2185
+ children: [/* @__PURE__ */ jsx("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }), /* @__PURE__ */ jsx("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })]
2186
+ }),
2187
+ /* @__PURE__ */ jsx("span", {
2188
+ className: "md-file-view-name",
2189
+ children: name
2190
+ }),
2191
+ /* @__PURE__ */ jsx("span", {
2192
+ className: "md-file-view-size",
2193
+ "data-testid": "file-pill-size",
2194
+ children: size
2195
+ })
2196
+ ]
2197
+ }), /* @__PURE__ */ jsx("span", {
2198
+ className: "md-file-view-content md-atom-view-content",
2199
+ children
2200
+ })]
2201
+ });
2202
+ }
2037
2203
  function renderTokens(code, tokens) {
2038
2204
  const out = [];
2039
2205
  let pos = 0;
@@ -2135,6 +2301,15 @@ function wrapMark(mark, children, context) {
2135
2301
  children
2136
2302
  });
2137
2303
  }
2304
+ case "mdFile": {
2305
+ const attrs = mark.attrs;
2306
+ return /* @__PURE__ */ jsx(FileView, {
2307
+ href: attrs.href,
2308
+ name: attrs.name,
2309
+ context,
2310
+ children
2311
+ });
2312
+ }
2138
2313
  case "mdMath": {
2139
2314
  const attrs = mark.attrs;
2140
2315
  return /* @__PURE__ */ jsx(MathView, {
@@ -2195,7 +2370,10 @@ function renderRuns(runs, depth, context) {
2195
2370
  function renderInline(node, context) {
2196
2371
  const text = node.textContent;
2197
2372
  if (!text) return null;
2198
- return renderRuns(inlineTextToMarkChunks(getMarkBuilders(), text).map(([from, to, marks]) => ({
2373
+ return renderRuns(inlineTextToMarkChunks(getMarkBuilders(), text, {
2374
+ resolveFileLink: context.resolveFileLink,
2375
+ resolveWikiEmbed: context.resolveWikiEmbed
2376
+ }).map(([from, to, marks]) => ({
2199
2377
  text: text.slice(from, to),
2200
2378
  marks: Mark.setFrom(marks)
2201
2379
  })), 0, context);
@@ -2251,18 +2429,22 @@ function renderBlock(node, context) {
2251
2429
  * and CSS (the root carries `ProseMirror` + `data-mark-mode` so the existing
2252
2430
  * stylesheet applies). Requires a DOM environment.
2253
2431
  *
2254
- * Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
2432
+ * Callbacks (`onWikilinkClick`, etc.) and resolvers should be stable; pass them via
2255
2433
  * `useCallback` to avoid re-rendering the whole tree.
2256
2434
  */
2257
- function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, onTaskClick, className }) {
2435
+ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }) {
2258
2436
  const content = useMemo(() => {
2259
2437
  const doc = markdownToDoc(markdown, { frontmatter });
2260
2438
  const context = {
2261
2439
  interactive,
2262
2440
  resolveImageUrl,
2441
+ resolveFileLink,
2442
+ resolveWikiEmbed,
2443
+ resolveFileInfo,
2263
2444
  onWikilinkClick: interactive ? onWikilinkClick : void 0,
2264
2445
  onLinkClick: interactive ? onLinkClick : void 0,
2265
2446
  onImageClick: interactive ? onImageClick : void 0,
2447
+ onFileClick: interactive ? onFileClick : void 0,
2266
2448
  onTaskClick: interactive ? onTaskClick : void 0,
2267
2449
  taskCounter: { value: 0 },
2268
2450
  keyCounter: { value: 0 }
@@ -2273,9 +2455,13 @@ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, intera
2273
2455
  frontmatter,
2274
2456
  interactive,
2275
2457
  resolveImageUrl,
2458
+ resolveFileLink,
2459
+ resolveWikiEmbed,
2460
+ resolveFileInfo,
2276
2461
  onWikilinkClick,
2277
2462
  onLinkClick,
2278
2463
  onImageClick,
2464
+ onFileClick,
2279
2465
  onTaskClick
2280
2466
  ]);
2281
2467
  return /* @__PURE__ */ jsx("div", {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.47.0",
4
+ "version": "0.49.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -24,9 +24,9 @@
24
24
  "@prosekit/pm": "^0.1.19-beta.3",
25
25
  "@prosekit/react": "^0.8.0-beta.21",
26
26
  "clsx": "^2.1.1",
27
- "lucide-react": "^1.21.0",
27
+ "lucide-react": "^1.24.0",
28
28
  "react-property": "^2.0.2",
29
- "@meowdown/core": "0.47.0"
29
+ "@meowdown/core": "0.49.0"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "react": "^19.0.0",
@@ -41,17 +41,17 @@
41
41
  }
42
42
  },
43
43
  "devDependencies": {
44
- "@css-modules-kit/codegen": "^1.3.0",
44
+ "@css-modules-kit/codegen": "^1.4.0",
45
45
  "@ocavue/tsconfig": "^0.7.1",
46
- "@tsdown/css": "^0.22.3",
46
+ "@tsdown/css": "^0.22.7",
47
47
  "@types/react": "^19.2.17",
48
48
  "@types/react-dom": "^19.2.3",
49
- "@vitest/browser-playwright": "^4.1.9",
49
+ "@vitest/browser-playwright": "^4.1.10",
50
50
  "dedent": "^1.7.2",
51
51
  "react": "^19.2.7",
52
52
  "react-dom": "^19.2.7",
53
- "tsdown": "^0.22.3",
54
- "vitest": "^4.1.9",
53
+ "tsdown": "^0.22.7",
54
+ "vitest": "^4.1.10",
55
55
  "vitest-browser-commands": "^0.2.1",
56
56
  "vitest-browser-react": "^2.2.0",
57
57
  "@meowdown/vitest": "0.0.0"