// 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 annotations; List _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 annotations = []; int index = 0; int offset = 0; int lineNo = 1; int columnNo = 1; List lineStarts = []; lineStarts.add(offset); while (index < annotatedCode.length) { Match startMatch = start.matchAsPrefix(annotatedCode, index); if (startMatch != null) { int startIndex = startMatch.end; Iterable 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 = []; _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 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 splitByPrefixes( AnnotatedCode annotatedCode, Iterable prefixes) { Map> map = >{}; for (String prefix in prefixes) { map[prefix] = []; } 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 split = {}; map.forEach((String prefix, List annotations) { split[prefix] = new AnnotatedCode(annotatedCode.sourceCode, annotations); }); return split; }