pd-editor-core
v2.2.1
Published
Framework-agnostic Markdown editor engine for technical content workflows
Maintainers
Readme
pd-editor-core
Framework-agnostic Markdown editor engine for technical content workflows, powered by CodeMirror 6.
Use it directly in vanilla JavaScript, or as the foundation for pd-editor-react and pd-editor-vue.
pd-editor-core focuses on the hard editor work: selection-safe Markdown commands, polished typing flow, plugin lifecycle, image upload, TOC, themes, toolbar state, and CodeMirror extension interop.
Try the interactive demo: laochen1994.github.io/pd-markdown-editor.
Why It Feels Good
- 🚀 High-performance editor core - CodeMirror 6 editor state, transactions, history, search, folding, completion, and Markdown language support.
- 🧠 Markdown-aware typing -
Enter,Tab, andShift+Tabunderstand lists, tasks, ordered lists, and quotes. - 🧩 Plugin system - install plugins at creation time or dynamically with
editor.use()/editor.unuse(). - 🖼️ Image upload plugin - paste and drag images, insert upload placeholders, replace with final URLs.
- 🧭 TOC plugin - live heading extraction using parser-generated stable heading ids.
- 🛠️ Built-in toolbar - formatting, lists, links, image, quote, code, table, and horizontal rule.
- 🌓 Light/dark themes - GitHub-inspired defaults with CSS variable hooks.
- 🎯 Command state API - power active/disabled toolbar states with
isActive,canExecute, andgetCommandState. - 🔌 CodeMirror-native extension escape hatch - pass your own CM6 extensions when you need low-level control.
- 🧱 Framework independent - no React/Vue dependency in core.
Install
pnpm add pd-editor-core
# npm install pd-editor-core
# yarn add pd-editor-core30-Second Usage
import { MarkdownEditor } from "pd-editor-core";
const editor = new MarkdownEditor({
parent: document.querySelector("#editor")!,
initialValue: "# Hello pd-editor-core",
theme: "light",
placeholder: "Start writing...",
onChange: (value) => {
console.log(value);
},
onSave: (value) => {
console.log("save", value);
},
});<div id="editor" style="height: 600px"></div>Feature Matrix
| Area | Included |
| --- | --- |
| Editing | Markdown language mode, history, search, folding, bracket matching, autocomplete |
| Commands | Bold, italic, strikethrough, heading 1-3, link, image, unordered/ordered/task list, quote, inline code, code block, table, horizontal rule, undo, redo, search/replace |
| Typing flow | Continue list/task/ordered/quote on Enter; exit empty markers; indent/outdent block lines with Tab / Shift+Tab |
| Toolbar | Default toolbar, custom toolbar items, plugin-provided toolbar items |
| Plugins | Image upload, TOC, custom CM6 extensions, runtime install/remove |
| State | getValue, setValue, getSelection, isActive, canExecute, getCommandState, setReadOnly, setTheme |
| Packaging | ESM + CJS, TypeScript declarations, tree-shakeable JS |
Keyboard Shortcuts
| Shortcut | Command |
| --- | --- |
| Ctrl/Cmd+B | Bold |
| Ctrl/Cmd+I | Italic |
| Ctrl/Cmd+K | Link |
| Ctrl/Cmd+Shift+X | Strikethrough |
| Ctrl/Cmd+Alt+1 | Heading 1 |
| Ctrl/Cmd+Alt+2 | Heading 2 |
| Ctrl/Cmd+Alt+3 | Heading 3 |
| Ctrl/Cmd+Shift+7 | Ordered list |
| Ctrl/Cmd+Shift+8 | Bullet list |
| Ctrl/Cmd+Shift+9 | Quote |
| Ctrl/Cmd+F | Search and replace |
| Ctrl/Cmd+S | onSave callback |
| Enter | Continue Markdown list/task/ordered/quote block |
| Tab | Indent Markdown block line |
| Shift+Tab | Outdent Markdown block line |
Built-In Toolbar
The default toolbar includes:
undo, redo, bold, italic, strikethrough, heading1, heading2, heading3, unorderedList, orderedList, taskList, link, image, quote, code, codeBlock, table, horizontalRule, search.
Disable it:
const editor = new MarkdownEditor({
parent,
toolbar: false,
});Provide your own:
import type { ToolbarItem } from "pd-editor-core";
const toolbar: ToolbarItem[] = [
{ command: "bold", label: "Bold", icon: "<strong>B</strong>", shortcut: "Ctrl+B" },
{ command: "quote", label: "Quote", icon: "<span>></span>" },
];
const editor = new MarkdownEditor({
parent,
toolbar,
});Custom icon values are treated as trusted application HTML. Keep them internal to your app.
Command API
editor.executeCommand("bold");
editor.executeCommand("heading2");
editor.executeCommand("table");
editor.isActive("bold"); // true/false
editor.canExecute("link"); // false when readOnly
editor.getCommandState("heading2"); // { active: boolean, enabled: boolean }Editor Instance API
editor.getValue();
editor.setValue("# Updated");
editor.setValue("# External sync", { emitChange: false }); // update without onChange/plugin update callbacks
editor.focus();
editor.setTheme("dark");
editor.setReadOnly(true);
editor.setMaxLength(5000);
editor.getCharacterCount();
editor.setToolbar(true, { bold: "加粗", "image-upload": "上传图片" });
editor.setMessages(zhCN);
editor.getMessage("tableOfContents");
editor.replaceSelection("new text");
editor.wrapSelection("**", "**");
editor.getSelection();
editor.insertAtCursor("<!-- note -->");
editor.getEditorView(); // underlying CodeMirror EditorView
editor.destroy();Internationalization
import { MarkdownEditor, zhCN } from "pd-editor-core";
const editor = new MarkdownEditor({ parent, messages: zhCN });
editor.setMessages({ ...zhCN, tableOfContents: "文章目录" });The built-in enUS and zhCN catalogs cover toolbar labels, TOC, image upload placeholders, lint diagnostics, and preview status messages. Existing labels overrides remain supported and take precedence for toolbar commands.
Copy or download content without adding another dependency:
import { copyHtml, copyMarkdown, downloadMarkdown } from "pd-editor-core";
await copyMarkdown(editor.getValue());
await copyHtml(renderedPreview.innerHTML);
downloadMarkdown(editor.getValue(), "notes.md");Plugins
Image Upload
Paste or drag image files into the editor. The plugin inserts a temporary Markdown image and replaces it when your upload handler resolves.
import { MarkdownEditor, imageUploadPlugin } from "pd-editor-core";
const editor = new MarkdownEditor({
parent,
plugins: [
imageUploadPlugin({
maxSize: 5 * 1024 * 1024,
accept: ["image/png", "image/jpeg", "image/webp"],
handler: async (file, { signal, reportProgress }) => {
const form = new FormData();
form.append("file", file);
reportProgress(10);
const response = await fetch("/api/upload", {
method: "POST",
body: form,
signal,
});
const data = await response.json() as { url: string };
reportProgress(100);
return data.url;
},
onStatusChange: (upload) => {
console.log(upload.status, upload.progress);
// upload.cancel?.();
// upload.retry?.();
},
}),
],
});Options:
| Option | Type | Default |
| --- | --- | --- |
| handler | (file: File, context: ImageUploadContext) => Promise<string> | required |
| accept | string[] | ["image/*"] |
| maxSize | number | unlimited |
| pasteUpload | boolean | true |
| dragUpload | boolean | true |
| onStatusChange | (update: ImageUploadUpdate) => void | - |
| onReject | (file, reason) => void | - |
| onError | (error, file) => void | - |
| label | string | "Upload Image" |
One-argument handlers remain compatible. Use context.signal to abort the request and context.reportProgress(0..100) when the transport exposes progress. Status updates include cancel while uploading and retry after failure or cancellation.
Table Of Contents
Render a live TOC from Markdown headings. Heading ids come from pd-markdown/parser, so duplicate headings stay stable (intro, intro-1, ...).
import { MarkdownEditor, tocPlugin } from "pd-editor-core";
const toc = document.querySelector("#toc")!;
const editor = new MarkdownEditor({
parent,
plugins: [
tocPlugin({
container: toc,
maxLevel: 3,
}),
],
});<div id="toc"></div>
<div id="editor"></div>Write Your Own Plugin
Plugins can add CodeMirror extensions, toolbar items, and update hooks.
import { EditorView } from "@codemirror/view";
import type { EditorPlugin } from "pd-editor-core";
export const wordCountPlugin = (container: HTMLElement): EditorPlugin => ({
name: "word-count",
install: (editor) => {
container.textContent = `${editor.getValue().length} chars`;
return EditorView.updateListener.of((update) => {
if (update.docChanged) {
container.textContent = `${update.state.doc.length} chars`;
}
});
},
toolbar: () => ({
command: "horizontalRule",
label: "Divider",
icon: "<span>—</span>",
divider: false,
}),
onUpdate: ({ value }) => {
console.log("updated", value.length);
},
destroy: () => {
container.textContent = "";
},
});Runtime lifecycle:
editor.use(wordCountPlugin(document.querySelector("#meta")!));
editor.unuse("word-count");CodeMirror Extensions
Use native CodeMirror extensions when you need deep customization:
import { EditorView } from "@codemirror/view";
import { MarkdownEditor } from "pd-editor-core";
const editor = new MarkdownEditor({
parent,
extensions: [
EditorView.lineWrapping,
EditorView.theme({
"&": { fontSize: "14px" },
}),
],
});Fenced code language data is opt-in to keep the default editor bundle smaller. If you need CodeMirror syntax highlighting inside fenced code blocks, install and pass your own language resolver:
import { languages } from "@codemirror/language-data";
import { MarkdownEditor } from "pd-editor-core";
const editor = new MarkdownEditor({
parent,
codeLanguages: languages,
});Options
| Option | Type | Default |
| --- | --- | --- |
| parent | HTMLElement | required |
| initialValue | string | "" |
| theme | "light" \| "dark" | "light" |
| onChange | (value: string) => void | - |
| onSave | (value: string) => void | - |
| placeholder | string | - |
| readOnly | boolean | false |
| maxLength | number | unlimited |
| extensions | Extension[] | [] |
| codeLanguages | MarkdownCodeLanguages | - |
| plugins | EditorPlugin[] | [] |
| toolbar | boolean \| ToolbarItem[] | true |
| labels | EditorLabels | {} |
FAQ
Can I use this without React or Vue?
Yes. This package is the DOM-only core. Use pd-editor-react or pd-editor-vue only if you want framework adapters.
Does read-only mode block toolbar and programmatic commands?
Yes. canExecute() returns false, and command helpers avoid document changes while read-only.
Can I update the document without firing change callbacks?
Yes. editor.setValue(value, { emitChange: false }) updates the document and toolbar state without calling onChange or plugin onUpdate hooks. Framework adapters use this for controlled value sync.
Can I add plugins after the editor is mounted?
Yes. Use editor.use(plugin) and editor.unuse(pluginName). Runtime plugins can return CodeMirror extensions and toolbar items.
How do I customize the toolbar active state?
Use editor.getCommandState(command). Framework adapters can use this to render active/disabled buttons.
Does the core render Markdown preview?
No. Core is focused on editing. Preview rendering lives in pd-editor-react and pd-editor-vue, powered by pd-markdown and pd-markdown-ui.
Is it SSR safe?
The editor needs a browser DOM. In SSR apps, instantiate it in a client-only boundary.
Related Packages
pd-editor-react- React component and hook.pd-editor-vue- Vue 3 component and composable.pd-markdown- Markdown parser/renderer used by preview adapters.pd-markdown-ui- Styled Markdown preview primitives.
License
MIT
