Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 63 additions & 45 deletions lib/src/highlighter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ class Highlighter {
required this.language,
required this.theme,
}) {
_grammar = _cache[language]!;
final grammar = _cache[language];
if (grammar == null) {
throw UnsupportedError(
'Language "$language" has not been initialized or added.',
);
}
_grammar = grammar;
}

/// Initializes the [Highlighter] with the given list of [languages]. This
Expand Down Expand Up @@ -118,39 +124,36 @@ class Highlighter {

(TextSpan, int) _formatBrackets(String text, int bracketCounter) {
var spans = <TextSpan>[];
var plainText = '';
for (var char in text.characters) {
if (_isStartingBracket(char)) {
if (plainText.isNotEmpty) {
spans.add(TextSpan(text: plainText));
plainText = '';
}
final plainText = StringBuffer();

void flushPlainText() {
if (plainText.isEmpty) return;
spans.add(TextSpan(text: plainText.toString()));
plainText.clear();
}

for (var i = 0; i < text.length; i += 1) {
final charCode = text.codeUnitAt(i);
final char = text[i];
if (_isStartingBracket(charCode)) {
flushPlainText();
spans.add(TextSpan(
text: char,
style: _getBracketStyle(bracketCounter),
));
bracketCounter += 1;
plainText = '';
} else if (_isEndingBracket(char)) {
if (plainText.isNotEmpty) {
spans.add(TextSpan(text: plainText));
plainText = '';
}

} else if (_isEndingBracket(charCode)) {
flushPlainText();
bracketCounter -= 1;
spans.add(TextSpan(
text: char,
style: _getBracketStyle(bracketCounter),
));
plainText = '';
} else {
plainText += char;
plainText.writeCharCode(charCode);
}
}
if (plainText.isNotEmpty) {
spans.add(TextSpan(text: plainText));
}
flushPlainText();

if (spans.length == 1) {
return (spans[0], bracketCounter);
Expand All @@ -166,12 +169,12 @@ class Highlighter {
return _bracketStyles[bracketCounter % _bracketStyles.length];
}

bool _isStartingBracket(String bracket) {
return bracket == '{' || bracket == '[' || bracket == '(';
bool _isStartingBracket(int charCode) {
return charCode == 0x7B || charCode == 0x5B || charCode == 0x28;
}

bool _isEndingBracket(String bracket) {
return bracket == '}' || bracket == ']' || bracket == ')';
bool _isEndingBracket(int charCode) {
return charCode == 0x7D || charCode == 0x5D || charCode == 0x29;
}
}

Expand All @@ -180,6 +183,7 @@ class HighlighterTheme {
final TextStyle _wrapper;
TextStyle? _fallback;
final _scopes = <String, TextStyle>{};
final _styleCache = <String, TextStyle?>{};

HighlighterTheme._({required TextStyle wrapper}) : _wrapper = wrapper;

Expand Down Expand Up @@ -287,14 +291,18 @@ class HighlighterTheme {

var fontStyleSetting = setting['fontStyle'];
if (fontStyleSetting is String) {
if (fontStyleSetting == 'italic') {
fontStyle = FontStyle.italic;
} else if (fontStyleSetting == 'bold') {
fontWeight = FontWeight.bold;
} else if (fontStyleSetting == 'underline') {
textDecoration = TextDecoration.underline;
} else {
throw Exception('WARNING unknown style: $fontStyleSetting');
final styles = fontStyleSetting.trim().split(RegExp(r'\s+'));
for (final style in styles) {
if (style.isEmpty) continue;
if (style == 'italic') {
fontStyle = FontStyle.italic;
} else if (style == 'bold') {
fontWeight = FontWeight.bold;
} else if (style == 'underline') {
textDecoration = TextDecoration.underline;
} else {
throw Exception('WARNING unknown style: $fontStyleSetting');
}
}
}

Expand All @@ -308,28 +316,38 @@ class HighlighterTheme {

void _addScope(String scope, TextStyle style) {
_scopes[scope] = style;
_styleCache.clear();
}

TextStyle? _getStyle(List<String> scope) {
for (var s in scope) {
var fallbacks = _fallbacks(s);
for (var f in fallbacks) {
var style = _scopes[f];
if (style != null) {
return style;
}
final style = _getStyleForScope(s);
if (style != null) {
return style;
}
}
return _fallback;
}

List<String> _fallbacks(String scope) {
var fallbacks = <String>[];
var parts = scope.split('.');
for (var i = 0; i < parts.length; i++) {
var s = parts.sublist(0, i + 1).join('.');
fallbacks.add(s);
TextStyle? _getStyleForScope(String scope) {
if (_styleCache.containsKey(scope)) {
return _styleCache[scope];
}

String candidate = scope;
while (candidate.isNotEmpty) {
final style = _scopes[candidate];
if (style != null) {
_styleCache[scope] = style;
return style;
}

final lastSeparator = candidate.lastIndexOf('.');
if (lastSeparator == -1) break;
candidate = candidate.substring(0, lastSeparator);
}
return fallbacks.reversed.toList();

_styleCache[scope] = null;
return null;
}
}
102 changes: 61 additions & 41 deletions lib/src/span_parser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -236,14 +236,19 @@ abstract class GrammarMatcher {
final matchStartLocation = location;
if (captures != null) {
final match = scanner.substring(start, end);
var captureSearchOffset = 0;
for (int i = 0; i <= lastMatch.groupCount; ++i) {
// Skip if we don't have a scope or nested patterns for this capture.
if (!captures.containsKey(i.toString())) continue;

final captureText = lastMatch.group(i);
if (captureText == null || captureText.isEmpty) continue;

final startOffset = match.indexOf(captureText);
final startOffset = i == 0
? 0
: match.indexOf(captureText, captureSearchOffset);
if (startOffset < 0) continue;
captureSearchOffset = startOffset + captureText.length;
final capture = captures[i.toString()] as Map<String, Object?>;
final captureStartLocation = matchStartLocation.offset(startOffset);
final captureEndLocation =
Expand Down Expand Up @@ -272,11 +277,11 @@ abstract class GrammarMatcher {

/// A simple matcher which matches a single line.
class _SimpleMatcher extends GrammarMatcher {
_SimpleMatcher(Map<String, Object?> json)
_SimpleMatcher(super.json)
: match = RegExp(json['match'] as String, multiLine: true),
captures = (json['captures'] as Map<String, Object?>?)
?.cast<String, Map<String, Object?>>(),
super._(json);
super._();

static bool isType(Map<String, Object?> json) {
return json.containsKey('match');
Expand Down Expand Up @@ -309,7 +314,7 @@ class _SimpleMatcher extends GrammarMatcher {
}

class _MultilineMatcher extends GrammarMatcher {
_MultilineMatcher(Map<String, Object?> json)
_MultilineMatcher(super.json)
: begin = RegExp(json['begin'] as String, multiLine: true),
beginCaptures = json['beginCaptures'] as Map<String, Object?>?,
contentName = json['contentName'] as String?,
Expand All @@ -325,8 +330,9 @@ class _MultilineMatcher extends GrammarMatcher {
?.cast<Map<String, Object?>>()
.map((e) => GrammarMatcher.parse(e))
.toList()
.cast<GrammarMatcher>(),
super._(json);
.cast<GrammarMatcher>() ??
const <GrammarMatcher>[],
super._();

static bool isType(Map<String, Object?> json) {
return json.containsKey('begin') &&
Expand Down Expand Up @@ -374,7 +380,7 @@ class _MultilineMatcher extends GrammarMatcher {
/// be null if this property is provided.
final Map<String, Object?>? captures;

final List<GrammarMatcher>? patterns;
final List<GrammarMatcher> patterns;

void _scanBegin(Grammar grammar, LineScanner scanner, ScopeStack scopeStack) {
final location = scanner.location;
Expand All @@ -397,21 +403,14 @@ class _MultilineMatcher extends GrammarMatcher {
LineScanner scanner,
ScopeStack scopeStack,
) {
while (!scanner.isDone) {
if (String.fromCharCode(scanner.peekChar()!) == '\n') {
scanner.readChar();
break;
}
bool foundMatch = false;
for (final pattern in patterns ?? <GrammarMatcher>[]) {
if (pattern.scan(grammar, scanner, scopeStack)) {
foundMatch = true;
break;
}
}
if (!foundMatch) {
scanner.readChar();
}
_scanUntil(
grammar,
scanner,
scopeStack,
() => scanner.peekChar() == 0x0A,
);
if (!scanner.isDone) {
scanner.readChar();
}
}

Expand All @@ -420,21 +419,41 @@ class _MultilineMatcher extends GrammarMatcher {
LineScanner scanner,
ScopeStack scopeStack,
) {
while (!scanner.isDone && end != null && !scanner.matches(end!)) {
bool foundMatch = false;
for (final pattern in patterns ?? <GrammarMatcher>[]) {
if (pattern.scan(grammar, scanner, scopeStack)) {
foundMatch = true;
break;
}
}
if (!foundMatch) {
// Move forward by a character, try again.
if (end == null) return;
_scanUntil(
grammar,
scanner,
scopeStack,
() => scanner.matches(end!),
);
}

void _scanUntil(
Grammar grammar,
LineScanner scanner,
ScopeStack scopeStack,
bool Function() shouldStop,
) {
while (!scanner.isDone && !shouldStop()) {
if (!_scanPatterns(grammar, scanner, scopeStack)) {
scanner.readChar();
}
}
}

bool _scanPatterns(
Grammar grammar,
LineScanner scanner,
ScopeStack scopeStack,
) {
for (final pattern in patterns) {
if (pattern.scan(grammar, scanner, scopeStack)) {
return true;
}
}
return false;
}

void _scanEnd(Grammar grammar, LineScanner scanner, ScopeStack scopeStack) {
final location = scanner.location;
if (end != null && !scanner.scan(end!)) {
Expand Down Expand Up @@ -531,31 +550,32 @@ class _MultilineMatcher extends GrammarMatcher {
if (end != null) 'end': end!.pattern,
if (endCaptures != null) 'endCaptures': endCaptures,
if (whileCond != null) 'while': whileCond!.pattern,
if (patterns != null)
'patterns': patterns!.map((e) => e.toJson()).toList(),
if (patterns.isNotEmpty)
'patterns': patterns.map((e) => e.toJson()).toList(),
};
}
}

class _PatternMatcher extends GrammarMatcher {
_PatternMatcher(Map<String, Object?> json)
_PatternMatcher(super.json)
: patterns = (json['patterns'] as List<Object?>?)
?.cast<Map<String, Object?>>()
.map((e) => GrammarMatcher.parse(e))
.toList()
.cast<GrammarMatcher>(),
super._(json);
.cast<GrammarMatcher>() ??
const <GrammarMatcher>[],
super._();

static bool isType(Map<String, Object?> json) {
return json.containsKey('patterns');
}

final List<GrammarMatcher>? patterns;
final List<GrammarMatcher> patterns;

@override
bool scan(Grammar grammar, LineScanner scanner, ScopeStack scopeStack) {
// Try each rule in the include and return after the first successful match.
for (final pattern in patterns!) {
for (final pattern in patterns) {
if (pattern.scan(grammar, scanner, scopeStack)) {
return true;
}
Expand All @@ -567,8 +587,8 @@ class _PatternMatcher extends GrammarMatcher {
Map<String, Object?> toJson() {
return {
if (name != null) 'name': name,
if (patterns != null)
'patterns': patterns!.map((e) => e.toJson()).toList(),
if (patterns.isNotEmpty)
'patterns': patterns.map((e) => e.toJson()).toList(),
};
}
}
Expand Down
2 changes: 0 additions & 2 deletions lib/syntax_highlight.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,2 @@
library syntax_highlight;

export 'src/highlighter.dart';
export 'src/code_editor.dart';
Loading