use regex::Regex;
use crate::text::Text;
use crate::theme::Theme;
fn byte_to_char_map(s: &str) -> Vec<usize> {
let mut map = vec![0; s.len() + 1];
let mut char_idx = 0;
for (byte_idx, _) in s.char_indices() {
map[byte_idx] = char_idx;
char_idx += 1;
}
map[s.len()] = char_idx;
map
}
pub trait Highlighter: Send + Sync {
fn highlight(&self, text: &mut Text);
fn highlight_text(&self, text: &Text) -> Text {
let mut result = text.clone();
self.highlight(&mut result);
result
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct NullHighlighter;
impl Highlighter for NullHighlighter {
fn highlight(&self, _text: &mut Text) {
}
}
#[derive(Debug, Clone)]
pub struct RegexHighlighter {
pub highlights: Vec<Regex>,
pub base_style: String,
theme: Theme,
}
impl RegexHighlighter {
pub fn new(patterns: &[&str], base_style: &str) -> Self {
let highlights = patterns
.iter()
.map(|p| Regex::new(p).expect("Invalid regex pattern"))
.collect();
RegexHighlighter {
highlights,
base_style: base_style.to_string(),
theme: Theme::new(),
}
}
pub fn try_new(patterns: &[&str], base_style: &str) -> Result<Self, regex::Error> {
let highlights: Result<Vec<_>, _> = patterns.iter().map(|p| Regex::new(p)).collect();
Ok(RegexHighlighter {
highlights: highlights?,
base_style: base_style.to_string(),
theme: Theme::new(),
})
}
pub fn with_theme(mut self, theme: Theme) -> Self {
self.theme = theme;
self
}
}
impl Highlighter for RegexHighlighter {
fn highlight(&self, text: &mut Text) {
let plain = text.plain_text().to_string();
let byte_to_char = byte_to_char_map(&plain);
for regex in &self.highlights {
for captures in regex.captures_iter(&plain) {
for name in regex.capture_names().flatten() {
if let Some(m) = captures.name(name) {
let start_char = byte_to_char[m.start()];
let end_char = byte_to_char[m.end()];
let style_name = format!("{}{}", self.base_style, name);
if let Some(style) = self.theme.get_style(&style_name) {
text.stylize(start_char, end_char, style);
}
}
}
}
}
}
}
pub fn combine_regex(patterns: &[&str]) -> String {
patterns.join("|")
}
pub fn repr_highlighter() -> RegexHighlighter {
repr_highlighter_with_theme(Theme::new())
}
pub fn repr_highlighter_with_theme(theme: Theme) -> RegexHighlighter {
let patterns = [
r"(?P<tag_start><)(?P<tag_name>[-\w.:|]*)(?P<tag_contents>[\w\W]*?)(?P<tag_end>>)",
r#"(?P<attrib_name>[\w_]{1,50})=(?P<attrib_value>"?[\w_]+"?)?"#,
r"(?P<brace>[\[\]{}()])",
&combine_regex(&[
r"(?P<ipv4>[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})",
r"(?P<ipv6>([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4})",
r"(?P<eui64>(?:[0-9A-Fa-f]{1,2}-){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){7}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){3}[0-9A-Fa-f]{4})",
r"(?P<eui48>(?:[0-9A-Fa-f]{1,2}-){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{1,2}:){5}[0-9A-Fa-f]{1,2}|(?:[0-9A-Fa-f]{4}\.){2}[0-9A-Fa-f]{4})",
r"(?P<uuid>[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12})",
r"(?P<call>[\w.]*?)\(",
r"\b(?P<bool_true>True)\b|\b(?P<bool_false>False)\b|\b(?P<none>None)\b",
r"(?P<ellipsis>\.\.\.)",
r"(?P<number_complex>(?:\-?[0-9]+\.?[0-9]*(?:e[-+]?\d+?)?)(?:[-+](?:[0-9]+\.?[0-9]*(?:e[-+]?\d+)?))?j)",
r"(?P<number>\b\-?[0-9]+\.?[0-9]*(e[-+]?\d+?)?\b|0x[0-9a-fA-F]*)",
r"(?P<path>\B(/[-\w._+]+)*\/)(?P<filename>[-\w._+]*)?",
r#"(?P<str>b?'''.*?'''|b?'[^']*'|b?""".*?"""|b?"[^"]*")"#,
r"(?P<url>(file|https|http|ws|wss)://[-0-9a-zA-Z$_+!`(),.?/;:&=%#~@]*)",
]),
];
let owned_patterns: Vec<String> = patterns.iter().map(|s| s.to_string()).collect();
let pattern_refs: Vec<&str> = owned_patterns.iter().map(|s| s.as_str()).collect();
RegexHighlighter::new(&pattern_refs, "repr.").with_theme(theme)
}
pub fn json_highlighter() -> RegexHighlighter {
let json_str = r#"(?P<str>"[^"\\]*(?:\\.[^"\\]*)*")"#;
let pattern = combine_regex(&[
r"(?P<brace>[\[\]{}()])",
r"\b(?P<bool_true>true)\b|\b(?P<bool_false>false)\b|\b(?P<null>null)\b",
r"(?P<number>\b\-?[0-9]+\.?[0-9]*(e[\-\+]?\d+?)?\b|0x[0-9a-fA-F]*)",
json_str,
]);
RegexHighlighter::new(&[&pattern], "json.")
}
pub fn path_highlighter() -> RegexHighlighter {
let patterns = [
r"(?P<path>(?:/[-\w._+]+)+/)(?P<filename>[-\w._+]*)",
r"(?P<path>(?:[A-Za-z]:)?(?:\\[-\w._+]+)+\\)(?P<filename>[-\w._+]*)",
];
RegexHighlighter::new(&patterns, "traceback.")
}
pub fn iso8601_highlighter() -> RegexHighlighter {
let patterns = [
r"\b(?P<year>[0-9]{4})-(?P<month>1[0-2]|0[1-9])\b",
r"\b(?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]))\b",
r"\b(?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]))\b",
r"\b(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9]))\b",
r"\b(?P<date>(?P<year>[0-9]{4})-?W(?P<week>5[0-3]|[1-4][0-9]|0[1-9])-?(?P<day>[1-7]))\b",
r"\b(?P<time>(?P<hour>2[0-3]|[01][0-9]):?(?P<minute>[0-5][0-9]))\b",
r"\b(?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))\b",
r"(?P<timezone>Z|[+-](?:2[0-3]|[01][0-9])(?::?(?:[0-5][0-9]))?)\b",
r"\b(?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"\b(?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]))[T ](?P<time>(?P<hour>2[0-3]|[01][0-9]):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9]))",
r"\b(?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]))[T ](?P<time>(?P<hour>2[0-3]|[01][0-9])(?P<minute>[0-5][0-9])(?P<second>[0-5][0-9]))",
r"\b(?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"\b(?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"\b(?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])?",
];
RegexHighlighter::new(&patterns, "iso8601.")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_null_highlighter_no_modification() {
let highlighter = NullHighlighter;
let mut text = Text::plain("hello world");
let original_text = text.plain_text().to_string();
let original_spans_len = text.spans().len();
highlighter.highlight(&mut text);
assert_eq!(text.plain_text(), original_text);
assert_eq!(text.spans().len(), original_spans_len);
}
#[test]
fn test_combine_regex() {
assert_eq!(combine_regex(&["a", "b", "c"]), "a|b|c");
assert_eq!(combine_regex(&["foo"]), "foo");
assert_eq!(combine_regex(&[]), "");
}
#[test]
fn test_combine_regex_with_groups() {
let combined = combine_regex(&[r"(?P<num>\d+)", r"(?P<word>\w+)"]);
assert_eq!(combined, r"(?P<num>\d+)|(?P<word>\w+)");
}
#[test]
fn test_regex_highlighter_new_compiles() {
let _highlighter = RegexHighlighter::new(&[r"\d+", r"\w+"], "test.");
}
#[test]
fn test_regex_highlighter_try_new_valid() {
let result = RegexHighlighter::try_new(&[r"\d+", r"\w+"], "test.");
assert!(result.is_ok());
}
#[test]
fn test_regex_highlighter_try_new_invalid() {
let result = RegexHighlighter::try_new(&[r"[invalid"], "test.");
assert!(result.is_err());
}
#[test]
#[should_panic(expected = "Invalid regex pattern")]
fn test_regex_highlighter_new_invalid_panics() {
let _highlighter = RegexHighlighter::new(&[r"[invalid"], "test.");
}
#[test]
fn test_repr_highlighter_creates() {
let highlighter = repr_highlighter();
assert!(!highlighter.highlights.is_empty());
assert_eq!(highlighter.base_style, "repr.");
}
#[test]
fn test_json_highlighter_creates() {
let highlighter = json_highlighter();
assert!(!highlighter.highlights.is_empty());
assert_eq!(highlighter.base_style, "json.");
}
#[test]
fn test_iso8601_highlighter_creates() {
let highlighter = iso8601_highlighter();
assert!(!highlighter.highlights.is_empty());
assert_eq!(highlighter.base_style, "iso8601.");
}
#[test]
fn test_path_highlighter_creates() {
let highlighter = path_highlighter();
assert!(!highlighter.highlights.is_empty());
assert_eq!(highlighter.base_style, "traceback.");
}
#[test]
fn test_regex_highlighter_basic_highlight() {
let highlighter = RegexHighlighter::new(&[r"(?P<number>\d+)"], "repr.");
let mut text = Text::plain("value is 42");
highlighter.highlight(&mut text);
assert!(!text.spans().is_empty());
let span = &text.spans()[0];
assert_eq!(span.start, 9);
assert_eq!(span.end, 11);
}
#[test]
fn test_highlight_text_returns_new_copy() {
let highlighter = RegexHighlighter::new(&[r"(?P<number>\d+)"], "repr.");
let text = Text::plain("value is 42");
let highlighted = highlighter.highlight_text(&text);
assert_eq!(text.spans().len(), 0);
assert!(!highlighted.spans().is_empty());
assert_eq!(highlighted.plain_text(), text.plain_text());
}
#[test]
fn test_null_highlighter_highlight_text() {
let highlighter = NullHighlighter;
let text = Text::plain("hello");
let result = highlighter.highlight_text(&text);
assert_eq!(result.plain_text(), "hello");
assert_eq!(result.spans().len(), 0);
}
#[test]
fn test_highlighter_trait_object() {
let highlighters: Vec<Box<dyn Highlighter>> = vec![
Box::new(NullHighlighter),
Box::new(RegexHighlighter::new(&[r"\d+"], "")),
];
let mut text = Text::plain("test 123");
for h in &highlighters {
h.highlight(&mut text);
}
}
}