阅读 1251

Flutter 文本解读 7 | RichText 写个代码高亮组件

前言

经过前面两篇的富文本介绍。已经基本上认识了 StringScanner 的使用,以前看 flutter/gallery 中有代码块的高亮功能,就研究了一下,用在了 FlutterUnit 中。目前 flutter/gallery 通过 codeviewer_cli 把所有的代码对应的 TextSpan 给直接生成了,一个 2.6 MB 的 45295 行 超大文件,并且将通过静态方法向外提供所需的 TextSpan,殊途同归吧,都是生成对应的 TextSpan 。之前的代码高亮逻辑可以查看这个包 syntax_highlighter


效果:

本文将一步步完成一个简单的代码高亮显示器:

未高亮已高亮

本系列其他文章

一、高亮关键字

1.资源介绍

这里的测试代码字符串放在 assets 目录下。并在 pubspec.yaml 中进行配置。

通过 rootBundle#loadString 读取字符串。

void _loadData() async {
  content = await rootBundle.loadString("assets/code.dart");
}
复制代码

2.高亮指定单词

比如我们现在想让 final 单词高亮显示,该如何做呢?实现需要找到每个 final 在文本中出现的 起始和结束位置,然后将这两个位置记录下来。这里通过 SpanBean 进行存储信息。

class SpanBean {
  SpanBean(this.start, this.end, {this.recognizer});

  final int start;
  final int end;

  String text(String src){
    return src.substring(start,end);
  }

  TextStyle get style {
    return TextStyle(
      color: Colors.green,
      fontWeight: FontWeight.bold
    );
  }

  final GestureRecognizer recognizer;
}
复制代码

对应一个 CodeParser 类用于解析代码字符串。实现通过 _parseContent 方法,使用 StringScanner 对文本进行扫描。通过正则表达式 RegExp(r'\w+') 可以匹配单词,如果该单词为 final ,就收录到 _spans 中。扫描完毕后通过 _formInlineSpanByBean 生成 InlineSpan

class CodeParser {
  StringScanner _scanner;

  InlineSpan parser(String content) {
    _scanner = StringScanner(content);
    _parseContent();

    return _formInlineSpanByBean(content);
  }

  List<SpanBean> _spans = [];

  void _parseContent() {
    while (!_scanner.isDone) {
      if (_scanner.scan(RegExp(r'\w+'))) {
        int startIndex = _scanner.lastMatch.start;
        int endIndex = _scanner.lastMatch.end;
        String word = _scanner.lastMatch[0];
        if (word == 'final'){
          _spans.add(SpanBean(startIndex, endIndex));
        }
      }

      if (!_scanner.isDone) {
        _scanner.position++;
      }
    }
  }

  void dispose() {
    _spans.forEach((element) {
      element.recognizer?.dispose();
    });
  }

  InlineSpan _formInlineSpanByBean(String content) {
    final List<TextSpan> spans = <TextSpan>[];
    int currentPosition = 0;

    for (SpanBean span in _spans) {
      if (currentPosition != span.start) {
        spans.add(
            TextSpan(text: content.substring(currentPosition, span.start)));
      }

      spans.add(TextSpan(
          style: span.style,
          text: span.text(content),
          recognizer: span.recognizer));
      currentPosition = span.end;
    }

    if (currentPosition != content.length)
      spans.add(
          TextSpan(text: content.substring(currentPosition, content.length)));

    return TextSpan(style: TextStyleSupport.defaultStyle, children: spans);
  }
}
复制代码

3.关键字高亮

现在完成了从 0 到 1 的质变,其后就比较简单了。考虑到不同的语言会有不同的关键字,为了方便拓展,可以定义一个接口 Language

abstract class Language {
  final String name;

  const Language(this.name);

  bool containsKeywords(String word);
}
复制代码

这样可以通过 DartLanguage 实现 Dart 语法关键字的高亮,如果 Dart 添加或去除了某些关键字也比较容易添加和修改。

class DartLanguage extends Language{

  const DartLanguage() : super('Dart');

  static const List<String> _kDartKeywords = [
  'abstract', 'as', 'assert', 'async', 'await', 'break', 'case', 'catch',
  'class', 'const', 'continue', 'default', 'deferred', 'do', 'dynamic', 'else',
  'enum', 'export', 'external', 'extends', 'factory', 'false', 'final',
  'finally', 'for', 'get', 'if', 'implements', 'import', 'in', 'is', 'library',
  'new', 'null', 'operator', 'part', 'rethrow', 'return', 'set', 'static',
  'super', 'switch', 'sync', 'this', 'throw', 'true', 'try', 'typedef', 'var',
  'void', 'while', 'with', 'yield'
  ];

  @override
  bool containsKeywords(String word)=>_kDartInTypes.contains(word);
  
}
复制代码

可以在 CodeParser 中传入 Language 对象,在解析时通过 language.containsKeywords 判断是否为该语言的关键字。


