flutter-markdown自定义彩色文本内联语法/构建器丢失内部样式问题
问题描述
需要在flutter-markdown中通过自定义语法与解析器实现动态文本颜色功能,现有实现可正常为文本上色,但会丢失标签包裹范围内的内部Markdown文本样式。
测试用例字符串:
"not blue <span data-color='rgb(0,0,255)'>**blue text**</span> not blue"
实际渲染仅显示纯蓝色文本,未呈现预期的蓝色+加粗效果。
补充说明:动态上色并非Markdown原生设计能力,但实现后可大幅降低项目开发难度;曾评估flutter_html方案,因该库可选文本支持存在限制未选用。
现有问题代码
class FontColorSyntax extends md.InlineSyntax { //Hideous => matches groups as follows: <span data-color="rgb(1,2,3)">4</span> FontColorSyntax() : super( r'''<span(?:.*?)data-color=['"]rgb *\((?: *([0-9]{1,3}))[, ]+(?: *([0-9]{1,3}))[, ]+(?: *([0-9]{1,3}))[, ]*\)['"](?:.*?)>(.*?)</span>'''); @override bool onMatch(md.InlineParser parse, Match match) { md.Element colorTag = md.Element.text('fontcolor', match.group(4) ?? "matched text"); colorTag.attributes["fontColorRed"] = match.group(1) ?? "0"; colorTag.attributes["fontColorGreen"] = match.group(2) ?? "0"; colorTag.attributes["fontColorBlue"] = match.group(3) ?? "0"; parse.addNode(colorTag); return true; } } class FontColorBuilder extends MarkdownElementBuilder { int getColor(md.Element el, String color) => int.tryParse(el.attributes["fontColor$color"] ?? "0") ?? 0; @override Widget visitElementAfter(element, style) { final red = getColor(element, "Red"); final green = getColor(element, "Green"); final blue = getColor(element, "Blue"); return SelectableText.rich( TextSpan( text: element.textContent, style: TextStyle( color: Color.fromRGBO(red, green, blue, 1), )), ); } }
问题根因
- 语法解析层问题:当前正则一次性匹配整个
<span>...</span>块,直接将捕获到的标签内文本作为纯文本传入md.Element.text创建节点,标签内部的**、*等Markdown语法完全没有进入解析流程,自然不会生成加粗、斜体等样式节点。 - 组件构建层问题:
visitElementAfter中直接新建独立的SelectableText.rich,仅读取节点纯文本内容渲染,完全脱离了flutter_markdown原有的子节点渲染、样式继承逻辑,即使内部有样式节点也会被忽略。
修复方案
- 调整语法解析逻辑:匹配到span开标签后先读取RGB属性,定位到对应闭合标签位置,将开闭标签之间的内容交还给内联解析器做二次解析,解析生成的子节点挂载到fontcolor元素下,保证内部Markdown语法正常生效。
- 调整组件构建逻辑:不要独立创建
SelectableText组件,仅需要基于框架传入的现有样式合并颜色属性返回TextSpan,框架会自动渲染子节点、继承所有原有内联样式。 - 代码规范优化:Dart中私有方法/变量以下划线开头,参数名使用完整语义化命名,正则增强兼容性,支持rgb值前后带空格的写法。
修复后完整代码:
class FontColorSyntax extends md.InlineSyntax { FontColorSyntax() : super( r'<span\s+data-color=[\'"](?:rgb\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\))[\'"]>'); @override bool onMatch(md.InlineParser parser, Match match) { // 读取RGB三通道值 final red = match.group(1) ?? '0'; final green = match.group(2) ?? '0'; final blue = match.group(3) ?? '0'; // 消费匹配到的开标签 parser.advanceBy(match.group(0)!.length); // 创建颜色容器节点,传入空子节点数组 final colorElement = md.Element('fontcolor', []); colorElement.attributes['fontColorRed'] = red; colorElement.attributes['fontColorGreen'] = green; colorElement.attributes['fontColorBlue'] = blue; // 查找对应闭合标签位置 final closeTagReg = RegExp(r'</span>'); final remainingContent = parser.source.substring(parser.position); final closeMatch = closeTagReg.firstMatch(remainingContent); if (closeMatch != null) { // 解析开闭标签之间的内容,所有内部Markdown语法会正常生成子节点 final innerNodes = parser.parseInline( remainingContent.substring(0, closeMatch.start), ); colorElement.children?.addAll(innerNodes); // 消费闭合标签 parser.advanceBy(closeMatch.end); } else { // 未匹配到闭合标签的容错处理 colorElement.children?.add(md.Text(remainingContent)); parser.advanceBy(remainingContent.length); } parser.addNode(colorElement); return true; } } class FontColorBuilder extends MarkdownElementBuilder { int _getColorChannel(md.Element el, String channel) => int.tryParse(el.attributes['fontColor$channel'] ?? '0') ?? 0; @override Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) { final red = _getColorChannel(element, 'Red'); final green = _getColorChannel(element, 'Green'); final blue = _getColorChannel(element, 'Blue'); final targetColor = Color.fromRGBO(red, green, blue, 1); // 仅在传入样式基础上叠加颜色属性,返回TextSpan交由框架统一渲染 return TextSpan( style: (preferredStyle ?? const TextStyle()).copyWith(color: targetColor), ); } }
内容的提问来源于stack exchange,提问作者daxicks
相关产品推荐
相关产品推荐

