icann_rdap_client/md/
string.rs

1use chrono::DateTime;
2
3use super::{MdOptions, MdParams};
4
5pub trait StringUtil {
6    fn replace_ws(self) -> String;
7    fn to_em(self, options: &MdOptions) -> String;
8    fn to_bold(self, options: &MdOptions) -> String;
9    fn to_inline(self, options: &MdOptions) -> String;
10    fn to_header(self, level: usize, options: &MdOptions) -> String;
11    fn to_right(self, width: usize, options: &MdOptions) -> String;
12    fn to_right_em(self, width: usize, options: &MdOptions) -> String;
13    fn to_right_bold(self, width: usize, options: &MdOptions) -> String;
14    fn to_left(self, width: usize, options: &MdOptions) -> String;
15    fn to_left_em(self, width: usize, options: &MdOptions) -> String;
16    fn to_left_bold(self, width: usize, options: &MdOptions) -> String;
17    fn to_center(self, width: usize, options: &MdOptions) -> String;
18    fn to_center_em(self, width: usize, options: &MdOptions) -> String;
19    fn to_center_bold(self, width: usize, options: &MdOptions) -> String;
20    fn to_title_case(self) -> String;
21    fn to_words_title_case(self) -> String;
22    fn to_cap_acronyms(self) -> String;
23    fn format_date_time(self, params: MdParams) -> Option<String>;
24}
25
26impl<T: ToString> StringUtil for T {
27    fn replace_ws(self) -> String {
28        self.to_string().replace(|c: char| c.is_whitespace(), " ")
29    }
30
31    fn to_em(self, options: &MdOptions) -> String {
32        format!(
33            "{}{}{}",
34            options.text_style_char,
35            self.to_string().replace_ws(),
36            options.text_style_char
37        )
38    }
39
40    fn to_bold(self, options: &MdOptions) -> String {
41        format!(
42            "{}{}{}{}{}",
43            options.text_style_char,
44            options.text_style_char,
45            self.to_string().replace_ws(),
46            options.text_style_char,
47            options.text_style_char
48        )
49    }
50
51    fn to_inline(self, _options: &MdOptions) -> String {
52        format!("`{}`", self.to_string().replace_ws(),)
53    }
54
55    fn to_header(self, level: usize, options: &MdOptions) -> String {
56        let s = self.to_string().replace_ws();
57        if options.hash_headers {
58            format!("{} {s}\n\n", "#".repeat(level))
59        } else {
60            let line = if level == 1 {
61                "=".repeat(s.len())
62            } else {
63                "-".repeat(s.len())
64            };
65            format!("{s}\n{line}\n\n")
66        }
67    }
68
69    fn to_right(self, width: usize, options: &MdOptions) -> String {
70        let str = self.to_string().replace_ws();
71        if options.no_unicode_chars {
72            format!("{str:>width$}")
73        } else {
74            format!("{str:\u{2003}>width$}")
75        }
76    }
77
78    fn to_right_em(self, width: usize, options: &MdOptions) -> String {
79        if options.style_in_justify {
80            self.to_em(options).to_right(width, options)
81        } else {
82            self.to_right(width, options).to_em(options)
83        }
84    }
85
86    fn to_right_bold(self, width: usize, options: &MdOptions) -> String {
87        if options.style_in_justify {
88            self.to_bold(options).to_right(width, options)
89        } else {
90            self.to_right(width, options).to_bold(options)
91        }
92    }
93
94    fn to_left(self, width: usize, options: &MdOptions) -> String {
95        let str = self.to_string().replace_ws();
96        if options.no_unicode_chars {
97            format!("{str:<width$}")
98        } else {
99            format!("{str:\u{2003}<width$}")
100        }
101    }
102
103    fn to_left_em(self, width: usize, options: &MdOptions) -> String {
104        if options.style_in_justify {
105            self.to_em(options).to_left(width, options)
106        } else {
107            self.to_left(width, options).to_em(options)
108        }
109    }
110
111    fn to_left_bold(self, width: usize, options: &MdOptions) -> String {
112        if options.style_in_justify {
113            self.to_bold(options).to_left(width, options)
114        } else {
115            self.to_left(width, options).to_bold(options)
116        }
117    }
118
119    fn to_center(self, width: usize, options: &MdOptions) -> String {
120        let str = self.to_string().replace_ws();
121        if options.no_unicode_chars {
122            format!("{str:^width$}")
123        } else {
124            format!("{str:\u{2003}^width$}")
125        }
126    }
127
128    fn to_center_em(self, width: usize, options: &MdOptions) -> String {
129        if options.style_in_justify {
130            self.to_em(options).to_center(width, options)
131        } else {
132            self.to_center(width, options).to_bold(options)
133        }
134    }
135
136    fn to_center_bold(self, width: usize, options: &MdOptions) -> String {
137        if options.style_in_justify {
138            self.to_bold(options).to_center(width, options)
139        } else {
140            self.to_center(width, options).to_bold(options)
141        }
142    }
143
144    fn to_title_case(self) -> String {
145        self.to_string()
146            .replace_ws()
147            .char_indices()
148            .map(|(i, mut c)| {
149                if i == 0 {
150                    c.make_ascii_uppercase();
151                    c
152                } else {
153                    c
154                }
155            })
156            .collect::<String>()
157    }
158
159    fn to_words_title_case(self) -> String {
160        self.to_string()
161            .replace_ws()
162            .split_whitespace()
163            .map(|s| s.to_title_case())
164            .collect::<Vec<String>>()
165            .join(" ")
166    }
167
168    fn format_date_time(self, _params: MdParams) -> Option<String> {
169        let date = DateTime::parse_from_rfc3339(&self.to_string()).ok()?;
170        Some(date.format("%a, %v %X %Z").to_string())
171    }
172
173    fn to_cap_acronyms(self) -> String {
174        self.to_string()
175            .replace_ws()
176            .replace("rdap", "RDAP")
177            .replace("icann", "ICANN")
178            .replace("arin", "ARIN")
179            .replace("ripe", "RIPE")
180            .replace("apnic", "APNIC")
181            .replace("lacnic", "LACNIC")
182            .replace("afrinic", "AFRINIC")
183            .replace("nro", "NRO")
184            .replace("ietf", "IETF")
185    }
186}
187
188pub(crate) trait StringListUtil {
189    fn make_list_all_title_case(self) -> Vec<String>;
190    fn make_title_case_list(self) -> String;
191}
192
193impl<T: ToString> StringListUtil for &[T] {
194    fn make_list_all_title_case(self) -> Vec<String> {
195        self.iter()
196            .map(|s| s.to_string().to_words_title_case())
197            .collect::<Vec<String>>()
198    }
199
200    fn make_title_case_list(self) -> String {
201        self.make_list_all_title_case().join(", ")
202    }
203}
204
205#[cfg(test)]
206#[allow(non_snake_case)]
207mod tests {
208    use rstest::rstest;
209
210    use super::{StringListUtil, StringUtil};
211
212    #[rstest]
213    #[case("foo", "Foo")]
214    #[case("FOO", "FOO")]
215    fn GIVEN_word_WHEN_make_title_case_THEN_first_char_is_upper(
216        #[case] word: &str,
217        #[case] expected: &str,
218    ) {
219        // GIVEN in arguments
220
221        // WHEN
222        let actual = word.to_title_case();
223
224        // THEN
225        assert_eq!(actual, expected);
226    }
227
228    #[rstest]
229    #[case("foo bar", "Foo Bar")]
230    #[case("foo  bar", "Foo Bar")]
231    #[case("foO  baR", "FoO BaR")]
232    fn GIVEN_sentence_WHEN_make_all_title_case_THEN_first_chars_is_upper(
233        #[case] sentence: &str,
234        #[case] expected: &str,
235    ) {
236        // GIVEN in arguments
237
238        // WHEN
239        let actual = sentence.to_words_title_case();
240
241        // THEN
242        assert_eq!(actual, expected);
243    }
244
245    #[test]
246    fn GIVEN_list_of_sentences_WHEN_make_list_all_title_case_THEN_each_sentence_all_title_cased() {
247        // GIVEN
248        let v = ["foo bar", "foO baR"];
249
250        // WHEN
251        let actual = v.make_list_all_title_case();
252
253        // THEN
254        assert_eq!(actual, vec!["Foo Bar".to_string(), "FoO BaR".to_string()])
255    }
256
257    #[test]
258    fn GIVEN_list_WHEN_make_title_case_list_THEN_comma_separated_title_cased() {
259        // GIVEN
260        let list = ["foo bar", "bizz buzz"];
261
262        // WHEN
263        let actual = list.make_title_case_list();
264
265        // THEN
266        assert_eq!(actual, "Foo Bar, Bizz Buzz");
267    }
268}