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

@do-md/core-react

v0.6.0

Published

A Markdown WYSIWYG editor for React

Readme

@do-md/core-react

npm license

A from-scratch Markdown WYSIWYG editor kernel for React.

The Markdown document itself is the editing source of truth — there is no separate internal model to keep in sync. The kernel handles parsing, rendering, editing, undo/redo, and streaming injection directly against the text. It is platform-agnostic: the host application supplies platform capabilities (image resolution, syntax highlighting) through injection points — and can extend the inline syntax itself declaratively, from a [[wikilink]] to a fully custom React-rendered <{.mention id=11}Name>, without touching the parse pipeline (see "Custom inline syntax").

Install

npm i @do-md/core-react

Peer dependencies (you provide these):

  • react >=18 — required
  • react-dom >=18 — required
  • immer ^10 — required; the store is Immer-based and shares your app's Immer instance

Quick start

Wrap the editor surface in a DOMDProvider and render <DOMD /> inside it. Import the stylesheet once.

import { DOMDProvider, DOMD } from "@do-md/core-react";
import "@do-md/core-react/style.css";

export function MyEditor() {
  return (
    <DOMDProvider initMd={"# Title\n\nStart writing…"}>
      <DOMD />
    </DOMDProvider>
  );
}

Render read-only by passing editable={false} — the caret is suppressed and links navigate on click.

Reading and writing Markdown

Use useEditorStoreApi() for imperative one-off reads/writes:

import { useEditorStoreApi } from "@do-md/core-react";

function ExportButton() {
  const store = useEditorStoreApi();
  return (
    <button onClick={() => console.log(store?.toMarkdown())}>Export Markdown</button>
  );
}

Use useEditorStore(selector) for reactive reads — the component re-renders only when the selected slice changes:

import { useEditorStore } from "@do-md/core-react";

function CursorReadout() {
  const cursor = useEditorStore((s) => s.startCursorInfo);
  return <span>{cursor ? `block ${cursor.uuid} @ ${cursor.offset}` : "no cursor"}</span>;
}

Streaming injection

Feed model output into the document chunk by chunk. Partial constructs — open code fences, half-built tables, unfinished lists — render correctly as they arrive.

import { useEditor } from "@do-md/core-react";

function useStreamIntoEditor() {
  const editor = useEditor();
  return async (stream: AsyncIterable<string>) => {
    for await (const chunk of stream) {
      editor?.aiInsertInCursor(chunk);
    }
  };
}

Injection points

The core ships no platform assumptions. Wire host capabilities through provider props:

<DOMDProvider
  imageLoader={async (src) => toDisplayableUrl(src)} // resolve an app-specific ref to a URL the browser can render
  codeTokenizer={(code, lang) => highlight(code, lang)} // supply your own syntax-highlight tokens for code blocks
>
  <DOMD />
</DOMDProvider>

Custom inline syntax (inlineRules)

The kernel hardcodes only standard-Markdown inline syntax. Everything beyond it — including the built-in ==highlight== — is a declarative rule. Register your own delimiters through the inlineRules prop; rules run inside the editor's single parse pipeline on every keystroke, so Markdown round-trip, undo, cursor behavior and collaboration all work with no extra wiring.

import { DOMDProvider, defaultInlineRules } from "@do-md/core-react";

<DOMDProvider
  inlineRules={[
    ...defaultInlineRules, // keep ==highlight== (omit to replace it)
    // ^superscript^
    { open: "^", close: "^", exactLen: true, allowSpace: false, tagName: "sup" },
    // ~subscript~  (~~strikethrough~~ still works)
    { open: "~", close: "~", exactLen: true, allowSpace: false, tagName: "sub" },
    // [[wikilink]]  (`[links](…)` are unaffected)
    { open: "[[", close: "]]", tagName: "span", className: "wikilink" },
  ]}
>
  <DOMD />
</DOMDProvider>

