@mp-lb/mdkit 0.2.5 → 0.3.1
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 +1 -0
- package/dist/document/useMdKitDocument.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/markdown/MarkdownBubbleMenu.js +4 -2
- package/dist/markdown/MarkdownPasteExtension.d.ts +3 -0
- package/dist/markdown/MarkdownPasteExtension.js +62 -0
- package/dist/markdown/MarkdownSearchExtension.d.ts +9 -0
- package/dist/markdown/MarkdownSearchExtension.js +42 -0
- package/dist/markdown/MarkdownSearchPanel.d.ts +13 -0
- package/dist/markdown/MarkdownSearchPanel.js +25 -0
- package/dist/markdown/MdKitEditor.d.ts +2 -0
- package/dist/markdown/MdKitView.d.ts +2 -1
- package/dist/markdown/MdKitView.js +6 -2
- package/dist/markdown/TiptapMarkdownSurface.d.ts +4 -0
- package/dist/markdown/TiptapMarkdownSurface.js +168 -7
- package/dist/markdown/createMdKitTiptapExtensions.js +5 -1
- package/dist/markdown/yamlFrontMatter.d.ts +16 -0
- package/dist/markdown/yamlFrontMatter.js +88 -0
- package/dist/theme/editorTheme.js +4 -4
- package/dist/yjs/MdKitMarkdownYjs.d.ts +1 -0
- package/dist/yjs/MdKitMarkdownYjs.js +23 -4
- package/docs/.vitepress/config.ts +2 -0
- package/docs/api.md +6 -0
- package/docs/index.md +5 -1
- package/docs/plain-text.md +131 -0
- package/docs/shadcn.md +5 -1
- package/docs/styling.md +6 -1
- package/package.json +2 -1
- package/src/styles.css +230 -13
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ const emptyDocumentState = {
|
|
|
5
5
|
version: null,
|
|
6
6
|
};
|
|
7
7
|
export const useMdKitDocument = (options) => {
|
|
8
|
-
const { adapter, debounceMs =
|
|
8
|
+
const { adapter, debounceMs = 1000, documentId, pollMs = 2000 } = options;
|
|
9
9
|
const [local, setLocal] = useState("");
|
|
10
10
|
const [base, setBase] = useState("");
|
|
11
11
|
const [version, setVersion] = useState(null);
|
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { MdKitConflictPanel } from "./document/MdKitConflictPanel.js";
|
|
|
6
6
|
export { MdKitDocumentToolbar } from "./document/MdKitDocumentToolbar.js";
|
|
7
7
|
export { MdKitEditor } from "./markdown/MdKitEditor.js";
|
|
8
8
|
export { MdKitView } from "./markdown/MdKitView.js";
|
|
9
|
+
export { extractYamlFrontMatter, hasYamlFrontMatter, parseYamlFrontMatter, prependYamlFrontMatter, removeYamlFrontMatter, } from "./markdown/yamlFrontMatter.js";
|
|
9
10
|
export { MdKitThemeEditor } from "./theme/MdKitThemeEditor.js";
|
|
10
11
|
export { createMdKitRestAdapter } from "./transport/rest.js";
|
|
11
12
|
export { createMdKitEditorThemeStyle, darkMdKitEditorTheme, defaultMdKitEditorTheme, } from "./theme/editorTheme.js";
|
|
@@ -21,6 +22,7 @@ export type { MdKitDocumentToolbarProps } from "./document/MdKitDocumentToolbar.
|
|
|
21
22
|
export type { MdKitEditorProps } from "./markdown/MdKitEditor.js";
|
|
22
23
|
export type { MdKitEditorDebugEvent } from "./markdown/editorDebug.js";
|
|
23
24
|
export type { MdKitViewProps } from "./markdown/MdKitView.js";
|
|
25
|
+
export type { MdKitYamlFrontMatter, MdKitYamlFrontMatterExtraction, } from "./markdown/yamlFrontMatter.js";
|
|
24
26
|
export type { MdKitThemeEditorProps } from "./theme/MdKitThemeEditor.js";
|
|
25
27
|
export type { CreateMdKitRestAdapterOptions } from "./transport/rest.js";
|
|
26
28
|
export type { MdKitEditorTheme, MdKitEditorThemeStyle, } from "./theme/editorTheme.js";
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ export { MdKitConflictPanel } from "./document/MdKitConflictPanel.js";
|
|
|
6
6
|
export { MdKitDocumentToolbar } from "./document/MdKitDocumentToolbar.js";
|
|
7
7
|
export { MdKitEditor } from "./markdown/MdKitEditor.js";
|
|
8
8
|
export { MdKitView } from "./markdown/MdKitView.js";
|
|
9
|
+
export { extractYamlFrontMatter, hasYamlFrontMatter, parseYamlFrontMatter, prependYamlFrontMatter, removeYamlFrontMatter, } from "./markdown/yamlFrontMatter.js";
|
|
9
10
|
export { MdKitThemeEditor } from "./theme/MdKitThemeEditor.js";
|
|
10
11
|
export { createMdKitRestAdapter } from "./transport/rest.js";
|
|
11
12
|
export { createMdKitEditorThemeStyle, darkMdKitEditorTheme, defaultMdKitEditorTheme, } from "./theme/editorTheme.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEditorState } from "@tiptap/react";
|
|
3
3
|
import { BubbleMenu } from "@tiptap/react/menus";
|
|
4
|
-
import { Bold, Code2, Heading1, Heading2, Italic, Link2, List, ListOrdered, Quote, Strikethrough, } from "lucide-react";
|
|
4
|
+
import { Bold, Code2, Heading1, Heading2, Heading3, Italic, Link2, List, ListOrdered, Quote, Strikethrough, } from "lucide-react";
|
|
5
5
|
import { joinClassNames } from "../ui/joinClassNames.js";
|
|
6
6
|
const toolbarActiveStateIsEqual = (left, right) => {
|
|
7
7
|
if (!right) {
|
|
@@ -13,6 +13,7 @@ const toolbarActiveStateIsEqual = (left, right) => {
|
|
|
13
13
|
left.codeBlock === right.codeBlock &&
|
|
14
14
|
left.heading1 === right.heading1 &&
|
|
15
15
|
left.heading2 === right.heading2 &&
|
|
16
|
+
left.heading3 === right.heading3 &&
|
|
16
17
|
left.italic === right.italic &&
|
|
17
18
|
left.link === right.link &&
|
|
18
19
|
left.orderedList === right.orderedList &&
|
|
@@ -27,6 +28,7 @@ const useToolbarActiveState = (editor) => useEditorState({
|
|
|
27
28
|
codeBlock: currentEditor.isActive("codeBlock"),
|
|
28
29
|
heading1: currentEditor.isActive("heading", { level: 1 }),
|
|
29
30
|
heading2: currentEditor.isActive("heading", { level: 2 }),
|
|
31
|
+
heading3: currentEditor.isActive("heading", { level: 3 }),
|
|
30
32
|
italic: currentEditor.isActive("italic"),
|
|
31
33
|
link: currentEditor.isActive("link"),
|
|
32
34
|
orderedList: currentEditor.isActive("orderedList"),
|
|
@@ -80,5 +82,5 @@ export const MarkdownBubbleMenu = ({ editor }) => {
|
|
|
80
82
|
const activeState = useToolbarActiveState(editor);
|
|
81
83
|
return (_jsxs(BubbleMenu, { className: "mp-lb-mdkit-toolbar", editor: editor, options: {
|
|
82
84
|
placement: "top",
|
|
83
|
-
}, shouldShow: shouldShowMarkdownBubbleMenu, children: [_jsx(ToolbarButton, { ariaLabel: "Bold", isActive: activeState.bold, onAction: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, {}) }), _jsx(ToolbarButton, { ariaLabel: "Italic", isActive: activeState.italic, onAction: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, {}) }), _jsx(ToolbarButton, { ariaLabel: "Strikethrough", isActive: activeState.strike, onAction: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, {}) }), _jsx(ToolbarButton, { ariaLabel: "Code block", isActive: activeState.codeBlock, onAction: () => editor.chain().focus().toggleCodeBlock().run(), children: _jsx(Code2, {}) }), _jsx("div", { className: "mp-lb-mdkit-toolbar-divider" }), _jsx(ToolbarButton, { ariaLabel: "Heading 1", isActive: activeState.heading1, onAction: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), children: _jsx(Heading1, {}) }), _jsx(ToolbarButton, { ariaLabel: "Heading 2", isActive: activeState.heading2, onAction: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), children: _jsx(Heading2, {}) }), _jsx("div", { className: "mp-lb-mdkit-toolbar-divider" }), _jsx(ToolbarButton, { ariaLabel: "Bullet list", isActive: activeState.bulletList, onAction: () => editor.chain().focus().toggleBulletList().run(), children: _jsx(List, {}) }), _jsx(ToolbarButton, { ariaLabel: "Ordered list", isActive: activeState.orderedList, onAction: () => editor.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrdered, {}) }), _jsx(ToolbarButton, { ariaLabel: "Blockquote", isActive: activeState.blockquote, onAction: () => editor.chain().focus().toggleBlockquote().run(), children: _jsx(Quote, {}) }), _jsx("div", { className: "mp-lb-mdkit-toolbar-divider" }), _jsx(ToolbarButton, { ariaLabel: "Link", isActive: activeState.link, onAction: () => setLink(editor), children: _jsx(Link2, {}) })] }));
|
|
85
|
+
}, shouldShow: shouldShowMarkdownBubbleMenu, children: [_jsx(ToolbarButton, { ariaLabel: "Bold", isActive: activeState.bold, onAction: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, {}) }), _jsx(ToolbarButton, { ariaLabel: "Italic", isActive: activeState.italic, onAction: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, {}) }), _jsx(ToolbarButton, { ariaLabel: "Strikethrough", isActive: activeState.strike, onAction: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, {}) }), _jsx(ToolbarButton, { ariaLabel: "Code block", isActive: activeState.codeBlock, onAction: () => editor.chain().focus().toggleCodeBlock().run(), children: _jsx(Code2, {}) }), _jsx("div", { className: "mp-lb-mdkit-toolbar-divider" }), _jsx(ToolbarButton, { ariaLabel: "Heading 1", isActive: activeState.heading1, onAction: () => editor.chain().focus().toggleHeading({ level: 1 }).run(), children: _jsx(Heading1, {}) }), _jsx(ToolbarButton, { ariaLabel: "Heading 2", isActive: activeState.heading2, onAction: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), children: _jsx(Heading2, {}) }), _jsx(ToolbarButton, { ariaLabel: "Heading 3", isActive: activeState.heading3, onAction: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), children: _jsx(Heading3, {}) }), _jsx("div", { className: "mp-lb-mdkit-toolbar-divider" }), _jsx(ToolbarButton, { ariaLabel: "Bullet list", isActive: activeState.bulletList, onAction: () => editor.chain().focus().toggleBulletList().run(), children: _jsx(List, {}) }), _jsx(ToolbarButton, { ariaLabel: "Ordered list", isActive: activeState.orderedList, onAction: () => editor.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrdered, {}) }), _jsx(ToolbarButton, { ariaLabel: "Blockquote", isActive: activeState.blockquote, onAction: () => editor.chain().focus().toggleBlockquote().run(), children: _jsx(Quote, {}) }), _jsx("div", { className: "mp-lb-mdkit-toolbar-divider" }), _jsx(ToolbarButton, { ariaLabel: "Link", isActive: activeState.link, onAction: () => setLink(editor), children: _jsx(Link2, {}) })] }));
|
|
84
86
|
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
const markdownPastePluginKey = new PluginKey("mdkitMarkdownPaste");
|
|
4
|
+
const blockMarkdownPatterns = [
|
|
5
|
+
/^ {0,3}#{1,6}\s+\S/m,
|
|
6
|
+
/^ {0,3}(?:[-+*]|\d+[.)])\s+\S/m,
|
|
7
|
+
/^ {0,3}>\s+\S/m,
|
|
8
|
+
/^ {0,3}(?:```|~~~)/m,
|
|
9
|
+
/^ {0,3}(?:-{3,}|\*{3,}|_{3,})\s*$/m,
|
|
10
|
+
/^\|.+\|\s*\n\|(?:\s*:?-{3,}:?\s*\|)+/m,
|
|
11
|
+
];
|
|
12
|
+
const inlineMarkdownPatterns = [
|
|
13
|
+
/!\[[^\]]*]\([^)]+\)/,
|
|
14
|
+
/\[[^\]]+]\([^)]+\)/,
|
|
15
|
+
/(^|[^\w])(?:\*\*|__)\S[\s\S]*?\S(?:\*\*|__)($|[^\w])/,
|
|
16
|
+
/(^|[^\w])`[^`\n]+`($|[^\w])/,
|
|
17
|
+
];
|
|
18
|
+
const getClipboardValue = (event, mimeType) => {
|
|
19
|
+
try {
|
|
20
|
+
return event.clipboardData?.getData(mimeType) ?? "";
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export const shouldPastePlainTextAsMarkdown = (text) => {
|
|
27
|
+
const trimmedText = text.trim();
|
|
28
|
+
if (trimmedText.length === 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return [...blockMarkdownPatterns, ...inlineMarkdownPatterns].some((pattern) => pattern.test(trimmedText));
|
|
32
|
+
};
|
|
33
|
+
export const MarkdownPasteExtension = Extension.create({
|
|
34
|
+
name: "mdkitMarkdownPaste",
|
|
35
|
+
addProseMirrorPlugins() {
|
|
36
|
+
return [
|
|
37
|
+
new Plugin({
|
|
38
|
+
key: markdownPastePluginKey,
|
|
39
|
+
props: {
|
|
40
|
+
handlePaste: (view, event) => {
|
|
41
|
+
if (view.state.selection.$from.parent.type.spec.code) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
const markdownText = getClipboardValue(event, "text/markdown");
|
|
45
|
+
const plainText = getClipboardValue(event, "text/plain");
|
|
46
|
+
const pastedMarkdown = markdownText || plainText;
|
|
47
|
+
if (!pastedMarkdown.trim()) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
if (!markdownText &&
|
|
51
|
+
!shouldPastePlainTextAsMarkdown(pastedMarkdown)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
return this.editor.commands.insertContent(pastedMarkdown, {
|
|
55
|
+
contentType: "markdown",
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
}),
|
|
60
|
+
];
|
|
61
|
+
},
|
|
62
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
import { DecorationSet } from "@tiptap/pm/view";
|
|
4
|
+
export type MarkdownSearchMatch = {
|
|
5
|
+
from: number;
|
|
6
|
+
to: number;
|
|
7
|
+
};
|
|
8
|
+
export declare const markdownSearchPluginKey: PluginKey<DecorationSet>;
|
|
9
|
+
export declare const MarkdownSearchExtension: Extension<any, any>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Extension } from "@tiptap/core";
|
|
2
|
+
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
|
3
|
+
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
4
|
+
export const markdownSearchPluginKey = new PluginKey("mdkitMarkdownSearch");
|
|
5
|
+
const createSearchDecorations = (document, { activeIndex, matches }) => DecorationSet.create(document, matches.map((match, index) => Decoration.inline(match.from, match.to, {
|
|
6
|
+
class: index === activeIndex
|
|
7
|
+
? "mp-lb-mdkit-search-match mp-lb-mdkit-search-match-active"
|
|
8
|
+
: "mp-lb-mdkit-search-match",
|
|
9
|
+
})));
|
|
10
|
+
export const MarkdownSearchExtension = Extension.create({
|
|
11
|
+
name: "mdkitMarkdownSearch",
|
|
12
|
+
addProseMirrorPlugins() {
|
|
13
|
+
return [
|
|
14
|
+
new Plugin({
|
|
15
|
+
key: markdownSearchPluginKey,
|
|
16
|
+
props: {
|
|
17
|
+
decorations(state) {
|
|
18
|
+
return markdownSearchPluginKey.getState(state);
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
state: {
|
|
22
|
+
apply(transaction, previousDecorations) {
|
|
23
|
+
const searchMeta = transaction.getMeta(markdownSearchPluginKey);
|
|
24
|
+
if (searchMeta) {
|
|
25
|
+
return createSearchDecorations(transaction.doc, searchMeta);
|
|
26
|
+
}
|
|
27
|
+
if (transaction.docChanged) {
|
|
28
|
+
return previousDecorations.map(transaction.mapping, transaction.doc);
|
|
29
|
+
}
|
|
30
|
+
return previousDecorations;
|
|
31
|
+
},
|
|
32
|
+
init(_config, instance) {
|
|
33
|
+
return createSearchDecorations(instance.doc, {
|
|
34
|
+
activeIndex: 0,
|
|
35
|
+
matches: [],
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
}),
|
|
40
|
+
];
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { RefObject } from "react";
|
|
2
|
+
type MarkdownSearchPanelProps = {
|
|
3
|
+
activeMatchNumber: number;
|
|
4
|
+
inputRef: RefObject<HTMLInputElement | null>;
|
|
5
|
+
matchCount: number;
|
|
6
|
+
onClose: () => void;
|
|
7
|
+
onPrevious: () => void;
|
|
8
|
+
onNext: () => void;
|
|
9
|
+
onQueryChange: (query: string) => void;
|
|
10
|
+
query: string;
|
|
11
|
+
};
|
|
12
|
+
export declare const MarkdownSearchPanel: ({ activeMatchNumber, inputRef, matchCount, onClose, onPrevious, onNext, onQueryChange, query, }: MarkdownSearchPanelProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ChevronDown, ChevronUp, Search, X } from "lucide-react";
|
|
3
|
+
export const MarkdownSearchPanel = ({ activeMatchNumber, inputRef, matchCount, onClose, onPrevious, onNext, onQueryChange, query, }) => {
|
|
4
|
+
const submitSearch = (event) => {
|
|
5
|
+
event.preventDefault();
|
|
6
|
+
onNext();
|
|
7
|
+
};
|
|
8
|
+
const handleSearchKeyDown = (event) => {
|
|
9
|
+
if (event.key === "Escape") {
|
|
10
|
+
event.preventDefault();
|
|
11
|
+
onClose();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (event.key === "Enter" && event.shiftKey) {
|
|
15
|
+
event.preventDefault();
|
|
16
|
+
onPrevious();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
const matchStatus = query.trim().length === 0
|
|
20
|
+
? "No query"
|
|
21
|
+
: matchCount === 0
|
|
22
|
+
? "No matches"
|
|
23
|
+
: `${activeMatchNumber} of ${matchCount}`;
|
|
24
|
+
return (_jsxs("form", { "aria-label": "Search document", className: "mp-lb-mdkit-search-panel", onSubmit: submitSearch, children: [_jsx(Search, { "aria-hidden": "true", className: "mp-lb-mdkit-search-icon" }), _jsx("input", { ref: inputRef, "aria-label": "Search document", className: "mp-lb-mdkit-search-input", onChange: (event) => onQueryChange(event.target.value), onKeyDown: handleSearchKeyDown, placeholder: "Search", type: "search", value: query }), _jsx("span", { className: "mp-lb-mdkit-search-status", children: matchStatus }), _jsx("button", { "aria-label": "Previous match", className: "mp-lb-mdkit-search-button", disabled: matchCount === 0, onClick: onPrevious, title: "Previous match", type: "button", children: _jsx(ChevronUp, { "aria-hidden": "true" }) }), _jsx("button", { "aria-label": "Next match", className: "mp-lb-mdkit-search-button", disabled: matchCount === 0, title: "Next match", type: "submit", children: _jsx(ChevronDown, { "aria-hidden": "true" }) }), _jsx("button", { "aria-label": "Close search", className: "mp-lb-mdkit-search-button", onClick: onClose, title: "Close search", type: "button", children: _jsx(X, { "aria-hidden": "true" }) })] }));
|
|
25
|
+
};
|
|
@@ -4,10 +4,12 @@ import type { MdKitEditorDebugEvent } from "./editorDebug.js";
|
|
|
4
4
|
type MdKitEditorBaseProps = {
|
|
5
5
|
className?: string;
|
|
6
6
|
fillHeight?: boolean;
|
|
7
|
+
ignoreYamlFrontMatter?: boolean;
|
|
7
8
|
instanceKey?: string | number;
|
|
8
9
|
onDebugEvent?: (event: MdKitEditorDebugEvent) => void;
|
|
9
10
|
onFocusChange?: (focused: boolean) => void;
|
|
10
11
|
readOnly?: boolean;
|
|
12
|
+
search?: boolean;
|
|
11
13
|
style?: CSSProperties;
|
|
12
14
|
};
|
|
13
15
|
type LocalMdKitEditorProps = MdKitEditorBaseProps & {
|
|
@@ -2,8 +2,9 @@ import type { CSSProperties } from "react";
|
|
|
2
2
|
export type MdKitViewProps = {
|
|
3
3
|
className?: string;
|
|
4
4
|
fillHeight?: boolean;
|
|
5
|
+
ignoreYamlFrontMatter?: boolean;
|
|
5
6
|
placeholder?: string;
|
|
6
7
|
style?: CSSProperties;
|
|
7
8
|
value: string;
|
|
8
9
|
};
|
|
9
|
-
export declare const MdKitView: ({ className, fillHeight, placeholder, style, value, }: MdKitViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export declare const MdKitView: ({ className, fillHeight, ignoreYamlFrontMatter, placeholder, style, value, }: MdKitViewProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -2,8 +2,12 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import ReactMarkdown from "react-markdown";
|
|
3
3
|
import remarkGfm from "remark-gfm";
|
|
4
4
|
import { joinClassNames } from "../ui/joinClassNames.js";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { removeYamlFrontMatter } from "./yamlFrontMatter.js";
|
|
6
|
+
export const MdKitView = ({ className, fillHeight = false, ignoreYamlFrontMatter = false, placeholder, style, value, }) => {
|
|
7
|
+
const markdownValue = ignoreYamlFrontMatter
|
|
8
|
+
? removeYamlFrontMatter(value)
|
|
9
|
+
: value;
|
|
10
|
+
const renderedValue = markdownValue.trim().length > 0 ? markdownValue : (placeholder ?? "");
|
|
7
11
|
return (_jsx("div", { className: joinClassNames("mp-lb-mdkit-markdown-editor", "mp-lb-mdkit-markdown-view", fillHeight && "mp-lb-mdkit-markdown-editor-fill-height", className), "data-read-only": "true", style: style, children: _jsx("div", { className: "mp-lb-mdkit-editor-shell", children: _jsx("div", { className: "mp-lb-mdkit-editor-surface", children: renderedValue.length > 0 ? (_jsx("div", { className: "mp-lb-mdkit-tiptap mp-lb-mdkit-view-content", children: _jsx(ReactMarkdown, { components: {
|
|
8
12
|
a: ({ children, ...linkProps }) => (_jsx("a", { ...linkProps, rel: "noopener noreferrer", target: "_blank", children: children })),
|
|
9
13
|
}, remarkPlugins: [remarkGfm], children: renderedValue }) })) : (_jsx("div", { className: "mp-lb-mdkit-editor-empty" })) }) }) }));
|
|
@@ -5,8 +5,10 @@ type LocalTiptapMarkdownSurfaceProps = {
|
|
|
5
5
|
onChange?: (markdown: string) => void;
|
|
6
6
|
onDebugEvent?: (event: MdKitEditorDebugEvent) => void;
|
|
7
7
|
onFocusChange?: (focused: boolean) => void;
|
|
8
|
+
ignoreYamlFrontMatter?: boolean;
|
|
8
9
|
placeholder?: string;
|
|
9
10
|
readOnly?: boolean;
|
|
11
|
+
search?: boolean;
|
|
10
12
|
value: string;
|
|
11
13
|
};
|
|
12
14
|
type CollaborativeTiptapMarkdownSurfaceProps = {
|
|
@@ -14,8 +16,10 @@ type CollaborativeTiptapMarkdownSurfaceProps = {
|
|
|
14
16
|
onChange?: (markdown: string) => void;
|
|
15
17
|
onDebugEvent?: (event: MdKitEditorDebugEvent) => void;
|
|
16
18
|
onFocusChange?: (focused: boolean) => void;
|
|
19
|
+
ignoreYamlFrontMatter?: boolean;
|
|
17
20
|
placeholder?: string;
|
|
18
21
|
readOnly?: boolean;
|
|
22
|
+
search?: boolean;
|
|
19
23
|
value?: string;
|
|
20
24
|
};
|
|
21
25
|
type TiptapMarkdownSurfaceProps = CollaborativeTiptapMarkdownSurfaceProps | LocalTiptapMarkdownSurfaceProps;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useMemo, useRef } from "react";
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import Collaboration from "@tiptap/extension-collaboration";
|
|
4
4
|
import CollaborationCaret from "@tiptap/extension-collaboration-caret";
|
|
5
5
|
import { EditorContent, useEditor } from "@tiptap/react";
|
|
6
6
|
import { createMdKitTiptapExtensions } from "./createMdKitTiptapExtensions.js";
|
|
7
|
+
import { extractYamlFrontMatter, prependYamlFrontMatter, } from "./yamlFrontMatter.js";
|
|
7
8
|
import { MarkdownBubbleMenu } from "./MarkdownBubbleMenu.js";
|
|
9
|
+
import { MarkdownSearchPanel } from "./MarkdownSearchPanel.js";
|
|
10
|
+
import { markdownSearchPluginKey, } from "./MarkdownSearchExtension.js";
|
|
8
11
|
import { normalizeMarkdownSerialization } from "./normalizeMarkdownSerialization.js";
|
|
9
12
|
import { prepareMarkdownForEditorHydration } from "./prepareMarkdownForEditorHydration.js";
|
|
10
13
|
const describeElement = (element) => {
|
|
@@ -58,17 +61,24 @@ const createEditorDebugSnapshot = (editor, phase) => {
|
|
|
58
61
|
};
|
|
59
62
|
};
|
|
60
63
|
export const TiptapMarkdownSurface = (props) => {
|
|
61
|
-
const { collaboration = null, onDebugEvent, onFocusChange, placeholder = "Start writing...", readOnly = false, } = props;
|
|
64
|
+
const { collaboration = null, ignoreYamlFrontMatter = false, onDebugEvent, onFocusChange, placeholder = "Start writing...", readOnly = false, search = false, } = props;
|
|
62
65
|
const markdownValue = "value" in props && typeof props.value === "string" ? props.value : "";
|
|
63
66
|
const editorSurfaceRef = useRef(null);
|
|
67
|
+
const searchInputRef = useRef(null);
|
|
64
68
|
const onDebugEventRef = useRef(onDebugEvent);
|
|
65
69
|
const onFocusChangeRef = useRef(onFocusChange);
|
|
66
70
|
const onChangeRef = useRef(props.onChange);
|
|
67
71
|
const currentMarkdownRef = useRef(markdownValue);
|
|
72
|
+
const yamlFrontMatterRef = useRef(ignoreYamlFrontMatter
|
|
73
|
+
? extractYamlFrontMatter(markdownValue).frontMatter
|
|
74
|
+
: null);
|
|
68
75
|
const isApplyingExternalValueRef = useRef(false);
|
|
69
76
|
const pendingControlledEchoesRef = useRef(new Set());
|
|
70
77
|
const pendingContentFocusRef = useRef(null);
|
|
71
78
|
const shouldFocusAfterPointerRef = useRef(false);
|
|
79
|
+
const [searchOpen, setSearchOpen] = useState(false);
|
|
80
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
81
|
+
const [activeSearchMatchIndex, setActiveSearchMatchIndex] = useState(0);
|
|
72
82
|
const collaborationDocument = collaboration?.document ?? null;
|
|
73
83
|
const collaborationProvider = collaboration?.provider ?? null;
|
|
74
84
|
const collaborationUserColor = collaboration?.collaborator.color ?? "";
|
|
@@ -121,7 +131,9 @@ export const TiptapMarkdownSurface = (props) => {
|
|
|
121
131
|
const editor = useEditor({
|
|
122
132
|
content: hasCollaboration
|
|
123
133
|
? undefined
|
|
124
|
-
: prepareMarkdownForEditorHydration(
|
|
134
|
+
: prepareMarkdownForEditorHydration(ignoreYamlFrontMatter
|
|
135
|
+
? extractYamlFrontMatter(markdownValue).body
|
|
136
|
+
: markdownValue),
|
|
125
137
|
contentType: "markdown",
|
|
126
138
|
editable: !readOnly,
|
|
127
139
|
editorProps: {
|
|
@@ -166,7 +178,7 @@ export const TiptapMarkdownSurface = (props) => {
|
|
|
166
178
|
}
|
|
167
179
|
const nextSerializedMarkdown = normalizeMarkdownSerialization(updatedEditor.getMarkdown());
|
|
168
180
|
const previousMarkdown = currentMarkdownRef.current;
|
|
169
|
-
const nextMarkdown = nextSerializedMarkdown;
|
|
181
|
+
const nextMarkdown = prependYamlFrontMatter(yamlFrontMatterRef.current, nextSerializedMarkdown);
|
|
170
182
|
currentMarkdownRef.current = nextMarkdown;
|
|
171
183
|
if (nextMarkdown !== previousMarkdown) {
|
|
172
184
|
pendingControlledEchoesRef.current.add(nextMarkdown);
|
|
@@ -177,8 +189,150 @@ export const TiptapMarkdownSurface = (props) => {
|
|
|
177
189
|
collaborationCaretExtensions,
|
|
178
190
|
collaborationDocument,
|
|
179
191
|
hasCollaboration,
|
|
192
|
+
ignoreYamlFrontMatter,
|
|
180
193
|
placeholder,
|
|
181
194
|
]);
|
|
195
|
+
const searchMatches = useMemo(() => {
|
|
196
|
+
const query = searchQuery.trim().toLocaleLowerCase();
|
|
197
|
+
if (!editor || query.length === 0) {
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
const matches = [];
|
|
201
|
+
editor.state.doc.descendants((node, position) => {
|
|
202
|
+
if (!node.isText || typeof node.text !== "string") {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const text = node.text.toLocaleLowerCase();
|
|
206
|
+
let fromIndex = text.indexOf(query);
|
|
207
|
+
while (fromIndex >= 0) {
|
|
208
|
+
matches.push({
|
|
209
|
+
from: position + fromIndex,
|
|
210
|
+
to: position + fromIndex + query.length,
|
|
211
|
+
});
|
|
212
|
+
fromIndex = text.indexOf(query, fromIndex + query.length);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
return matches;
|
|
216
|
+
}, [editor, searchQuery, markdownValue]);
|
|
217
|
+
const activeSearchMatchNumber = searchMatches.length === 0 ? 0 : activeSearchMatchIndex + 1;
|
|
218
|
+
const scrollActiveSearchMatchIntoView = useCallback(() => {
|
|
219
|
+
window.requestAnimationFrame(() => {
|
|
220
|
+
const activeMatch = editorSurfaceRef.current?.querySelector(".mp-lb-mdkit-search-match-active");
|
|
221
|
+
if (!activeMatch || !("scrollIntoView" in activeMatch)) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
activeMatch.scrollIntoView({
|
|
225
|
+
block: "center",
|
|
226
|
+
inline: "nearest",
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
}, []);
|
|
230
|
+
const selectSearchMatch = useCallback((matchIndex) => {
|
|
231
|
+
if (!editor || searchMatches.length === 0) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const nextIndex = ((matchIndex % searchMatches.length) + searchMatches.length) %
|
|
235
|
+
searchMatches.length;
|
|
236
|
+
setActiveSearchMatchIndex(nextIndex);
|
|
237
|
+
editor.view.dispatch(editor.state.tr
|
|
238
|
+
.setMeta(markdownSearchPluginKey, {
|
|
239
|
+
activeIndex: nextIndex,
|
|
240
|
+
matches: searchMatches,
|
|
241
|
+
})
|
|
242
|
+
.setMeta("addToHistory", false));
|
|
243
|
+
scrollActiveSearchMatchIntoView();
|
|
244
|
+
}, [editor, scrollActiveSearchMatchIntoView, searchMatches]);
|
|
245
|
+
const openSearch = useCallback(() => {
|
|
246
|
+
if (!search) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
setSearchOpen(true);
|
|
250
|
+
window.requestAnimationFrame(() => {
|
|
251
|
+
searchInputRef.current?.focus();
|
|
252
|
+
searchInputRef.current?.select();
|
|
253
|
+
});
|
|
254
|
+
}, [search]);
|
|
255
|
+
const closeSearch = useCallback(() => {
|
|
256
|
+
setSearchOpen(false);
|
|
257
|
+
if (!editor) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const activeMatch = searchMatches[activeSearchMatchIndex];
|
|
261
|
+
if (!activeMatch) {
|
|
262
|
+
editor.commands.focus();
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
editor
|
|
266
|
+
.chain()
|
|
267
|
+
.focus()
|
|
268
|
+
.setTextSelection({ from: activeMatch.from, to: activeMatch.to })
|
|
269
|
+
.scrollIntoView()
|
|
270
|
+
.run();
|
|
271
|
+
}, [activeSearchMatchIndex, editor, searchMatches]);
|
|
272
|
+
const selectNextSearchMatch = useCallback(() => {
|
|
273
|
+
selectSearchMatch(activeSearchMatchIndex + 1);
|
|
274
|
+
}, [activeSearchMatchIndex, selectSearchMatch]);
|
|
275
|
+
const selectPreviousSearchMatch = useCallback(() => {
|
|
276
|
+
selectSearchMatch(activeSearchMatchIndex - 1);
|
|
277
|
+
}, [activeSearchMatchIndex, selectSearchMatch]);
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (!search) {
|
|
280
|
+
setSearchOpen(false);
|
|
281
|
+
setSearchQuery("");
|
|
282
|
+
}
|
|
283
|
+
}, [search]);
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (!search || !editor) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const handleSearchShortcut = (event) => {
|
|
289
|
+
const isFindShortcut = (event.metaKey || event.ctrlKey) &&
|
|
290
|
+
!event.altKey &&
|
|
291
|
+
event.key.toLocaleLowerCase() === "f";
|
|
292
|
+
if (!isFindShortcut) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (document.activeElement instanceof Element &&
|
|
296
|
+
!editorSurfaceRef.current?.contains(document.activeElement)) {
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
event.preventDefault();
|
|
300
|
+
openSearch();
|
|
301
|
+
};
|
|
302
|
+
document.addEventListener("keydown", handleSearchShortcut);
|
|
303
|
+
return () => {
|
|
304
|
+
document.removeEventListener("keydown", handleSearchShortcut);
|
|
305
|
+
};
|
|
306
|
+
}, [editor, openSearch, search]);
|
|
307
|
+
useEffect(() => {
|
|
308
|
+
setActiveSearchMatchIndex(0);
|
|
309
|
+
}, [searchQuery]);
|
|
310
|
+
useEffect(() => {
|
|
311
|
+
if (!searchOpen || searchQuery.trim().length === 0) {
|
|
312
|
+
editor?.view.dispatch(editor.state.tr
|
|
313
|
+
.setMeta(markdownSearchPluginKey, {
|
|
314
|
+
activeIndex: 0,
|
|
315
|
+
matches: [],
|
|
316
|
+
})
|
|
317
|
+
.setMeta("addToHistory", false));
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const nextActiveSearchMatchIndex = Math.min(activeSearchMatchIndex, Math.max(0, searchMatches.length - 1));
|
|
321
|
+
editor?.view.dispatch(editor.state.tr
|
|
322
|
+
.setMeta(markdownSearchPluginKey, {
|
|
323
|
+
activeIndex: nextActiveSearchMatchIndex,
|
|
324
|
+
matches: searchMatches,
|
|
325
|
+
})
|
|
326
|
+
.setMeta("addToHistory", false));
|
|
327
|
+
scrollActiveSearchMatchIntoView();
|
|
328
|
+
}, [
|
|
329
|
+
activeSearchMatchIndex,
|
|
330
|
+
editor,
|
|
331
|
+
scrollActiveSearchMatchIntoView,
|
|
332
|
+
searchMatches,
|
|
333
|
+
searchOpen,
|
|
334
|
+
searchQuery,
|
|
335
|
+
]);
|
|
182
336
|
useEffect(() => {
|
|
183
337
|
editor?.setEditable(!readOnly);
|
|
184
338
|
}, [editor, readOnly]);
|
|
@@ -215,6 +369,9 @@ export const TiptapMarkdownSurface = (props) => {
|
|
|
215
369
|
}
|
|
216
370
|
if (hasCollaboration) {
|
|
217
371
|
currentMarkdownRef.current = markdownValue;
|
|
372
|
+
yamlFrontMatterRef.current = ignoreYamlFrontMatter
|
|
373
|
+
? extractYamlFrontMatter(markdownValue).frontMatter
|
|
374
|
+
: null;
|
|
218
375
|
pendingControlledEchoesRef.current.clear();
|
|
219
376
|
return;
|
|
220
377
|
}
|
|
@@ -228,15 +385,19 @@ export const TiptapMarkdownSurface = (props) => {
|
|
|
228
385
|
}
|
|
229
386
|
pendingControlledEchoesRef.current.clear();
|
|
230
387
|
isApplyingExternalValueRef.current = true;
|
|
231
|
-
|
|
388
|
+
const frontMatter = ignoreYamlFrontMatter
|
|
389
|
+
? extractYamlFrontMatter(markdownValue)
|
|
390
|
+
: null;
|
|
391
|
+
editor.commands.setContent(prepareMarkdownForEditorHydration(frontMatter?.body ?? markdownValue), {
|
|
232
392
|
contentType: "markdown",
|
|
233
393
|
emitUpdate: false,
|
|
234
394
|
});
|
|
235
395
|
currentMarkdownRef.current = markdownValue;
|
|
396
|
+
yamlFrontMatterRef.current = frontMatter?.frontMatter ?? null;
|
|
236
397
|
window.queueMicrotask(() => {
|
|
237
398
|
isApplyingExternalValueRef.current = false;
|
|
238
399
|
});
|
|
239
|
-
}, [editor, hasCollaboration, markdownValue]);
|
|
400
|
+
}, [editor, hasCollaboration, ignoreYamlFrontMatter, markdownValue]);
|
|
240
401
|
if (!editor) {
|
|
241
402
|
return (_jsx("div", { className: "mp-lb-mdkit-editor-shell", children: _jsx("div", { className: "mp-lb-mdkit-editor-empty", children: collaboration
|
|
242
403
|
? "Connecting collaboration session..."
|
|
@@ -449,5 +610,5 @@ export const TiptapMarkdownSurface = (props) => {
|
|
|
449
610
|
});
|
|
450
611
|
queueEditorFocusAtPosition(getEditorPositionAtClientPoint(event.clientX, event.clientY, event.target));
|
|
451
612
|
};
|
|
452
|
-
return (_jsx("div", { className: "mp-lb-mdkit-editor-shell", children: _jsxs("div", { ref: editorSurfaceRef, className: "mp-lb-mdkit-editor-surface", onPointerDownCapture: focusEditorBackgroundOnPointerDown, onPointerUpCapture: focusEditorBackgroundOnPointerUp, children: [_jsx(MarkdownBubbleMenu, { editor: editor }), _jsx(EditorContent, { editor: editor })] }) }));
|
|
613
|
+
return (_jsx("div", { className: "mp-lb-mdkit-editor-shell", children: _jsxs("div", { ref: editorSurfaceRef, className: "mp-lb-mdkit-editor-surface", onPointerDownCapture: focusEditorBackgroundOnPointerDown, onPointerUpCapture: focusEditorBackgroundOnPointerUp, children: [search && searchOpen ? (_jsx(MarkdownSearchPanel, { activeMatchNumber: activeSearchMatchNumber, inputRef: searchInputRef, matchCount: searchMatches.length, onClose: closeSearch, onNext: selectNextSearchMatch, onPrevious: selectPreviousSearchMatch, onQueryChange: setSearchQuery, query: searchQuery })) : null, _jsx(MarkdownBubbleMenu, { editor: editor }), _jsx(EditorContent, { editor: editor })] }) }));
|
|
453
614
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { Markdown } from "@tiptap/markdown";
|
|
2
2
|
import Placeholder from "@tiptap/extension-placeholder";
|
|
3
3
|
import StarterKit from "@tiptap/starter-kit";
|
|
4
|
+
import { MarkdownPasteExtension } from "./MarkdownPasteExtension.js";
|
|
5
|
+
import { MarkdownSearchExtension } from "./MarkdownSearchExtension.js";
|
|
4
6
|
export const defaultMdKitMarkdownPlaceholder = "Start writing...";
|
|
5
7
|
export const createMdKitTiptapExtensions = ({ placeholder = defaultMdKitMarkdownPlaceholder, undoRedo = true, } = {}) => [
|
|
6
8
|
StarterKit.configure({
|
|
7
|
-
heading: { levels: [1, 2] },
|
|
9
|
+
heading: { levels: [1, 2, 3, 4, 5, 6] },
|
|
8
10
|
link: {
|
|
9
11
|
HTMLAttributes: {
|
|
10
12
|
rel: "noopener noreferrer",
|
|
@@ -24,4 +26,6 @@ export const createMdKitTiptapExtensions = ({ placeholder = defaultMdKitMarkdown
|
|
|
24
26
|
gfm: true,
|
|
25
27
|
},
|
|
26
28
|
}),
|
|
29
|
+
MarkdownPasteExtension,
|
|
30
|
+
MarkdownSearchExtension,
|
|
27
31
|
];
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type MdKitYamlFrontMatter = {
|
|
2
|
+
data: unknown;
|
|
3
|
+
raw: string;
|
|
4
|
+
trailingWhitespace: string;
|
|
5
|
+
yaml: string;
|
|
6
|
+
};
|
|
7
|
+
export type MdKitYamlFrontMatterExtraction = {
|
|
8
|
+
body: string;
|
|
9
|
+
errors: string[];
|
|
10
|
+
frontMatter: MdKitYamlFrontMatter | null;
|
|
11
|
+
};
|
|
12
|
+
export declare const parseYamlFrontMatter: (yaml: string) => unknown;
|
|
13
|
+
export declare const extractYamlFrontMatter: (markdown: string) => MdKitYamlFrontMatterExtraction;
|
|
14
|
+
export declare const hasYamlFrontMatter: (markdown: string) => boolean;
|
|
15
|
+
export declare const removeYamlFrontMatter: (markdown: string) => string;
|
|
16
|
+
export declare const prependYamlFrontMatter: (frontMatter: MdKitYamlFrontMatter | string | null, body: string) => string;
|