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

+46
View File
@@ -0,0 +1,46 @@
import { createHighlighterCore, type HighlighterCore } from 'shiki/core'
import { createJavaScriptRegexEngine } from 'shiki/engine/javascript'
import { bundledLanguages } from 'shiki/langs'
import githubDark from 'shiki/themes/github-dark.mjs'
import type { HighlightRequest, HighlightResponse, TokenRun } from './protocol'
const THEME = 'github-dark'
const highlighterReady: Promise<HighlighterCore> = createHighlighterCore({
themes: [githubDark],
langs: [],
engine: createJavaScriptRegexEngine({ forgiving: true }),
})
// 同一语言的并发请求共用一次加载
const langLoads = new Map<string, Promise<boolean>>()
function ensureLanguage(hl: HighlighterCore, lang: string): Promise<boolean> {
let load = langLoads.get(lang)
if (!load) {
const importer = bundledLanguages[lang as keyof typeof bundledLanguages]
load = importer
? importer().then((mod) => hl.loadLanguage(mod.default)).then(() => true)
: Promise.resolve(false)
langLoads.set(lang, load)
}
return load
}
async function highlight(req: HighlightRequest): Promise<TokenRun | null> {
const hl = await highlighterReady
if (!(await ensureLanguage(hl, req.lang))) return null
const run: TokenRun = []
for (const line of hl.codeToTokensBase(req.code, { lang: req.lang, theme: THEME })) {
for (const t of line) {
if (!t.content.trim()) continue
run.push(t.offset, t.content.length, t.color ?? '', t.fontStyle ?? 0)
}
}
return run
}
self.addEventListener('message', async (e: MessageEvent<HighlightRequest>) => {
const res: HighlightResponse = { id: e.data.id, tokens: await highlight(e.data) }
;(self as unknown as Worker).postMessage(res)
})