#![no_std]
#![forbid(unsafe_code)]
#![doc(html_root_url = "https://docs.rs/strip-indent/0.1.0")]
extern crate alloc;
use alloc::string::{String, ToString};
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
fn is_js_whitespace(c: char) -> bool {
matches!(
c,
'\u{0009}'
| '\u{000A}'
| '\u{000B}'
| '\u{000C}'
| '\u{000D}'
| '\u{0020}'
| '\u{00A0}'
| '\u{1680}'
| '\u{2000}'
..='\u{200A}'
| '\u{2028}'
| '\u{2029}'
| '\u{202F}'
| '\u{205F}'
| '\u{3000}'
| '\u{FEFF}'
)
}
fn line_indent(line: &str) -> Option<usize> {
let bytes = line.as_bytes();
let mut indent = 0;
while indent < bytes.len() && (bytes[indent] == b' ' || bytes[indent] == b'\t') {
indent += 1;
}
match line[indent..].chars().next() {
Some(c) if !is_js_whitespace(c) => Some(indent),
_ => None,
}
}
#[must_use]
pub fn min_indent(string: &str) -> usize {
string
.split('\n')
.filter_map(line_indent)
.min()
.unwrap_or(0)
}
#[must_use]
pub fn strip_indent(string: &str) -> String {
let indent = min_indent(string);
if indent == 0 {
return string.to_string();
}
let mut out = String::with_capacity(string.len());
for (index, line) in string.split('\n').enumerate() {
if index > 0 {
out.push('\n');
}
let bytes = line.as_bytes();
let mut leading = 0;
while leading < bytes.len() && (bytes[leading] == b' ' || bytes[leading] == b'\t') {
leading += 1;
}
if leading >= indent {
out.push_str(&line[indent..]);
} else {
out.push_str(line);
}
}
out
}
#[must_use]
pub fn dedent(string: &str) -> String {
strip_indent(trim_blank_lines(string))
}
fn trim_blank_lines(string: &str) -> &str {
let bytes = string.as_bytes();
let is_space = |b: u8| b == b' ' || b == b'\t';
let mut start = 0;
loop {
let mut j = start;
while j < bytes.len() && is_space(bytes[j]) {
j += 1;
}
if j < bytes.len() && bytes[j] == b'\n' {
start = j + 1;
} else if j + 1 < bytes.len() && bytes[j] == b'\r' && bytes[j + 1] == b'\n' {
start = j + 2;
} else {
break;
}
}
let mut end = bytes.len();
loop {
let mut j = end;
while j > start && is_space(bytes[j - 1]) {
j -= 1;
}
if j > start && bytes[j - 1] == b'\n' {
let mut newline_start = j - 1;
if newline_start > start && bytes[newline_start - 1] == b'\r' {
newline_start -= 1;
}
end = newline_start;
} else {
break;
}
}
&string[start..end]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strips_spaces_and_tabs() {
assert_eq!(strip_indent(" foo\n bar"), "foo\n bar");
assert_eq!(strip_indent("\tfoo\n\t\tbar"), "foo\n\tbar");
assert_eq!(strip_indent(" only"), "only");
assert_eq!(strip_indent("\n x"), "\nx");
assert_eq!(strip_indent(" mix\n\ttab"), " mix\ntab");
}
#[test]
fn blank_lines_and_no_indent() {
assert_eq!(strip_indent(" a\n\n b"), "a\n\n b");
assert_eq!(strip_indent("no indent"), "no indent");
assert_eq!(strip_indent(""), "");
assert_eq!(strip_indent("\n\n"), "\n\n");
assert_eq!(strip_indent(" trailing \n x"), "trailing \nx");
}
#[test]
fn min_indent_values() {
assert_eq!(min_indent(" foo\n bar"), 4);
assert_eq!(min_indent("\tfoo\n\t\tbar"), 1);
assert_eq!(min_indent(" a\n\n b"), 2);
assert_eq!(min_indent("no indent"), 0);
assert_eq!(min_indent(" only"), 3);
}
#[test]
fn dedent_trims_blank_lines() {
assert_eq!(dedent("\n\n foo\n bar\n\n"), "foo\nbar");
assert_eq!(dedent(" \n a\n b\n "), "a\nb");
assert_eq!(dedent("no\nchange"), "no\nchange");
assert_eq!(dedent("\r\n x\r\n y\r\n"), "x\r\ny");
assert_eq!(dedent(""), "");
}
}