use crate::color::Style;
use crate::rules::Rule;
use crate::theme::{tokens, Theme};
#[derive(Debug, Clone)]
pub struct Colorizer {
rules: Vec<Rule>,
theme: Theme,
}
impl Colorizer {
pub fn new(rules: Vec<Rule>, theme: Theme) -> Colorizer {
Colorizer { rules, theme }
}
pub fn theme(&self) -> &Theme {
&self.theme
}
pub fn rules(&self) -> &[Rule] {
&self.rules
}
pub fn set_theme(&mut self, theme: Theme) {
self.theme = theme;
}
pub fn colorize_line(&self, line: &str) -> String {
if line.is_empty() {
return String::new();
}
let len = line.len();
let mut owner = vec![usize::MAX; len];
let mut token_names: Vec<&str> = Vec::new();
for rule in &self.rules {
if rule.has_named_groups {
for caps in rule.regex.captures_iter(line) {
for (gi, tok) in rule.group_tokens.iter().enumerate() {
let Some(tok) = tok else { continue };
if let Some(m) = caps.get(gi) {
claim(&mut owner, &mut token_names, m.start(), m.end(), tok);
}
}
}
} else {
for m in rule.regex.find_iter(line) {
claim(&mut owner, &mut token_names, m.start(), m.end(), &rule.whole_token);
}
}
}
self.render(line, &owner, &token_names)
}
fn render(&self, line: &str, owner: &[usize], token_names: &[&str]) -> String {
let default_style = self.theme.style(tokens::DEFAULT);
let styles: Vec<Style> = token_names.iter().map(|n| self.theme.style(n)).collect();
let mut out = String::with_capacity(line.len() + 16);
let mut i = 0;
let n = line.len();
while i < n {
let cur = owner[i];
let mut j = i + 1;
while j < n && owner[j] == cur {
j += 1;
}
let style = if cur == usize::MAX { default_style } else { styles[cur] };
out.push_str(&style.paint(&line[i..j]));
i = j;
}
out
}
pub fn colorize_text(&self, text: &str) -> String {
let mut out = String::with_capacity(text.len() + text.len() / 8);
for line in text.split_inclusive('\n') {
if let Some(stripped) = line.strip_suffix('\n') {
out.push_str(&self.colorize_line(stripped));
out.push('\n');
} else {
out.push_str(&self.colorize_line(line));
}
}
out
}
}
fn claim<'a>(
owner: &mut [usize],
names: &mut Vec<&'a str>,
start: usize,
end: usize,
token: &'a str,
) {
let id = match names.iter().position(|n| *n == token) {
Some(i) => i,
None => {
names.push(token);
names.len() - 1
}
};
for b in &mut owner[start..end] {
if *b == usize::MAX {
*b = id;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::rules::{builtin_generic, compile_all};
use crate::theme::cyberpunk;
fn engine() -> Colorizer {
let rules = compile_all(&builtin_generic()).unwrap();
Colorizer::new(rules, cyberpunk())
}
#[test]
fn empty_line() {
assert_eq!(engine().colorize_line(""), "");
}
#[test]
fn plain_line_gets_base_style() {
let out = engine().colorize_line("hello world");
assert!(out.contains("\x1b["));
assert!(out.contains("hello"));
}
#[test]
fn error_word_is_painted_bold() {
let out = engine().colorize_line("ERROR something broke");
assert!(out.contains("\x1b[1;"));
assert!(out.contains("ERROR"));
}
#[test]
fn newlines_preserved() {
let out = engine().colorize_text("a\nb\n");
assert_eq!(out.matches('\n').count(), 2);
}
#[test]
fn earlier_rule_wins_overlap() {
let out = engine().colorize_line("192.168.0.1");
assert!(out.contains("38;5;135"));
assert_eq!(out.matches("\x1b[0m").count(), 1);
}
}