You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Lexical JS中查找匹配文本并正确应用高亮样式?

在Lexical JS中实现文本匹配高亮的解决方案

需求场景

借助write-good检测语法错误并高亮匹配文本(示例简化为查找"Hello"并高亮),当前尝试代码如下:

import {useLexicalComposerContext} from "@lexical/react/LexicalComposerContext";
import {$createRangeSelection, $getRoot, $isParagraphNode, ParagraphNode} from "lexical";


const HighlightSearchButton = () => {
    const [editor] = useLexicalComposerContext();

    const handleClick = async () => {
        editor.update(() => {
            const searchStr = 'Hello';
            const regex = new RegExp(searchStr, 'gi')
            const children = $getRoot().getChildren();
            for (const child of children) {
                if (!$isParagraphNode(child)) continue;
                const paragraphNode = child as ParagraphNode;
                const text = child.getTextContent();
                const indexes = [];
                let result;
                while (result = regex.exec(text)) {
                    indexes.push(result.index);
                }
                for (const index of indexes) {
                    const selection = $createRangeSelection();
                    selection.anchor.key = paragraphNode.getKey(),
                    selection.anchor.offset = index,
                    selection.focus.key = paragraphNode.getKey(),
                    selection.focus.offset = index + searchStr.length
                    // Note: This makes the entire paragraph bold
                    selection.formatText('bold'); // Note: actually want to apply a css style
                }
            }
        });
    };

    return <button onClick={handleClick}>Highlight Search</button>
};

export default HighlightSearchButton;

遇到的问题

  • 使用$createRangeSelection时,指定段落节点key后无法正确定位文本的锚点/焦点偏移
  • 通过getTextContent()获取段落全文后,无法映射选择范围到对应的文本节点
  • 段落中存在多格式(粗体、斜体等)文本节点时,无法跟踪节点状态并正确高亮

解决方案

1. 自定义高亮标记节点

Lexical推荐通过自定义节点实现样式高亮,而非直接使用内置格式工具。先创建一个自定义HighlightNode:

import { TextNode } from 'lexical';

export class HighlightNode extends TextNode {
  static getType() {
    return 'highlight';
  }

  static clone(node) {
    return new HighlightNode(node.__text, node.__key);
  }

  createDOM(config) {
    const dom = super.createDOM(config);
    dom.style.backgroundColor = '#ffff00'; // 自定义高亮背景色
    return dom;
  }

  exportJSON() {
    return {
      ...super.exportJSON(),
      type: 'highlight',
    };
  }
}

// 注册节点到编辑器配置
const editorConfig = {
  nodes: [HighlightNode],
  // 其他编辑器配置项...
};

2. 遍历文本节点精准定位匹配范围

不再依赖段落的getTextContent(),改为递归遍历每个文本节点,维护全局偏移量来匹配搜索范围:

const handleClick = async () => {
  editor.update(() => {
    const searchStr = 'Hello';
    const regex = new RegExp(searchStr, 'gi');
    let globalOffset = 0;

    // 递归遍历所有节点,定位文本节点
    const traverseNodes = (node) => {
      if (node.isTextNode()) {
        const text = node.getTextContent();
        let match;
        // 在当前文本节点内查找匹配项
        while ((match = regex.exec(text)) !== null) {
          const nodeStart = match.index;
          const nodeEnd = nodeStart + searchStr.length;

          // 创建范围选择并替换为高亮节点
          const selection = $createRangeSelection();
          selection.setStart(node, nodeStart);
          selection.setEnd(node, nodeEnd);
          selection.replaceWith(new HighlightNode(match[0]));
        }
        globalOffset += text.length;
      } else if (node.getChildren) {
        node.getChildren().forEach(traverseNodes);
      }
    };

    $getRoot().getChildren().forEach(traverseNodes);
  });
};

3. 处理跨格式节点的匹配

当匹配范围跨越多个已拆分的格式节点时,需要拆分节点并逐个转换为高亮节点:

// 处理跨节点匹配的工具函数
const handleCrossNodeMatch = (startNode, startOffset, endNode, endOffset) => {
  // 拆分起始节点,分离出匹配起始部分
  const [_, highlightStart] = startNode.splitText(startOffset);
  // 拆分结束节点,分离出匹配结束部分
  const [highlightEnd, __] = endNode.splitText(endOffset);
  
  // 转换起始和结束节点为高亮类型
  highlightStart.setType('highlight');
  highlightEnd.setType('highlight');
  
  // 遍历中间节点,统一转换为高亮类型
  let currentNode = highlightStart.getNextSibling();
  while (currentNode !== highlightEnd) {
    if (currentNode.isTextNode()) {
      currentNode.setType('highlight');
    }
    currentNode = currentNode.getNextSibling();
  }
};

4. 实时高亮:使用Node Transforms

如果需要在文本输入时实时检测并高亮(比如实时语法检查),可以注册Node Transforms监听文本变化:

import { $isTextNode } from 'lexical';
import { HighlightNode } from './HighlightNode';

editor.registerNodeTransform(TextNode, (node) => {
  if ($isTextNode(node)) {
    const text = node.getTextContent();
    const regex = new RegExp('Hello', 'gi');
    let match;
    while ((match = regex.exec(text)) !== null) {
      const start = match.index;
      const end = start + match[0].length;
      // 拆分文本节点,替换匹配部分为高亮节点
      const [before, highlightSegment, after] = node.splitText(start, end);
      highlightSegment.replace(new HighlightNode(highlightSegment.getTextContent()));
    }
  }
});

关键注意事项

  • Lexical的文本偏移是基于单个文本节点的,而非整个段落,避免直接用段落偏移定位
  • 自定义节点是实现自定义样式高亮的标准方案,比formatText更灵活可控
  • 递归遍历节点时维护全局偏移量,确保匹配位置精准对应到文本节点

内容的提问来源于stack exchange,提问作者OrderAndChaos

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 21:35:18