第四章(上):HTML 到 Delta 转换的核心方法解析

4.1 引言

本文将深入解析 HtmlToDelta 类中的核心方法 convertnodeToOperation,这些方法定义了从 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 = truehtmlToOp 添加 {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 = falsehtmlToOp 添加 {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$documentnodesToProcess
  • 节点转换nodesToProcess 遍历 → nodeToOperationoperationsdelta.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.resolveCurrentElementoperations → 返回。

测试数据应用

  • <h1> 子节点 "作"operations = [{insert: "作"}]
  • <span>ops = [{insert: "家助", attributes: {custom: "foreshadows", uuid: ""}}](若 customBlocks 匹配)。
  • <h1> 结束nextIsBlock = trueoperations.add({insert: "\n", attributes: {header: 1}})

4.2.3 性能优化与扩展

  • 性能nodesToProcess 遍历可优化为迭代器,减少内存占用。
  • 扩展htmlToOp.resolveCurrentElement 可支持更多 CSS 属性(如 font-size)。

4.3 总结

本章深入解析了 convertnodeToOperation 方法,展示了 HTML 到 Delta 转换的底层逻辑和数据流向。下一部分(下篇)将分析 Delta 到 HTML 转换。欢迎高级开发者交流优化方案!

相关推荐
阅文作家助手开发团队_山神4 小时前
第四章(下) Delta 到 HTML 转换:块级与行内样式渲染深度解析
flutter
MaoJiu4 小时前
Flutter造轮子系列:flutter_permission_kit
flutter·swiftui
阅文作家助手开发团队_山神8 小时前
第四章(下):Delta 到 HTML 转换的核心方法解析
flutter
xiaoyan201511 小时前
flutter3.32+deepseek+dio+markdown搭建windows版流式输出AI模板
flutter·openai·deepseek
stringwu12 小时前
Flutter高效开发利器:Riverpod框架简介及实践指南
flutter
耳東陈12 小时前
Flutter开箱即用一站式解决方案2.0-全局无需Context的Toast
flutter
阅文作家助手开发团队_山神1 天前
第三章: Flutter-quill 数据格式Delta
flutter
阅文作家助手开发团队_山神1 天前
第二章:Document 模块与 DOM 树详解
flutter
程序员老刘1 天前
20%的选择决定80%的成败
flutter·架构·客户端