@meowdown/react 0.47.0 → 0.49.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 +39 -5
- package/dist/index.js +194 -8
- package/package.json +8 -8
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
|
|
@@ -333,6 +351,11 @@ interface EditorProps {
|
|
|
333
351
|
* Off by default.
|
|
334
352
|
*/
|
|
335
353
|
bulletAfterHeading?: boolean;
|
|
354
|
+
/**
|
|
355
|
+
* Replaces typed character sequences with their typographic equivalents,
|
|
356
|
+
* e.g. `->` with `→` and `(c)` with `©`. On by default.
|
|
357
|
+
*/
|
|
358
|
+
substitution?: boolean;
|
|
336
359
|
/** Handles a leading `---` frontmatter block (off by default). */
|
|
337
360
|
frontmatter?: boolean;
|
|
338
361
|
/**
|
|
@@ -369,7 +392,7 @@ interface EditorProps {
|
|
|
369
392
|
/** Nodes rendered inside the editor's ProseKit context. */
|
|
370
393
|
children?: ReactNode;
|
|
371
394
|
}
|
|
372
|
-
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, 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;
|
|
373
396
|
//#endregion
|
|
374
397
|
//#region src/components/markdown-view.d.ts
|
|
375
398
|
/** Payload for {@link TaskClickHandler}. */
|
|
@@ -409,7 +432,7 @@ interface MarkdownViewProps {
|
|
|
409
432
|
/** Peel a leading YAML frontmatter block before rendering. Off by default. */
|
|
410
433
|
frontmatter?: boolean;
|
|
411
434
|
/**
|
|
412
|
-
* Whether rendered links, images, and task checkboxes can be activated.
|
|
435
|
+
* Whether rendered links, images, file pills, and task checkboxes can be activated.
|
|
413
436
|
* Defaults to `true`. When `false`, callbacks are ignored, the rendered tree
|
|
414
437
|
* contains no anchors or focusable task controls, and recognized tweet and
|
|
415
438
|
* YouTube embeds are omitted before any image resolver runs.
|
|
@@ -417,12 +440,23 @@ interface MarkdownViewProps {
|
|
|
417
440
|
interactive?: boolean;
|
|
418
441
|
/** Map an image `src` to a displayable URL, or `undefined` to skip it. */
|
|
419
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;
|
|
420
452
|
/** Called when a rendered wikilink is clicked. Pass a stable function. */
|
|
421
453
|
onWikilinkClick?: WikilinkClickHandler;
|
|
422
454
|
/** Called when a rendered Markdown link is clicked. Pass a stable function. */
|
|
423
455
|
onLinkClick?: LinkClickHandler;
|
|
424
456
|
/** Called when a rendered image is clicked. Pass a stable function. */
|
|
425
457
|
onImageClick?: ImageClickHandler;
|
|
458
|
+
/** Called when a rendered file pill is clicked. Pass a stable function. */
|
|
459
|
+
onFileClick?: FileClickHandler;
|
|
426
460
|
/** Called when a rendered task checkbox is clicked. Pass a stable function. */
|
|
427
461
|
onTaskClick?: TaskClickHandler;
|
|
428
462
|
/** Extra class on the content root (alongside `ProseMirror meowdown-content`). */
|
|
@@ -436,10 +470,10 @@ interface MarkdownViewProps {
|
|
|
436
470
|
* and CSS (the root carries `ProseMirror` + `data-mark-mode` so the existing
|
|
437
471
|
* stylesheet applies). Requires a DOM environment.
|
|
438
472
|
*
|
|
439
|
-
* Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
|
|
473
|
+
* Callbacks (`onWikilinkClick`, etc.) and resolvers should be stable; pass them via
|
|
440
474
|
* `useCallback` to avoid re-rendering the whole tree.
|
|
441
475
|
*/
|
|
442
|
-
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;
|
|
443
477
|
//#endregion
|
|
444
478
|
//#region src/components/wikilink-hover-card.d.ts
|
|
445
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, 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";
|
|
@@ -295,7 +295,7 @@ function DropIndicator$1() {
|
|
|
295
295
|
|
|
296
296
|
//#endregion
|
|
297
297
|
//#region src/components/editor-extensions.tsx
|
|
298
|
-
function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, placeholder, readOnly, wikilinkEnabled, spellCheck }) {
|
|
298
|
+
function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick, onTagClick, onExitBoundary, resolveImageUrl, resolveFileInfo, onFileClick, onFilePaste, onFileSaveError, onImageClick, embedPaste, linkPaste, bulletAfterHeading, substitution, placeholder, readOnly, wikilinkEnabled, spellCheck }) {
|
|
299
299
|
const editor = useEditor$1();
|
|
300
300
|
useEffect(() => {
|
|
301
301
|
editor.commands.setMarkMode(markMode);
|
|
@@ -358,6 +358,9 @@ function EditorExtensions({ markMode, onDocChange, onWikilinkClick, onLinkClick,
|
|
|
358
358
|
useExtension$1(useMemo(() => {
|
|
359
359
|
return bulletAfterHeading ? defineBulletAfterHeading() : null;
|
|
360
360
|
}, [bulletAfterHeading]));
|
|
361
|
+
useExtension$1(useMemo(() => {
|
|
362
|
+
return substitution ? defineSubstitution() : null;
|
|
363
|
+
}, [substitution]));
|
|
361
364
|
useExtension$1(useMemo(() => {
|
|
362
365
|
return placeholder ? definePlaceholder({
|
|
363
366
|
placeholder,
|
|
@@ -1542,10 +1545,77 @@ function resolveSelection(doc, selection) {
|
|
|
1542
1545
|
return TextSelection.between(doc.resolve(anchor), doc.resolve(head));
|
|
1543
1546
|
}
|
|
1544
1547
|
}
|
|
1545
|
-
function
|
|
1548
|
+
function decodeHeadingFragment(fragment) {
|
|
1549
|
+
const source = fragment.startsWith("#") ? fragment.slice(1) : fragment;
|
|
1550
|
+
try {
|
|
1551
|
+
return decodeURIComponent(source);
|
|
1552
|
+
} catch {
|
|
1553
|
+
return source;
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
function headingLookupKey(value) {
|
|
1557
|
+
return value.normalize("NFKC").trim().replaceAll(/\s+/g, " ").toLowerCase();
|
|
1558
|
+
}
|
|
1559
|
+
const HEADING_HIDDEN_MARKS = /* @__PURE__ */ new Set([
|
|
1560
|
+
"mdMark",
|
|
1561
|
+
"mdLinkUri",
|
|
1562
|
+
"mdLinkTitle"
|
|
1563
|
+
]);
|
|
1564
|
+
const HEADING_ATOM_MARKS = /* @__PURE__ */ new Set([
|
|
1565
|
+
"mdWikilink",
|
|
1566
|
+
"mdImage",
|
|
1567
|
+
"mdFile",
|
|
1568
|
+
"mdMath"
|
|
1569
|
+
]);
|
|
1570
|
+
function headingAtomText(mark) {
|
|
1571
|
+
const attrs = mark.attrs;
|
|
1572
|
+
const stringAttr = (name) => {
|
|
1573
|
+
const value = attrs[name];
|
|
1574
|
+
return typeof value === "string" ? value : "";
|
|
1575
|
+
};
|
|
1576
|
+
if (mark.type.name === "mdWikilink") return stringAttr("display") || stringAttr("target");
|
|
1577
|
+
if (mark.type.name === "mdImage") return stringAttr("alt");
|
|
1578
|
+
if (mark.type.name === "mdFile") return stringAttr("name");
|
|
1579
|
+
if (mark.type.name === "mdMath") return stringAttr("formula");
|
|
1580
|
+
return "";
|
|
1581
|
+
}
|
|
1582
|
+
/** The heading as its live-preview marks display it, with syntax runs omitted. */
|
|
1583
|
+
function headingDisplayText(heading) {
|
|
1584
|
+
let output = "";
|
|
1585
|
+
let previousAtom;
|
|
1586
|
+
heading.forEach((child) => {
|
|
1587
|
+
if (!child.isText || !child.text) return;
|
|
1588
|
+
const atom = child.marks.find((mark) => HEADING_ATOM_MARKS.has(mark.type.name));
|
|
1589
|
+
if (atom) {
|
|
1590
|
+
if (!previousAtom?.eq(atom)) output += headingAtomText(atom);
|
|
1591
|
+
previousAtom = atom;
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
previousAtom = void 0;
|
|
1595
|
+
if (child.marks.some((mark) => HEADING_HIDDEN_MARKS.has(mark.type.name))) return;
|
|
1596
|
+
output += child.text;
|
|
1597
|
+
});
|
|
1598
|
+
return output;
|
|
1599
|
+
}
|
|
1600
|
+
function findHeadingPosition(doc, fragment) {
|
|
1601
|
+
const target = headingLookupKey(decodeHeadingFragment(fragment));
|
|
1602
|
+
if (!target) return;
|
|
1603
|
+
let match;
|
|
1604
|
+
doc.descendants((node, pos) => {
|
|
1605
|
+
if (match != null) return false;
|
|
1606
|
+
if (node.type.name === "heading" && (headingLookupKey(node.textContent) === target || headingLookupKey(headingDisplayText(node)) === target)) {
|
|
1607
|
+
match = pos + 1;
|
|
1608
|
+
return false;
|
|
1609
|
+
}
|
|
1610
|
+
return true;
|
|
1611
|
+
});
|
|
1612
|
+
return match;
|
|
1613
|
+
}
|
|
1614
|
+
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 }) {
|
|
1546
1615
|
const [editor] = useState(() => {
|
|
1547
1616
|
const editor = createEditor({ extension: union(defineEditorExtension({
|
|
1548
1617
|
resolveFileLink,
|
|
1618
|
+
resolveWikiEmbed,
|
|
1549
1619
|
markMode
|
|
1550
1620
|
}), defineCodeBlockView()) });
|
|
1551
1621
|
if (initialMarkdown) editor.setContent(markdownToDoc(initialMarkdown, {
|
|
@@ -1581,7 +1651,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1581
1651
|
function getState() {
|
|
1582
1652
|
return [getMarkdown(), getSelection()];
|
|
1583
1653
|
}
|
|
1584
|
-
function
|
|
1654
|
+
function replaceState(markdown, selection, addToHistory = true) {
|
|
1585
1655
|
if (markdown == null && !selection) return;
|
|
1586
1656
|
const transaction = editor.state.tr;
|
|
1587
1657
|
if (markdown != null) {
|
|
@@ -1592,6 +1662,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1592
1662
|
transaction.replaceWith(0, transaction.doc.content.size, doc.content);
|
|
1593
1663
|
}
|
|
1594
1664
|
if (selection) transaction.setSelection(resolveSelection(transaction.doc, selection)).scrollIntoView();
|
|
1665
|
+
if (!addToHistory) transaction.setMeta("addToHistory", false);
|
|
1595
1666
|
suppressDocChangeRef.current = true;
|
|
1596
1667
|
try {
|
|
1597
1668
|
editor.view.dispatch(transaction);
|
|
@@ -1599,9 +1670,16 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1599
1670
|
suppressDocChangeRef.current = false;
|
|
1600
1671
|
}
|
|
1601
1672
|
}
|
|
1673
|
+
function setState(markdown, selection) {
|
|
1674
|
+
replaceState(markdown, selection);
|
|
1675
|
+
}
|
|
1602
1676
|
function setMarkdown(markdown) {
|
|
1603
1677
|
setState(markdown);
|
|
1604
1678
|
}
|
|
1679
|
+
function refreshMarkdownRendering() {
|
|
1680
|
+
const [markdown, selection] = getState();
|
|
1681
|
+
replaceState(markdown, selection, false);
|
|
1682
|
+
}
|
|
1605
1683
|
function insertMarkdown(markdown) {
|
|
1606
1684
|
editor.commands.insertMarkdown(markdown);
|
|
1607
1685
|
}
|
|
@@ -1614,6 +1692,13 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1614
1692
|
function scrollIntoView() {
|
|
1615
1693
|
editor.commands.scrollIntoView();
|
|
1616
1694
|
}
|
|
1695
|
+
function revealHeading(fragment) {
|
|
1696
|
+
const position = findHeadingPosition(editor.state.doc, fragment);
|
|
1697
|
+
if (position == null) return false;
|
|
1698
|
+
const selection = TextSelection.near(editor.state.doc.resolve(position));
|
|
1699
|
+
editor.view.dispatch(editor.state.tr.setSelection(selection).scrollIntoView());
|
|
1700
|
+
return true;
|
|
1701
|
+
}
|
|
1617
1702
|
function getSelectedTextFromState() {
|
|
1618
1703
|
return getSelectedText(editor.state);
|
|
1619
1704
|
}
|
|
@@ -1639,10 +1724,12 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1639
1724
|
insertMarkdown,
|
|
1640
1725
|
getState,
|
|
1641
1726
|
setState,
|
|
1727
|
+
refreshMarkdownRendering,
|
|
1642
1728
|
getSelection,
|
|
1643
1729
|
setSelection,
|
|
1644
1730
|
focus,
|
|
1645
1731
|
scrollIntoView,
|
|
1732
|
+
revealHeading,
|
|
1646
1733
|
getSelectedText: getSelectedTextFromState,
|
|
1647
1734
|
openSelectionMenu: openSelectionMenuFromHandle,
|
|
1648
1735
|
startPendingReplacement,
|
|
@@ -1687,6 +1774,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1687
1774
|
embedPaste,
|
|
1688
1775
|
linkPaste,
|
|
1689
1776
|
bulletAfterHeading,
|
|
1777
|
+
substitution,
|
|
1690
1778
|
placeholder,
|
|
1691
1779
|
readOnly,
|
|
1692
1780
|
wikilinkEnabled: !!onWikilinkSearch,
|
|
@@ -1725,7 +1813,7 @@ function ProseKitEditor({ markMode = "focus", initialMarkdown, onDocChange, onSl
|
|
|
1725
1813
|
|
|
1726
1814
|
//#endregion
|
|
1727
1815
|
//#region src/components/editor.tsx
|
|
1728
|
-
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, frontmatter = false, blockHandle = true, placeholder, readOnly, spellCheck, timeFormat, editorClassName, wrapperClassName, handleRef, children }) {
|
|
1816
|
+
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 }) {
|
|
1729
1817
|
const childRef = useRef(null);
|
|
1730
1818
|
useImperativeHandle(handleRef, () => {
|
|
1731
1819
|
function getMarkdown() {
|
|
@@ -1747,6 +1835,9 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
|
|
|
1747
1835
|
function setState(markdown, selection) {
|
|
1748
1836
|
childRef.current?.setState(markdown, selection);
|
|
1749
1837
|
}
|
|
1838
|
+
function refreshMarkdownRendering() {
|
|
1839
|
+
childRef.current?.refreshMarkdownRendering();
|
|
1840
|
+
}
|
|
1750
1841
|
function getSelection() {
|
|
1751
1842
|
return childRef.current?.getSelection() ?? {
|
|
1752
1843
|
type: "text",
|
|
@@ -1763,6 +1854,9 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
|
|
|
1763
1854
|
function scrollIntoView() {
|
|
1764
1855
|
childRef.current?.scrollIntoView();
|
|
1765
1856
|
}
|
|
1857
|
+
function revealHeading(fragment) {
|
|
1858
|
+
return childRef.current?.revealHeading(fragment) ?? false;
|
|
1859
|
+
}
|
|
1766
1860
|
function getSelectedText() {
|
|
1767
1861
|
return childRef.current?.getSelectedText() ?? "";
|
|
1768
1862
|
}
|
|
@@ -1787,10 +1881,12 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
|
|
|
1787
1881
|
insertMarkdown,
|
|
1788
1882
|
getState,
|
|
1789
1883
|
setState,
|
|
1884
|
+
refreshMarkdownRendering,
|
|
1790
1885
|
getSelection,
|
|
1791
1886
|
setSelection,
|
|
1792
1887
|
focus,
|
|
1793
1888
|
scrollIntoView,
|
|
1889
|
+
revealHeading,
|
|
1794
1890
|
getSelectedText,
|
|
1795
1891
|
openSelectionMenu,
|
|
1796
1892
|
startPendingReplacement,
|
|
@@ -1823,6 +1919,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
|
|
|
1823
1919
|
onExitBoundary,
|
|
1824
1920
|
resolveImageUrl,
|
|
1825
1921
|
resolveFileLink,
|
|
1922
|
+
resolveWikiEmbed,
|
|
1826
1923
|
resolveFileInfo,
|
|
1827
1924
|
onFileClick,
|
|
1828
1925
|
onFilePaste,
|
|
@@ -1831,6 +1928,7 @@ function MeowdownEditor({ mode = "focus", initialMarkdown, onDocChange, onSlashM
|
|
|
1831
1928
|
embedPaste,
|
|
1832
1929
|
linkPaste,
|
|
1833
1930
|
bulletAfterHeading,
|
|
1931
|
+
substitution,
|
|
1834
1932
|
frontmatter,
|
|
1835
1933
|
blockHandle,
|
|
1836
1934
|
placeholder,
|
|
@@ -2034,6 +2132,74 @@ function ImageView(props) {
|
|
|
2034
2132
|
})]
|
|
2035
2133
|
});
|
|
2036
2134
|
}
|
|
2135
|
+
function FileView(props) {
|
|
2136
|
+
const { href, name, context, children } = props;
|
|
2137
|
+
const resolveFileInfo = context.resolveFileInfo;
|
|
2138
|
+
const [resolvedSize, setResolvedSize] = useState();
|
|
2139
|
+
useEffect(() => {
|
|
2140
|
+
if (!resolveFileInfo) return;
|
|
2141
|
+
let active = true;
|
|
2142
|
+
const load = async () => {
|
|
2143
|
+
try {
|
|
2144
|
+
const info = await resolveFileInfo(href);
|
|
2145
|
+
if (!active || info?.size == null || !Number.isFinite(info.size) || info.size < 0) return;
|
|
2146
|
+
setResolvedSize({
|
|
2147
|
+
href,
|
|
2148
|
+
resolver: resolveFileInfo,
|
|
2149
|
+
text: formatFileSize(info.size)
|
|
2150
|
+
});
|
|
2151
|
+
} catch (error) {
|
|
2152
|
+
console.error("[meowdown] resolveFileInfo failed:", error);
|
|
2153
|
+
}
|
|
2154
|
+
};
|
|
2155
|
+
load();
|
|
2156
|
+
return () => {
|
|
2157
|
+
active = false;
|
|
2158
|
+
};
|
|
2159
|
+
}, [resolveFileInfo, href]);
|
|
2160
|
+
const size = resolvedSize?.href === href && resolvedSize.resolver === resolveFileInfo ? resolvedSize.text : "";
|
|
2161
|
+
const handleClick = context.onFileClick ? (event) => context.onFileClick?.({
|
|
2162
|
+
href,
|
|
2163
|
+
name,
|
|
2164
|
+
event: event.nativeEvent
|
|
2165
|
+
}) : void 0;
|
|
2166
|
+
return /* @__PURE__ */ jsxs("span", {
|
|
2167
|
+
className: "md-file-view md-atom-view",
|
|
2168
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
2169
|
+
className: "md-file-view-preview md-atom-view-preview",
|
|
2170
|
+
"data-testid": "file-pill",
|
|
2171
|
+
"data-file-kind": getFileKind(href),
|
|
2172
|
+
contentEditable: false,
|
|
2173
|
+
title: name,
|
|
2174
|
+
onClick: handleClick,
|
|
2175
|
+
children: [
|
|
2176
|
+
/* @__PURE__ */ jsxs("svg", {
|
|
2177
|
+
className: "md-file-view-icon",
|
|
2178
|
+
viewBox: "0 0 24 24",
|
|
2179
|
+
"aria-hidden": "true",
|
|
2180
|
+
fill: "none",
|
|
2181
|
+
stroke: "currentColor",
|
|
2182
|
+
strokeWidth: "2",
|
|
2183
|
+
strokeLinecap: "round",
|
|
2184
|
+
strokeLinejoin: "round",
|
|
2185
|
+
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" })]
|
|
2186
|
+
}),
|
|
2187
|
+
/* @__PURE__ */ jsx("span", {
|
|
2188
|
+
className: "md-file-view-name",
|
|
2189
|
+
children: name
|
|
2190
|
+
}),
|
|
2191
|
+
/* @__PURE__ */ jsx("span", {
|
|
2192
|
+
className: "md-file-view-size",
|
|
2193
|
+
"data-testid": "file-pill-size",
|
|
2194
|
+
children: size
|
|
2195
|
+
})
|
|
2196
|
+
]
|
|
2197
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2198
|
+
className: "md-file-view-content md-atom-view-content",
|
|
2199
|
+
children
|
|
2200
|
+
})]
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2037
2203
|
function renderTokens(code, tokens) {
|
|
2038
2204
|
const out = [];
|
|
2039
2205
|
let pos = 0;
|
|
@@ -2135,6 +2301,15 @@ function wrapMark(mark, children, context) {
|
|
|
2135
2301
|
children
|
|
2136
2302
|
});
|
|
2137
2303
|
}
|
|
2304
|
+
case "mdFile": {
|
|
2305
|
+
const attrs = mark.attrs;
|
|
2306
|
+
return /* @__PURE__ */ jsx(FileView, {
|
|
2307
|
+
href: attrs.href,
|
|
2308
|
+
name: attrs.name,
|
|
2309
|
+
context,
|
|
2310
|
+
children
|
|
2311
|
+
});
|
|
2312
|
+
}
|
|
2138
2313
|
case "mdMath": {
|
|
2139
2314
|
const attrs = mark.attrs;
|
|
2140
2315
|
return /* @__PURE__ */ jsx(MathView, {
|
|
@@ -2195,7 +2370,10 @@ function renderRuns(runs, depth, context) {
|
|
|
2195
2370
|
function renderInline(node, context) {
|
|
2196
2371
|
const text = node.textContent;
|
|
2197
2372
|
if (!text) return null;
|
|
2198
|
-
return renderRuns(inlineTextToMarkChunks(getMarkBuilders(), text
|
|
2373
|
+
return renderRuns(inlineTextToMarkChunks(getMarkBuilders(), text, {
|
|
2374
|
+
resolveFileLink: context.resolveFileLink,
|
|
2375
|
+
resolveWikiEmbed: context.resolveWikiEmbed
|
|
2376
|
+
}).map(([from, to, marks]) => ({
|
|
2199
2377
|
text: text.slice(from, to),
|
|
2200
2378
|
marks: Mark.setFrom(marks)
|
|
2201
2379
|
})), 0, context);
|
|
@@ -2251,18 +2429,22 @@ function renderBlock(node, context) {
|
|
|
2251
2429
|
* and CSS (the root carries `ProseMirror` + `data-mark-mode` so the existing
|
|
2252
2430
|
* stylesheet applies). Requires a DOM environment.
|
|
2253
2431
|
*
|
|
2254
|
-
* Callbacks (`onWikilinkClick`, etc.) should be stable; pass them via
|
|
2432
|
+
* Callbacks (`onWikilinkClick`, etc.) and resolvers should be stable; pass them via
|
|
2255
2433
|
* `useCallback` to avoid re-rendering the whole tree.
|
|
2256
2434
|
*/
|
|
2257
|
-
function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, resolveImageUrl, onWikilinkClick, onLinkClick, onImageClick, onTaskClick, className }) {
|
|
2435
|
+
function MarkdownView({ markdown, markMode = "hide", frontmatter = false, interactive = true, resolveImageUrl, resolveFileLink, resolveWikiEmbed, resolveFileInfo, onWikilinkClick, onLinkClick, onImageClick, onFileClick, onTaskClick, className }) {
|
|
2258
2436
|
const content = useMemo(() => {
|
|
2259
2437
|
const doc = markdownToDoc(markdown, { frontmatter });
|
|
2260
2438
|
const context = {
|
|
2261
2439
|
interactive,
|
|
2262
2440
|
resolveImageUrl,
|
|
2441
|
+
resolveFileLink,
|
|
2442
|
+
resolveWikiEmbed,
|
|
2443
|
+
resolveFileInfo,
|
|
2263
2444
|
onWikilinkClick: interactive ? onWikilinkClick : void 0,
|
|
2264
2445
|
onLinkClick: interactive ? onLinkClick : void 0,
|
|
2265
2446
|
onImageClick: interactive ? onImageClick : void 0,
|
|
2447
|
+
onFileClick: interactive ? onFileClick : void 0,
|
|
2266
2448
|
onTaskClick: interactive ? onTaskClick : void 0,
|
|
2267
2449
|
taskCounter: { value: 0 },
|
|
2268
2450
|
keyCounter: { value: 0 }
|
|
@@ -2273,9 +2455,13 @@ function MarkdownView({ markdown, markMode = "hide", frontmatter = false, intera
|
|
|
2273
2455
|
frontmatter,
|
|
2274
2456
|
interactive,
|
|
2275
2457
|
resolveImageUrl,
|
|
2458
|
+
resolveFileLink,
|
|
2459
|
+
resolveWikiEmbed,
|
|
2460
|
+
resolveFileInfo,
|
|
2276
2461
|
onWikilinkClick,
|
|
2277
2462
|
onLinkClick,
|
|
2278
2463
|
onImageClick,
|
|
2464
|
+
onFileClick,
|
|
2279
2465
|
onTaskClick
|
|
2280
2466
|
]);
|
|
2281
2467
|
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.49.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
"@prosekit/pm": "^0.1.19-beta.3",
|
|
25
25
|
"@prosekit/react": "^0.8.0-beta.21",
|
|
26
26
|
"clsx": "^2.1.1",
|
|
27
|
-
"lucide-react": "^1.
|
|
27
|
+
"lucide-react": "^1.24.0",
|
|
28
28
|
"react-property": "^2.0.2",
|
|
29
|
-
"@meowdown/core": "0.
|
|
29
|
+
"@meowdown/core": "0.49.0"
|
|
30
30
|
},
|
|
31
31
|
"peerDependencies": {
|
|
32
32
|
"react": "^19.0.0",
|
|
@@ -41,17 +41,17 @@
|
|
|
41
41
|
}
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@css-modules-kit/codegen": "^1.
|
|
44
|
+
"@css-modules-kit/codegen": "^1.4.0",
|
|
45
45
|
"@ocavue/tsconfig": "^0.7.1",
|
|
46
|
-
"@tsdown/css": "^0.22.
|
|
46
|
+
"@tsdown/css": "^0.22.7",
|
|
47
47
|
"@types/react": "^19.2.17",
|
|
48
48
|
"@types/react-dom": "^19.2.3",
|
|
49
|
-
"@vitest/browser-playwright": "^4.1.
|
|
49
|
+
"@vitest/browser-playwright": "^4.1.10",
|
|
50
50
|
"dedent": "^1.7.2",
|
|
51
51
|
"react": "^19.2.7",
|
|
52
52
|
"react-dom": "^19.2.7",
|
|
53
|
-
"tsdown": "^0.22.
|
|
54
|
-
"vitest": "^4.1.
|
|
53
|
+
"tsdown": "^0.22.7",
|
|
54
|
+
"vitest": "^4.1.10",
|
|
55
55
|
"vitest-browser-commands": "^0.2.1",
|
|
56
56
|
"vitest-browser-react": "^2.2.0",
|
|
57
57
|
"@meowdown/vitest": "0.0.0"
|