Source code

Revision control

Copy as Markdown

Other Tools

//! Functions related to adding and removing indentation from lines of
//! text.
//!
//! The functions here can be used to uniformly indent or dedent
//! (unindent) word wrapped lines of text.
/// Indent each line by the given prefix.
///
/// # Examples
///
/// ```
/// use textwrap::indent;
///
/// assert_eq!(indent("First line.\nSecond line.\n", " "),
/// " First line.\n Second line.\n");
/// ```
///
/// When indenting, trailing whitespace is stripped from the prefix.
/// This means that empty lines remain empty afterwards:
///
/// ```
/// use textwrap::indent;
///
/// assert_eq!(indent("First line.\n\n\nSecond line.\n", " "),
/// " First line.\n\n\n Second line.\n");
/// ```
///
/// Notice how `"\n\n\n"` remained as `"\n\n\n"`.
///
/// This feature is useful when you want to indent text and have a
/// space between your prefix and the text. In this case, you _don't_
/// want a trailing space on empty lines:
///
/// ```
/// use textwrap::indent;
///
/// assert_eq!(indent("foo = 123\n\nprint(foo)\n", "# "),
/// "# foo = 123\n#\n# print(foo)\n");
/// ```
///
/// Notice how `"\n\n"` became `"\n#\n"` instead of `"\n# \n"` which
/// would have trailing whitespace.
///
/// Leading and trailing whitespace coming from the text itself is
/// kept unchanged:
///
/// ```
/// use textwrap::indent;
///
/// assert_eq!(indent(" \t Foo ", "->"), "-> \t Foo ");
/// ```
pub fn indent(s: &str, prefix: &str) -> String {
// We know we'll need more than s.len() bytes for the output, but
// without counting '\n' characters (which is somewhat slow), we
// don't know exactly how much. However, we can preemptively do
// the first doubling of the output size.
let mut result = String::with_capacity(2 * s.len());
let trimmed_prefix = prefix.trim_end();
for (idx, line) in s.split_terminator('\n').enumerate() {
if idx > 0 {
result.push('\n');
}
if line.trim().is_empty() {
result.push_str(trimmed_prefix);
} else {
result.push_str(prefix);
}
result.push_str(line);
}
if s.ends_with('\n') {
// split_terminator will have eaten the final '\n'.
result.push('\n');
}
result
}
/// Removes common leading whitespace from each line.
///
/// This function will look at each non-empty line and determine the
/// maximum amount of whitespace that can be removed from all lines:
///
/// ```
/// use textwrap::dedent;
///
/// assert_eq!(dedent("
/// 1st line
/// 2nd line
/// 3rd line
/// "), "
/// 1st line
/// 2nd line
/// 3rd line
/// ");
/// ```
pub fn dedent(s: &str) -> String {
let mut prefix = "";
let mut lines = s.lines();
// We first search for a non-empty line to find a prefix.
for line in &mut lines {
let mut whitespace_idx = line.len();
for (idx, ch) in line.char_indices() {
if !ch.is_whitespace() {
whitespace_idx = idx;
break;
}
}
// Check if the line had anything but whitespace
if whitespace_idx < line.len() {
prefix = &line[..whitespace_idx];
break;
}
}
// We then continue looking through the remaining lines to
// possibly shorten the prefix.
for line in &mut lines {
let mut whitespace_idx = line.len();
for ((idx, a), b) in line.char_indices().zip(prefix.chars()) {
if a != b {
whitespace_idx = idx;
break;
}
}
// Check if the line had anything but whitespace and if we
// have found a shorter prefix
if whitespace_idx < line.len() && whitespace_idx < prefix.len() {
prefix = &line[..whitespace_idx];
}
}
// We now go over the lines a second time to build the result.
let mut result = String::new();
for line in s.lines() {
if line.starts_with(prefix) && line.chars().any(|c| !c.is_whitespace()) {
let (_, tail) = line.split_at(prefix.len());
result.push_str(tail);
}
result.push('\n');
}
if result.ends_with('\n') && !s.ends_with('\n') {
let new_len = result.len() - 1;
result.truncate(new_len);
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn indent_empty() {
assert_eq!(indent("\n", " "), "\n");
}
#[test]
#[rustfmt::skip]
fn indent_nonempty() {
let text = [
" foo\n",
"bar\n",
" baz\n",
].join("");
let expected = [
"// foo\n",
"// bar\n",
"// baz\n",
].join("");
assert_eq!(indent(&text, "// "), expected);
}
#[test]
#[rustfmt::skip]
fn indent_empty_line() {
let text = [
" foo",
"bar",
"",
" baz",
].join("\n");
let expected = [
"// foo",
"// bar",
"//",
"// baz",
].join("\n");
assert_eq!(indent(&text, "// "), expected);
}
#[test]
fn dedent_empty() {
assert_eq!(dedent(""), "");
}
#[test]
#[rustfmt::skip]
fn dedent_multi_line() {
let x = [
" foo",
" bar",
" baz",
].join("\n");
let y = [
" foo",
"bar",
" baz"
].join("\n");
assert_eq!(dedent(&x), y);
}
#[test]
#[rustfmt::skip]
fn dedent_empty_line() {
let x = [
" foo",
" bar",
" ",
" baz"
].join("\n");
let y = [
" foo",
"bar",
"",
" baz"
].join("\n");
assert_eq!(dedent(&x), y);
}
#[test]
#[rustfmt::skip]
fn dedent_blank_line() {
let x = [
" foo",
"",
" bar",
" foo",
" bar",
" baz",
].join("\n");
let y = [
"foo",
"",
" bar",
" foo",
" bar",
" baz",
].join("\n");
assert_eq!(dedent(&x), y);
}
#[test]
#[rustfmt::skip]
fn dedent_whitespace_line() {
let x = [
" foo",
" ",
" bar",
" foo",
" bar",
" baz",
].join("\n");
let y = [
"foo",
"",
" bar",
" foo",
" bar",
" baz",
].join("\n");
assert_eq!(dedent(&x), y);
}
#[test]
#[rustfmt::skip]
fn dedent_mixed_whitespace() {
let x = [
"\tfoo",
" bar",
].join("\n");
let y = [
"\tfoo",
" bar",
].join("\n");
assert_eq!(dedent(&x), y);
}
#[test]
#[rustfmt::skip]
fn dedent_tabbed_whitespace() {
let x = [
"\t\tfoo",
"\t\t\tbar",
].join("\n");
let y = [
"foo",
"\tbar",
].join("\n");
assert_eq!(dedent(&x), y);
}
#[test]
#[rustfmt::skip]
fn dedent_mixed_tabbed_whitespace() {
let x = [
"\t \tfoo",
"\t \t\tbar",
].join("\n");
let y = [
"foo",
"\tbar",
].join("\n");
assert_eq!(dedent(&x), y);
}
#[test]
#[rustfmt::skip]
fn dedent_mixed_tabbed_whitespace2() {
let x = [
"\t \tfoo",
"\t \tbar",
].join("\n");
let y = [
"\tfoo",
" \tbar",
].join("\n");
assert_eq!(dedent(&x), y);
}
#[test]
#[rustfmt::skip]
fn dedent_preserve_no_terminating_newline() {
let x = [
" foo",
" bar",
].join("\n");
let y = [
"foo",
" bar",
].join("\n");
assert_eq!(dedent(&x), y);
}
}