use crate::core::config::*;
use crate::core::span_pipeline::Pipeline;
use crate::core::span_pipeline::finders::date_dash::DateDashFinder;
use crate::core::span_pipeline::finders::date_time::DateTimeFinder;
use crate::core::span_pipeline::finders::duration::DurationFinder;
use crate::core::span_pipeline::finders::email::EmailFinder;
use crate::core::span_pipeline::finders::ip_v4::IpV4Finder;
use crate::core::span_pipeline::finders::ip_v6::IpV6Finder;
use crate::core::span_pipeline::finders::json::JsonFinder;
use crate::core::span_pipeline::finders::jvm_stack::JvmStackFinder;
use crate::core::span_pipeline::finders::key_value::KeyValueFinder;
use crate::core::span_pipeline::finders::keyword::KeywordFinder;
use crate::core::span_pipeline::finders::number::NumberFinder;
use crate::core::span_pipeline::finders::pointer::PointerFinder;
use crate::core::span_pipeline::finders::quote::QuoteFinder;
use crate::core::span_pipeline::finders::regex::RegexFinder;
use crate::core::span_pipeline::finders::unix_path::UnixPathFinder;
use crate::core::span_pipeline::finders::unix_process::UnixProcessFinder;
use crate::core::span_pipeline::finders::url::UrlFinder;
use crate::core::span_pipeline::finders::uuid::UuidFinder;
use crate::core::span_pipeline::palette::Palette;
use crate::core::span_pipeline::span::Finder;
use std::borrow::Cow;
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct Highlighter {
inner: Pipeline,
}
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("Regex error: {0}")]
Regex(#[from] regex::Error),
#[error("Pattern error: {0}")]
Pattern(#[from] aho_corasick::BuildError),
}
impl Highlighter {
pub const fn builder() -> HighlighterBuilder {
HighlighterBuilder {
finders: Vec::new(),
palette: Palette::new(),
first_error: None,
}
}
#[must_use]
pub fn apply<'a>(&self, input: &'a str) -> Cow<'a, str> {
self.inner.apply(input)
}
}
impl Default for Highlighter {
fn default() -> Self {
Highlighter::builder()
.with_json_highlighter(JsonConfig::default())
.with_date_time_highlighter(DateTimeConfig::default())
.with_ip_v4_highlighter(IpV4Config::default())
.with_url_highlighter(UrlConfig::default())
.with_email_highlighter(EmailConfig::default())
.with_unix_path_highlighter(UnixPathConfig::default())
.with_key_value_highlighter(KeyValueConfig::default())
.with_uuid_highlighter(UuidConfig::default())
.with_pointer_highlighter(PointerConfig::default())
.with_unix_process_highlighter(UnixProcessConfig::default())
.with_duration_highlighter(DurationConfig::default())
.with_number_highlighter(NumberConfig::default())
.with_quote_highlighter(QuoteConfig::default())
.build()
.expect("Default constructor should never fail")
}
}
#[derive(Debug)]
#[must_use]
pub struct HighlighterBuilder {
finders: Vec<Box<dyn Finder>>,
palette: Palette,
first_error: Option<Error>,
}
impl HighlighterBuilder {
pub fn with_number_highlighter(mut self, config: NumberConfig) -> Self {
let finder = NumberFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_uuid_highlighter(mut self, config: UuidConfig) -> Self {
let finder = UuidFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_unix_path_highlighter(mut self, config: UnixPathConfig) -> Self {
let finder = UnixPathFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_unix_process_highlighter(mut self, config: UnixProcessConfig) -> Self {
let finder = UnixProcessFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_duration_highlighter(mut self, config: DurationConfig) -> Self {
let finder = DurationFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_key_value_highlighter(mut self, config: KeyValueConfig) -> Self {
let finder = KeyValueFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_date_time_highlighter(mut self, config: DateTimeConfig) -> Self {
let date_time = DateTimeFinder::new(config, &mut self.palette);
self.add_finder(date_time);
let date_dash = DateDashFinder::new(config, &mut self.palette);
self.add_finder(date_dash);
self
}
pub fn with_ip_v6_highlighter(mut self, config: IpV6Config) -> Self {
let finder = IpV6Finder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_jvm_stack_trace_highlighter(mut self, config: JvmStackTraceConfig) -> Self {
let finder = JvmStackFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_ip_v4_highlighter(mut self, config: IpV4Config) -> Self {
let finder = IpV4Finder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_url_highlighter(mut self, config: UrlConfig) -> Self {
let finder = UrlFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_email_highlighter(mut self, config: EmailConfig) -> Self {
let finder = EmailFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_pointer_highlighter(mut self, config: PointerConfig) -> Self {
let finder = PointerFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_regex_highlighter(mut self, config: RegexConfig) -> Self {
let finder = RegexFinder::new(&config.regex, config.style, &mut self.palette).map_err(Error::Regex);
self.try_add_finder(finder);
self
}
pub fn with_quote_highlighter(mut self, config: QuoteConfig) -> Self {
let finder = QuoteFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_json_highlighter(mut self, config: JsonConfig) -> Self {
let finder = JsonFinder::new(config, &mut self.palette);
self.add_finder(finder);
self
}
pub fn with_keyword_highlighters(mut self, keyword_configs: Vec<KeywordConfig>) -> Self {
let finder = KeywordFinder::new(&keyword_configs, &mut self.palette).map_err(Error::Pattern);
self.try_add_finder(finder);
self
}
pub fn build(self) -> Result<Highlighter, Error> {
if let Some(err) = self.first_error {
Err(err)
} else {
Ok(Highlighter {
inner: Pipeline::new(self.finders, self.palette),
})
}
}
fn add_finder<F: Finder + 'static>(&mut self, finder: F) {
self.try_add_finder(Ok(finder));
}
fn try_add_finder<F: Finder + 'static>(&mut self, finder: Result<F, Error>) {
if self.first_error.is_some() {
return;
}
match finder {
Ok(f) => self.finders.push(Box::new(f)),
Err(e) => self.first_error = Some(e),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::tests::escape_code_converter::ConvertEscapeCodes;
use crate::style::{Color, Style};
fn number_then_quote_highlighter() -> Highlighter {
Highlighter::builder()
.with_number_highlighter(NumberConfig {
style: Style::new().fg(Color::Cyan),
})
.with_quote_highlighter(QuoteConfig {
quote_token: b'"',
style: Style::new().fg(Color::Yellow),
})
.build()
.unwrap()
}
#[test]
fn test_quote_highlights_around_existing_number() {
let highlighter = number_then_quote_highlighter();
let input = r#"count is "value 42 here" end"#;
let expected = r#"count is [yellow]"value [reset][cyan]42[reset][yellow] here"[reset] end"#;
let actual = highlighter.apply(input);
assert_eq!(actual.to_string().convert_escape_codes(), expected);
}
#[test]
fn test_quote_with_no_highlights_inside() {
let highlighter = number_then_quote_highlighter();
let input = r#"msg "hello world" end"#;
let expected = r#"msg [yellow]"hello world"[reset] end"#;
let actual = highlighter.apply(input);
assert_eq!(actual.to_string().convert_escape_codes(), expected);
}
#[test]
fn test_number_outside_quotes_unaffected() {
let highlighter = number_then_quote_highlighter();
let input = r#"code 200 "error" end"#;
let expected = r#"code [cyan]200[reset] [yellow]"error"[reset] end"#;
let actual = highlighter.apply(input);
assert_eq!(actual.to_string().convert_escape_codes(), expected);
}
#[test]
fn test_multiple_numbers_inside_quotes() {
let highlighter = number_then_quote_highlighter();
let input = r#""port 8080 and 443""#;
let expected = r#"[yellow]"port [reset][cyan]8080[reset][yellow] and [reset][cyan]443[reset][yellow]"[reset]"#;
let actual = highlighter.apply(input);
assert_eq!(actual.to_string().convert_escape_codes(), expected);
}
#[test]
fn test_multiple_quoted_segments() {
let highlighter = number_then_quote_highlighter();
let input = r#""count 1" and "count 2""#;
let expected = r#"[yellow]"count [reset][cyan]1[reset][yellow]"[reset] and [yellow]"count [reset][cyan]2[reset][yellow]"[reset]"#;
let actual = highlighter.apply(input);
assert_eq!(actual.to_string().convert_escape_codes(), expected);
}
#[test]
fn test_no_quotes_only_numbers() {
let highlighter = number_then_quote_highlighter();
let input = "status 200 ok";
let expected = "status [cyan]200[reset] ok";
let actual = highlighter.apply(input);
assert_eq!(actual.to_string().convert_escape_codes(), expected);
}
#[test]
fn invalid_regex_fails_build_and_first_error_wins() {
let result = Highlighter::builder()
.with_regex_highlighter(RegexConfig {
regex: "(unclosed".to_string(),
style: Style::default(),
})
.with_keyword_highlighters(vec![kw(&["ok"], Style::default())])
.build();
assert!(matches!(result, Err(Error::Regex(_))));
}
fn kw(words: &[&str], style: Style) -> KeywordConfig {
KeywordConfig {
words: words.iter().map(ToString::to_string).collect(),
style,
}
}
}