跪拜 Guibai
← Back to the summary

MSFE: A Source-First Markdown Editor Framework That Stops Format Drift

In the AI era, everyone is competing on note-taking apps: AI writing, AI notes, AI knowledge bases, each more dazzling than the last.

But if you open up the foundation, everyone is still using the same few pieces, and hitting the same old problems: content stored as JSON, rewritten when converting back and forth to Markdown; WYSIWYG and source mode as two separate editors, losing something each time you switch; and once collaboration is added, the structure starts misbehaving. No matter how fast AI generates, it still loses formatting and misplaces things when it lands in these frameworks.

I've been building ProseMirror-related products for three years, and I've stepped on most of these pits. This time I don't want to make another editor app; I want to get the editor framework right first. This article covers one line of that: MSFE (Markdown Source-First Editor), a source-first Markdown editor framework.

Here's the finished product, ready to use: https://msfe.quirekit.com

MSFE 官网首页

One: What Others Do

Let's look at what's under the hood of popular note-taking apps and editors from recent years. For open-source ones, I read the repository code; for closed-source, I only use official docs and engineering blogs; anything official didn't state is marked "undisclosed" (as of October 2026):

Product Editing method What is edited Editor foundation Where content lives Open source
Notion Block editing (rich text) Block data; Markdown only converted on import/export Proprietary Server-side database No
Obsidian Live preview: renders directly in source; also source mode, reading mode Markdown source CodeMirror 6 Local Markdown files No
Feishu Docs Block editing (rich text) Block tree; Markdown only on import/export Proprietary Cloud No
SiYuan Notes Block editing (rich text), can input Markdown syntax Node tree; Markdown only on import/export Proprietary Protyle Per-note .sy file, node tree JSON Yes
Typora WYSIWYG, also source mode Undisclosed Proprietary, kernel undisclosed Local Markdown files No
AFFiNE Block editing (rich text) + whiteboard Block data BlockSuite Yjs Yes
Logseq Outline block editing: current block shows source, others render Markdown source of current block Each block a textarea File version: Markdown/Org; DB version (Beta): SQLite Yes
AppFlowy Block editing (rich text) Block data Proprietary Flutter editor yrs (Rust Yjs), local RocksDB Yes
Memos Short notes, block editing Block structure, in/out as Markdown Proprietary Markdown text in database Yes
MarkText WYSIWYG + source mode, two sets WYSIWYG mode edits rendered doc, saves back via turndown Proprietary Muya + CodeMirror 5 Local Markdown files Yes
MiaoYan Source editing + syntax highlighting + interface preview Markdown source System native text box + WebView preview Local Markdown files Yes
NoteGen Rich text + source mode, two sets Rich text mode edits Tiptap document tree, source mode edits source Tiptap 3 + CodeMirror 6 Local Markdown files Yes
Blinko Short notes, Vditor editor (supports WYSIWYG, instant render, split preview) Depends on Vditor mode Vditor Markdown text in PostgreSQL Yes
Flowix Rich text + source mode, two sets Rich text mode edits Tiptap document tree, converts to Markdown on save; source mode edits source Tiptap 3 + CodeMirror 6 Local Markdown files, plus SQLite index Yes
Joplin (desktop) Rich text + source, two sets Rich text mode edits TinyMCE document, converts on switching TinyMCE + CodeMirror Markdown in SQLite Yes

The above only collects public information for comparing technical approaches, not judging product quality. Every product's trade-offs suit its use case.

By "what is edited," they roughly fall into three categories:

Many products that emphasize local, offline storage keep content in a database or JSON, using Markdown mainly for import/export.

Two: Main Problem: Conversion Only Works as Specified, Your Source Is Not Preserved

The rich text school needs to read and write Markdown, so they have to convert back and forth between ProseMirror nodes, marks, and Markdown source. The problem is that this conversion only works "as specified."

Look at the most commonly used implementations (all from reading source):

prosemirror-markdown 1.13.4

Tiptap's official @tiptap/markdown (3.30)

Community tiptap-markdown is no longer maintained. For nodes it can't express, it outputs a [nodeName] placeholder; tables with merged cells degrade to HTML, losing column alignment.

These aren't bugs; they're the inevitable result of that architecture: the JSON tree is the authoritative document, and Markdown is just one "print format" for it. With only one print format, the source you wrote is already gone after the first save.

For a single person writing notes, it might not matter. But it's critical in these scenarios:

Three: "Markdown Can't Do Complex Blocks" Is a Big Misconception

