还在用正则解析富文本?教你用 AST 优雅实现 wangEditor 数据双向转换

0 阅读1分钟

富文本编辑器展示和编辑的是HTML。如果业务需要的文本格式不是HTML,就需要做双向转换。 双向转换的方式有2种:

  1. 使用正则,将文本字符串转为HTML字符串

这个方式简单,但是不能处理复杂的文本格式。遇到特殊符号时,正则表达式容易错误地将文本截断,导致显示的内容不全。

  1. 使用AST,将文本字符串转为AST树

这个方式相对复杂一些,但更健壮。通过设计合理的AST结构,可以准确地表示文本中的各种元素(如段落、公式、图片等),并且在转换过程中不容易出错。

举个例子:有一个文本字符串:

这是一个含有latex公式{{5500mathrmmm5500\\mathrm{mm}}}的字符串。这是一个含有图片[[example.com/image.png]]…

我们约定:{{$$}}中的内容需要解析成latex公式,[[]]中的内容需要解析成图片。

AST结构设计

根据wangEditor-next的 官方文档源码,常用的节点类型有以下几种:

类型说明
paragraph段落
image图片
table表格

如果内置的节点类型还不够用,可以自定义扩展新功能。也可以用 @wangeditor-next/plugin-formula 插件的formula节点类型。

我们设计一个AST树,来表示这个文本字符串。

  • 段落(Paragraph):文本字符串中使用换行符 \n 进行段落划分。在 AST 中,每一个通过换行符分割出的文本行,都会被转换为一个 type: "paragraph" 的块级节点。
  • 数学公式(Formula)
    • 字符串特征:使用 {{...}} 包裹的内容表示公式。
    • 转换逻辑:在转换为 AST 时,需要剥离外层的 {{}}。此外,如果内部还使用了 LaTeX 常用的 $ 包裹(如 {{$...$}}),也会将首尾的 $ 去除,因为 wangEditor 的 formula 插件只接受内部真实的 LaTeX 语法。在 AST 中对应 type: "formula" 节点,核心数据存放在 value 字段。
    • 还原逻辑:从 AST 转回字符串时,固定在公式内容两端补上 $,即输出格式为 {{$...$}}
  • 图片(Image)
    • 字符串特征:使用 [[...]] 包裹的内容表示图片,内部内容为图片的地址(src)。
    • 转换逻辑:转换为 AST 时,剥离 [[]],将提取出的地址赋给节点的 src 属性,对应 type: "image" 节点,并补充默认的 stylealthref 属性。
    • 还原逻辑:从 AST 转回字符串时,重新拼接为 [[src]] 格式。

转换算法设计

1. 文本字符串转 AST

将带有特殊标记的纯文本字符串解析成 wangEditor 兼容的 AST 数组。

核心处理步骤

  1. 空值与边界处理:如果传入的字符串为空,直接返回一个包含空文本的默认段落节点。
  2. 段落切分:按照 \n 将字符串切分为多个段落文本(paragraphs)。
  3. 节点解析(正则匹配)
    • 遍历每个段落文本,若为空则直接返回空段落。
    • 根据预设规则对段落进行切割,分离出公式、图片和普通文本片段。
  4. 生成 AST 子节点
    • 公式片段:判断是否以 {{ 开头且 }} 结尾。截取内部内容,并去除可能的 $ 包裹符,最终生成 type: "formula" 节点。
    • 图片片段:判断是否以 [[ 开头且 ]] 结尾。截取内部内容作为 src,生成 type: "image" 节点。
    • 普通文本:未匹配特殊语法的片段直接生成文本节点 { text: part }
  5. 子节点兜底处理:如果解析后没有任何子节点,主动推入一个 { text: "" } 以防止编辑器报错。最后将子节点数组包裹在 type: "paragraph" 中返回。

代码实现

export function stringToAst(str) {
  if (!str) return [{ type: "paragraph", children: [{ text: "" }] }];

  const paragraphs = str.split("\n");
  const ast = paragraphs.map((pText) => {
    if (!pText) {
      return { type: "paragraph", children: [{ text: "" }] };
    }

    const parts = pText.split(/(\{\{.*?\}\}|\[\[.*?\]\])/g);
    const children = parts.filter(Boolean).map((part) => {
      if (part.startsWith("{{") && part.endsWith("}}")) {
        let val = part.slice(2, -2);
        // 如果外层还包裹了 $...$,需要把它去掉,因为 WangEditor 的 formula 插件只接受内部真实的 latex 语法
        if (val.startsWith("$") && val.endsWith("$")) {
          val = val.slice(1, -1);
        }
        return {
          type: "formula",
          value: val,
          children: [{ text: "" }],
        };
      }
      if (part.startsWith("[[") && part.endsWith("]]")) {
        return {
          type: "image",
          src: part.slice(2, -2),
          style: { width: "", height: "" },
          alt: "",
          href: "",
          children: [{ text: "" }],
        };
      }
      return { text: part };
    });

    if (children.length === 0) {
      children.push({ text: "" });
    }

    return {
      type: "paragraph",
      children,
    };
  });

  return ast;
}

2. AST 转文本字符串

将编辑器产生的 AST 结构拍平并还原成约定的纯文本格式。

核心处理步骤

  1. 类型校验:确保传入的 AST 数据结构为数组。
  2. 深度优先遍历(递归解析):定义内部函数 nodeToString 处理任意类型的节点:
    • 公式节点:当 node.type === "formula" 时,提取 node.value,并拼接成 {{$value$}} 格式返回。
    • 图片节点:当 node.type === "image" 时,提取 node.src,拼接成 [[src]] 格式返回。
    • 文本节点:如果节点存在 text 属性,说明是叶子文本节点,直接返回 node.text
    • 包含子节点的节点(如段落):如果存在 children 数组,则递归处理所有子节点,并将结果拼接(join(""))。
  3. 段落还原:遍历最外层的 AST 节点(通常都是段落),将每个节点转换出的字符串通过换行符 \n 连接起来,生成最终的完整字符串。

代码实现

export function astToString(ast) {
  if (!Array.isArray(ast)) return "";

  function nodeToString(node) {
    if (node.type === "formula") {
      // 转回字符串时,重新补上 $...$ 符号
      return `{{$${node.value || ""}$}}`;
    }
    if (node.type === "image") {
      return `[[${node.src || ""}]]`;
    }
    if (node.text !== undefined) {
      return node.text;
    }
    if (Array.isArray(node.children)) {
      return node.children.map(nodeToString).join("");
    }
    return "";
  }

  return ast.map(nodeToString).join("\n");
}

运行效果

20260406_221037_image.png

优点:

  1. 支持复杂的文本格式,如公式、图片等;支持特殊字符。
  2. 易于扩展,可以添加新的节点类型(如表格、列表等),而不用大量修改业务代码。
  3. 与 wangEditor 插件(如 formula 插件)无缝集成,无需修改插件代码。