前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Flutter 文本解读 7 | RichText 写个代码高亮组件

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

作者头像
张风捷特烈
发布2021-01-27 14:30:01
1.4K0
发布2021-01-27 14:30:01
举报

@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


效果:

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

未高亮

已高亮

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

本系列其他文章

一、高亮关键字
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.解析处理

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

image-20210121152548126
image-20210121152548126
代码语言: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,),),
  };
}
复制代码

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


本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
    • 效果:
      • 本系列其他文章
      • 一、高亮关键字
        • 1.资源介绍
          • 2.高亮指定单词
            • 3.关键字高亮
            • 二、 类名和注释高亮
              • 1.高亮类型定义
                • 2.类名的解析
                • 三、注释高亮
                  • 1.增加类型
                    • 2.解析处理
                    • 四、字符串解析
                      • 1.增加类型
                        • 2.解析处理
                        • 五、数字和标点
                          • 1.增加类型
                            • 2.解析处理
                            • 六、代码样式切换
                            领券
                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档