import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext"; import { mergeRegister } from "@lexical/utils"; import { type LexicalEditor, SELECTION_CHANGE_COMMAND, TextNode } from "lexical"; import { useEffect } from "react"; import { $createFormattedTextMarkerNode, $createFormattedTextNode, $isFormattedTextMarkerNode, $isFormattedTextNode, FormattedTextMarkerNode, FormattedTextNode } from "../nodes/formatted_text"; import { type TextFormat, textFormats } from "../serialized_editor_content"; export function FormattedTextPlugin() { const [editor] = useLexicalComposerContext(); useEffect(() => mergeRegister( // Create formatted text ...Object.keys(textFormats).map((format) => editor.registerNodeTransform(TextNode, createFormattedTextNode(format as TextFormat, editor)) ), // Remove formatted text marker nodes when not inside formatted text node editor.registerNodeTransform(FormattedTextMarkerNode, (node) => { const parent = node.getParent(); if (!parent) return; if ($isFormattedTextNode(parent)) { const format = parent.getStyle(); const markerChars = textFormats[format]; if (node.getTextContent() === markerChars) return; } node.getChildren().reverse().forEach((child) => node.insertAfter(child)); node.remove(); }), // Remove formatted text nodes without matching markers editor.registerNodeTransform(FormattedTextNode, (node) => { const format = node.getStyle(); const markerChars = textFormats[format]; const firstMarker = node.getFirstChild(); const lastMarker = node.getLastChild(); if ( !$isFormattedTextMarkerNode(firstMarker) || !$isFormattedTextMarkerNode(lastMarker) ) { node.getChildren().reverse().forEach((child) => node.insertAfter(child)); node.remove(); return; } const firstMarkerContent = firstMarker.getTextContent(); const lastMarkerContent = lastMarker.getTextContent(); if (firstMarkerContent !== markerChars || lastMarkerContent !== markerChars) { node.getChildren().reverse().forEach((child) => node.insertAfter(child)); node.remove(); return; } }), // Remove formatted text nodes without content editor.registerNodeTransform(FormattedTextNode, (node) => { const format = node.getStyle(); const formattedChars = textFormats[format]; const content = node.getTextContent(); if (content !== formattedChars + formattedChars) return; node.getChildren().reverse().forEach((child) => node.insertAfter(child)); node.remove(); }), )); return null; } const createFormattedTextNode = (format: TextFormat, editor: LexicalEditor) => (node: TextNode) => { const markerChars = textFormats[format]; const markerCharsLen = markerChars.length; const content = node.getTextContent(); // TODO: search in all text nodes within the parent block node const start = content.indexOf(markerChars); if (start === -1) return; const end = content.indexOf(markerChars, start + markerCharsLen); if (end === -1) return; if (start === end - markerCharsLen) return; const startIndex = start > 0 ? 1 : 0; const contentIndex = startIndex + 1; const endIndex = contentIndex + 1; const textNodes = node.splitText(start, start + markerCharsLen, end, end + markerCharsLen); const formattedTextNode = $createFormattedTextNode(format); textNodes[startIndex]!.insertBefore(formattedTextNode); const startMarker = $createFormattedTextMarkerNode(); startMarker.append(textNodes[startIndex]!); const endMarker = $createFormattedTextMarkerNode(); endMarker.append(textNodes[endIndex]!); formattedTextNode.append(startMarker, textNodes[contentIndex]!, endMarker); editor.dispatchCommand(SELECTION_CHANGE_COMMAND, undefined); }