@meowdown/react 0.20.0 → 0.21.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
@@ -42,7 +42,7 @@ The Markdown editor component. Renders inside a `div.meowdown` wrapper that fill
42
42
  - `initialMarkdown?: string`: first render only.
43
43
  - `onDocChange?: VoidFunction`: called on every user-driven document change. Programmatic `setMarkdown` / `setState` on the handle do not fire it.
44
44
  - `onTagSearch?: (query: string) => TagItem[] | Promise<TagItem[]>`: enables the tag menu, which opens when typing `#` followed by text in a rich mode. Returns ranked rows `{ tag, label?, detail?, onSelect? }` (the menu does not re-sort). Selecting a row inserts `#tag ` then runs its `onSelect`. Omit to disable.
45
- - `onWikilinkSearch?: (query: string) => WikilinkItem[] | Promise<WikilinkItem[]>`: enables the wikilink menu, which opens as soon as `[[` or `@` is typed in a rich mode. Returns ranked rows `{ target, label?, detail?, onSelect? }` (the menu does not re-sort). Selecting a row inserts `[[target]]` then runs its `onSelect`. Omit to disable.
45
+ - `onWikilinkSearch?: (query: string) => WikilinkItem[] | Promise<WikilinkItem[]>`: enables the wikilink menu, which opens as soon as `[[` or `@` is typed, or `Mod-Shift-k` is pressed, in a rich mode; the shortcut seeds the query from any selected text. Returns ranked rows `{ target, label?, detail?, onSelect? }` (the menu does not re-sort). Selecting a row inserts `[[target]]` then runs its `onSelect`. Omit to disable.
46
46
  - `onWikilinkClick?: (payload: { target: string; event: MouseEvent }) => void`: called when a rendered wiki link is clicked. A plain click inside a link the caret already sits in just places the caret; `Mod`-click always fires. Pass a stable function (e.g. from `useCallback`). Ignored in source mode.
47
47
  - `onLinkClick?: (payload: { href: string; event: MouseEvent }) => void`: called with its `href` when a rendered Markdown link (`[text](url)`) is clicked. A plain click inside a link the caret already sits in just places the caret; `Mod`-click always fires. Pass a stable function (e.g. from `useCallback`). Ignored in source mode.
48
48
  - `resolveImageUrl?: (src: string) => string | undefined`: maps an image `src` to a displayable URL (or `undefined` to skip). Enables inline image rendering: `![alt](src)` stays literal text and the image renders beneath its line. Pass a stable function. Ignored in source mode.
@@ -60,6 +60,27 @@ The Markdown editor component. Renders inside a `div.meowdown` wrapper that fill
60
60
  - `handleRef?: Ref<EditorHandle>`
61
61
  - `children?: ReactNode`: rendered inside the editor's ProseKit context, so children can call `useEditor()`. Only rendered in the rich modes; source mode ignores them.
62
62
 
63
+ ### `<MarkdownView>`
64
+
65
+ A read-only renderer that turns Markdown into a React tree styled exactly like `<MeowdownEditor>` in `hide` mode: inline marks, wiki-link chips, images, tweet/YouTube embeds, and syntax-highlighted code. It mounts no editor and holds no ProseMirror view, so it is cheap to render many times (backlink lists, search results, previews). Import `@meowdown/core/style.css` for the theme; the root carries the `ProseMirror` and `meowdown-content` classes plus `data-mark-mode`, so the editor stylesheet applies unchanged.
66
+
67
+ ```tsx
68
+ import '@meowdown/core/style.css'
69
+ import { MarkdownView } from '@meowdown/react'
70
+ ;<MarkdownView
71
+ markdown="See [[Some Note]] and **bold**."
72
+ onWikilinkClick={({ target }) => open(target)}
73
+ />
74
+ ```
75
+
76
+ - `markdown: string`: the Markdown to render. Live: changing it re-renders.
77
+ - `markMode?: 'hide' | 'focus' | 'show'`: defaults to `'hide'`.
78
+ - `frontmatter?: boolean`: peel a leading YAML frontmatter block first. Off by default.
79
+ - `resolveImageUrl?`, `onWikilinkClick?`, `onLinkClick?`, `onImageClick?`: the same shapes as the matching `<MeowdownEditor>` props. Pass stable functions.
80
+ - `className?: string`: extra class on the content root.
81
+
82
+ Code blocks render their content and syntax highlighting (the same `tok-*` classes as the editor) but without the editor's language picker / copy toolbar. `<MarkdownView>` requires a DOM environment; it is not a server-side HTML-string renderer.
83
+
63
84
  ### `useEditor`
