@meowdown/react 0.48.0 → 0.50.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 +34 -0
- package/dist/index.d.ts +34 -5
- package/dist/index.js +193 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -56,6 +56,40 @@ 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
|
+
### Wiki embeds
|
|
60
|
+
|
|
61
|
+
Obsidian-style wiki embeds (`![[path]]`, with optional `|width` or
|
|
62
|
+
`|widthxheight`) stay literal and editable unless the host classifies them.
|
|
63
|
+
`resolveWikiEmbed` is a pure, creation-time resolver: return an image, file, or
|
|
64
|
+
note result to reuse the corresponding Meowdown atom and its existing click
|
|
65
|
+
hook; return `undefined` for missing or ambiguous targets.
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
<MeowdownEditor
|
|
69
|
+
initialMarkdown="![[assets/photo.png|320]]"
|
|
70
|
+
resolveWikiEmbed={({ target }) => (target.endsWith('.png') ? { kind: 'image' } : undefined)}
|
|
71
|
+
resolveImageUrl={resolveLocalImageUrl}
|
|
72
|
+
onImageClick={openLightbox}
|
|
73
|
+
/>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Use `EditorHandle.revealHeading('#Section%20name')` to move the selection to a
|
|
77
|
+
matching heading and scroll it into view after following a heading link.
|
|
78
|
+
|
|
79
|
+
Both `MeowdownEditor` and the read-only `MarkdownView` accept
|
|
80
|
+
`resolveFileLink`. Return `true` to render a standard `[label](path)` Markdown
|
|
81
|
+
link with the same file pill, metadata resolver, and click hook used by wiki
|
|
82
|
+
file embeds; return `false` to keep it as a normal link.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
<MarkdownView
|
|
86
|
+
markdown="[Quarterly report](docs/report.pdf)"
|
|
87
|
+
resolveFileLink={({ href }) => href.startsWith('docs/')}
|
|
88
|
+
resolveFileInfo={resolveLocalFileInfo}
|
|
89
|
+
onFileClick={openLocalFile}
|
|
90
|
+
/>
|
|
91
|
+
```
|
|
92
|
+
|
|
59
93
|
### Wiki-link hover cards
|
|
60
94
|
|
|
61
95
|
Mount `WikilinkHoverCard` inside `MeowdownEditor` and render host-owned preview
|
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, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikilinkClickHandler, WikilinkHoverHit } from "@meowdown/core";
|
|
2
|
+
import { AcceptPendingReplacementOptions, ExitBoundaryHandler, FileClickHandler, FileInfoResolver, FileLinkResolver, FilePasteOptions, FileViewOptions, ImageClickHandler, ImageOptions, LinkClickHandler, LinkCopyHandler, ListMarker, MarkMode, PendingReplacement, PendingReplacementOutcome, PlaceholderOptions, StartPendingReplacementOptions, TagClickHandler, TypedEditor, WikiEmbedResolver, WikilinkClickHandler, WikilinkHoverHit } 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
|
//#region src/utils/date-format.d.ts
|
|
@@ -45,6 +45,12 @@ interface EditorHandle {
|
|
|
45
45
|
* is mapped through the change.
|
|
46
46
|
*/
|
|
47
47
|
setState: (markdown?: string, selection?: SelectionHint) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Reparse the current Markdown with the editor's creation-time resolvers,
|
|
50
|
+
* preserving the selection without adding an undo-history entry. Hosts use
|
|
51
|
+
* this when data consulted by a stable resolver changes out of band.
|
|
52
|
+
*/
|
|
53
|
+
refreshMarkdownRendering: () => void;
|
|
48
54
|
/** Returns the current selection. */
|
|
49
55
|
getSelection: () => SelectionJSON$1;
|
|
50
56
|
/** Restores a selection with the same hint semantics as `setState`. */
|
|
@@ -53,6 +59,12 @@ interface EditorHandle {
|
|
|
53
59
|
focus: () => void;
|
|
54
60
|
/** Scrolls the selection into view. */
|
|
55
61
|
scrollIntoView: () => void;
|
|
62
|
+
/**
|
|
63
|
+
* Moves the selection to the first heading matching a decoded `#fragment`
|
|
64
|
+
* (case-insensitive, with whitespace collapsed) and scrolls it into view.
|
|
65
|
+
* Returns `false` when no heading matches.
|
|
66
|
+
*/
|
|
67
|
+
revealHeading: (fragment: string) => boolean;
|
|
56
68
|
/**
|
|
57
69
|
* The current selection as Markdown: block structure (list markers,
|
|
58
70
|
* headings) is serialized, and inline syntax is already literal text. A
|
|
@@ -288,6 +300,12 @@ interface EditorProps {
|
|
|
288
300
|
* changes are ignored, like `initialMarkdown`.
|
|
289
301
|
*/
|
|
290
302
|
resolveFileLink?: FileLinkResolver;
|
|
303
|
+
/**
|
|
304
|
+
* Classifies `![[target]]` as an image, file, or note atom. Return
|
|
305
|
+
* `undefined` for missing or ambiguous targets to leave the source literal
|
|
306
|
+
* and editable. Must be pure and is read once when the editor is created.
|
|
307
|
+
*/
|
|
308
|
+
resolveWikiEmbed?: WikiEmbedResolver;
|
|
291
309
|
/**
|
|
292
310
|
* Resolves the metadata (file size in bytes) shown on a file pill, directly
|
|
293
311
|
* or as a promise; the pill renders immediately and fills the size in when
|
|
@@ -374,7 +392,7 @@ interface EditorProps {
|
|
|
374
392
|
/** Nodes rendered inside the editor's ProseKit context. */
|
|
375
393
|
children?: ReactNode;
|
|
376
394
|
}
|
|
377
|
-
declare function MeowdownEditor({ mode, initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, frontmatter, blockHandle, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }: EditorProps): import("react").JSX.Element;
|
|
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;
|
|
378
396
|
//#endregion
|
|
379
397
|
//#region src/components/markdown-view.d.ts
|
|
380
398
|
/** Payload for {@link TaskClickHandler}. */
|
|
@@ -414,7 +432,7 @@ interface MarkdownViewProps {
|
|
|
414
432
|
/** Peel a leading YAML frontmatter block before rendering. Off by default. */
|
|
415
433
|
frontmatter?: boolean;
|
|
416
434
|
/**
|
|
417
|
-
* Whether rendered links, images, and task checkboxes can be activated.
|
|
435
|
+
* Whether rendered links, images, file pills, and task checkboxes can be activated.
|
|
418
436
|
* Defaults to `true`. When `false`, callbacks are ignored, the rendered tree
|
|
419
437
|
* contains no anchors or focusable task controls, and recognized tweet and
|
|
420
438
|
* YouTube embeds are omitted before any image resolver runs.
|
|
@@ -422,12 +440,23 @@ interface MarkdownViewProps {
|
|
|
422
440
|
interactive?: boolean;
|
|
423
441
|
/** Map an image `src` to a displayable URL, or `undefined` to skip it. */
|
|
424
442
|
resolveImageUrl?: (src: string) => string | undefined;
|
|
443
|
+
/**
|
|
444
|
+
* Claim a `[label](url)` link as a file pill instead of a regular link.
|
|
445
|
+
* Must be pure; return `false` for links that should render normally.
|
|
446
|
+
*/
|
|
447
|
+
resolveFileLink?: FileLinkResolver;
|
|
448
|
+
/** Classify `![[target]]` as an image, file, or note; unresolved source stays literal. */
|
|
449
|
+
resolveWikiEmbed?: WikiEmbedResolver;
|
|
450
|
+
/** Resolve metadata shown on a file pill. */
|
|
451
|
+
resolveFileInfo?: FileInfoResolver;
|
|
425
452
|
/** Called when a rendered wikilink is clicked. Pass a stable function. */
|
|
426
453
|
onWikilinkClick?: WikilinkClickHandler;
|
|
427
454
|
/** Called when a rendered Markdown link is clicked. Pass a stable function. */
|
|
428
455
|
onLinkClick?: LinkClickHandler;
|
|
429
456
|
/** Called when a rendered image is clicked. Pass a stable function. */
|
|
430
457
|
onImageClick?: ImageClickHandler;
|
|
458
|
+
/** Called when a rendered file pill is clicked. Pass a stable function. */
|
|
459
|
+
onFileClick?: FileClickHandler;
|
|
431
460
|
/** Called when a rendered task checkbox is clicked. Pass a stable function. */
|
|
432
461
|
onTaskClick?: TaskClickHandler;
|
|
433
462
|
/** Extra class on the content root (alongside `ProseMirror meowdown-content`). */
|
|
@@ -441,10 +470,10 @@ interface MarkdownViewProps {
|
|
|
441
470
|
* and CSS (the root carries `ProseMirror` + `data-mark-mode` so the existing
|
|
442
471
|
* stylesheet applies). Requires a DOM environment.
|
|
443
472
|
*
|
|
444
|
-
* Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
|
|
473
|
+
* Callbacks (`onWikilinkClick`, etc.) and resolvers should be stable; pass them via
|
|
445
474
|
* `useCallback` to avoid re-rendering the whole tree.
|
|
446
475
|
*/
|
|
447
|
-
declare function MarkdownView({ markdown, markMode, frontmatter, interactive, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, onTaskClick, className }: MarkdownViewProps): ReactElement;
|
|
476
|
+
declare function MarkdownView({ markdown, markMode, frontmatter, interactive, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }: MarkdownViewProps): ReactElement;
|
|
448
477
|
//#endregion
|
|
449
478
|
//#region src/components/wikilink-hover-card.d.ts
|
|
450
479
|
/** Props for {@link WikilinkHoverCard}. */
|
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, getCodeTokens, 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, 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";
|
|
@@ -1090,6 +1090,11 @@ function SlashMenu({ timeFormat = "12", onSlashMenuSearch, onFilePaste, onFileSa
|
|
|
1090
1090
|
"data-testid": "slash-menu",
|
|
1091
1091
|
children: [
|
|
1092
1092
|
!inTableCell && /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
1093
|
+
/* @__PURE__ */ jsx(SlashMenuItem, {
|
|
1094
|
+
label: "Text",
|
|
1095
|
+
keywords: ["paragraph", "plain"],
|
|
1096
|
+
onSelect: () => editor.commands.turnIntoText()
|
|
1097
|
+
}),
|
|
1093
1098
|
/* @__PURE__ */ jsx(SlashMenuItem, {
|
|
1094
1099
|
label: "Heading 1",
|
|
1095
1100
|
kbd: "#",
|
|
@@ -1545,10 +1550,77 @@ function resolveSelection(doc, selection) {
|
|
|
1545
1550
|
return TextSelection.between(doc.resolve(anchor), doc.resolve(head));
|
|
1546
1551
|
}
|
|
1547
1552
|
}
|
|
1548
|
-
function
|
|
1553
|
+
function decodeHeadingFragment(fragment) {
|
|
1554
|
+
const source = fragment.startsWith("#") ? fragment.slice(1) : fragment;
|
|
1555
|
+
try {
|
|
1556
|
+
return decodeURIComponent(source);
|
|
1557
|
+
} catch {
|
|
1558
|
+
return source;
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
function headingLookupKey(value) {
|
|
1562
|
+
return value.normalize("NFKC").trim().replaceAll(/\s+/g, " ").toLowerCase();
|
|
1563
|
+
}
|
|
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
|
+
function findHeadingPosition(doc, fragment) {
|
|
1606
|
+
const target = headingLookupKey(decodeHeadingFragment(fragment));
|
|
1607
|
+
if (!target) return;
|
|
1608
|
+
let match;
|
|
1609
|
+
doc.descendants((node, pos) => {
|
|
1610
|
+
if (match != null) return false;
|
|
1611
|
+
if (node.type.name === "heading" && (headingLookupKey(node.textContent) === target || headingLookupKey(headingDisplayText(node)) === target)) {
|
|
1612
|
+
match = pos + 1;
|
|
1613
|
+
return false;
|
|
1614
|
+
}
|
|
1615
|
+
return true;
|
|
1616
|
+
});
|
|
1617
|
+
return match;
|
|
1618
|
+
}
|
|
1619
|
+
function ProseKitEditor({ markMode = "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, linkPaste, bulletAfterHeading, substitution = true, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, ref, children }) {
|
|
1549
1620
|
const [editor] = useState(() => {
|
|
1550
1621
|
const editor = createEditor({ extension: union(defineEditorExtension({
|
|
1551
1622
|
resolveFileLink,
|
|
1623
|
+
resolveWikiEmbed,
|
|
1552
1624
|
markMode
|
|
1553
1625
|
}), defineCodeBlockView()) });
|
|
1554
1626
|
if (initialMarkdown) editor.setContent(markdownToDoc(initialMarkdown, {
|
|
@@ -1584,7 +1656,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1584
1656
|
function getState() {
|
|
1585
1657
|
return [getMarkdown(), getSelection()];
|
|
1586
1658
|
}
|
|
1587
|
-
function
|
|
1659
|
+
function replaceState(markdown, selection, addToHistory = true) {
|
|
1588
1660
|
if (markdown == null && !selection) return;
|
|
1589
1661
|
const transaction = editor.state.tr;
|
|
1590
1662
|
if (markdown != null) {
|
|
@@ -1595,6 +1667,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1595
1667
|
transaction.replaceWith(0, transaction.doc.content.size, doc.content);
|
|
1596
1668
|
}
|
|
1597
1669
|
if (selection) transaction.setSelection(resolveSelection(transaction.doc, selection)).scrollIntoView();
|
|
1670
|
+
if (!addToHistory) transaction.setMeta("addToHistory", false);
|
|
1598
1671
|
suppressDocChangeRef.current = true;
|
|
1599
1672
|
try {
|
|
1600
1673
|
editor.view.dispatch(transaction);
|
|
@@ -1602,9 +1675,16 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1602
1675
|
suppressDocChangeRef.current = false;
|
|
1603
1676
|
}
|
|
1604
1677
|
}
|
|
1678
|
+
function setState(markdown, selection) {
|
|
1679
|
+
replaceState(markdown, selection);
|
|
1680
|
+
}
|
|
1605
1681
|
function setMarkdown(markdown) {
|
|
1606
1682
|
setState(markdown);
|
|
1607
1683
|
}
|
|
1684
|
+
function refreshMarkdownRendering() {
|
|
1685
|
+
const [markdown, selection] = getState();
|
|
1686
|
+
replaceState(markdown, selection, false);
|
|
1687
|
+
}
|
|
1608
1688
|
function insertMarkdown(markdown) {
|
|
1609
1689
|
editor.commands.insertMarkdown(markdown);
|
|
1610
1690
|
}
|
|
@@ -1617,6 +1697,13 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1617
1697
|
function scrollIntoView() {
|
|
1618
1698
|
editor.commands.scrollIntoView();
|
|
1619
1699
|
}
|
|
1700
|
+
function revealHeading(fragment) {
|
|
1701
|
+
const position = findHeadingPosition(editor.state.doc, fragment);
|
|
1702
|
+
if (position == null) return false;
|
|
1703
|
+
const selection = TextSelection.near(editor.state.doc.resolve(position));
|
|
1704
|
+
editor.view.dispatch(editor.state.tr.setSelection(selection).scrollIntoView());
|
|
1705
|
+
return true;
|
|
1706
|
+
}
|
|
1620
1707
|
function getSelectedTextFromState() {
|
|
1621
1708
|
return getSelectedText(editor.state);
|
|
1622
1709
|
}
|
|
@@ -1642,10 +1729,12 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1642
1729
|
insertMarkdown,
|
|
1643
1730
|
getState,
|
|
1644
1731
|
setState,
|
|
1732
|
+
refreshMarkdownRendering,
|
|
1645
1733
|
getSelection,
|
|
1646
1734
|
setSelection,
|
|
1647
1735
|
focus,
|
|
1648
1736
|
scrollIntoView,
|
|
1737
|
+
revealHeading,
|
|
1649
1738
|
getSelectedText: getSelectedTextFromState,
|
|
1650
1739
|
openSelectionMenu: openSelectionMenuFromHandle,
|
|
1651
1740
|
startPendingReplacement,
|
|
@@ -1729,7 +1818,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1729
1818
|
|
|
1730
1819
|
//#endregion
|
|
1731
1820
|
//#region src/components/editor.tsx
|
|
1732
|
-
function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashMenuSearch, onTagSearch, onWikilinkSearch, onSelectionMenuSearch, selectionMenuAffordance = true, pendingReplacementActions, onPendingReplacementResolve, onWikilinkClick, onLinkClick, onLinkCopy, onTagClick, onExitBoundary, resolveImageUrl, resolveFileLink, 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 }) {
|
|
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 }) {
|
|
1733
1822
|
const childRef = useRef(null);
|
|
1734
1823
|
useImperativeHandle(handleRef, () => {
|
|
1735
1824
|
function getMarkdown() {
|
|
@@ -1751,6 +1840,9 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
|
|
|
1751
1840
|
function setState(markdown, selection) {
|
|
1752
1841
|
childRef.current?.setState(markdown, selection);
|
|
1753
1842
|
}
|
|
1843
|
+
function refreshMarkdownRendering() {
|
|
1844
|
+
childRef.current?.refreshMarkdownRendering();
|
|
1845
|
+
}
|
|
1754
1846
|
function getSelection() {
|
|
1755
1847
|
return childRef.current?.getSelection() ?? {
|
|
1756
1848
|
type: "text",
|
|
@@ -1767,6 +1859,9 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
|
|
|
1767
1859
|
function scrollIntoView() {
|
|
1768
1860
|
childRef.current?.scrollIntoView();
|
|
1769
1861
|
}
|
|
1862
|
+
function revealHeading(fragment) {
|
|
1863
|
+
return childRef.current?.revealHeading(fragment) ?? false;
|
|
1864
|
+
}
|
|
1770
1865
|
function getSelectedText() {
|
|
1771
1866
|
return childRef.current?.getSelectedText() ?? "";
|
|
1772
1867
|
}
|
|
@@ -1791,10 +1886,12 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
|
|
|
1791
1886
|
insertMarkdown,
|
|
1792
1887
|
getState,
|
|
1793
1888
|
setState,
|
|
1889
|
+
refreshMarkdownRendering,
|
|
1794
1890
|
getSelection,
|
|
1795
1891
|
setSelection,
|
|
1796
1892
|
focus,
|
|
1797
1893
|
scrollIntoView,
|
|
1894
|
+
revealHeading,
|
|
1798
1895
|
getSelectedText,
|
|
1799
1896
|
openSelectionMenu,
|
|
1800
1897
|
startPendingReplacement,
|
|
@@ -1827,6 +1924,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
|
|
|
1827
1924
|
onExitBoundary,
|
|
1828
1925
|
resolveImageUrl,
|
|
1829
1926
|
resolveFileLink,
|
|
1927
|
+
resolveWikiEmbed,
|
|
1830
1928
|
resolveFileInfo,
|
|
1831
1929
|
onFileClick,
|
|
1832
1930
|
onFilePaste,
|
|
@@ -2039,6 +2137,74 @@ function ImageView(props) {
|
|
|
2039
2137
|
})]
|
|
2040
2138
|
});
|
|
2041
2139
|
}
|
|
2140
|
+
function FileView(props) {
|
|
2141
|
+
const { href, name, context, children } = props;
|
|
2142
|
+
const resolveFileInfo = context.resolveFileInfo;
|
|
2143
|
+
const [resolvedSize, setResolvedSize] = useState();
|
|
2144
|
+
useEffect(() => {
|
|
2145
|
+
if (!resolveFileInfo) return;
|
|
2146
|
+
let active = true;
|
|
2147
|
+
const load = async () => {
|
|
2148
|
+
try {
|
|
2149
|
+
const info = await resolveFileInfo(href);
|
|
2150
|
+
if (!active || info?.size == null || !Number.isFinite(info.size) || info.size < 0) return;
|
|
2151
|
+
setResolvedSize({
|
|
2152
|
+
href,
|
|
2153
|
+
resolver: resolveFileInfo,
|
|
2154
|
+
text: formatFileSize(info.size)
|
|
2155
|
+
});
|
|
2156
|
+
} catch (error) {
|
|
2157
|
+
console.error("[meowdown] resolveFileInfo failed:", error);
|
|
2158
|
+
}
|
|
2159
|
+
};
|
|
2160
|
+
load();
|
|
2161
|
+
return () => {
|
|
2162
|
+
active = false;
|
|
2163
|
+
};
|
|
2164
|
+
}, [resolveFileInfo, href]);
|
|
2165
|
+
const size = resolvedSize?.href === href && resolvedSize.resolver === resolveFileInfo ? resolvedSize.text : "";
|
|
2166
|
+
const handleClick = context.onFileClick ? (event) => context.onFileClick?.({
|
|
2167
|
+
href,
|
|
2168
|
+
name,
|
|
2169
|
+
event: event.nativeEvent
|
|
2170
|
+
}) : void 0;
|
|
2171
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
2172
|
+
className: "md-file-view md-atom-view",
|
|
2173
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
2174
|
+
className: "md-file-view-preview md-atom-view-preview",
|
|
2175
|
+
"data-testid": "file-pill",
|
|
2176
|
+
"data-file-kind": getFileKind(href),
|
|
2177
|
+
contentEditable: false,
|
|
2178
|
+
title: name,
|
|
2179
|
+
onClick: handleClick,
|
|
2180
|
+
children: [
|
|
2181
|
+
/* @__PURE__ */ jsxs("svg", {
|
|
2182
|
+
className: "md-file-view-icon",
|
|
2183
|
+
viewBox: "0 0 24 24",
|
|
2184
|
+
"aria-hidden": "true",
|
|
2185
|
+
fill: "none",
|
|
2186
|
+
stroke: "currentColor",
|
|
2187
|
+
strokeWidth: "2",
|
|
2188
|
+
strokeLinecap: "round",
|
|
2189
|
+
strokeLinejoin: "round",
|
|
2190
|
+
children: [/* @__PURE__ */ jsx("path", { d: "M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z" }), /* @__PURE__ */ jsx("path", { d: "M14 2v4a2 2 0 0 0 2 2h4" })]
|
|
2191
|
+
}),
|
|
2192
|
+
/* @__PURE__ */ jsx("span", {
|
|
2193
|
+
className: "md-file-view-name",
|
|
2194
|
+
children: name
|
|
2195
|
+
}),
|
|
2196
|
+
/* @__PURE__ */ jsx("span", {
|
|
2197
|
+
className: "md-file-view-size",
|
|
2198
|
+
"data-testid": "file-pill-size",
|
|
2199
|
+
children: size
|
|
2200
|
+
})
|
|
2201
|
+
]
|
|
2202
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2203
|
+
className: "md-file-view-content md-atom-view-content",
|
|
2204
|
+
children
|
|
2205
|
+
})]
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2042
2208
|
function renderTokens(code, tokens) {
|
|
2043
2209
|
const out = [];
|
|
2044
2210
|
let pos = 0;
|
|
@@ -2140,6 +2306,15 @@ function wrapMark(mark, children, context) {
|
|
|
2140
2306
|
children
|
|
2141
2307
|
});
|
|
2142
2308
|
}
|
|
2309
|
+
case "mdFile": {
|
|
2310
|
+
const attrs = mark.attrs;
|
|
2311
|
+
return /* @__PURE__ */ jsx(FileView, {
|
|
2312
|
+
href: attrs.href,
|
|
2313
|
+
name: attrs.name,
|
|
2314
|
+
context,
|
|
2315
|
+
children
|
|
2316
|
+
});
|
|
2317
|
+
}
|
|
2143
2318
|
case "mdMath": {
|
|
2144
2319
|
const attrs = mark.attrs;
|
|
2145
2320
|
return /* @__PURE__ */ jsx(MathView, {
|
|
@@ -2200,7 +2375,10 @@ function renderRuns(runs, depth, context) {
|
|
|
2200
2375
|
function renderInline(node, context) {
|
|
2201
2376
|
const text = node.textContent;
|
|
2202
2377
|
if (!text) return null;
|
|
2203
|
-
return renderRuns(inlineTextToMarkChunks(getMarkBuilders(), text
|
|
2378
|
+
return renderRuns(inlineTextToMarkChunks(getMarkBuilders(), text, {
|
|
2379
|
+
resolveFileLink: context.resolveFileLink,
|
|
2380
|
+
resolveWikiEmbed: context.resolveWikiEmbed
|
|
2381
|
+
}).map(([from, to, marks]) => ({
|
|
2204
2382
|
text: text.slice(from, to),
|
|
2205
2383
|
marks: Mark.setFrom(marks)
|
|
2206
2384
|
})), 0, context);
|
|
@@ -2256,18 +2434,22 @@ function renderBlock(node, context) {
|
|
|
2256
2434
|
* and CSS (the root carries `ProseMirror` + `data-mark-mode` so the existing
|
|
2257
2435
|
* stylesheet applies). Requires a DOM environment.
|
|
2258
2436
|
*
|
|
2259
|
-
* Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
|
|
2437
|
+
* Callbacks (`onWikilinkClick`, etc.) and resolvers should be stable; pass them via
|
|
2260
2438
|
* `useCallback` to avoid re-rendering the whole tree.
|
|
2261
2439
|
*/
|
|
2262
|
-
function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, onTaskClick, className }) {
|
|
2440
|
+
function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }) {
|
|
2263
2441
|
const content = useMemo(() => {
|
|
2264
2442
|
const doc = markdownToDoc(markdown, { frontmatter });
|
|
2265
2443
|
const context = {
|
|
2266
2444
|
interactive,
|
|
2267
2445
|
resolveImageUrl,
|
|
2446
|
+
resolveFileLink,
|
|
2447
|
+
resolveWikiEmbed,
|
|
2448
|
+
resolveFileInfo,
|
|
2268
2449
|
onWikilinkClick: interactive ? onWikilinkClick : void 0,
|
|
2269
2450
|
onLinkClick: interactive ? onLinkClick : void 0,
|
|
2270
2451
|
onImageClick: interactive ? onImageClick : void 0,
|
|
2452
|
+
onFileClick: interactive ? onFileClick : void 0,
|
|
2271
2453
|
onTaskClick: interactive ? onTaskClick : void 0,
|
|
2272
2454
|
taskCounter: { value: 0 },
|
|
2273
2455
|
keyCounter: { value: 0 }
|
|
@@ -2278,9 +2460,13 @@ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, intera
|
|
|
2278
2460
|
frontmatter,
|
|
2279
2461
|
interactive,
|
|
2280
2462
|
resolveImageUrl,
|
|
2463
|
+
resolveFileLink,
|
|
2464
|
+
resolveWikiEmbed,
|
|
2465
|
+
resolveFileInfo,
|
|
2281
2466
|
onWikilinkClick,
|
|
2282
2467
|
onLinkClick,
|
|
2283
2468
|
onImageClick,
|
|
2469
|
+
onFileClick,
|
|
2284
2470
|
onTaskClick
|
|
2285
2471
|
]);
|
|
2286
2472
|
return /* @__PURE__ */ jsx("div", {
|
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.50.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"clsx": "^2.1.1",
|
|
27
27
|
"lucide-react": "^1.24.0",
|
|
28
28
|
"react-property": "^2.0.2",
|
|
29
|
-
"@meowdown/core": "0.
|
|
29
|
+
"@meowdown/core": "0.50.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": "^19.0.0",
|