Question
Was ProseMirror, through Milkdown Kit, the right document model for Leafdown's hybrid editor, and what alternatives was it chosen over?
Use temporary source projection establishes why projection is the correct approach within a ProseMirror document model, and records what it beat there: decorations and widgets cannot make synthetic markers natively editable without recreating selection, deletion, clipboard, IME, and keyboard behavior, and a permanent Markdown-token schema would conflict with Milkdown's CommonMark/GFM model. Those alternatives are settled and are not reopened here.
The open question sits one level below: projection exists because the document model renders semantic content and does not natively carry Markdown syntax as editable text. A source-first model inverts that — source is the document, and regions are decorated to render richly. Use Milkdown Kit records no alternatives, so whether that inversion was considered, and why it was not chosen, is unrecorded.
Context
The editor is the largest subsystem in the project: 10,903 source lines and 8,230 test lines across 15 plugins and 12 command modules, of which source projection is roughly 4.0k lines across 8 source files, two of them over a thousand lines each.
Use Milkdown Kit carries a rationale but no rejected alternatives, which means the most consequential technical decision in the project cannot be revisited on its merits — any future doubt restarts the evaluation from nothing. The projection decision demonstrates the standard worth meeting: it names what it rejected and why, and remains useful precisely because it does.
Confirming Milkdown is an expected outcome, and a confirmation that records what it beat is the deliverable either way. Cost is the reason to answer now rather than later: the editor grows, and every month makes both the evaluation and any change that follows more expensive.
Related context
- Related issues or pull requests: #44, #46, #63, #64 — the origin and generalization of source projection.
- Documentation, decisions, or prior investigation:
docs/decisions.md, "Editor Decisions" — Use Milkdown Kit, Accept Milkdown GFM preset behavior, Do not use Crepe, Use temporary source projection. Per docs/README.md, Decisions has authority over Architecture and Specification.
docs/decisions.md, Gate on defect classes automation can reach — already characterizes this project's substantive defects as boundary-semantics failures. Use that characterization rather than re-deriving it.
docs/architecture.md, "Editor Architecture" — Milkdown responsibilities, Leafdown responsibilities, clipboard ownership, source projection.
docs/specification.md — editor behavior, clipboard semantics, and save output rules that any substrate must preserve.
Validate
-
Establish whether the difficulty is model-rooted. Group the projection and editor clipboard fixes in git log by root cause rather than symptom, and determine whether each traces to the document model rendering semantic content rather than source, or to complexity inherent in the hybrid-editing goal that any substrate would carry. Treat the reasoning already recorded in Use temporary source projection as established. If the causes are inherent rather than model-rooted, the question is answered: record that conclusion and stop before step 2.
-
Evaluate substrates against the product goal in docs/specification.md — one hybrid surface, no source/preview split, no separate modes, local raw-source editing for inline and source-oriented constructs, Markdown semantics preserved on save:
- Staying on Milkdown. The baseline every alternative must beat, evaluated as seriously as the rest.
- CodeMirror 6, as the source-first inversion. Assess how far Obsidian's Live Preview is relevant prior art for reaching this product goal on that model.
- Lexical.
- ProseMirror-based alternatives such as TipTap. Reason explicitly about whether changing the abstraction over an unchanged document model could address a model-rooted cause, rather than listing them as if they were a different substrate.
For each: how naturally it expresses local raw-source editing, Markdown round-trip fidelity and what owns it, GFM and footnote support, Shiki integration, table editing, clipboard control at the fidelity documented in Clipboard Ownership, extension model maturity, maintenance health, and license compatibility with GPL-3.0-or-later.
-
Quantify migration cost for any recommended change: what carries over, what is rewritten, and which behavior specified in docs/specification.md is at risk. A migration that silently drops specified behavior is not a migration.
Exit criteria
Outcome
Conclusion: ProseMirror, through Milkdown Kit, was and remains the right document model. The confirmation is recorded with what it was chosen over, so the decision can be revisited on its merits rather than re-derived from nothing.
The decisive finding is not that Milkdown wins on features. It is that the substrate choice is a choice about which half of the hybrid goal the substrate carries for free, and Leafdown's specification assigns the larger half to the rendered side. A source-first inversion does not remove the cost recorded in Use temporary source projection; it moves that cost onto the block and visual objects that ProseMirror carries today, and adds three capability gaps that the current stack already fills.
Method, boundary, and stop conditions
Evidence plan, in the order run, each step able to end the spike:
- Revalidate the question and every version and API assumption against the manifests, the installed packages, the owning documentation, and the upstream registries and repositories.
- Classify the projection and clipboard fix history by root cause, then test each class against the falsifiable question: would this defect class exist if the document were source?
- Measure the inverse cost through observable prior art rather than assertion, because this repository cannot exhibit it.
- Check each candidate substrate against the specification's hard requirements, stopping at the first requirement a candidate cannot meet.
Effort boundary: read-only inspection and measurement, no prototype, no branch. The boundary held. A prototype was considered and not built: the facts that discriminate between the options are structural and visible without one — a missing footnote parser, an absent Shiki integration path, a clipboard payload that must be generated rather than derived, and a table editor whose cost is already measurable in a shipping open-source implementation. A prototype would have re-demonstrated them at a much higher price.
Stop conditions: stop at step 2 if the causes proved inherent to the hybrid goal rather than model-rooted (not triggered — see below); stop before step 3's migration costing if no change is recommended (triggered); stop before any implementation, including the documentation update this spike requires.
Numbers below were re-measured at 5ec6a55e and supersede the estimates in Context: the editor is 11,009 source lines and 9,379 test lines across 15 plugins and 12 command modules, of which source projection is 4,778 source lines (43%) and 3,269 test lines across 10 source files.
1. The difficulty is model-rooted — and that alone does not favor inverting the model
Of the 75 fix: commits touching src/features/editor, 38 touch a source-projection module and 5 touch a clipboard module. Grouped by root cause rather than symptom, every one of the 38 falls into one of three classes, and all three trace to the document model rendering semantic content rather than source:
- Entry and exit are document mutations (19):
b3e16369 suppressing the dirty flag a projection's own mutation raised, ed489096 and de176373 keeping housekeeping out of native history, c0cafefd preserving selections across the boundary, plus 0e378df3, e6c3b9a0, 2d382981, 986b5b35, 7cfeeb74, ec0dab36, b919aa11, f7ad59de, 1540fd11, 6d98d32a, fc74e55e, d428b319, 8e27f570, c69e558f, 7fc6d06c.
- ProseMirror's flat mark set against Markdown's nested syntax (9):
28b31f78 scoping projection to an exact mark combination, 8d6da5b2 reassembling content ProseMirror had split, plus d6b64865, 8d7cb9ad, 0ac9d71e, 66d2bbeb, aefcfed2, d2f4d40e, c10847d8.
- Regenerating source from semantic content (10):
0b1c39a1 and 6e562bd0, whose placeholder-token workaround in logicalLinkMarkdown.ts exists only because the serializer fragments a link with mixed inner marks, plus f53304f3, b560df66, 1c2d9399, 00c39954, ccc01900, aa77e730, c3497ae4, be98f10e.
Applying the falsifiable test: in a source-first model none of these classes exists. There is no entry or exit to bridge, the syntax tree is nested so the node under the caret is unambiguous, and there is no serializer because the source is never regenerated. So the answer to step 1 is that the causes are model-rooted, and the spike does not stop here.
It does not follow that inverting the model reduces the difficulty, and the same history shows why. The rendered structural half of the specification produced 3 fixes in the same period — 0c95dfc1 protecting the GFM header row, a4fc01bc choosing an insertion block, 6a4156b0 preserving image edit focus — and all three are Leafdown policy choices, not work to make tables, lists, blockquotes, code blocks, or images render and edit at all. Eleven table commands, list continuation and indentation, cell keyboard navigation, and structural rendering arrive from prosemirror-tables, prosemirror-schema-list, and the Milkdown GFM schema.
The fix population therefore concentrates exactly where the substrate does not natively carry the requirement. ProseMirror carries the rendered half and not the source half; CodeMirror 6 carries the source half and not the rendered half. Inversion is a lateral move, and the specification decides its direction: seven inline objects take source projection, while headings, lists, blockquotes, horizontal rules, code blocks, tables, footnote definitions, task lists, and images must render structurally, with Treat marker presentation as object-specific and the Blocks rules explicitly forbidding raw delimiter exposure for the visual objects.
Measured, not asserted, using Zettlr — a GPL-3.0, actively maintained, CodeMirror 6 hybrid Markdown editor — as the proxy for what the rendered half costs on a source model:
| Half of the goal |
Leafdown on ProseMirror |
Zettlr on CodeMirror 6 |
| Source shown and edited locally |
~136 KB (10 projection modules) |
~0, native to the model |
| Objects rendered and edited structurally |
~42 KB (table commands and keyboard, image view, marker and link presentation) |
~161 KB (table-editor/ ~105 KB, comparable subset of renderers/ ~57 KB) |
The proxy is an order-of-magnitude comparison, not like-for-like: Zettlr's renderers cover elements Leafdown does not support, and its table editor handles Pandoc tables. Even discounted, it shows the inversion trading a cost for a cost of the same order, with a full rewrite in between.
2. Options evaluated
Staying on Milkdown Kit — selected. @milkdown/kit 7.21.3 is pinned; upstream is 7.22.0, released 2026-08-03, MIT, on a roughly monthly cadence. It supplies the CommonMark and GFM schema including footnoteDefinitionSchema and footnoteReferenceSchema, the remark parser and serializer that own Markdown round-trip, view.serializeForClipboard producing both clipboard representations from one call, and @milkdown/plugin-highlight/shiki, which is how the bundled Shiki assets reach code blocks today. The accepted cost is the projection subsystem and its three defect classes, which are now recorded as a known, bounded, and tested ceiling rather than an open question.
A finding that materially lowers the risk of this choice: 109 of the 150 @milkdown/* imports in non-test src/ code (73%) are @milkdown/kit/prose/*, which re-exports upstream prosemirror-* packages. The Milkdown-specific surface is the Ctx and plugin-registration layer, the transformer, the presets, and the highlight plugin. The real substrate commitment is to ProseMirror; Milkdown is a replaceable wrapper over it, and its loss would be a de-wrapping rather than a substrate migration.
CodeMirror 6 as the source-first inversion — rejected. MIT, healthy (@codemirror/view 6.43.8 on 2026-08-04), and the model genuinely eliminates all three defect classes above. It fails on the rendered half and on three capability gaps that are filled today:
- Footnotes are not in the parser.
@lezer/markdown 1.7.2 ships Table, TaskList, Strikethrough, Autolink, plus Subscript, Superscript, and Emoji. Footnotes are absent, so the specification's footnote references and definitions would require a Lezer parser extension — a parser change, not a plugin.
- No Shiki path.
@shikijs/codemirror does not exist; the only candidate, codemirror-shiki, is at 0.3.0 and was last published 2025-07-19. Bundle Shiki themes and grammars is an architectural security rule, so this is not cosmetic.
- The clipboard contract inverts. Clipboard Ownership requires
text/plain Markdown and a semantically equivalent text/html fragment. On ProseMirror both come from one serializeForClipboard call; on CodeMirror 6 the plain text is free but the HTML fragment must be rendered from source, and HTML paste ingress must be converted back to Markdown — a second parser pipeline. Zettlr carries exactly this shape, running @lezer/markdown in the editor alongside remark and rehype-remark outside it.
On the prior art: Obsidian's Live Preview is relevant but limited. Obsidian's own documentation states that "when your cursor enters formatted content, the underlying syntax becomes visible for editing", which is the reveal-on-cursor convention Leafdown wants for its 8 inline objects — and, for tables, is precisely what Leafdown's specification forbids, since entering a table exposes its pipe-delimited source. Obsidian is closed source, so it evidences feasibility and supplies no implementation. Zettlr shows the compliant alternative exists and costs ~105 KB of bespoke table editor.
Lexical — rejected. MIT and the most actively developed candidate, but it is a semantic document model, so it reproduces the projection problem class rather than removing it, and it is weaker than the incumbent on the axis that matters: @lexical/markdown is a transformer list rather than a CommonMark parser, with no footnote support, so Markdown round-trip fidelity would become Leafdown-owned. It remains pre-1.0 at 0.49.0. Taking the same problem with a less capable Markdown layer is strictly worse than the baseline.
TipTap and other ProseMirror abstractions — rejected on reasoning, not on features. TipTap 3.29.2 is MIT and popular, but its document "is stored internally as a ProseMirror node" and its recommended format is JSON. Changing the abstraction cannot address a model-rooted cause, because the model is unchanged: the flat mark set, the semantic schema, and the serializer round trip all survive the swap, so all three defect classes survive with them. It also moves Markdown ownership from an upstream Markdown-native stack into Leafdown, which trades no benefit for real risk. Any ProseMirror-based alternative fails this test the same way, and the honest framing is that this row is not a substrate option at all.
Cross-cutting on maintenance health, since it discriminates less than expected: Milkdown is effectively single-maintainer (20 of 40 recent human commits from one author). So are ProseMirror and CodeMirror, both by the same author, who has moved their canonical repositories to code.haverbeke.berlin and archived the GitHub mirrors while continuing to publish (prosemirror-view 1.42.2, 2026-07-24; @codemirror/view 6.43.8, 2026-08-04). The two multi-maintainer options, Lexical and TipTap, are the two that are weakest on Markdown fidelity. Bus factor cannot be optimized away by this decision, and all candidates are MIT, compatible with GPL-3.0-or-later.
3. Migration cost
Not triggered: no change is recommended. The order-of-magnitude figures in the table above are what made a detailed costing unnecessary — a CodeMirror 6 migration rewrites the whole editor subsystem, retains only the specification and the Rust boundary, and puts every behavior in Editor Model at risk, most sharply the table rules, the marker-visibility rules for visual objects, and the Clipboard Ownership text/html contract.
Residual uncertainty
- The inverse cost is measured through a proxy, not a prototype. Zettlr's scope differs from Leafdown's, so the ~161 KB figure bounds the order of magnitude, not the exact number.
- The three CodeMirror 6 gaps are gaps in available integrations, not proofs of difficulty. Each is buildable; none is free, and together they land on the same side of the ledger.
- The placeholder-token serializer workaround in
logicalLinkMarkdown.ts remains the least comfortable consequence of this decision. It is tested and bounded, and it stays a known ceiling rather than an open action.
Roadmap context added after the spike concluded
The product direction is to match and surpass AFFiNE while staying local-first, including an endless drawing surface with frames interleaved between prose blocks. This strengthens the conclusion rather than reopening it: interleaved frames are atomic non-text blocks carrying their own editing surface, which is a rich block model, and AFFiNE reaches it through BlockSuite's Yjs block tree rather than through a source model. A frame block is a schema node plus a NodeView on ProseMirror, the pattern already shipped for images; on a source model it is a widget anchored to a text offset that shifts with every edit above it. Do not reopen the substrate question without this context. The document format question this raises — what a Leafdown document is once it can contain a drawing — is separate, unshaped, and does not bear on the substrate.
Follow-up work selected
One item, required by this issue's exit criteria: update Use Milkdown Kit in docs/decisions.md so it records the alternatives and reasons, in Decision / Rationale / Consequences form. Delivered in pull request #191. No other follow-up is selected. No migration is accepted, so no staged path is defined.
Documentation updates required
docs/decisions.md, Use Milkdown Kit, only. Durable direction is unchanged, so docs/architecture.md and docs/specification.md need no edit; Use temporary source projection already records the within-model reasoning and is not reopened.
Question
Was ProseMirror, through Milkdown Kit, the right document model for Leafdown's hybrid editor, and what alternatives was it chosen over?
Use temporary source projectionestablishes why projection is the correct approach within a ProseMirror document model, and records what it beat there: decorations and widgets cannot make synthetic markers natively editable without recreating selection, deletion, clipboard, IME, and keyboard behavior, and a permanent Markdown-token schema would conflict with Milkdown's CommonMark/GFM model. Those alternatives are settled and are not reopened here.The open question sits one level below: projection exists because the document model renders semantic content and does not natively carry Markdown syntax as editable text. A source-first model inverts that — source is the document, and regions are decorated to render richly.
Use Milkdown Kitrecords no alternatives, so whether that inversion was considered, and why it was not chosen, is unrecorded.Context
The editor is the largest subsystem in the project: 10,903 source lines and 8,230 test lines across 15 plugins and 12 command modules, of which source projection is roughly 4.0k lines across 8 source files, two of them over a thousand lines each.
Use Milkdown Kitcarries a rationale but no rejected alternatives, which means the most consequential technical decision in the project cannot be revisited on its merits — any future doubt restarts the evaluation from nothing. The projection decision demonstrates the standard worth meeting: it names what it rejected and why, and remains useful precisely because it does.Confirming Milkdown is an expected outcome, and a confirmation that records what it beat is the deliverable either way. Cost is the reason to answer now rather than later: the editor grows, and every month makes both the evaluation and any change that follows more expensive.
Related context
docs/decisions.md, "Editor Decisions" —Use Milkdown Kit,Accept Milkdown GFM preset behavior,Do not use Crepe,Use temporary source projection. Perdocs/README.md, Decisions has authority over Architecture and Specification.docs/decisions.md,Gate on defect classes automation can reach— already characterizes this project's substantive defects as boundary-semantics failures. Use that characterization rather than re-deriving it.docs/architecture.md, "Editor Architecture" — Milkdown responsibilities, Leafdown responsibilities, clipboard ownership, source projection.docs/specification.md— editor behavior, clipboard semantics, and save output rules that any substrate must preserve.Validate
Establish whether the difficulty is model-rooted. Group the projection and editor clipboard fixes in
git logby root cause rather than symptom, and determine whether each traces to the document model rendering semantic content rather than source, or to complexity inherent in the hybrid-editing goal that any substrate would carry. Treat the reasoning already recorded inUse temporary source projectionas established. If the causes are inherent rather than model-rooted, the question is answered: record that conclusion and stop before step 2.Evaluate substrates against the product goal in
docs/specification.md— one hybrid surface, no source/preview split, no separate modes, local raw-source editing for inline and source-oriented constructs, Markdown semantics preserved on save:For each: how naturally it expresses local raw-source editing, Markdown round-trip fidelity and what owns it, GFM and footnote support, Shiki integration, table editing, clipboard control at the fidelity documented in Clipboard Ownership, extension model maturity, maintenance health, and license compatibility with GPL-3.0-or-later.
Quantify migration cost for any recommended change: what carries over, what is rewritten, and which behavior specified in
docs/specification.mdis at risk. A migration that silently drops specified behavior is not a migration.Exit criteria
Use Milkdown Kitindocs/decisions.mdgains the alternatives it was chosen over and the reasons, in the repository's Decision, Rationale, Consequences format — regardless of which way the spike lands.Outcome
Conclusion: ProseMirror, through Milkdown Kit, was and remains the right document model. The confirmation is recorded with what it was chosen over, so the decision can be revisited on its merits rather than re-derived from nothing.
The decisive finding is not that Milkdown wins on features. It is that the substrate choice is a choice about which half of the hybrid goal the substrate carries for free, and Leafdown's specification assigns the larger half to the rendered side. A source-first inversion does not remove the cost recorded in
Use temporary source projection; it moves that cost onto the block and visual objects that ProseMirror carries today, and adds three capability gaps that the current stack already fills.Method, boundary, and stop conditions
Evidence plan, in the order run, each step able to end the spike:
Effort boundary: read-only inspection and measurement, no prototype, no branch. The boundary held. A prototype was considered and not built: the facts that discriminate between the options are structural and visible without one — a missing footnote parser, an absent Shiki integration path, a clipboard payload that must be generated rather than derived, and a table editor whose cost is already measurable in a shipping open-source implementation. A prototype would have re-demonstrated them at a much higher price.
Stop conditions: stop at step 2 if the causes proved inherent to the hybrid goal rather than model-rooted (not triggered — see below); stop before step 3's migration costing if no change is recommended (triggered); stop before any implementation, including the documentation update this spike requires.
Numbers below were re-measured at
5ec6a55eand supersede the estimates in Context: the editor is 11,009 source lines and 9,379 test lines across 15 plugins and 12 command modules, of which source projection is 4,778 source lines (43%) and 3,269 test lines across 10 source files.1. The difficulty is model-rooted — and that alone does not favor inverting the model
Of the 75
fix:commits touchingsrc/features/editor, 38 touch a source-projection module and 5 touch a clipboard module. Grouped by root cause rather than symptom, every one of the 38 falls into one of three classes, and all three trace to the document model rendering semantic content rather than source:b3e16369suppressing the dirty flag a projection's own mutation raised,ed489096andde176373keeping housekeeping out of native history,c0cafefdpreserving selections across the boundary, plus0e378df3,e6c3b9a0,2d382981,986b5b35,7cfeeb74,ec0dab36,b919aa11,f7ad59de,1540fd11,6d98d32a,fc74e55e,d428b319,8e27f570,c69e558f,7fc6d06c.28b31f78scoping projection to an exact mark combination,8d6da5b2reassembling content ProseMirror had split, plusd6b64865,8d7cb9ad,0ac9d71e,66d2bbeb,aefcfed2,d2f4d40e,c10847d8.0b1c39a1and6e562bd0, whose placeholder-token workaround inlogicalLinkMarkdown.tsexists only because the serializer fragments a link with mixed inner marks, plusf53304f3,b560df66,1c2d9399,00c39954,ccc01900,aa77e730,c3497ae4,be98f10e.Applying the falsifiable test: in a source-first model none of these classes exists. There is no entry or exit to bridge, the syntax tree is nested so the node under the caret is unambiguous, and there is no serializer because the source is never regenerated. So the answer to step 1 is that the causes are model-rooted, and the spike does not stop here.
It does not follow that inverting the model reduces the difficulty, and the same history shows why. The rendered structural half of the specification produced 3 fixes in the same period —
0c95dfc1protecting the GFM header row,a4fc01bcchoosing an insertion block,6a4156b0preserving image edit focus — and all three are Leafdown policy choices, not work to make tables, lists, blockquotes, code blocks, or images render and edit at all. Eleven table commands, list continuation and indentation, cell keyboard navigation, and structural rendering arrive fromprosemirror-tables,prosemirror-schema-list, and the Milkdown GFM schema.The fix population therefore concentrates exactly where the substrate does not natively carry the requirement. ProseMirror carries the rendered half and not the source half; CodeMirror 6 carries the source half and not the rendered half. Inversion is a lateral move, and the specification decides its direction: seven inline objects take source projection, while headings, lists, blockquotes, horizontal rules, code blocks, tables, footnote definitions, task lists, and images must render structurally, with
Treat marker presentation as object-specificand the Blocks rules explicitly forbidding raw delimiter exposure for the visual objects.Measured, not asserted, using Zettlr — a GPL-3.0, actively maintained, CodeMirror 6 hybrid Markdown editor — as the proxy for what the rendered half costs on a source model:
table-editor/~105 KB, comparable subset ofrenderers/~57 KB)The proxy is an order-of-magnitude comparison, not like-for-like: Zettlr's renderers cover elements Leafdown does not support, and its table editor handles Pandoc tables. Even discounted, it shows the inversion trading a cost for a cost of the same order, with a full rewrite in between.
2. Options evaluated
Staying on Milkdown Kit — selected.
@milkdown/kit7.21.3 is pinned; upstream is 7.22.0, released 2026-08-03, MIT, on a roughly monthly cadence. It supplies the CommonMark and GFM schema includingfootnoteDefinitionSchemaandfootnoteReferenceSchema, the remark parser and serializer that own Markdown round-trip,view.serializeForClipboardproducing both clipboard representations from one call, and@milkdown/plugin-highlight/shiki, which is how the bundled Shiki assets reach code blocks today. The accepted cost is the projection subsystem and its three defect classes, which are now recorded as a known, bounded, and tested ceiling rather than an open question.A finding that materially lowers the risk of this choice: 109 of the 150
@milkdown/*imports in non-testsrc/code (73%) are@milkdown/kit/prose/*, which re-exports upstreamprosemirror-*packages. The Milkdown-specific surface is the Ctx and plugin-registration layer, the transformer, the presets, and the highlight plugin. The real substrate commitment is to ProseMirror; Milkdown is a replaceable wrapper over it, and its loss would be a de-wrapping rather than a substrate migration.CodeMirror 6 as the source-first inversion — rejected. MIT, healthy (
@codemirror/view6.43.8 on 2026-08-04), and the model genuinely eliminates all three defect classes above. It fails on the rendered half and on three capability gaps that are filled today:@lezer/markdown1.7.2 shipsTable,TaskList,Strikethrough,Autolink, plus Subscript, Superscript, and Emoji. Footnotes are absent, so the specification's footnote references and definitions would require a Lezer parser extension — a parser change, not a plugin.@shikijs/codemirrordoes not exist; the only candidate,codemirror-shiki, is at 0.3.0 and was last published 2025-07-19.Bundle Shiki themes and grammarsis an architectural security rule, so this is not cosmetic.text/plainMarkdown and a semantically equivalenttext/htmlfragment. On ProseMirror both come from oneserializeForClipboardcall; on CodeMirror 6 the plain text is free but the HTML fragment must be rendered from source, and HTML paste ingress must be converted back to Markdown — a second parser pipeline. Zettlr carries exactly this shape, running@lezer/markdownin the editor alongsideremarkandrehype-remarkoutside it.On the prior art: Obsidian's Live Preview is relevant but limited. Obsidian's own documentation states that "when your cursor enters formatted content, the underlying syntax becomes visible for editing", which is the reveal-on-cursor convention Leafdown wants for its 8 inline objects — and, for tables, is precisely what Leafdown's specification forbids, since entering a table exposes its pipe-delimited source. Obsidian is closed source, so it evidences feasibility and supplies no implementation. Zettlr shows the compliant alternative exists and costs ~105 KB of bespoke table editor.
Lexical — rejected. MIT and the most actively developed candidate, but it is a semantic document model, so it reproduces the projection problem class rather than removing it, and it is weaker than the incumbent on the axis that matters:
@lexical/markdownis a transformer list rather than a CommonMark parser, with no footnote support, so Markdown round-trip fidelity would become Leafdown-owned. It remains pre-1.0 at 0.49.0. Taking the same problem with a less capable Markdown layer is strictly worse than the baseline.TipTap and other ProseMirror abstractions — rejected on reasoning, not on features. TipTap 3.29.2 is MIT and popular, but its document "is stored internally as a ProseMirror node" and its recommended format is JSON. Changing the abstraction cannot address a model-rooted cause, because the model is unchanged: the flat mark set, the semantic schema, and the serializer round trip all survive the swap, so all three defect classes survive with them. It also moves Markdown ownership from an upstream Markdown-native stack into Leafdown, which trades no benefit for real risk. Any ProseMirror-based alternative fails this test the same way, and the honest framing is that this row is not a substrate option at all.
Cross-cutting on maintenance health, since it discriminates less than expected: Milkdown is effectively single-maintainer (20 of 40 recent human commits from one author). So are ProseMirror and CodeMirror, both by the same author, who has moved their canonical repositories to
code.haverbeke.berlinand archived the GitHub mirrors while continuing to publish (prosemirror-view1.42.2, 2026-07-24;@codemirror/view6.43.8, 2026-08-04). The two multi-maintainer options, Lexical and TipTap, are the two that are weakest on Markdown fidelity. Bus factor cannot be optimized away by this decision, and all candidates are MIT, compatible with GPL-3.0-or-later.3. Migration cost
Not triggered: no change is recommended. The order-of-magnitude figures in the table above are what made a detailed costing unnecessary — a CodeMirror 6 migration rewrites the whole editor subsystem, retains only the specification and the Rust boundary, and puts every behavior in Editor Model at risk, most sharply the table rules, the marker-visibility rules for visual objects, and the Clipboard Ownership
text/htmlcontract.Residual uncertainty
logicalLinkMarkdown.tsremains the least comfortable consequence of this decision. It is tested and bounded, and it stays a known ceiling rather than an open action.Roadmap context added after the spike concluded
The product direction is to match and surpass AFFiNE while staying local-first, including an endless drawing surface with frames interleaved between prose blocks. This strengthens the conclusion rather than reopening it: interleaved frames are atomic non-text blocks carrying their own editing surface, which is a rich block model, and AFFiNE reaches it through BlockSuite's Yjs block tree rather than through a source model. A frame block is a schema node plus a NodeView on ProseMirror, the pattern already shipped for images; on a source model it is a widget anchored to a text offset that shifts with every edit above it. Do not reopen the substrate question without this context. The document format question this raises — what a Leafdown document is once it can contain a drawing — is separate, unshaped, and does not bear on the substrate.
Follow-up work selected
One item, required by this issue's exit criteria: update
Use Milkdown Kitindocs/decisions.mdso it records the alternatives and reasons, in Decision / Rationale / Consequences form. Delivered in pull request #191. No other follow-up is selected. No migration is accepted, so no staged path is defined.Documentation updates required
docs/decisions.md,Use Milkdown Kit, only. Durable direction is unchanged, sodocs/architecture.mdanddocs/specification.mdneed no edit;Use temporary source projectionalready records the within-model reasoning and is not reopened.