You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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原有的子节点渲染、样式继承逻辑,即使内部有样式节点也会被忽略。
修复方案
  1. 调整语法解析逻辑:匹配到span开标签后先读取RGB属性,定位到对应闭合标签位置,将开闭标签之间的内容交还给内联解析器做二次解析,解析生成的子节点挂载到fontcolor元素下,保证内部Markdown语法正常生效。
  2. 调整组件构建逻辑:不要独立创建SelectableText组件,仅需要基于框架传入的现有样式合并颜色属性返回TextSpan,框架会自动渲染子节点、继承所有原有内联样式。
  3. 代码规范优化: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.01 19:09:52