| // 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 indents wrapped lines the same amount. |
| /// |
| /// If [hangingIndent] is supplied, then that many spaces are 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); |
| List<String> 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. |
| var firstLineWrap = wrapTextAsLines(trimmedText, |
| length: length - leadingWhitespace.length); |
| notIndented = [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) { |
| 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; |
| } |
| |
| if (length == null) return text.split('\n'); |
| |
| var result = <String>[]; |
| var 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; |
| int? 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; |
| } |