第四章(下):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 操作分组为不同类型的组(如 ListGroupTableGroupBlockGroupInlineGroup)。

    • 源码

      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>

数据流向

  • 输入_rawDeltaOpsgetGroupedOpsList<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 操作列表。

  • 处理

    • 分组:getGroupedOpsList<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

相关推荐
九丝城主5 小时前
2025使用VM虚拟机安装配置Macos苹果系统下Flutter开发环境保姆级教程--上篇
服务器·flutter·macos·vmware
瓜子三百克10 小时前
七、性能优化
flutter·性能优化
恋猫de小郭18 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
小蜜蜂嗡嗡1 天前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
瓜子三百克1 天前
十、高级概念
flutter
帅次2 天前
Objective-C面向对象编程:类、对象、方法详解(保姆级教程)
flutter·macos·ios·objective-c·iphone·swift·safari
小蜜蜂嗡嗡2 天前
flutter flutter_vlc_player播放视频设置循环播放失效、初始化后获取不到视频宽高
flutter
孤鸿玉2 天前
[Flutter小技巧] Row中widget高度自适应的几种方法
flutter
bawomingtian1232 天前
FlutterView 源码解析
flutter
Zender Han2 天前
Flutter 进阶:实现带圆角的 CircularProgressIndicator
flutter