富文本编辑器展示和编辑的是HTML。如果业务需要的文本格式不是HTML,就需要做双向转换。 双向转换的方式有2种:
- 使用正则,将文本字符串转为HTML字符串
这个方式简单,但是不能处理复杂的文本格式。遇到特殊符号时,正则表达式容易错误地将文本截断,导致显示的内容不全。
- 使用AST,将文本字符串转为AST树
这个方式相对复杂一些,但更健壮。通过设计合理的AST结构,可以准确地表示文本中的各种元素(如段落、公式、图片等),并且在转换过程中不容易出错。
举个例子:有一个文本字符串:
这是一个含有latex公式{{}}的字符串。这是一个含有图片[[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"节点,并补充默认的style、alt和href属性。 - 还原逻辑:从 AST 转回字符串时,重新拼接为
[[src]]格式。
- 字符串特征:使用
转换算法设计
1. 文本字符串转 AST
将带有特殊标记的纯文本字符串解析成 wangEditor 兼容的 AST 数组。
核心处理步骤
- 空值与边界处理:如果传入的字符串为空,直接返回一个包含空文本的默认段落节点。
- 段落切分:按照
\n将字符串切分为多个段落文本(paragraphs)。 - 节点解析(正则匹配):
- 遍历每个段落文本,若为空则直接返回空段落。
- 根据预设规则对段落进行切割,分离出公式、图片和普通文本片段。
- 生成 AST 子节点:
- 公式片段:判断是否以
{{开头且}}结尾。截取内部内容,并去除可能的$包裹符,最终生成type: "formula"节点。 - 图片片段:判断是否以
[[开头且]]结尾。截取内部内容作为src,生成type: "image"节点。 - 普通文本:未匹配特殊语法的片段直接生成文本节点
{ text: part }。
- 公式片段:判断是否以
- 子节点兜底处理:如果解析后没有任何子节点,主动推入一个
{ 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 结构拍平并还原成约定的纯文本格式。
核心处理步骤
- 类型校验:确保传入的 AST 数据结构为数组。
- 深度优先遍历(递归解析):定义内部函数
nodeToString处理任意类型的节点:- 公式节点:当
node.type === "formula"时,提取node.value,并拼接成{{$value$}}格式返回。 - 图片节点:当
node.type === "image"时,提取node.src,拼接成[[src]]格式返回。 - 文本节点:如果节点存在
text属性,说明是叶子文本节点,直接返回node.text。 - 包含子节点的节点(如段落):如果存在
children数组,则递归处理所有子节点,并将结果拼接(join(""))。
- 公式节点:当
- 段落还原:遍历最外层的 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");
}
运行效果
优点:
- 支持复杂的文本格式,如公式、图片等;支持特殊字符。
- 易于扩展,可以添加新的节点类型(如表格、列表等),而不用大量修改业务代码。
- 与 wangEditor 插件(如 formula 插件)无缝集成,无需修改插件代码。