1use chrono::DateTime;
2
3use super::{MdOptions, MdParams};
4
5pub trait StringUtil {
6 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 let actual = word.to_title_case();
233
234 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 let actual = sentence.to_words_title_case();
247
248 assert_eq!(actual, expected);
250 }
251
252 #[test]
253 fn test_list_of_sentences() {
254 let v = ["foo bar", "foO baR"];
256
257 let actual = v.make_list_all_title_case();
259
260 assert_eq!(actual, vec!["Foo Bar".to_string(), "FoO BaR".to_string()])
262 }
263
264 #[test]
265 fn test_list() {
266 let list = ["foo bar", "bizz buzz"];
268
269 let actual = list.make_title_case_list();
271
272 assert_eq!(actual, "Foo Bar, Bizz Buzz");
274 }
275
276 #[test]
277 fn test_replace_md_chars() {
278 let s = "The *brown* | fox # \tjumped*** over** the* fence.";
280
281 let actual = s.replace_md_chars();
283
284 assert_eq!(r#"The \*brown \| fox \# jumped over the fence."#, &actual);
286 }
287}