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_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 let actual = word.to_title_case();
228
229 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 let actual = sentence.to_words_title_case();
242
243 assert_eq!(actual, expected);
245 }
246
247 #[test]
248 fn test_list_of_sentences() {
249 let v = ["foo bar", "foO baR"];
251
252 let actual = v.make_list_all_title_case();
254
255 assert_eq!(actual, vec!["Foo Bar".to_string(), "FoO BaR".to_string()])
257 }
258
259 #[test]
260 fn test_list() {
261 let list = ["foo bar", "bizz buzz"];
263
264 let actual = list.make_title_case_list();
266
267 assert_eq!(actual, "Foo Bar, Bizz Buzz");
269 }
270
271 #[test]
272 fn test_replace_md_chars() {
273 let s = "The *brown* | fox # \tjumped*** over** the* fence.";
275
276 let actual = s.replace_md_chars();
278
279 assert_eq!(r#"The \*brown \| fox \# jumped over the fence."#, &actual);
281 }
282}