@meowdown/react 0.68.3 → 0.70.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
@@ -66,6 +66,7 @@ Common `MeowdownEditor` props:
66
66
  | `onFilePaste` | Persist pasted or dropped files |
67
67
  | `onSlashMenuSearch` / `onTagSearch` / `onWikilinkSearch` / `onSelectionMenuSearch` | Search menus for `/`, `#`, `[[`, and selection commands |
68
68
  | `readOnly` / `placeholder` / `blockHandle` / `caretGlide` / `embedPaste` / `linkPaste` / `bulletAfterHeading` | Behavior toggles |
69
+ | `CodeBlockView` | Custom React node view component for code blocks |
69
70
 
70
71
  Every prop, callback, and `EditorHandle` method is documented in the [API reference](https://npmx.dev/package-docs/@meowdown%2Freact/).
71
72
 
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { ReactElement, ReactNode, Ref } from "react";
2
- import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileInfoResolver, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, LinkPreview, LinkPreviewResolver, LinkPreviewResolver as LinkPreviewResolver$1, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, SearchStatusHandler, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikiEmbedResolver, WikilinkClickHandler, WikilinkHoverHit, WikilinkResolver } from "@meowdown/core";
2
+ import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileInfoResolver, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, LinkPreview, LinkPreviewResolver, LinkPreviewResolver as LinkPreviewResolver$1, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, SearchStatusHandler, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikiEmbedResolver, WikilinkClickHandler, WikilinkHoverHit, WikilinkResolver, XPostResolver, XPostResolver as XPostResolver$1, YouTubeVideoResolver, YouTubeVideoResolver as YouTubeVideoResolver$1 } from "@meowdown/core";
3
3
  import { SelectionJSON, SelectionJSON as SelectionJSON$1 } from "@prosekit/core";
4
- import { useEditor, useExtension, useKeymap } from "@prosekit/react";
4
+ import { ReactNodeViewComponent, ReactNodeViewComponent as ReactNodeViewComponent$1, useEditor, useExtension, useKeymap } from "@prosekit/react";
5
5
  //#region src/utils/date-format.d.ts
6
6
  type TimeFormat = '12' | '24';
7
7
  //#endregion
