1#![no_std]
21#![forbid(unsafe_code)]
22#![doc(html_root_url = "https://docs.rs/strip-indent/0.1.0")]
23
24extern crate alloc;
25
26use alloc::string::{String, ToString};
27
28#[cfg(doctest)]
30#[doc = include_str!("../README.md")]
31struct ReadmeDoctests;
32
33fn is_js_whitespace(c: char) -> bool {
35 matches!(
36 c,
37 '\u{0009}'
38 | '\u{000A}'
39 | '\u{000B}'
40 | '\u{000C}'
41 | '\u{000D}'
42 | '\u{0020}'
43 | '\u{00A0}'
44 | '\u{1680}'
45 | '\u{2000}'
46 ..='\u{200A}'
47 | '\u{2028}'
48 | '\u{2029}'
49 | '\u{202F}'
50 | '\u{205F}'
51 | '\u{3000}'
52 | '\u{FEFF}'
53 )
54}
55
56fn line_indent(line: &str) -> Option<usize> {
60 let bytes = line.as_bytes();
61 let mut indent = 0;
62 while indent < bytes.len() && (bytes[indent] == b' ' || bytes[indent] == b'\t') {
63 indent += 1;
64 }
65 match line[indent..].chars().next() {
66 Some(c) if !is_js_whitespace(c) => Some(indent),
67 _ => None,
68 }
69}
70
71#[must_use]
82pub fn min_indent(string: &str) -> usize {
83 string
84 .split('\n')
85 .filter_map(line_indent)
86 .min()
87 .unwrap_or(0)
88}
89
90#[must_use]
101pub fn strip_indent(string: &str) -> String {
102 let indent = min_indent(string);
103 if indent == 0 {
104 return string.to_string();
105 }
106
107 let mut out = String::with_capacity(string.len());
108 for (index, line) in string.split('\n').enumerate() {
109 if index > 0 {
110 out.push('\n');
111 }
112 let bytes = line.as_bytes();
113 let mut leading = 0;
114 while leading < bytes.len() && (bytes[leading] == b' ' || bytes[leading] == b'\t') {
115 leading += 1;
116 }
117 if leading >= indent {
118 out.push_str(&line[indent..]);
120 } else {
121 out.push_str(line);
122 }
123 }
124 out
125}
126
127#[must_use]
134pub fn dedent(string: &str) -> String {
135 strip_indent(trim_blank_lines(string))
136}
137
138fn trim_blank_lines(string: &str) -> &str {
140 let bytes = string.as_bytes();
141 let is_space = |b: u8| b == b' ' || b == b'\t';
142
143 let mut start = 0;
145 loop {
146 let mut j = start;
147 while j < bytes.len() && is_space(bytes[j]) {
148 j += 1;
149 }
150 if j < bytes.len() && bytes[j] == b'\n' {
151 start = j + 1;
152 } else if j + 1 < bytes.len() && bytes[j] == b'\r' && bytes[j + 1] == b'\n' {
153 start = j + 2;
154 } else {
155 break;
156 }
157 }
158
159 let mut end = bytes.len();
161 loop {
162 let mut j = end;
163 while j > start && is_space(bytes[j - 1]) {
164 j -= 1;
165 }
166 if j > start && bytes[j - 1] == b'\n' {
167 let mut newline_start = j - 1;
168 if newline_start > start && bytes[newline_start - 1] == b'\r' {
169 newline_start -= 1;
170 }
171 end = newline_start;
172 } else {
173 break;
174 }
175 }
176
177 &string[start..end]
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183
184 #[test]
185 fn strips_spaces_and_tabs() {
186 assert_eq!(strip_indent(" foo\n bar"), "foo\n bar");
187 assert_eq!(strip_indent("\tfoo\n\t\tbar"), "foo\n\tbar");
188 assert_eq!(strip_indent(" only"), "only");
189 assert_eq!(strip_indent("\n x"), "\nx");
190 assert_eq!(strip_indent(" mix\n\ttab"), " mix\ntab");
191 }
192
193 #[test]
194 fn blank_lines_and_no_indent() {
195 assert_eq!(strip_indent(" a\n\n b"), "a\n\n b");
196 assert_eq!(strip_indent("no indent"), "no indent");
197 assert_eq!(strip_indent(""), "");
198 assert_eq!(strip_indent("\n\n"), "\n\n");
199 assert_eq!(strip_indent(" trailing \n x"), "trailing \nx");
200 }
201
202 #[test]
203 fn min_indent_values() {
204 assert_eq!(min_indent(" foo\n bar"), 4);
205 assert_eq!(min_indent("\tfoo\n\t\tbar"), 1);
206 assert_eq!(min_indent(" a\n\n b"), 2);
207 assert_eq!(min_indent("no indent"), 0);
208 assert_eq!(min_indent(" only"), 3);
209 }
210
211 #[test]
212 fn dedent_trims_blank_lines() {
213 assert_eq!(dedent("\n\n foo\n bar\n\n"), "foo\nbar");
214 assert_eq!(dedent(" \n a\n b\n "), "a\nb");
215 assert_eq!(dedent("no\nchange"), "no\nchange");
216 assert_eq!(dedent("\r\n x\r\n y\r\n"), "x\r\ny");
217 assert_eq!(dedent(""), "");
218 }
219}