Markdown plugins
Connect new syntax to incremental parsing, semantic queries and source presentation.
Distinguish three plugin responsibilities
| Capability | MSFE entry | Mutates source? |
|---|---|---|
| Recognize syntax | Lezer extensions through addMarkdownExtensions() | No |
| Render recognized syntax | addSource(), addNodeView(), addProjectionLayers() | Rendering itself does not |
| Normalize typed or pasted input | InputRule, PasteRule | Yes, through transactions and history |
markdown-it plugins consume a token stream, rule chain and HTML renderer. MSFE uses Lezer's incremental tree. A plugin intended for md.use(plugin) cannot be installed directly as an MSFE extension. A third-party renderer may run inside an isolated non-editable preview, but source ranges, parsing and writes still need integration.
A working parser plugin
Optional document properties
Frontmatter recognizes source only. Explicitly install DocumentProperties to display a leading YAML
header as a property table. MD Editor does not install it by default. Vue consumers can reuse the public form:
<script setup lang="ts">
import { MdEditor, MdDocumentProperties } from '@quirekit/md-editor/vue'
import { DocumentProperties } from '@quirekit/msfe-markdown-extras'
import { VueComponentViewRenderer } from '@quirekit/msfe-vue'
const extensions = [DocumentProperties.configure({
render: VueComponentViewRenderer(MdDocumentProperties), locale: 'en',
defaultCollapsed: true,
})]
const markdown = '---\ntitle: Example\ntags: [writing, editing]\n---\n\nBody'
</script>
<template><MdEditor :markdown="markdown" :extensions="extensions" /></template>
MD Editor includes Frontmatter; direct MSFE Core consumers must install it alongside DocumentProperties.
Omitting render uses a native property display and a source entry point.
setDocumentPropertiesEnabled(editor, false) restores the literal Frontmatter; true enables the table again.
It preserves the instance, undo history, body and selection. Read the applied preference with
documentPropertiesEnabled(editor.state). The example page uses this public API for its Document properties switch.
defaultCollapsed: true initially shows only the heading and count, keeping the document body prominent.
Omit it or use false to start expanded. Both the Vue panel and native renderer honor this setting.
Manual choices survive edits, mode changes, projection toggles and viewport remounts within the same document.
replaceDocument reapplies the default for the next document. enabled: false shows literal YAML instead
of a collapsed property panel. Hosts can persist a preference across sessions and pass it as the next default.
Read the current state with documentPropertiesCollapsed(editor.state). Call
setDocumentPropertiesCollapsed(editor, true) to collapse or pass false to expand. Custom renderers use
view.collapsed and view.setCollapsed(). Readonly mode still allows folding without changing edit permissions.
MdDocumentProperties initially previews six fields; long text and tag lists
expand individually. Click a value to edit it with confirmation and cancellation on the same row. Enter saves
a single-line field, Escape cancels, and ⌘/Ctrl + Enter saves multiline fields. Collapsing retains unsaved drafts;
expansion state is never written to Markdown.
Strings, numbers, booleans and string lists are editable through source transactions and undo. Other values remain literal. Invalid YAML, duplicate keys, aliases, anchors, explicit tags, flow or indented root maps, explicit or multiline keys, TOML, unclosed headers and headers over 20,000 UTF-16 units fall back to source. New property names are always encoded as string keys. Edits replace only the target range, retaining other fields, comments and blank lines. Obsolete forms are rejected after a document or header change. Source mode always shows the original header; readonly editors expose no modification controls.
A custom badge
The example recognizes :badge[label]:, retains delimiters and adds theme color. Unclosed, oversized or nested-bracket forms remain ordinary source. The same spelling inside a code span is not a badge.
1. Contribute syntax
Depend on @quirekit/lezer-markdown and return a MarkdownExtension. plugins/syntax.ts registers ExampleBadge through defineNodes. Its inline parser checks the exact prefix at a colon and examines at most 82 following units for the closing form.
context.elt() uses canonical source coordinates, and addElement() returns the consumed endpoint. Return -1 when not applicable. before / after controls parser ordering, not visual priority. Nesting is intentionally unsupported here. More complex syntax needs explicit block/inline rules and fallback behavior.
For simple, non-nested, single-line literal delimiters, use defineSource({ syntax }) as in the status node. Avoid maintaining a second whole-document parser for a simple token.
2. Add semantic queries
addSource() reads confirmed ExampleBadge nodes as { label }. Read only the matched range. Add attribute validation and a precise patch if fields become editable. The semantic layer does not need HTML rendering.
const query = editor.querySource('exampleBadge')
if (query.status === 'ready') {
for (const match of query.matches) console.log(match.from, match.to, match.attrs)
}
Queries default to the current selections or accept an explicit bounded range. pending means parsing is unconfirmed, not that syntax is absent; unsupported means the semantic entry is unavailable. Use requestMarkdownParse(editor.state) to register demand, then retry on accepted-state notification. Do not synchronously parse the whole document during input.
3. Choose presentation and editing
The example's inline layer returns marks and uses a theme. It does not hide delimiters. Use a NodeView for interactive controls or a block layer for multiline replacements. A display plugin should not silently normalize source when loaded.
Porting a markdown-it plugin
- Describe syntax, input/output, nesting, escapes and references.
- Implement Lezer handling for incomplete input, code contexts and nesting.
- Describe fields in a source codec; keep fields readonly until exact encoding exists.
- Render through projections or components; implement HTML export independently.
- Create source fixtures for unchanged reads, precise writes, undo, failures, readonly and incomplete parsing.
Many common plugins already have built-in equivalents. Check Features and installation, then Input rules and Projection layers.