@meowdown/react 0.19.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 +22 -1
- package/dist/index.d.ts +47 -6
- package/dist/index.js +313 -6
- package/package.json +3 -3
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 `[[` 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: `` 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";
|
|
@@ -71,8 +71,8 @@ interface WikilinkItem {
|
|
|
71
71
|
}
|
|
72
72
|
/**
|
|
73
73
|
* Searches notes for the wikilink menu. Receives the query typed after
|
|
74
|
-
* `[[` (lowercased, punctuation stripped, may be empty or contain
|
|
75
|
-
* and returns the rows to show, either synchronously or as a promise.
|
|
74
|
+
* `[[` or `@` (lowercased, punctuation stripped, may be empty or contain
|
|
75
|
+
* spaces) and returns the rows to show, either synchronously or as a promise.
|
|
76
76
|
*/
|
|
77
77
|
type WikilinkSearchHandler = (query: string) => WikilinkItem[] | Promise<WikilinkItem[]>;
|
|
78
78
|
//#endregion
|
|
@@ -105,8 +105,8 @@ interface EditorProps {
|
|
|
105
105
|
*/
|
|
106
106
|
onTagSearch?: TagSearchHandler;
|
|
107
107
|
/**
|
|
108
|
-
* Searches notes for the wikilink menu, which opens as soon as `[[`
|
|
109
|
-
* typed in a rich mode. Receives the query (lowercased, punctuation
|
|
108
|
+
* Searches notes for the wikilink menu, which opens as soon as `[[` or `@`
|
|
109
|
+
* is typed in a rich mode. Receives the query (lowercased, punctuation
|
|
110
110
|
* stripped, may be empty) and returns the note names to show,
|
|
111
111
|
* synchronously or as a promise. Pass a stable function (e.g. from
|
|
112
112
|
* `useCallback`). Omit to disable the wikilink menu. Ignored in source
|
|
@@ -211,4 +211,45 @@ declare function MeowdownEditor({
|
|
|
211
211
|
children
|
|
212
212
|
}: EditorProps): import("react").JSX.Element;
|
|
213
213
|
//#endregion
|
|
214
|
-
|
|
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
|
|
|
@@ -695,7 +699,7 @@ function TagMenu({ onTagSearch }) {
|
|
|
695
699
|
|
|
696
700
|
//#endregion
|
|
697
701
|
//#region src/components/wikilink-menu.tsx
|
|
698
|
-
const regex =
|
|
702
|
+
const regex = canUseRegexLookbehind() ? /(?:\[\[[^[\]]*|(?<!\S)@(?:[^[\]\s][^[\]]*)?)$/u : /(?:\[\[[^[\]]*|@(?:[^[\]\s][^[\]]*)?)$/u;
|
|
699
703
|
function WikilinkMenu({ onWikilinkSearch }) {
|
|
700
704
|
const editor = useEditor$1();
|
|
701
705
|
const [open, setOpen] = useState(false);
|
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
33
|
+
"@meowdown/core": "0.21.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"react": "^19.0.0",
|