mirror of
				https://github.com/langgenius/dify.git
				synced 2025-11-04 04:43:09 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			329 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			329 lines
		
	
	
		
			8.6 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
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 && (typeof text === 'string') ? 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,
 | 
						|
    },
 | 
						|
  })
 | 
						|
}
 |