@powerduck/md-editor 0.10.9 → 0.11.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 +316 -387
- package/dist/index.cjs +28 -30
- package/dist/index.mjs +2132 -4691
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,466 +1,395 @@
|
|
|
1
1
|
# @powerduck/md-editor
|
|
2
2
|
|
|
3
|
-
High-performance embeddable Markdown editor
|
|
3
|
+
High-performance embeddable Markdown editor with syntax highlighting, math formulas, mindmaps, @mentions, document link cards, and incremental rendering. Built on CodeMirror 6 with a React wrapper.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@powerduck/md-editor)
|
|
6
|
+
[](https://github.com/PowerDuckie/md-editor/blob/main/LICENSE)
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
- **Math formulas** via self-contained KaTeX plugin (`$E=mc^2$` inline, `$$...$$` block) -- no texmath dependency, no double-rendering
|
|
9
|
-
- **Mindmaps** via Markmap (```` ```mindmap ```` code fences)
|
|
10
|
-
- **Code highlighting** via highlight.js -- 20+ languages, macOS-style traffic-light header, one-click copy button
|
|
11
|
-
- **Admonition blocks** -- `:::notice`, `:::warning`, `:::tip`, `:::danger`, `:::info`, `:::success` with icons and colored borders
|
|
12
|
-
- **github-markdown-css** preview -- proper heading, list, table, and typography styling
|
|
13
|
-
- **Rich toolbar** -- heading, bold, italic, link, image, video, YouTube, quote, lists, code, table, math, mindmap, horizontal rule, tip block, preview toggle, help, theme toggle
|
|
14
|
-
- **Popup dialogs** -- image URL, video URL, YouTube URL/ID, table rows/columns
|
|
15
|
-
- **Keyboard shortcuts** -- Ctrl+B bold, Ctrl+I italic, Ctrl+K link, etc. with a Help dialog listing all shortcuts
|
|
16
|
-
- **Configurable toolbar** -- show/hide items, custom labels and icons via `toolbar` option
|
|
17
|
-
- **Image upload hook** -- paste/drop images, call `onImageUpload(file)` to get a URL
|
|
18
|
-
- **Video rendering** -- `.mp4`, `.webm`, `.ogg` links render as `<video>` elements
|
|
19
|
-
- **Standalone renderer** -- `renderMarkdown(source, options)` for consistent styling outside the editor
|
|
20
|
-
- **Block-level incremental rendering** -- only changed sections re-render
|
|
21
|
-
- **Adaptive debounce** -- preview render delay scales with document length
|
|
22
|
-
- **Simple / Complex modes** -- toggle toolbar and status bar at runtime
|
|
23
|
-
- **Light / Dark themes** -- CSS custom properties, inherits host design tokens
|
|
24
|
-
- **React component** -- `forwardRef` with imperative handle
|
|
25
|
-
|
|
26
|
-
## Installation
|
|
8
|
+
## Links
|
|
27
9
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
10
|
+
- [Official Website](https://www.powerduck.com/opensource/md-editor.html)
|
|
11
|
+
- [Documentation](https://www.powerduck.com/docs/md-editor/introduction)
|
|
12
|
+
- [Live Demo](https://www.powerduck.com/demo/md-editor.html)
|
|
13
|
+
- [GitHub](https://github.com/PowerDuckie/md-editor)
|
|
14
|
+
- [npm](https://www.npmjs.com/package/@powerduck/md-editor)
|
|
31
15
|
|
|
32
|
-
|
|
16
|
+
---
|
|
33
17
|
|
|
34
|
-
|
|
18
|
+
## Quick Start
|
|
35
19
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const editor = new MarkdownEditor('#editor', {
|
|
41
|
-
value: '# Hello\n\nStart typing...',
|
|
42
|
-
mode: 'complex', // 'simple' | 'complex'
|
|
43
|
-
theme: 'light', // 'light' | 'dark'
|
|
44
|
-
math: true,
|
|
45
|
-
mindmap: true,
|
|
46
|
-
onChange: (value) => console.log(value)
|
|
47
|
-
});
|
|
20
|
+
### Install
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @powerduck/md-editor
|
|
48
24
|
```
|
|
49
25
|
|
|
50
|
-
### React
|
|
26
|
+
### React Component
|
|
51
27
|
|
|
52
28
|
```tsx
|
|
53
|
-
import {
|
|
54
|
-
import
|
|
29
|
+
import { useRef, useState } from "react";
|
|
30
|
+
import { MarkdownEditorReact, type MarkdownEditorHandle } from "@powerduck/md-editor/react";
|
|
31
|
+
import "@powerduck/md-editor/dist/style.css";
|
|
55
32
|
|
|
56
33
|
function App() {
|
|
34
|
+
const [value, setValue] = useState("# Hello\n\nStart typing...");
|
|
35
|
+
const editorRef = useRef<MarkdownEditorHandle>(null);
|
|
36
|
+
|
|
57
37
|
return (
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
38
|
+
<div style={{ height: "600px" }}>
|
|
39
|
+
<MarkdownEditorReact
|
|
40
|
+
ref={editorRef}
|
|
41
|
+
value={value}
|
|
42
|
+
onChange={setValue}
|
|
43
|
+
mode="complex"
|
|
44
|
+
theme="light"
|
|
45
|
+
/>
|
|
46
|
+
<button onClick={() => console.log(editorRef.current?.getHtml())}>
|
|
47
|
+
Get HTML
|
|
48
|
+
</button>
|
|
49
|
+
</div>
|
|
63
50
|
);
|
|
64
51
|
}
|
|
65
52
|
```
|
|
66
53
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
### MarkdownEditorOptions
|
|
70
|
-
|
|
71
|
-
| Option | Type | Default | Description |
|
|
72
|
-
|--------|------|---------|-------------|
|
|
73
|
-
| `value` | `string` | `''` | Initial content |
|
|
74
|
-
| `mode` | `'simple' \| 'complex'` | `'simple'` | Toolbar + status bar in complex mode |
|
|
75
|
-
| `theme` | `'light' \| 'dark'` | `'light'` | Color theme |
|
|
76
|
-
| `math` | `boolean` | `true` | Enable KaTeX math |
|
|
77
|
-
| `mindmap` | `boolean` | `true` | Enable Markmap mindmaps |
|
|
78
|
-
| `codeHighlight` | `boolean` | `true` | Enable highlight.js code blocks with copy button |
|
|
79
|
-
| `tips` | `boolean` | `true` | Enable admonition/tip blocks (:::notice, :::warning, etc.) |
|
|
80
|
-
| `preview` | `boolean` | `true` | Show preview pane |
|
|
81
|
-
| `toolbar` | `ToolbarConfig` | all items | Configure which toolbar items show, their labels and icons |
|
|
82
|
-
| `onImageUpload` | `(file: File) => Promise<string>` | - | Image upload hook for paste/drop |
|
|
83
|
-
| `autoPreview` | `boolean` | `true` | Auto-render preview on edit |
|
|
84
|
-
| `renderDebounce` | `number \| false` | adaptive | Preview debounce in ms |
|
|
85
|
-
| `onChange` | `(value: string) => void` | - | Change callback |
|
|
86
|
-
|
|
87
|
-
### Methods
|
|
88
|
-
|
|
89
|
-
- `getValue(): string`
|
|
90
|
-
- `setValue(value: string): void`
|
|
91
|
-
- `getHtml(): string`
|
|
92
|
-
- `renderNow(): void` -- manual preview render (use with `autoPreview: false`)
|
|
93
|
-
- `setMode(mode: EditorMode): void`
|
|
94
|
-
- `setTheme(theme: EditorTheme): void`
|
|
95
|
-
- `setAutoPreview(auto: boolean): void`
|
|
96
|
-
- `focus(): void`
|
|
97
|
-
- `destroy(): void`
|
|
54
|
+
### Vanilla JS
|
|
98
55
|
|
|
99
|
-
|
|
56
|
+
```typescript
|
|
57
|
+
import { MarkdownEditor } from "@powerduck/md-editor";
|
|
58
|
+
import "@powerduck/md-editor/dist/style.css";
|
|
100
59
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
60
|
+
const editor = new MarkdownEditor("#editor", {
|
|
61
|
+
value: "# Hello\n\nStart typing...",
|
|
62
|
+
mode: "complex",
|
|
63
|
+
theme: "light",
|
|
64
|
+
onChange: (value) => console.log(value.length, "chars"),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Read and write content
|
|
68
|
+
const markdown = editor.getValue();
|
|
69
|
+
editor.setValue("# Updated content");
|
|
70
|
+
|
|
71
|
+
// Render to HTML
|
|
72
|
+
const html = editor.getHtml();
|
|
109
73
|
```
|
|
110
|
-
````
|
|
111
74
|
|
|
112
|
-
|
|
75
|
+
### @mention Integration
|
|
113
76
|
|
|
114
|
-
|
|
77
|
+
```typescript
|
|
78
|
+
import { MarkdownEditor, type MentionItem } from "@powerduck/md-editor";
|
|
79
|
+
|
|
80
|
+
const USERS: MentionItem[] = [
|
|
81
|
+
{ id: "1", label: "Alice", avatar: "https://example.com/a.png", description: "alice@example.com" },
|
|
82
|
+
{ id: "2", label: "Bob", description: "bob@example.com" },
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
new MarkdownEditor("#editor", {
|
|
86
|
+
mode: "complex",
|
|
87
|
+
mention: {
|
|
88
|
+
onMentionSearch: async (query) =>
|
|
89
|
+
USERS.filter((u) => u.label.toLowerCase().includes(query.toLowerCase())),
|
|
90
|
+
onMentionSelect: (item) => `[@${item.label}](mention:${item.id})`,
|
|
91
|
+
minChars: 0,
|
|
92
|
+
maxItems: 8,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Document Link Card
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { MarkdownEditor, type DocItem } from "@powerduck/md-editor";
|
|
101
|
+
|
|
102
|
+
const DOCS: DocItem[] = [
|
|
103
|
+
{ id: "d1", title: "Getting Started", url: "https://docs.example.com/start", description: "Quick start guide", thumbnail: "https://picsum.photos/seed/start/96/72" },
|
|
104
|
+
{ id: "d2", title: "API Reference", url: "https://docs.example.com/api", description: "Full API documentation" },
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
new MarkdownEditor("#editor", {
|
|
108
|
+
mode: "complex",
|
|
109
|
+
docLink: {
|
|
110
|
+
onDocSearch: async (query) =>
|
|
111
|
+
DOCS.filter((d) => d.title.toLowerCase().includes(query.toLowerCase())),
|
|
112
|
+
onFetchDocMeta: async (url) => {
|
|
113
|
+
const res = await fetch(`/api/og?url=${encodeURIComponent(url)}`);
|
|
114
|
+
return res.json();
|
|
115
|
+
},
|
|
116
|
+
insertStyle: "auto", // "auto" | "card" | "link"
|
|
117
|
+
minChars: 0,
|
|
118
|
+
maxItems: 8,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Features
|
|
126
|
+
|
|
127
|
+
- **Syntax highlighting** — CodeMirror 6 with Markdown grammar and token colors
|
|
128
|
+
- **Math formulas** — KaTeX rendering for inline `$...$` and block `$$...$$`
|
|
129
|
+
- **Mindmaps** — Markmap interactive SVG diagrams from ```` ```mindmap ```` fenced blocks
|
|
130
|
+
- **Code blocks** — highlight.js with traffic-light header and copy button
|
|
131
|
+
- **@mentions** — Searchable dropdown with custom search/select hooks, inserts `[@label](mention:id)` for renderer distinction
|
|
132
|
+
- **Document link cards** — Insert plain links or preview cards with auto-fetched title/thumbnail/description, with custom fetch hook
|
|
133
|
+
- **Image upload** — Paste/drag/select image files with custom upload hook
|
|
134
|
+
- **Task lists** — `- [ ]` / `- [x]` with clickable toggles in preview
|
|
135
|
+
- **Callouts** — `:::notice`, `:::warning`, `:::danger`, etc.
|
|
136
|
+
- **Tables** — Quick-insert popup with configurable rows/columns
|
|
137
|
+
- **Incremental rendering** — Block-level DOM reuse for large documents, adaptive debounce
|
|
138
|
+
- **Themes** — Light and dark themes with CSS custom properties
|
|
139
|
+
- **Two modes** — `simple` (pure edit + preview) and `complex` (toolbar + status bar + line numbers)
|
|
140
|
+
- **React wrapper** — `forwardRef` component with controlled/uncontrolled value and imperative handle
|
|
141
|
+
- **Dual ESM/CJS builds** — Works with `import` and `require`
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## MarkdownEditorOptions
|
|
115
146
|
|
|
116
|
-
````markdown
|
|
117
147
|
```typescript
|
|
118
|
-
interface
|
|
119
|
-
|
|
120
|
-
|
|
148
|
+
interface MarkdownEditorOptions {
|
|
149
|
+
value?: string;
|
|
150
|
+
mode?: "simple" | "complex"; // default: "simple"
|
|
151
|
+
theme?: "light" | "dark"; // default: "light"
|
|
152
|
+
math?: boolean; // default: true
|
|
153
|
+
mindmap?: boolean; // default: true
|
|
154
|
+
codeHighlight?: boolean; // default: true
|
|
155
|
+
tips?: boolean; // default: true
|
|
156
|
+
preview?: boolean; // default: true
|
|
157
|
+
onChange?: (value: string) => void;
|
|
158
|
+
toolbar?: ToolbarConfig;
|
|
159
|
+
onImageUpload?: (file: File) => Promise<string>;
|
|
160
|
+
mention?: MentionOptions;
|
|
161
|
+
docLink?: DocLinkOptions;
|
|
162
|
+
renderDebounce?: number | false; // adaptive by default
|
|
163
|
+
autoPreview?: boolean; // default: true
|
|
121
164
|
}
|
|
122
165
|
```
|
|
123
|
-
````
|
|
124
166
|
|
|
125
|
-
|
|
167
|
+
### MentionOptions
|
|
126
168
|
|
|
127
|
-
|
|
169
|
+
```typescript
|
|
170
|
+
interface MentionOptions {
|
|
171
|
+
onMentionSearch?: (query: string) => MentionItem[] | Promise<MentionItem[]>;
|
|
172
|
+
onMentionSelect?: (item: MentionItem) => string; // default: `[@label](mention:id)`
|
|
173
|
+
minChars?: number; // default: 0
|
|
174
|
+
maxItems?: number; // default: 8
|
|
175
|
+
}
|
|
128
176
|
|
|
129
|
-
|
|
177
|
+
interface MentionItem {
|
|
178
|
+
id: string;
|
|
179
|
+
label: string;
|
|
180
|
+
avatar?: string;
|
|
181
|
+
description?: string;
|
|
182
|
+
[key: string]: unknown;
|
|
183
|
+
}
|
|
184
|
+
```
|
|
130
185
|
|
|
131
|
-
|
|
132
|
-
- **Video** (`video` button): URL (.mp4, .webm, .ogg) -> renders as `<video controls>` in preview
|
|
133
|
-
- **YouTube** (`youtube` button): accepts any YouTube URL format (watch?v=, youtu.be, embed, shorts) or raw 11-char ID -> inserts a linked thumbnail image that opens the video on click
|
|
186
|
+
### DocLinkOptions
|
|
134
187
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
188
|
+
```typescript
|
|
189
|
+
interface DocLinkOptions {
|
|
190
|
+
onDocSearch?: (query: string) => DocItem[] | Promise<DocItem[]>;
|
|
191
|
+
onFetchDocMeta?: (url: string) => Promise<DocMeta>;
|
|
192
|
+
insertStyle?: "card" | "link" | "auto"; // default: "auto"
|
|
193
|
+
triggerChar?: string; // default: "/"
|
|
194
|
+
minChars?: number; // default: 0
|
|
195
|
+
maxItems?: number; // default: 8
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface DocItem {
|
|
199
|
+
id: string;
|
|
200
|
+
title: string;
|
|
201
|
+
url: string;
|
|
202
|
+
thumbnail?: string;
|
|
203
|
+
description?: string;
|
|
204
|
+
[key: string]: unknown;
|
|
205
|
+
}
|
|
138
206
|
```
|
|
139
207
|
|
|
140
|
-
|
|
208
|
+
---
|
|
141
209
|
|
|
142
|
-
|
|
210
|
+
## React Component Props
|
|
143
211
|
|
|
144
|
-
```
|
|
145
|
-
|
|
146
|
-
|
|
212
|
+
```typescript
|
|
213
|
+
interface MarkdownEditorProps extends Omit<MarkdownEditorOptions, "value" | "onChange"> {
|
|
214
|
+
value?: string; // controlled value
|
|
215
|
+
defaultValue?: string; // uncontrolled initial value (mount only)
|
|
216
|
+
onChange?: (value: string) => void;
|
|
217
|
+
className?: string;
|
|
218
|
+
style?: CSSProperties;
|
|
219
|
+
}
|
|
147
220
|
|
|
148
|
-
|
|
149
|
-
|
|
221
|
+
interface MarkdownEditorHandle {
|
|
222
|
+
getValue: () => string;
|
|
223
|
+
setValue: (value: string) => void;
|
|
224
|
+
getHtml: () => string;
|
|
225
|
+
focus: () => void;
|
|
226
|
+
setMode: (mode: "simple" | "complex") => void;
|
|
227
|
+
setTheme: (theme: "light" | "dark") => void;
|
|
228
|
+
renderNow: () => void;
|
|
229
|
+
}
|
|
150
230
|
```
|
|
151
231
|
|
|
152
|
-
|
|
232
|
+
> **Performance note:** Structural options (`mode`, `math`, `mindmap`, `preview`, `autoPreview`, `renderDebounce`) recreate the underlying instance when they change. `value`, `onChange`, and `theme` use lightweight sync paths and never recreate the instance, so controlled usage stays responsive on large documents.
|
|
153
233
|
|
|
154
|
-
|
|
234
|
+
---
|
|
155
235
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
Click the **Help** (?) button in the toolbar to see all shortcuts at runtime.
|
|
171
|
-
|
|
172
|
-
## Configurable Toolbar
|
|
173
|
-
|
|
174
|
-
Control which buttons appear and customize their labels/icons:
|
|
175
|
-
|
|
176
|
-
```ts
|
|
177
|
-
// Only show specific buttons
|
|
178
|
-
new MarkdownEditor('#editor', {
|
|
179
|
-
mode: 'complex',
|
|
180
|
-
toolbar: ['bold', 'italic', 'link', 'code']
|
|
236
|
+
## Toolbar Actions
|
|
237
|
+
|
|
238
|
+
22 built-in toolbar actions in default order:
|
|
239
|
+
|
|
240
|
+
`heading`, `bold`, `italic`, `link`, `image`, `video`, `youtube`, `quote`, `ul`, `ol`, `tasklist`, `mention`, `doclink`, `code`, `table`, `math`, `mindmap`, `hr`, `tips`, `preview`, `help`, `theme`
|
|
241
|
+
|
|
242
|
+
### Configure Toolbar
|
|
243
|
+
|
|
244
|
+
```typescript
|
|
245
|
+
// Array form: include-list (only these actions show, in this order)
|
|
246
|
+
new MarkdownEditor("#editor", {
|
|
247
|
+
mode: "complex",
|
|
248
|
+
toolbar: ["bold", "italic", "link", "code", "math"],
|
|
181
249
|
});
|
|
182
250
|
|
|
183
|
-
//
|
|
184
|
-
new MarkdownEditor(
|
|
185
|
-
mode:
|
|
251
|
+
// Record form: per-item overrides (everything not listed stays visible)
|
|
252
|
+
new MarkdownEditor("#editor", {
|
|
253
|
+
mode: "complex",
|
|
186
254
|
toolbar: {
|
|
187
|
-
bold: { label:
|
|
255
|
+
bold: { label: "Make bold" },
|
|
188
256
|
image: { show: false },
|
|
189
|
-
youtube: { show: false }
|
|
190
|
-
}
|
|
257
|
+
youtube: { show: false },
|
|
258
|
+
},
|
|
191
259
|
});
|
|
192
260
|
```
|
|
193
261
|
|
|
194
|
-
|
|
262
|
+
---
|
|
195
263
|
|
|
196
|
-
|
|
264
|
+
## Standalone Rendering
|
|
197
265
|
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
266
|
+
```typescript
|
|
267
|
+
import { renderMarkdown } from "@powerduck/md-editor";
|
|
268
|
+
import "@powerduck/md-editor/dist/style.css";
|
|
269
|
+
|
|
270
|
+
const html = renderMarkdown("# Hello\n\n$E=mc^2$", { math: true });
|
|
271
|
+
document.getElementById("output")!.innerHTML = html;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
> `renderMarkdown` returns a string and does not mount an editor. Mindmap placeholders are emitted as inert `<div>`s; call `Renderer.hydrate` yourself if you need live diagrams outside the editor.
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Public API (Vanilla JS)
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
class MarkdownEditor {
|
|
282
|
+
constructor(container: HTMLElement | string, options?: MarkdownEditorOptions);
|
|
283
|
+
|
|
284
|
+
getValue(): string;
|
|
285
|
+
setValue(value: string): void;
|
|
286
|
+
getHtml(): string;
|
|
287
|
+
renderNow(): void;
|
|
288
|
+
setAutoPreview(auto: boolean): void;
|
|
289
|
+
setMode(mode: "simple" | "complex"): void;
|
|
290
|
+
setTheme(theme: "light" | "dark"): void;
|
|
291
|
+
focus(): void;
|
|
292
|
+
destroy(): void;
|
|
293
|
+
}
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
---
|
|
297
|
+
|
|
298
|
+
## Keyboard Shortcuts
|
|
299
|
+
|
|
300
|
+
| Shortcut | Action |
|
|
301
|
+
|---|---|
|
|
302
|
+
| `Ctrl/Cmd + B` | Bold |
|
|
303
|
+
| `Ctrl/Cmd + I` | Italic |
|
|
304
|
+
| `Ctrl/Cmd + K` | Link |
|
|
305
|
+
| `Ctrl/Cmd + S` | (custom via onChange) |
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Mindmap Syntax
|
|
310
|
+
|
|
311
|
+
````markdown
|
|
312
|
+
```mindmap
|
|
313
|
+
# Root topic
|
|
314
|
+
## Branch one
|
|
315
|
+
- Child A
|
|
316
|
+
- Child B
|
|
317
|
+
## Branch two
|
|
318
|
+
- Child C
|
|
319
|
+
```
|
|
320
|
+
````
|
|
321
|
+
|
|
322
|
+
Renders as an interactive markmap SVG diagram in the preview pane.
|
|
323
|
+
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
## Math Syntax
|
|
327
|
+
|
|
328
|
+
```markdown
|
|
329
|
+
Inline: $E = mc^2$
|
|
330
|
+
|
|
331
|
+
Block:
|
|
332
|
+
$$
|
|
333
|
+
\int_{a}^{b} f(x)\,dx
|
|
334
|
+
$$
|
|
209
335
|
```
|
|
210
336
|
|
|
211
|
-
|
|
337
|
+
Powered by KaTeX.
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## Callout Syntax
|
|
212
342
|
|
|
213
343
|
```markdown
|
|
214
|
-
:::
|
|
215
|
-
|
|
344
|
+
:::notice Title
|
|
345
|
+
Notice content.
|
|
216
346
|
:::
|
|
217
347
|
|
|
218
|
-
:::warning
|
|
219
|
-
|
|
348
|
+
:::warning Warning
|
|
349
|
+
Warning content.
|
|
220
350
|
:::
|
|
221
351
|
|
|
222
|
-
:::danger
|
|
223
|
-
|
|
352
|
+
:::danger Danger
|
|
353
|
+
Danger content.
|
|
224
354
|
:::
|
|
225
|
-
```
|
|
226
355
|
|
|
227
|
-
|
|
356
|
+
:::tip Tip
|
|
357
|
+
Tip content.
|
|
358
|
+
:::
|
|
359
|
+
```
|
|
228
360
|
|
|
229
|
-
|
|
361
|
+
Available types: `notice`, `info`, `tip`, `success`, `warning`, `danger`.
|
|
230
362
|
|
|
231
|
-
|
|
232
|
-
- **Incremental rendering**: unchanged blocks reuse their DOM nodes directly, including already-hydrated mindmap SVGs.
|
|
233
|
-
- **Content-hash cache**: render results are cached by content hash, so undo/redo and copy-paste hit the cache.
|
|
234
|
-
- **Adaptive debounce**: `adaptiveDebounceMs(docLength)` scales from 120ms to 600ms based on document size.
|
|
235
|
-
- **`content-visibility: auto`**: off-screen blocks skip layout and paint entirely.
|
|
363
|
+
---
|
|
236
364
|
|
|
237
365
|
## Development
|
|
238
366
|
|
|
239
367
|
```bash
|
|
368
|
+
# Install dependencies
|
|
240
369
|
npm install
|
|
241
|
-
npm run typecheck # src only
|
|
242
|
-
npm run typecheck:test # src + tests
|
|
243
|
-
npm test # run all tests (vitest + jsdom)
|
|
244
|
-
npm run build # vite build + d.ts generation
|
|
245
|
-
```
|
|
246
|
-
|
|
247
|
-
## React Demo
|
|
248
|
-
```jsx
|
|
249
|
-
import { useRef, useCallback } from "react";
|
|
250
|
-
import { type MentionItem, type DocItem } from "@powerduck/md-editor";
|
|
251
|
-
import {
|
|
252
|
-
MarkdownEditorReact,
|
|
253
|
-
type MarkdownEditorHandle,
|
|
254
|
-
} from "@powerduck/md-editor/react";
|
|
255
|
-
import "@powerduck/md-editor/dist/style.css";
|
|
256
370
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
id: "1",
|
|
260
|
-
label: "Alice",
|
|
261
|
-
avatar: "https://i.pravatar.cc/40?u=alice",
|
|
262
|
-
description: "alice@example.com",
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
id: "2",
|
|
266
|
-
label: "Bob",
|
|
267
|
-
avatar: "https://i.pravatar.cc/40?u=bob",
|
|
268
|
-
description: "bob@example.com",
|
|
269
|
-
},
|
|
270
|
-
{ id: "3", label: "Charlie", description: "charlie@example.com" },
|
|
271
|
-
];
|
|
371
|
+
# Type check
|
|
372
|
+
npm run typecheck
|
|
272
373
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
id: "d1",
|
|
276
|
-
title: "Getting Started",
|
|
277
|
-
url: "https://docs.example.com/start",
|
|
278
|
-
description: "Quick start guide",
|
|
279
|
-
thumbnail: "https://picsum.photos/seed/start/96/72",
|
|
280
|
-
},
|
|
281
|
-
{
|
|
282
|
-
id: "d2",
|
|
283
|
-
title: "API Reference",
|
|
284
|
-
url: "https://docs.example.com/api",
|
|
285
|
-
description: "Full API documentation",
|
|
286
|
-
},
|
|
287
|
-
{
|
|
288
|
-
id: "d3",
|
|
289
|
-
title: "Changelog",
|
|
290
|
-
url: "https://docs.example.com/changelog",
|
|
291
|
-
description: "Release notes",
|
|
292
|
-
},
|
|
293
|
-
];
|
|
374
|
+
# Build (ESM + CJS + type declarations + CSS)
|
|
375
|
+
npm run build
|
|
294
376
|
|
|
295
|
-
|
|
296
|
-
|
|
377
|
+
# Run tests
|
|
378
|
+
npm test
|
|
297
379
|
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}, []);
|
|
302
|
-
|
|
303
|
-
// Stable callbacks so the editor does not recreate on every render
|
|
304
|
-
const searchUsers = useCallback(async (query: string) => {
|
|
305
|
-
return USERS.filter((u) =>
|
|
306
|
-
u.label.toLowerCase().includes(query.toLowerCase()),
|
|
307
|
-
);
|
|
308
|
-
}, []);
|
|
309
|
-
|
|
310
|
-
const searchDocs = useCallback(async (query: string) => {
|
|
311
|
-
return DOCS.filter((d) =>
|
|
312
|
-
d.title.toLowerCase().includes(query.toLowerCase()),
|
|
313
|
-
);
|
|
314
|
-
}, []);
|
|
315
|
-
|
|
316
|
-
const fetchDocMeta = useCallback(async (url: string) => {
|
|
317
|
-
// In production, route through your backend to avoid CORS:
|
|
318
|
-
// const res = await fetch(`/api/og?url=${encodeURIComponent(url)}`);
|
|
319
|
-
// return res.json();
|
|
320
|
-
// For demo: return whatever metadata the item already has
|
|
321
|
-
const doc = DOCS.find((d) => d.url === url);
|
|
322
|
-
return {
|
|
323
|
-
title: doc?.title ?? new URL(url).hostname,
|
|
324
|
-
description: doc?.description,
|
|
325
|
-
thumbnail: doc?.thumbnail,
|
|
326
|
-
url,
|
|
327
|
-
};
|
|
328
|
-
}, []);
|
|
380
|
+
# Dev server
|
|
381
|
+
npm run dev
|
|
382
|
+
```
|
|
329
383
|
|
|
330
|
-
|
|
331
|
-
<div style={{ padding: 24, maxWidth: 1100, margin: "0 auto" }}>
|
|
332
|
-
<div style={{ marginBottom: 12, display: "flex", gap: 8 }}>
|
|
333
|
-
<button onClick={handleSave}>Save</button>
|
|
334
|
-
<button onClick={() => editorRef.current?.setTheme("dark")}>
|
|
335
|
-
Dark
|
|
336
|
-
</button>
|
|
337
|
-
<button onClick={() => editorRef.current?.setTheme("light")}>
|
|
338
|
-
Light
|
|
339
|
-
</button>
|
|
340
|
-
</div>
|
|
384
|
+
---
|
|
341
385
|
|
|
342
|
-
|
|
343
|
-
ref={editorRef}
|
|
344
|
-
defaultValue={[
|
|
345
|
-
"# Welcome",
|
|
346
|
-
"",
|
|
347
|
-
"Type **@** to mention a user, or **/** to insert a document link.",
|
|
348
|
-
"",
|
|
349
|
-
"## Task list",
|
|
350
|
-
"- [ ] This is checked (green)",
|
|
351
|
-
"- [x] This is unchecked",
|
|
352
|
-
"- [ x] Spaces inside brackets also work",
|
|
353
|
-
"",
|
|
354
|
-
"## Table",
|
|
355
|
-
"| Name | Role |",
|
|
356
|
-
"|------|------|",
|
|
357
|
-
"| Alice | Admin |",
|
|
358
|
-
"| Bob | Editor |",
|
|
359
|
-
].join("\n")}
|
|
360
|
-
mode="complex"
|
|
361
|
-
theme="light"
|
|
362
|
-
onChange={(v) => console.log("auto-save:", v.length)}
|
|
363
|
-
mention={{
|
|
364
|
-
onMentionSearch: searchUsers,
|
|
365
|
-
minChars: 0,
|
|
366
|
-
maxItems: 8,
|
|
367
|
-
}}
|
|
368
|
-
docLink={{
|
|
369
|
-
onDocSearch: searchDocs,
|
|
370
|
-
onFetchDocMeta: fetchDocMeta,
|
|
371
|
-
insertStyle: "auto",
|
|
372
|
-
minChars: 0,
|
|
373
|
-
maxItems: 8,
|
|
374
|
-
}}
|
|
375
|
-
/>
|
|
376
|
-
</div>
|
|
377
|
-
);
|
|
378
|
-
}
|
|
386
|
+
## Related Packages
|
|
379
387
|
|
|
380
|
-
|
|
381
|
-
|
|
388
|
+
- [`@powerduck/openapi-parser`](https://www.npmjs.com/package/@powerduck/openapi-parser) — OpenAPI 3.2 parser, validator, and upgrader
|
|
389
|
+
- [`@powerduck/conf-patch`](https://www.npmjs.com/package/@powerduck/conf-patch) — Configuration file editor with atomic writes and locking
|
|
390
|
+
- [`@powerduck/openapi-codegen`](https://www.npmjs.com/package/@powerduck/openapi-codegen) — Generate runnable request examples in 21 languages
|
|
382
391
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
### v0.8.4
|
|
386
|
-
|
|
387
|
-
- **Fixed**: React component `MarkdownEditorReact` now passes through all extra props (`mention`, `docLink`, `toolbar`, `onImageUpload`, `includeCommonHeaders`, `includeCookies`, `placeholder`, etc.) to the underlying `MarkdownEditor` instance. Previously only a subset of props were forwarded, so `mention` and `docLink` configs were silently ignored.
|
|
388
|
-
- **Test**: 311 tests, all passing, zero unhandled errors.
|
|
389
|
-
|
|
390
|
-
### v0.8.3
|
|
391
|
-
|
|
392
|
-
- **Changed**: Task list checkbox semantics inverted per project convention. `- [ ]` now renders as **checked** (green checkmark); `- [x]` / `- [X]` renders as **unchecked** (empty box). `toggleTaskList()` and all tests updated accordingly.
|
|
393
|
-
- **Improved**: Clicking the @mention or document-link toolbar button without configuring the search hook now logs a clear `console.warn` explaining that `mention.onMentionSearch` / `docLink.onDocSearch` must be provided.
|
|
394
|
-
- **Test**: 310 tests, all passing, zero unhandled errors.
|
|
395
|
-
|
|
396
|
-
### v0.8.2
|
|
397
|
-
|
|
398
|
-
- **Fixed**: @mention and `/` doc-link dropdowns did not appear. Root cause: v0.8.1 mounted dropdowns inside `.md-editor-root` where ancestor `overflow:hidden` / stacking contexts could clip or misplace `position:fixed` elements. Reverted to `document.body` mounting with `position:fixed`, and added explicit light/dark colors via `data-theme` attribute (no dependency on editor-scoped CSS variables).
|
|
399
|
-
- **Fixed**: Clicking toolbar button icons (SVG) did not open popups. Root cause: `mousedown` event bubbled to `document` and triggered the newly-opened Popup's outside-click handler (because `e.target` was the SVG, not the button). Fixed in two ways: Popup now uses `anchor.contains(target)` instead of `target !== anchor`; toolbar `mousedown` handler calls `e.stopPropagation()`.
|
|
400
|
-
- **Fixed**: Task list checkboxes now render with a clear green checkmark when checked (`- [x]` or `- [X]`). Previously used native `accent-color` (orange) which was hard to distinguish in disabled state. Custom checkbox: gray border when unchecked, solid green with white checkmark when checked.
|
|
401
|
-
- **Added**: Help dialog now includes a "Syntax" section with quick-reference guidance for task lists, @mentions, document links, math, mindmaps, and callouts.
|
|
402
|
-
- **Fixed**: Help dialog and all popup-internal styles no longer use editor-scoped CSS variables (undefined when popup is on `document.body`), ensuring consistent light/dark rendering.
|
|
403
|
-
- **Test**: 310 tests, all passing, zero unhandled errors.
|
|
404
|
-
|
|
405
|
-
### v0.8.1
|
|
406
|
-
|
|
407
|
-
- **Fixed**: Toolbar buttons now use `mousedown` + `preventDefault` instead of `click`, preventing the editor from losing focus and collapsing the selection before wrap operations. This fixes the long-standing issue where selecting text and clicking quote/list/other toolbar buttons replaced the selection with placeholder text ("quoted text", "list item").
|
|
408
|
-
- **Fixed**: `toggleTaskList()` mixed-state logic. Previously, a mix of checked and unchecked items (or all-unchecked) removed all task markers. Now: mixed or all-unchecked → check all; all-checked → uncheck all. Also supports uppercase `[X]` markers.
|
|
409
|
-
- **Fixed**: Code block copy button now falls back to `document.execCommand('copy')` when `navigator.clipboard` is unavailable (non-HTTPS contexts), instead of silently failing.
|
|
410
|
-
- **Fixed**: @mention and doc-link dropdowns now close when the editor loses focus (blur), with a 150ms delay so clicks on the dropdown itself register first.
|
|
411
|
-
- **Fixed**: @mention and doc-link dropdowns had transparent backgrounds because they used editor-scoped CSS variables (`--_surface`) while being appended to `document.body`. Now appended inside `.md-editor-root` with `position: fixed` (not clipped by `overflow: hidden`), so theme variables resolve correctly.
|
|
412
|
-
- **Fixed**: Popup dialogs (image, video, YouTube, table, help, callout) now respect the editor theme. Previously always light-themed; now dark-themed when the editor is in dark mode.
|
|
413
|
-
- **Test**: 310 tests, all passing, zero unhandled errors.
|
|
414
|
-
|
|
415
|
-
### v0.8.0
|
|
416
|
-
|
|
417
|
-
- **Added**: Document/article link inserter. Type `/` to open a searchable dropdown of documents. `onDocSearch(query)` returns matching documents; on selection, inserts either a plain markdown link or a `:::doc-link` preview card.
|
|
418
|
-
- **Added**: `:::doc-link` preview card rendering — displays title, thumbnail, description, and domain as a clickable card. Auto-fetches Open Graph / meta tags via `onFetchDocMeta(url)` hook; falls back to plain link when metadata is unavailable or fetch fails.
|
|
419
|
-
- **Added**: `insertStyle` option: `'auto'` (default, card when meta available), `'card'` (always card, fallback to link), `'link'` (always plain link).
|
|
420
|
-
- **Added**: `docLink` option to `MarkdownEditorOptions` and `CodeEditorOptions`, with `triggerChar`, `minChars`, `maxItems` configuration.
|
|
421
|
-
- **Added**: Toolbar button for document link insertion.
|
|
422
|
-
- **Test**: 307 tests, all passing, zero unhandled errors.
|
|
423
|
-
|
|
424
|
-
### v0.7.0
|
|
425
|
-
|
|
426
|
-
- **Fixed**: Mindmap nested code fences no longer lose content. Custom block rule tracks fence nesting depth (lines with info string open, bare backticks close), so ` ```js ` inside a ` ```mindmap ` block is preserved.
|
|
427
|
-
- **Added**: Task list (checkbox) toolbar button and `toggleTaskList()` API. Toggles `- [ ]` / `- [x]` on current line or selected lines. Renders as checkboxes in preview. Keyboard shortcut: `Ctrl+Shift+9`.
|
|
428
|
-
- **Added**: @mention dropdown with configurable hooks. `onMentionSearch(query)` returns matching items, `onMentionSelect(item)` returns replacement text. Supports keyboard navigation (ArrowUp/Down, Enter, Escape), avatars, descriptions, `minChars`, and `maxItems` options.
|
|
429
|
-
- **Added**: `mention` option to `MarkdownEditorOptions` and `CodeEditorOptions`.
|
|
430
|
-
- **Test**: 281 tests, all passing, zero unhandled errors.
|
|
431
|
-
|
|
432
|
-
### v0.6.0
|
|
433
|
-
|
|
434
|
-
- **Fixed**: Mindmap `transform: translate(NaN,NaN) scale(NaN)` console errors (hundreds per mindmap). Root cause: `autoFit: true` computed transform on zero-size SVG before layout, and d3-zoom scheduled hundreds of animation frames each throwing "Expected number". Fix: create with `autoFit: false`, set safe default transform immediately, defer `fit()` to next animation frame with dimension guard.
|
|
435
|
-
- **Fixed**: Code block colors now sync with editor theme (light/dark). Previously hardcoded to atom-one-dark in all themes. Now uses CSS variables (`--code-bg`, `--code-fg`, `--code-keyword`, etc.) with GitHub-style light and dark palettes.
|
|
436
|
-
- **Fixed**: UL/OL list styling in preview now has consistent spacing, bullet alignment, and line height.
|
|
437
|
-
- **Improved**: Removed `highlight.js/styles/atom-one-dark.css` import; syntax highlighting colors are now fully theme-aware via CSS variables.
|
|
438
|
-
- **Improved**: Removed stale `markdown-it-texmath` from vite external deps (removed in v0.4.0).
|
|
439
|
-
- **Improved**: Vitest config now excludes `__MACOSX` and AppleDouble (`._*`) files to prevent false test failures from macOS zip artifacts.
|
|
440
|
-
- **Test**: 251 tests, all passing, zero unhandled errors.
|
|
441
|
-
|
|
442
|
-
### v0.5.0
|
|
443
|
-
|
|
444
|
-
- **Fixed**: Admonition `:::` closing marker no longer leaks into rendered body content.
|
|
445
|
-
- **Fixed**: Quote, unordered list, and ordered list toolbar actions now wrap selected text (or the current line) instead of replacing it.
|
|
446
|
-
- **Improved**: Tip/admonition toolbar button now opens a type-picker popup with all six styles (notice, info, tip, success, warning, danger).
|
|
447
|
-
- **Improved**: Help dialog now displays keyboard shortcuts with styled key caps and modifier-key symbols (⌘, ⌥, ⇧).
|
|
448
|
-
- **Improved**: All toolbar icons redesigned in a clean Lucide/Feather line style for a modern Western aesthetic.
|
|
449
|
-
- **Fixed**: Mindmap `transform: translate(NaN,NaN)` error guarded when SVG has zero layout size.
|
|
450
|
-
- **Added**: `formatShortcutHtml()` export for rendering styled key caps.
|
|
451
|
-
- **Test**: 251 tests, all passing.
|
|
452
|
-
|
|
453
|
-
### v0.4.0
|
|
454
|
-
|
|
455
|
-
- Self-contained KaTeX math plugin (removed `markdown-it-texmath`); currency `$5` no longer misparsed.
|
|
456
|
-
- Image upload hook `onImageUpload(file)` with paste/drag support.
|
|
457
|
-
- 11 keyboard shortcuts with macOS Cmd auto-mapping.
|
|
458
|
-
- Help dialog listing all shortcuts.
|
|
459
|
-
- Table rows/columns popup dialog.
|
|
460
|
-
- Theme toggle fixed for dark mode with github-markdown-css.
|
|
461
|
-
- Admonition/tip blocks (`:::notice`, `:::warning`, etc.).
|
|
462
|
-
- Configurable toolbar (include-list or per-item show/label/icon).
|
|
463
|
-
- Standalone `renderMarkdown(source, options)` API export.
|
|
392
|
+
---
|
|
464
393
|
|
|
465
394
|
## License
|
|
466
395
|
|