Last verified: 2026-08-18 · commit
eae4434Source:packages/draftly/src/plugins/For the contract these all implement, see plugin-system.md.
14 built-in plugins. All are exported individually from draftly/plugins and bundled
via createEssentialPlugins() (draftly/plugins — the 11 light plugins) and
createAllPlugins() (draftly/plugins/all — those plus MathPlugin, MermaidPlugin and
EmojiPlugin, which sit behind their own entry points because of their dependencies).
| Plugin | Ver | Prio | LOC | Base class | Owns |
|---|---|---|---|---|---|
paragraph |
1.0.0 | 100 | 38 | DraftlyPlugin |
Paragraph — render-only, no decorations |
heading |
1.0.0 | 10 | 197 | DecorationPlugin |
ATXHeading1–6, HeaderMark |
quote |
1.0.0 | 10 | 146 | DecorationPlugin |
Blockquote, QuoteMark |
hr |
1.0.0 | 10 | 102 | DecorationPlugin |
HorizontalRule |
inline |
1.0.0 | 20 | 305 | DecorationPlugin |
Bold, italic, strikethrough, highlight |
list |
1.0.0 | 20 | 492 | DecorationPlugin |
Bullet/ordered lists, task lists |
table |
2.0.0 | 20 | 1759 | DecorationPlugin |
GFM tables — full interactive editing |
emoji |
1.0.0 | 20 | 140 | DecorationPlugin |
:shortcode: → emoji (custom parser node) |
link |
1.0.0 | 22 | 509 | DecorationPlugin |
Link |
code |
1.0.0 | 25 | 1368 | DecorationPlugin |
InlineCode, FencedCode + diff view |
image |
1.0.0 | 25 | 447 | DecorationPlugin |
Image — inline rendering, captions |
math |
1.0.0 | 25 | 526 | DecorationPlugin |
$…$ / $$…$$ via KaTeX (custom parser nodes) |
mermaid |
1.0.0 | 25 | 500 | DecorationPlugin |
```mermaid diagram blocks |
html |
1.0.0 | 30 | 419 | DecorationPlugin |
Raw HTML: blocks, tags, comments |
code-plugin.theme.ts (426 LOC) holds CodePlugin's styles separately — the precedent
for splitting a theme out when it dominates the plugin file.
heading, quote, hr. Apply Decoration.line() to whole lines and hide leading
markers (#, >, ---) when the selection is elsewhere. Simplest plugins in the
codebase — read heading-plugin.ts first when learning the system.
inline owns bold/italic/strikethrough/highlight. Notable for contributing all three
kinds of extension at once:
getMarkdownConfig()— adds==highlight==to the parsergetKeymap()—Mod-b/Mod-ietc. viatoggleMarkdownStyle()getExtensions()—createWrapSelectionInputHandlerfromlib/, so typing*with text selected wraps it (added for issue #1)
list handles nesting, bullet substitution, and task checkboxes. table is the outlier
— see plugin-table.md.
link, code, image, math, mermaid. All replace source ranges with rendered
WidgetType output. Shared concerns:
- Widgets must implement
eq()correctly, or CodeMirror re-creates DOM on every update.eq()returningfalseis a deliberate "always rebuild" (used byTableControlsWidgetto keep handler closures fresh) — never an accident. ignoreEvent()decides whether the editor sees events inside the widget. Interactive widgets (copy buttons, table controls) returnfalse.mermaidde-duplicates concurrent renders of the same definition (C-030): a module-level map holds the in-flight promise, retracted the moment it settles. Not a cache — no settled result is ever retained, so an edited diagram cannot be served a stale SVG and a failed render is retried by the next caller.mathandmermaidrender asynchronously; both must handle the widget being destroyed before their render resolves.
html runs last so it sees the fully-decorated document before deciding how to treat raw
HTML. It is also the main consumer of ctx.sanitize() — and therefore the plugin most
affected by the server-side sanitization gap documented in
preview-pipeline.md.
Until C-012 it declared no requiredNodes and no renderToHTML, so it was silently
absent from preview on the one node type where absence is dangerous — HTML nodes fell
through to the renderer's unescaped leaf fallback. It now claims HTMLBlock, HTMLTag,
Comment and CommentBlock.
Its HTMLTag path is worth knowing about: DOMPurify balances the fragment it is handed,
so a lone <b> becomes <b></b> and a lone </b> becomes "". The plugin therefore
sanitizes inside a balanced probe and reads the verdict off the result, re-emitting the
tag in its original role. Do not "simplify" it to a direct ctx.sanitize() call.
Most plugins decorate nodes Lezer already produces. These three add new node types via
getMarkdownConfig():
| Plugin | New nodes | Syntax |
|---|---|---|
inline |
highlight mark | ==text== |
math |
InlineMath, MathBlock, + their marks |
$x$, $$x$$ |
mermaid |
MermaidBlock, MermaidBlockMark |
```mermaid |
emoji |
Emoji, EmojiMark |
:smile: |
table |
re-exports @lezer/markdown's GFM Table |
| a | b | |
Parser extensions are the one thing that must be identical across editor and preview —
which the contract guarantees by having both call getMarkdownConfig().
| Plugin | Dependency | Weight | Notes |
|---|---|---|---|
math |
katex (optional peer) |
not bundled | Ships no CSS by default; the host imports katex/dist/katex.min.css. injectStyles: true injects katex-styles.generated.ts instead — KaTeX's CSS with all 20 faces as data: URIs, behind a dynamic import() (C-029) |
mermaid |
mermaid |
very large | Async render; dominates bundle size |
html |
dompurify |
medium | Browser-only effectiveness |
emoji |
node-emoji |
small |
These are why the three sit behind their own entry points rather than in the barrel. A consumer who does not need
diagrams should not import MermaidPlugin at all — the subpath exports and tsup's
splitting: true make that a real bundle saving.
- Create
packages/draftly/src/plugins/<feature>-plugin.ts. - Extend
DecorationPlugin(orDraftlyPluginif render-only). - Set
name,version,decorationPriority,requiredNodes. - Add the named export and the
createEssentialPlugins()entry inplugins/index.ts— unless the plugin pulls a heavy dependency, in which case it gets its own entry point and an entry inplugins/all.tsinstead. - Add a row to the catalog table above.
- Verify both surfaces in the playground: the editor pane and the rendered preview.
Split into a directory (plugins/<feature>/) rather than growing past ~500 LOC.