blob: 068fcca3a27348c790499fe50dcddc1830648aae [file] [log] [blame]
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
const int _LF = 0x0A;
const int _CR = 0x0D;
const Pattern atBraceStart = '@{';
const Pattern braceEnd = '}';
final Pattern commentStart = new RegExp(r'/\*');
final Pattern commentEnd = new RegExp(r'\*/\s*');
class Annotation {
/// 1-based line number of the annotation.
final int lineNo;
/// 1-based column number of the annotation.
final int columnNo;
/// 0-based character offset of the annotation within the source text.
final int offset;
/// The text in the annotation.
final String text;
Annotation(this.lineNo, this.columnNo, this.offset, this.text);
String toString() =>
'Annotation(lineNo=$lineNo,columnNo=$columnNo,offset=$offset,text=$text)';
}
/// A source code text with annotated positions.
///
/// An [AnnotatedCode] can be created from a [String] of source code where
/// annotated positions are embedded, by default using the syntax `@{text}`.
/// For instance
///
/// main() {
/// @{foo-call}foo();
/// bar@{bar-args}();
/// }
///
/// the position of `foo` call will hold an annotation with text 'foo-call' and
/// the position of `bar` arguments will hold an annotation with text
/// 'bar-args'.
///
/// Annotation text cannot span multiple lines and cannot contain '}'.
class AnnotatedCode {
/// The source code without annotations.
final String sourceCode;
/// The annotations for the source code.
final List<Annotation> annotations;
List<int> _lineStarts;
AnnotatedCode(this.sourceCode, this.annotations);
AnnotatedCode.internal(this.sourceCode, this.annotations, this._lineStarts);
/// Creates an [AnnotatedCode] by processing [annotatedCode]. Annotation
/// delimited by [start] and [end] are converted into [Annotation]s and
/// removed from the [annotatedCode] to produce the source code.
factory AnnotatedCode.fromText(String annotatedCode,
[Pattern start = atBraceStart, Pattern end = braceEnd]) {
StringBuffer codeBuffer = new StringBuffer();
List<Annotation> annotations = <Annotation>[];
int index = 0;
int offset = 0;
int lineNo = 1;
int columnNo = 1;
List<int> lineStarts = <int>[];
lineStarts.add(offset);
while (index < annotatedCode.length) {
Match startMatch = start.matchAsPrefix(annotatedCode, index);
if (startMatch != null) {
int startIndex = startMatch.end;
Iterable<Match> endMatches =
end.allMatches(annotatedCode, startMatch.end);
if (!endMatches.isEmpty) {
Match endMatch = endMatches.first;
annotatedCode.indexOf(end, startIndex);
String text = annotatedCode.substring(startMatch.end, endMatch.start);
annotations.add(new Annotation(lineNo, columnNo, offset, text));
index = endMatch.end;
continue;
}
}
int charCode = annotatedCode.codeUnitAt(index);
switch (charCode) {
case _LF:
codeBuffer.write('\n');
offset++;
lineStarts.add(offset);
lineNo++;
columnNo = 1;
break;
case _CR:
if (index + 1 < annotatedCode.length &&
annotatedCode.codeUnitAt(index + 1) == _LF) {
index++;
}
codeBuffer.write('\n');
offset++;
lineStarts.add(offset);
lineNo++;
columnNo = 1;
break;
default:
codeBuffer.writeCharCode(charCode);
offset++;
columnNo++;
}
index++;
}
lineStarts.add(offset);
return new AnnotatedCode.internal(
codeBuffer.toString(), annotations, lineStarts);
}
void _ensureLineStarts() {
if (_lineStarts == null) {
int index = 0;
int offset = 0;
_lineStarts = <int>[];
_lineStarts.add(offset);
while (index < sourceCode.length) {
int charCode = sourceCode.codeUnitAt(index);
switch (charCode) {
case _LF:
offset++;
_lineStarts.add(offset);
break;
case _CR:
if (index + 1 < sourceCode.length &&
sourceCode.codeUnitAt(index + 1) == _LF) {
index++;
}
offset++;
_lineStarts.add(offset);
break;
default:
offset++;
}
index++;
}
_lineStarts.add(offset);
}
}
void addAnnotation(int lineNo, int columnNo, String text) {
_ensureLineStarts();
int offset = _lineStarts[lineNo - 1] + (columnNo - 1);
annotations.add(new Annotation(lineNo, columnNo, offset, text));
}
String toText() {
StringBuffer sb = new StringBuffer();
List<Annotation> list = annotations.toList()
..sort((a, b) => a.offset.compareTo(b.offset));
int offset = 0;
for (Annotation annotation in list) {
sb.write(sourceCode.substring(offset, annotation.offset));
sb.write('@{${annotation.text}}');
offset = annotation.offset;
}
sb.write(sourceCode.substring(offset));
return sb.toString();
}
}
/// Split the annotations in [annotatedCode] by [prefixes].
///
/// Returns a map containing an [AnnotatedCode] object for each prefix,
/// containing only the annotations whose text started with the given prefix.
/// If no prefix match the annotation text, the annotation is added to all
/// [AnnotatedCode] objects.
///
/// The prefixes are removed from the annotation texts in the returned
/// [AnnotatedCode] objects.
Map<String, AnnotatedCode> splitByPrefixes(
AnnotatedCode annotatedCode, Iterable<String> prefixes) {
Map<String, List<Annotation>> map = <String, List<Annotation>>{};
for (String prefix in prefixes) {
map[prefix] = <Annotation>[];
}
outer:
for (Annotation annotation in annotatedCode.annotations) {
for (String prefix in prefixes) {
if (annotation.text.startsWith(prefix)) {
map[prefix].add(new Annotation(annotation.lineNo, annotation.columnNo,
annotation.offset, annotation.text.substring(prefix.length)));
continue outer;
}
}
for (String prefix in prefixes) {
map[prefix].add(annotation);
}
}
Map<String, AnnotatedCode> split = <String, AnnotatedCode>{};
map.forEach((String prefix, List<Annotation> annotations) {
split[prefix] = new AnnotatedCode(annotatedCode.sourceCode, annotations);
});
return split;
}