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

4.1 引言

本章将深入解析 vsc_quill_delta_to_html 包中的 QuillDeltaToHtmlConverter 类源码,结合测试数据对应的 Delta 操作,逐行分析核心方法 convert 的实现逻辑,展示数据流向和输出结构,


4.2 QuillDeltaToHtmlConverter 类的源码解析

QuillDeltaToHtmlConverter 类是 Delta 到 HTML 转换的核心入口,负责将 Delta 操作序列转换为 HTML 字符串。以下是源码及逐行中文注释,结合测试数据展示执行过程和数据流向。

4.2.1 核心方法 convert 的源码解析

convert 方法是 Delta 到 HTML 转换的入口,负责将 Delta 操作序列转换为 HTML 字符串。以下是源码及逐行注释:

scss 复制代码
/// 将 Delta 操作转换为 HTML 字符串。
String convert() {
  final groups = getGroupedOps(); // 获取分组后的操作列表。
  return groups.map((group) {
    if (group is ListGroup) { // 如果是列表组,渲染为 HTML 列表。
      return _renderWithCallbacks(GroupType.list, group, () => _renderList(group));
    }
    if (group is TableGroup) { // 如果是表格组,渲染为 HTML 表格。
      return _renderWithCallbacks(GroupType.table, group, () => _renderTable(group));
    }
    if (group is BlockGroup) { // 如果是块组(如标题、段落),渲染为块级 HTML。
      return _renderWithCallbacks(GroupType.block, group, () => renderBlock(group.op, group.ops));
    }
    if (group is BlotBlock) { // 如果是嵌入块,调用自定义渲染。
      return _renderCustom(group.op, null);
    }
    if (group is VideoItem) { // 如果是视频项,渲染为视频 HTML。
      return _renderWithCallbacks(GroupType.video, group, () {
        var converter = OpToHtmlConverter(group.op, _converterOptions);
        return converter.getHtml();
      });
    }
    // 内联组,渲染为内联 HTML。
    return _renderWithCallbacks(
        GroupType.inlineGroup, group, () => renderInlines((group as InlineGroup).ops, true));
  }).join(''); // 将所有组的 HTML 拼接为最终字符串。
}

