diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /third_party/rust/textwrap/src/fill.rs | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | third_party/rust/textwrap/src/fill.rs | 298 |
1 files changed, 298 insertions, 0 deletions
diff --git a/third_party/rust/textwrap/src/fill.rs b/third_party/rust/textwrap/src/fill.rs new file mode 100644 index 0000000000..fbcaab9e21 --- /dev/null +++ b/third_party/rust/textwrap/src/fill.rs @@ -0,0 +1,298 @@ +//! Functions for filling text. + +use crate::{wrap, wrap_algorithms, Options, WordSeparator}; + +/// Fill a line of text at a given width. +/// +/// The result is a [`String`], complete with newlines between each +/// line. Use [`wrap()`] if you need access to the individual lines. +/// +/// The easiest way to use this function is to pass an integer for +/// `width_or_options`: +/// +/// ``` +/// use textwrap::fill; +/// +/// assert_eq!( +/// fill("Memory safety without garbage collection.", 15), +/// "Memory safety\nwithout garbage\ncollection." +/// ); +/// ``` +/// +/// If you need to customize the wrapping, you can pass an [`Options`] +/// instead of an `usize`: +/// +/// ``` +/// use textwrap::{fill, Options}; +/// +/// let options = Options::new(15) +/// .initial_indent("- ") +/// .subsequent_indent(" "); +/// assert_eq!( +/// fill("Memory safety without garbage collection.", &options), +/// "- Memory safety\n without\n garbage\n collection." +/// ); +/// ``` +pub fn fill<'a, Opt>(text: &str, width_or_options: Opt) -> String +where + Opt: Into<Options<'a>>, +{ + let options = width_or_options.into(); + + if text.len() < options.width && !text.contains('\n') && options.initial_indent.is_empty() { + String::from(text.trim_end_matches(' ')) + } else { + fill_slow_path(text, options) + } +} + +/// Slow path for fill. +/// +/// This is taken when `text` is longer than `options.width`. +pub(crate) fn fill_slow_path(text: &str, options: Options<'_>) -> String { + // This will avoid reallocation in simple cases (no + // indentation, no hyphenation). + let mut result = String::with_capacity(text.len()); + + let line_ending_str = options.line_ending.as_str(); + for (i, line) in wrap(text, options).iter().enumerate() { + if i > 0 { + result.push_str(line_ending_str); + } + result.push_str(line); + } + + result +} + +/// Fill `text` in-place without reallocating the input string. +/// +/// This function works by modifying the input string: some `' '` +/// characters will be replaced by `'\n'` characters. The rest of the +/// text remains untouched. +/// +/// Since we can only replace existing whitespace in the input with +/// `'\n'` (there is no space for `"\r\n"`), we cannot do hyphenation +/// nor can we split words longer than the line width. We also need to +/// use `AsciiSpace` as the word separator since we need `' '` +/// characters between words in order to replace some of them with a +/// `'\n'`. Indentation is also ruled out. In other words, +/// `fill_inplace(width)` behaves as if you had called [`fill()`] with +/// these options: +/// +/// ``` +/// # use textwrap::{core, LineEnding, Options, WordSplitter, WordSeparator, WrapAlgorithm}; +/// # let width = 80; +/// Options::new(width) +/// .break_words(false) +/// .line_ending(LineEnding::LF) +/// .word_separator(WordSeparator::AsciiSpace) +/// .wrap_algorithm(WrapAlgorithm::FirstFit) +/// .word_splitter(WordSplitter::NoHyphenation); +/// ``` +/// +/// The wrap algorithm is +/// [`WrapAlgorithm::FirstFit`](crate::WrapAlgorithm::FirstFit) since +/// this is the fastest algorithm — and the main reason to use +/// `fill_inplace` is to get the string broken into newlines as fast +/// as possible. +/// +/// A last difference is that (unlike [`fill()`]) `fill_inplace` can +/// leave trailing whitespace on lines. This is because we wrap by +/// inserting a `'\n'` at the final whitespace in the input string: +/// +/// ``` +/// let mut text = String::from("Hello World!"); +/// textwrap::fill_inplace(&mut text, 10); +/// assert_eq!(text, "Hello \nWorld!"); +/// ``` +/// +/// If we didn't do this, the word `World!` would end up being +/// indented. You can avoid this if you make sure that your input text +/// has no double spaces. +/// +/// # Performance +/// +/// In benchmarks, `fill_inplace` is about twice as fast as +/// [`fill()`]. Please see the [`linear` +/// benchmark](https://github.com/mgeisler/textwrap/blob/master/benchmarks/linear.rs) +/// for details. +pub fn fill_inplace(text: &mut String, width: usize) { + let mut indices = Vec::new(); + + let mut offset = 0; + for line in text.split('\n') { + let words = WordSeparator::AsciiSpace + .find_words(line) + .collect::<Vec<_>>(); + let wrapped_words = wrap_algorithms::wrap_first_fit(&words, &[width as f64]); + + let mut line_offset = offset; + for words in &wrapped_words[..wrapped_words.len() - 1] { + let line_len = words + .iter() + .map(|word| word.len() + word.whitespace.len()) + .sum::<usize>(); + + line_offset += line_len; + // We've advanced past all ' ' characters -- want to move + // one ' ' backwards and insert our '\n' there. + indices.push(line_offset - 1); + } + + // Advance past entire line, plus the '\n' which was removed + // by the split call above. + offset += line.len() + 1; + } + + let mut bytes = std::mem::take(text).into_bytes(); + for idx in indices { + bytes[idx] = b'\n'; + } + *text = String::from_utf8(bytes).unwrap(); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::WrapAlgorithm; + + #[test] + fn fill_simple() { + assert_eq!(fill("foo bar baz", 10), "foo bar\nbaz"); + } + + #[test] + fn fill_unicode_boundary() { + // https://github.com/mgeisler/textwrap/issues/390 + fill("\u{1b}!Ͽ", 10); + } + + #[test] + fn non_breaking_space() { + let options = Options::new(5).break_words(false); + assert_eq!(fill("foo bar baz", &options), "foo bar baz"); + } + + #[test] + fn non_breaking_hyphen() { + let options = Options::new(5).break_words(false); + assert_eq!(fill("foo‑bar‑baz", &options), "foo‑bar‑baz"); + } + + #[test] + fn fill_preserves_line_breaks_trims_whitespace() { + assert_eq!(fill(" ", 80), ""); + assert_eq!(fill(" \n ", 80), "\n"); + assert_eq!(fill(" \n \n \n ", 80), "\n\n\n"); + } + + #[test] + fn preserve_line_breaks() { + assert_eq!(fill("", 80), ""); + assert_eq!(fill("\n", 80), "\n"); + assert_eq!(fill("\n\n\n", 80), "\n\n\n"); + assert_eq!(fill("test\n", 80), "test\n"); + assert_eq!(fill("test\n\na\n\n", 80), "test\n\na\n\n"); + assert_eq!( + fill( + "1 3 5 7\n1 3 5 7", + Options::new(7).wrap_algorithm(WrapAlgorithm::FirstFit) + ), + "1 3 5 7\n1 3 5 7" + ); + assert_eq!( + fill( + "1 3 5 7\n1 3 5 7", + Options::new(5).wrap_algorithm(WrapAlgorithm::FirstFit) + ), + "1 3 5\n7\n1 3 5\n7" + ); + } + + #[test] + fn break_words_line_breaks() { + assert_eq!(fill("ab\ncdefghijkl", 5), "ab\ncdefg\nhijkl"); + assert_eq!(fill("abcdefgh\nijkl", 5), "abcde\nfgh\nijkl"); + } + + #[test] + fn break_words_empty_lines() { + assert_eq!( + fill("foo\nbar", &Options::new(2).break_words(false)), + "foo\nbar" + ); + } + + #[test] + fn fill_inplace_empty() { + let mut text = String::from(""); + fill_inplace(&mut text, 80); + assert_eq!(text, ""); + } + + #[test] + fn fill_inplace_simple() { + let mut text = String::from("foo bar baz"); + fill_inplace(&mut text, 10); + assert_eq!(text, "foo bar\nbaz"); + } + + #[test] + fn fill_inplace_multiple_lines() { + let mut text = String::from("Some text to wrap over multiple lines"); + fill_inplace(&mut text, 12); + assert_eq!(text, "Some text to\nwrap over\nmultiple\nlines"); + } + + #[test] + fn fill_inplace_long_word() { + let mut text = String::from("Internationalization is hard"); + fill_inplace(&mut text, 10); + assert_eq!(text, "Internationalization\nis hard"); + } + + #[test] + fn fill_inplace_no_hyphen_splitting() { + let mut text = String::from("A well-chosen example"); + fill_inplace(&mut text, 10); + assert_eq!(text, "A\nwell-chosen\nexample"); + } + + #[test] + fn fill_inplace_newlines() { + let mut text = String::from("foo bar\n\nbaz\n\n\n"); + fill_inplace(&mut text, 10); + assert_eq!(text, "foo bar\n\nbaz\n\n\n"); + } + + #[test] + fn fill_inplace_newlines_reset_line_width() { + let mut text = String::from("1 3 5\n1 3 5 7 9\n1 3 5 7 9 1 3"); + fill_inplace(&mut text, 10); + assert_eq!(text, "1 3 5\n1 3 5 7 9\n1 3 5 7 9\n1 3"); + } + + #[test] + fn fill_inplace_leading_whitespace() { + let mut text = String::from(" foo bar baz"); + fill_inplace(&mut text, 10); + assert_eq!(text, " foo bar\nbaz"); + } + + #[test] + fn fill_inplace_trailing_whitespace() { + let mut text = String::from("foo bar baz "); + fill_inplace(&mut text, 10); + assert_eq!(text, "foo bar\nbaz "); + } + + #[test] + fn fill_inplace_interior_whitespace() { + // To avoid an unwanted indentation of "baz", it is important + // to replace the final ' ' with '\n'. + let mut text = String::from("foo bar baz"); + fill_inplace(&mut text, 10); + assert_eq!(text, "foo bar \nbaz"); + } +} |