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}