github.com/ngocphuongnb/tetua@v0.0.7-alpha/packages/editor/src/extensions/codeblock-lowlight/lowlight-plugin.ts (about) 1 import { Plugin, PluginKey } from 'prosemirror-state' 2 import { Decoration, DecorationSet } from 'prosemirror-view' 3 import { Node as ProsemirrorNode } from 'prosemirror-model' 4 import { findChildren } from '@tiptap/core' 5 6 function parseNodes(nodes: any[], className: string[] = []): { text: string, classes: string[] }[] { 7 return nodes 8 .map(node => { 9 const classes = [ 10 ...className, 11 ...node.properties 12 ? node.properties.className 13 : [], 14 ] 15 16 if (node.children) { 17 return parseNodes(node.children, classes) 18 } 19 20 return { 21 text: node.value, 22 classes, 23 } 24 }) 25 .flat() 26 } 27 28 function getHighlightNodes(result: any) { 29 // `.value` for lowlight v1, `.children` for lowlight v2 30 return result.value || result.children || [] 31 } 32 33 function getDecorations({ 34 doc, 35 name, 36 lowlight, 37 defaultLanguage, 38 }: { doc: ProsemirrorNode, name: string, lowlight: any, defaultLanguage: string | null | undefined }) { 39 const decorations: Decoration[] = [] 40 41 findChildren(doc, node => node.type.name === name) 42 .forEach(block => { 43 let from = block.pos + 1 44 const language = block.node.attrs.language || defaultLanguage 45 const languages = lowlight.listLanguages() 46 const nodes = language && languages.includes(language) 47 ? getHighlightNodes(lowlight.highlight(language, block.node.textContent)) 48 : getHighlightNodes(lowlight.highlightAuto(block.node.textContent)) 49 50 parseNodes(nodes).forEach(node => { 51 const to = from + node.text.length 52 53 if (node.classes.length) { 54 const decoration = Decoration.inline(from, to, { 55 class: node.classes.join(' '), 56 }) 57 58 decorations.push(decoration) 59 } 60 61 from = to 62 }) 63 }) 64 65 return DecorationSet.create(doc, decorations) 66 } 67 68 export function LowlightPlugin({ name, lowlight, defaultLanguage }: { name: string, lowlight: any, defaultLanguage: string | null | undefined }) { 69 return new Plugin({ 70 key: new PluginKey('lowlight'), 71 72 state: { 73 init: (_, { doc }) => getDecorations({ 74 doc, 75 name, 76 lowlight, 77 defaultLanguage, 78 }), 79 apply: (transaction, decorationSet, oldState, newState) => { 80 const oldNodeName = oldState.selection.$head.parent.type.name 81 const newNodeName = newState.selection.$head.parent.type.name 82 const oldNodes = findChildren(oldState.doc, node => node.type.name === name) 83 const newNodes = findChildren(newState.doc, node => node.type.name === name) 84 85 if ( 86 transaction.docChanged 87 // Apply decorations if: 88 && ( 89 // selection includes named node, 90 [oldNodeName, newNodeName].includes(name) 91 // OR transaction adds/removes named node, 92 || newNodes.length !== oldNodes.length 93 // OR transaction has changes that completely encapsulte a node 94 // (for example, a transaction that affects the entire document). 95 // Such transactions can happen during collab syncing via y-prosemirror, for example. 96 || transaction.steps.some(step => { 97 // @ts-ignore 98 return step.from !== undefined 99 // @ts-ignore 100 && step.to !== undefined 101 && oldNodes.some(node => { 102 // @ts-ignore 103 return node.pos >= step.from 104 // @ts-ignore 105 && node.pos + node.node.nodeSize <= step.to 106 }) 107 }) 108 ) 109 ) { 110 return getDecorations({ 111 doc: transaction.doc, 112 name, 113 lowlight, 114 defaultLanguage, 115 }) 116 } 117 118 return decorationSet.map(transaction.mapping, transaction.doc) 119 }, 120 }, 121 122 props: { 123 decorations(state) { 124 return this.getState(state) 125 }, 126 }, 127 }) 128 }