@meowdown/react 0.36.2 → 0.37.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
@@ -60,6 +60,8 @@ Slash menu host items can include `keywords` to match hidden terms without chang
60
60
 
61
61
  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).
62
62
 
63
+ Math (`$E=mc^2$` inline, `$$` blocks, and ` ```math ` fences) is compiled by KaTeX to native MathML.
64
+
63
65
  ## License
64
66
 
65
67
  MIT
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { clsx } from "clsx/lite";
2
- import { Fragment, createElement, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
3
- import { codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineMarkdownCopy, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSpellCheckPlugin, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, getPendingReplacement, getSelectedText, getTableColumnAlign, getVirtualElementFromRange, inlineTextToMarkChunks, isSelectionInTableCell, listenForTweetHeight, markdownToDoc, matchEmbed } from "@meowdown/core";
2
+ import { Fragment, createElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
+ import { codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineMarkdownCopy, definePendingReplacementHandler, definePlaceholder, defineReadonly, defineSpellCheckPlugin, defineTagClickHandler, defineWikilinkClickHandler, defineWikilinkTrigger, docToMarkdown, getCodeTokens, getMarkBuilders, getPendingReplacement, getSelectedText, getTableColumnAlign, getVirtualElementFromRange, inlineTextToMarkChunks, isCodeBlockPreviewHiddenDecoration, 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";
@@ -16,6 +16,27 @@ import { MenuItem, MenuPopup, MenuPositioner } from "@prosekit/react/menu";
16
16
  import { TableHandleColumnMenuRoot, TableHandleColumnMenuTrigger, TableHandleColumnPopup, TableHandleColumnPositioner, TableHandleDragPreview, TableHandleDropIndicator, TableHandleRoot, TableHandleRowMenuRoot, TableHandleRowMenuTrigger, TableHandleRowPopup, TableHandleRowPositioner } from "@prosekit/react/table-handle";
17
17
  import { Mark } from "@prosekit/pm/model";
18
18
 
19
+ //#region src/hooks/use-katex.ts
20
+ /**
21
+ * The lazily loaded KaTeX render function, or `undefined` while it loads (or when
22
+ * `enabled` is false, so a document without math never loads it).
23
+ */
24
+ function useKaTeX(enabled) {
25
+ const [katex, setKaTeX] = useState(void 0);
26
+ useEffect(() => {
27
+ if (!enabled || katex) return;
28
+ let cancelled = false;
29
+ loadKaTeX().then((render) => {
30
+ if (!cancelled) setKaTeX(() => render);
31
+ });
32
+ return () => {
33
+ cancelled = true;
34
+ };
35
+ }, [enabled, katex]);
36
+ return katex;
37
+ }
38
+
39
+ //#endregion
19
40
  //#region src/components/code-block-view.module.css
20
41
  var code_block_view_module_default = {
21
42
  "CopyButton": "meow_CopyButton_D3j_9q",
@@ -26,6 +47,7 @@ var code_block_view_module_default = {
26
47
  "List": "meow_List_D3j_9q",
27
48
  "Popup": "meow_Popup_D3j_9q",
28
49
  "Positioner": "meow_Positioner_D3j_9q",
50
+ "Preview": "meow_Preview_D3j_9q",
29
51
  "Root": "meow_Root_D3j_9q",
30
52
  "Search": "meow_Search_D3j_9q",
31
53
  "SearchRow": "meow_SearchRow_D3j_9q",
@@ -68,10 +90,53 @@ function CopyButton({ getText, label, onCopy, className, ...rest }) {
68
90
  });
69
91
  }
70
92
 
93
+ //#endregion
94
+ //#region src/components/math-render.tsx
95
+ /**
96
+ * KaTeX output rendered into a real element, the same way the editor's
97
+ * `MathMarkView` does. A span host matches KaTeX's own output shape; display
98
+ * mode emits a block-level `math[display="block"]` element.
99
+ */
100
+ function MathRender(props) {
101
+ const { katex, formula, displayMode, className, onMouseDown } = props;
102
+ const ref = useRef(null);
103
+ useLayoutEffect(() => {
104
+ const element = ref.current;
105
+ if (!element || !katex) return;
106
+ renderMathInto(katex, element, formula, displayMode);
107
+ }, [
108
+ katex,
109
+ formula,
110
+ displayMode
111
+ ]);
112
+ return /* @__PURE__ */ jsx("span", {
113
+ ref,
114
+ className,
115
+ contentEditable: false,
116
+ "data-testid": props["data-testid"],
117
+ onMouseDown
118
+ });
119
+ }
120
+
71
121
  //#endregion
72
122
  //#region src/components/code-block-view.tsx
73
123
  function CodeBlockView(props) {
74
124
  const language = props.node.attrs.language || "";
125
+ const isMath = language === "math";
126
+ const code = props.node.textContent;
127
+ const caretInside = props.decorations.some(isCodeBlockPreviewHiddenDecoration);
128
+ const katex = useKaTeX(isMath);
129
+ const showMathPreview = isMath && katex != null;
130
+ const previewOnly = showMathPreview && !caretInside && code.trim() !== "";
131
+ const focusSource = (event) => {
132
+ event.preventDefault();
133
+ const pos = props.getPos();
134
+ if (pos == null) return;
135
+ const { view } = props;
136
+ const selection = TextSelection.near(view.state.doc.resolve(pos + 1), 1);
137
+ view.dispatch(view.state.tr.setSelection(selection));
138
+ view.focus();
139
+ };
75
140
  const selected = useMemo(() => {
76
141
  return codeBlockLanguages.find((item) => item.value === language) ?? {
77
142
  value: language,
@@ -94,86 +159,98 @@ function CodeBlockView(props) {
94
159
  };
95
160
  return /* @__PURE__ */ jsxs("div", {
96
161
  className: code_block_view_module_default.Root,
97
- children: [/* @__PURE__ */ jsxs("div", {
98
- className: code_block_view_module_default.Toolbar,
99
- contentEditable: false,
100
- "data-open": comboboxOpen || void 0,
101
- children: [/* @__PURE__ */ jsxs(Combobox.Root, {
102
- items: itemsForView,
103
- value: selected,
104
- onValueChange: setLanguage,
105
- inputValue: query,
106
- onInputValueChange: setQuery,
107
- onOpenChange: (open) => {
108
- if (open) setComboboxOpen(true);
109
- else setQuery("");
110
- },
111
- onOpenChangeComplete: (open) => {
112
- if (!open) setComboboxOpen(false);
113
- },
114
- children: [/* @__PURE__ */ jsxs(Combobox.Trigger, {
115
- className: code_block_view_module_default.Trigger,
116
- "data-testid": "code-block-language",
117
- children: [/* @__PURE__ */ jsx(Combobox.Value, { placeholder: "Plain Text" }), /* @__PURE__ */ jsx(Combobox.Icon, {
118
- className: code_block_view_module_default.TriggerIcon,
119
- children: /* @__PURE__ */ jsx(ChevronsUpDownIcon, {})
120
- })]
121
- }), /* @__PURE__ */ jsx(Combobox.Portal, { children: /* @__PURE__ */ jsx(Combobox.Positioner, {
122
- className: code_block_view_module_default.Positioner,
123
- sideOffset: 4,
124
- children: /* @__PURE__ */ jsxs(Combobox.Popup, {
125
- className: code_block_view_module_default.Popup,
126
- children: [
127
- /* @__PURE__ */ jsx("div", {
128
- className: code_block_view_module_default.SearchRow,
129
- children: /* @__PURE__ */ jsx(Combobox.Input, {
130
- className: code_block_view_module_default.Search,
131
- placeholder: "Search or type a language",
132
- "data-testid": "code-block-language-search"
162
+ "data-preview": previewOnly || void 0,
163
+ children: [
164
+ /* @__PURE__ */ jsxs("div", {
165
+ className: code_block_view_module_default.Toolbar,
166
+ contentEditable: false,
167
+ "data-open": comboboxOpen || void 0,
168
+ children: [/* @__PURE__ */ jsxs(Combobox.Root, {
169
+ items: itemsForView,
170
+ value: selected,
171
+ onValueChange: setLanguage,
172
+ inputValue: query,
173
+ onInputValueChange: setQuery,
174
+ onOpenChange: (open) => {
175
+ if (open) setComboboxOpen(true);
176
+ else setQuery("");
177
+ },
178
+ onOpenChangeComplete: (open) => {
179
+ if (!open) setComboboxOpen(false);
180
+ },
181
+ children: [/* @__PURE__ */ jsxs(Combobox.Trigger, {
182
+ className: code_block_view_module_default.Trigger,
183
+ "data-testid": "code-block-language",
184
+ children: [/* @__PURE__ */ jsx(Combobox.Value, { placeholder: "Plain Text" }), /* @__PURE__ */ jsx(Combobox.Icon, {
185
+ className: code_block_view_module_default.TriggerIcon,
186
+ children: /* @__PURE__ */ jsx(ChevronsUpDownIcon, {})
187
+ })]
188
+ }), /* @__PURE__ */ jsx(Combobox.Portal, { children: /* @__PURE__ */ jsx(Combobox.Positioner, {
189
+ className: code_block_view_module_default.Positioner,
190
+ sideOffset: 4,
191
+ children: /* @__PURE__ */ jsxs(Combobox.Popup, {
192
+ className: code_block_view_module_default.Popup,
193
+ children: [
194
+ /* @__PURE__ */ jsx("div", {
195
+ className: code_block_view_module_default.SearchRow,
196
+ children: /* @__PURE__ */ jsx(Combobox.Input, {
197
+ className: code_block_view_module_default.Search,
198
+ placeholder: "Search or type a language",
199
+ "data-testid": "code-block-language-search"
200
+ })
201
+ }),
202
+ /* @__PURE__ */ jsx(Combobox.Empty, {
203
+ className: code_block_view_module_default.Empty,
204
+ children: "No languages found."
205
+ }),
206
+ /* @__PURE__ */ jsx(Combobox.List, {
207
+ className: code_block_view_module_default.List,
208
+ children: (item) => /* @__PURE__ */ jsxs(Combobox.Item, {
209
+ value: item,
210
+ className: code_block_view_module_default.Item,
211
+ children: [/* @__PURE__ */ jsx(Combobox.ItemIndicator, {
212
+ className: code_block_view_module_default.ItemIndicator,
213
+ children: /* @__PURE__ */ jsx(CheckIcon, {})
214
+ }), /* @__PURE__ */ jsx("span", {
215
+ className: code_block_view_module_default.ItemText,
216
+ children: item.label
217
+ })]
218
+ }, item.label)
133
219
  })
134
- }),
135
- /* @__PURE__ */ jsx(Combobox.Empty, {
136
- className: code_block_view_module_default.Empty,
137
- children: "No languages found."
138
- }),
139
- /* @__PURE__ */ jsx(Combobox.List, {
140
- className: code_block_view_module_default.List,
141
- children: (item) => /* @__PURE__ */ jsxs(Combobox.Item, {
142
- value: item,
143
- className: code_block_view_module_default.Item,
144
- children: [/* @__PURE__ */ jsx(Combobox.ItemIndicator, {
145
- className: code_block_view_module_default.ItemIndicator,
146
- children: /* @__PURE__ */ jsx(CheckIcon, {})
147
- }), /* @__PURE__ */ jsx("span", {
148
- className: code_block_view_module_default.ItemText,
149
- children: item.label
150
- })]
151
- }, item.label)
152
- })
153
- ]
154
- })
155
- }) })]
156
- }), /* @__PURE__ */ jsx(CopyButton, {
157
- getText: () => props.node.textContent,
158
- label: "Copy code",
159
- className: code_block_view_module_default.CopyButton,
160
- "data-testid": "code-block-copy"
161
- })]
162
- }), /* @__PURE__ */ jsx("pre", {
163
- ref: props.contentRef,
164
- "data-language": language
165
- })]
220
+ ]
221
+ })
222
+ }) })]
223
+ }), /* @__PURE__ */ jsx(CopyButton, {
224
+ getText: () => props.node.textContent,
225
+ label: "Copy code",
226
+ className: code_block_view_module_default.CopyButton,
227
+ "data-testid": "code-block-copy"
228
+ })]
229
+ }),
230
+ /* @__PURE__ */ jsx("pre", {
231
+ ref: props.contentRef,
232
+ "data-language": language
233
+ }),
234
+ showMathPreview && /* @__PURE__ */ jsx(MathRender, {
235
+ katex,
236
+ formula: code,
237
+ displayMode: true,
238
+ className: code_block_view_module_default.Preview,
239
+ "data-testid": "code-block-math-preview",
240
+ onMouseDown: focusSource
241
+ })
242
+ ]
166
243
  });
167
244
  }
168
245
 
169
246
  //#endregion
170
247
  //#region src/extensions/code-block-view.ts
171
248
  function defineCodeBlockView() {
172
- return defineReactNodeView({
249
+ return union(defineReactNodeView({
173
250
  name: "codeBlock",
174
251
  contentAs: "code",
175
252
  component: CodeBlockView
176
- });
253
+ }), defineCodeBlockPreviewPlugin());
177
254
  }
178
255
 
179
256
  //#endregion
@@ -588,7 +665,7 @@ const SCROLL_TOP_MARGIN = 16;
588
665
  function closestScrollable(element) {
589
666
  for (let node = element.parentElement; node; node = node.parentElement) {
590
667
  const { overflowY } = getComputedStyle(node);
591
- if (/(auto|scroll|overlay)/.test(overflowY) && node.scrollHeight > node.clientHeight) return node;
668
+ if ((overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay") && node.scrollHeight > node.clientHeight) return node;
592
669
  }
593
670
  return document.scrollingElement;
594
671
  }
@@ -1026,6 +1103,12 @@ function SlashMenu({ timeFormat = "12", onSlashMenuSearch }) {
1026
1103
  kbd: "```",
1027
1104
  onSelect: () => editor.commands.setCodeBlock()
1028
1105
  }),
