1#[derive(Clone, Copy, PartialEq, Eq, Debug)]
2pub struct Indentation(pub usize);
3
4impl Indentation
5{
6 pub fn indent_str(self, text: &str) -> String
7 {
8 indent_lines(text, self.0)
9 }
10
11 pub fn unindent_str(self, text: &str) -> Result<String, Unindent_Error>
12 {
13 unindent_lines(text, self.0)
14 }
15
16 pub fn parse(code: &mut &str) -> Self
17 {
18 let index = code.find(|x| x!=' ').unwrap_or(code.len());
19 *code = &(*code)[index..];
20 return Self(index);
21 }
22}
23
24fn indent_lines(input: &str, indentation: usize) -> String
25{
26 let mut output = String::with_capacity((input.len()+1)*(indentation+1));
27
28 let indentation = std::iter::repeat(' ').take(indentation);
29 for line in input.lines()
30 {
31 if !line.is_empty()
32 {
33 output.extend(indentation.clone());
34 output.push_str(line);
35 }
36 output.push('\n');
37 }
38
39 output
40}
41
42fn unindent_lines(input: &str, indentation: usize) -> Result<String, Unindent_Error>
43{
44 let mut output = String::with_capacity(input.len());
45
46 for line in input.lines()
47 {
48 let keep_after = line.len().min(indentation);
49 if !line.as_bytes()[..keep_after].iter().all(|&x| x==b' ')
50 { return Err(Unindent_Error::NON_WS_IN_INDENTATION) }
51 output.push_str(&line[keep_after..]);
52 output.push('\n');
53 }
54
55 Ok(output)
56}
57
58impl fmt::Display for Indentation
59{
60 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
61 {
62 for _ in 0..self.0
63 {
64 f.write_char(' ')?;
65 }
66 Ok(())
67 }
68}
69
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub enum Unindent_Error
72{
73 NON_WS_IN_INDENTATION,
74}
75
76impl fmt::Display for Unindent_Error
77{
78 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result
79 {
80 use Unindent_Error::*;
81 match self
82 {
83 NON_WS_IN_INDENTATION => write!(f, "Non whitespace character in indentation"),
84 }
85 }
86}
87
88pub fn ensure_tailing_linebreak(mut xs: String) -> String
89{
90 if !xs.is_empty() && !xs.ends_with('\n')
91 {
92 xs.push('\n');
93 }
94
95 xs
96}
97
98#[cfg(test)]
99mod test
100{
101 macro_rules! indent {
102 ($i:expr, $str:expr) => {
103 crate::indentation::Indentation($i).indent_str($str).as_str()
104 };
105 }
106
107 macro_rules! unindent {
108 ($i:expr, $str:expr) => {
109 crate::indentation::Indentation($i).unindent_str($str).unwrap().as_str()
110 };
111 }
112
113 macro_rules! assert_indent {
114 ($i:expr, $unindented:expr, $indented:expr) => {
115 assert_eq!(indent!($i, $unindented), $indented);
116 assert_eq!(unindent!($i, $indented), crate::indentation::ensure_tailing_linebreak($unindented.to_owned()));
117 };
118 }
119
120 #[test]
121 fn test_trivial()
122 {
123 assert_indent!(0, "", "");
124 assert_indent!(0, "x", "x\n");
125 assert_indent!(0, "x\ny", "x\ny\n");
126 }
127
128 #[test]
129 fn test_simpl()
130 {
131 assert_indent!(2, "", "");
132 assert_indent!(2, "x", " x\n");
133 assert_indent!(2, "x\ny", " x\n y\n");
134 assert_indent!(4, "Hello, World!", " Hello, World!\n");
135 }
136
137 #[test]
138 fn test_with_lienbreak()
139 {
140 assert_indent!(2, "x\ny\nz", " x\n y\n z\n");
141 }
142
143 #[test]
144 fn test_dont_add_trailing_whitespace()
145 {
146 assert_indent!(2, "x\n\n\ny", " x\n\n\n y\n");
147 assert_indent!(2, "x\n\ny\n\n\nz", " x\n\n y\n\n\n z\n");
148 assert_indent!(2, "x\n", " x\n");
149 }
150
151 #[test]
152 fn test_difficult_cases()
153 {
154 assert_indent!(2, "\nx", "\n x\n");
155 }
156
157 #[test]
158 fn test_unindent_invalid_indentation()
159 {
160 assert_eq!(crate::indentation::Indentation(2).unindent_str("xyz"), Err(crate::indentation::Unindent_Error::NON_WS_IN_INDENTATION));
161 assert_eq!(crate::indentation::Indentation(2).unindent_str(" \n").unwrap(), "\n");
162 }
163
164 #[test]
165 fn test_parse_indent()
166 {
167 fn parse(mut code: &str) -> (usize, &str)
168 {
169 use crate::indentation::Indentation;
170 let Indentation(i) = Indentation::parse(&mut code);
171 return (i, code);
172 }
173
174 assert_eq!(parse(""), (0, ""));
175 assert_eq!(parse(" "), (1, ""));
176 assert_eq!(parse(" x"), (3, "x"));
177 }
178}
179
180use std::fmt::{self, Write};