A rule is open + close (they may differ — asymmetric delimiters work), plus how the span renders. Delimiters are punctuation strings; ` \ { } can never be part of one.

Passing inlineRules replaces the default set — spread defaultInlineRules to keep ==. Rules are compiled once at mount (remount the provider to change them), and collaborative peers must be configured with the same set (same contract as codeTokenizer).

{…} parameters

Every rule accepts an optional Pandoc/Djot-style parameter block right after the opening delimiter: {.variant .class #id key=value key2="quoted value"}. Params drive rendering through declarative attrs templates ({key} = named param, {} = first positional):

// %%{bg=red}text%% → translucent red highlight, no pre-provisioned CSS needed
{
  open: "%%", close: "%%", tagName: "mark",
  attrs: { style: "background-color: color-mix(in srgb, {bg} 80%, transparent)" },
}
  • Attr targets are whitelisted: class, style, href, title, id, data-*, aria-*. Event handlers (on*) are unreachable, javascript: hrefs are blocked, and style text compiles into a structured style object.
  • An attr referencing a missing param is voided entirely — never rendered half-substituted.
  • Every .word lands as a class, so %%{.warn}text%% is stylable from plain CSS.
  • The raw {…} text stays verbatim in the document — params affect presentation only and can never corrupt the Markdown.

Variants — one delimiter, many meanings

The first .word selects a variant: the same == syntax can highlight by default and become a comment, a mention, anything — per occurrence:

{
  open: "==", close: "==", tagName: "mark",
  variants: {
    comment: { tagName: "q", className: "comment" },
  },
}
// ==plain highlight==            → <mark>
// =={.comment author=w}text==    → <q class="comment …">

Unregistered variants degrade gracefully: the .word is just a class, and other Pandoc-family tools still recognize the attribute syntax.

Custom render components

For rendering that CSS can't express (icons, click handlers, tooltips), a rule or variant may declare a React component — antd-style, replacing the kernel element. Components bind to variants (semantics), and variants are plain data you can attach to any delimiter — syntax and semantics stay orthogonal:

import { viewOnlyProps, type InlineRuleComponentProps } from "@do-md/core-react";

function MentionSpan({ domProps, children, params }: InlineRuleComponentProps) {
  return (
    <span {...domProps} onClick={() => openProfile(params.named.id)}>
      <span {...viewOnlyProps} className="badge">@</span>
      {children}
    </span>
  );
}

const mention = { tagName: "span", className: "mention", component: MentionSpan };

// The same semantic on two syntaxes:
inlineRules={[
  { open: "==", close: "==", tagName: "mark", variants: { mention } },
  { open: "<",  close: ">",  tagName: "span", variants: { mention } },
]}
// =={.mention id=11}Jintao Wang== ≡ <{.mention id=11}Jintao Wang>

Components receive the full occurrence context: params (named / positional / classes / id), variant, rawCapture, contentText and the dispatched tagName. Overlays (popovers, tooltips) should render through a portal to document.body so they never enter the editable DOM.

Hard contract (violations break cursor mapping): spread domProps on your root element, render children verbatim, and spread viewOnlyProps on every decoration element you add (badges, icons — anything that is not document text). The marker keeps decorations out of the DOM→model text pipeline; without it a text-bearing badge would be read back as typed input and duplicate on every reparse. A throwing component falls back to the kernel's default rendering — the document is never at risk.

Reserved delimiters

Structural Markdown delimiters (<, >, [, *, ~~) can be registered too. Such rules only fire on shapes the builtin never produces, so existing documents keep parsing:

  • {…} disambiguation<{.mention id=1}Name> fires your </> rule; <https://x.com> and <u>…</u> still parse as before.
  • Longest-prefix precedence[[wikilink]] beats the builtin [ link parse; [link](url) is untouched.

Precedence is always: your rule → builtin syntax → literal text.

Credits

This API grew out of do-md/domd#13. Thanks to Paul Hammant for the concrete, well-argued feature request — it pushed on exactly the right boundary and made the extension surface better than what either side first proposed. The example syntaxes follow kotaindah55/extended-markdown-syntax.

Embed / submit-on-Enter mode

For chat-style inputs, rebind the "real newline" key so a bare Enter yields to the host and fires onEnter:

<DOMDProvider
  newlineKey="Shift+Enter" // Shift+Enter inserts a newline; bare Enter is freed
  onEnter={(store) => submit(store.toMarkdown())}
>
  <DOMD />
</DOMDProvider>

API reference

Components

  • DOMDProvider — context provider; configures the editor and hosts the surface.
  • DOMD — the editor surface. Render inside a DOMDProvider.

DOMDProvider props

  • children ReactNode — editor surface; render <DOMD /> here.
  • initMd string — initial Markdown document.
  • editable boolean — when false, renders read-only (no caret; links navigate).
  • placeholder string — shown when the document is empty.
  • imageLoader (src: string) => Promise<string> — resolve an image reference to a displayable URL.
  • codeTokenizer (code: string, lang?: string) => unknown[] — provide syntax-highlight tokens for code blocks.
  • codeBeautify (code: string, lang?: string) => string | undefined — optional code formatter, invoked on Enter inside a code block; return undefined to skip.
  • inlineRules InlineRule[] — declarative custom inline syntax (see "Custom inline syntax"); replaces the default set (defaultInlineRules).
  • newlineKey "Shift+Enter" or "Mod+Enter" — rebind the newline key so bare Enter fires onEnter.
  • onEnter (store: EditorStoreApi, event: KeyboardEvent) => void — called on bare Enter when newlineKey is set.

Hooks

  • useEditor()Editor or null — imperative handle: focus(), aiInsertInCursor(text), editorStore.
  • useEditorStore(selector)T — reactive selector; re-renders when the selected slice changes.
  • useEditorStoreApi()EditorStoreApi or null — imperative store handle for one-off reads/writes.
  • useRenderData()RenderData — current parsed-document handle (opaque).
  • useEditorDom(){ textAreaDomRef } — ref to the editable DOM node.

EditorStoreApi (selected members)

  • toMarkdown() string — serialize the document to Markdown.
  • resetMD(md) void — replace the whole document.
  • insertText(text) void — insert text at the cursor.
  • insertImage(url, altText?) void — insert an image at the cursor.
  • getTitle() string — plain-text title derived from the first block.
  • getSelectionState(contextChars?) SelectionState — selection / cursor snapshot.
  • startCursorInfo CursorInfo or null — reactive cursor position.
  • isEditable boolean — reactive editable flag.
  • duringComposition boolean — reactive IME-composition flag.
  • subscribe(listener) () => void — subscribe to any store change; returns an unsubscribe fn.

Utilities

  • toMarkdown(data: RenderData)string (or null) — serialize a RenderData handle (from useRenderData()) to Markdown.
  • defaultInlineRules InlineRule[] — the shipped rule set (== highlight); spread it to compose.
  • viewOnlyProps — prop bag for decoration elements inside inline-rule component renders (marks the subtree invisible to the text pipeline + contentEditable={false} + aria-hidden).
  • DATA_VIEW_ONLY string — the underlying decoration-marker attribute name, for non-JSX consumers.
  • Types: InlineRule, InlineRuleVariant, InlineRuleParams, InlineRuleComponentProps.

License

PolyForm Noncommercial 1.0.0. Distributed as a prebuilt build artifact. Free for personal, educational, and non-commercial open-source use. Commercial use requires prior written authorization from the author.