1106
+ /* @__PURE__ */ jsx(SlashMenuItem, {
1107
+ label: "Math",
1108
+ keywords: ["latex"],
1109
+ kbd: "```math",
1110
+ onSelect: () => editor.commands.insertMarkdown("```math\n```")
1111
+ }),
1029
1112
  /* @__PURE__ */ jsx(SlashMenuItem, {
1030
1113
  label: "Table",
1031
1114
  onSelect: () => editor.commands.insertTable({
@@ -1875,6 +1958,42 @@ function CodeBlock({ code, language }) {
1875
1958
  children: /* @__PURE__ */ jsx("code", { children: tokens.length > 0 ? renderTokens(code, tokens) : code })
1876
1959
  });
1877
1960
  }
1961
+ /**
1962
+ * Mirrors the editor's `MathMarkView` DOM: a KaTeX preview next to the source
1963
+ * text, flipped by the same mode CSS (the read-only view has no caret, so the
1964
+ * preview always shows in hide/focus modes).
1965
+ */
1966
+ function MathView(props) {
1967
+ const { formula, children } = props;
1968
+ return /* @__PURE__ */ jsxs("span", {
1969
+ className: "md-math-view",
1970
+ children: [/* @__PURE__ */ jsx(MathRender, {
1971
+ katex: useKaTeX(true),
1972
+ formula,
1973
+ displayMode: false,
1974
+ className: "md-math-view-preview",
1975
+ "data-testid": "math-preview"
1976
+ }), /* @__PURE__ */ jsx("span", {
1977
+ className: "md-math-view-content",
1978
+ children
1979
+ })]
1980
+ });
1981
+ }
1982
+ /** A `math` code block: the rendered formula alone, the source while KaTeX loads. */
1983
+ function MathCodeBlock({ code }) {
1984
+ const katex = useKaTeX(true);
1985
+ if (!katex) return /* @__PURE__ */ jsx(CodeBlock, {
1986
+ code,
1987
+ language: "math"
1988
+ });
1989
+ return /* @__PURE__ */ jsx(MathRender, {
1990
+ katex,
1991
+ formula: code,
1992
+ displayMode: true,
1993
+ className: code_block_view_module_default.Preview,
1994
+ "data-testid": "code-block-math-preview"
1995
+ });
1996
+ }
1878
1997
  /** Wrap inline `children` in one mark, special-casing the view/link marks. */
1879
1998
  function wrapMark(mark, children, context) {
1880
1999
  switch (mark.type.name) {
@@ -1897,6 +2016,13 @@ function wrapMark(mark, children, context) {
1897
2016
  children
1898
2017
  });
1899
2018
  }
2019
+ case "mdMath": {
2020
+ const attrs = mark.attrs;
2021
+ return /* @__PURE__ */ jsx(MathView, {
2022
+ formula: attrs.formula,
2023
+ children
2024
+ });
2025
+ }
1900
2026
  case "mdLinkText": {
1901
2027
  const attrs = mark.attrs;
1902
2028
  const handleClick = context.onLinkClick ? (event) => context.onLinkClick?.({
@@ -1955,6 +2081,7 @@ function renderBlock(node, key, context) {
1955
2081
  if (node.type.name === "codeBlock") {
1956
2082
  const attrs = node.attrs;
1957
2083
  const language = typeof attrs.language === "string" ? attrs.language : "";
2084
+ if (language === "math") return /* @__PURE__ */ jsx(MathCodeBlock, { code: node.textContent }, key);
1958
2085
  return /* @__PURE__ */ jsx(CodeBlock, {
1959
2086
  code: node.textContent,
1960
2087
  language
package/dist/style.css CHANGED
@@ -2,6 +2,20 @@
2
2
  position: relative;
3
3
  }
4
4
 
5
+ .meow_Root_D3j_9q[data-preview] pre, .meow_Root_D3j_9q[data-preview] .meow_Toolbar_D3j_9q {
6
+ display: none;
7
+ }
8
+
9
+ .meow_Preview_D3j_9q {
10
+ text-align: center;
11
+ cursor: text;
12
+ -webkit-user-select: none;
13
+ user-select: none;
14
+ padding: .25rem 0;
15
+ display: block;
16
+ overflow-x: auto;
17
+ }
18
+
5
19
  .meow_Toolbar_D3j_9q {
6
20
  z-index: 1;
7
21
  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.36.2",
4
+ "version": "0.37.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -25,7 +25,7 @@
25
25
  "@prosekit/react": "^0.8.0-beta.17",
26
26
  "clsx": "^2.1.1",
27
27
  "lucide-react": "^1.21.0",
28
- "@meowdown/core": "0.36.2"
28
+ "@meowdown/core": "0.37.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "react": "^19.0.0",