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

2021-01-27 14:30:01 浏览数 (1)

@charset "UTF-8";.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1:first-child,.markdown-body h2:first-child,.markdown-body h3:first-child,.markdown-body h4:first-child,.markdown-body h5:first-child,.markdown-body h6:first-child{margin-top:-1.5rem;margin-bottom:1rem}.markdown-body h1:before,.markdown-body h2:before,.markdown-body h3:before,.markdown-body h4:before,.markdown-body h5:before,.markdown-body h6:before{content:"#";display:inline-block;color:#3eaf7c;padding-right:.23em}.markdown-body h1{position:relative;font-size:2.5rem;margin-bottom:5px}.markdown-body h1:before{font-size:2.5rem}.markdown-body h2{padding-bottom:.5rem;font-size:2.2rem;border-bottom:1px solid #ececec}.markdown-body h3{font-size:1.5rem;padding-bottom:0}.markdown-body h4{font-size:1.25rem}.markdown-body h5{font-size:1rem}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body strong{color:#3eaf7c}.markdown-body img{max-width:100%;border-radius:2px;display:block;margin:auto;border:3px solid rgba(62,175,124,.2)}.markdown-body hr{border:none;border-top:1px solid #3eaf7c;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;overflow-x:auto;padding:.2rem .5rem;margin:0;color:#3eaf7c;font-weight:700;font-size:.85em;background-color:rgba(27,31,35,.05);border-radius:3px}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75;border-radius:6px;border:2px solid #3eaf7c}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{font-weight:500;text-decoration:none;color:#3eaf7c}.markdown-body a:active,.markdown-body a:hover{border-bottom:1.5px solid #3eaf7c}.markdown-body a:before{content:"⇲"}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #3eaf7c}.markdown-body thead{background:#3eaf7c;color:#fff;text-align:left}.markdown-body tr:nth-child(2n){background-color:rgba(62,175,124,.2)}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:.5rem solid;border-color:#42b983;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body details{outline:none;border:none;border-left:4px solid #3eaf7c;padding-left:10px;margin-left:4px}.markdown-body details summary{cursor:pointer;border:none;outline:none;background:#fff;margin:0 -17px}.markdown-body details summary::-webkit-details-marker{color:#3eaf7c}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body ol li::marker{color:#3eaf7c}.markdown-body ul li{list-style:none}.markdown-body ul li:before{content:"•";margin-right:4px;color:#3eaf7c}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}

前言

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


效果:

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

未高亮

已高亮

未高亮未高亮
已高亮已高亮

本系列其他文章
  • 《Flutter 文本解读 1 | 从源码认识 Text 组件》
  • 《Flutter 文本解读 2 | Text 是如何画出来的》
  • 《Flutter 文本解读 3 | Text 组件使用介绍 》
  • 《Flutter 文本解读 4 | TextStyle 文字样式解读 》
  • 《Flutter 文本解读 5 | RichText 富文本的使用 (上)》
  • 《Flutter 文本解读 6 | RichText 富文本的使用 (中)》

一、高亮关键字
1.资源介绍

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

通过 rootBundle#loadString 读取字符串。

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

2.高亮指定单词

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

代码语言:javascript复制
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

代码语言:javascript复制
class CodeParser {
  StringScanner _scanner;

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

    return _formInlineSpanByBean(content);
  }

  List _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 spans = [];
    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

代码语言:javascript复制
abstract class Language {
  final String name;

  const Language(this.name);

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

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

代码语言:javascript复制
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 判断是否为该语言的关键字。

代码语言:javascript复制
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 就不需要用分支结构一一判断了。

代码语言:javascript复制
enum SpanType { keyword, clazz }

class StyleSupport {
  static const Map 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.类名的解析

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

代码语言:javascript复制
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 ,并提供对应的样式:

代码语言:javascript复制
enum SpanType { keyword, clazz, comment }

class StyleSupport {
  static const Map 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 为结束标识,如果最后一行是注释,则需单独处理一下。

代码语言:javascript复制
// 块注释
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 ,并提供对应的样式:

代码语言:javascript复制
enum SpanType { keyword, clazz, comment, string }

class StyleSupport {
  static const Map 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.解析处理

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

代码语言:javascript复制
//  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 ,并提供对应的样式:

代码语言:javascript复制
enum SpanType { keyword, clazz, comment, string, number, punctuation,}
class StyleSupport {
  static const Map 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.解析处理

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

代码语言:javascript复制
// 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

样式1样式1
样式2样式2
代码语言:javascript复制
class StyleSupport {
  static const Map 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 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,),),
  };
}
复制代码

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


0 人点赞