use std::sync::OnceLock;
use fancy_regex::Regex;
use crate::protocol::Highlighter;
use crate::repr_patterns::REPR_PATTERNS;
use crate::text::Text;
const ISO8601_PATTERNS: &[&str] = &[
r"^(?P<year>[0-9]{4})-(?P<month>1[0-2]|0[1-9])$",
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]))$",
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]))$",
r"^(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9]))$",
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]))$",
r"^(?P<time>(?P<hour>2[0-3]|[01][0-9]):?(?P<minute>[0-5][0-9]))$",
r"^(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))$",
r"^(?P<timezone>(Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?))$",
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]))?)$",
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]))$",
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]))$",
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])?$",
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])?$",
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])?$",
];
fn compile(patterns: &[&str]) -> Vec<Regex> {
patterns
.iter()
.map(|pattern| Regex::new(pattern).expect("valid highlighter pattern"))
.collect()
}
fn run_highlighter(text: &mut Text, base_style: &str, patterns: &[Regex]) {
for regex in patterns {
text.highlight_with_regex(regex, None, base_style);
}
}
pub struct RegexHighlighter {
base_style: String,
patterns: Vec<Regex>,
}
impl RegexHighlighter {
pub fn new(base_style: impl Into<String>, patterns: &[&str]) -> Self {
RegexHighlighter {
base_style: base_style.into(),
patterns: compile(patterns),
}
}
}
impl Highlighter for RegexHighlighter {
fn highlight(&self, text: &mut Text) {
run_highlighter(text, &self.base_style, &self.patterns);
}
}
#[derive(Default)]
pub struct ReprHighlighter;
impl ReprHighlighter {
pub fn new() -> Self {
ReprHighlighter
}
}
fn repr_patterns() -> &'static [Regex] {
static COMPILED: OnceLock<Vec<Regex>> = OnceLock::new();
COMPILED.get_or_init(|| compile(&REPR_PATTERNS))
}
impl Highlighter for ReprHighlighter {
fn highlight(&self, text: &mut Text) {
run_highlighter(text, "repr.", repr_patterns());
}
}
#[derive(Default)]
pub struct ISO8601Highlighter;
impl ISO8601Highlighter {
pub fn new() -> Self {
ISO8601Highlighter
}
}
fn iso8601_patterns() -> &'static [Regex] {
static COMPILED: OnceLock<Vec<Regex>> = OnceLock::new();
COMPILED.get_or_init(|| compile(ISO8601_PATTERNS))
}
impl Highlighter for ISO8601Highlighter {
fn highlight(&self, text: &mut Text) {
run_highlighter(text, "iso8601.", iso8601_patterns());
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::color::ColorSystem;
use crate::console::Console;
use crate::style::Style;
fn console() -> Console {
Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(80)
.no_color(false)
.build()
}
fn highlight(input: &str) -> String {
let mut text = Text::new(input);
ReprHighlighter::new().highlight(&mut text);
console().render_to_string(&text)
}
#[test]
fn highlight_colours_follow_the_consoles_theme() {
let mut theme = crate::theme::Theme::default_theme();
theme.insert("repr.number", Style::parse("bold red").unwrap());
let themed = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(80)
.no_color(false)
.theme(theme)
.build();
let mut text = Text::new("n = 42");
ReprHighlighter::new().highlight(&mut text);
assert_eq!(themed.render_to_string(&text), "n = \x1b[1;31m42\x1b[0m");
assert_eq!(console().render_to_string(&text), "n = \x1b[1;36m42\x1b[0m");
}
#[test]
fn custom_group_names_resolve_against_the_theme() {
let mut theme = crate::theme::Theme::default_theme();
theme.insert("sql.keyword", Style::parse("bold blue").unwrap());
let sql = Console::builder()
.force_terminal(true)
.color_system(Some(ColorSystem::Truecolor))
.width(80)
.no_color(false)
.theme(theme)
.build();
let mut text = Text::new("SELECT x");
RegexHighlighter::new("sql.", &[r"(?P<keyword>SELECT)"]).highlight(&mut text);
assert_eq!(sql.render_to_string(&text), "\x1b[1;34mSELECT\x1b[0m x");
}
fn highlight_iso(input: &str) -> String {
let mut text = Text::new(input);
ISO8601Highlighter::new().highlight(&mut text);
console().render_to_string(&text)
}
#[test]
fn highlights_numbers_bools_none() {
assert_eq!(
highlight("value = 42 and 3.14"),
"value = \x1b[1;36m42\x1b[0m and \x1b[1;36m3.14\x1b[0m"
);
assert_eq!(
highlight("flag is True, x is None"),
"flag is \x1b[3;92mTrue\x1b[0m, x is \x1b[3;35mNone\x1b[0m"
);
}
#[test]
fn highlights_paths_strings_urls_and_more() {
assert_eq!(
highlight("path /usr/bin done"),
"path \x1b[35m/usr/\x1b[0m\x1b[95mbin\x1b[0m done"
);
assert_eq!(
highlight("s = 'hello world' end"),
"s = \x1b[32m'hello world'\x1b[0m end"
);
assert_eq!(
highlight("see https://example.com/x now"),
"see \x1b[4;94mhttps://example.com/x\x1b[0m now"
);
assert_eq!(
highlight("list [1, 2, 3] and (a, b)"),
"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"
);
assert_eq!(
highlight("call func(x) here"),
"call \x1b[1;35mfunc\x1b[0m\x1b[1m(\x1b[0mx\x1b[1m)\x1b[0m here"
);
assert_eq!(
highlight("id 12345678-1234-1234-1234-123456789abc x"),
"id \x1b[93m12345678-1234-1234-1234-123456789abc\x1b[0m x"
);
assert_eq!(
highlight("ip 192.168.0.1 addr"),
"ip \x1b[1;92m192.168.0.1\x1b[0m addr"
);
assert_eq!(
highlight("ratio 3:4 and dots ..."),
"ratio \x1b[1;92m3:4\x1b[0m and dots \x1b[33m...\x1b[0m"
);
}
#[test]
fn iso8601_dates_times_and_zones() {
assert_eq!(
highlight_iso("2023-06-15"),
"\x1b[34m2023\x1b[0m\x1b[34m-\x1b[0m\x1b[34m06\x1b[0m\x1b[34m-\x1b[0m\x1b[34m15\x1b[0m"
);
assert_eq!(
highlight_iso("13:45:30"),
"\x1b[35m13\x1b[0m\x1b[35m:\x1b[0m\x1b[35m45\x1b[0m\x1b[35m:\x1b[0m\x1b[35m30\x1b[0m"
);
assert_eq!(
highlight_iso("2023-06-15T13:45:30.123+02:00"),
"\x1b[34m2023\x1b[0m\x1b[34m-\x1b[0m\x1b[34m06\x1b[0m\x1b[34m-\x1b[0m\x1b[34m15\x1b[0mT\
\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"
);
assert_eq!(highlight_iso("not a date"), "not a date");
}
#[test]
fn iso8601_compact_and_basic_forms() {
assert_eq!(
highlight_iso("20230615"),
"\x1b[34m2023\x1b[0m\x1b[34m06\x1b[0m\x1b[34m15\x1b[0m"
);
assert_eq!(
highlight_iso("2023-166"),
"\x1b[34m2023\x1b[0m\x1b[34m-\x1b[0m\x1b[34m166\x1b[0m"
);
assert_eq!(
highlight_iso("2023-W36"),
"\x1b[34m2023\x1b[0m\x1b[34m-W\x1b[0m\x1b[34m36\x1b[0m"
);
assert_eq!(
highlight_iso("2023-W36-7"),
"\x1b[34m2023\x1b[0m\x1b[34m-W\x1b[0m\x1b[34m36\x1b[0m\x1b[34m-\x1b[0m\x1b[34m7\x1b[0m"
);
assert_eq!(highlight_iso("1345"), "\x1b[35m13\x1b[0m\x1b[35m45\x1b[0m");
assert_eq!(
highlight_iso("134530"),
"\x1b[35m13\x1b[0m\x1b[35m45\x1b[0m\x1b[35m30\x1b[0m"
);
assert_eq!(highlight_iso("Z"), "\x1b[33mZ\x1b[0m");
assert_eq!(highlight_iso("+02:00"), "\x1b[33m+02:00\x1b[0m");
assert_eq!(
highlight_iso("153000Z"),
"\x1b[35m15\x1b[0m\x1b[35m30\x1b[0m\x1b[35m00\x1b[0m\x1b[33mZ\x1b[0m"
);
assert_eq!(
highlight_iso("2023-06-15 13:45:30"),
"\x1b[34m2023\x1b[0m\x1b[34m-\x1b[0m\x1b[34m06\x1b[0m\x1b[34m-\x1b[0m\x1b[34m15\x1b[0m \
\x1b[35m13\x1b[0m\x1b[35m:\x1b[0m\x1b[35m45\x1b[0m\x1b[35m:\x1b[0m\x1b[35m30\x1b[0m"
);
assert_eq!(
highlight_iso("20230615 134530"),
"\x1b[34m2023\x1b[0m\x1b[34m06\x1b[0m\x1b[34m15\x1b[0m \
\x1b[35m13\x1b[0m\x1b[35m45\x1b[0m\x1b[35m30\x1b[0m"
);
}
}