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