npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pd-editor-core

v2.0.0

Published

Framework-agnostic Markdown editor engine for technical content workflows

Readme

pd-editor-core

npm Demo TypeScript CodeMirror 6 License

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, and Shift+Tab understand 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, and getCommandState.
  • 🔌 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-core

30-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 | | 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+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:

bold, italic, strikethrough, heading1, heading2, heading3, unorderedList, orderedList, taskList, link, image, quote, code, codeBlock, table, horizontalRule.

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>&gt;</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.replaceSelection("new text");
editor.wrapSelection("**", "**");
editor.getSelection();
editor.insertAtCursor("<!-- note -->");
editor.getEditorView(); // underlying CodeMirror EditorView
editor.destroy();

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) => {
        const form = new FormData();
        form.append("file", file);

        const response = await fetch("/api/upload", {
          method: "POST",
          body: form,
        });

        const data = await response.json() as { url: string };
        return data.url;
      },
    }),
  ],
});

Options:

| Option | Type | Default | | --- | --- | --- | | handler | (file: File) => Promise<string> | required | | accept | string[] | ["image/*"] | | maxSize | number | unlimited | | pasteUpload | boolean | true | | dragUpload | boolean | true |

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 | | extensions | Extension[] | [] | | codeLanguages | MarkdownCodeLanguages | - | | plugins | EditorPlugin[] | [] | | toolbar | boolean \| ToolbarItem[] | true |

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