@rtdui/editor
Version:
React rich text editor based on tiptap
172 lines (169 loc) • 5.67 kB
JavaScript
'use client';
import { findChildren } from '@tiptap/core';
import { Plugin, PluginKey } from '@tiptap/pm/state';
import { DecorationSet, Decoration } from '@tiptap/pm/view';
import { initHighlighter, loadTheme, loadLanguage, getShiki } from './shiki-highlighter.mjs';
function tokenStyleToCssStyle(token) {
let style = "";
if (token.color)
style += `color: ${token.color};`;
if (token.fontStyle) {
if (token.fontStyle & 1)
style += "font-style: italic;";
if (token.fontStyle & 2)
style += "font-weight: bold;";
if (token.fontStyle & 4)
style += "text-underline: underline;";
if (token.fontStyle & 8)
style += "text-underline: line-through;";
}
return style;
}
function getDecorations({
doc,
name,
defaultTheme,
defaultLanguage
}) {
const decorations = [];
const codeBlocks = findChildren(doc, (node) => node.type.name === name);
codeBlocks.forEach((block) => {
let from = block.pos + 1;
let language = block.node.attrs.language || defaultLanguage;
const theme = block.node.attrs.theme || defaultTheme;
const highlighter = getShiki();
if (!highlighter)
return;
if (!highlighter.getLoadedLanguages().includes(language)) {
language = "plaintext";
}
const themeToApply = highlighter.getLoadedThemes().includes(theme) ? theme : highlighter.getLoadedThemes()[0];
const tokens = highlighter.codeToTokensBase(block.node.textContent, {
lang: language,
theme: themeToApply
});
for (const line of tokens) {
for (const token of line) {
const to = from + token.content.length;
const decoration = Decoration.inline(from, to, {
// style: variantsStyleToCssStyle(token.variants), // 多主题模式
style: tokenStyleToCssStyle(token)
// 单主题模式
});
decorations.push(decoration);
from = to;
}
from += 1;
}
});
return DecorationSet.create(doc, decorations);
}
function ProseMirrorShikiPlugin({
name,
defaultLanguage,
defaultTheme
}) {
const shikiPlugin = new Plugin({
key: new PluginKey("shiki"),
view(view) {
class ShikiPluginView {
constructor() {
this.initDecorations();
}
update() {
this.checkUndecoratedBlocks();
}
destroy() {
}
// Initialize shiki async, and then highlight initial document
async initDecorations() {
const doc = view.state.doc;
await initHighlighter({ doc, name, defaultLanguage, defaultTheme });
const tr = view.state.tr.setMeta("shikiPluginForceDecoration", true);
view.dispatch(tr);
}
// When new codeblocks were added and they have missing themes or
// languages, load those and then add code decorations once again.
async checkUndecoratedBlocks() {
const codeBlocks = findChildren(
view.state.doc,
(node) => node.type.name === name
);
const loadStates = await Promise.all(
codeBlocks.flatMap((block) => [
loadTheme(block.node.attrs.theme),
loadLanguage(block.node.attrs.language)
])
);
const didLoadSomething = loadStates.includes(true);
if (didLoadSomething) {
const tr = view.state.tr.setMeta(
"shikiPluginForceDecoration",
true
);
view.dispatch(tr);
}
}
}
return new ShikiPluginView();
},
state: {
init: (_, { doc }) => {
return getDecorations({
doc,
name,
defaultLanguage,
defaultTheme
});
},
apply: (transaction, decorationSet, oldState, newState) => {
const oldNodeName = oldState.selection.$head.parent.type.name;
const newNodeName = newState.selection.$head.parent.type.name;
const oldNodes = findChildren(
oldState.doc,
(node) => node.type.name === name
);
const newNodes = findChildren(
newState.doc,
(node) => node.type.name === name
);
const didChangeSomeCodeBlock = transaction.docChanged && // Apply decorations if:
// selection includes named node,
([oldNodeName, newNodeName].includes(name) || // OR transaction adds/removes named node,
newNodes.length !== oldNodes.length || // OR transaction has changes that completely encapsulte a node
// (for example, a transaction that affects the entire document).
// Such transactions can happen during collab syncing via y-prosemirror, for example.
transaction.steps.some((step) => {
return (
// @ts-ignore
step.from !== void 0 && // @ts-ignore
step.to !== void 0 && oldNodes.some((node) => {
return (
// @ts-ignore
node.pos >= step.from && // @ts-ignore
node.pos + node.node.nodeSize <= step.to
);
})
);
}));
if (transaction.getMeta("shikiPluginForceDecoration") || didChangeSomeCodeBlock) {
return getDecorations({
doc: transaction.doc,
name,
defaultLanguage,
defaultTheme
});
}
return decorationSet.map(transaction.mapping, transaction.doc);
}
},
props: {
decorations(state) {
return shikiPlugin.getState(state);
}
}
});
return shikiPlugin;
}
export { ProseMirrorShikiPlugin };
//# sourceMappingURL=prosemirror-shiki-plugin.mjs.map