@meowdown/react 0.50.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,10 +1,11 @@
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, 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";
7
7
  import { ProseKit, defineReactNodeView, useEditor, useEditor as useEditor$1, useEditorDerivedValue, useExtension, useExtension as useExtension$1, useKeymap } from "@prosekit/react";
8
+ import GithubSlugger from "github-slugger";
8
9
  import { Combobox } from "@base-ui/react/combobox";
9
10
  import { CheckIcon, ChevronsUpDownIcon, CopyIcon, GripHorizontalIcon, GripVerticalIcon, PencilIcon, SparklesIcon, UnlinkIcon } from "lucide-react";
10
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
@@ -18,6 +19,31 @@ import { Mark } from "@prosekit/pm/model";
18
19
  import { BOOLEAN, OVERLOADED_BOOLEAN, getPropertyInfo, possibleStandardNames } from "react-property";
19
20
  import { PreviewCard } from "@base-ui/react/preview-card";
20
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
21
47
  //#region src/hooks/use-katex.ts
22
48
  /**
23
49
  * The lazily loaded KaTeX render function, or `undefined` while it loads (or when
@@ -47,6 +73,7 @@ var code_block_view_module_default = {
47
73
  "ItemIndicator": "meow_ItemIndicator_D3j_9q",
48
74
  "ItemText": "meow_ItemText_D3j_9q",
49
75
  "List": "meow_List_D3j_9q",
76
+ "MermaidPreview": "meow_MermaidPreview_D3j_9q",
50
77
  "Popup": "meow_Popup_D3j_9q",
51
78
  "Positioner": "meow_Positioner_D3j_9q",
52
79
  "Preview": "meow_Preview_D3j_9q",
@@ -120,16 +147,64 @@ function MathRender(props) {
120
147
  });
121
148
  }
122
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
+
123
195
  //#endregion
124
196
  //#region src/components/code-block-view.tsx
125
197
  function CodeBlockView(props) {
126
198
  const language = props.node.attrs.language || "";
127
199
  const isMath = language === "math";
200
+ const isMermaid = language === "mermaid";
128
201
  const code = props.node.textContent;
129
202
  const caretInside = props.decorations.some(isCodeBlockPreviewHiddenDecoration);
130
203
  const katex = useKaTeX(isMath);
204
+ const mermaid = useBeautifulMermaid(isMermaid);
131
205
  const showMathPreview = isMath && katex != null;
132
- const previewOnly = showMathPreview && !caretInside && code.trim() !== "";
206
+ const showMermaidPreview = isMermaid && mermaid != null;
207
+ const previewOnly = (showMathPreview || showMermaidPreview) && !caretInside && code.trim() !== "";
133
208
  const focusSource = (event) => {
134
209
  event.preventDefault();
135
210
  const pos = props.getPos();
@@ -240,6 +315,13 @@ function CodeBlockView(props) {
240
315
  className: code_block_view_module_default.Preview,
241
316
  "data-testid": "code-block-math-preview",
242
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
243
325
  })
244
326
  ]
245
327
  });
@@ -1561,54 +1643,19 @@ function decodeHeadingFragment(fragment) {
1561
1643
  function headingLookupKey(value) {
1562
1644
  return value.normalize("NFKC").trim().replaceAll(/\s+/g, " ").toLowerCase();
1563
1645
  }
1564
- const HEADING_HIDDEN_MARKS = /* @__PURE__ */ new Set([
1565
- "mdMark",
1566
- "mdLinkUri",
1567
- "mdLinkTitle"
1568
- ]);
1569
- const HEADING_ATOM_MARKS = /* @__PURE__ */ new Set([
1570
- "mdWikilink",
1571
- "mdImage",
1572
- "mdFile",
1573
- "mdMath"
1574
- ]);
1575
- function headingAtomText(mark) {
1576
- const attrs = mark.attrs;
1577
- const stringAttr = (name) => {
1578
- const value = attrs[name];
1579
- return typeof value === "string" ? value : "";
1580
- };
1581
- if (mark.type.name === "mdWikilink") return stringAttr("display") || stringAttr("target");
1582
- if (mark.type.name === "mdImage") return stringAttr("alt");
1583
- if (mark.type.name === "mdFile") return stringAttr("name");
1584
- if (mark.type.name === "mdMath") return stringAttr("formula");
1585
- return "";
1586
- }
1587
- /** The heading as its live-preview marks display it, with syntax runs omitted. */
1588
- function headingDisplayText(heading) {
1589
- let output = "";
1590
- let previousAtom;
1591
- heading.forEach((child) => {
1592
- if (!child.isText || !child.text) return;
1593
- const atom = child.marks.find((mark) => HEADING_ATOM_MARKS.has(mark.type.name));
1594
- if (atom) {
1595
- if (!previousAtom?.eq(atom)) output += headingAtomText(atom);
1596
- previousAtom = atom;
1597
- return;
1598
- }
1599
- previousAtom = void 0;
1600
- if (child.marks.some((mark) => HEADING_HIDDEN_MARKS.has(mark.type.name))) return;
1601
- output += child.text;
1602
- });
1603
- return output;
1604
- }
1605
1646
  function findHeadingPosition(doc, fragment) {
1606
- const target = headingLookupKey(decodeHeadingFragment(fragment));
1647
+ const decodedTarget = decodeHeadingFragment(fragment);
1648
+ const target = headingLookupKey(decodedTarget);
1607
1649
  if (!target) return;
1650
+ const slugTarget = decodedTarget.normalize("NFKC").toLowerCase();
1651
+ const slugger = new GithubSlugger();
1608
1652
  let match;
1609
1653
  doc.descendants((node, pos) => {
1610
1654
  if (match != null) return false;
1611
- if (node.type.name === "heading" && (headingLookupKey(node.textContent) === target || headingLookupKey(headingDisplayText(node)) === target)) {
1655
+ if (!isNodeOfType(node, "heading")) return true;
1656
+ const displayText = getTextblockDisplayText(node);
1657
+ const slug = slugger.slug(displayText);
1658
+ if (headingLookupKey(node.textContent) === target || headingLookupKey(displayText) === target || slug === slugTarget) {
1612
1659
  match = pos + 1;
1613
1660
  return false;
1614
1661
  }
@@ -1818,7 +1865,8 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
1818
1865
 
1819
1866
  //#endregion
1820
1867
  //#region src/components/editor.tsx
1821
- 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 }) {
1822
1870
  const childRef = useRef(null);
1823
1871
  useImperativeHandle(handleRef, () => {
1824
1872
  function getMarkdown() {
@@ -1905,6 +1953,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1905
1953
  }, []);
1906
1954
  return /* @__PURE__ */ jsx("div", {
1907
1955
  className: clsx("meowdown", wrapperClassName),
1956
+ style: caretGlide ? void 0 : CARET_GLIDE_OFF,
1908
1957
  children: /* @__PURE__ */ jsx(ProseKitEditor, {
1909
1958
  ref: childRef,
1910
1959
  markMode: mode,
@@ -1976,7 +2025,7 @@ function attributesToProps(attributes = {}, nodeName) {
1976
2025
  let propName = getPropName(attributeNameLowerCased);
1977
2026
  if (propName) {
1978
2027
  const propertyInfo = getPropertyInfo(propName);
1979
- if (UNCONTROLLED_COMPONENT_ATTRIBUTES.includes(propName) && UNCONTROLLED_COMPONENT_NAMES.includes(nodeName) && !isInputValueOnly) propName = getPropName("default" + attributeNameLowerCased);
2028
+ if (!isInputValueOnly && UNCONTROLLED_COMPONENT_ATTRIBUTES.includes(propName) && UNCONTROLLED_COMPONENT_NAMES.includes(nodeName)) propName = getPropName("default" + attributeNameLowerCased);
1980
2029
  props[propName] = attributeValue;
1981
2030
  switch (propertyInfo?.type) {
1982
2031
  case BOOLEAN:
@@ -2284,6 +2333,19 @@ function MathCodeBlock({ code }) {
2284
2333
  "data-testid": "code-block-math-preview"
2285
2334
  });
2286
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
+ }
2287
2349
  /** Wrap inline `children` in one mark, special-casing the view/link marks. */
2288
2350
  function wrapMark(mark, children, context) {
2289
2351
  switch (mark.type.name) {
@@ -2411,6 +2473,7 @@ function renderBlock(node, context) {
2411
2473
  const attrs = node.attrs;
2412
2474
  const language = typeof attrs.language === "string" ? attrs.language : "";
2413
2475
  if (language === "math") return /* @__PURE__ */ jsx(MathCodeBlock, { code: node.textContent }, key);
2476
+ if (language === "mermaid") return /* @__PURE__ */ jsx(MermaidCodeBlock, { code: node.textContent }, key);
2414
2477
  return /* @__PURE__ */ jsx(CodeBlock, {
2415
2478
  code: node.textContent,
2416
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.50.0",
4
+ "version": "0.52.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -23,10 +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",
28
+ "github-slugger": "^2.0.0",
27
29
  "lucide-react": "^1.24.0",
28
30
  "react-property": "^2.0.2",
29
- "@meowdown/core": "0.50.0"
31
+ "@meowdown/core": "0.52.0"
30
32
  },
31
33
  "peerDependencies": {
32
34
  "react": "^19.0.0",
@@ -43,14 +45,14 @@
43
45
  "devDependencies": {
44
46
  "@css-modules-kit/codegen": "^1.4.0",
45
47
  "@ocavue/tsconfig": "^0.7.1",
46
- "@tsdown/css": "^0.22.7",
48
+ "@tsdown/css": "^0.22.8",
47
49
  "@types/react": "^19.2.17",
48
50
  "@types/react-dom": "^19.2.3",
49
51
  "@vitest/browser-playwright": "^4.1.10",
50
52
  "dedent": "^1.7.2",
51
53
  "react": "^19.2.7",
52
54
  "react-dom": "^19.2.7",
53
- "tsdown": "^0.22.7",
55
+ "tsdown": "^0.22.8",
54
56
  "vitest": "^4.1.10",
55
57
  "vitest-browser-commands": "^0.2.1",
56
58
  "vitest-browser-react": "^2.2.0",