md-dragger
v2.0.2
Published
Core markdown block drag-and-drop engine
Maintainers
Readme
md-dragger
Platform-agnostic markdown block drag-and-drop engine.
md-dragger is a headless core for block-level drag and drop in markdown editors: it detects markdown blocks (headings, lists, tables, callouts, code, math, …), runs the drag gesture, computes the move, and produces a minimal document edit. It does not import Obsidian, CodeMirror, DOM events, or any editor API — hosts wire it into their editor of choice.
- Headless — no UI, no editor, no platform dependency in the core.
- Markdown-aware — block detection, nesting, container rules, list renumbering, and table/math/fence integrity are all computed on the document model.
- Two integration levels — drop in the CodeMirror 6 adapter, or drive the runtime with your own input/commit/locate hosts.
- Small & typed — strict TypeScript, tree-shakeable entry points.
Reference hosts:
- obsidian-dragger — Obsidian plugin built on the CM6 adapter.
site/— an in-repo Astro playground that demonstrates the adapter on ink-mde: runpnpm --dir site dev.
Install
npm install md-dragger
# peer dependencies for the CM6 adapter
npm install @codemirror/state @codemirror/viewEntry points
| Import | Contents |
| --- | --- |
| md-dragger | Domain + runtime aggregates |
| md-dragger/domain | Pure markdown model: detectBlock, parseLine, planMove, moveTx, locateDropPosition, selection helpers |
| md-dragger/runtime | Headless DraggerRuntime, input/locate/commit host types |
| md-dragger/runtime/modules | Reusable runtime modules (e.g. autoScroll) |
| md-dragger/adapter/codemirror | CM6 wiring: mdDragger(), dragHandleGutter, dragRuntime, paint/decorations helpers |
Quick start (CodeMirror 6)
import { EditorState } from '@codemirror/state';
import { EditorView } from '@codemirror/view';
import { mdDragger } from 'md-dragger/adapter/codemirror';
const view = new EditorView({
parent: document.body,
state: EditorState.create({
doc: '# Title\n\n- item one\n- item two',
extensions: [
mdDragger({
// Required: structural config. tabSize is re-read live from the
// editor state; listIndentUnit is the nesting step in columns.
config: { tabSize: 4, listIndentUnit: 4 },
// Required: rendered pixel width of one list nesting level
// (x-axis drag step). Measure it from your theme.
listIndentWidthPx: 36,
}),
],
}),
});The adapter paints a ⋮⋮ handle per block in a gutter and wires pointer input, drop hit-testing, cross-pane commits, and the drag pipeline. Hosts provide the CSS (the protocol classes are documented in docs/api-reference.md; see the Obsidian plugin's styles.css for a reference implementation).
For non-CM6 editors, drive DraggerRuntime directly — see docs/getting-started.md.
Architecture
┌──────────────────────────────────────────────────────────┐
│ Host — owns the editor, renders handles/seam, styles │
│ (obsidian-dragger, the site playground, …) │
└──────────────────────────┬───────────────────────────────┘
│ mdDragger() / DraggerRuntime
┌──────────────────────────┴───────────────────────────────┐
│ adapter/codemirror — CM6 gutter, pointer input, locate, │
│ commit routing, paint decorations │
├──────────────────────────────────────────────────────────┤
│ runtime — headless gesture state machine & drag pipeline │
├──────────────────────────────────────────────────────────┤
│ domain — pure markdown block model, parsing, move plans │
└──────────────────────────────────────────────────────────┘- domain is pure: text in, plans/edits out. No I/O, no DOM.
- runtime is headless: it consumes an
InputSource, aDocumentHost, aLocateHost, and aCommitHostand drives the gesture/drop pipeline. - adapter connects the runtime to CodeMirror 6 and exposes decoration builders hosts paint from.
See docs/architecture.md for the full host responsibility checklist.
Documentation
- Getting started — detailed integration guide (adapter and headless runtime), including the
enabledview filter. - API reference — every public option and export.
- Architecture — layering, host contract, and the paint protocol.
- Changelog