@@ -408,6 +408,20 @@ interface EditorProps {
408
408
  * (e.g. from `useCallback`).
409
409
  */
410
410
  resolveFileInfo?: FileViewOptions['resolveFileInfo'];
411
+ /**
412
+ * Resolves the data behind an X post URL, directly or as a promise; the post
413
+ * renders as a `post-embed-x-post` card. Defaults to `defaultResolveXPost`,
414
+ * which fetches through react-tweet's hosted proxy. Pass a stable function
415
+ * (e.g. from `useCallback`).
416
+ */
417
+ resolveXPost?: XPostResolver$1;
418
+ /**
419
+ * Resolves the data behind a YouTube video URL, directly or as a promise;
420
+ * the video renders as a `post-embed-youtube-video` card. Defaults to
421
+ * `defaultResolveYouTubeVideo`, which reads YouTube's oEmbed endpoint. Pass
422
+ * a stable function (e.g. from `useCallback`).
423
+ */
424
+ resolveYouTubeVideo?: YouTubeVideoResolver$1;
411
425
  /**
412
426
  * Called when the user clicks a rendered file pill (or presses `Mod-Enter`
413
427
  * with the caret on one), with its `href`, `name`, and the originating
@@ -509,6 +523,11 @@ interface EditorProps {
509
523
  * Class on the outer `.meowdown` wrapper div.
510
524
  */
511
525
  wrapperClassName?: string;
526
+ /**
527
+ * React component that renders code blocks in place of the built-in one.
528
+ * `false` disables the React code block view.
529
+ */
530
+ CodeBlockView?: ReactNodeViewComponent$1 | false | undefined;
512
531
  /**
513
532
  * Imperative handle for the editor.
514
533
  */
@@ -524,7 +543,7 @@ interface EditorProps {
524
543
  *
525
544
  * Callbacks and resolvers should be stable; pass them via `useCallback`.
526
545
  */
527
- export declare function MeowdownEditor({ mode, initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, frontmatter, blockHandle, caretGlide, placeholder, readOnly, spellCheck, searchQuery, onSearchChange, timeFormat, editorClassName, wrapperClassName, handleRef, children }: EditorProps): ReactElement;
546
+ export declare function MeowdownEditor({ mode, initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, resolveYouTubeVideo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, frontmatter, blockHandle, caretGlide, placeholder, readOnly, spellCheck, searchQuery, onSearchChange, timeFormat, editorClassName, wrapperClassName, CodeBlockView, handleRef, children }: EditorProps): ReactElement;
528
547
  //#endregion
529
548
  //#region src/components/markdown-view.d.ts
530
549
  /**
@@ -613,6 +632,16 @@ interface MarkdownViewProps {
613
632
  * Resolve metadata shown on a file pill.
614
633
  */
615
634
  resolveFileInfo?: FileInfoResolver;
635
+ /**
636
+ * Resolve the data behind an X post URL, rendered as a `post-embed-x-post`
637
+ * card. Defaults to `defaultResolveXPost`.
638
+ */
639
+ resolveXPost?: XPostResolver$1;
640
+ /**
641
+ * Resolve the data behind a YouTube video URL, rendered as a
642
+ * `post-embed-youtube-video` card. Defaults to `defaultResolveYouTubeVideo`.
643
+ */
644
+ resolveYouTubeVideo?: YouTubeVideoResolver$1;
616
645
  /**
617
646
  * Called when a rendered wikilink is clicked. Pass a stable function.
618
647
  */
@@ -649,7 +678,7 @@ interface MarkdownViewProps {
649
678
  * Callbacks (`onWikilinkClick`, etc.) and resolvers should be stable; pass them via
650
679
  * `useCallback` to avoid re-rendering the whole tree.
651
680
  */
652
- export declare function MarkdownView({ markdown, markMode, frontmatter, interactive, expandCollapsed, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }: MarkdownViewProps): ReactElement;
681
+ export declare function MarkdownView({ markdown, markMode, frontmatter, interactive, expandCollapsed, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, resolveYouTubeVideo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }: MarkdownViewProps): ReactElement;
653
682
  //#endregion
654
683
  //#region src/components/wikilink-hover-card.d.ts
655
684
  /**
@@ -675,4 +704,4 @@ interface WikilinkHoverCardProps {
675
704
  */
676
705
  export declare function WikilinkHoverCard({ children, className }: WikilinkHoverCardProps): ReactNode;
677
706
  //#endregion
678
- export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, type LinkPreview, type LinkPreviewResolver, type MarkdownViewProps, type PendingReplacementResolveHandler, type SelectionHint, type SelectionJSON, type SelectionMenuContext, type SelectionMenuItem, type SelectionMenuSearchHandler, type SlashMenuItem, type SlashMenuSearchHandler, type TagItem, type TagSearchHandler, type TaskClickHandler, type TaskClickPayload, type TimeFormat, type WikilinkHoverCardProps, type WikilinkItem, type WikilinkSearchHandler, useEditor, useExtension, useKeymap };
707
+ export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, type LinkPreview, type LinkPreviewResolver, type MarkdownViewProps, type PendingReplacementResolveHandler, type ReactNodeViewComponent, type SelectionHint, type SelectionJSON, type SelectionMenuContext, type SelectionMenuItem, type SelectionMenuSearchHandler, type SlashMenuItem, type SlashMenuSearchHandler, type TagItem, type TagSearchHandler, type TaskClickHandler, type TaskClickPayload, type TimeFormat, type WikilinkHoverCardProps, type WikilinkItem, type WikilinkSearchHandler, type XPostResolver, type YouTubeVideoResolver, useEditor, useExtension, useKeymap };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { clsx } from "clsx/lite";
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, defineSearchStatusHandler, defineSubstitution, defineTagClickHandler, defineViewAttributes, defineVirtualCaret, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, docToMarkdown, formatFileSize, getCodeTokens, getFileKind, getLinkText, getMarkBuilders, getPendingReplacement, getSearchStatus, getSelectedText, getTableColumnAlign, getTextblockDisplayText, getVirtualElementFromRange, inlineTextToMarkChunksWithContext, isCodeBlockPreviewHiddenDecoration, isLinkTextForHref, isModEvent, isNodeOfType, isSelectionInTableCell, listenForTweetHeight, loadKaTeX, markdownToDoc, matchEmbed, normalizeHref, renderMathInto } from "@meowdown/core";
2
+ import { Fragment, cloneElement, createElement, memo, useCallback, useDeferredValue, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { buildFileMarkdown, codeBlockLanguages, collectReferenceDefinitions, defaultResolveImageUrl, defaultResolveXPost, defaultResolveYouTubeVideo, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSearchStatusHandler, defineSubstitution, defineTagClickHandler, defineViewAttributes, defineVirtualCaret, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, docToMarkdown, formatFileSize, getCodeTokens, getFileKind, getLinkText, getMarkBuilders, getPendingReplacement, getSearchStatus, getSelectedText, getTableColumnAlign, getTextblockDisplayText, getVirtualElementFromRange, inlineTextToMarkChunksWithContext, isCodeBlockPreviewHiddenDecoration, isLinkTextForHref, isModEvent, isNodeOfType, isReferenceDefinitionNode, isSelectionInTableCell, loadKaTeX, markdownToDoc, matchPostEmbed, normalizeHref, parsePostEmbedSnapshot, 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";
@@ -15,6 +15,8 @@ import { Popover } from "@base-ui/react/popover";
15
15
  import { AutocompleteEmpty, AutocompleteItem, AutocompletePopup, AutocompletePositioner, AutocompleteRoot } from "@prosekit/react/autocomplete";
16
16
  import { MenuItem, MenuPopup, MenuPositioner } from "@prosekit/react/menu";
17
17
  import { TableHandleColumnMenuRoot, TableHandleColumnMenuTrigger, TableHandleColumnPopup, TableHandleColumnPositioner, TableHandleDragPreview, TableHandleDropIndicator, TableHandleRoot, TableHandleRowMenuRoot, TableHandleRowMenuTrigger, TableHandleRowPopup, TableHandleRowPositioner } from "@prosekit/react/table-handle";
18
+ import { registerXPost } from "@post-embed/elements/x";
19
+ import { registerYouTubeVideo } from "@post-embed/elements/youtube";
18
20
  import { Mark } from "@prosekit/pm/model";
19
21
  import { BOOLEAN, OVERLOADED_BOOLEAN, getPropertyInfo, possibleStandardNames } from "react-property";
20
22
  import { PreviewCard } from "@base-ui/react/preview-card";
@@ -353,11 +355,11 @@ function CodeBlockToolbar({ code, language, setLanguage }) {
353
355
 
354
356
  //#endregion
355
357
  //#region src/extensions/code-block-view.ts
356
- function defineCodeBlockView() {
358
+ function defineCodeBlockView(component = CodeBlockView) {
357
359
  return union(defineReactNodeView({
358
360
  name: "codeBlock",
359
361
  contentAs: "code",
360
- component: CodeBlockView
362
+ component
361
363
  }), defineCodeBlockPreviewPlugin());
362
364
  }
363
365
 
@@ -401,7 +403,7 @@ function DropIndicator$1() {
401
403
 
402
404
  //#endregion
403
405
  //#region src/components/editor-extensions.tsx
404
- 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 }) {
406
+ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, resolveFileInfo, resolveXPost, resolveYouTubeVideo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, placeholder, readOnly, wikilinkEnabled, spellCheck, searchQuery, onSearchChange, editorClassName }) {
405
407
  const editor = useEditor$1();
406
408
  useLayoutEffect(() => {
407
409
  editor.commands.setMarkMode(markMode);
@@ -458,8 +460,16 @@ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick,
458
460
  return onExitBoundary ? defineExitBoundaryHandler(onExitBoundary) : null;
459
461
  }, [onExitBoundary]));
460
462
  useExtension$1(useMemo(() => {
461
- return defineImage({ resolveImageUrl });
462
- }, [resolveImageUrl]));
463
+ return defineImage({
464
+ resolveImageUrl,
465
+ resolveXPost,
466
+ resolveYouTubeVideo
467
+ });
468
+ }, [
469
+ resolveImageUrl,
470
+ resolveXPost,
471
+ resolveYouTubeVideo
472
+ ]));
463
473
  useExtension$1(useMemo(() => {
464
474
  return defineFileView({ resolveFileInfo });
465
475
  }, [resolveFileInfo]));
