syntax_highlighter.dart 9.82 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
        formattedText.add(TextSpan(style: span.textStyle(_style), text: span.textForSpan(_src!)));
102 103 104 105

        currentPosition = span.end;
      }

106 107
      if (currentPosition != _src!.length)
        formattedText.add(TextSpan(text: _src!.substring(currentPosition, _src!.length)));
108

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

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

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

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

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

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

146
        _spans.add(_HighlightSpan(
147 148
          _HighlightType.comment,
          startComment,
149
          endComment,
150 151 152 153 154 155 156 157 158
        ));

        if (eof)
          break;

        continue;
      }

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

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

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

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

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

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

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

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

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

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

      // Words
259
      if (_scanner.scan(RegExp(r'\w+'))) {
260
        _HighlightType? type;
261

262
        String word = _scanner.lastMatch![0]!;
263
        if (word.startsWith('_'))
264 265
          word = word.substring(1);

266
        if (_keywords.contains(word))
267
          type = _HighlightType.keyword;
268
        else if (_builtInTypes.contains(word))
269 270 271
          type = _HighlightType.keyword;
        else if (_firstLetterIsUpperCase(word))
          type = _HighlightType.klass;
272
        else if (word.length >= 2 && word.startsWith('k') && _firstLetterIsUpperCase(word.substring(1)))
273 274 275
          type = _HighlightType.constant;

        if (type != null) {
276
          _spans.add(_HighlightSpan(
277
            type,
278 279
            _scanner.lastMatch!.start,
            _scanner.lastMatch!.end,
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296
          ));
        }
      }

      // 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() {
297
    for (int i = _spans.length - 2; i >= 0; i -= 1) {
298
      if (_spans[i].type == _spans[i + 1].type && _spans[i].end == _spans[i + 1].start) {
299
        _spans[i] = _HighlightSpan(
300 301
          _spans[i].type,
          _spans[i].start,
302
          _spans[i + 1].end,
303 304 305 306 307 308 309
        );
        _spans.removeAt(i + 1);
      }
    }
  }

  bool _firstLetterIsUpperCase(String str) {
310
    if (str.isNotEmpty) {
311
      final String first = str.substring(0, 1);
312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
      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);
  }

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