Add support for wrapping command descriptions and other help text.
diff --git a/lib/command_runner.dart b/lib/command_runner.dart
index 3135dbf..62f7fe7 100644
--- a/lib/command_runner.dart
+++ b/lib/command_runner.dart
@@ -39,7 +39,7 @@
///
/// This includes usage for the global arguments as well as a list of
/// top-level commands.
- String get usage => "$description\n\n$_usageWithoutDescription";
+ String get usage => _wrap("$description\n\n") + _usageWithoutDescription;
/// An optional footer for [usage].
///
@@ -49,17 +49,20 @@
/// Returns [usage] with [description] removed from the beginning.
String get _usageWithoutDescription {
+ const usagePrefix = "Usage:";
var usage = '''
-Usage: $invocation
+$usagePrefix ${_wrap(invocation, hangingIndent: usagePrefix.length)}
-Global options:
+${_wrap('Global options:')}
${argParser.usage}
-${_getCommandUsage(_commands)}
+${_getCommandUsage(_commands, lineLength: argParser.usageLineLength)}
-Run "$executableName help <command>" for more information about a command.''';
+${_wrap('Run "$executableName help <command>" for more information about a command.')}''';
- if (usageFooter != null) usage += "\n$usageFooter";
+ if (usageFooter != null) {
+ usage += "\n${_wrap(usageFooter)}";
+ }
return usage;
}
@@ -193,6 +196,9 @@
return (await command.run()) as T;
}
+
+ String _wrap(String text, {int hangingIndent}) => wrapText(text,
+ length: argParser.usageLineLength, hangingIndent: hangingIndent);
}
/// A single command.
@@ -277,7 +283,7 @@
///
/// This includes usage for the command's arguments as well as a list of
/// subcommands, if there are any.
- String get usage => "$description\n\n$_usageWithoutDescription";
+ String get usage => _wrap("$description\n\n") + _usageWithoutDescription;
/// An optional footer for [usage].
///
@@ -285,23 +291,36 @@
/// added to the end of [usage].
String get usageFooter => null;
+ String _wrap(String text, {int hangingIndent}) {
+ return wrapText(text,
+ length: argParser.usageLineLength, hangingIndent: hangingIndent);
+ }
+
/// Returns [usage] with [description] removed from the beginning.
String get _usageWithoutDescription {
+ final length = argParser.usageLineLength;
+ const usagePrefix = "Usage: ";
var buffer = new StringBuffer()
- ..writeln('Usage: $invocation')
+ ..writeln(
+ usagePrefix + _wrap(invocation, hangingIndent: usagePrefix.length))
..writeln(argParser.usage);
if (_subcommands.isNotEmpty) {
buffer.writeln();
- buffer.writeln(_getCommandUsage(_subcommands, isSubcommand: true));
+ buffer.writeln(_getCommandUsage(
+ _subcommands,
+ isSubcommand: true,
+ lineLength: length,
+ ));
}
buffer.writeln();
- buffer.write('Run "${runner.executableName} help" to see global options.');
+ buffer.write(
+ _wrap('Run "${runner.executableName} help" to see global options.'));
if (usageFooter != null) {
buffer.writeln();
- buffer.write(usageFooter);
+ buffer.write(_wrap(usageFooter));
}
return buffer.toString();
@@ -355,7 +374,8 @@
/// The return value is wrapped in a `Future` if necessary and returned by
/// [CommandRunner.runCommand].
FutureOr<T> run() {
- throw new UnimplementedError("Leaf command $this must implement run().");
+ throw new UnimplementedError(
+ _wrap("Leaf command $this must implement run()."));
}
/// Adds [Command] as a subcommand of this.
@@ -376,7 +396,7 @@
/// Throws a [UsageException] with [message].
void usageException(String message) =>
- throw new UsageException(message, _usageWithoutDescription);
+ throw new UsageException(_wrap(message), _usageWithoutDescription);
}
/// Returns a string representation of [commands] fit for use in a usage string.
@@ -384,7 +404,7 @@
/// [isSubcommand] indicates whether the commands should be called "commands" or
/// "subcommands".
String _getCommandUsage(Map<String, Command> commands,
- {bool isSubcommand: false}) {
+ {bool isSubcommand: false, int lineLength}) {
// Don't include aliases.
var names =
commands.keys.where((name) => !commands[name].aliases.contains(name));
@@ -400,13 +420,15 @@
var buffer =
new StringBuffer('Available ${isSubcommand ? "sub" : ""}commands:');
for (var name in names) {
- var lines = commands[name].summary.split("\n");
+ var columnStart = length + 5;
+ var lines = wrapTextAsLines(commands[name].summary,
+ start: columnStart, length: lineLength);
buffer.writeln();
buffer.write(' ${padRight(name, length)} ${lines.first}');
for (var line in lines.skip(1)) {
buffer.writeln();
- buffer.write(' ' * (length + 5));
+ buffer.write(' ' * columnStart);
buffer.write(line);
}
}
diff --git a/lib/src/usage.dart b/lib/src/usage.dart
index 8a1e764..01775ff 100644
--- a/lib/src/usage.dart
+++ b/lib/src/usage.dart
@@ -5,6 +5,7 @@
import 'dart:math' as math;
import '../args.dart';
+import 'utils.dart';
/// Takes an [ArgParser] and generates a string of usage (i.e. help) text for
/// its defined options.
@@ -177,45 +178,6 @@
numHelpLines = 0;
}
- /// Wraps a single line of text into lines no longer than [lineLength],
- /// starting at the [start] column.
- ///
- /// Tries to split at whitespace, but if that's not good enough to keep it
- /// under the limit, then splits in the middle of a word.
- List<String> _wrap(String text, int start) {
- assert(lineLength != null, "Should wrap when given a length.");
- assert(start >= 0);
-
- text = text.trim();
-
- var length = math.max(lineLength - start, 10);
- if (text.length <= length) return [text];
-
- var result = <String>[];
- var currentLineStart = 0;
- int lastWhitespace;
- for (var i = 0; i < text.length; ++i) {
- if (_isWhitespace(text, i)) lastWhitespace = i;
-
- if (i - currentLineStart >= length) {
- // Back up to the last whitespace, unless there wasn't any, in which
- // case we just split where we are.
- if (lastWhitespace != null) i = lastWhitespace;
-
- result.add(text.substring(currentLineStart, i));
-
- // Skip any intervening whitespace.
- while (_isWhitespace(text, i) && i < text.length) i++;
-
- currentLineStart = i;
- lastWhitespace = null;
- }
- }
-
- result.add(text.substring(currentLineStart));
- return result;
- }
-
void write(int column, String text) {
var lines = text.split('\n');
// If we are writing the last column, word wrap it to fit.
@@ -226,7 +188,8 @@
.reduce((start, width) => start += width);
for (var line in lines) {
- wrappedLines.addAll(_wrap(line, start));
+ wrappedLines
+ .addAll(wrapTextAsLines(line, start: start, length: lineLength));
}
lines = wrappedLines;
@@ -307,23 +270,3 @@
return allowedBuffer.toString();
}
}
-
-/// Returns true if the code unit at [index] in [text] is a whitespace
-/// character.
-///
-/// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode
-bool _isWhitespace(String text, int index) {
- var rune = text.codeUnitAt(index);
- return rune >= 0x0009 && rune <= 0x000D ||
- rune == 0x0020 ||
- rune == 0x0085 ||
- rune == 0x1680 ||
- rune == 0x180E ||
- rune >= 0x2000 && rune <= 0x200A ||
- rune == 0x2028 ||
- rune == 0x2029 ||
- rune == 0x202F ||
- rune == 0x205F ||
- rune == 0x3000 ||
- rune == 0xFEFF;
-}
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 2469fac..9c3972f 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -1,7 +1,147 @@
// Copyright (c) 2014, 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:math' as math;
/// Pads [source] to [length] by adding spaces at the end.
String padRight(String source, int length) =>
source + ' ' * (length - source.length);
+
+/// Wraps a block of text into lines no longer than [length].
+///
+/// Tries to split at whitespace, but if that's not good enough to keep it
+/// under the limit, then it splits in the middle of a word.
+///
+/// Preserves indentation (leading whitespace) for each line (delimited by '\n')
+/// in the input, and will indent wrapped lines the same amount.
+///
+/// If [hangingIndent] is supplied, then that many spaces will be added to each
+/// line, except for the first line. This is useful for flowing text with a
+/// heading prefix (e.g. "Usage: "):
+///
+/// ```dart
+/// var prefix = "Usage: ";
+/// print(prefix + wrapText(invocation, hangingIndent: prefix.length, length: 40));
+/// ```
+///
+/// yields:
+/// ```
+/// Usage: app main_command <subcommand>
+/// [arguments]
+/// ```
+///
+/// If [length] is not specified, then no wrapping occurs, and the original
+/// [text] is returned unchanged.
+String wrapText(String text, {int length, int hangingIndent}) {
+ if (length == null) return text;
+ hangingIndent ??= 0;
+ var splitText = text.split('\n');
+ var result = <String>[];
+ for (var line in splitText) {
+ var trimmedText = line.trimLeft();
+ final leadingWhitespace =
+ line.substring(0, line.length - trimmedText.length);
+ var notIndented;
+ if (hangingIndent != 0) {
+ // When we have a hanging indent, we want to wrap the first line at one
+ // width, and the rest at another (offset by hangingIndent), so we wrap
+ // them twice and recombine.
+ final firstLineWrap = wrapTextAsLines(
+ trimmedText,
+ length: length - leadingWhitespace.length,
+ );
+ notIndented = <String>[firstLineWrap.removeAt(0)];
+ trimmedText = trimmedText.substring(notIndented[0].length).trimLeft();
+ if (firstLineWrap.isNotEmpty) {
+ notIndented.addAll(wrapTextAsLines(
+ trimmedText,
+ length: length - leadingWhitespace.length - hangingIndent,
+ ));
+ }
+ } else {
+ notIndented = wrapTextAsLines(
+ trimmedText,
+ length: length - leadingWhitespace.length,
+ );
+ }
+ String hangingIndentString;
+ result.addAll(notIndented.map<String>(
+ (String line) {
+ // Don't return any lines with just whitespace on them.
+ if (line.isEmpty) return "";
+ var result = "${hangingIndentString ?? ""}$leadingWhitespace$line";
+ hangingIndentString ??= " " * hangingIndent;
+ return result;
+ },
+ ));
+ }
+ return result.join('\n');
+}
+
+/// Wraps a block of text into lines no longer than [length],
+/// starting at the [start] column, and returns the result as a list of strings.
+///
+/// Tries to split at whitespace, but if that's not good enough to keep it
+/// under the limit, then splits in the middle of a word. Preserves embedded
+/// newlines, but not indentation (it trims whitespace from each line).
+///
+/// If [length] is not specified, then no wrapping occurs, and the original
+/// [text] is returned after splitting it on newlines. Whitespace is not trimmed
+/// in this case.
+List<String> wrapTextAsLines(String text, {int start = 0, int length}) {
+ assert(start >= 0);
+
+ /// Returns true if the code unit at [index] in [text] is a whitespace
+ /// character.
+ ///
+ /// Based on: https://en.wikipedia.org/wiki/Whitespace_character#Unicode
+ bool isWhitespace(String text, int index) {
+ final rune = text.codeUnitAt(index);
+ return rune >= 0x0009 && rune <= 0x000D ||
+ rune == 0x0020 ||
+ rune == 0x0085 ||
+ rune == 0x1680 ||
+ rune == 0x180E ||
+ rune >= 0x2000 && rune <= 0x200A ||
+ rune == 0x2028 ||
+ rune == 0x2029 ||
+ rune == 0x202F ||
+ rune == 0x205F ||
+ rune == 0x3000 ||
+ rune == 0xFEFF;
+ }
+
+ if (length == null) return text.split('\n');
+
+ final result = <String>[];
+ final effectiveLength = math.max(length - start, 10);
+ for (var line in text.split('\n')) {
+ line = line.trim();
+ if (line.length <= effectiveLength) {
+ result.add(line);
+ continue;
+ }
+
+ var currentLineStart = 0;
+ var lastWhitespace;
+ for (var i = 0; i < line.length; ++i) {
+ if (isWhitespace(line, i)) lastWhitespace = i;
+
+ if (i - currentLineStart >= effectiveLength) {
+ // Back up to the last whitespace, unless there wasn't any, in which
+ // case we just split where we are.
+ if (lastWhitespace != null) i = lastWhitespace;
+
+ result.add(line.substring(currentLineStart, i).trim());
+
+ // Skip any intervening whitespace.
+ while (isWhitespace(line, i) && i < line.length) i++;
+
+ currentLineStart = i;
+ lastWhitespace = null;
+ }
+ }
+ result.add(line.substring(currentLineStart).trim());
+ }
+ return result;
+}
diff --git a/test/args_test.dart b/test/args_test.dart
index 8741ad8..d738f00 100644
--- a/test/args_test.dart
+++ b/test/args_test.dart
@@ -4,7 +4,7 @@
import 'package:test/test.dart';
import 'package:args/args.dart';
-import 'utils.dart';
+import 'test_utils.dart';
void main() {
group('ArgParser.addFlag()', () {
diff --git a/test/command_parse_test.dart b/test/command_parse_test.dart
index 269ed01..a5a1f56 100644
--- a/test/command_parse_test.dart
+++ b/test/command_parse_test.dart
@@ -4,7 +4,7 @@
import 'package:test/test.dart';
import 'package:args/args.dart';
-import 'utils.dart';
+import 'test_utils.dart';
void main() {
group('ArgParser.addCommand()', () {
diff --git a/test/command_runner_test.dart b/test/command_runner_test.dart
index ebb2f61..0799927 100644
--- a/test/command_runner_test.dart
+++ b/test/command_runner_test.dart
@@ -5,7 +5,7 @@
import 'package:args/command_runner.dart';
import 'package:test/test.dart';
-import 'utils.dart';
+import 'test_utils.dart';
const _DEFAULT_USAGE = """
Usage: test <command> [arguments]
@@ -330,6 +330,59 @@
});
});
+ group("with a footer and wrapping", () {
+ setUp(() {
+ runner = new CommandRunnerWithFooterAndWrapping(
+ "test", "A test command runner.");
+ });
+ test("includes the footer in the usage string", () {
+ expect(runner.usage, equals("""
+A test command runner.
+
+Usage: test <command> [arguments]
+
+Global options:
+-h, --help Print this usage
+ information.
+
+Available commands:
+ help Display help information for
+ test.
+
+Run "test help <command>" for more
+information about a command.
+LONG footer! This is a long footer, so
+we can check wrapping on long footer
+messages.
+
+And make sure that they preserve
+newlines properly."""));
+ });
+
+ test("includes the footer in usage errors", () {
+ expect(runner.run(["--bad"]),
+ throwsUsageException('Could not find an option named "bad".', """
+Usage: test <command> [arguments]
+
+Global options:
+-h, --help Print this usage
+ information.
+
+Available commands:
+ help Display help information for
+ test.
+
+Run "test help <command>" for more
+information about a command.
+LONG footer! This is a long footer, so
+we can check wrapping on long footer
+messages.
+
+And make sure that they preserve
+newlines properly."""));
+ });
+ });
+
group("throws a useful error when", () {
test("arg parsing fails", () {
expect(
diff --git a/test/command_test.dart b/test/command_test.dart
index c4f29d4..4c99fa2 100644
--- a/test/command_test.dart
+++ b/test/command_test.dart
@@ -4,7 +4,7 @@
import 'package:args/command_runner.dart';
import 'package:test/test.dart';
-import 'utils.dart';
+import 'test_utils.dart';
void main() {
var foo;
@@ -87,6 +87,58 @@
Run "test help" to see global options."""));
});
+
+ test("wraps long command descriptions with subcommands", () {
+ var wrapping = new WrappingCommand();
+
+ // Make sure [Command.runner] is set up.
+ new CommandRunner("longtest", "A long-lined test command runner.")
+ .addCommand(wrapping);
+
+ wrapping.addSubcommand(new LongCommand());
+ expect(wrapping.usage, equals("""
+This command overrides the argParser so
+that it will wrap long lines.
+
+Usage: longtest wrapping <subcommand>
+ [arguments]
+-h, --help Print this usage
+ information.
+
+Available subcommands:
+ long This command has a long
+ description that needs to be
+ wrapped sometimes.
+
+Run "longtest help" to see global
+options."""));
+ });
+
+ test("wraps long command descriptions", () {
+ var longCommand = new LongCommand();
+
+ // Make sure [Command.runner] is set up.
+ new CommandRunner("longtest", "A long-lined test command runner.")
+ .addCommand(longCommand);
+
+ expect(longCommand.usage, equals("""
+This command has a long description that
+needs to be wrapped sometimes.
+It has embedded newlines,
+ and indented lines that also need
+ to be wrapped and have their
+ indentation preserved.
+0123456789012345678901234567890123456789
+0123456789012345678901234567890123456789
+01234567890123456789
+
+Usage: longtest long [arguments]
+-h, --help Print this usage
+ information.
+
+Run "longtest help" to see global
+options."""));
+ });
});
test("usageException splits up the message and usage", () {
diff --git a/test/parse_test.dart b/test/parse_test.dart
index d212cf0..3fa2065 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -4,7 +4,7 @@
import 'package:test/test.dart';
import 'package:args/args.dart';
-import 'utils.dart';
+import 'test_utils.dart';
void main() {
group('ArgParser.parse()', () {
diff --git a/test/utils.dart b/test/test_utils.dart
similarity index 66%
rename from test/utils.dart
rename to test/test_utils.dart
index d9e4f70..99a6ba0 100644
--- a/test/utils.dart
+++ b/test/test_utils.dart
@@ -16,6 +16,20 @@
: super(executableName, description);
}
+class CommandRunnerWithFooterAndWrapping extends CommandRunner {
+ @override
+ String get usageFooter => "LONG footer! "
+ "This is a long footer, so we can check wrapping on long footer messages.\n\n"
+ "And make sure that they preserve newlines properly.";
+
+ @override
+ ArgParser get argParser => _argParser;
+ final _argParser = new ArgParser(usageLineLength: 40);
+
+ CommandRunnerWithFooterAndWrapping(String executableName, String description)
+ : super(executableName, description);
+}
+
class FooCommand extends Command {
var hasRun = false;
@@ -80,6 +94,55 @@
}
}
+class WrappingCommand extends Command {
+ var hasRun = false;
+
+ @override
+ ArgParser get argParser => _argParser;
+ final _argParser = new ArgParser(usageLineLength: 40);
+
+ @override
+ final name = "wrapping";
+
+ @override
+ final description =
+ "This command overrides the argParser so that it will wrap long lines.";
+
+ @override
+ final takesArguments = false;
+
+ @override
+ void run() {
+ hasRun = true;
+ }
+}
+
+class LongCommand extends Command {
+ var hasRun = false;
+
+ @override
+ ArgParser get argParser => _argParser;
+ final _argParser = new ArgParser(usageLineLength: 40);
+
+ @override
+ final name = "long";
+
+ @override
+ final description = "This command has a long description that needs to be "
+ "wrapped sometimes.\nIt has embedded newlines,\n"
+ " and indented lines that also need to be wrapped and have their "
+ "indentation preserved.\n" +
+ ("0123456789" * 10);
+
+ @override
+ final takesArguments = false;
+
+ @override
+ void run() {
+ hasRun = true;
+ }
+}
+
class MultilineSummaryCommand extends MultilineCommand {
@override
String get summary => description;
diff --git a/test/usage_test.dart b/test/usage_test.dart
index c556b6d..11b0f34 100644
--- a/test/usage_test.dart
+++ b/test/usage_test.dart
@@ -335,25 +335,37 @@
parser.addFlag('solid',
help:
'The-flag-with-no-whitespace-that-will-be-wrapped-by-splitting-a-word.');
+ parser.addFlag('longWhitespace',
+ help:
+ ' The flag with a really long help text and whitespace at the start.');
+ parser.addFlag('longTrailspace',
+ help:
+ 'The flag with a really long help text and whitespace at the end. ');
parser.addFlag('small1', help: ' a ');
parser.addFlag('small2', help: ' a');
parser.addFlag('small3', help: 'a ');
validateUsage(parser, '''
- --[no-]long The flag with a really long help text
- that will be wrapped.
-
- --[no-]longNewline The flag with a really long help text
- and newlines
-
- that will still be wrapped because it
- is really long.
-
- --[no-]solid The-flag-with-no-whitespace-that-will-
- be-wrapped-by-splitting-a-word.
-
- --[no-]small1 a
- --[no-]small2 a
- --[no-]small3 a
+ --[no-]long The flag with a really long help
+ text that will be wrapped.
+
+ --[no-]longNewline The flag with a really long help
+ text and newlines
+
+ that will still be wrapped because
+ it is really long.
+
+ --[no-]solid The-flag-with-no-whitespace-that-wi
+ ll-be-wrapped-by-splitting-a-word.
+
+ --[no-]longWhitespace The flag with a really long help
+ text and whitespace at the start.
+
+ --[no-]longTrailspace The flag with a really long help
+ text and whitespace at the end.
+
+ --[no-]small1 a
+ --[no-]small2 a
+ --[no-]small3 a
''');
});
@@ -370,67 +382,43 @@
parser.addFlag('solid',
help:
'The-flag-with-no-whitespace-that-will-be-wrapped-by-splitting-a-word.');
- parser.addFlag('longWhitespace',
- help:
- ' The flag with a really long help text and whitespace at the start.');
- parser.addFlag('longTrailspace',
- help:
- 'The flag with a really long help text and whitespace at the end. ');
parser.addFlag('small1', help: ' a ');
parser.addFlag('small2', help: ' a');
parser.addFlag('small3', help: 'a ');
validateUsage(parser, '''
- --[no-]long The flag
- with a
- really
- long help
- text that
- will be
- wrapped.
+ --[no-]long The flag
+ with a
+ really
+ long help
+ text that
+ will be
+ wrapped.
- --[no-]longNewline The flag
- with a
- really
- long help
- text and
- newlines
-
- that will
- still be
- wrapped
- because it
- is really
- long.
+ --[no-]longNewline The flag
+ with a
+ really
+ long help
+ text and
+ newlines
+
+ that will
+ still be
+ wrapped
+ because it
+ is really
+ long.
- --[no-]solid The-flag-w
- ith-no-whi
- tespace-th
- at-will-be
- -wrapped-b
- y-splittin
- g-a-word.
+ --[no-]solid The-flag-w
+ ith-no-whi
+ tespace-th
+ at-will-be
+ -wrapped-b
+ y-splittin
+ g-a-word.
- --[no-]longWhitespace The flag
- with a
- really
- long help
- text and
- whitespace
- at the
- start.
-
- --[no-]longTrailspace The flag
- with a
- really
- long help
- text and
- whitespace
- at the
- end.
-
- --[no-]small1 a
- --[no-]small2 a
- --[no-]small3 a
+ --[no-]small1 a
+ --[no-]small2 a
+ --[no-]small3 a
''');
});
diff --git a/test/utils_test.dart b/test/utils_test.dart
new file mode 100644
index 0000000..7c6463a
--- /dev/null
+++ b/test/utils_test.dart
@@ -0,0 +1,217 @@
+// Copyright (c) 2018, 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 'package:test/test.dart';
+
+import '../lib/src/utils.dart';
+
+const _lineLength = 40;
+const _longLine = "This is a long line that needs to be wrapped.";
+final _longLineWithNewlines = "This is a long line with newlines that\n"
+ "needs to be wrapped.\n\n" +
+ "0123456789" * 5;
+final _indentedLongLineWithNewlines =
+ " This is an indented long line with newlines that\n"
+ "needs to be wrapped.\n\tAnd preserves tabs.\n \n " +
+ "0123456789" * 5;
+const _shortLine = "Short line.";
+const _indentedLongLine = " This is an indented long line that needs to be "
+ "wrapped and indentation preserved.";
+
+void main() {
+ group("padding", () {
+ test("can pad on the right.", () {
+ expect(padRight("foo", 6), equals("foo "));
+ });
+ });
+ group("text wrapping", () {
+ test("doesn't wrap short lines.", () {
+ expect(wrapText(_shortLine, length: _lineLength), equals(_shortLine));
+ });
+ test("doesn't wrap at all if not given a length", () {
+ expect(wrapText(_longLine), equals(_longLine));
+ });
+ test("able to wrap long lines", () {
+ expect(wrapText(_longLine, length: _lineLength), equals("""
+This is a long line that needs to be
+wrapped."""));
+ });
+ test("wrap long lines with no whitespace", () {
+ expect(wrapText("0123456789" * 5, length: _lineLength), equals("""
+0123456789012345678901234567890123456789
+0123456789"""));
+ });
+ test("refuses to wrap to a column smaller than 10 characters", () {
+ expect(wrapText("$_longLine " + "0123456789" * 4, length: 1), equals("""
+This is a
+long line
+that needs
+to be
+wrapped.
+0123456789
+0123456789
+0123456789
+0123456789"""));
+ });
+ test("preserves indentation", () {
+ expect(wrapText(_indentedLongLine, length: _lineLength), equals("""
+ This is an indented long line that
+ needs to be wrapped and indentation
+ preserved."""));
+ });
+ test("preserves indentation and stripping trailing whitespace", () {
+ expect(wrapText("$_indentedLongLine ", length: _lineLength), equals("""
+ This is an indented long line that
+ needs to be wrapped and indentation
+ preserved."""));
+ });
+ test("wraps text with newlines", () {
+ expect(wrapText(_longLineWithNewlines, length: _lineLength), equals("""
+This is a long line with newlines that
+needs to be wrapped.
+
+0123456789012345678901234567890123456789
+0123456789"""));
+ });
+ test("preserves indentation in the presence of newlines", () {
+ expect(wrapText(_indentedLongLineWithNewlines, length: _lineLength),
+ equals("""
+ This is an indented long line with
+ newlines that
+needs to be wrapped.
+\tAnd preserves tabs.
+
+ 01234567890123456789012345678901234567
+ 890123456789"""));
+ });
+ test("removes trailing whitespace when wrapping", () {
+ expect(wrapText("$_longLine \t", length: _lineLength), equals("""
+This is a long line that needs to be
+wrapped."""));
+ });
+ test("preserves trailing whitespace when not wrapping", () {
+ expect(wrapText("$_longLine \t"), equals("$_longLine \t"));
+ });
+ test("honors hangingIndent parameter", () {
+ expect(
+ wrapText(_longLine, length: _lineLength, hangingIndent: 6), equals("""
+This is a long line that needs to be
+ wrapped."""));
+ });
+ test("handles hangingIndent with a single unwrapped line.", () {
+ expect(wrapText(_shortLine, length: _lineLength, hangingIndent: 6),
+ equals("""
+Short line."""));
+ });
+ test(
+ "handles hangingIndent with two unwrapped lines and the second is empty.",
+ () {
+ expect(wrapText("$_shortLine\n", length: _lineLength, hangingIndent: 6),
+ equals("""
+Short line.
+"""));
+ });
+ test("honors hangingIndent parameter on already indented line.", () {
+ expect(wrapText(_indentedLongLine, length: _lineLength, hangingIndent: 6),
+ equals("""
+ This is an indented long line that
+ needs to be wrapped and
+ indentation preserved."""));
+ });
+ test("honors hangingIndent parameter on already indented line.", () {
+ expect(
+ wrapText(_indentedLongLineWithNewlines,
+ length: _lineLength, hangingIndent: 6),
+ equals("""
+ This is an indented long line with
+ newlines that
+needs to be wrapped.
+ And preserves tabs.
+
+ 01234567890123456789012345678901234567
+ 890123456789"""));
+ });
+ });
+ group("text wrapping as lines", () {
+ test("doesn't wrap short lines.", () {
+ expect(wrapTextAsLines(_shortLine, length: _lineLength),
+ equals([_shortLine]));
+ });
+ test("doesn't wrap at all if not given a length", () {
+ expect(wrapTextAsLines(_longLine), equals([_longLine]));
+ });
+ test("able to wrap long lines", () {
+ expect(wrapTextAsLines(_longLine, length: _lineLength),
+ equals(["This is a long line that needs to be", "wrapped."]));
+ });
+ test("wrap long lines with no whitespace", () {
+ expect(wrapTextAsLines("0123456789" * 5, length: _lineLength),
+ equals(["0123456789012345678901234567890123456789", "0123456789"]));
+ });
+
+ test("refuses to wrap to a column smaller than 10 characters", () {
+ expect(
+ wrapTextAsLines("$_longLine " + "0123456789" * 4, length: 1),
+ equals([
+ "This is a",
+ "long line",
+ "that needs",
+ "to be",
+ "wrapped.",
+ "0123456789",
+ "0123456789",
+ "0123456789",
+ "0123456789"
+ ]));
+ });
+ test("doesn't preserve indentation", () {
+ expect(
+ wrapTextAsLines(_indentedLongLine, length: _lineLength),
+ equals([
+ "This is an indented long line that needs",
+ "to be wrapped and indentation preserved."
+ ]));
+ });
+ test("strips trailing whitespace", () {
+ expect(
+ wrapTextAsLines("$_indentedLongLine ", length: _lineLength),
+ equals([
+ "This is an indented long line that needs",
+ "to be wrapped and indentation preserved."
+ ]));
+ });
+ test("splits text with newlines properly", () {
+ expect(
+ wrapTextAsLines(_longLineWithNewlines, length: _lineLength),
+ equals([
+ "This is a long line with newlines that",
+ "needs to be wrapped.",
+ "",
+ "0123456789012345678901234567890123456789",
+ "0123456789"
+ ]));
+ });
+ test("does not preserves indentation in the presence of newlines", () {
+ expect(
+ wrapTextAsLines(_indentedLongLineWithNewlines, length: _lineLength),
+ equals([
+ "This is an indented long line with",
+ "newlines that",
+ "needs to be wrapped.",
+ "And preserves tabs.",
+ "",
+ "0123456789012345678901234567890123456789",
+ "0123456789"
+ ]));
+ });
+ test("removes trailing whitespace when wrapping", () {
+ expect(wrapTextAsLines("$_longLine \t", length: _lineLength),
+ equals(["This is a long line that needs to be", "wrapped."]));
+ });
+ test("preserves trailing whitespace when not wrapping", () {
+ expect(
+ wrapTextAsLines("$_longLine \t"), equals(["$_longLine \t"]));
+ });
+ });
+}