| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329 | import { $isAtNodeEnd } from '@lexical/selection'import type {  ElementNode,  Klass,  LexicalEditor,  LexicalNode,  RangeSelection,  TextNode,} from 'lexical'import {  $createTextNode,  $getSelection,  $isRangeSelection,  $isTextNode,} from 'lexical'import type { EntityMatch } from '@lexical/text'import { CustomTextNode } from './plugins/custom-text/node'import type { MenuTextMatch } from './types'export function getSelectedNode(  selection: RangeSelection,): TextNode | ElementNode {  const anchor = selection.anchor  const focus = selection.focus  const anchorNode = selection.anchor.getNode()  const focusNode = selection.focus.getNode()  if (anchorNode === focusNode)    return anchorNode  const isBackward = selection.isBackward()  if (isBackward)    return $isAtNodeEnd(focus) ? anchorNode : focusNode  else    return $isAtNodeEnd(anchor) ? anchorNode : focusNode}export function registerLexicalTextEntity<T extends TextNode>(  editor: LexicalEditor,  getMatch: (text: string) => null | EntityMatch,  targetNode: Klass<T>,  createNode: (textNode: TextNode) => T,) {  const isTargetNode = (node: LexicalNode | null | undefined): node is T => {    return node instanceof targetNode  }  const replaceWithSimpleText = (node: TextNode): void => {    const textNode = $createTextNode(node.getTextContent())    textNode.setFormat(node.getFormat())    node.replace(textNode)  }  const getMode = (node: TextNode): number => {    return node.getLatest().__mode  }  const textNodeTransform = (node: TextNode) => {    if (!node.isSimpleText())      return    const prevSibling = node.getPreviousSibling()    let text = node.getTextContent()    let currentNode = node    let match    if ($isTextNode(prevSibling)) {      const previousText = prevSibling.getTextContent()      const combinedText = previousText + text      const prevMatch = getMatch(combinedText)      if (isTargetNode(prevSibling)) {        if (prevMatch === null || getMode(prevSibling) !== 0) {          replaceWithSimpleText(prevSibling)          return        }        else {          const diff = prevMatch.end - previousText.length          if (diff > 0) {            const concatText = text.slice(0, diff)            const newTextContent = previousText + concatText            prevSibling.select()            prevSibling.setTextContent(newTextContent)            if (diff === text.length) {              node.remove()            }            else {              const remainingText = text.slice(diff)              node.setTextContent(remainingText)            }            return          }        }      }      else if (prevMatch === null || prevMatch.start < previousText.length) {        return      }    }    while (true) {      match = getMatch(text)      let nextText = match === null ? '' : text.slice(match.end)      text = nextText      if (nextText === '') {        const nextSibling = currentNode.getNextSibling()        if ($isTextNode(nextSibling)) {          nextText = currentNode.getTextContent() + nextSibling.getTextContent()          const nextMatch = getMatch(nextText)          if (nextMatch === null) {            if (isTargetNode(nextSibling))              replaceWithSimpleText(nextSibling)            else              nextSibling.markDirty()            return          }          else if (nextMatch.start !== 0) {            return          }        }      }      else {        const nextMatch = getMatch(nextText)        if (nextMatch !== null && nextMatch.start === 0)          return      }      if (match === null)        return      if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity())        continue      let nodeToReplace      if (match.start === 0)        [nodeToReplace, currentNode] = currentNode.splitText(match.end)      else        [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)      const replacementNode = createNode(nodeToReplace)      replacementNode.setFormat(nodeToReplace.getFormat())      nodeToReplace.replace(replacementNode)      if (currentNode == null)        return    }  }  const reverseNodeTransform = (node: T) => {    const text = node.getTextContent()    const match = getMatch(text)    if (match === null || match.start !== 0) {      replaceWithSimpleText(node)      return    }    if (text.length > match.end) {      // This will split out the rest of the text as simple text      node.splitText(match.end)      return    }    const prevSibling = node.getPreviousSibling()    if ($isTextNode(prevSibling) && prevSibling.isTextEntity()) {      replaceWithSimpleText(prevSibling)      replaceWithSimpleText(node)    }    const nextSibling = node.getNextSibling()    if ($isTextNode(nextSibling) && nextSibling.isTextEntity()) {      replaceWithSimpleText(nextSibling) // This may have already been converted in the previous block      if (isTargetNode(node))        replaceWithSimpleText(node)    }  }  const removePlainTextTransform = editor.registerNodeTransform(CustomTextNode, textNodeTransform)  const removeReverseNodeTransform = editor.registerNodeTransform(targetNode, reverseNodeTransform)  return [removePlainTextTransform, removeReverseNodeTransform]}export const decoratorTransform = (  node: CustomTextNode,  getMatch: (text: string) => null | EntityMatch,  createNode: (textNode: TextNode) => LexicalNode,) => {  if (!node.isSimpleText())    return  const prevSibling = node.getPreviousSibling()  let text = node.getTextContent()  let currentNode = node  let match  while (true) {    match = getMatch(text)    let nextText = match === null ? '' : text.slice(match.end)    text = nextText    if (nextText === '') {      const nextSibling = currentNode.getNextSibling()      if ($isTextNode(nextSibling)) {        nextText = currentNode.getTextContent() + nextSibling.getTextContent()        const nextMatch = getMatch(nextText)        if (nextMatch === null) {          nextSibling.markDirty()          return        }        else if (nextMatch.start !== 0) {          return        }      }    }    else {      const nextMatch = getMatch(nextText)      if (nextMatch !== null && nextMatch.start === 0)        return    }    if (match === null)      return    if (match.start === 0 && $isTextNode(prevSibling) && prevSibling.isTextEntity())      continue    let nodeToReplace    if (match.start === 0)      [nodeToReplace, currentNode] = currentNode.splitText(match.end)    else      [, nodeToReplace, currentNode] = currentNode.splitText(match.start, match.end)    const replacementNode = createNode(nodeToReplace)    nodeToReplace.replace(replacementNode)    if (currentNode == null)      return  }}function getFullMatchOffset(  documentText: string,  entryText: string,  offset: number,): number {  let triggerOffset = offset  for (let i = triggerOffset; i <= entryText.length; i++) {    if (documentText.substr(-i) === entryText.substr(0, i))      triggerOffset = i  }  return triggerOffset}export function $splitNodeContainingQuery(match: MenuTextMatch): TextNode | null {  const selection = $getSelection()  if (!$isRangeSelection(selection) || !selection.isCollapsed())    return null  const anchor = selection.anchor  if (anchor.type !== 'text')    return null  const anchorNode = anchor.getNode()  if (!anchorNode.isSimpleText())    return null  const selectionOffset = anchor.offset  const textContent = anchorNode.getTextContent().slice(0, selectionOffset)  const characterOffset = match.replaceableString.length  const queryOffset = getFullMatchOffset(    textContent,    match.matchingString,    characterOffset,  )  const startOffset = selectionOffset - queryOffset  if (startOffset < 0)    return null  let newNode  if (startOffset === 0)    [newNode] = anchorNode.splitText(selectionOffset)  else    [, newNode] = anchorNode.splitText(startOffset, selectionOffset)  return newNode}export function textToEditorState(text: string) {  const paragraph = text ? text.split('\n') : ['']  return JSON.stringify({    root: {      children: paragraph.map((p) => {        return {          children: [{            detail: 0,            format: 0,            mode: 'normal',            style: '',            text: p,            type: 'custom-text',            version: 1,          }],          direction: 'ltr',          format: '',          indent: 0,          type: 'paragraph',          version: 1,        }      }),      direction: 'ltr',      format: '',      indent: 0,      type: 'root',      version: 1,    },  })}
 |