1import Prism, { LanguageMap } from 'prismjs'; 2import { Block, Text, Decoration } from 'slate'; 3import { Plugin } from '@grafana/slate-react'; 4import Options, { OptionsFormat } from './options'; 5import TOKEN_MARK from './TOKEN_MARK'; 6 7export interface Token { 8 content: string; 9 offsets?: { 10 start: number; 11 end: number; 12 }; 13 types: string[]; 14 aliases: string[]; 15 prev?: Token | null; 16 next?: Token | null; 17} 18 19/** 20 * A Slate plugin to highlight code syntax. 21 */ 22export function SlatePrism(optsParam: OptionsFormat = {}, prismLanguages = Prism.languages as LanguageMap): Plugin { 23 const opts: Options = new Options(optsParam); 24 25 return { 26 decorateNode: (node, editor, next) => { 27 if (!opts.onlyIn(node)) { 28 return next(); 29 } 30 31 const block = Block.create(node as Block); 32 const grammarName = opts.getSyntax(block); 33 const grammar = prismLanguages[grammarName]; 34 35 if (!grammar) { 36 // Grammar not loaded 37 return []; 38 } 39 40 // Tokenize the whole block text 41 const texts = block.getTexts(); 42 const blockText = texts.map((text) => text && text.getText()).join('\n'); 43 const tokens = Prism.tokenize(blockText, grammar); 44 const flattened = flattenTokens(tokens); 45 46 const newData = editor.value.data.set('tokens', flattened); 47 editor.setData(newData); 48 return decorateNode(opts, tokens, block); 49 }, 50 51 renderDecoration: (props, editor, next) => 52 opts.renderDecoration( 53 { 54 children: props.children, 55 decoration: props.decoration, 56 }, 57 editor as any, 58 next 59 ), 60 }; 61} 62 63/** 64 * Returns the decoration for a node 65 */ 66function decorateNode(opts: Options, tokens: Array<string | Prism.Token>, block: Block) { 67 const texts = block.getTexts(); 68 69 // The list of decorations to return 70 const decorations: Decoration[] = []; 71 let textStart = 0; 72 let textEnd = 0; 73 74 texts.forEach((text) => { 75 textEnd = textStart + text!.getText().length; 76 77 let offset = 0; 78 function processToken(token: string | Prism.Token, accu?: string | number) { 79 if (typeof token === 'string') { 80 if (accu) { 81 const decoration = createDecoration({ 82 text: text!, 83 textStart, 84 textEnd, 85 start: offset, 86 end: offset + token.length, 87 className: `prism-token token ${accu}`, 88 block, 89 }); 90 91 if (decoration) { 92 decorations.push(decoration); 93 } 94 } 95 offset += token.length; 96 } else { 97 accu = `${accu} ${token.type}`; 98 if (token.alias) { 99 accu += ' ' + token.alias; 100 } 101 102 if (typeof token.content === 'string') { 103 const decoration = createDecoration({ 104 text: text!, 105 textStart, 106 textEnd, 107 start: offset, 108 end: offset + token.content.length, 109 className: `prism-token token ${accu}`, 110 block, 111 }); 112 113 if (decoration) { 114 decorations.push(decoration); 115 } 116 117 offset += token.content.length; 118 } else { 119 // When using token.content instead of token.matchedStr, token can be deep 120 for (let i = 0; i < token.content.length; i += 1) { 121 // @ts-ignore 122 processToken(token.content[i], accu); 123 } 124 } 125 } 126 } 127 128 tokens.forEach(processToken); 129 textStart = textEnd + 1; // account for added `\n` 130 }); 131 132 return decorations; 133} 134 135/** 136 * Return a decoration range for the given text. 137 */ 138function createDecoration({ 139 text, 140 textStart, 141 textEnd, 142 start, 143 end, 144 className, 145 block, 146}: { 147 text: Text; // The text being decorated 148 textStart: number; // Its start position in the whole text 149 textEnd: number; // Its end position in the whole text 150 start: number; // The position in the whole text where the token starts 151 end: number; // The position in the whole text where the token ends 152 className: string; // The prism token classname 153 block: Block; 154}): Decoration | null { 155 if (start >= textEnd || end <= textStart) { 156 // Ignore, the token is not in the text 157 return null; 158 } 159 160 // Shrink to this text boundaries 161 start = Math.max(start, textStart); 162 end = Math.min(end, textEnd); 163 164 // Now shift offsets to be relative to this text 165 start -= textStart; 166 end -= textStart; 167 168 const myDec = block.createDecoration({ 169 object: 'decoration', 170 anchor: { 171 key: text.key, 172 offset: start, 173 object: 'point', 174 }, 175 focus: { 176 key: text.key, 177 offset: end, 178 object: 'point', 179 }, 180 type: TOKEN_MARK, 181 data: { className }, 182 }); 183 184 return myDec; 185} 186 187function flattenToken(token: string | Prism.Token | Array<string | Prism.Token>): Token[] { 188 if (typeof token === 'string') { 189 return [ 190 { 191 content: token, 192 types: [], 193 aliases: [], 194 }, 195 ]; 196 } else if (Array.isArray(token)) { 197 return token.flatMap((t) => flattenToken(t)); 198 } else if (token instanceof Prism.Token) { 199 return flattenToken(token.content).flatMap((t) => { 200 let aliases: string[] = []; 201 if (typeof token.alias === 'string') { 202 aliases = [token.alias]; 203 } else { 204 aliases = token.alias ?? []; 205 } 206 207 return { 208 content: t.content, 209 types: [token.type, ...t.types], 210 aliases: [...aliases, ...t.aliases], 211 }; 212 }); 213 } 214 215 return []; 216} 217 218export function flattenTokens(token: string | Prism.Token | Array<string | Prism.Token>) { 219 const tokens = flattenToken(token); 220 221 if (!tokens.length) { 222 return []; 223 } 224 225 const firstToken = tokens[0]; 226 firstToken.prev = null; 227 firstToken.next = tokens.length >= 2 ? tokens[1] : null; 228 firstToken.offsets = { 229 start: 0, 230 end: firstToken.content.length, 231 }; 232 233 for (let i = 1; i < tokens.length - 1; i++) { 234 tokens[i].prev = tokens[i - 1]; 235 tokens[i].next = tokens[i + 1]; 236 237 tokens[i].offsets = { 238 start: tokens[i - 1].offsets!.end, 239 end: tokens[i - 1].offsets!.end + tokens[i].content.length, 240 }; 241 } 242 243 const lastToken = tokens[tokens.length - 1]; 244 lastToken.prev = tokens.length >= 2 ? tokens[tokens.length - 2] : null; 245 lastToken.next = null; 246 lastToken.offsets = { 247 start: tokens.length >= 2 ? tokens[tokens.length - 2].offsets!.end : 0, 248 end: 249 tokens.length >= 2 ? tokens[tokens.length - 2].offsets!.end + lastToken.content.length : lastToken.content.length, 250 }; 251 252 return tokens; 253} 254