icann_rdap_client/md/
string.rs

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