// Copyright 2019 The Flutter team. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'package:string_scanner/string_scanner.dart'; abstract class SyntaxPrehighlighter { List format(String src); } class DartSyntaxPrehighlighter extends SyntaxPrehighlighter { DartSyntaxPrehighlighter() { _spans = <_HighlightSpan>[]; } static const List _keywords = [ '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', ]; static const List _builtInTypes = [ 'int', 'double', 'num', 'bool', ]; String _src; StringScanner _scanner; List<_HighlightSpan> _spans; @override List format(String src) { _src = src; _scanner = StringScanner(_src); if (_generateSpans()) { // Successfully parsed the code final formattedText = []; var currentPosition = 0; for (final span in _spans) { if (currentPosition != span.start) { formattedText .add(CodeSpan(text: _src.substring(currentPosition, span.start))); } formattedText .add(CodeSpan(type: span.type, text: span.textForSpan(_src))); currentPosition = span.end; } if (currentPosition != _src.length) { formattedText .add(CodeSpan(text: _src.substring(currentPosition, _src.length))); } return formattedText; } else { // Parsing failed, return with only basic formatting return [CodeSpan(type: _HighlightType.base, text: src)]; } } bool _generateSpans() { var lastLoopPosition = _scanner.position; while (!_scanner.isDone) { // Skip White space _scanner.scan(RegExp(r'\s+')); // Block comments if (_scanner.scan(RegExp(r'/\*(.|\n)*\*/'))) { _spans.add(_HighlightSpan( _HighlightType.comment, _scanner.lastMatch.start, _scanner.lastMatch.end, )); continue; } // Line comments if (_scanner.scan('//')) { final startComment = _scanner.lastMatch.start; var eof = false; int endComment; if (_scanner.scan(RegExp(r'.*\n'))) { endComment = _scanner.lastMatch.end - 1; } else { eof = true; endComment = _src.length; } _spans.add(_HighlightSpan( _HighlightType.comment, startComment, endComment, )); if (eof) { break; } continue; } // Raw r"String" if (_scanner.scan(RegExp(r'r".*"'))) { _spans.add(_HighlightSpan( _HighlightType.string, _scanner.lastMatch.start, _scanner.lastMatch.end, )); continue; } // Raw r'String' if (_scanner.scan(RegExp(r"r'.*'"))) { _spans.add(_HighlightSpan( _HighlightType.string, _scanner.lastMatch.start, _scanner.lastMatch.end, )); continue; } // Multiline """String""" if (_scanner.scan(RegExp(r'"""(?:[^"\\]|\\(.|\n))*"""'))) { _spans.add(_HighlightSpan( _HighlightType.string, _scanner.lastMatch.start, _scanner.lastMatch.end, )); continue; } // Multiline '''String''' if (_scanner.scan(RegExp(r"'''(?:[^'\\]|\\(.|\n))*'''"))) { _spans.add(_HighlightSpan( _HighlightType.string, _scanner.lastMatch.start, _scanner.lastMatch.end, )); continue; } // "String" if (_scanner.scan(RegExp(r'"(?:[^"\\]|\\.)*"'))) { _spans.add(_HighlightSpan( _HighlightType.string, _scanner.lastMatch.start, _scanner.lastMatch.end, )); continue; } // 'String' if (_scanner.scan(RegExp(r"'(?:[^'\\]|\\.)*'"))) { _spans.add(_HighlightSpan( _HighlightType.string, _scanner.lastMatch.start, _scanner.lastMatch.end, )); continue; } // Double if (_scanner.scan(RegExp(r'\d+\.\d+'))) { _spans.add(_HighlightSpan( _HighlightType.number, _scanner.lastMatch.start, _scanner.lastMatch.end, )); continue; } // Integer if (_scanner.scan(RegExp(r'\d+'))) { _spans.add(_HighlightSpan(_HighlightType.number, _scanner.lastMatch.start, _scanner.lastMatch.end)); continue; } // Punctuation if (_scanner.scan(RegExp(r'[\[\]{}().!=<>&\|\?\+\-\*/%\^~;:,]'))) { _spans.add(_HighlightSpan( _HighlightType.punctuation, _scanner.lastMatch.start, _scanner.lastMatch.end, )); continue; } // Meta data if (_scanner.scan(RegExp(r'@\w+'))) { _spans.add(_HighlightSpan( _HighlightType.keyword, _scanner.lastMatch.start, _scanner.lastMatch.end, )); continue; } // Words if (_scanner.scan(RegExp(r'\w+'))) { _HighlightType type; var word = _scanner.lastMatch[0]; if (word.startsWith('_')) { word = word.substring(1); } if (_keywords.contains(word)) { type = _HighlightType.keyword; } else if (_builtInTypes.contains(word)) { type = _HighlightType.keyword; } else if (_firstLetterIsUpperCase(word)) { type = _HighlightType.klass; } else if (word.length >= 2 && word.startsWith('k') && _firstLetterIsUpperCase(word.substring(1))) { type = _HighlightType.constant; } if (type != null) { _spans.add(_HighlightSpan( type, _scanner.lastMatch.start, _scanner.lastMatch.end, )); } } // 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() { for (var i = _spans.length - 2; i >= 0; i -= 1) { if (_spans[i].type == _spans[i + 1].type && _spans[i].end == _spans[i + 1].start) { _spans[i] = _HighlightSpan( _spans[i].type, _spans[i].start, _spans[i + 1].end, ); _spans.removeAt(i + 1); } } } bool _firstLetterIsUpperCase(String str) { if (str.isNotEmpty) { final first = str.substring(0, 1); return first == first.toUpperCase(); } return false; } } enum _HighlightType { number, comment, keyword, string, punctuation, klass, constant, base, } 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); } } class CodeSpan { CodeSpan({this.type = _HighlightType.base, this.text}); final _HighlightType type; final String text; @override String toString() { return 'TextSpan(' 'style: codeStyle.${_styleNameOf(type)}, ' "text: '${_escape(text)}'" ')'; } } String _styleNameOf(_HighlightType type) { switch (type) { case _HighlightType.number: return 'numberStyle'; case _HighlightType.comment: return 'commentStyle'; case _HighlightType.keyword: return 'keywordStyle'; case _HighlightType.string: return 'stringStyle'; case _HighlightType.punctuation: return 'punctuationStyle'; case _HighlightType.klass: return 'classStyle'; case _HighlightType.constant: return 'constantStyle'; case _HighlightType.base: return 'baseStyle'; } return ''; } String _escape(String text) { final escapedText = StringBuffer(); for (final char in text.runes) { if (char < 0x20 || char >= 0x7F || char == 0x22 || char == 0x24 || char == 0x27 || char == 0x5C) { if (char <= 0xffff) { escapedText.write('\\u${_encodeAndPad(char)}'); } else { escapedText.write('\\u{${_encode(char)}}'); } } else { escapedText.write(String.fromCharCode(char)); } } return escapedText.toString(); } String _encode(int charCode) { return charCode.toRadixString(16); } String _encodeAndPad(int charCode) { final encoded = _encode(charCode); return '0' * (4 - encoded.length) + encoded; }