@meowdown/react 0.51.0 → 0.52.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,8 @@ 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
+ The caret glides between positions by default; pass `caretGlide={false}` to move it instantly (hosts can also tune the duration via the `--meowdown-caret-glide` CSS variable).
60
+
59
61
  ### Wiki embeds
60
62
 
61
63
  Obsidian-style wiki embeds (`![[path]]`, with optional `|width` or
@@ -119,6 +121,18 @@ resolver that accepts only trusted local image sources.
119
121
  <MarkdownView markdown={markdown} interactive={false} resolveImageUrl={resolveLocalImageUrl} />
120
122
  ```
121
123
 
124
+ ### Mermaid code blocks
125
+
126
+ Fenced `mermaid` code blocks render as diagrams in both `MeowdownEditor` and
127
+ `MarkdownView`. The editor shows only the preview while the caret is outside
128
+ the block, then shows source and a live preview while editing it.
129
+
130
+ Rendering uses `beautiful-mermaid`, which currently supports Flowchart, State,
131
+ Sequence, Class, ER, and XY Chart diagrams. Unsupported syntax renders an error
132
+ instead of an empty preview. Override `--meowdown-mermaid-bg`,
133
+ `--meowdown-mermaid-fg`, or `--meowdown-mermaid-error` to customize the
134
+ diagram surface.
135
+
122
136
  ## Styling
123
137
 
124
138
  Import both stylesheets: `@meowdown/core/style.css` (the editor theme and variables) and `@meowdown/react/style.css` (the component layout). The core theme is documented in [`@meowdown/core`](https://www.npmjs.com/package/@meowdown/core).
@@ -0,0 +1,3 @@
1
+ import { renderMermaidSVG } from "beautiful-mermaid";
2
+
3
+ export { renderMermaidSVG };
package/dist/index.d.ts CHANGED
@@ -366,6 +366,12 @@ interface EditorProps {
366
366
  * Ignored when `readOnly` is set.
367
367
  */
368
368
  blockHandle?: boolean;
369
+ /**
370
+ * Glides the caret between positions instead of jumping instantly. On by
371
+ * default. Set to `false` to disable the movement animation; equivalent to
372
+ * setting the `--meowdown-caret-glide` CSS variable to `0ms`.
373
+ */
374
+ caretGlide?: boolean;
369
375
  /**
370
376
  * Placeholder text shown when the whole document is empty. A function
371
377
  * receives the editor state. Pass a stable function.
@@ -392,7 +398,7 @@ interface EditorProps {
392
398
  /** Nodes rendered inside the editor's ProseKit context. */
393
399
  children?: ReactNode;
394
400
  }
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;
401
+ declare function MeowdownEditor({ mode, initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, frontmatter, blockHandle, caretGlide, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }: EditorProps): import("react").JSX.Element;
396
402
  //#endregion
397
403
  //#region src/components/markdown-view.d.ts
398
404
  /** Payload for {@link TaskClickHandler}. */
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { clsx } from "clsx/lite";
2
2
  import { Fragment, cloneElement, createElement, useCallback, 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, defineSubstitution, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkHoverHandler, defineWikilinkTrigger, docToMarkdown, formatFileSize, getCodeTokens, getFileKind, getMarkBuilders, getPendingReplacement, getSelectedText, getTableColumnAlign, getTextblockDisplayText, 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, getTextblockDisplayText, getVirtualElementFromRange, inlineTextToMarkChunks, isCodeBlockPreviewHiddenDecoration, isNodeOfType, isSelectionInTableCell, listenForTweetHeight, loadKaTeX, markdownToDoc, matchEmbed, renderMathInto } from "@meowdown/core";
4
4
  import { clamp } from "@ocavue/utils";
5
5
  import { canUseRegexLookbehind, createEditor, defineDocChangeHandler, defineUpdateHandler, isTextSelection, union } from "@prosekit/core";
6
6
  import { Selection, TextSelection } from "@prosekit/pm/state";
@@ -19,6 +19,31 @@ import { Mark } from "@prosekit/pm/model";
19
19
  import { BOOLEAN, OVERLOADED_BOOLEAN, getPropertyInfo, possibleStandardNames } from "react-property";
20
20
  import { PreviewCard } from "@base-ui/react/preview-card";
21
21
 
22
+ //#region src/hooks/use-beautiful-mermaid.ts
23
+ let beautifulMermaidPromise;
24
+ function loadBeautifulMermaid() {
25
+ beautifulMermaidPromise ??= import("./beautiful-mermaid-chunk-Cc6FHgAa.js").then((module) => module.renderMermaidSVG).catch((error) => {
26
+ console.error("[meowdown] Failed to load beautiful-mermaid.", error);
27
+ throw error;
28
+ });
29
+ return beautifulMermaidPromise;
30
+ }
31
+ function useBeautifulMermaid(enabled) {
32
+ const [renderer, setRenderer] = useState(void 0);
33
+ useEffect(() => {
34
+ if (!enabled || renderer) return;
35
+ let cancelled = false;
36
+ loadBeautifulMermaid().then((loadedRenderer) => {
37
+ if (!cancelled) setRenderer(() => loadedRenderer);
38
+ }).catch(() => {});
39
+ return () => {
40
+ cancelled = true;
41
+ };
42
+ }, [enabled, renderer]);
43
+ return renderer;
44
+ }
45
+
46
+ //#endregion
22
47
  //#region src/hooks/use-katex.ts
23
48
  /**
24
49
  * The lazily loaded KaTeX render function, or `undefined` while it loads (or when
@@ -48,6 +73,7 @@ var code_block_view_module_default = {
48
73
  "ItemIndicator": "meow_ItemIndicator_D3j_9q",
49
74
  "ItemText": "meow_ItemText_D3j_9q",
50
75
  "List": "meow_List_D3j_9q",
76
+ "MermaidPreview": "meow_MermaidPreview_D3j_9q",
51
77
  "Popup": "meow_Popup_D3j_9q",
52
78
  "Positioner": "meow_Positioner_D3j_9q",
53
79
  "Preview": "meow_Preview_D3j_9q",
@@ -121,16 +147,64 @@ function MathRender(props) {
121
147
  });
122
148
  }
123
149
 
150
+ //#endregion
151
+ //#region src/components/mermaid-render.tsx
152
+ const MERMAID_OPTIONS = {
153
+ bg: "var(--meowdown-mermaid-bg)",
154
+ fg: "var(--meowdown-mermaid-fg)",
155
+ transparent: true,
156
+ interactive: false
157
+ };
158
+ function renderMermaid(renderer, source) {
159
+ try {
160
+ const svg = renderer(source, MERMAID_OPTIONS);
161
+ const document = new DOMParser().parseFromString(svg, "image/svg+xml");
162
+ const element = document.documentElement;
163
+ if (document.querySelector("parsererror") || element.localName !== "svg" || element.namespaceURI !== "http://www.w3.org/2000/svg") return { error: "Invalid SVG output." };
164
+ return { element };
165
+ } catch (error) {
166
+ return { error: error instanceof Error ? error.message : String(error) };
167
+ }
168
+ }
169
+ function MermaidRender(props) {
170
+ const { renderer, source, className, onMouseDown } = props;
171
+ const output = useMemo(() => renderMermaid(renderer, source), [renderer, source]);
172
+ const ref = useRef(null);
173
+ useLayoutEffect(() => {
174
+ const host = ref.current;
175
+ if (!host || !output.element) return;
176
+ host.replaceChildren(window.document.importNode(output.element, true));
177
+ }, [output]);
178
+ if (output.error) return /* @__PURE__ */ jsx("span", {
179
+ className,
180
+ contentEditable: false,
181
+ "data-error": true,
182
+ "data-testid": props["data-testid"],
183
+ onMouseDown,
184
+ children: output.error
185
+ }, "error");
186
+ return /* @__PURE__ */ jsx("span", {
187
+ ref,
188
+ className,
189
+ contentEditable: false,
190
+ "data-testid": props["data-testid"],
191
+ onMouseDown
192
+ }, "svg");
193
+ }
194
+
124
195
  //#endregion
125
196
  //#region src/components/code-block-view.tsx
126
197
  function CodeBlockView(props) {
127
198
  const language = props.node.attrs.language || "";
128
199
  const isMath = language === "math";
200
+ const isMermaid = language === "mermaid";
129
201
  const code = props.node.textContent;
130
202
  const caretInside = props.decorations.some(isCodeBlockPreviewHiddenDecoration);
131
203
  const katex = useKaTeX(isMath);
204
+ const mermaid = useBeautifulMermaid(isMermaid);
132
205
  const showMathPreview = isMath && katex != null;
133
- const previewOnly = showMathPreview && !caretInside && code.trim() !== "";
206
+ const showMermaidPreview = isMermaid && mermaid != null;
207
+ const previewOnly = (showMathPreview || showMermaidPreview) && !caretInside && code.trim() !== "";
134
208
  const focusSource = (event) => {
135
209
  event.preventDefault();
136
210
  const pos = props.getPos();
@@ -241,6 +315,13 @@ function CodeBlockView(props) {
241
315
  className: code_block_view_module_default.Preview,
242
316
  "data-testid": "code-block-math-preview",
243
317
  onMouseDown: focusSource
318
+ }),
319
+ showMermaidPreview && /* @__PURE__ */ jsx(MermaidRender, {
320
+ renderer: mermaid,
321
+ source: code,
322
+ className: `${code_block_view_module_default.Preview} ${code_block_view_module_default.MermaidPreview}`,
323
+ "data-testid": "code-block-mermaid-preview",
324
+ onMouseDown: focusSource
244
325
  })
245
326
  ]
246
327
  });
@@ -1571,7 +1652,7 @@ function findHeadingPosition(doc, fragment) {
1571
1652
  let match;
1572
1653
  doc.descendants((node, pos) => {
1573
1654
  if (match != null) return false;
1574
- if (node.type.name !== "heading") return true;
1655
+ if (!isNodeOfType(node, "heading")) return true;
1575
1656
  const displayText = getTextblockDisplayText(node);
1576
1657
  const slug = slugger.slug(displayText);
1577
1658
  if (headingLookupKey(node.textContent) === target || headingLookupKey(displayText) === target || slug === slugTarget) {
@@ -1784,7 +1865,8 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1784
1865
 
1785
1866
  //#endregion
1786
1867
  //#region src/components/editor.tsx
1787
- 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 }) {
1868
+ const CARET_GLIDE_OFF = { "--meowdown-caret-glide": "0ms" };
1869
+ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste = true, linkPaste = true, bulletAfterHeading = false, substitution = true, frontmatter = false, blockHandle = true, caretGlide = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }) {
1788
1870
  const childRef = useRef(null);
1789
1871
  useImperativeHandle(handleRef, () => {
1790
1872
  function getMarkdown() {
@@ -1871,6 +1953,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1871
1953
  }, []);
1872
1954
  return /* @__PURE__ */ jsx("div", {
1873
1955
  className: clsx("meowdown", wrapperClassName),
1956
+ style: caretGlide ? void 0 : CARET_GLIDE_OFF,
1874
1957
  children: /* @__PURE__ */ jsx(ProseKitEditor, {
1875
1958
  ref: childRef,
1876
1959
  markMode: mode,
@@ -2250,6 +2333,19 @@ function MathCodeBlock({ code }) {
2250
2333
  "data-testid": "code-block-math-preview"
2251
2334
  });
2252
2335
  }
2336
+ function MermaidCodeBlock({ code }) {
2337
+ const renderer = useBeautifulMermaid(true);
2338
+ if (!renderer || code.trim() === "") return /* @__PURE__ */ jsx(CodeBlock, {
2339
+ code,
2340
+ language: "mermaid"
2341
+ });
2342
+ return /* @__PURE__ */ jsx(MermaidRender, {
2343
+ renderer,
2344
+ source: code,
2345
+ className: `${code_block_view_module_default.Preview} ${code_block_view_module_default.MermaidPreview}`,
2346
+ "data-testid": "code-block-mermaid-preview"
2347
+ });
2348
+ }
2253
2349
  /** Wrap inline `children` in one mark, special-casing the view/link marks. */
2254
2350
  function wrapMark(mark, children, context) {
2255
2351
  switch (mark.type.name) {
@@ -2377,6 +2473,7 @@ function renderBlock(node, context) {
2377
2473
  const attrs = node.attrs;
2378
2474
  const language = typeof attrs.language === "string" ? attrs.language : "";
2379
2475
  if (language === "math") return /* @__PURE__ */ jsx(MathCodeBlock, { code: node.textContent }, key);
2476
+ if (language === "mermaid") return /* @__PURE__ */ jsx(MermaidCodeBlock, { code: node.textContent }, key);
2380
2477
  return /* @__PURE__ */ jsx(CodeBlock, {
2381
2478
  code: node.textContent,
2382
2479
  language
package/dist/style.css CHANGED
@@ -16,6 +16,23 @@
16
16
  overflow-x: auto;
17
17
  }
18
18
 
19
+ .meow_MermaidPreview_D3j_9q {
20
+ text-align: initial;
21
+
22
+ &[data-error] {
23
+ color: var(--meowdown-mermaid-error);
24
+ font-family: var(--meowdown-font-mono);
25
+ white-space: pre-wrap;
26
+ }
27
+
28
+ & svg {
29
+ max-width: 100%;
30
+ height: auto;
31
+ margin-inline: auto;
32
+ display: block;
33
+ }
34
+ }
35
+
19
36
  .meow_Toolbar_D3j_9q {
20
37
  z-index: 1;
21
38
  user-select: none;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.51.0",
4
+ "version": "0.52.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -23,11 +23,12 @@
23
23
  "@prosekit/core": "^0.13.0-beta.6",
24
24
  "@prosekit/pm": "^0.1.19-beta.3",
25
25
  "@prosekit/react": "^0.8.0-beta.21",
26
+ "beautiful-mermaid": "^1.1.3",
26
27
  "clsx": "^2.1.1",
27
28
  "github-slugger": "^2.0.0",
28
29
  "lucide-react": "^1.24.0",
29
30
  "react-property": "^2.0.2",
30
- "@meowdown/core": "0.51.0"
31
+ "@meowdown/core": "0.52.0"
31
32
  },
32
33
  "peerDependencies": {
33
34
  "react": "^19.0.0",