64 lines
3.1 KiB
TypeScript
64 lines
3.1 KiB
TypeScript
import type { EditorState, Range } from '@codemirror/state'
|
|
import type { DecorationSet } from '@codemirror/view'
|
|
import type { SyntaxNode } from '@lezer/common'
|
|
import { syntaxTree } from '@codemirror/language'
|
|
import { StateField } from '@codemirror/state'
|
|
import { Decoration, EditorView } from '@codemirror/view'
|
|
import { codeRenderers, notePath, pluginRefresh } from '../../../common/plugins/registry'
|
|
import { codeContentRange, fenceLanguage } from './codeblocks'
|
|
import { editorFocused, setEditorFocus } from './focus'
|
|
import { CodeRendererWidget, MathWidget } from './widgets'
|
|
|
|
// 公式块与 Mermaid 图表:未编辑时整块替换为渲染结果;光标在块内时显示源码,并在下方实时预览。
|
|
// 跨行的块级替换只能由 StateField 提供;与表格一样只处理顶层的块
|
|
|
|
/** 返回块的源码内容及其相对块起点的偏移(点击渲染结果时光标落在内容开头) */
|
|
function blockContent(state: EditorState, node: SyntaxNode): { text: string, offset: number, language?: string } | null {
|
|
if (node.name === 'BlockMath') {
|
|
const marks = node.getChildren('BlockMathMark')
|
|
const from = state.doc.lineAt(node.from).to + 1
|
|
const to = marks.length > 1 ? state.doc.lineAt(marks[marks.length - 1].from).from - 1 : node.to
|
|
return { text: from <= to ? state.sliceDoc(from, to) : '', offset: Math.min(from, node.to) - node.from }
|
|
}
|
|
if (node.name === 'FencedCode') {
|
|
const info = node.getChild('CodeInfo')
|
|
if (!info) return null
|
|
const language = fenceLanguage(state, info.from, info.to).toLowerCase()
|
|
if (!codeRenderers.has(language)) return null
|
|
const { from, to } = codeContentRange(state, node)
|
|
return { text: state.sliceDoc(from, to), offset: from - node.from, language }
|
|
}
|
|
return null
|
|
}
|
|
|
|
function buildBlocks(state: EditorState): DecorationSet {
|
|
const out: Range<Decoration>[] = []
|
|
const sel = state.field(editorFocused) ? state.selection.ranges : []
|
|
for (let node = syntaxTree(state).topNode.firstChild; node; node = node.nextSibling) {
|
|
const content = blockContent(state, node)
|
|
if (!content) continue
|
|
const editing = sel.some(r => r.from <= node.to && r.to >= node.from)
|
|
const widget = (editOffset: number | null) =>
|
|
node.name === 'BlockMath' ? new MathWidget(content.text, true, editOffset) : new CodeRendererWidget(content.text, editOffset, content.language!, state.facet(notePath).path, codeRenderers.get(content.language!)!)
|
|
if (editing) out.push(Decoration.widget({ widget: widget(null), block: true, side: 1 }).range(node.to))
|
|
else out.push(Decoration.replace({ widget: widget(content.offset), block: true }).range(node.from, node.to))
|
|
}
|
|
return Decoration.set(out)
|
|
}
|
|
|
|
export const renderedBlocks = StateField.define<DecorationSet>({
|
|
create: buildBlocks,
|
|
update(value, tr) {
|
|
if (
|
|
tr.docChanged
|
|
|| tr.selection
|
|
|| tr.effects.some(e => e.is(setEditorFocus) || e.is(pluginRefresh))
|
|
|| syntaxTree(tr.startState) !== syntaxTree(tr.state)
|
|
) {
|
|
return buildBlocks(tr.state)
|
|
}
|
|
return value
|
|
},
|
|
provide: f => EditorView.decorations.from(f),
|
|
})
|