// Copyright (c) 2013, 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. /// Contains a code printer that generates code by recording the source maps. library source_maps.printer; import 'package:source_span/source_span.dart'; import 'builder.dart'; import 'src/source_map_span.dart'; import 'src/utils.dart'; /// A simple printer that keeps track of offset locations and records source /// maps locations. class Printer { final String filename; final StringBuffer _buff = StringBuffer(); final SourceMapBuilder _maps = SourceMapBuilder(); String get text => _buff.toString(); String get map => _maps.toJson(filename); /// Current source location mapping. SourceLocation? _loc; /// Current line in the buffer; int _line = 0; /// Current column in the buffer. int _column = 0; Printer(this.filename); /// Add [str] contents to the output, tracking new lines to track correct /// positions for span locations. When [projectMarks] is true, this method /// adds a source map location on each new line, projecting that every new /// line in the target file (printed here) corresponds to a new line in the /// source file. void add(String str, {bool projectMarks = false}) { var chars = str.runes.toList(); var length = chars.length; for (var i = 0; i < length; i++) { var c = chars[i]; if (c == lineFeed || (c == carriageReturn && (i + 1 == length || chars[i + 1] != lineFeed))) { // Return not followed by line-feed is treated as a new line. _line++; _column = 0; { // **Warning**: Any calls to `mark` will change the value of `_loc`, // so this local variable is no longer up to date after that point. // // This is why it has been put inside its own block to limit the // scope in which it is available. var loc = _loc; if (projectMarks && loc != null) { if (loc is FileLocation) { var file = loc.file; mark(file.location(file.getOffset(loc.line + 1))); } else { mark(SourceLocation(0, sourceUrl: loc.sourceUrl, line: loc.line + 1, column: 0)); } } } } else { _column++; } } _buff.write(str); } /// Append a [total] number of spaces in the target file. Typically used for /// formatting indentation. void addSpaces(int total) { for (var i = 0; i < total; i++) { _buff.write(' '); } _column += total; } /// Marks that the current point in the target file corresponds to the [mark] /// in the source file, which can be either a [SourceLocation] or a /// [SourceSpan]. When the mark is a [SourceMapSpan] with `isIdentifier` set, /// this also records the name of the identifier in the source map /// information. void mark(Object mark) { late final SourceLocation loc; String? identifier; if (mark is SourceLocation) { loc = mark; } else if (mark is SourceSpan) { loc = mark.start; if (mark is SourceMapSpan && mark.isIdentifier) identifier = mark.text; } _maps.addLocation(loc, SourceLocation(_buff.length, line: _line, column: _column), identifier); _loc = loc; } } /// A more advanced printer that keeps track of offset locations to record /// source maps, but additionally allows nesting of different kind of items, /// including [NestedPrinter]s, and it let's you automatically indent text. /// /// This class is especially useful when doing code generation, where different /// pieces of the code are generated independently on separate printers, and are /// finally put together in the end. class NestedPrinter implements NestedItem { /// Items recoded by this printer, which can be [String] literals, /// [NestedItem]s, and source map information like [SourceLocation] and /// [SourceSpan]. final List _items = []; /// Internal buffer to merge consecutive strings added to this printer. StringBuffer? _buff; /// Current indentation, which can be updated from outside this class. int indent; /// [Printer] used during the last call to [build], if any. Printer? printer; /// Returns the text produced after calling [build]. String? get text => printer?.text; /// Returns the source-map information produced after calling [build]. String? get map => printer?.map; /// Item used to indicate that the following item is copied from the original /// source code, and hence we should preserve source-maps on every new line. static final _original = Object(); NestedPrinter([this.indent = 0]); /// Adds [object] to this printer. [object] can be a [String], /// [NestedPrinter], or anything implementing [NestedItem]. If [object] is a /// [String], the value is appended directly, without doing any formatting /// changes. If you wish to add a line of code with automatic indentation, use /// [addLine] instead. [NestedPrinter]s and [NestedItem]s are not processed /// until [build] gets called later on. We ensure that [build] emits every /// object in the order that they were added to this printer. /// /// The [location] and [span] parameters indicate the corresponding source map /// location of [object] in the original input. Only one, [location] or /// [span], should be provided at a time. /// /// Indicate [isOriginal] when [object] is copied directly from the user code. /// Setting [isOriginal] will make this printer propagate source map locations /// on every line-break. void add(Object object, {SourceLocation? location, SourceSpan? span, bool isOriginal = false}) { if (object is! String || location != null || span != null || isOriginal) { _flush(); assert(location == null || span == null); if (location != null) _items.add(location); if (span != null) _items.add(span); if (isOriginal) _items.add(_original); } if (object is String) { _appendString(object); } else { _items.add(object); } } /// Append `2 * indent` spaces to this printer. void insertIndent() => _indent(indent); /// Add a [line], autoindenting to the current value of [indent]. Note, /// indentation is not inferred from the contents added to this printer. If a /// line starts or ends an indentation block, you need to also update [indent] /// accordingly. Also, indentation is not adapted for nested printers. If /// you add a [NestedPrinter] to this printer, its indentation is set /// separately and will not include any the indentation set here. /// /// The [location] and [span] parameters indicate the corresponding source map /// location of [line] in the original input. Only one, [location] or /// [span], should be provided at a time. void addLine(String? line, {SourceLocation? location, SourceSpan? span}) { if (location != null || span != null) { _flush(); assert(location == null || span == null); if (location != null) _items.add(location); if (span != null) _items.add(span); } if (line == null) return; if (line != '') { // We don't indent empty lines. _indent(indent); _appendString(line); } _appendString('\n'); } /// Appends a string merging it with any previous strings, if possible. void _appendString(String s) { var buf = _buff ??= StringBuffer(); buf.write(s); } /// Adds all of the current [_buff] contents as a string item. void _flush() { if (_buff != null) { _items.add(_buff.toString()); _buff = null; } } void _indent(int indent) { for (var i = 0; i < indent; i++) { _appendString(' '); } } /// Returns a string representation of all the contents appended to this /// printer, including source map location tokens. @override String toString() { _flush(); return (StringBuffer()..writeAll(_items)).toString(); } /// Builds the output of this printer and source map information. After /// calling this function, you can use [text] and [map] to retrieve the /// geenrated code and source map information, respectively. void build(String filename) { writeTo(printer = Printer(filename)); } /// Implements the [NestedItem] interface. @override void writeTo(Printer printer) { _flush(); var propagate = false; for (var item in _items) { if (item is NestedItem) { item.writeTo(printer); } else if (item is String) { printer.add(item, projectMarks: propagate); propagate = false; } else if (item is SourceLocation || item is SourceSpan) { printer.mark(item); } else if (item == _original) { // we insert booleans when we are about to quote text that was copied // from the original source. In such case, we will propagate marks on // every new-line. propagate = true; } else { throw UnsupportedError('Unknown item type: $item'); } } } } /// An item added to a [NestedPrinter]. abstract class NestedItem { /// Write the contents of this item into [printer]. void writeTo(Printer printer); }