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