syntax_highlighter.dart 9.92 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/material.dart';
import 'package:string_scanner/string_scanner.dart';

class SyntaxHighlighterStyle {
  SyntaxHighlighterStyle({
    this.baseStyle,
    this.numberStyle,
    this.commentStyle,
    this.keywordStyle,
    this.stringStyle,
    this.punctuationStyle,
    this.classStyle,
17
    this.constantStyle,
18 19
  });

20
  static SyntaxHighlighterStyle lightThemeStyle() {
21
    return SyntaxHighlighterStyle(
22 23 24 25 26 27 28
      baseStyle: const TextStyle(color: Color(0xFF000000)),
      numberStyle: const TextStyle(color: Color(0xFF1565C0)),
      commentStyle: const TextStyle(color: Color(0xFF9E9E9E)),
      keywordStyle: const TextStyle(color: Color(0xFF9C27B0)),
      stringStyle: const TextStyle(color: Color(0xFF43A047)),
      punctuationStyle: const TextStyle(color: Color(0xFF000000)),
      classStyle: const TextStyle(color: Color(0xFF512DA8)),
29
      constantStyle: const TextStyle(color: Color(0xFF795548)),
30 31 32
    );
  }

33
  static SyntaxHighlighterStyle darkThemeStyle() {
34
    return SyntaxHighlighterStyle(
35 36 37 38 39 40 41
      baseStyle: const TextStyle(color: Color(0xFFFFFFFF)),
      numberStyle: const TextStyle(color: Color(0xFF1565C0)),
      commentStyle: const TextStyle(color: Color(0xFF9E9E9E)),
      keywordStyle: const TextStyle(color: Color(0xFF80CBC4)),
      stringStyle: const TextStyle(color: Color(0xFF009688)),
      punctuationStyle: const TextStyle(color: Color(0xFFFFFFFF)),
      classStyle: const TextStyle(color: Color(0xFF009688)),
42
      constantStyle: const TextStyle(color: Color(0xFF795548)),
43 44 45
    );
  }

46 47 48 49 50 51 52 53
  final TextStyle? baseStyle;
  final TextStyle? numberStyle;
  final TextStyle? commentStyle;
  final TextStyle? keywordStyle;
  final TextStyle? stringStyle;
  final TextStyle? punctuationStyle;
  final TextStyle? classStyle;
  final TextStyle? constantStyle;
54 55
}

56
abstract class SyntaxHighlighter {
57 58 59 60 61 62
  TextSpan format(String src);
}

class DartSyntaxHighlighter extends SyntaxHighlighter {
  DartSyntaxHighlighter([this._style]) {
    _spans = <_HighlightSpan>[];
63
    _style ??= SyntaxHighlighterStyle.darkThemeStyle();
64 65
  }

66
  SyntaxHighlighterStyle? _style;
67

68
  static const List<String> _keywords = <String>[
69 70 71 72 73 74
    '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',
75
    'void', 'while', 'with', 'yield',
76 77
  ];

78
  static const List<String> _builtInTypes = <String>[
79
    'int', 'double', 'num', 'bool',
80 81
  ];

82 83
  String? _src;
  late StringScanner _scanner;
84

85
  late List<_HighlightSpan> _spans;
86 87

  @override
88
  TextSpan format(String? src) {
89
    _src = src;
90
    _scanner = StringScanner(_src!);
91 92 93

    if (_generateSpans()) {
      // Successfully parsed the code
94
      final List<TextSpan> formattedText = <TextSpan>[];
95 96
      int currentPosition = 0;

97
      for (final _HighlightSpan span in _spans) {
98
        if (currentPosition != span.start) {
99
          formattedText.add(TextSpan(text: _src!.substring(currentPosition, span.start)));
100
        }
101

102
        formattedText.add(TextSpan(style: span.textStyle(_style), text: span.textForSpan(_src!)));
103 104 105 106

        currentPosition = span.end;
      }

107
      if (currentPosition != _src!.length) {
108
        formattedText.add(TextSpan(text: _src!.substring(currentPosition, _src!.length)));
109
      }
110

111
      return TextSpan(style: _style!.baseStyle, children: formattedText);
112 113
    } else {
      // Parsing failed, return with only basic formatting
114
      return TextSpan(style: _style!.baseStyle, text: src);
115 116 117 118 119 120
    }
  }

