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
)。 -
源码:
phpList<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); // 处理列表嵌套。 }
-
执行过程:
-
转换操作 :
InsertOpsConverter.convert
将_rawDeltaOps
转换为DeltaInsertOp
对象,仅保留insert
操作。- 输入:
[{"insert": "作"}, ...]
- 输出:
List<DeltaInsertOp>
,每个操作包含insert
值和attributes
。
- 输入:
-
配对块 :
Grouper.pairOpsWithTheirBlock
将操作与其块上下文配对。- 例如,
[{"insert": "作"}, {"insert": "家助", ...}, {"insert": "手"}, {"insert": "\n", "attributes": {"header": 1}}]
配对为一个块,"\n"
是块终止符。 - 输出:
List
包含块配对,如[Block(op4, [op1, op2, op3])]
.
- 例如,
-
分组相同样式块 :
Grouper.groupConsecutiveSameStyleBlocks
将连续的相同样式块分组。- 测试数据中,每个块(header 1、header 2、header 3、paragraph)样式不同,无需合并。
- 输出:
List
包含[BlockGroup(header 1), BlockGroup(header 2), BlockGroup(header 3), BlockGroup(paragraph)]
。
-
合并块 :
Grouper.reduceConsecutiveSameStyleBlocksToOne
合并连续块(测试数据无连续块)。 -
表格分组 :
TableGrouper().group
处理表格操作(测试数据无表格)。 -
列表嵌套 :
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"}]
- BlockGroup(header 1):
-
-
步骤 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:渲染块
-
源码:
scssString 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
-
作用:渲染内联操作。
-
源码:
kotlinString 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:
欢迎各位大佬
。
- 对于 header 1 的内联:
_renderInline
-
作用:渲染单个内联操作。
-
源码:
scssString _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
方法:scssFuture<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 转换逻辑,结合测试数据展示了数据流向和输出结构。欢迎高级开发者交流优化方案!