1use std::sync::OnceLock;
14
15use fancy_regex::Regex;
16
17use crate::protocol::Highlighter;
18use crate::repr_patterns::REPR_PATTERNS;
19use crate::text::Text;
20
21const ISO8601_PATTERNS: &[&str] = &[
29 r"^(?P<year>[0-9]{4})-(?P<month>1[0-2]|0[1-9])$",
31 r"^(?P<date>(?P<year>[0-9]{4})(?P<month>1[0-2]|0[1-9])(?P<day>3[01]|0[1-9]|[12][0-9]))$",
33 r"^(?P<date>(?P<year>[0-9]{4})-?(?P<day>36[0-6]|3[0-5][0-9]|[12][0-9]{2}|0[1-9][0-9]|00[1-9]))$",
35 r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9]))$",
37 r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9])-?(?P<day>[1-7]))$",
39 r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):?(?P<minute>[0-5][0-9]))$",
41 r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))$",
43 r"^(?P<timezone>(Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?))$",
45 r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?)$",
47 r"^(?P<date>(?P<year>[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9])) (?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9]))$",
49 r"^(?P<date>(?P<year>[0-9]{4})(?P<month>1[0-2]|0[1-9])(?P<day>3[01]|0[1-9]|[12][0-9])) (?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))$",
51 r"^(?P<date>(?P<year>-?(?:[1-9][0-9]*)?[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9]))(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
53 r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])(?P<frac>\.[0-9]+)?)(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
55 r"^(?P<date>(?P<year>-?(?:[1-9][0-9]*)?[0-9]{4})-(?P<month>1[0-2]|0[1-9])-(?P<day>3[01]|0[1-9]|[12][0-9]))T(?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])(?P<ms>\.[0-9]+)?)(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])?$",
57];
58
59fn compile(patterns: &[&str]) -> Vec<Regex> {
61 patterns
62 .iter()
63 .map(|pattern| Regex::new(pattern).expect("valid highlighter pattern"))
64 .collect()
65}
66
67fn run_highlighter(text: &mut Text, base_style: &str, patterns: &[Regex]) {
78 for regex in patterns {
79 text.highlight_with_regex(regex, None, base_style);
80 }
81}
82
83pub struct RegexHighlighter {
91 base_style: String,
92 patterns: Vec<Regex>,
93}
94
95impl RegexHighlighter {
96 pub fn new(base_style: impl Into<String>, patterns: &[&str]) -> Self {
97 RegexHighlighter {
98 base_style: base_style.into(),
99 patterns: compile(patterns),
100 }
101 }
102}
103
104impl Highlighter for RegexHighlighter {
105 fn highlight(&self, text: &mut Text) {
106 run_highlighter(text, &self.base_style, &self.patterns);
107 }
108}
109
110#[derive(Default)]
113pub struct ReprHighlighter;
114
115impl ReprHighlighter {
116 pub fn new() -> Self {
117 ReprHighlighter
118 }
119}
120
121fn repr_patterns() -> &'static [Regex] {
122 static COMPILED: OnceLock<Vec<Regex>> = OnceLock::new();
123 COMPILED.get_or_init(|| compile(&REPR_PATTERNS))
124}
125
126impl Highlighter for ReprHighlighter {
127 fn highlight(&self, text: &mut Text) {
128 run_highlighter(text, "repr.", repr_patterns());
129 }
130}
131
132#[derive(Default)]
135pub struct ISO8601Highlighter;
136
137impl ISO8601Highlighter {
138 pub fn new() -> Self {
139 ISO8601Highlighter
140 }
141}
142
143fn iso8601_patterns() -> &'static [Regex] {
144 static COMPILED: OnceLock<Vec<Regex>> = OnceLock::new();
145 COMPILED.get_or_init(|| compile(ISO8601_PATTERNS))
146}
147
148impl Highlighter for ISO8601Highlighter {
149 fn highlight(&self, text: &mut Text) {
150 run_highlighter(text, "iso8601.", iso8601_patterns());
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157 use crate::color::ColorSystem;
158 use crate::console::Console;
159 use crate::style::Style;
160
161 fn console() -> Console {
162 Console::builder()
163 .force_terminal(true)
164 .color_system(Some(ColorSystem::Truecolor))
165 .width(80)
166 .no_color(false)
167 .build()
168 }
169
170 fn highlight(input: &str) -> String {
171 let mut text = Text::new(input);
172 ReprHighlighter::new().highlight(&mut text);
173 console().render_to_string(&text)
174 }
175
176 #[test]
185 fn highlight_colours_follow_the_consoles_theme() {
186 let mut theme = crate::theme::Theme::default_theme();
187 theme.insert("repr.number", Style::parse("bold red").unwrap());
188 let themed = Console::builder()
189 .force_terminal(true)
190 .color_system(Some(ColorSystem::Truecolor))
191 .width(80)
192 .no_color(false)
193 .theme(theme)
194 .build();
195
196 let mut text = Text::new("n = 42");
197 ReprHighlighter::new().highlight(&mut text);
198
199 assert_eq!(themed.render_to_string(&text), "n = \x1b[1;31m42\x1b[0m");
200 assert_eq!(console().render_to_string(&text), "n = \x1b[1;36m42\x1b[0m");
203 }
204
205 #[test]
208 fn custom_group_names_resolve_against_the_theme() {
209 let mut theme = crate::theme::Theme::default_theme();
210 theme.insert("sql.keyword", Style::parse("bold blue").unwrap());
211 let sql = Console::builder()
212 .force_terminal(true)
213 .color_system(Some(ColorSystem::Truecolor))
214 .width(80)
215 .no_color(false)
216 .theme(theme)
217 .build();
218
219 let mut text = Text::new("SELECT x");
220 RegexHighlighter::new("sql.", &[r"(?P<keyword>SELECT)"]).highlight(&mut text);
221 assert_eq!(sql.render_to_string(&text), "\x1b[1;34mSELECT\x1b[0m x");
222 }
223
224 fn highlight_iso(input: &str) -> String {
225 let mut text = Text::new(input);
226 ISO8601Highlighter::new().highlight(&mut text);
227 console().render_to_string(&text)
228 }
229
230 #[test]
231 fn highlights_numbers_bools_none() {
232 assert_eq!(
234 highlight("value = 42 and 3.14"),
235 "value = \x1b[1;36m42\x1b[0m and \x1b[1;36m3.14\x1b[0m"
236 );
237 assert_eq!(
238 highlight("flag is True, x is None"),
239 "flag is \x1b[3;92mTrue\x1b[0m, x is \x1b[3;35mNone\x1b[0m"
240 );
241 }
242
243 #[test]
244 fn highlights_paths_strings_urls_and_more() {
245 assert_eq!(
247 highlight("path /usr/bin done"),
248 "path \x1b[35m/usr/\x1b[0m\x1b[95mbin\x1b[0m done"
249 );
250 assert_eq!(
251 highlight("s = 'hello world' end"),
252 "s = \x1b[32m'hello world'\x1b[0m end"
253 );
254 assert_eq!(
255 highlight("see https://example.com/x now"),
256 "see \x1b[4;94mhttps://example.com/x\x1b[0m now"
257 );
258 assert_eq!(
259 highlight("list [1, 2, 3] and (a, b)"),
260 "list \x1b[1m[\x1b[0m\x1b[1;36m1\x1b[0m, \x1b[1;36m2\x1b[0m, \x1b[1;36m3\x1b[0m\x1b[1m]\x1b[0m and \x1b[1m(\x1b[0ma, b\x1b[1m)\x1b[0m"
261 );
262 assert_eq!(
263 highlight("call func(x) here"),
264 "call \x1b[1;35mfunc\x1b[0m\x1b[1m(\x1b[0mx\x1b[1m)\x1b[0m here"
265 );
266 assert_eq!(
267 highlight("id 12345678-1234-1234-1234-123456789abc x"),
268 "id \x1b[93m12345678-1234-1234-1234-123456789abc\x1b[0m x"
269 );
270 assert_eq!(
271 highlight("ip 192.168.0.1 addr"),
272 "ip \x1b[1;92m192.168.0.1\x1b[0m addr"
273 );
274 assert_eq!(
276 highlight("ratio 3:4 and dots ..."),
277 "ratio \x1b[1;92m3:4\x1b[0m and dots \x1b[33m...\x1b[0m"
278 );
279 }
280
281 #[test]
282 fn iso8601_dates_times_and_zones() {
283 assert_eq!(
287 highlight_iso("2023-06-15"),
288 "\x1b[34m2023\x1b[0m\x1b[34m-\x1b[0m\x1b[34m06\x1b[0m\x1b[34m-\x1b[0m\x1b[34m15\x1b[0m"
289 );
290 assert_eq!(
291 highlight_iso("13:45:30"),
292 "\x1b[35m13\x1b[0m\x1b[35m:\x1b[0m\x1b[35m45\x1b[0m\x1b[35m:\x1b[0m\x1b[35m30\x1b[0m"
293 );
294 assert_eq!(
295 highlight_iso("2023-06-15T13:45:30.123+02:00"),
296 "\x1b[34m2023\x1b[0m\x1b[34m-\x1b[0m\x1b[34m06\x1b[0m\x1b[34m-\x1b[0m\x1b[34m15\x1b[0mT\
297 \x1b[35m13\x1b[0m\x1b[35m:\x1b[0m\x1b[35m45\x1b[0m\x1b[35m:\x1b[0m\x1b[35m30\x1b[0m\x1b[35m.123\x1b[0m\x1b[33m+02:00\x1b[0m"
298 );
299 assert_eq!(highlight_iso("not a date"), "not a date");
300 }
301
302 #[test]
303 fn iso8601_compact_and_basic_forms() {
304 assert_eq!(
307 highlight_iso("20230615"),
308 "\x1b[34m2023\x1b[0m\x1b[34m06\x1b[0m\x1b[34m15\x1b[0m"
309 );
310 assert_eq!(
311 highlight_iso("2023-166"),
312 "\x1b[34m2023\x1b[0m\x1b[34m-\x1b[0m\x1b[34m166\x1b[0m"
313 );
314 assert_eq!(
315 highlight_iso("2023-W36"),
316 "\x1b[34m2023\x1b[0m\x1b[34m-W\x1b[0m\x1b[34m36\x1b[0m"
317 );
318 assert_eq!(
319 highlight_iso("2023-W36-7"),
320 "\x1b[34m2023\x1b[0m\x1b[34m-W\x1b[0m\x1b[34m36\x1b[0m\x1b[34m-\x1b[0m\x1b[34m7\x1b[0m"
321 );
322 assert_eq!(highlight_iso("1345"), "\x1b[35m13\x1b[0m\x1b[35m45\x1b[0m");
323 assert_eq!(
324 highlight_iso("134530"),
325 "\x1b[35m13\x1b[0m\x1b[35m45\x1b[0m\x1b[35m30\x1b[0m"
326 );
327 assert_eq!(highlight_iso("Z"), "\x1b[33mZ\x1b[0m");
328 assert_eq!(highlight_iso("+02:00"), "\x1b[33m+02:00\x1b[0m");
329 assert_eq!(
330 highlight_iso("153000Z"),
331 "\x1b[35m15\x1b[0m\x1b[35m30\x1b[0m\x1b[35m00\x1b[0m\x1b[33mZ\x1b[0m"
332 );
333 assert_eq!(
335 highlight_iso("2023-06-15 13:45:30"),
336 "\x1b[34m2023\x1b[0m\x1b[34m-\x1b[0m\x1b[34m06\x1b[0m\x1b[34m-\x1b[0m\x1b[34m15\x1b[0m \
337 \x1b[35m13\x1b[0m\x1b[35m:\x1b[0m\x1b[35m45\x1b[0m\x1b[35m:\x1b[0m\x1b[35m30\x1b[0m"
338 );
339 assert_eq!(
340 highlight_iso("20230615 134530"),
341 "\x1b[34m2023\x1b[0m\x1b[34m06\x1b[0m\x1b[34m15\x1b[0m \
342 \x1b[35m13\x1b[0m\x1b[35m45\x1b[0m\x1b[35m30\x1b[0m"
343 );
344 }
345}