  bool _generateSpans() {
    int lastLoopPosition = _scanner.position;

121
    while (!_scanner.isDone) {
122
      // Skip White space
123
      _scanner.scan(RegExp(r'\s+'));
124 125

      // Block comments
126 127
      if (_scanner.scan(RegExp(r'/\*(.|\n)*\*/'))) {
        _spans.add(_HighlightSpan(
128
          _HighlightType.comment,
129 130
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end,
131 132 133 134 135
        ));
        continue;
      }

      // Line comments
136
      if (_scanner.scan('//')) {
137
        final int startComment = _scanner.lastMatch!.start;
138 139 140

        bool eof = false;
        int endComment;
141
        if (_scanner.scan(RegExp(r'.*\n'))) {
142
          endComment = _scanner.lastMatch!.end - 1;
143 144
        } else {
          eof = true;
145
          endComment = _src!.length;
146 147
        }

148
        _spans.add(_HighlightSpan(
149 150
          _HighlightType.comment,
          startComment,
151
          endComment,
152 153
        ));

154
        if (eof) {
155
          break;
156
        }
157 158 159 160 161

        continue;
      }

      // Raw r"String"
162 163
      if (_scanner.scan(RegExp(r'r".*"'))) {
        _spans.add(_HighlightSpan(
164
          _HighlightType.string,
165 166
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end,
167 168 169 170 171
        ));
        continue;
      }

      // Raw r'String'
172 173
      if (_scanner.scan(RegExp(r"r'.*'"))) {
        _spans.add(_HighlightSpan(
174
          _HighlightType.string,
175 176
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end,
177 178 179 180 181
        ));
        continue;
      }

      // Multiline """String"""
182 183
      if (_scanner.scan(RegExp(r'"""(?:[^"\\]|\\(.|\n))*"""'))) {
        _spans.add(_HighlightSpan(
184
          _HighlightType.string,
185 186
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end,
187 188 189 190 191
        ));
        continue;
      }

      // Multiline '''String'''
192 193
      if (_scanner.scan(RegExp(r"'''(?:[^'\\]|\\(.|\n))*'''"))) {
        _spans.add(_HighlightSpan(
194
          _HighlightType.string,
195 196
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end,
197 198 199 200 201
        ));
        continue;
      }

      // "String"
202 203
      if (_scanner.scan(RegExp(r'"(?:[^"\\]|\\.)*"'))) {
        _spans.add(_HighlightSpan(
204
          _HighlightType.string,
205 206
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end,
207 208 209 210 211
        ));
        continue;
      }

      // 'String'
212 213
      if (_scanner.scan(RegExp(r"'(?:[^'\\]|\\.)*'"))) {
        _spans.add(_HighlightSpan(
214
          _HighlightType.string,
215 216
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end,
217 218 219 220 221
        ));
        continue;
      }

      // Double
222 223
      if (_scanner.scan(RegExp(r'\d+\.\d+'))) {
        _spans.add(_HighlightSpan(
224
          _HighlightType.number,
225 226
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end,
227 228 229 230 231
        ));
        continue;
      }

      // Integer
232 233
      if (_scanner.scan(RegExp(r'\d+'))) {
        _spans.add(_HighlightSpan(
234
          _HighlightType.number,
235 236
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end)
237 238 239 240 241
        );
        continue;
      }

      // Punctuation
242 243
      if (_scanner.scan(RegExp(r'[\[\]{}().!=<>&\|\?\+\-\*/%\^~;:,]'))) {
        _spans.add(_HighlightSpan(
244
          _HighlightType.punctuation,
245 246
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end,
247 248 249 250
        ));
        continue;
      }

251
      // Meta data
252 253
      if (_scanner.scan(RegExp(r'@\w+'))) {
        _spans.add(_HighlightSpan(
254
          _HighlightType.keyword,
255 256
          _scanner.lastMatch!.start,
          _scanner.lastMatch!.end,
257 258 259 260 261
        ));
        continue;
      }

      // Words
262
      if (_scanner.scan(RegExp(r'\w+'))) {
263
        _HighlightType? type;
264

265
        String word = _scanner.lastMatch![0]!;
266
        if (word.startsWith('_')) {
267
          word = word.substring(1);
268
        }
269

270
        if (_keywords.contains(word)) {
271
          type = _HighlightType.keyword;
272
        } else if (_builtInTypes.contains(word)) {
273
          type = _HighlightType.keyword;
274
        } else if (_firstLetterIsUpperCase(word)) {
275
          type = _HighlightType.klass;
276
        } else if (word.length >= 2 && word.startsWith('k') && _firstLetterIsUpperCase(word.substring(1))) {
277
          type = _HighlightType.constant;
278
        }
279 280

        if (type != null) {
281
          _spans.add(_HighlightSpan(
282
            type,
283 284
            _scanner.lastMatch!.start,
            _scanner.lastMatch!.end,
285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301
          ));
        }
      }

      // Check if this loop did anything
      if (lastLoopPosition == _scanner.position) {
        // Failed to parse this file, abort gracefully
        return false;
      }
      lastLoopPosition = _scanner.position;
    }

    _simplify();
    return true;
  }

  void _simplify() {
302
    for (int i = _spans.length - 2; i >= 0; i -= 1) {
303
      if (_spans[i].type == _spans[i + 1].type && _spans[i].end == _spans[i + 1].start) {
304
        _spans[i] = _HighlightSpan(
305 306
          _spans[i].type,
          _spans[i].start,
307
          _spans[i + 1].end,
308 309 310 311 312 313 314
        );
        _spans.removeAt(i + 1);
      }
    }
  }

  bool _firstLetterIsUpperCase(String str) {
315
    if (str.isNotEmpty) {
316
      final String first = str.substring(0, 1);
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342
      return first == first.toUpperCase();
    }
    return false;
  }
}

enum _HighlightType {
  number,
  comment,
  keyword,
  string,
  punctuation,
  klass,
  constant
}

class _HighlightSpan {
  _HighlightSpan(this.type, this.start, this.end);
  final _HighlightType type;
  final int start;
  final int end;

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

343
  TextStyle? textStyle(SyntaxHighlighterStyle? style) {
344
    if (type == _HighlightType.number) {
345
      return style!.numberStyle;
346
    } else if (type == _HighlightType.comment) {
347
      return style!.commentStyle;
348
    } else if (type == _HighlightType.keyword) {
349
      return style!.keywordStyle;
350
    } else if (type == _HighlightType.string) {
351
      return style!.stringStyle;
352
    } else if (type == _HighlightType.punctuation) {
353
      return style!.punctuationStyle;
354
    } else if (type == _HighlightType.klass) {
355
      return style!.classStyle;
356
    } else if (type == _HighlightType.constant) {
357
      return style!.constantStyle;
358
    } else {
359
      return style!.baseStyle;
360
    }
361 362
  }
}