第四章(下):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

相关推荐
LawrenceLan15 小时前
Flutter 零基础入门(九):构造函数、命名构造函数与 this 关键字
开发语言·flutter·dart
一豆羹16 小时前
macOS 环境下 ADB 无线调试连接失败、Protocol Fault 及端口占用的深度排查
flutter
行者9616 小时前
OpenHarmony上Flutter粒子效果组件的深度适配与实践
flutter·交互·harmonyos·鸿蒙
行者9619 小时前
Flutter与OpenHarmony深度集成:数据导出组件的实战优化与性能提升
flutter·harmonyos·鸿蒙
小雨下雨的雨19 小时前
Flutter 框架跨平台鸿蒙开发 —— Row & Column 布局之轴线控制艺术
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨19 小时前
Flutter 框架跨平台鸿蒙开发 —— Center 控件之完美居中之道
flutter·ui·华为·harmonyos·鸿蒙
小雨下雨的雨20 小时前
Flutter 框架跨平台鸿蒙开发 —— Icon 控件之图标交互美学
flutter·华为·交互·harmonyos·鸿蒙系统
小雨下雨的雨20 小时前
Flutter 框架跨平台鸿蒙开发 —— Placeholder 控件之布局雏形美学
flutter·ui·华为·harmonyos·鸿蒙系统
行者9621 小时前
OpenHarmony Flutter弹出菜单组件深度实践:从基础到高级的完整指南
flutter·harmonyos·鸿蒙
前端不太难21 小时前
Flutter / RN / iOS,在长期维护下的性能差异本质
flutter·ios