执行过程与数据流向

  • 输入:测试数据的 Delta 操作:

    swift 复制代码
    [
      {"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"}
    ]
  • 步骤 1:调用 getGroupedOps

    • 作用 :将原始 Delta 操作分组为不同类型的组(如 ListGroup、TableGroup、BlockGroup、InlineGroup)。

    • 源码:

      php 复制代码
      List<TDataGroup> getGroupedOps() {
        var deltaOps = InsertOpsConverter.convert(_rawDeltaOps, _options.sanitizerOptions); // 将原始 Delta 操作转换为 DeltaInsertOp 对象。
        var pairedOps = Grouper.pairOpsWithTheirBlock(deltaOps); // 将操作与其块上下文配对。
        var groupedSameStyleBlocks = Grouper.groupConsecutiveSameStyleBlocks(
          pairedOps,
          blockquotes: _options.multiLineBlockquote ?? false,
          header: _options.multiLineHeader ?? false,
          codeBlocks: _options.multiLineCodeblock ?? false,
          customBlocks: _options.multiLineCustomBlock ?? false,
        ); // 将连续的相同样式块分组。
        var groupedOps = Grouper.reduceConsecutiveSameStyleBlocksToOne(groupedSameStyleBlocks); // 合并连续的相同样式块。
        groupedOps = TableGrouper().group(groupedOps); // 处理表格分组。
        return ListNester().nest(groupedOps); // 处理列表嵌套。
      }
    • 执行过程:

      1. 转换操作 :InsertOpsConverter.convert 将 _rawDeltaOps 转换为 DeltaInsertOp 对象,仅保留 insert 操作。

        • 输入:[{"insert": "作"}, ...]
        • 输出:List<DeltaInsertOp>,每个操作包含 insert 值和 attributes。
      2. 配对块 :Grouper.pairOpsWithTheirBlock 将操作与其块上下文配对。

        • 例如,[{"insert": "作"}, {"insert": "家助", ...}, {"insert": "手"}, {"insert": "\n", "attributes": {"header": 1}}] 配对为一个块,"\n" 是块终止符。
        • 输出:List 包含块配对,如 [Block(op4, [op1, op2, op3])].
      3. 分组相同样式块 :Grouper.groupConsecutiveSameStyleBlocks 将连续的相同样式块分组。

        • 测试数据中,每个块(header 1、header 2、header 3、paragraph)样式不同,无需合并。
        • 输出:List 包含 [BlockGroup(header 1), BlockGroup(header 2), BlockGroup(header 3), BlockGroup(paragraph)]。
      4. 合并块 :Grouper.reduceConsecutiveSameStyleBlocksToOne 合并连续块(测试数据无连续块)。

      5. 表格分组 :TableGrouper().group 处理表格操作(测试数据无表格)。

      6. 列表嵌套 :ListNester().nest 处理列表嵌套(测试数据无列表)。

    • 输出 :groups 包含四个 BlockGroup:

      • BlockGroup(header 1):[{"insert": "作"}, {"insert": "家助", ...}, {"insert": "手"}, {"insert": "\n", "attributes": {"header": 1}}]
      • BlockGroup(header 2):[{"insert": "大"}, {"insert": "前端", ...}, {"insert": "开发"}, {"insert": "\n", "attributes": {"header": 2}}]
      • BlockGroup(header 3):[{"insert": "王金山"}, {"insert": "\n", "attributes": {"header": 3}}]
      • BlockGroup(paragraph):[{"insert": "欢迎各位大佬"}, {"insert": "\n"}]
  • 步骤 2:遍历组并渲染

    • 逻辑 :对每个组调用 _renderWithCallbacks,根据组类型选择渲染方法。

    • 测试数据应用:

      • BlockGroup(header 1) :

        • 调用 _renderWithCallbacks(GroupType.block, group, () => renderBlock(group.op, group.ops))。
        • group.op 是 {"insert": "\n", "attributes": {"header": 1}}。
        • group.ops 是 [{"insert": "作"}, {"insert": "家助", ...}, {"insert": "手"}].
      • BlockGroup(header 2) :

        • 类似处理,group.op 是 {"insert": "\n", "attributes": {"header": 2}}。
      • BlockGroup(header 3) :

        • group.op 是 {"insert": "\n", "attributes": {"header": 3}}。
      • BlockGroup(paragraph) :

        • group.op 是 {"insert": "\n"}。
  • 步骤 3:渲染块

    • 源码:

      scss 复制代码
      String renderBlock(DeltaInsertOp bop, List<DeltaInsertOp> ops) {
        final converter = OpToHtmlConverter(bop, _converterOptions); // 创建操作转换器。
        final htmlParts = converter.getHtmlParts(); // 获取 HTML 标签部分。
        if (bop.isCodeBlock()) {
          return htmlParts.openingTag +
              encodeHtml(ops
                  .map((iop) => iop.isCustomEmbed() ? _renderCustom(iop, bop) : iop.insert.value)
                  .join('')) +
              htmlParts.closingTag; // 处理代码块。
        }
        final inlines = ops.map((op) => _renderInline(op, bop)).join(''); // 渲染内联操作。
        return htmlParts.openingTag + (inlines.isEmpty ? brTag : inlines) + htmlParts.closingTag; // 拼接块 HTML。
      }
    • 执行过程:

      • Header 1:

        • bop 是 {"insert": "\n", "attributes": {"header": 1}}。

        • converter.getHtmlParts() 返回 {openingTag: "<h1>", closingTag: "</h1>"}。

        • ops 是 [{"insert": "作"}, {"insert": "家助", ...}, {"insert": "手"}].

        • 调用 _renderInline:

          • {"insert": "作"} → "作"。
          • {"insert": "家助", "attributes": {"custom": "foreshadows", "uuid": ""}} → <span class="foreshadows" data-uuid="">家助</span>(假设 renderCustomWithCallback 定义)。
          • {"insert": "手"} → "手"。
        • 输出:<h1>作<span class="foreshadows" data-uuid="">家助</span>手</h1>。

      • Paragraph:

        • bop 是 {"insert": "\n"}。
        • converter.getHtmlParts() 返回 {openingTag: "<p>", closingTag: "</p>"}。
        • ops 是 [{"insert": "欢迎各位大佬"}].
        • 输出:<p>欢迎各位大佬</p>。
  • 步骤 4:拼接 HTML

    • 所有组的 HTML 拼接为:

      css 复制代码
      <h1>作<span class="foreshadows" data-uuid="">家助</span>手</h1>
      <h2>大<span style="background-color: var(--warning1-25);">前端</span>开发</h2>
      <h3>王金山</h3>
      <p>欢迎各位大佬</p>

数据流向

  • 输入 :_rawDeltaOps → getGroupedOps → List<TDataGroup>。
  • 处理:遍历组 → 调用渲染方法 → 生成 HTML 片段。
  • 输出:拼接后的 HTML 字符串。

4.2.2 其他关键方法解析

_renderWithCallbacks

  • 作用:包装渲染函数,允许在渲染前后调用回调。

  • 源码:

    javascript 复制代码
    _renderWithCallbacks(
      GroupType groupType,
      TDataGroup group,
      String Function() myRenderFn,
    ) {
      var html = _beforeRenderCallback?.call(groupType, group) ?? ''; // 调用渲染前回调。
      if (html.isEmpty) {
        html = myRenderFn(); // 执行渲染函数。
      }
      html = _afterRenderCallback?.call(groupType, html) ?? html; // 调用渲染后回调。
      return html; // 返回最终 HTML。
    }
  • 测试数据应用 :测试数据无自定义回调,myRenderFn 直接执行(如 renderBlock)。