class CodeParser {
  StringScanner _scanner;
  final Language language;
  CodeParser({this.language = const DartLanguage()});

---->[CodeParser#_parseContent]----
if (_scanner.scan(RegExp(r'\w+'))) {
  int startIndex = _scanner.lastMatch.start;
  int endIndex = _scanner.lastMatch.end;
  String word = _scanner.lastMatch[0];
  if (language.containsKeywords(word)){
    _spans.add(SpanBean(startIndex, endIndex));
  }
}
复制代码

这样,效果如下,可以通过 SpanBean 中的 style 修改高亮样式。


二、 类名和注释高亮

1.高亮类型定义

现在我们需要拓展高亮的类型,通过 SpanType 维护。并通过 StyleSupport.kGithubLight 维护一个,类型和文字样式的映射。在 SpanBean 中传入 SpanType,这样高亮类型对应的 TextStyle 就不需要用分支结构一一判断了。

enum SpanType { keyword, clazz }

class StyleSupport {
  static const Map<SpanType, TextStyle> kGithubLight = {
    SpanType.keyword: TextStyle(fontWeight: FontWeight.bold, color: const Color(0xFF009999)),
    SpanType.clazz: TextStyle(color: const Color(0xFF6F42C1)),
  };
}

class SpanBean {
  SpanBean(this.start, this.end, this.type, {this.recognizer});

  final int start;
  final int end;
  final SpanType type;

  String text(String src) {
    return src.substring(start, end);
  }
  
  TextStyle get style => StyleSupport.kGithubLight[type];

  final GestureRecognizer recognizer;
}

复制代码

2.类名的解析

类名的判断很简单,只需要看 首字母是否大写 即可。

if (_scanner.scan(RegExp(r'\w+'))) {
  int startIndex = _scanner.lastMatch.start;
  int endIndex = _scanner.lastMatch.end;
  String word = _scanner.lastMatch[0];
  if (language.containsKeywords(word)){
    _spans.add(SpanBean(startIndex, endIndex,SpanType.keyword));
  }else if (_firstLetterIsUpperCase(word)){
    // 类型为类名
    _spans.add(SpanBean(startIndex, endIndex,SpanType.clazz));
  }
}

bool _firstLetterIsUpperCase(String str) {
  if (str.isNotEmpty) {
    final String first = str.substring(0, 1);
    return first == first.toUpperCase();
  }
  return false;
}
复制代码

结果如下,不过可以看到,注释中的 类名 也被高亮了。只要在 类名的解析 之前处理即可,StringScanner 扫描完注释之后,就不会再对之后的处理有影响。


三、注释高亮

1.增加类型

如下在类型中增加 comment ,并提供对应的样式:

enum SpanType { keyword, clazz, comment }

class StyleSupport {
  static const Map<SpanType, TextStyle> kGithubLight = {
    SpanType.keyword: TextStyle(fontWeight: FontWeight.bold, color: const Color(0xFF009999)),
    SpanType.clazz: TextStyle(color: const Color(0xFF6F42C1)),
    SpanType.comment: TextStyle(color: Color(0xFF9D9D8D), fontStyle: FontStyle.italic),
  };
}
复制代码

2.解析处理

注释分为块注释行注释,由于行注释\n 为结束标识,如果最后一行是注释,则需单独处理一下。

// 块注释
if (_scanner.scan(RegExp(r'/\*(.|\n)*\*/'))) {
  int startIndex = _scanner.lastMatch.start;
  int endIndex = _scanner.lastMatch.end;
  _spans.add(SpanBean( startIndex, endIndex,SpanType.comment));
}

// 行注释
if (_scanner.scan('//')) {
  final int startIndex = _scanner.lastMatch.start;
  int endIndex;
  if (_scanner.scan(RegExp(r'.*\n'))) {
    endIndex = _scanner.lastMatch.end - 1;
  } else {
    endIndex = content.length;
  }
  _spans.add(SpanBean(startIndex, endIndex ,SpanType.comment));
}
复制代码

注释效果如下:


四、字符串解析

1.增加类型

如下在类型中增加 string ,并提供对应的样式:

enum SpanType { keyword, clazz, comment, string }

class StyleSupport {
  static const Map<SpanType, TextStyle> kGithubLight = {
    SpanType.keyword: TextStyle(fontWeight: FontWeight.bold, color: const Color(0xFF009999)),
    SpanType.clazz: TextStyle(color: const Color(0xFF6F42C1)),
    SpanType.comment: TextStyle(color: Color(0xFF9D9D8D), fontStyle: FontStyle.italic),
    SpanType.string: TextStyle(color: Color(0xFFDD1045)),
  };
}
复制代码

2.解析处理

字符串有六种情况,如下,依次判断添加即可:

image-20210121152548126

//  r"String"
if (_scanner.scan(RegExp(r'r".*"'))) {
  _spans.add(SpanBean(_scanner.lastMatch.start,
      _scanner.lastMatch.end,SpanType.string));
}

//  r'String'
if (_scanner.scan(RegExp(r"r'.*'"))) {
  _spans.add(SpanBean(_scanner.lastMatch.start,
      _scanner.lastMatch.end,SpanType.string));
}

//  """String"""
if (_scanner.scan(RegExp(r'"""(?:[^"\\]|\\(.|\n))*"""'))) {
  _spans.add(SpanBean(_scanner.lastMatch.start,
      _scanner.lastMatch.end,SpanType.string));
}

//  '''String'''
if (_scanner.scan(RegExp(r"'''(?:[^'\\]|\\(.|\n))*'''"))) {
  _spans.add(SpanBean(_scanner.lastMatch.start,
      _scanner.lastMatch.end,SpanType.string));
}

// "String"
if (_scanner.scan(RegExp(r'"(?:[^"\\]|\\.)*"'))) {
  _spans.add(SpanBean(_scanner.lastMatch.start,
      _scanner.lastMatch.end,SpanType.string));
}

// 'String'
if (_scanner.scan(RegExp(r"'(?:[^'\\]|\\.)*'"))) {
  _spans.add(SpanBean(_scanner.lastMatch.start,
      _scanner.lastMatch.end,SpanType.string));
}
复制代码

五、数字和标点

1.增加类型

如下在类型中增加 numberpunctuation ,并提供对应的样式:

enum SpanType { keyword, clazz, comment, string, number, punctuation,}
class StyleSupport {
  static const Map<SpanType, TextStyle> kGithubLight = {
    SpanType.keyword: TextStyle(fontWeight: FontWeight.bold, color: const Color(0xFF009999)),
    
    SpanType.clazz: TextStyle(color: const Color(0xFF6F42C1)),
    SpanType.comment: TextStyle(color: Color(0xFF9D9D8D), fontStyle: FontStyle.italic),
    SpanType.string: TextStyle(color: Color(0xFFDD1045),),
    SpanType.number: TextStyle(color: Color(0xFF008081),),
    SpanType.punctuation: TextStyle(color: Color(0xFF333333,),),
  };
}
复制代码

2.解析处理

处理如下,这样基本的代码高亮类型都有了,如果有其他需要,可以自己进行解析来拓展。总的来看,最重要的还是如何通过正则来解析。

// Double
if (_scanner.scan(RegExp(r'\d+\.\d+'))) {
  _spans.add(SpanBean(_scanner.lastMatch.start,
      _scanner.lastMatch.end,SpanType.number));
}

// Integer
if (_scanner.scan(RegExp(r'\d+'))) {
  _spans.add(SpanBean(_scanner.lastMatch.start,
      _scanner.lastMatch.end,SpanType.number));
}

// Punctuation
if (_scanner.scan(RegExp(r'[\[\]\{\}\(\).!=<>&\|\?\+\-\*/%\^~;:,]'))) {
  _spans.add(SpanBean(_scanner.lastMatch.start,
      _scanner.lastMatch.end,SpanType.punctuation));
}
复制代码


六、代码样式切换

可以在 StyleSupport 中定义其他样式,用来切换。也可以将样式作为 CodeParser 的成员,向外界暴露出去,方便自定义样式。

样式1样式2
class StyleSupport {
  static const Map<SpanType, TextStyle> kGithubLight = {
    SpanType.keyword: TextStyle(fontWeight: FontWeight.bold, color: const Color(0xFF009999)),
    SpanType.clazz: TextStyle(color: const Color(0xFF6F42C1)),
    SpanType.comment: TextStyle(color: Color(0xFF9D9D8D), fontStyle: FontStyle.italic),
    SpanType.string: TextStyle(color: Color(0xFFDD1045),),
    SpanType.number: TextStyle(color: Color(0xFF008081),),
    SpanType.punctuation: TextStyle(color: Color(0xFF333333,),),
  };

  static const Map<SpanType, TextStyle> kTolyDark = {
    SpanType.keyword: TextStyle(fontWeight: FontWeight.bold, color: const Color(0xFF80CBC4)),
    SpanType.clazz: TextStyle(color: const Color(0xFF7AA6DA)),
    SpanType.comment: TextStyle(color: Color(0xFF9E9E9E), fontStyle: FontStyle.italic),
    SpanType.string: TextStyle(color: Color(0xFFB9CA4A),),
    SpanType.number: TextStyle(color: Color(0xFFDF935F),),
    SpanType.punctuation: TextStyle(color: Color(0xFF333333,),),
  };
}
复制代码

核心的东西就是这样,如果有其他的高亮需求,也可以自己解析。也可以整理一下,提供一个组件方便使用。那么本篇就这样,谢谢观看~


@张风捷特烈 2021.01.22 未允禁转
我的公众号:编程之王
联系我--邮箱:1981462002@qq.com -- 微信:zdl1994328
~ END ~

文章分类
Android
文章标签