🚧 Refactor markdown preview extension

This commit is contained in:
2025-11-28 00:38:38 +08:00
parent ac086db1ed
commit 8d9bcdad7e
30 changed files with 2525 additions and 991 deletions

View File

@@ -0,0 +1,82 @@
import { syntaxTree } from '@codemirror/language';
import { EditorState, StateField } from '@codemirror/state';
import {
Decoration,
DecorationSet,
EditorView,
WidgetType
} from '@codemirror/view';
import DOMPurify from 'dompurify';
import { isCursorInRange } from '../util';
interface EmbedBlockData {
from: number;
to: number;
content: string;
}
function extractHTMLBlocks(state: EditorState) {
const blocks = new Array<EmbedBlockData>();
syntaxTree(state).iterate({
enter({ from, to, name }) {
if (name !== 'HTMLBlock') return;
if (isCursorInRange(state, [from, to])) return;
const html = state.sliceDoc(from, to);
const content = DOMPurify.sanitize(html, {
FORBID_ATTR: ['style']
});
blocks.push({
from,
to,
content
});
}
});
return blocks;
}
function blockToDecoration(blocks: EmbedBlockData[]) {
return blocks.map((block) =>
Decoration.widget({
widget: new HTMLBlockWidget(block),
block: true,
side: 1
}).range(block.to)
);
}
export const htmlBlock = StateField.define<DecorationSet>({
create(state) {
return Decoration.set(blockToDecoration(extractHTMLBlocks(state)));
},
update(value, tx) {
if (tx.docChanged || tx.selection) {
return Decoration.set(
blockToDecoration(extractHTMLBlocks(tx.state))
);
}
return value.map(tx.changes);
},
provide(field) {
return EditorView.decorations.from(field);
}
});
class HTMLBlockWidget extends WidgetType {
constructor(public data: EmbedBlockData, public isInline?: true) {
super();
}
toDOM(): HTMLElement {
const dom = document.createElement('div');
dom.style.display = this.isInline ? 'inline' : 'block';
// Contain child margins
dom.style.overflow = 'auto';
// This is sanitized!
dom.innerHTML = this.data.content;
return dom;
}
eq(widget: HTMLBlockWidget): boolean {
return JSON.stringify(widget.data) === JSON.stringify(this.data);
}
}