@powerduck/md-editor 0.10.7 → 0.10.9
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 +136 -0
- package/dist/Editor.d.ts +11 -0
- package/dist/index.cjs +31 -29
- package/dist/index.d.ts +2 -1
- package/dist/index.mjs +4780 -2115
- package/dist/plugins/syntax-highlight.d.ts +4 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -244,6 +244,142 @@ npm test # run all tests (vitest + jsdom)
|
|
|
244
244
|
npm run build # vite build + d.ts generation
|
|
245
245
|
```
|
|
246
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
|
+
|
|
257
|
+
const USERS: MentionItem[] = [
|
|
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
|
+
];
|
|
272
|
+
|
|
273
|
+
const DOCS: DocItem[] = [
|
|
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
|
+
];
|
|
294
|
+
|
|
295
|
+
function App() {
|
|
296
|
+
const editorRef = useRef<MarkdownEditorHandle>(null);
|
|
297
|
+
|
|
298
|
+
const handleSave = useCallback(() => {
|
|
299
|
+
const markdown = editorRef.current?.getValue() ?? "";
|
|
300
|
+
console.log("Saving:", markdown);
|
|
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
|
+
}, []);
|
|
329
|
+
|
|
330
|
+
return (
|
|
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>
|
|
341
|
+
|
|
342
|
+
<MarkdownEditorReact
|
|
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
|
+
}
|
|
379
|
+
|
|
380
|
+
export default App;
|
|
381
|
+
```
|
|
382
|
+
|
|
247
383
|
## Changelog
|
|
248
384
|
|
|
249
385
|
### v0.8.4
|
package/dist/Editor.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { EditorView } from '@codemirror/view';
|
|
2
2
|
import { type MentionOptions } from './plugins/mention.js';
|
|
3
3
|
import { type DocLinkOptions } from './plugins/doclink.js';
|
|
4
|
+
export type EditorTheme = 'light' | 'dark';
|
|
4
5
|
export interface CodeEditorOptions {
|
|
5
6
|
value: string;
|
|
6
7
|
onChange?: (value: string) => void;
|
|
@@ -13,16 +14,20 @@ export interface CodeEditorOptions {
|
|
|
13
14
|
mention?: MentionOptions;
|
|
14
15
|
/** Document link inserter configuration */
|
|
15
16
|
docLink?: DocLinkOptions;
|
|
17
|
+
/** Initial color theme for syntax highlighting. Defaults to 'light'. */
|
|
18
|
+
theme?: EditorTheme;
|
|
16
19
|
}
|
|
17
20
|
export declare class CodeEditor {
|
|
18
21
|
readonly view: EditorView;
|
|
19
22
|
private lineNumbersCompartment;
|
|
20
23
|
private customKeymapCompartment;
|
|
24
|
+
private highlightCompartment;
|
|
21
25
|
private onImageFile?;
|
|
22
26
|
private mention;
|
|
23
27
|
private docLink;
|
|
24
28
|
private blurTimeout;
|
|
25
29
|
private destroyed;
|
|
30
|
+
private theme;
|
|
26
31
|
constructor(container: HTMLElement, options: CodeEditorOptions);
|
|
27
32
|
private handlePaste;
|
|
28
33
|
private handleDrop;
|
|
@@ -37,6 +42,12 @@ export declare class CodeEditor {
|
|
|
37
42
|
getValue(): string;
|
|
38
43
|
setValue(value: string): void;
|
|
39
44
|
setLineNumbers(enabled: boolean): void;
|
|
45
|
+
/**
|
|
46
|
+
* Switch the syntax highlighting theme. Reconfigures only the highlight
|
|
47
|
+
* compartment — the document and view state are preserved, so this is
|
|
48
|
+
* effectively free on large documents.
|
|
49
|
+
*/
|
|
50
|
+
setTheme(theme: EditorTheme): void;
|
|
40
51
|
insertAtCursor(text: string): void;
|
|
41
52
|
/**
|
|
42
53
|
* Wrap the current selection with prefix and suffix. If nothing is
|