// 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 /// [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 = []; for (var line in splitText) { var trimmedText = line.trimLeft(); final leadingWhitespace = line.substring(0, line.length - trimmedText.length); List 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 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 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 = []; 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; }