@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 CHANGED
@@ -63,6 +63,7 @@ import {
63
63
 
64
64
  const document = useMdKitDocument({
65
65
  adapter,
66
+ debounceMs: 1000,
66
67
  documentId: "docs/brief.md",
67
68
  });
68
69
 
@@ -5,7 +5,7 @@ const emptyDocumentState = {
5
5
  version: null,
6
6
  };
7
7
  export const useMdKitDocument = (options) => {
8
- const { adapter, debounceMs = 350, documentId, pollMs = 2000 } = options;
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,3 @@
1
+ import { Extension } from "@tiptap/core";
2
+ export declare const shouldPastePlainTextAsMarkdown: (text: string) => boolean;
3
+ export declare const MarkdownPasteExtension: Extension<any, any>;
@@ -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
- export const MdKitView = ({ className, fillHeight = false, placeholder, style, value, }) => {
6
- const renderedValue = value.trim().length > 0 ? value : (placeholder ?? "");
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(markdownValue),
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
- editor.commands.setContent(prepareMarkdownForEditorHydration(markdownValue), {
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;