@@ -1925,7 +1935,7 @@ function findHeadingPosition(doc, fragment) {
1925
1935
  });
1926
1936
  return match;
1927
1937
  }
1928
- function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution = true, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, searchQuery = "", onSearchChange, timeFormat, editorClassName, ref, children }) {
1938
+ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, resolveYouTubeVideo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution = true, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, searchQuery = "", onSearchChange, timeFormat, editorClassName, CodeBlockView, ref, children }) {
1929
1939
  const [editor] = useState(() => {
1930
1940
  const baseExtension = defineEditorExtension({
1931
1941
  resolveFileLink,
@@ -1933,7 +1943,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1933
1943
  resolveWikilink,
1934
1944
  markMode
1935
1945
  });
1936
- const extension = union(baseExtension, defineCodeBlockView());
1946
+ const extension = CodeBlockView === false ? baseExtension : union(baseExtension, defineCodeBlockView(CodeBlockView));
1937
1947
  const editor = createEditor({ extension });
1938
1948
  if (initialMarkdown) editor.setContent(markdownToDoc(initialMarkdown, {
1939
1949
  nodes: editor.nodes,
@@ -2092,6 +2102,8 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
2092
2102
  onExitBoundary,
2093
2103
  resolveImageUrl,
2094
2104
  resolveFileInfo,
2105
+ resolveXPost,
2106
+ resolveYouTubeVideo,
2095
2107
  onFileClick,
2096
2108
  onFilePaste,
2097
2109
  onFileSaveError,
@@ -2150,7 +2162,7 @@ const CARET_GLIDE_OFF = { "--meowdown-caret-glide": "0ms" };
2150
2162
  *
2151
2163
  * Callbacks and resolvers should be stable; pass them via `useCallback`.
2152
2164
  */
2153
- function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, 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 }) {
2165
+ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, resolveLinkPreview, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, resolveYouTubeVideo, 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, CodeBlockView, handleRef, children }) {
2154
2166
  const childRef = useRef(null);
2155
2167
  useImperativeHandle(handleRef, () => {
2156
2168
  function getMarkdown() {
@@ -2269,6 +2281,8 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
2269
2281
  resolveWikiEmbed,
2270
2282
  resolveWikilink,
2271
2283
  resolveFileInfo,
2284
+ resolveXPost,
2285
+ resolveYouTubeVideo,
2272
2286
  onFileClick,
2273
2287
  onFilePaste,
2274
2288
  onFileSaveError,
@@ -2286,6 +2300,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
2286
2300
  onSearchChange,
2287
2301
  timeFormat,
2288
2302
  editorClassName,
2303
+ CodeBlockView,
2289
2304
  children
2290
2305
  })
2291
2306
  });
@@ -2419,44 +2434,45 @@ function WikilinkChip(props) {
2419
2434
  })]
2420
2435
  });
