feat(web): add M0 live preview editor prototype

- CodeMirror 6 editor with Obsidian/Typora-style live preview: block
  markup revealed per line, inline markup revealed per element
- Fenced code highlighting via Shiki (github-dark, JS regex engine) in a
  Web Worker, languages loaded on demand
- Lezer syntax tree to HTML renderer for share pages, checked against
  the CommonMark 0.31.2 spec (640/652; 12 known parser deviations)
- Use markdownLanguage directly to avoid bundling lang-html/css/js
This commit is contained in:
eezd committed 2026-10-16 17:23:58 +08:00
1 parent 4636649576
commit a1597697e5
19 files changed
+3870

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
import { defaultKeymap, history, historyKeymap, indentWithTab } from '@codemirror/commands'
import { markdownKeymap, markdownLanguage } from '@codemirror/lang-markdown'
import { EditorState, Prec, type Extension } from '@codemirror/state'
import { drawSelection, EditorView, keymap } from '@codemirror/view'
import { codeBlockHighlight } from './codeblocks'
import { livePreview, resolveImageUrl } from './livepreview'
import { markdownTheme } from './theme'
export interface EditorOptions {
parent: HTMLElement
doc: string
resolveImage?: (src: string) => string
onChange?: (state: EditorState) => void
extensions?: Extension[]
}
export function createEditor(opts: EditorOptions): EditorView {
const extensions: Extension[] = [
history(),
drawSelection(),
EditorView.lineWrapping,
// 不用 lang-markdown 的 markdown():它会静态引入 lang-html/css/javascript 用于嵌套 HTML 高亮,
// 首屏包 gzip 从约 108 KB 增至约 180 KB,而 Live Preview 不需要 HTML 高亮
markdownLanguage,
Prec.high(keymap.of(markdownKeymap)),
markdownTheme,
livePreview,
codeBlockHighlight,
keymap.of([indentWithTab, ...defaultKeymap, ...historyKeymap]),
EditorView.contentAttributes.of({ spellcheck: 'false', autocapitalize: 'sentences' }),
]
if (opts.resolveImage) extensions.push(resolveImageUrl.of(opts.resolveImage))
if (opts.onChange) {
const onChange = opts.onChange
extensions.push(EditorView.updateListener.of((u) => u.docChanged && onChange(u.state)))
}
if (opts.extensions) extensions.push(...opts.extensions)
return new EditorView({ parent: opts.parent, state: EditorState.create({ doc: opts.doc, extensions }) })
}