use regex::Regex;
use crate::style::Style;
use crate::text::Text;
pub trait Highlighter {
fn highlight(&self, text: &Text) -> Text;
}
pub struct NullHighlighter;
impl Highlighter for NullHighlighter {
fn highlight(&self, text: &Text) -> Text {
text.clone()
}
}
pub struct RegexHighlighter {
rules: Vec<(Regex, Style)>,
}
impl std::fmt::Debug for RegexHighlighter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegexHighlighter")
.field("rule_count", &self.rules.len())
.finish()
}
}
impl Clone for RegexHighlighter {
fn clone(&self) -> Self {
let mut cloned = Self::new();
for (re, style) in &self.rules {
cloned.rules.push((re.clone(), style.clone()));
}
cloned
}
}
impl RegexHighlighter {
pub fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn add_rule(&mut self, pattern: &str, style: Style) -> Result<(), regex::Error> {
let re = Regex::new(pattern)?;
self.rules.push((re, style));
Ok(())
}
}
impl Highlighter for RegexHighlighter {
fn highlight(&self, text: &Text) -> Text {
let mut result = text.clone();
for (re, style) in &self.rules {
let plain = result.plain.clone();
let mut new_text = Text::new("");
let mut last_end = 0usize;
for m in re.find_iter(&plain) {
if m.start() > last_end {
new_text.append(&plain[last_end..m.start()], None);
}
new_text.append_styled(m.as_str(), style.clone());
last_end = m.end();
}
if last_end < plain.len() {
new_text.append(&plain[last_end..], None);
}
result = new_text;
}
result
}
}
#[derive(Debug, Clone)]
pub struct ReprHighlighter {
highlighter: Option<Box<RegexHighlighter>>,
}
impl ReprHighlighter {
pub fn new() -> Self {
let mut rh = RegexHighlighter::new();
let _ = rh.add_rule(
r"https?://[^\s)\]}>]+",
Style::new()
.color(crate::color::Color::parse("bright_blue").unwrap())
.underline(true),
);
let _ = rh.add_rule(
r"(?<!\w)(-?\d+\.?\d*(?:e[+-]?\d+)?)(?!\w)",
Style::new()
.color(crate::color::Color::parse("cyan").unwrap())
.bold(true),
);
let _ = rh.add_rule(
r"(?<!\w)(?:/[\w.-]+)+/?(?!\w)",
Style::new()
.color(crate::color::Color::parse("magenta").unwrap()),
);
let _ = rh.add_rule(
r#""(?:[^"\\]|\\.)*""#,
Style::new()
.color(crate::color::Color::parse("green").unwrap()),
);
let _ = rh.add_rule(
r"'(?:[^'\\]|\\.)*'",
Style::new()
.color(crate::color::Color::parse("green").unwrap()),
);
Self {
highlighter: Some(Box::new(rh)),
}
}
pub fn highlight_str(&self, text: &str) -> Text {
let t = Text::new(text);
if let Some(ref h) = self.highlighter {
h.highlight(&t)
} else {
t
}
}
}
#[derive(Debug, Clone)]
pub struct ISO8601Highlighter {
highlighter: RegexHighlighter,
}
impl ISO8601Highlighter {
pub fn new() -> Self {
let mut h = RegexHighlighter::new();
let _ = h.add_rule(
r"\b\d{4}-\d{2}-\d{2}(?:[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})?)?\b",
Style::new()
.color(crate::color::Color::parse("bright_yellow").unwrap())
.bold(true),
);
Self { highlighter: h }
}
pub fn highlight_str(&self, text: &str) -> Text {
let t = Text::new(text);
self.highlighter.highlight(&t)
}
}
impl Highlighter for ISO8601Highlighter {
fn highlight(&self, text: &Text) -> Text {
self.highlighter.highlight(text)
}
}
#[derive(Debug, Clone)]
pub struct JSONHighlighter {
highlighter: RegexHighlighter,
}
impl JSONHighlighter {
pub fn new() -> Self {
let mut h = RegexHighlighter::new();
let _ = h.add_rule(
r#""(?:[^"\\]|\\.)*"\s*:"#,
Style::new()
.color(crate::color::Color::parse("bright_cyan").unwrap()),
);
let _ = h.add_rule(
r#""(?:[^"\\]|\\.)*""#,
Style::new()
.color(crate::color::Color::parse("green").unwrap()),
);
let _ = h.add_rule(
r"(?<!\w)-?\d+\.?\d*(?:[eE][+-]?\d+)?(?!\w)",
Style::new()
.color(crate::color::Color::parse("bright_yellow").unwrap()),
);
let _ = h.add_rule(
r"\b(?:true|false|null)\b",
Style::new()
.color(crate::color::Color::parse("magenta").unwrap())
.bold(true),
);
let _ = h.add_rule(
r"[{}\[\]]",
Style::new()
.color(crate::color::Color::parse("white").unwrap())
.bold(true),
);
Self { highlighter: h }
}
pub fn highlight_str(&self, text: &str) -> Text {
let t = Text::new(text);
self.highlighter.highlight(&t)
}
}
impl Highlighter for JSONHighlighter {
fn highlight(&self, text: &Text) -> Text {
self.highlighter.highlight(text)
}
}
#[derive(Debug, Clone)]
pub struct PathHighlighter {
highlighter: RegexHighlighter,
}
impl PathHighlighter {
pub fn new() -> Self {
let mut h = RegexHighlighter::new();
let _ = h.add_rule(
r"(?:\w:)?(?:[/\\][\w.\-]+)+(?:\.\w+)?(?::\d+(?::\d+)?)?",
Style::new()
.color(crate::color::Color::parse("bright_magenta").unwrap()),
);
Self { highlighter: h }
}
pub fn highlight_str(&self, text: &str) -> Text {
let t = Text::new(text);
self.highlighter.highlight(&t)
}
}
impl Highlighter for PathHighlighter {
fn highlight(&self, text: &Text) -> Text {
self.highlighter.highlight(text)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_null_highlighter() {
let h = NullHighlighter;
let t = Text::new("hello");
let result = h.highlight(&t);
assert_eq!(result.plain, "hello");
}
#[test]
fn test_repr_highlighter_numbers() {
let h = ReprHighlighter::new();
let result = h.highlight_str("num=42");
assert!(!result.plain.is_empty());
}
#[test]
fn test_iso8601_highlighter() {
let h = ISO8601Highlighter::new();
let result = h.highlight_str("2024-01-15T10:30:00Z");
assert!(!result.plain.is_empty());
}
#[test]
fn test_json_highlighter() {
let h = JSONHighlighter::new();
let result = h.highlight_str(r#"{"key": "value", "num": 42, "flag": true}"#);
assert!(!result.plain.is_empty());
}
#[test]
fn test_path_highlighter() {
let h = PathHighlighter::new();
let result = h.highlight_str("src/main.rs:42");
assert!(!result.plain.is_empty());
}
}