trim_margin/lib.rs
1/* Copyright 2018 Christopher Bacher
2 *
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16//! This crate is intended to ease the use of multi-line strings in Rust.
17//! When embedding strings with multiple lines in Rust all whitespaces, tabs, etc. are preserved even if they are just used for layouting one's code nicely.
18//!
19//! ```
20//! fn main() {
21//! println!("-----------------------");
22//! let misrepresented_multiline_string = "
23//! This is string
24//! spans over multiple lines,
25//! but its rendering preserves all whitespaces.
26//!
27//! Which is not what we usually intend in this case.
28//! ";
29//! println!("{}", misrepresented_multiline_string);
30//! println!("-----------------------");
31//!
32//! println!("-----------------------");
33//! let correctly_layouted_string = "For displaying
34//! the a multiline strin properly
35//! it would need to be layouted
36//! like this.
37//!
38//! Which is not very nice.";
39//! println!("{}", correctly_layouted_string);
40//! println!("-----------------------");
41//! }
42//! ```
43//!
44//! The `trim-margin` crate supports you with the proper layouting.
45//! By introducing a margin in the multi-line string the `trim_margin` method can filter out unwanted whitespaces and blank lines.
46//!
47//! ```
48//! extern crate trim_margin;
49//! use trim_margin::MarginTrimmable;
50//!
51//! fn main() {
52//! let multiline_string_with_margin = "
53//! |This string has a margin
54//! |indicated by the '|' character.
55//! |
56//! |The following method call will remove ...
57//! | * a blank first/last line
58//! | * blanks before the margin prefix
59//! | * the margin prefix itself
60//! ".trim_margin().unwrap();
61//! println!("{}", multiline_string_with_margin);
62//! }
63//! ```
64
65#[cfg(test)] #[macro_use] extern crate galvanic_assert;
66
67
68/// An interface for removing the margin of multi-line string-like objects.
69pub trait MarginTrimmable {
70 /// Removes blanks and the `margin_prefix` from multiline strings.
71 ///
72 /// If the first or last line is blank (contains only whitespace, tabs, etc.) they are removed.
73 /// From each remaining line leading blank characters and the subsequent are removed
74 ///
75 /// # Returns
76 /// * The trimmed string or `None` if not every line starts with a `margin_prefix`.
77 /// * Strings without line break unmodified
78 fn trim_margin_with<M: AsRef<str>>(&self, margin_prefix: M) -> Option<String>;
79
80 /// Short-hand for `trin_margin_with("|")`.
81 fn trim_margin(&self) -> Option<String> { self.trim_margin_with("|") }
82}
83
84impl<S: AsRef<str>> MarginTrimmable for S {
85 fn trim_margin_with<M: AsRef<str>>(&self, margin_prefix: M) -> Option<String> {
86 let lines: Vec<_> = self.as_ref().split('\n').map(|line| line.trim_left()).collect();
87 if lines.len() <= 1 {
88 return Some(self.as_ref().into());
89 }
90
91 let mut with_margin: Vec<&str> = Vec::with_capacity(lines.len());
92 let mut line_iter = lines.into_iter().peekable();
93 if line_iter.peek().map_or(false, |l| l.is_empty()) {
94 line_iter.next();
95 }
96
97 let prefix = margin_prefix.as_ref();
98 while let Some(line) = line_iter.next() {
99 let is_last_line = line_iter.peek().is_none();
100 if is_last_line && line.is_empty() {
101 continue;
102 }
103 if !line.starts_with(prefix) {
104 return None;
105 }
106 with_margin.push(&line[prefix.len()..]);
107 };
108
109 Some(with_margin.join("\n"))
110 }
111}
112
113
114#[cfg(test)]
115mod tests {
116 use galvanic_assert::matchers::*;
117 use galvanic_assert::matchers::variant::*;
118 use super::*;
119
120 #[test]
121 fn should_not_modify_empty_string() {
122 assert_that!(&"".trim_margin(), maybe_some(eq(String::new())));
123 }
124
125 #[test]
126 fn should_not_modify_single_line_string() {
127 assert_that!(&"hello, world".trim_margin(), maybe_some(eq("hello, world".into())));
128 }
129
130 #[test]
131 fn should_trim_margin_of_multiline_string() {
132 let txt = "|this
133 | is a
134 | multiline string
135 |with margin";
136 assert_that!(&txt.trim_margin(),
137 maybe_some(eq(vec!["this", " is a", " multiline string", "with margin"].join("\n"))));
138 }
139
140 #[test]
141 fn should_remove_first_and_last_line_if_blank() {
142 let txt = "
143 |ignore blank
144 |surrounding lines
145 ";
146 assert_that!(&txt.trim_margin(),
147 maybe_some(eq(vec!["ignore blank", "surrounding lines"].join("\n"))));
148 }
149
150 #[test]
151 fn should_allow_arbitrary_margin_character() {
152 let txt = "
153 #ignore blank
154 #surrounding lines
155 ";
156 assert_that!(&txt.trim_margin_with("#"),
157 maybe_some(eq(vec!["ignore blank", "surrounding lines"].join("\n"))));
158 }
159}