64
85
 
65
86
  Re-exported from `@prosekit/react`. Call it from a component passed as `children` to read the live editor instance.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { ReactNode, Ref } from "react";
1
+ import { ReactElement, ReactNode, Ref } from "react";
2
2
  import { ImageClickHandler, ImageOptions, LinkClickHandler, MarkMode, PlaceholderOptions, TypedEditor, WikilinkClickHandler } from "@meowdown/core";
3
3
  import { SelectionJSON, SelectionJSON as SelectionJSON$1 } from "@prosekit/core";
4
4
  import { useEditor, useExtension, useKeymap } from "@prosekit/react";
@@ -211,4 +211,45 @@ declare function MeowdownEditor({
211
211
  children
212
212
  }: EditorProps): import("react").JSX.Element;
213
213
  //#endregion
214
- export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, MeowdownEditor, type SelectionHint, type SelectionJSON, type TagItem, type TagSearchHandler, type WikilinkItem, type WikilinkSearchHandler, useEditor, useExtension, useKeymap };
214
+ //#region src/components/markdown-view.d.ts
215
+ interface MarkdownViewProps {
216
+ /** The Markdown to render. Live: changing it re-renders the content. */
217
+ markdown: string;
218
+ /** Mark mode for the read-only view. Defaults to `'hide'`. */
219
+ markMode?: MarkMode;
220
+ /** Peel a leading YAML frontmatter block before rendering. Off by default. */
221
+ frontmatter?: boolean;
222
+ /** Map an image `src` to a displayable URL, or `undefined` to skip it. */
223
+ resolveImageUrl?: (src: string) => string | undefined;
224
+ /** Called when a rendered wiki link is clicked. Pass a stable function. */
225
+ onWikilinkClick?: WikilinkClickHandler;
226
+ /** Called when a rendered Markdown link is clicked. Pass a stable function. */
227
+ onLinkClick?: LinkClickHandler;
228
+ /** Called when a rendered image is clicked. Pass a stable function. */
229
+ onImageClick?: ImageClickHandler;
230
+ /** Extra class on the content root (alongside `ProseMirror meowdown-content`). */
231
+ className?: string;
232
+ }
233
+ /**
234
+ * Render Markdown to a read-only React tree that looks exactly like the editor
235
+ * in `hide` mark mode: inline marks, wiki-link chips, images, tweet/YouTube
236
+ * embeds, and syntax-highlighted code. No editor, no ProseMirror view; just a
237
+ * walk over `markdownToDoc`'s document reusing meowdown's own parse, mark logic,
238
+ * and CSS (the root carries `ProseMirror` + `data-mark-mode` so the existing
239
+ * stylesheet applies). Requires a DOM environment.
240
+ *
241
+ * Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
242
+ * `useCallback` to avoid re-rendering the whole tree.
243
+ */
244
+ declare function MarkdownView({
245
+ markdown,
246
+ markMode,
247
+ frontmatter,
248
+ resolveImageUrl,
249
+ onWikilinkClick,
250
+ onLinkClick,
251
+ onImageClick,
252
+ className
253
+ }: MarkdownViewProps): ReactElement;
254
+ //#endregion
255
+ export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, MarkdownView, type MarkdownViewProps, MeowdownEditor, type SelectionHint, type SelectionJSON, type TagItem, type TagSearchHandler, type WikilinkItem, type WikilinkSearchHandler, useEditor, useExtension, useKeymap };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { clsx } from "clsx/lite";
2
- import { useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { Fragment, createElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { defaultKeymap, history, historyKeymap } from "@codemirror/commands";
4
4
  import { markdown, markdownLanguage } from "@codemirror/lang-markdown";
5
5
  import { defaultHighlightStyle, syntaxHighlighting } from "@codemirror/language";
@@ -7,7 +7,7 @@ import { Compartment, EditorSelection, EditorState } from "@codemirror/state";
7
7
  import { EditorView, keymap } from "@codemirror/view";
8
8
  import { clamp } from "@ocavue/utils";
9
9
  import { jsx, jsxs } from "react/jsx-runtime";
10
- import { codeBlockLanguages, defineBulletAfterHeading, defineEditorExtension, defineEmbedPaste, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineMarkMode, defineMarkdownCopy, definePlaceholder, defineReadonly, defineWikilinkClickHandler, docToMarkdown, markdownToDoc } from "@meowdown/core";
10
+ import { codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineEditorExtension, defineEmbedPaste, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineMarkMode, defineMarkdownCopy, definePlaceholder, defineReadonly, defineWikilinkClickHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, inlineTextToMarkChunks, listenForTweetHeight, markdownToDoc, matchEmbed } from "@meowdown/core";
11
11
  import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, union } from "@prosekit/core";
12
12
  import { Selection, TextSelection } from "@prosekit/pm/state";
13
13
  import { ProseKit, defineReactNodeView, useEditor, useEditor as useEditor$1, useEditorDerivedValue, useExtension, useExtension as useExtension$1, useKeymap } from "@prosekit/react";
@@ -18,6 +18,7 @@ import { DropIndicator } from "@prosekit/react/drop-indicator";
18
18
  import { AutocompleteEmpty, AutocompleteItem, AutocompletePopup, AutocompletePositioner, AutocompleteRoot } from "@prosekit/react/autocomplete";
19
19
  import { MenuItem, MenuPopup, MenuPositioner } from "@prosekit/react/menu";
20
20
  import { TableHandleColumnMenuRoot, TableHandleColumnMenuTrigger, TableHandleColumnPopup, TableHandleColumnPositioner, TableHandleDragPreview, TableHandleDropIndicator, TableHandleRoot, TableHandleRowMenuRoot, TableHandleRowMenuTrigger, TableHandleRowPopup, TableHandleRowPositioner } from "@prosekit/react/table-handle";
21
+ import { Mark } from "@prosekit/pm/model";
21
22
 
22
23
  //#region src/components/codemirror-editor.tsx
23
24
  function resolveSelection$1(selection, docLength) {
@@ -319,7 +320,7 @@ function DropIndicator$1() {
319
320
 
320
321
  //#endregion
321
322
  //#region src/components/editor-extensions.tsx
322
- function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste, bulletAfterHeading, placeholder, readOnly }) {
323
+ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, resolveImageUrl, onImagePaste, onImageSaveError, onImageClick, embedPaste, bulletAfterHeading, placeholder, readOnly, wikilinkEnabled }) {
323
324
  useExtension$1(useMemo(() => {
324
325
  return defineMarkMode(markMode);
325
326
  }, [markMode]));
@@ -363,6 +364,9 @@ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick,
363
364
  strategy: "doc"
364
365
  }) : null;
