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:
1 parent
4636649576
commit
a1597697e5
19 files changed
+3870
No files matched your search
@@ -0,0 +1,98 @@
|
||||
import { WidgetType, type EditorView } from '@codemirror/view'
|
||||
|
||||
export class BulletWidget extends WidgetType {
|
||||
eq() {
|
||||
return true
|
||||
}
|
||||
toDOM() {
|
||||
const el = document.createElement('span')
|
||||
el.className = 'cm-rm-bullet'
|
||||
el.textContent = '•'
|
||||
return el
|
||||
}
|
||||
}
|
||||
|
||||
export class CheckboxWidget extends WidgetType {
|
||||
constructor(readonly checked: boolean) {
|
||||
super()
|
||||
}
|
||||
eq(other: CheckboxWidget) {
|
||||
return other.checked === this.checked
|
||||
}
|
||||
toDOM(view: EditorView) {
|
||||
const el = document.createElement('input')
|
||||
el.type = 'checkbox'
|
||||
el.className = 'cm-rm-task'
|
||||
el.checked = this.checked
|
||||
// 用 mousedown 并阻止默认行为:否则编辑器先移动光标,行被展开为源码,点击落空
|
||||
el.addEventListener('mousedown', (e) => {
|
||||
e.preventDefault()
|
||||
const pos = view.posAtDOM(el)
|
||||
const marker = view.state.sliceDoc(pos, pos + 3)
|
||||
if (!/^\[[ xX]\]$/.test(marker)) return
|
||||
view.dispatch({ changes: { from: pos + 1, to: pos + 2, insert: this.checked ? ' ' : 'x' } })
|
||||
})
|
||||
return el
|
||||
}
|
||||
ignoreEvent() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
export class HorizontalRuleWidget extends WidgetType {
|
||||
eq() {
|
||||
return true
|
||||
}
|
||||
toDOM() {
|
||||
const el = document.createElement('span')
|
||||
el.className = 'cm-rm-hr'
|
||||
return el
|
||||
}
|
||||
}
|
||||
|
||||
export class CodeLangWidget extends WidgetType {
|
||||
constructor(readonly lang: string) {
|
||||
super()
|
||||
}
|
||||
eq(other: CodeLangWidget) {
|
||||
return other.lang === this.lang
|
||||
}
|
||||
toDOM() {
|
||||
const el = document.createElement('span')
|
||||
el.className = 'cm-rm-code-lang'
|
||||
el.textContent = this.lang
|
||||
return el
|
||||
}
|
||||
}
|
||||
|
||||
export class ImageWidget extends WidgetType {
|
||||
constructor(
|
||||
readonly src: string,
|
||||
readonly alt: string,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
eq(other: ImageWidget) {
|
||||
return other.src === this.src && other.alt === this.alt
|
||||
}
|
||||
toDOM(view: EditorView) {
|
||||
const wrap = document.createElement('span')
|
||||
wrap.className = 'cm-rm-image'
|
||||
const img = document.createElement('img')
|
||||
img.src = this.src
|
||||
img.alt = this.alt
|
||||
img.loading = 'lazy'
|
||||
img.decoding = 'async'
|
||||
// 图片加载完成后高度变化,需通知编辑器重新测量,否则光标定位会错位
|
||||
img.addEventListener('load', () => view.requestMeasure())
|
||||
img.addEventListener('error', () => {
|
||||
wrap.classList.add('cm-rm-image-error')
|
||||
wrap.textContent = this.alt || this.src
|
||||
})
|
||||
wrap.append(img)
|
||||
return wrap
|
||||
}
|
||||
get estimatedHeight() {
|
||||
return 200
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user