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 let actual = word.to_title_case();
223
224 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 let actual = sentence.to_words_title_case();
240
241 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 let v = ["foo bar", "foO baR"];
249
250 let actual = v.make_list_all_title_case();
252
253 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 let list = ["foo bar", "bizz buzz"];
261
262 let actual = list.make_title_case_list();
264
265 assert_eq!(actual, "Foo Bar, Bizz Buzz");
267 }
268}