mirror of
				https://github.com/langgenius/dify.git
				synced 2025-10-30 18:33:30 +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,
 | |
|     },
 | |
|   })
 | |
| }
 | 
