| // 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; | |
| } |