@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
读取字符串。
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 _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
。
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 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 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 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.增加类型
如下在类型中增加 number
和 punctuation
,并提供对应的样式:
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 |
---|---|
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,),),
};
}
复制代码
核心的东西就是这样,如果有其他的高亮需求,也可以自己解析。也可以整理一下,提供一个组件方便使用。那么本篇就这样,谢谢观看~