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  }