@meowdown/react 0.38.0 → 0.39.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/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ReactElement, ReactNode, Ref } from "react";
2
- import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikilinkClickHandler } from "@meowdown/core";
2
+ import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, StartPendingReplacementOptions, TagClickHandler, 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";
5
5
 
@@ -404,6 +404,35 @@ declare function MeowdownEditor({
404
404
  }: EditorProps): import("react").JSX.Element;
405
405
  //#endregion
406
406
  //#region src/components/markdown-view.d.ts
407
+ /** Payload for {@link TaskClickHandler}. */
408
+ interface TaskClickPayload {
409
+ /**
410
+ * Zero-based position of the clicked checkbox among all the checkboxes this
411
+ * view renders, in document order. Stable for a given `markdown`, so a host
412
+ * can map it back to the corresponding task item in its own parse of the
413
+ * same source.
414
+ */
415
+ index: number;
416
+ /** The checkbox's rendered state. The view never flips it — see the handler doc. */
417
+ checked: boolean;
418
+ /** The item's list marker as written (`+` renders a circle checkbox, `-`/`*` a square). */
419
+ marker: ListMarker;
420
+ /**
421
+ * First line of the item's own inline content, exactly as it appears in the
422
+ * source after the `[ ]`/`[x]` marker and one space. A host locating the task
423
+ * by {@link index} can cross-check this against its own parse and refuse a
424
+ * mismatch instead of toggling the wrong item.
425
+ */
426
+ text: string;
427
+ /** The originating click. Read modifier keys or position a popover from it. */
428
+ event: globalThis.MouseEvent;
429
+ }
430
+ /**
431
+ * Called when a rendered task checkbox is clicked. The view is a pure render
432
+ * of `markdown` and never flips the box itself: apply the toggle to the
433
+ * source and re-render, exactly like the other click handlers.
434
+ */
435
+ type TaskClickHandler = (payload: TaskClickPayload) => void;
407
436
  interface MarkdownViewProps {
408
437
  /** The Markdown to render. Live: changing it re-renders the content. */
409
438
  markdown: string;
@@ -419,6 +448,8 @@ interface MarkdownViewProps {
419
448
  onLinkClick?: LinkClickHandler;
420
449
  /** Called when a rendered image is clicked. Pass a stable function. */
421
450
  onImageClick?: ImageClickHandler;
451
+ /** Called when a rendered task checkbox is clicked. Pass a stable function. */
452
+ onTaskClick?: TaskClickHandler;
422
453
  /** Extra class on the content root (alongside `ProseMirror meowdown-content`). */
423
454
  className?: string;
424
455
  }
@@ -441,7 +472,8 @@ declare function MarkdownView({
441
472
  onWikilinkClick,
442
473
  onLinkClick,
443
474
  onImageClick,
475
+ onTaskClick,
444
476
  className
445
477
  }: MarkdownViewProps): ReactElement;
446
478
  //#endregion
447
- export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, MarkdownView, type MarkdownViewProps, MeowdownEditor, type PendingReplacementResolveHandler, type SelectionHint, type SelectionJSON, type SelectionMenuContext, type SelectionMenuItem, type SelectionMenuSearchHandler, type SlashMenuItem, type SlashMenuSearchHandler, type TagItem, type TagSearchHandler, type TimeFormat, type WikilinkItem, type WikilinkSearchHandler, useEditor, useExtension, useKeymap };
479
+ export { type EditorHandle, type EditorMode, type EditorProps, type EditorStateSnapshot, MarkdownView, type MarkdownViewProps, MeowdownEditor, type PendingReplacementResolveHandler, type SelectionHint, type SelectionJSON, type SelectionMenuContext, type SelectionMenuItem, type SelectionMenuSearchHandler, type SlashMenuItem, type SlashMenuSearchHandler, type TagItem, type TagSearchHandler, type TaskClickHandler, type TaskClickPayload, type TimeFormat, 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 { Fragment, createElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
2
+ import { Fragment, cloneElement, createElement, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useMemo, useRef, useState } from "react";
3
3
  import { codeBlockLanguages, defaultResolveImageUrl, defineBulletAfterHeading, defineCodeBlockPreviewPlugin, defineEditorExtension, defineEmbedPaste, defineExitBoundaryHandler, defineFileClickHandler, defineFilePaste, defineFileView, defineFollowLinkHandler, defineHTMLPaste, defineImage, defineImageClickHandler, defineLinkClickHandler, defineLinkEditKeymap, defineLinkHoverHandler, defineLinkPaste, 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";
@@ -15,6 +15,7 @@ import { AutocompleteEmpty, AutocompleteItem, AutocompletePopup, AutocompletePos
15
15
  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
+ import { BOOLEAN, OVERLOADED_BOOLEAN, getPropertyInfo, possibleStandardNames } from "react-property";
18
19
 
19
20
  //#region src/hooks/use-katex.ts
20
21
  /**
@@ -1788,51 +1789,102 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
1788
1789
  }
1789
1790
 
1790
1791
  //#endregion
1791
- //#region src/components/dom-output-spec.tsx
1792
- const ATTR_NAME_MAP = {
1793
- class: "className",
1794
- contenteditable: "contentEditable",
1795
- colspan: "colSpan",
1796
- rowspan: "rowSpan",
1797
- for: "htmlFor",
1798
- tabindex: "tabIndex",
1799
- viewbox: "viewBox"
1800
- };
1801
- function toReactProps(attrs, key) {
1802
- const props = { key };
1803
- if (!attrs) return props;
1804
- for (const [name, value] of Object.entries(attrs)) {
1805
- if (name === "style") continue;
1806
- props[ATTR_NAME_MAP[name] ?? name] = value;
1792
+ //#region src/components/attributes-to-props.ts
1793
+ const UNCONTROLLED_COMPONENT_ATTRIBUTES = ["checked", "value"];
1794
+ const UNCONTROLLED_COMPONENT_NAMES = [
1795
+ "input",
1796
+ "select",
1797
+ "textarea"
1798
+ ];
1799
+ /**
1800
+ * Converts HTML/SVG DOM attributes to React props.
1801
+ *
1802
+ * @param attributes - HTML/SVG DOM attributes.
1803
+ * @param nodeName - DOM node name.
1804
+ * @returns - React props.
1805
+ */
1806
+ function attributesToProps(attributes = {}, nodeName) {
1807
+ const props = {};
1808
+ const isInputValueOnly = nodeName === "input" || !!attributes["reset"] || !!attributes["submit"];
1809
+ for (const [attributeName, attributeValue] of Object.entries(attributes)) {
1810
+ if (attributeValue === void 0) continue;
1811
+ const attributeNameLowerCased = attributeName.toLowerCase();
1812
+ if (attributeNameLowerCased === "style") continue;
1813
+ if (attributeNameLowerCased === "contenteditable") continue;
1814
+ if (attributeNameLowerCased.startsWith("aria-") || attributeNameLowerCased.startsWith("data-")) {
1815
+ props[attributeName] = attributeValue;
1816
+ continue;
1817
+ }
1818
+ let propName = getPropName(attributeNameLowerCased);
1819
+ if (propName) {
1820
+ const propertyInfo = getPropertyInfo(propName);
1821
+ if (UNCONTROLLED_COMPONENT_ATTRIBUTES.includes(propName) && UNCONTROLLED_COMPONENT_NAMES.includes(nodeName) && !isInputValueOnly) propName = getPropName("default" + attributeNameLowerCased);
1822
+ props[propName] = attributeValue;
1823
+ switch (propertyInfo?.type) {
1824
+ case BOOLEAN:
1825
+ props[propName] = true;
1826
+ break;
1827
+ case OVERLOADED_BOOLEAN:
1828
+ if (attributeValue === "") props[propName] = true;
1829
+ break;
1830
+ }
1831
+ continue;
1832
+ }
1833
+ props[attributeName] = attributeValue;
1807
1834
  }
1808
1835
  return props;
1809
1836
  }
1810
1837
  /**
1811
- * Convert a ProseMirror `DOMOutputSpec` into a React node, substituting `content`
1812
- * for the spec's content hole (`0`). Reused for every node/mark spec the static
1813
- * walker does not special-case, so blocks and plain marks render off their real
1814
- * `toDOM`, exactly as the editor serializes them.
1838
+ * Gets prop name from lowercased attribute name.
1839
+ *
1840
+ * @param attributeName - Lowercased attribute name.
1841
+ * @returns - Prop name.
1815
1842
  */
1816
- function outputSpecToReact(spec, content, key = 0) {
1817
- if (typeof spec === "string") return spec;
1818
- if (!Array.isArray(spec)) return null;
1819
- const array = spec;
1820
- const tag = array[0];
1843
+ function getPropName(attributeName) {
1844
+ return possibleStandardNames[attributeName];
1845
+ }
1846
+
1847
+ //#endregion
1848
+ //#region src/components/dom-output-spec.tsx
1849
+ function normalizeDOMOutputSpec(domSpec) {
1850
+ const spec = domSpec;
1851
+ if (!spec || !Array.isArray(spec)) return;
1852
+ const tag = spec[0];
1821
1853
  let childStart = 1;
1822
1854
  let attrs;
1823
- const second = array[1];
1855
+ const second = spec[1];
1824
1856
  if (second != null && second !== 0 && typeof second === "object" && !Array.isArray(second)) {
1825
1857
  attrs = second;
1826
1858
  childStart = 2;
1827
1859
  }
1828
- const props = toReactProps(attrs, key);
1829
- const rest = array.slice(childStart);
1830
- if (rest.length === 0) return createElement(tag, props);
1831
- return createElement(tag, props, ...rest.map((child, index) => child === 0 ? /* @__PURE__ */ jsx(Fragment, { children: content }, index) : outputSpecToReact(child, content, index)));
1860
+ const rest = spec.slice(childStart);
1861
+ return [
1862
+ tag,
1863
+ attrs,
1864
+ rest
1865
+ ];
1832
1866
  }
1833
1867
 
1834
1868
  //#endregion
1835
1869
  //#region src/components/markdown-view.tsx
1870
+ /**
1871
+ * Convert a ProseMirror `DOMOutputSpec` into a React node, substituting `content`
1872
+ * for the spec's content hole (`0`). Reused for every node/mark spec the static
1873
+ * walker does not special-case, so blocks and plain marks render off their real
1874
+ * `toDOM`, exactly as the editor serializes them.
1875
+ */
1876
+ function outputSpecToReact(spec, content, context) {
1877
+ const key = context.keyCounter.value++;
1878
+ if (typeof spec === "string") return spec;
1879
+ if (spec === 0) return /* @__PURE__ */ jsx(Fragment, { children: content }, key);
1880
+ const normalized = normalizeDOMOutputSpec(spec);
1881
+ if (!normalized) return null;
1882
+ const [tag, attrs, rest] = normalized;
1883
+ const reactProps = { ...attributesToProps(attrs, tag) };
1884
+ reactProps.key = `${key} ${JSON.stringify(attrs)}`;
1885
+ if (tag === "input" && attrs?.["type"] === "checkbox") reactProps.readOnly = true;
1886
+ return createElement(tag, reactProps, ...rest.map((child) => outputSpecToReact(child, content, context)));
1887
+ }
1836
1888
  function WikilinkChip(props) {
1837
1889
  const { target, display, onWikilinkClick, children } = props;
1838
1890
  return /* @__PURE__ */ jsxs("span", {
@@ -2044,7 +2096,7 @@ function wrapMark(mark, children, context) {
2044
2096
  default: {
2045
2097
  const toDOM = mark.type.spec.toDOM;
2046
2098
  if (!toDOM) return children;
2047
- return outputSpecToReact(toDOM(mark, true), children);
2099
+ return outputSpecToReact(toDOM(mark, true), children, context);
2048
2100
  }
2049
2101
  }
2050
2102
  }
@@ -2082,8 +2134,31 @@ function renderInline(node, context) {
2082
2134
  marks: Mark.setFrom(marks)
2083
2135
  })), 0, context);
2084
2136
  }
2085
- function renderBlock(node, key, context) {
2086
- if (node.type.name === "codeBlock") {
2137
+ function renderBlock(node, context) {
2138
+ const key = context.keyCounter.value++;
2139
+ const typeName = node.type.name;
2140
+ let handleTaskClick;
2141
+ if (typeName === "list") {
2142
+ const attrs = node.attrs;
2143
+ const { onTaskClick } = context;
2144
+ if (attrs.kind === "task" && onTaskClick) {
2145
+ const index = context.taskCounter.value++;
2146
+ const checked = attrs.checked === true;
2147
+ const marker = attrs.marker ?? null;
2148
+ const text = node.firstChild?.isTextblock ? node.firstChild.textContent.split("\n", 1)[0] ?? "" : "";
2149
+ handleTaskClick = (event) => {
2150
+ event.preventDefault();
2151
+ onTaskClick({
2152
+ index,
2153
+ checked,
2154
+ marker,
2155
+ text,
2156
+ event: event.nativeEvent
2157
+ });
2158
+ };
2159
+ }
2160
+ }
2161
+ if (typeName === "codeBlock") {
2087
2162
  const attrs = node.attrs;
2088
2163
  const language = typeof attrs.language === "string" ? attrs.language : "";
2089
2164
  if (language === "math") return /* @__PURE__ */ jsx(MathCodeBlock, { code: node.textContent }, key);
@@ -2095,13 +2170,12 @@ function renderBlock(node, key, context) {
2095
2170
  const toDOM = node.type.spec.toDOM;
2096
2171
  if (node.isTextblock) {
2097
2172
  const inline = renderInline(node, context);
2098
- return toDOM ? outputSpecToReact(toDOM(node), inline, key) : /* @__PURE__ */ jsx(Fragment, { children: inline }, key);
2173
+ return toDOM ? outputSpecToReact(toDOM(node), inline, context) : /* @__PURE__ */ jsx(Fragment, { children: inline }, key);
2099
2174
  }
2100
- const children = [];
2101
- node.forEach((child, _offset, index) => {
2102
- children.push(renderBlock(child, index, context));
2103
- });
2104
- return toDOM ? outputSpecToReact(toDOM(node), children, key) : /* @__PURE__ */ jsx(Fragment, { children }, key);
2175
+ const children = node.content.content.map((child) => renderBlock(child, context));
2176
+ const reactNode = toDOM ? outputSpecToReact(toDOM(node), children, context) : /* @__PURE__ */ jsx(Fragment, { children }, key);
2177
+ if (typeName === "list" && handleTaskClick && typeof reactNode !== "string" && reactNode != null) return cloneElement(reactNode, { onClick: handleTaskClick });
2178
+ return reactNode;
2105
2179
  }
2106
2180
  /**
2107
2181
  * Render Markdown to a read-only React tree that looks exactly like the editor
@@ -2114,27 +2188,27 @@ function renderBlock(node, key, context) {
2114
2188
  * Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
2115
2189
  * `useCallback` to avoid re-rendering the whole tree.
2116
2190
  */
2117
- function MarkdownView({ markdown, markMode = "hide", frontmatter = false, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, className }) {
2191
+ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, onTaskClick, className }) {
2118
2192
  const content = useMemo(() => {
2119
2193
  const doc = markdownToDoc(markdown, { frontmatter });
2120
2194
  const context = {
2121
2195
  resolveImageUrl,
2122
2196
  onWikilinkClick,
2123
2197
  onLinkClick,
2124
- onImageClick
2198
+ onImageClick,
2199
+ onTaskClick,
2200
+ taskCounter: { value: 0 },
2201
+ keyCounter: { value: 0 }
2125
2202
  };
2126
- const blocks = [];
2127
- doc.forEach((node, _offset, index) => {
2128
- blocks.push(renderBlock(node, index, context));
2129
- });
2130
- return blocks;
2203
+ return doc.content.content.map((node) => renderBlock(node, context));
2131
2204
  }, [
2132
2205
  markdown,
2133
2206
  frontmatter,
2134
2207
  resolveImageUrl,
2135
2208
  onWikilinkClick,
2136
2209
  onLinkClick,
2137
- onImageClick
2210
+ onImageClick,
2211
+ onTaskClick
2138
2212
  ]);
2139
2213
  return /* @__PURE__ */ jsx("div", {
2140
2214
  className: clsx("ProseMirror", "meowdown-content", className),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@meowdown/react",
3
3
  "type": "module",
4
- "version": "0.38.0",
4
+ "version": "0.39.0",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -25,7 +25,8 @@
25
25
  "@prosekit/react": "^0.8.0-beta.19",
26
26
  "clsx": "^2.1.1",
27
27
  "lucide-react": "^1.21.0",
28
- "@meowdown/core": "0.38.0"
28
+ "react-property": "^2.0.2",
29
+ "@meowdown/core": "0.39.0"
29
30
  },
30
31
  "peerDependencies": {
31
32
  "react": "^19.0.0",