A common claim is that Markdown can't express complex structures like multi-dimensional tables and database views, so it must be stored as JSON. Logseq's co-founder also mentioned in 2024 why they made a database version, citing limited structured data support in Markdown, lacking persistent IDs, timestamps, and such.

Persistent IDs are indeed a real issue, discussed later. But "can't do complex blocks" is wrong.

Markdown already has a very useful container: code fences. In MSFE, multi-dimensional tables, flowcharts, mind maps, and whiteboards are stored as a code fence, with data inside that can be opened and edited repeatedly:

```quirekit-data-view
{ ...table fields, records, views... }
```

The complex-block problem isn't with Markdown; it's about designing a good landing spot for it.

Four: Where Markdown Really Limits

It's also dishonest to say Markdown can do everything. Its real limits are in the syntax itself, which I tested line by line with micromark 4.0.2 plus GFM extensions:

1. Table cells only accept inline content. Want a list or a code block inside a cell? The table breaks immediately:

| A | B |
| --- | --- |
| - 一
- 二 | x |

Parsing result: the first line becomes - 一 and an empty cell, - 二 | x | gets pushed outside the table, becoming a list item outside it. Putting a code block in a cell also messes up the whole table. To break lines inside a cell, you have to write <br>.

2. Formatting can't nest arbitrarily.

3. Chinese punctuation breaks bold, the pitfall that hurts Chinese users most.

这是**“加粗”**后面的字
注意**(重要)**这里

Neither line bolds; they render as literal asterisks. Reason: CommonMark decides whether ** can start or end emphasis by checking if either side is punctuation or whitespace. Chinese quotes and parentheses are punctuation, and they're immediately followed by a Han character, so the rule disallows it. English writing almost never hits this; Chinese writing hits it daily.

4. Some things simply don't exist in the syntax:

So my stance:

Five: Dual Editors Are Heavy and Disrespect the Source

The Markdown school wanting WYSIWYG often goes the most common route: a rich text editor plus a source editor, converting between them on switch.

Joplin desktop uses TinyMCE plus CodeMirror. Its official help page is honest, listing the problems you get editing Markdown notes in the rich text editor: most plugin formatting is lost; tables missing a header get a blank one added; checkboxes can't mix with regular list items in the same list; HTML in Markdown can be lost; reference-style links get converted to inline.

MarkText uses its own Muya plus CodeMirror 5, reading via marked to HTML and writing via turndown back to Markdown.

Dual editors mean:

Six: Collaboration: ProseMirror's Yjs Collaboration Has More Pits Than Expected

The rich text school for collaboration basically goes with Yjs plus y-prosemirror (Tiptap uses the same). We tested it carefully while building Word-level documents and went through upstream docs and issues:

