Overview
Markdown editor extended with inline review threads, anchored comments, and collaborative annotation state.
Usage
Guidance
Use When
- Building a document review experience that needs both a Markdown editor and anchored comment threads in one bundled surface.
- Threading reviewer commentary against specific selections inside a long-form document.
Avoid When
- Plain authoring with no review threads — markdown-editor is the lighter primitive.
- Reviewing diffs between two documents rather than annotating one — use diff-viewer instead.
Persisting and restoring
getState() returns a serializable ReviewState. Its threads are PersistedThread[], which deliberately drop the ProseMirror from/to positions — those only mean something against a live document, and a document can move on between save and restore.
To restore, bind value and threads from the same saved state and run the threads through toRuntimeThreads:
toRuntimeThreads seeds from/to with 0. That pair is an unplaced sentinel rather than a position: a collapsed range paints no highlight, and the anchor plugin locates each thread by its quote against the live document and writes the real positions back through the binding shortly after mount.
Restoring against different content is survivable. Because every restored thread goes through re-anchoring, a thread whose quote is no longer in the document comes back orphaned rather than placed: it is kept in threads, paints no highlight, shows in the sidebar as missing its text, and re-anchors on a later pass if the text returns. onthreaddelete does not fire, and no cleanup is owed — removing an orphaned thread is your decision, made with deleteThread.
The same is true while editing. Deleting anchored text orphans its thread instead of destroying it, because a deletion and the first half of a cut-and-paste are indistinguishable at the moment the text disappears, and re-anchoring is debounced 300ms — faster than anyone cutting a paragraph and pasting it back. Check anchor.status to tell the two states apart:
Comment exports carry the same signal: an orphaned thread serializes with status: 'orphaned', and its stale coordinates move to lastKnownSelection (omitted entirely when no genuine historical offset exists) so nothing reads as a current position.
The imperative alternative is setState(saved), which sets the content and re-anchors in one call:
Do not hand-compute anchor positions. If you seed threads directly, anchor.from/to are ProseMirror positions against the full document — including the front matter's character length when the content has any — while anchor.lastKnownOffset is a doc.textBetween() text offset. Neither is an index into the Markdown string. A newly seeded anchor that does not match the document is reported in dev and re-anchored by quote. The 0/0 sentinel created by toRuntimeThreads is intentionally exempt from that warning and re-anchors normally. Restore through toRuntimeThreads or setState, and let the component compute the positions.
When to use
- Building a document review experience that needs both a Markdown editor and anchored comment threads in one bundled surface.
- Threading reviewer commentary against specific selections inside a long-form document.
- Plain authoring with no review threads — markdown-editor is the lighter primitive.
- Reviewing diffs between two documents rather than annotating one — use diff-viewer instead.
Examples
Basic review editor
Editable markdown with source, rendered, and diff views.
Review editor with comments
Anchored text comments and document-level feedback.
Review editor with comments (readonly)
Same anchored thread as "Review editor with comments", but mode="readonly" — for cinder#1304's comment-navigation-chord finding: in readonly mode, ProseMirror sets contenteditable="false" on the editor DOM, which removes its native focusability, so a real Tab press lands on the outer .markdown-editor.surface host instead. See comment-anchor-a11y.playwright.ts.
Props
| Name | Type | Default | Description |
|---|---|---|---|
original bindable | text | '' | Original/baseline content for diff comparison. |
value bindable | text | '' | Current markdown content (two-way bindable). |
threads bindable | Thread[] | [] | Comment threads (two-way bindable).
Anchor positions use two different coordinate spaces, and neither is a raw
Markdown string index: anchor.from and anchor.to are ProseMirror
document positions, while anchor.lastKnownOffset and
anchor.originalPosition.offset are doc.textBetween() text offsets. All
four are expressed against the full document — when the content carries
YAML front matter, the component subtracts the front matter's character
length before handing anchors to the editor.
Positions are verified against the document. A newly seeded anchor whose
range does not match its quote is reported in dev and re-anchored by
quote. The intentional 0/0 sentinel created by
toRuntimeThreads(state.threads) is exempt from the warning and re-anchors
normally. Restore persisted state with that helper (or call setState)
rather than hand-computing positions. See shared/anchor-types.ts for the
field-by-field contract. |