Files
jiang13-forum/frontend/src/utils/codeBlockHighlightPlugin.ts

72 lines
2.2 KiB
TypeScript

import hljs from 'highlight.js/lib/common';
import type { Node as ProseMirrorNode } from '@tiptap/pm/model';
import { Plugin, PluginKey } from '@tiptap/pm/state';
import { Decoration, DecorationSet } from '@tiptap/pm/view';
import { registerAardioLanguage } from './hljsAardio';
registerAardioLanguage(hljs);
const key = new PluginKey('articleCodeBlockHighlight');
/** 将 hljs HTML 映射为 ProseMirror inline Decoration */
function decorationsForCodeBlock(node: ProseMirrorNode, pos: number): Decoration[] {
const language = ((node.attrs.language as string) || '').trim();
const text = node.textContent;
if (!text || !language || !hljs.getLanguage(language)) return [];
let html: string;
try {
html = hljs.highlight(text, { language, ignoreIllegals: true }).value;
} catch {
return [];
}
const root = document.createElement('div');
root.innerHTML = html;
const out: Decoration[] = [];
let offset = 0;
const walk = (dom: Node, className: string | null) => {
if (dom.nodeType === Node.TEXT_NODE) {
const len = dom.textContent?.length ?? 0;
if (len > 0 && className) {
const from = pos + 1 + offset;
out.push(Decoration.inline(from, from + len, { class: className }));
}
offset += len;
return;
}
if (dom.nodeType !== Node.ELEMENT_NODE) return;
const el = dom as Element;
const nextClass = el.className || className;
el.childNodes.forEach(child => walk(child, nextClass));
};
root.childNodes.forEach(child => walk(child, null));
return out;
}
function buildDecorations(doc: ProseMirrorNode): DecorationSet {
const decos: Decoration[] = [];
doc.descendants((node, pos) => {
if (node.type.name !== 'codeBlock') return true;
decos.push(...decorationsForCodeBlock(node, pos));
return false;
});
return DecorationSet.create(doc, decos);
}
/** 富文本代码块:用 Decoration 叠 hljs 着色,保持可编辑纯文本 */
export function createCodeBlockHighlightPlugin(): Plugin {
return new Plugin({
key,
state: {
init: (_, state) => buildDecorations(state.doc),
apply: (tr, old) => (tr.docChanged ? buildDecorations(tr.doc) : old),
},
props: {
decorations: state => key.getState(state),
},
});
}