Upstream acknowledged problems (y-prosemirror's CAVEATS and issues):

Our own tests (Yjs 13.6.33 + y-prosemirror 1.3.7, Tiptap's y-tiptap 3.0.9, Yjs 14 pre-release, all three give the same results):

Two people doing at the same time Result
A splits a paragraph into two, B inserts text into the original second half B's text moves to the end of the first half
A splits, B simultaneously bolds the second half The bold is lost
A splits, B simultaneously deletes text from the second half The deleted text comes back
A merges two paragraphs, B simultaneously inserts into the second paragraph B's insertion disappears

These aren't deployment issues; changing servers or adding more machines won't fix them. The root lies in "how to map a tree document to a CRDT."

Why MSFE's collaboration doesn't have these problems: MSFE collaborates on the Markdown source itself, a Yjs text. Any piece of text is valid Markdown, so there's no "invalid structure requiring deletion." Structure is reparsed from the text, so there's no "splitting/merging without representation." When two people edit the same spot, the result may need a human look, e.g., if they turn the same table row into different forms, but content won't vanish because of structural conflicts. Undo is collaborative undo, only reverting your own changes.

Seven: Why You Must Respect the Source

In the end, one sentence: the Markdown you wrote is the document itself.

Eight: CodeMirror Can Also Do "Rich Text"

Many people think CodeMirror can only make source editors and you need ProseMirror for WYSIWYG. Actually, CodeMirror 6's decorations and widgets are enough to make it render "looks like rich text": headings become larger, marker symbols get hidden, tables render as tables, formulas render as formulas, and data in code fences renders as flowcharts and multi-dimensional tables. But the underlying text hasn't changed a single character.

MSFE itself offers three editable presentations: document view (marker symbols all hidden, looks like rich text), live preview (where you're editing, Markdown markers show up), and source view. They are different presentations on the same CodeMirror instance, not three editors. Switching does no conversion; selection and undo history are retained, and it's always the same Markdown being edited.

QuireKit's MD Editor is the finished product assembled on top of MSFE: add toolbar, slash menu, and a read-only reading view to render the same source.

Document view: all markers hidden, looks rich.

MD Editor 文档视图

Live preview: where the cursor lives, that line shows Markdown source, e.g., here the formula line becomes LaTeX, other places render normally.

实时预览:光标所在的公式行露出 LaTeX 源码

Source view: the same content, this is that Markdown.

同一份内容的源码视图

Source-first also comes at a cost: every input must update the syntax tree, and large files get slow.

I've run into this problem before. I previously made an open-source project, Incremark, specifically to handle incremental rendering of Markdown from AI streaming output: each token the model emits doesn't reparse the whole document. When doing MSFE, I realized that the editor's incremental parsing and streaming rendering are the same problem, and it can be solved more thoroughly at the parser level. So I directly modified Lezer's incremental parsing and rewrote a new version of Incremark on top of it. Now MSFE editing, MD Editor's reading view, and AI streaming reply display all use the same parsing. The backstory of Incremark, and why I abandoned the first version, merits a separate article later.

The specific optimization is adding "reuse proof" to Lezer's incremental Markdown parsing: the parser records which ranges were reused as-is, and the editor uses that to skip the unchanged parts directly. For a 1 MiB document with 20 consecutive inputs:

Work Before optimization After optimization
Syntax tree visits 107,760 1,380 (98.7% less)
Syntax tree comparisons 153,940 660 (99.6% less)

Where the proof doesn't hold, it falls back to full comparison, so correctness is unaffected.

Nine: Why Package It as a Framework

Everything above could be done as a single editor app. I made it a framework because these problems face anyone building an editor; nobody should have to step on the same traps again.

MSFE's API is Tiptap-style: install extensions to edit what you want:

import { createMsfeEditor } from '@quirekit/msfe-core'
import { StarterKit } from '@quirekit/msfe-starter-kit'

const editor = createMsfeEditor({
  parent,
  documentId: 'note-1',
  markdown: '# Hello\n\n**Markdown** stays the source.\n',
  mode: 'live',
  extensions: [StarterKit],
})

editor.getMarkdown() // always that source

These are all MIT open source. AI reading/writing documents, presenting changes as "pending review," and annotations are commercial components, free to develop and evaluate.

Ten: MD Editor and the Surrounding Ecosystem

If you don't want to assemble it yourself, just use MD Editor: toolbar, four views, slash menu, tables, formulas, charts all set up, and you can put it into a Vue project in a few lines.

<script setup lang="ts">
import { MdEditor } from '@quirekit/md-editor/vue'
import '@quirekit/md-editor/styles.css'
</script>

<template>
  <MdEditor markdown="# Hello" label="Document" />
</template>

All surrounding editors are separate packages: formula editor (LaTeX to Word formula conversion), flowcharts, mind maps, whiteboards, Mermaid / PlantUML / Graphviz text drawing, multi-dimensional tables, and streaming Markdown rendering (Incremark) for AI incremental display. All UIs come from the same component library, with one-click light/dark switching.

Eleven: Professional Use: Word and PDF, Two Solutions

After writing Markdown, someone always needs Word, needs PDF. Here are two solutions.

Open-source solution (@quirekit/md-editor-basic-export, MIT): uses mature open-source libraries. Markdown to Word uses md-to-docx, Word to Markdown uses Mammoth plus Turndown, PDF uses pdfmake, all in-browser. MSFE-specific content degrades: flowcharts, mind maps, whiteboards export as preview images, other embedded blocks become placeholders. Every degradation is written into the export report, nothing silently lost.

Commercial solution: doesn't go the "Markdown to HTML to Word" path; instead converts Markdown into the same document model as our paginated document editor, Classic, and then Classic's typesetting engine outputs Word, PDF, and OFD:

Also: Classic

Classic is the paginated document editor mentioned above, another line of QuireKit: based on ProseMirror, with its own typesetting, truly paginated, drawn on Canvas or SVG. It solves a different class of problems, deserving its own article later. Its homepage shows it first, and you can try it directly: https://quirekit.com

Finally

QuireKit isn't a single editor, but a whole ecosystem: two Tiptap-style editor frameworks (the ProseMirror line and this CodeMirror line, MSFE), with paginated documents, block editors, Markdown editors, and a bunch of surrounding editors on top, sharing one UI component library. The open-source part is all MIT, and closed-source commercial use is allowed.

It's still alpha, the API will change. Welcome to try it and poke holes:

If you think this direction is interesting, a Star on GitHub is the biggest support. And I'd love to hear: which pit got you stuck the longest when building a Markdown editor?