4.1 引言
本文将深入解析 HtmlToDelta
类中的核心方法 convert
和 nodeToOperation
,这些方法定义了从 HTML 到 Delta 的转换逻辑。我将基于测试数据 <h1>作<span class="foreshadows" data-uuid="">家助</span>手</h1><h2>大<span style="background-color: var(--warning1-25);">前端</span>开发</h2><h3>王金山</h3><p>欢迎各位大佬</p>
,逐行分析代码执行过程,展示数据流向和输出结构,适合高级开发者深入理解和优化
4.2 核心方法解析
4.2.1 convert
方法解析
convert
方法是 HTML 到 Delta 转换的入口,负责将 HTML 字符串转换为 Delta 对象。以下是源码及注释:
ini
Delta convert(String htmlText) {
// 预处理 HTML 字符串:按换行符分割,每行左侧修剪空白,重新连接,并移除所有换行符。
final parsedText = htmlText
.split('\n')
.map((e) => e.trimLeft())
.join()
.removeAllNewLines;
final Delta delta = Delta(); // 初始化空 Delta 对象,用于存储转换结果。
// 解析 HTML 为 DOM 文档,若 replaceNormalNewLinesToBr 为 true,则将换行符替换为 <br>。
final dom.Document $document = dparser.parse(replaceNormalNewLinesToBr ? parsedText.transformNewLinesToBrTag : parsedText);
final dom.Element? $body = $document.body; // 获取 <body> 元素。
final dom.Element? $html = $document.documentElement; // 获取 <html> 元素。
// 确定需要处理的节点列表:优先 <body> 的子节点,其次 <html> 的子节点,最后文档根节点。
final List<dom.Node> nodesToProcess = $body?.nodes ?? $html?.nodes ?? $document.nodes;
for (int i = 0; i < nodesToProcess.length; i++) {
var node = nodesToProcess[i]; // 获取当前节点。
// 若节点是元素且存在自定义块,则检查是否匹配自定义块。
if (customBlocks != null && customBlocks!.isNotEmpty && node is dom.Element) {
for (var customBlock in customBlocks!) {
if (customBlock.matches(node)) {
// 使用自定义块转换逻辑生成操作并插入 Delta。
final operations = customBlock.convert(node);
operations.forEach((Operation op) {
delta.insert(op.data, op.attributes);
});
continue; // 匹配后跳过后续处理。
}
}
}
final nextNode = nodesToProcess.elementAtOrNull(i + 1); // 获取下一个节点。
final nextIsBlock = nextNode is dom.Element ? nextNode.isBlock : false; // 判断下一个节点是否为块级元素。
final List<Operation> operations = nodeToOperation(node, htmlToOp, nextIsBlock); // 将当前节点转换为操作列表。
if (operations.isNotEmpty) {
// 将操作插入 Delta。
for (final op in operations) {
delta.insert(op.data, op.attributes);
}
}
// 根据 shouldInsertANewLine 判断是否插入换行符。
final shouldInsertNewLine = shouldInsertANewLine?.call(node is dom.Element ? node.localName ?? 'no-localname' : 'text-node');
if (shouldInsertNewLine != null && shouldInsertNewLine) {
delta.insert('\n'); // 若需插入换行符。
}
}
// 确保最后一个操作为换行符,或若有属性但非换行符,则插入换行。
final lastOpdata = delta.last;
final bool lastDataIsNotNewLine = lastOpdata.data.toString() != '\n';
final bool hasAttributes = lastOpdata.attributes != null;
if (lastDataIsNotNewLine && hasAttributes || lastDataIsNotNewLine || !lastDataIsNotNewLine && hasAttributes) {
delta.insert('\n');
}
return delta; // 返回转换后的 Delta 对象。
}
执行过程与数据流向
- 输入:
css
htmlText = "<h1>作<span class='foreshadows' data-uuid=''>家助</span>手</h1>
<h2>大<span style='background-color: var(--warning1-25);'>前端</span>开发</h2>
<h3>王金山</h3><p>欢迎各位大佬</p>"
-
预处理:
parsedText
保持原样(无换行符)。
-
DOM 解析:
-
$document
解析为 DOM 树,$body?.nodes
包含:<h1>
(子节点:文本 "作",<span>
, 文本 "手")<h2>
(子节点:文本 "大",<span>
, 文本 "开发")<h3>
(子节点:文本 "王金山")<p>
(子节点:文本 "欢迎各位大佬")
-
nodesToProcess
为上述顶级节点列表。
-
-
节点遍历:
-
<h1>
:-
递归处理子节点:
- 文本 "作" →
nodeToOperation
生成{insert: "作"}
。 <span class='foreshadows' data-uuid=''>
→ 若customBlocks
匹配(如CustomSpan
),生成{insert: "家助", attributes: {custom: "foreshadows", uuid: ""}}
;否则,htmlToOp
处理为{insert: "家助"}
。- 文本 "手" →
{insert: "手"}
。
- 文本 "作" →
-
下一个节点
<h2>
是块级,nextIsBlock = true
,htmlToOp
添加{insert: "\n", attributes: {header: 1}}
。
-
-
<h2>
:- 文本 "大" →
{insert: "大"}
。 <span style="background-color: var(--warning1-25);">
→htmlToOp
识别样式,生成{insert: "前端", attributes: {background: "var(--warning1-25)"}}
。- 文本 "开发" →
{insert: "开发"}
。 - 下一个节点
<h3>
是块级,添加{insert: "\n", attributes: {header: 2}}
。
- 文本 "大" →
-
<h3>
:- 文本 "王金山" →
{insert: "王金山"}
。 - 下一个节点
<p>
是块级,添加{insert: "\n", attributes: {header: 3}}
。
- 文本 "王金山" →
-
<p>
:- 文本 "欢迎各位大佬" →
{insert: "欢迎各位大佬"}
。 - 下一个节点无,
nextIsBlock = false
,htmlToOp
添加{insert: "\n"}
。
- 文本 "欢迎各位大佬" →
-
-
末尾优化:
- 最后一个操作
{insert: "\n"}
,满足要求,无需额外插入。
- 最后一个操作
-
输出:
-
若
customBlocks
包含CustomSpan
:python[ {"insert": "作"}, {"insert": "家助", "attributes": {"custom": "foreshadows", "uuid": ""}}, {"insert": "手"}, {"insert": "\n", "attributes": {"header": 1"}}, {"insert": "大"}, {"insert": "前端", "attributes": {"background": "var(--warning1-25)"}}, {"insert": "开发"}, {"insert": "\n", "attributes": {"header": 2"}}, {"insert": "王金山"}, {"insert": "\n", "attributes": {"header": 3"}}, {"insert": "欢迎各位大佬"}, {"insert": "\n"} ]
-
若无
customBlocks
,<span class="foreshadows">
未特殊处理,uuid
未映射。
-
数据流向
- 输入 :
htmlText
→ 预处理 →parsedText
。 - DOM 解析 :
dparser.parse
→$document
→nodesToProcess
。 - 节点转换 :
nodesToProcess
遍历 →nodeToOperation
→operations
→delta.insert
。 - 优化 :末尾添加
\n
→ 最终delta
。
4.2.2 nodeToOperation
方法解析
nodeToOperation
方法将单个 DOM 节点转换为 Delta 操作列表,是转换的核心逻辑。以下是源码及注释:
scss
List<Operation> nodeToOperation(
dom.Node node,
HtmlOperations htmlToOp, [
bool nextIsBlock = false,
]) {
List<Operation> operations = []; // 初始化空的操作列表。
if (node is dom.Text) {
operations.add(Operation.insert(node.text)); // 若为文本节点,直接插入文本内容。
}
if (node is dom.Element) {
if (blackNodesList.contains(node.localName)) {
if (nextIsBlock) operations.add(Operation.insert('\n')); // 若为黑名单标签且下一个是块级元素,插入换行。
operations.add(Operation.insert(node.text)); // 将黑名单标签内容作为纯文本插入。
return operations; // 返回操作列表。
}
List<Operation> ops = htmlToOp.resolveCurrentElement(node); // 使用 htmlToOp 将元素转换为操作。
operations.addAll(ops); // 将转换后的操作添加到列表。
if (nextIsBlock) operations.add(Operation.insert('\n')); // 若下一个是块级元素,插入换行。
}
return operations; // 返回操作列表。
}
执行过程与数据流向
-
输入 :
node
(DOM 节点),htmlToOp
(操作映射规则),nextIsBlock
(是否为块级元素)。 -
处理逻辑:
-
文本节点(如 "作"):
operations = [{insert: "作"}]
-
元素节点 (如
<h1>
):-
若
blackNodesList
包含h1
(假设为空),跳过检查。 -
ops = htmlToOp.resolveCurrentElement(node)
:<h1>
→ops = [{insert: "\n", attributes: {header: 1}}]
(假设htmlToOp
定义)。<span style="background-color: var(--warning1-25);">
→ops = [{insert: "前端", attributes: {background: "var(--warning1-25)"}}]
。
-
nextIsBlock = true
(如<h1>
后是<h2>
),添加{insert: "\n"}
。
-
-
-
输出 :
operations
列表(如[{insert: "作"}, {insert: "\n", attributes: {header: 1}}]
)。 -
数据流向:
node
→ 类型检查 →htmlToOp.resolveCurrentElement
→operations
→ 返回。
测试数据应用
<h1>
子节点 "作" :operations = [{insert: "作"}]
<span>
:ops = [{insert: "家助", attributes: {custom: "foreshadows", uuid: ""}}]
(若customBlocks
匹配)。<h1>
结束 :nextIsBlock = true
,operations.add({insert: "\n", attributes: {header: 1}})
。
4.2.3 性能优化与扩展
- 性能 :
nodesToProcess
遍历可优化为迭代器,减少内存占用。 - 扩展 :
htmlToOp.resolveCurrentElement
可支持更多 CSS 属性(如font-size
)。
4.3 总结
本章深入解析了 convert
和 nodeToOperation
方法,展示了 HTML 到 Delta 转换的底层逻辑和数据流向。下一部分(下篇)将分析 Delta 到 HTML 转换。欢迎高级开发者交流优化方案!