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 .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 let actual = word.to_title_case();
225
226 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 let actual = sentence.to_words_title_case();
239
240 assert_eq!(actual, expected);
242 }
243
244 #[test]
245 fn test_list_of_sentences() {
246 let v = ["foo bar", "foO baR"];
248
249 let actual = v.make_list_all_title_case();
251
252 assert_eq!(actual, vec!["Foo Bar".to_string(), "FoO BaR".to_string()])
254 }
255
256 #[test]
257 fn test_list() {
258 let list = ["foo bar", "bizz buzz"];
260
261 let actual = list.make_title_case_list();
263
264 assert_eq!(actual, "Foo Bar, Bizz Buzz");
266 }
267}