blob: c0c356082b835f26e89c0043be3e30ab9ec5c2cb [file] [log] [blame]
// Copyright (c) 2022, 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.
import 'dart:convert';
import 'checks.dart' show Condition, describe;
/// Returns a pretty-printed representation of [object].
///
/// When possible, lines will be kept under [_maxLineLength]. This isn't
/// guaranteed, since individual objects may have string representations that
/// are too long, but most lines will be less than [_maxLineLength] long.
///
/// [Iterable]s and [Map]s will only print their first [_maxItems] elements or
/// key/value pairs, respectively.
Iterable<String> literal(Object? object) => _prettyPrint(object, 0, {}, true);
const _maxLineLength = 80;
const _maxItems = 25;
Iterable<String> _prettyPrint(
Object? object, int indentSize, Set<Object?> seen, bool isTopLevel) {
if (seen.contains(object)) return ['(recursive)'];
seen = seen.union({object});
Iterable<String> prettyPrintNested(Object? child) =>
_prettyPrint(child, indentSize + 2, seen, false);
if (object is Iterable) {
String open, close;
if (object is List) {
open = '[';
close = ']';
} else if (object is Set) {
open = '{';
close = '}';
} else {
open = '(';
close = ')';
}
final elements = object.map(prettyPrintNested).toList();
return _prettyPrintCollection(
open, close, elements, _maxLineLength - indentSize);
} else if (object is Map) {
final entries = object.entries.map((entry) {
final key = prettyPrintNested(entry.key);
final value = prettyPrintNested(entry.value);
return [
...key.take(key.length - 1),
'${key.last}: ${value.first}',
...value.skip(1)
];
}).toList();
return _prettyPrintCollection(
'{', '}', entries, _maxLineLength - indentSize);
} else if (object is String) {
if (object.isEmpty) return ["''"];
final escaped = const LineSplitter()
.convert(object)
.map(escape)
.map((line) => line.replaceAll("'", r"\'"))
.toList();
return prefixFirst("'", postfixLast("'", escaped));
} else if (object is Condition) {
final value = ['A value that:', ...describe(object)];
return isTopLevel ? prefixFirst('<', postfixLast('>', value)) : value;
} else {
final value = const LineSplitter().convert(object.toString());
return isTopLevel ? prefixFirst('<', postfixLast('>', value)) : value;
}
}
Iterable<String> _prettyPrintCollection(
String open, String close, List<Iterable<String>> elements, int maxLength) {
if (elements.length > _maxItems) {
elements.replaceRange(_maxItems - 1, elements.length, [
['...']
]);
}
if (elements.every((e) => e.length == 1)) {
final singleLine = '$open${elements.map((e) => e.single).join(', ')}$close';
if (singleLine.length <= maxLength) {
return [singleLine];
}
}
if (elements.length == 1) {
return prefixFirst(open, postfixLast(close, elements.single));
}
return [
...prefixFirst(open, postfixLast(',', elements.first)),
for (var element in elements.skip(1).take(elements.length - 2))
...postfixLast(',', element),
...postfixLast(close, elements.last),
];
}
Iterable<String> indent(Iterable<String> lines, [int depth = 1]) {
final indent = ' ' * depth;
return lines.map((line) => '$indent$line');
}
/// Prepends [prefix] to the first line of [lines].
///
/// If [lines] is empty, the result will be as well. The prefix will not be
/// returned for an empty input.
Iterable<String> prefixFirst(String prefix, Iterable<String> lines) sync* {
var isFirst = true;
for (var line in lines) {
if (isFirst) {
yield '$prefix$line';
isFirst = false;
} else {
yield line;
}
}
}
/// Append [postfix] to the last line of [lines].
///
/// If [lines] is empty, the result will be as well. The postfix will not be
/// returned for an empty input.
Iterable<String> postfixLast(String postfix, Iterable<String> lines) sync* {
var iterator = lines.iterator;
var hasNext = iterator.moveNext();
while (hasNext) {
final line = iterator.current;
hasNext = iterator.moveNext();
yield hasNext ? line : '$line$postfix';
}
}
/// Returns [output] with all whitespace characters represented as their escape
/// sequences.
///
/// Backslash characters are escaped as `\\`
String escape(String output) {
output = output.replaceAll('\\', r'\\');
return output.replaceAllMapped(_escapeRegExp, (match) {
var mapped = _escapeMap[match[0]];
if (mapped != null) return mapped;
return _hexLiteral(match[0]!);
});
}
/// A [RegExp] that matches whitespace characters that should be escaped.
final _escapeRegExp = RegExp(
'[\\x00-\\x07\\x0E-\\x1F${_escapeMap.keys.map(_hexLiteral).join()}]');
/// A [Map] between whitespace characters and their escape sequences.
const _escapeMap = {
'\n': r'\n',
'\r': r'\r',
'\f': r'\f',
'\b': r'\b',
'\t': r'\t',
'\v': r'\v',
'\x7F': r'\x7F', // delete
};
/// Given single-character string, return the hex-escaped equivalent.
String _hexLiteral(String input) {
var rune = input.runes.single;
return r'\x' + rune.toRadixString(16).toUpperCase().padLeft(2, '0');
}