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::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::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::span::Finder;
use crate::core::utils::normalizer::normalize_keyword_configs;
use std::borrow::Cow;
use std::fmt;
use thiserror::Error;
pub struct Highlighter {
inner: Pipeline,
}
#[derive(Debug, Error)]
pub enum Error {
#[error("Regex error: {0}")]
RegexError(#[from] regex::Error),
#[error("Pattern error: {0}")]
PatternError(#[from] aho_corasick::BuildError),
}
impl Highlighter {
pub const fn builder() -> HighlighterBuilder {
HighlighterBuilder {
finders: Vec::new(),
first_error: None,
}
}
#[must_use]
pub fn apply<'a>(&self, input: &'a str) -> Cow<'a, str> {
self.inner.apply_sequential(input)
}
}
impl fmt::Debug for Highlighter {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Highlighter").field("pipeline", &self.inner).finish()
}
}
impl Default for Highlighter {
fn default() -> Self {
Highlighter::builder()
.with_json_highlighter(JsonConfig::default())
.with_date_time_highlighters(DateTimeConfig::default())
.with_url_highlighter(UrlConfig::default())
.with_email_highlighter(EmailConfig::default())
.with_ip_v4_highlighter(IpV4Config::default())
.with_uuid_highlighter(UuidConfig::default())
.with_pointer_highlighter(PointerConfig::default())
.with_unix_path_highlighter(UnixPathConfig::default())
.with_unix_process_highlighter(UnixProcessConfig::default())
.with_key_value_highlighter(KeyValueConfig::default())
.with_number_highlighter(NumberConfig::default())
.with_quote_highlighter(QuoteConfig::default())
.build()
.expect("Default constructor should never fail")
}
}
#[must_use]
pub struct HighlighterBuilder {
finders: Vec<Box<dyn Finder>>,
first_error: Option<Error>,
}
impl fmt::Debug for HighlighterBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HighlighterBuilder")
.field("finders", &self.finders.len())
.field("has_error", &self.first_error.is_some())
.finish()
}
}
impl HighlighterBuilder {
pub fn with_number_highlighter(mut self, config: NumberConfig) -> Self {
self.add_finder(NumberFinder::new(config.style));
self
}
pub fn with_uuid_highlighter(mut self, config: UuidConfig) -> Self {
self.add_finder(UuidFinder::new(config.number, config.letter, config.separator));
self
}
pub fn with_unix_path_highlighter(mut self, config: UnixPathConfig) -> Self {
self.add_finder(UnixPathFinder::new(config.segment, config.separator));
self
}
pub fn with_unix_process_highlighter(mut self, config: UnixProcessConfig) -> Self {
self.add_finder(UnixProcessFinder::new(config.name, config.id, config.bracket));
self
}
pub fn with_key_value_highlighter(mut self, config: KeyValueConfig) -> Self {
self.add_finder(KeyValueFinder::new(config.key, config.separator));
self
}
pub fn with_date_time_highlighters(mut self, config: DateTimeConfig) -> Self {
self.add_finder(DateTimeFinder::new(config.time, config.zone, config.separator));
self.add_finder(DateDashFinder::new(config.date, config.separator));
self
}
pub fn with_ip_v6_highlighter(mut self, config: IpV6Config) -> Self {
self.add_finder(IpV6Finder::new(config.number, config.letter, config.separator));
self
}
pub fn with_ip_v4_highlighter(mut self, config: IpV4Config) -> Self {
self.add_finder(IpV4Finder::new(config.number, config.separator));
self
}
pub fn with_url_highlighter(mut self, config: UrlConfig) -> Self {
self.add_finder(UrlFinder::new(
config.http,
config.https,
config.host,
config.path,
config.query_params_key,
config.query_params_value,
config.symbols,
));
self
}
pub fn with_email_highlighter(mut self, config: EmailConfig) -> Self {
self.add_finder(EmailFinder::new(
config.local_part,
config.at_sign,
config.domain,
config.dot,
));
self
}
pub fn with_pointer_highlighter(mut self, config: PointerConfig) -> Self {
self.add_finder(PointerFinder::new(config.number, config.letter, config.x));
self
}
pub fn with_regex_highlighter(mut self, config: RegexConfig) -> Self {
if self.first_error.is_some() {
return self;
}
match RegexFinder::new(&config.regex, config.style) {
Ok(f) => self.finders.push(Box::new(f)),
Err(e) => self.first_error = Some(Error::RegexError(e)),
}
self
}
pub fn with_quote_highlighter(mut self, config: QuoteConfig) -> Self {
self.add_finder(QuoteFinder::new(config.quote_token, config.style));
self
}
pub fn with_json_highlighter(mut self, config: JsonConfig) -> Self {
self.add_finder(JsonFinder::new(
config.key,
config.quote_token,
config.curly_bracket,
config.square_bracket,
config.comma,
config.colon,
));
self
}
pub fn with_keyword_highlighter(mut self, keyword_configs: Vec<KeywordConfig>) -> Self {
let normalized_keyword_configs = normalize_keyword_configs(keyword_configs);
for keyword_config in normalized_keyword_configs {
if self.first_error.is_some() {
continue;
}
let word_refs: Vec<&str> = keyword_config.words.iter().map(String::as_str).collect();
match KeywordFinder::new(&word_refs, keyword_config.style) {
Ok(f) => self.finders.push(Box::new(f)),
Err(e) => self.first_error = Some(Error::PatternError(e)),
}
}
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),
})
}
}
fn add_finder<F: Finder + 'static>(&mut self, finder: F) {
if self.first_error.is_none() {
self.finders.push(Box::new(finder));
}
}
}
#[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);
}
}