第四章(上):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 转换。欢迎高级开发者交流优化方案!

相关推荐
程序员老刘1 小时前
一杯奶茶钱,PicGo + 阿里云 OSS 搭建永久稳定的个人图床
flutter·markdown
奋斗的小青年!!5 小时前
OpenHarmony Flutter 拖拽排序组件性能优化与跨平台适配指南
flutter·harmonyos·鸿蒙
小雨下雨的雨6 小时前
Flutter 框架跨平台鸿蒙开发 —— Stack 控件之三维层叠艺术
flutter·华为·harmonyos
行者967 小时前
OpenHarmony平台Flutter手风琴菜单组件的跨平台适配实践
flutter·harmonyos·鸿蒙
小雨下雨的雨8 小时前
Flutter 框架跨平台鸿蒙开发 —— Flex 控件之响应式弹性布局
flutter·ui·华为·harmonyos·鸿蒙系统
cn_mengbei8 小时前
Flutter for OpenHarmony 实战:CheckboxListTile 复选框列表项详解
flutter
cn_mengbei9 小时前
Flutter for OpenHarmony 实战:Switch 开关按钮详解
flutter
奋斗的小青年!!9 小时前
OpenHarmony Flutter实战:打造高性能订单确认流程步骤条
flutter·harmonyos·鸿蒙
Coder_Boy_9 小时前
Flutter基础介绍-跨平台移动应用开发框架
spring boot·flutter
cn_mengbei9 小时前
Flutter for OpenHarmony 实战:Slider 滑块控件详解
flutter