365
366
  }, [placeholder]));
367
+ useExtension$1(useMemo(() => {
368
+ return wikilinkEnabled ? defineWikilinkTrigger() : null;
369
+ }, [wikilinkEnabled]));
366
370
  return null;
367
371
  }
368
372
 
@@ -859,7 +863,8 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onTa
859
863
  embedPaste,
860
864
  bulletAfterHeading,
861
865
  placeholder,
862
- readOnly
866
+ readOnly,
867
+ wikilinkEnabled: !!onWikilinkSearch
863
868
  }),
864
869
  blockHandle && !readOnly && /* @__PURE__ */ jsx(BlockHandle, {}),
865
870
  !readOnly && /* @__PURE__ */ jsx(TableHandle, {}),
@@ -958,4 +963,306 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onTagSea
958
963
  }
959
964
 
960
965
  //#endregion
961
- export { MeowdownEditor, useEditor, useExtension, useKeymap };
966
+ //#region src/components/dom-output-spec.tsx
967
+ const ATTR_NAME_MAP = {
968
+ class: "className",
969
+ contenteditable: "contentEditable",
970
+ colspan: "colSpan",
971
+ rowspan: "rowSpan",
972
+ for: "htmlFor",
973
+ tabindex: "tabIndex",
974
+ viewbox: "viewBox"
975
+ };
976
+ function toReactProps(attrs, key) {
977
+ const props = { key };
978
+ if (!attrs) return props;
979
+ for (const [name, value] of Object.entries(attrs)) {
980
+ if (name === "style") continue;
981
+ props[ATTR_NAME_MAP[name] ?? name] = value;
982
+ }
983
+ return props;
984
+ }
985
+ /**
986
+ * Convert a ProseMirror `DOMOutputSpec` into a React node, substituting `content`
987
+ * for the spec's content hole (`0`). Reused for every node/mark spec the static
988
+ * walker does not special-case, so blocks and plain marks render off their real
989
+ * `toDOM`, exactly as the editor serializes them.
990
+ */
991
+ function outputSpecToReact(spec, content, key = 0) {
992
+ if (typeof spec === "string") return spec;
993
+ if (!Array.isArray(spec)) return null;
994
+ const array = spec;
995
+ const tag = array[0];
996
+ let childStart = 1;
997
+ let attrs;
998
+ const second = array[1];
999
+ if (second != null && second !== 0 && typeof second === "object" && !Array.isArray(second)) {
1000
+ attrs = second;
1001
+ childStart = 2;
1002
+ }
1003
+ const props = toReactProps(attrs, key);
1004
+ const rest = array.slice(childStart);
1005
+ if (rest.length === 0) return createElement(tag, props);
1006
+ return createElement(tag, props, ...rest.map((child, index) => child === 0 ? /* @__PURE__ */ jsx(Fragment, { children: content }, index) : outputSpecToReact(child, content, index)));
1007
+ }
1008
+
1009
+ //#endregion
1010
+ //#region src/components/markdown-view.tsx
1011
+ function WikilinkChip(props) {
1012
+ const { target, display, onWikilinkClick, children } = props;
1013
+ return /* @__PURE__ */ jsxs("span", {
1014
+ className: "md-wikilink-view",
1015
+ children: [/* @__PURE__ */ jsx("span", { children }), /* @__PURE__ */ jsx("span", {
1016
+ className: "md-wikilink-label",
1017
+ "data-testid": "wikilink",
1018
+ contentEditable: false,
1019
+ onClick: onWikilinkClick ? (event) => onWikilinkClick({
1020
+ target,
1021
+ event: event.nativeEvent
1022
+ }) : void 0,
1023
+ children: display || target
1024
+ })]
1025
+ });
1026
+ }
1027
+ function EmbedFrame({ embed }) {
1028
+ const iframeRef = useRef(null);
1029
+ useEffect(() => {
1030
+ if (embed.kind !== "tweet") return;
1031
+ const iframe = iframeRef.current;
1032
+ if (!iframe) return;
1033
+ return listenForTweetHeight(iframe);
1034
+ }, [embed.kind, embed.key]);
1035
+ return /* @__PURE__ */ jsx("span", {
1036
+ className: "md-image-preview md-image-preview-embed",
1037
+ contentEditable: false,
1038
+ children: /* @__PURE__ */ jsx("iframe", {
1039
+ ref: iframeRef,
1040
+ src: embed.src,
1041
+ title: embed.title,
1042
+ className: embed.className,
1043
+ "data-testid": embed.testid,
1044
+ loading: "lazy",
1045
+ referrerPolicy: "strict-origin-when-cross-origin",
1046
+ frameBorder: "0",
1047
+ allow: embed.allow,
1048
+ allowFullScreen: embed.allowFullscreen
1049
+ }, embed.key)
1050
+ });
1051
+ }
1052
+ function ImagePreview(props) {
1053
+ const { src, alt, resolveImageUrl, onImageClick } = props;
1054
+ const embed = matchEmbed(src);
1055
+ if (embed) return /* @__PURE__ */ jsx(EmbedFrame, { embed });
1056
+ const url = (resolveImageUrl ?? defaultResolveImageUrl)(src);
1057
+ if (!url) return null;
1058
+ return /* @__PURE__ */ jsx("span", {
1059
+ className: "md-image-preview md-image-preview-img",
1060
+ "data-testid": "image-preview",
1061
+ contentEditable: false,
1062
+ children: /* @__PURE__ */ jsx("img", {
1063
+ src: url,
1064
+ alt,
1065
+ draggable: false,
1066
+ onClick: onImageClick ? (event) => onImageClick({
1067
+ src,
1068
+ alt,
1069
+ event: event.nativeEvent
1070
+ }) : void 0
1071
+ })
1072
+ });
1073
+ }
1074
+ function ImageView(props) {
1075
+ const { src, alt, context, children } = props;
1076
+ return /* @__PURE__ */ jsxs("span", {
1077
+ className: "md-image-view",
1078
+ children: [/* @__PURE__ */ jsx("span", {
1079
+ className: "md-image-view-content",
1080
+ children
1081
+ }), /* @__PURE__ */ jsx(ImagePreview, {
1082
+ src,
1083
+ alt,
1084
+ resolveImageUrl: context.resolveImageUrl,
1085
+ onImageClick: context.onImageClick
1086
+ })]
1087
+ });
1088
+ }
1089
+ function renderTokens(code, tokens) {
1090
+ const out = [];
1091
+ let pos = 0;
1092
+ let index = 0;
1093
+ for (const [from, to, classes] of tokens) {
1094
+ if (from > pos) out.push(/* @__PURE__ */ jsx(Fragment, { children: code.slice(pos, from) }, `gap-${index}`));
1095
+ out.push(/* @__PURE__ */ jsx("span", {
1096
+ className: classes,
1097
+ children: code.slice(from, to)
1098
+ }, index));
1099
+ pos = to;
1100
+ index++;
1101
+ }
1102
+ if (pos < code.length) out.push(/* @__PURE__ */ jsx(Fragment, { children: code.slice(pos) }, "tail"));
1103
+ return out;
1104
+ }
1105
+ function CodeBlock({ code, language }) {
1106
+ const syncTokens = useMemo(() => {
1107
+ const result = getCodeTokens(code, language);
1108
+ return Array.isArray(result) ? result : null;
1109
+ }, [code, language]);
1110
+ const [asyncTokens, setAsyncTokens] = useState(null);
1111
+ useEffect(() => {
1112
+ if (syncTokens) return;
1113
+ let active = true;
1114
+ const result = getCodeTokens(code, language);
1115
+ if (!Array.isArray(result)) result.then((loaded) => {
1116
+ if (active) setAsyncTokens(loaded);
1117
+ });
1118
+ return () => {
1119
+ active = false;
1120
+ };
1121
+ }, [
1122
+ code,
1123
+ language,
1124
+ syncTokens
1125
+ ]);
1126
+ const tokens = syncTokens ?? asyncTokens ?? [];
1127
+ return /* @__PURE__ */ jsx("pre", {
1128
+ "data-language": language || void 0,
1129
+ children: /* @__PURE__ */ jsx("code", { children: tokens.length > 0 ? renderTokens(code, tokens) : code })
1130
+ });
1131
+ }
1132
+ /** Wrap inline `children` in one mark, special-casing the view/link marks. */
1133
+ function wrapMark(mark, children, context) {
1134
+ switch (mark.type.name) {
1135
+ case "mdWikilinkView": {
1136
+ const attrs = mark.attrs;
1137
+ return /* @__PURE__ */ jsx(WikilinkChip, {
1138
+ target: attrs.target,
1139
+ display: attrs.display,
1140
+ onWikilinkClick: context.onWikilinkClick,
1141
+ children
1142
+ });
1143
+ }
1144
+ case "mdImageView": {
1145
+ const attrs = mark.attrs;
1146
+ return /* @__PURE__ */ jsx(ImageView, {
1147
+ src: attrs.src,
1148
+ alt: attrs.alt,
1149
+ context,
1150
+ children
1151
+ });
1152
+ }
1153
+ case "mdLinkText": {
1154
+ const attrs = mark.attrs;
1155
+ const handleClick = context.onLinkClick ? (event) => context.onLinkClick?.({
1156
+ href: attrs.href,
1157
+ event: event.nativeEvent
1158
+ }) : void 0;
1159
+ return /* @__PURE__ */ jsx("a", {
1160
+ className: "md-link",
1161
+ href: attrs.href,
1162
+ onClick: handleClick,
1163
+ children
1164
+ });
1165
+ }
1166
+ default: {
1167
+ const toDOM = mark.type.spec.toDOM;
1168
+ if (!toDOM) return children;
1169
+ return outputSpecToReact(toDOM(mark, true), children);
1170
+ }
1171
+ }
1172
+ }
1173
+ /**
1174
+ * Render a run of inline pieces, sharing a parent element across adjacent pieces
1175
+ * that have the same mark at `depth`. This mirrors ProseMirror's DOM
1176
+ * serialization, which keeps a mark element open across consecutive content (so
1177
+ * `**bold**` is one `<strong>` wrapping `**`, `bold`, `**`, not three).
1178
+ */
1179
+ function renderRuns(runs, depth, context) {
1180
+ const out = [];
1181
+ let index = 0;
1182
+ let key = 0;
1183
+ while (index < runs.length) {
1184
+ const run = runs[index];
1185
+ if (run.marks.length <= depth) {
1186
+ out.push(/* @__PURE__ */ jsx(Fragment, { children: run.text }, key++));
1187
+ index++;
1188
+ continue;
1189
+ }
1190
+ const mark = run.marks[depth];
1191
+ let end = index + 1;
1192
+ while (end < runs.length && runs[end].marks.length > depth && runs[end].marks[depth].eq(mark)) end++;
1193
+ const inner = renderRuns(runs.slice(index, end), depth + 1, context);
1194
+ out.push(/* @__PURE__ */ jsx(Fragment, { children: wrapMark(mark, inner, context) }, key++));
1195
+ index = end;
1196
+ }
1197
+ return out;
1198
+ }
1199
+ function renderInline(node, context) {
1200
+ const text = node.textContent;
1201
+ if (!text) return null;
1202
+ return renderRuns(inlineTextToMarkChunks(getMarkBuilders(), text).map(([from, to, marks]) => ({
1203
+ text: text.slice(from, to),
1204
+ marks: Mark.setFrom(marks)
1205
+ })), 0, context);
1206
+ }
1207
+ function renderBlock(node, key, context) {
1208
+ if (node.type.name === "codeBlock") {
1209
+ const attrs = node.attrs;
1210
+ const language = typeof attrs.language === "string" ? attrs.language : "";
1211
+ return /* @__PURE__ */ jsx(CodeBlock, {
1212
+ code: node.textContent,
1213
+ language
1214
+ }, key);
1215
+ }
1216
+ const toDOM = node.type.spec.toDOM;
1217
+ if (node.isTextblock) {
1218
+ const inline = renderInline(node, context);
1219
+ return toDOM ? outputSpecToReact(toDOM(node), inline, key) : /* @__PURE__ */ jsx(Fragment, { children: inline }, key);
1220
+ }
1221
+ const children = [];
1222
+ node.forEach((child, _offset, index) => {
1223
+ children.push(renderBlock(child, index, context));
1224
+ });
1225
+ return toDOM ? outputSpecToReact(toDOM(node), children, key) : /* @__PURE__ */ jsx(Fragment, { children }, key);
1226
+ }
1227
+ /**
1228
+ * Render Markdown to a read-only React tree that looks exactly like the editor
1229
+ * in `hide` mark mode: inline marks, wiki-link chips, images, tweet/YouTube
1230
+ * embeds, and syntax-highlighted code. No editor, no ProseMirror view; just a
1231
+ * walk over `markdownToDoc`'s document reusing meowdown's own parse, mark logic,
1232
+ * and CSS (the root carries `ProseMirror` + `data-mark-mode` so the existing
1233
+ * stylesheet applies). Requires a DOM environment.
1234
+ *
1235
+ * Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
1236
+ * `useCallback` to avoid re-rendering the whole tree.
1237
+ */
1238
+ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, className }) {
1239
+ const content = useMemo(() => {
1240
+ const doc = markdownToDoc(markdown, { frontmatter });
1241
+ const context = {
1242
+ resolveImageUrl,
1243
+ onWikilinkClick,
1244
+ onLinkClick,
1245
+ onImageClick
1246
+ };
1247
+ const blocks = [];
1248
+ doc.forEach((node, _offset, index) => {
1249
+ blocks.push(renderBlock(node, index, context));
1250
+ });
1251
+ return blocks;
1252
+ }, [
1253
+ markdown,
1254
+ frontmatter,
1255
+ resolveImageUrl,
1256
+ onWikilinkClick,
1257
+ onLinkClick,
1258
+ onImageClick
1259
+ ]);
1260
+ return /* @__PURE__ */ jsx("div", {
1261
+ className: clsx("ProseMirror", "meowdown-content", className),
1262
+ "data-mark-mode": markMode,
1263
+ children: content
1264
+ });
1265
+ }
1266
+
1267
+ //#endregion
1268
+ export { MarkdownView, MeowdownEditor, useEditor, useExtension, useKeymap };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.20.0",
4
+ "version": "0.21.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -27,10 +27,10 @@
27
27
  "@ocavue/utils": "^1.7.0",
28
28
  "@prosekit/core": "^0.13.0-beta.3",
29
29
  "@prosekit/pm": "^0.1.19-beta.0",
30
- "@prosekit/react": "^0.8.0-beta.9",
30
+ "@prosekit/react": "^0.8.0-beta.10",
31
31
  "clsx": "^2.1.1",
32
32
  "lucide-react": "^1.21.0",
33
- "@meowdown/core": "0.20.0"
33
+ "@meowdown/core": "0.21.0"
34
34
  },
35
35
  "peerDependencies": {
36
36
  "react": "^19.0.0",