1# Composer coordinates and serialization
2
3The draft store owns Markdown, while Tiptap owns the editing document. The rich-text
4setting changes the installed extensions, so toggling it remounts the editor from the
5stored draft. Plain mode must leave formatting markers literal. Rich mode preserves
6whitespace and chip sources, but canonicalizes supported delimiters (`__` to `**`,
7`_` to `*`) and checkbox case (`[X]` to `[x]`).
8
9Store cursors are not ProseMirror positions: collapsed coordinates count a chip as one
10character, expanded coordinates count its source text, and ProseMirror also counts
11block boundaries. Keep conversion in [the document model](../../apps/web/src/composer-rich-text-doc.ts).
12Empty paragraphs need real caret positions even though they contain no text. Markers
13shown beside styled text are decorations, so offsets inside them clamp to the text edge.
14
15Only replace editor content when the controlled text changes. Replacing it for a cursor
16move creates undo entries and regenerates citation identities. Pending citation popovers
17must wait until the requested draft has reached the editor before locating their chip.
18
19Rich tasks split through native editor commands so marks and chips survive. Literal
20lists use [store edits](../../apps/web/src/composer-list-continuation.ts). Newlines become
21paragraph splits: trailing hard breaks otherwise appear to require two presses.
22Programmatic moves must explicitly scroll the caret into view.
23
24Clipboard text must come from the Markdown serializer, not DOM text: chip labels omit
25the source and marker decorations are not content. Structured context records accompany
26that text when available. Paste completes trailing chip delimiters and adds a leading
27boundary when inserting a chip directly after text.
28
29Block nodes (fences, lists, quotes, rules, headings) keep their exact Markdown
30delimiters as node attributes, so the round trip is byte-identical and offsets
31inside a delimiter clamp to the block's text like task and style markers. The
32serializers write only what the grammar has: a quote holds paragraphs, a list
33holds paragraphs and lists. So blocks may only form at a top-level paragraph.
34Input rules check this, and Tiptap's own block chords (Mod-Shift-8, Mod-Alt-c,
35and so on) are removed, because nesting a block the serializer cannot write
36drops its text from the stored draft. The list grammar is the plain-mode
37continuation's in [composer-list-continuation](../../apps/web/src/composer-list-continuation.ts);
38keep the two in step.
39
40Enter sends, so the fence handling in `handleKeyDown` must run before the send
41handler: checked afterwards, a fence never opens and a newline inside one sends
42the draft.