Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: Try wrapping text without actually allocating any space #545

Open
kkew3 opened this issue Aug 28, 2024 · 0 comments
Open

Comments

@kkew3
Copy link

kkew3 commented Aug 28, 2024

I roughly scan over the code, and don't find similar functionality. So this post can be regarded as a feature request, and I'm glad to PR if possible.

Proposal

I'm seeking for a function to try wrapping a paragraph, and return a vec of displayed widths. The signature of such function could be:

fn try_wrap<'a, Opt: Into<Options<'a>>>(text: &str, width_or_options: Opt) -> Vec<usize>;

An example use would be:

// assert_eq!(wrap("  foo bar", 4), vec!["", "foo", "bar"]);
assert_eq!(try_wrap("  foo bar", 4), vec![0, 3, 3]);

Use case

For example, one may want to find the smallest width such that any wrapped line won't protrude out, without the need to actually wrap the text into string.

Implementation

A naive implementation is to mimic this function:

textwrap/src/wrap.rs

Lines 215 to 292 in 6397036

pub(crate) fn wrap_single_line_slow_path<'a>(
line: &'a str,
options: &Options<'_>,
lines: &mut Vec<Cow<'a, str>>,
) {
let initial_width = options
.width
.saturating_sub(display_width(options.initial_indent));
let subsequent_width = options
.width
.saturating_sub(display_width(options.subsequent_indent));
let line_widths = [initial_width, subsequent_width];
let words = options.word_separator.find_words(line);
let split_words = split_words(words, &options.word_splitter);
let broken_words = if options.break_words {
let mut broken_words = break_words(split_words, line_widths[1]);
if !options.initial_indent.is_empty() {
// Without this, the first word will always go into the
// first line. However, since we break words based on the
// _second_ line width, it can be wrong to unconditionally
// put the first word onto the first line. An empty
// zero-width word fixed this.
broken_words.insert(0, Word::from(""));
}
broken_words
} else {
split_words.collect::<Vec<_>>()
};
let wrapped_words = options.wrap_algorithm.wrap(&broken_words, &line_widths);
let mut idx = 0;
for words in wrapped_words {
let last_word = match words.last() {
None => {
lines.push(Cow::from(""));
continue;
}
Some(word) => word,
};
// We assume here that all words are contiguous in `line`.
// That is, the sum of their lengths should add up to the
// length of `line`.
let len = words
.iter()
.map(|word| word.len() + word.whitespace.len())
.sum::<usize>()
- last_word.whitespace.len();
// The result is owned if we have indentation, otherwise we
// can simply borrow an empty string.
let mut result = if lines.is_empty() && !options.initial_indent.is_empty() {
Cow::Owned(options.initial_indent.to_owned())
} else if !lines.is_empty() && !options.subsequent_indent.is_empty() {
Cow::Owned(options.subsequent_indent.to_owned())
} else {
// We can use an empty string here since string
// concatenation for `Cow` preserves a borrowed value when
// either side is empty.
Cow::from("")
};
result += &line[idx..idx + len];
if !last_word.penalty.is_empty() {
result.to_mut().push_str(last_word.penalty);
}
lines.push(result);
// Advance by the length of `result`, plus the length of
// `last_word.whitespace` -- even if we had a penalty, we need
// to skip over the whitespace.
idx += len + last_word.whitespace.len();
}
}

but replace all lines.push(x) with counts.push(textwrap::core::display_width(&x), where counts is the vec to return.
The downside is that there could be a lot of duplicate code.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant