diff options
Diffstat (limited to 'vendor/indoc/src/unindent.rs')
-rw-r--r-- | vendor/indoc/src/unindent.rs | 131 |
1 files changed, 131 insertions, 0 deletions
diff --git a/vendor/indoc/src/unindent.rs b/vendor/indoc/src/unindent.rs new file mode 100644 index 000000000..11d19d222 --- /dev/null +++ b/vendor/indoc/src/unindent.rs @@ -0,0 +1,131 @@ +use std::iter::Peekable; +use std::slice::Split; + +pub fn unindent(s: &str) -> String { + let bytes = s.as_bytes(); + let unindented = unindent_bytes(bytes); + String::from_utf8(unindented).unwrap() +} + +// Compute the maximal number of spaces that can be removed from every line, and +// remove them. +pub fn unindent_bytes(s: &[u8]) -> Vec<u8> { + // Document may start either on the same line as opening quote or + // on the next line + let ignore_first_line = s.starts_with(b"\n") || s.starts_with(b"\r\n"); + + // Largest number of spaces that can be removed from every + // non-whitespace-only line after the first + let spaces = s + .lines() + .skip(1) + .filter_map(count_spaces) + .min() + .unwrap_or(0); + + let mut result = Vec::with_capacity(s.len()); + for (i, line) in s.lines().enumerate() { + if i > 1 || (i == 1 && !ignore_first_line) { + result.push(b'\n'); + } + if i == 0 { + // Do not un-indent anything on same line as opening quote + result.extend_from_slice(line); + } else if line.len() > spaces { + // Whitespace-only lines may have fewer than the number of spaces + // being removed + result.extend_from_slice(&line[spaces..]); + } + } + result +} + +pub trait Unindent { + type Output; + + fn unindent(&self) -> Self::Output; +} + +impl Unindent for str { + type Output = String; + + fn unindent(&self) -> Self::Output { + unindent(self) + } +} + +impl Unindent for String { + type Output = String; + + fn unindent(&self) -> Self::Output { + unindent(self) + } +} + +impl Unindent for [u8] { + type Output = Vec<u8>; + + fn unindent(&self) -> Self::Output { + unindent_bytes(self) + } +} + +impl<'a, T: ?Sized + Unindent> Unindent for &'a T { + type Output = T::Output; + + fn unindent(&self) -> Self::Output { + (**self).unindent() + } +} + +// Number of leading spaces in the line, or None if the line is entirely spaces. +fn count_spaces(line: &[u8]) -> Option<usize> { + for (i, ch) in line.iter().enumerate() { + if *ch != b' ' && *ch != b'\t' { + return Some(i); + } + } + None +} + +// Based on core::str::StrExt. +trait BytesExt { + fn lines(&self) -> Lines; +} + +impl BytesExt for [u8] { + fn lines(&self) -> Lines { + fn is_newline(b: &u8) -> bool { + *b == b'\n' + } + let bytestring = if self.starts_with(b"\r\n") { + &self[1..] + } else { + self + }; + Lines { + split: bytestring.split(is_newline as fn(&u8) -> bool).peekable(), + } + } +} + +struct Lines<'a> { + split: Peekable<Split<'a, u8, fn(&u8) -> bool>>, +} + +impl<'a> Iterator for Lines<'a> { + type Item = &'a [u8]; + + fn next(&mut self) -> Option<Self::Item> { + match self.split.next() { + None => None, + Some(fragment) => { + if fragment.is_empty() && self.split.peek().is_none() { + None + } else { + Some(fragment) + } + } + } + } +} |