renderInlines

  • 作用:渲染内联操作。

  • 源码:

    kotlin 复制代码
    String renderInlines(List<DeltaInsertOp> ops, [bool isInlineGroup = true]) {
      final opsLen = ops.length - 1;
      final html = ops.mapIndexed((i, op) {
        if (i > 0 && i == opsLen && op.isJustNewline()) {
          return ''; // 忽略最后一个换行符。
        }
        return _renderInline(op, null); // 渲染内联操作。
      }).join('');
      if (!isInlineGroup) {
        return html; // 非内联组直接返回。
      }
      final startParaTag = makeStartTag(_converterOptions.paragraphTag); // 获取段落开始标签。
      final endParaTag = makeEndTag(_converterOptions.paragraphTag); // 获取段落结束标签。
      if (html == brTag || _options.multiLineParagraph == true) {
        return startParaTag + html + endParaTag; // 多行段落处理。
      }
      return startParaTag +
          html.split(brTag).map((v) => v.isEmpty ? brTag : v).join(endParaTag + startParaTag) +
          endParaTag; // 处理多段落。
    }
  • 测试数据应用:

    • 对于 header 1 的内联:作<span class="foreshadows" data-uuid="">家助</span>手。
    • 对于 paragraph:欢迎各位大佬。

_renderInline

  • 作用:渲染单个内联操作。

  • 源码:

    scss 复制代码
    String _renderInline(DeltaInsertOp op, DeltaInsertOp? contextOp) {
      if (op.isCustomEmbed() || YwQuillFormatUtil.isYwCustomSpan(op)) {
        return _renderCustom(op, contextOp); // 处理自定义嵌入或自定义 span。
      }
      final converter = OpToHtmlConverter(op, _converterOptions); // 创建操作转换器。
      return converter.getHtml().replaceAll('\n', brTag); // 将换行符替换为 <br>。
    }
  • 测试数据应用:

    • {"insert": "家助", "attributes": {"custom": "foreshadows", "uuid": ""}} → <span class="foreshadows" data-uuid="">家助</span>。

4.2.3 异步解析的补充

当前源码是同步操作,但在实际应用中可能涉及异步场景:

  • 外部资源加载 :如 <img> 或 <video> 的 src 属性可能需要异步获取。

  • 大型 Delta 处理 :可通过 Future 包装 convert 方法:

    scss 复制代码
    Future<String> convertAsync() async {
      return await Future(() => convert());
    }

4.2.4 数据流向与输出结构

  • 输入:Delta 操作列表。

  • 处理:

    • 分组:getGroupedOps → List<TDataGroup>。
    • 渲染:遍历组 → 调用渲染方法 → 生成 HTML。
  • 输出:HTML 字符串。

示例输出

  • 输入 Delta:如上。

  • 输出 HTML:

    css 复制代码
    <h1>作<span class="foreshadows" data-uuid="">家助</span>手</h1>
    <h2>大<span style="background-color: var(--warning1-25);">前端</span>开发</h2>
    <h3>王金山</h3>
    <p>欢迎各位大佬</p>

4.2.5 优化与扩展建议

  • 性能优化:缓存分组结果,减少重复计算。
  • 样式扩展 :增强 OpToHtmlConverter 支持更多 CSS 属性。
  • 异步优化:支持异步渲染外部资源。

4.3 总结

本章通过源码解析,详细讲解了 QuillDeltaToHtmlConverter 类及其 convert 方法的 Delta 到 HTML 转换逻辑,结合测试数据展示了数据流向和输出结构。欢迎高级开发者交流优化方案!


Key Citations

相关推荐
曹小强wxx1 小时前
让 AI 写代码不“翻车”:用 Claude Code + Codex 给 Flutter 项目搭一套工程规范
flutter·ai编程·claude
西西学代码4 小时前
像素和颜色之间的转换(Flutter)
前端·javascript·flutter
曹小强wxx4 小时前
Flutter 局域网设备发现:UDP 广播 + mDNS 双路合并,以及靠谱的在线判定与断线重连
物联网·网络协议·flutter
FungLeo7 小时前
成为全栈·Flutter App 篇·移动端错误与限流:429、重试和写请求边界
flutter·限流·dart·dio·错误处理·成为全栈
恋猫de小郭7 小时前
Kotlin Toolchain 0.13,JetBrains 推荐 KMP 都切换到到全新工具链
android·前端·flutter
m0_738185827 小时前
Flutter 鸿蒙化实战:rive 适配 OpenHarmony,Rive 动画
flutter·华为·harmonyos·鸿蒙
Geek_Vison19 小时前
保险APP已经用了Flutter,还能接入小程序吗?
flutter·小程序·uni-app·mpaas·小程序容器
FungLeo19 小时前
成为全栈·Flutter App 篇·go_router 与四 Tab App Shell:保留导航状态、深链和登录回跳
flutter·移动开发·成为全栈·go_router·路由设计·深链接
m0_7381858220 小时前
Flutter 鸿蒙化实战:screen_protector 适配 OpenHarmony,防截屏/防泄露
flutter·华为·harmonyos·鸿蒙
梦想的颜色1 天前
【编程实战】AI 时代 APP 开发全栈硬核指南:技术选型 + AI 架构 + 模型落地全维度决策
python·flutter·react native·react.js·ai·桌面应用·milvus