UNPKG

@rtdui/editor

Version:

React rich text editor based on tiptap

172 lines (169 loc) 5.67 kB
'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