2421
2436
  }
2422
- function EmbedFrame(props) {
2423
- const { embed, width, height } = props;
2424
- const iframeRef = useRef(null);
2425
- useEffect(() => {
2426
- if (embed.kind !== "tweet") return;
2427
- const iframe = iframeRef.current;
2428
- if (!iframe) return;
2429
- return listenForTweetHeight(iframe);
2430
- }, [embed.kind, embed.key]);
2431
- const youtubeWidth = embed.kind === "youtube" ? width : null;
2432
- const tweetHeight = embed.kind === "tweet" ? height : null;
2437
+ function PostEmbed(props) {
2438
+ const { kind, src, width, snapshot, resolveXPost, resolveYouTubeVideo } = props;
2439
+ registerXPost();
2440
+ registerYouTubeVideo();
2441
+ const saved = snapshot == null ? void 0 : parsePostEmbedSnapshot(snapshot);
2433
2442
  return /* @__PURE__ */ jsx("span", {
2434
2443
  className: "md-image-view-preview md-atom-view-preview",
2435
2444
  contentEditable: false,
2436
- children: /* @__PURE__ */ jsx("iframe", {
2437
- ref: iframeRef,
2438
- src: embed.src,
2439
- title: embed.title,
2440
- className: embed.className,
2441
- "data-testid": embed.testid,
2442
- loading: "lazy",
2443
- referrerPolicy: "strict-origin-when-cross-origin",
2444
- frameBorder: "0",
2445
- allow: embed.allow,
2446
- allowFullScreen: embed.allowFullscreen,
2447
- style: youtubeWidth != null ? { width: youtubeWidth } : tweetHeight != null ? { height: tweetHeight } : void 0,
2448
- "data-sized": tweetHeight == null ? void 0 : ""
2449
- }, embed.key)
2445
+ "data-testid": `${kind}-embed`,
2446
+ children: kind === "x-post" ? createElement("post-embed-x-post", {
2447
+ data: saved?.kind === "x-post" ? saved.data : null,
2448
+ url: src,
2449
+ resolver: resolveXPost
2450
+ }) : createElement("post-embed-youtube-video", {
2451
+ data: saved?.kind === "youtube-video" ? saved.data : null,
2452
+ url: src,
2453
+ resolver: resolveYouTubeVideo,
2454
+ playback: "inline",
2455
+ style: width == null ? void 0 : {
2456
+ width,
2457
+ maxWidth: "none"
2458
+ }
2459
+ })
2450
2460
  });
2451
2461
  }
2452
2462
  function ImagePreview(props) {
2453
- const { src, alt, width, height, resolveImageUrl, onImageClick, interactive } = props;
2454
- const embed = matchEmbed(src);
2455
- if (embed) return interactive ? /* @__PURE__ */ jsx(EmbedFrame, {
2456
- embed,
2457
- width,
2458
- height
2459
- }) : null;
2463
+ const { src, alt, width, snapshot, resolveImageUrl, resolveXPost, resolveYouTubeVideo, onImageClick, interactive } = props;
2464
+ const kind = matchPostEmbed(src);
2465
+ if (kind) {
2466
+ if (!interactive) return null;
2467
+ return /* @__PURE__ */ jsx(PostEmbed, {
2468
+ kind,
2469
+ src,
2470
+ width,
2471
+ snapshot,
2472
+ resolveXPost: resolveXPost ?? defaultResolveXPost,
2473
+ resolveYouTubeVideo: resolveYouTubeVideo ?? defaultResolveYouTubeVideo
2474
+ }, src);
2475
+ }
2460
2476
  const url = (resolveImageUrl ?? defaultResolveImageUrl)(src);
2461
2477
  if (!url) return null;
2462
2478
  return /* @__PURE__ */ jsx("span", {
@@ -2480,15 +2496,17 @@ function ImagePreview(props) {
2480
2496
  });
2481
2497
  }
2482
2498
  function ImageView(props) {
2483
- const { src, alt, width, height, context, children } = props;
2499
+ const { src, alt, width, snapshot, context, children } = props;
2484
2500
  return /* @__PURE__ */ jsxs("span", {
2485
2501
  className: "md-image-view md-atom-view",
2486
2502
  children: [/* @__PURE__ */ jsx(ImagePreview, {
2487
2503
  src,
2488
2504
  alt,
2489
2505
  width,
2490
- height,
2506
+ snapshot,
2491
2507
  resolveImageUrl: context.resolveImageUrl,
2508
+ resolveXPost: context.resolveXPost,
2509
+ resolveYouTubeVideo: context.resolveYouTubeVideo,
2492
2510
  onImageClick: context.onImageClick,
2493
2511
  interactive: context.interactive
2494
2512
  }), /* @__PURE__ */ jsx("span", {
@@ -2684,7 +2702,7 @@ function wrapMark(mark, children, context) {
2684
2702
  src: attrs.src,
2685
2703
  alt: attrs.alt,
2686
2704
  width: attrs.width,
2687
- height: attrs.height,
2705
+ snapshot: attrs.snapshot,
2688
2706
  context,
2689
2707
  children
2690
2708
  });
@@ -2766,10 +2784,7 @@ function renderInline(node, context) {
2766
2784
  resolveFileLink: context.resolveFileLink,
2767
2785
  resolveWikiEmbed: context.resolveWikiEmbed,
2768
2786
  resolveWikilink: context.resolveWikilink
2769
- }, {
2770
- referenceDefinitions: context.referenceDefinitions,
2771
- isReferenceDefinition: context.referenceDefinitionNodes.has(node)
2772
- }).map(([from, to, marks]) => ({
2787
+ }, { referenceDefinitions: context.referenceDefinitions }).map(([from, to, marks]) => ({
2773
2788
  text: text.slice(from, to),
2774
2789
  marks: Mark.setFrom(marks)
2775
2790
  })), 0, context);
@@ -2814,8 +2829,8 @@ function renderCodeBlock(node, key) {
2814
2829
  language
2815
2830
  }, key);
2816
2831
  }
2817
- function renderBlock(node, context) {
2818
- if (context.referenceDefinitionNodes.has(node)) return null;
2832
+ function renderBlock(node, context, parent, index) {
2833
+ if (isReferenceDefinitionNode(node, parent, index)) return null;
2819
2834
  const key = context.keyCounter.value++;
2820
2835
  const typeName = node.type.name;
2821
2836
  let handleTaskClick;
@@ -2829,11 +2844,64 @@ function renderBlock(node, context) {
2829
2844
  const inline = renderInline(node, context);
2830
2845
  return toDOM ? outputSpecToReact(toDOM(node), inline, context) : /* @__PURE__ */ jsx(Fragment, { children: inline }, key);
2831
2846
  }
2832
- const children = node.content.content.map((child) => renderBlock(child, context));
2847
+ const children = node.content.content.map((child, childIndex) => {
2848
+ return renderBlock(child, context, node, childIndex);
2849
+ });
2833
2850
  const reactNode = toDOM ? outputSpecToReact(toDOM(node), children, context) : /* @__PURE__ */ jsx(Fragment, { children }, key);
2834
2851
  if (typeName === "list" && handleTaskClick && typeof reactNode !== "string" && reactNode != null) return cloneElement(reactNode, { onClick: handleTaskClick });
2835
2852
  return reactNode;
2836
2853
  }
2854
+ function isTaskList(node) {
2855
+ return isNodeOfType(node, "list") && node.attrs.kind === "task";
2856
+ }
2857
+ /**
2858
+ * Checkboxes a block renders, in the same pre-order `renderBlock` walks, so a
2859
+ * block's first checkbox index is the sum over the blocks before it.
2860
+ */
2861
+ function countTaskItems(node) {
2862
+ let count = isTaskList(node) ? 1 : 0;
2863
+ node.descendants((child) => {
2864
+ if (isTaskList(child)) count++;
2865
+ return true;
2866
+ });
2867
+ return count;
2868
+ }
2869
+ /**
2870
+ * The effective definitions as one string, so blocks can compare them by
2871
+ * content: `collectReferenceDefinitions` builds a new map on every parse.
2872
+ */
2873
+ function definitionsSignature(definitions) {
2874
+ let signature = "";
2875
+ for (const { key, href, title } of definitions.values()) signature += `${key}\0${href}\0${title}\n`;
2876
+ return signature;
2877
+ }
2878
+ function splitBlocks(doc) {
2879
+ const blocks = [];
2880
+ let taskBase = 0;
2881
+ for (const node of doc.content.content) {
2882
+ blocks.push({
2883
+ node,
2884
+ taskBase
2885
+ });
2886
+ taskBase += countTaskItems(node);
2887
+ }
2888
+ return blocks;
2889
+ }
2890
+ /**
2891
+ * One top-level block. Memoized on the block's own content (`Node.eq`), its
2892
+ * first checkbox index, the shared props object, and the definitions'
2893
+ * content, so a growing document re-renders only the block that changed.
2894
+ */
2895
+ const MarkdownBlock = memo(function MarkdownBlock({ node, taskBase, context, referenceDefinitions }) {
2896
+ return renderBlock(node, {
2897
+ ...context,
2898
+ referenceDefinitions,
2899
+ taskCounter: { value: taskBase },
2900
+ keyCounter: { value: 0 }
2901
+ }, null, 0);
2902
+ }, (previous, next) => {
2903
+ return previous.taskBase === next.taskBase && previous.context === next.context && previous.definitionsKey === next.definitionsKey && (previous.node === next.node || previous.node.eq(next.node));
2904
+ });
2837
2905
  /**
2838
2906
  * Render Markdown to a read-only React tree that looks exactly like the editor
2839
2907
  * in `hide` mark mode: inline marks, wikilink chips, images, tweet/YouTube
@@ -2845,32 +2913,8 @@ function renderBlock(node, context) {
2845
2913
  * Callbacks (`onWikilinkClick`, etc.) and resolvers should be stable; pass them via
2846
2914
  * `useCallback` to avoid re-rendering the whole tree.
2847
2915
  */
2848
- function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, expandCollapsed = false, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }) {
2849
- const content = useMemo(() => {
2850
- const doc = markdownToDoc(markdown, { frontmatter });
2851
- const referenceIndex = collectReferenceDefinitions(doc);
2852
- const context = {
2853
- interactive,
2854
- expandCollapsed,
2855
- resolveImageUrl,
2856
- resolveFileLink,
2857
- resolveWikiEmbed,
2858
- resolveWikilink,
2859
- resolveFileInfo,
2860
- onWikilinkClick: interactive ? onWikilinkClick : void 0,
2861
- onLinkClick: interactive ? onLinkClick : void 0,
2862
- onImageClick: interactive ? onImageClick : void 0,
2863
- onFileClick: interactive ? onFileClick : void 0,
2864
- onTaskClick: interactive ? onTaskClick : void 0,
2865
- referenceDefinitions: referenceIndex.definitions,
2866
- referenceDefinitionNodes: referenceIndex.nodes,
2867
- taskCounter: { value: 0 },
2868
- keyCounter: { value: 0 }
2869
- };
2870
- return doc.content.content.map((node) => renderBlock(node, context));
2871
- }, [
2872
- markdown,
2873
- frontmatter,
2916
+ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, expandCollapsed = false, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveWikilink, resolveFileInfo, resolveXPost, resolveYouTubeVideo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }) {
2917
+ const context = useMemo(() => ({
2874
2918
  interactive,
2875
2919
  expandCollapsed,
2876
2920
  resolveImageUrl,
@@ -2878,16 +2922,48 @@ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, intera
2878
2922
  resolveWikiEmbed,
2879
2923
  resolveWikilink,
2880
2924
  resolveFileInfo,
2925
+ resolveXPost,
2926
+ resolveYouTubeVideo,
2927
+ onWikilinkClick: interactive ? onWikilinkClick : void 0,
2928
+ onLinkClick: interactive ? onLinkClick : void 0,
2929
+ onImageClick: interactive ? onImageClick : void 0,
2930
+ onFileClick: interactive ? onFileClick : void 0,
2931
+ onTaskClick: interactive ? onTaskClick : void 0
2932
+ }), [
2933
+ interactive,
2934
+ expandCollapsed,
2935
+ resolveImageUrl,
2936
+ resolveFileLink,
2937
+ resolveWikiEmbed,
2938
+ resolveWikilink,
2939
+ resolveFileInfo,
2940
+ resolveXPost,
2941
+ resolveYouTubeVideo,
2881
2942
  onWikilinkClick,
2882
2943
  onLinkClick,
2883
2944
  onImageClick,
2884
2945
  onFileClick,
2885
2946
  onTaskClick
2886
2947
  ]);
2948
+ const { blocks, referenceDefinitions, definitionsKey } = useMemo(() => {
2949
+ const doc = markdownToDoc(markdown, { frontmatter });
2950
+ const referenceDefinitions = collectReferenceDefinitions(doc).definitions;
2951
+ return {
2952
+ blocks: splitBlocks(doc),
2953
+ referenceDefinitions,
2954
+ definitionsKey: definitionsSignature(referenceDefinitions)
2955
+ };
2956
+ }, [markdown, frontmatter]);
2887
2957
  return /* @__PURE__ */ jsx("div", {
2888
2958
  className: clsx("ProseMirror", "meowdown-content", className),
2889
2959
  "data-mark-mode": markMode,
2890
- children: content
2960
+ children: blocks.map(({ node, taskBase }, index) => /* @__PURE__ */ jsx(MarkdownBlock, {
2961
+ node,
2962
+ taskBase,
2963
+ context,
2964
+ referenceDefinitions,
2965
+ definitionsKey
2966
+ }, index))
2891
2967
  });
2892
2968
  }
2893
2969
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.68.3",
4
+ "version": "0.70.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -23,6 +23,8 @@
23
23
  "dependencies": {
24
24
  "@base-ui/react": "^1.7.0",
25
25
  "@ocavue/utils": "^1.8.0",
26
+ "@post-embed/elements": "^0.2.0",
27
+ "@post-embed/types": "^0.2.0",
26
28
  "@prosekit/core": "^0.13.2",
27
29
  "@prosekit/pm": "^0.1.19",
28
30
  "@prosekit/react": "^0.8.2",
@@ -31,7 +33,7 @@
31
33
  "github-slugger": "^2.0.0",
32
34
  "lucide-react": "^1.38.0",
33
35
  "react-property": "^2.0.2",
34
- "@meowdown/core": "0.68.3"
36
+ "@meowdown/core": "0.70.0"
35
37
  },
36
38
  "peerDependencies": {
37
39
  "react": "^19.0.0",
@@ -49,12 +51,12 @@
49
51
  "@css-modules-kit/codegen": "^1.4.0",
50
52
  "@ocavue/tsconfig": "^0.7.1",
51
53
  "@tsdown/css": "^0.23.0-rc.1",
52
- "@types/react": "^19.2.18",
53
- "@types/react-dom": "^19.2.5",
54
+ "@types/react": "^19.3.0",
55
+ "@types/react-dom": "^19.3.0",
54
56
  "@vitest/browser-playwright": "^5.0.0",
55
57
  "dedent": "^1.7.2",
56
- "react": "^19.2.8",
57
- "react-dom": "^19.2.8",
58
+ "react": "^19.3.0",
59
+ "react-dom": "^19.3.0",
58
60
  "tsdown": "^0.23.0-rc.1",
59
61
  "vitest": "^5.0.0",
60
62
  "vitest-browser-commands": "^0.4.1",