kaneo (minimalist kanban) fork to experiment adding a tangled integration github.com/usekaneo/kaneo
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 146 lines 4.4 kB view raw
1import { Extension, findChildren } from "@tiptap/core"; 2import type { Node as ProseMirrorNode } from "@tiptap/pm/model"; 3import type { EditorState, Transaction } from "@tiptap/pm/state"; 4import { Plugin, PluginKey } from "@tiptap/pm/state"; 5import { Decoration, DecorationSet } from "@tiptap/pm/view"; 6import type { Highlighter } from "shiki"; 7 8type ShikiCodeBlockOptions = { 9 defaultLanguage: string; 10 highlighter: Highlighter | null | (() => Highlighter | null); 11 resolveLanguage: (language: string) => string; 12 themeDark: string; 13 themeLight: string; 14}; 15 16export const SHIKI_CODEBLOCK_REFRESH_META = "shiki-codeblock-refresh"; 17const shikiPluginKey = new PluginKey("shiki-codeblock"); 18 19function getCurrentTheme(options: ShikiCodeBlockOptions) { 20 if (typeof document === "undefined") return options.themeDark; 21 return document.documentElement.classList.contains("dark") 22 ? options.themeDark 23 : options.themeLight; 24} 25 26function resolveHighlighter(options: ShikiCodeBlockOptions) { 27 if (typeof options.highlighter === "function") { 28 return options.highlighter(); 29 } 30 31 return options.highlighter; 32} 33 34function getDecorations(doc: ProseMirrorNode, options: ShikiCodeBlockOptions) { 35 const highlighter = resolveHighlighter(options); 36 if (!highlighter) return DecorationSet.empty; 37 38 const decorations: Decoration[] = []; 39 const theme = getCurrentTheme(options); 40 41 findChildren(doc, (node) => node.type.name === "codeBlock").forEach( 42 (block) => { 43 const code = block.node.textContent; 44 if (!code) return; 45 46 const rawLanguage = 47 (block.node.attrs.language as string | undefined) || ""; 48 const normalizedLanguage = options.resolveLanguage( 49 rawLanguage || options.defaultLanguage, 50 ); 51 let tokensResult: ReturnType<Highlighter["codeToTokens"]>; 52 53 try { 54 tokensResult = highlighter.codeToTokens(code, { 55 lang: normalizedLanguage as never, 56 theme: theme as never, 57 }); 58 } catch { 59 tokensResult = highlighter.codeToTokens(code, { 60 lang: "text" as never, 61 theme: theme as never, 62 }); 63 } 64 65 let from = block.pos + 1; 66 for ( 67 let lineIndex = 0; 68 lineIndex < tokensResult.tokens.length; 69 lineIndex += 1 70 ) { 71 const line = tokensResult.tokens[lineIndex]; 72 for (const token of line) { 73 const text = token.content || ""; 74 if (!text.length) continue; 75 76 const to = from + text.length; 77 const styles: string[] = []; 78 if (token.color) styles.push(`color:${token.color}`); 79 if (typeof token.fontStyle === "number") { 80 if ((token.fontStyle & 1) !== 0) styles.push("font-style:italic"); 81 if ((token.fontStyle & 2) !== 0) styles.push("font-weight:600"); 82 if ((token.fontStyle & 4) !== 0) { 83 styles.push("text-decoration:underline"); 84 } 85 } 86 87 if (styles.length > 0) { 88 decorations.push( 89 Decoration.inline(from, to, { style: styles.join(";") }), 90 ); 91 } 92 from = to; 93 } 94 95 if (lineIndex < tokensResult.tokens.length - 1) { 96 from += 1; 97 } 98 } 99 }, 100 ); 101 102 return DecorationSet.create(doc, decorations); 103} 104 105export const ShikiCodeBlock = Extension.create<ShikiCodeBlockOptions>({ 106 name: "shikiCodeBlock", 107 108 addOptions() { 109 return { 110 defaultLanguage: "text", 111 highlighter: null, 112 resolveLanguage: (language: string) => language, 113 themeDark: "github-dark", 114 themeLight: "github-light", 115 }; 116 }, 117 118 addProseMirrorPlugins() { 119 const options = this.options; 120 121 return [ 122 new Plugin({ 123 key: shikiPluginKey, 124 state: { 125 init: (_config: unknown, state: EditorState) => 126 getDecorations(state.doc, options), 127 apply: (transaction: Transaction, decorationSet: DecorationSet) => { 128 if ( 129 transaction.docChanged || 130 transaction.getMeta(SHIKI_CODEBLOCK_REFRESH_META) 131 ) { 132 return getDecorations(transaction.doc, options); 133 } 134 135 return decorationSet.map(transaction.mapping, transaction.doc); 136 }, 137 }, 138 props: { 139 decorations(state: EditorState) { 140 return shikiPluginKey.getState(state); 141 }, 142 }, 143 }), 144 ]; 145 }, 146});