我将详细解释 renderBlock
和 renderInlines
方法的实现原理,特别是 OpToHtmlConverter
如何将 Delta 操作转换为 HTML 标签。以下是对这两个方法的逐行分析:
1. renderBlock
方法详解
dart
@visibleForTesting
String renderBlock(DeltaInsertOp bop, List<DeltaInsertOp> ops) {
// 1. 创建块级转换器 - 核心转换逻辑
final converter = OpToHtmlConverter(bop, _converterOptions);
// 2. 获取块级标签的开闭部分
final htmlParts = converter.getHtmlParts();
// 3. 特殊处理:代码块
if (bop.isCodeBlock()) {
return htmlParts.openingTag + // <pre> 开始标签
encodeHtml(ops.map((iop) => // HTML 实体转义
iop.isCustomEmbed()
? _renderCustom(iop, bop) // 自定义嵌入处理
: iop.insert.value) // 普通文本内容
.join('')) + // 拼接所有内容
htmlParts.closingTag; // </pre> 结束标签
}
// 4. 处理非代码块的块级元素
final inlines = ops.map((op) => _renderInline(op, bop)).join('');
// 5. 组合块级标签和内联内容
return htmlParts.openingTag + // 块级开始标签(如 <h1>)
(inlines.isEmpty
? brTag // 空内容时添加换行符防止折叠
: inlines) + // 内联内容
htmlParts.closingTag; // 块级结束标签(如 </h1>)
}
关键点解析:
-
OpToHtmlConverter 核心作用:
-
将块级操作转换为对应的 HTML 标签
-
处理块级元素的属性和样式
-
示例转换:
dart// 输入:{ insert: "\n", attributes: { header: 1 } } // 输出:openingTag = '<h1>', closingTag = '</h1>'
-
-
代码块特殊处理:
- 直接拼接内容,不解析内联样式
- 使用
encodeHtml
防止代码中的 HTML 标签被渲染 - 保留所有空白和换行符
-
非代码块处理:
- 递归处理内部的内联操作
- 空内容时添加
<br/>
防止块级元素折叠
2. renderInlines
方法详解
dart
@visibleForTesting
String renderInlines(List<DeltaInsertOp> ops, [bool isInlineGroup = true]) {
// 1. 处理末尾的纯换行符
final opsLen = ops.length - 1;
final html = ops.mapIndexed((i, op) {
// 跳过最后一个纯换行操作
if (i > 0 && i == opsLen && op.isJustNewline()) {
return '';
}
// 转换单个内联操作
return _renderInline(op, null);
}).join(''); // 拼接所有内联内容
// 2. 非独立内联组直接返回(作为块级内容的一部分)
if (!isInlineGroup) {
return html;
}
// 3. 获取段落标签(默认为 <p>)
final startParaTag = makeStartTag(_converterOptions.paragraphTag);
final endParaTag = makeEndTag(_converterOptions.paragraphTag);
// 4. 特殊处理:单行段落或多行段落配置
if (html == brTag || _options.multiLineParagraph == true) {
// 整个内容包裹在单个段落中:<p>内容</p>
return startParaTag + html + endParaTag;
}
// 5. 多行段落处理:按换行符分割内容
return startParaTag +
html.split(brTag) // 按换行符分割
.map((v) => v.isEmpty
? brTag // 空行处理
: v) // 非空内容
.join(endParaTag + startParaTag) + // 段落分隔
endParaTag;
}
关键点解析:
-
末尾换行处理:
- 跳过文档末尾的纯换行符,避免多余空行
- 保留内容中间的换行符
-
段落包裹逻辑:
-
当内容只有一个换行时:
<p><br/></p>
-
多行内容处理:
dart// 输入: "Line1<br/>Line2<br/>Line3" // 输出: "<p>Line1</p><p>Line2</p><p>Line3</p>"
-
3. OpToHtmlConverter
转换原理
OpToHtmlConverter
是样式转换的核心类,它根据操作类型和属性生成 HTML 标签:
核心转换流程:
dart
class OpToHtmlConverter {
final DeltaInsertOp op;
final OpConverterOptions options;
OpHtmlParts getHtmlParts() {
// 1. 确定块级标签类型
final tag = _getTagName(); // h1, p, blockquote 等
// 2. 构建属性字符串
final attrs = _getAttributes(); // class, style 等
// 3. 返回开闭标签
return OpHtmlParts(
openingTag: '<$tag$attrs>',
closingTag: '</$tag>'
);
}
String getHtml() {
// 1. 内联样式转换
if (op.isInline()) {
final tag = _getInlineTag(); // span, strong 等
final attrs = _getAttributes();
final content = _getContent(); // 文本或嵌入内容
return '<$tag$attrs>$content</$tag>';
}
// 2. 嵌入内容转换
if (op.isEmbed()) {
return _convertEmbed();
}
// 3. 普通文本
return _escapeHtml(op.insert.value);
}
String _getAttributes() {
final attrs = StringBuffer();
// 处理 class 属性
if (op.attributes.className != null) {
attrs.write(' class="${op.attributes.className}"');
}
// 处理 style 属性
final styles = _getCssStyles();
if (styles.isNotEmpty) {
attrs.write(' style="$styles"');
}
// 处理其他属性(如链接的 target)
if (options.linkTarget != null && op.isLink()) {
attrs.write(' target="${options.linkTarget}"');
}
return attrs.toString();
}
String _getCssStyles() {
final styles = <String>[];
// 基本样式映射
if (op.isBold()) styles.add('font-weight:bold');
if (op.isItalic()) styles.add('font-style:italic');
if (op.isUnderline()) styles.add('text-decoration:underline');
if (op.isStrike()) styles.add('text-decoration:line-through');
// 颜色处理
if (op.attributes.color != null) {
styles.add('color:${op.attributes.color}');
}
if (op.attributes.background != null) {
styles.add('background-color:${op.attributes.background}');
}
// 字体处理
if (op.attributes.font != null) {
styles.add('font-family:${op.attributes.font}');
}
if (op.attributes.size != null) {
styles.add('font-size:${op.attributes.size}');
}
// 自定义样式处理
if (options.customCssStyles != null) {
final customStyles = options.customCssStyles!(op);
if (customStyles != null) {
styles.addAll(customStyles);
}
}
return styles.join(';');
}
}
转换示例:
-
块级标题:
dart// Delta 操作 DeltaInsertOp('\n', OpAttributes()..header = 1) // 转换结果 htmlParts.openingTag = '<h1>' htmlParts.closingTag = '</h1>'
-
加粗文本:
dart// Delta 操作 DeltaInsertOp('重要', OpAttributes()..bold = true) // 转换结果 '<strong>重要</strong>' // 或根据配置:'<span style="font-weight:bold">重要</span>'
-
带样式的链接:
dart// Delta 操作 DeltaInsertOp('访问网站', OpAttributes() ..link = 'https://example.com' ..color = 'blue' ..underline = true) // 转换结果 '<a href="https://example.com" target="_blank" style="color:blue;text-decoration:underline">访问网站</a>'
-
自定义块级元素:
dart// Delta 操作 DeltaInsertOp('\n', OpAttributes()..block = 'warning') // 转换结果 '<div class="warning">...</div>'
4. 样式转换规则详解
块级元素映射表
Delta 属性 | HTML 标签 | 示例输出 |
---|---|---|
header: 1 |
<h1> |
<h1>标题</h1> |
header: 2 |
<h2> |
<h2>子标题</h2> |
blockquote: true |
<blockquote> |
<blockquote>引用</blockquote> |
code-block: true |
<pre> |
<pre>代码</pre> |
list: bullet |
<li> |
<ul><li>项目</li></ul> |
block: "custom" |
<div> |
<div class="custom"> |
行内样式映射表
Delta 属性 | CSS 属性 | 示例输出 |
---|---|---|
bold: true |
font-weight: bold |
<strong>文本</strong> |
italic: true |
font-style: italic |
<em>文本</em> |
underline: true |
text-decoration: underline |
<u>文本</u> |
strike: true |
text-decoration: line-through |
<s>文本</s> |
color: "red" |
color: red |
<span style="color:red">文本</span> |
background: "yellow" |
background-color: yellow |
<span style="background-color:yellow">文本</span> |
link: "url" |
N/A | <a href="url" target="_blank">链接</a> |
高级样式处理
-
自定义 CSS 转换:
dartQuillDeltaToHtmlConverter( converterOptions: OpConverterOptions( customCssStyles: (op) { if (op.isBlockquote()) { return ['border-left: 4px solid #ccc', 'padding-left: 16px']; } if (op.attributes.indent != null) { return ['padding-left: ${op.attributes.indent * 40}px']; } return null; } ) )
-
自定义标签转换:
dartQuillDeltaToHtmlConverter( converterOptions: OpConverterOptions( paragraphTag: 'div', // 修改默认段落标签 inlineStylesFlag: false, // 使用语义化标签代替样式 ) )
5. 完整转换示例
Delta 输入:
json
[
{"insert": "标题", "attributes": {"header": 1}},
{"insert": "\n"},
{"insert": "正文内容"},
{"insert": "重要", "attributes": {"bold": true, "color": "red"}},
{"insert": "普通文本\n"},
{"insert": "多行\n内容\n"},
{"insert": {"image": "image.jpg"}}
]
HTML 输出:
html
<h1>标题</h1>
<p>正文内容<strong style="color:red">重要</strong>普通文本</p>
<p>多行</p>
<p>内容</p>
<img src="image.jpg">
转换过程:
- 块级
header: 1
→<h1>
标签 - 文本内容直接输出
- 加粗红色文本 →
<strong>
标签带样式 - 多行内容分割为多个段落
- 图片嵌入 →
<img>
标签
通过这种分层转换策略,Flutter Quill 能够高效准确地将 Delta 格式的富文本内容转换为结构化的 HTML,同时保留所有样式和语义信息。