mod accumulator;
mod color;
mod format;
mod style;
use self::format::{DisplayStyle, NodeDetails};
use crate::utils::CommaArray;
use crate::PadItem;
use itertools::Itertools;
use log::*;
use std::collections::HashMap;
pub use self::accumulator::ColorAccumulator;
pub use self::color::Color;
pub use self::style::{Style, WriteStyle};
pub struct Selector {
segments: Vec<Segment>,
}
impl Selector {
pub fn new() -> Selector {
Selector { segments: vec![] }
}
pub fn glob() -> GlobSelector {
Selector::new().add_glob()
}
pub fn star() -> Selector {
Selector::new().add_star()
}
pub fn name(name: &'static str) -> Selector {
Selector::new().add(name)
}
pub fn add_glob(self) -> GlobSelector {
let mut segments = self.segments;
segments.push(Segment::Glob);
GlobSelector { segments }
}
pub fn add_star(mut self) -> Selector {
self.segments.push(Segment::Star);
self
}
pub fn add(mut self, segment: &'static str) -> Selector {
self.segments.push(Segment::Name(segment));
self
}
}
pub struct GlobSelector {
segments: Vec<Segment>,
}
impl GlobSelector {
pub fn add_star(self) -> Selector {
let mut segments = self.segments;
segments.push(Segment::Star);
Selector { segments }
}
pub fn add(self, segment: &'static str) -> Selector {
let mut segments = self.segments;
segments.push(Segment::Name(segment));
Selector { segments }
}
}
impl IntoIterator for Selector {
type Item = Segment;
type IntoIter = ::std::vec::IntoIter<Segment>;
fn into_iter(self) -> ::std::vec::IntoIter<Segment> {
self.segments.into_iter()
}
}
impl IntoIterator for GlobSelector {
type Item = Segment;
type IntoIter = ::std::vec::IntoIter<Segment>;
fn into_iter(self) -> ::std::vec::IntoIter<Segment> {
self.segments.into_iter()
}
}
impl From<&'static str> for Selector {
fn from(from: &'static str) -> Selector {
let segments = from.split(' ');
let segments = segments.map(|part| part.into());
Selector {
segments: segments.collect(),
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum Segment {
Root,
Star,
Glob,
Name(&'static str),
}
impl From<&'static str> for Segment {
fn from(from: &'static str) -> Segment {
if from == "**" {
Segment::Glob
} else if from == "*" {
Segment::Star
} else {
Segment::Name(from)
}
}
}
#[derive(Debug)]
struct Node {
segment: Segment,
children: HashMap<Segment, Node>,
declarations: Option<Style>,
}
impl Node {
fn new(segment: Segment) -> Node {
Node {
segment,
children: HashMap::new(),
declarations: None,
}
}
fn display<'a>(&'a self) -> NodeDetails<'a> {
NodeDetails::new(self.segment, &self.declarations)
}
fn terminal(&self) -> Option<&Node> {
match self.children.get(&Segment::Glob) {
None => if self.children.is_empty() {
return Some(self);
} else {
return None;
},
Some(glob) => return Some(glob),
};
}
fn add(&mut self, selector: impl IntoIterator<Item = Segment>, declarations: impl Into<Style>) {
let mut path = selector.into_iter();
match path.next() {
None => {
self.declarations = Some(declarations.into());
}
Some(name) => self
.children
.entry(name)
.or_insert(Node::new(name))
.add(path, declarations),
}
}
fn find<'a>(&self, names: &[&'static str], debug_nesting: usize) -> Option<Style> {
trace!(
"{}In {}, finding {:?} (children={})",
PadItem(" ", debug_nesting),
self,
names.join(" "),
CommaArray(self.children.keys().map(|k| k.to_string()).collect())
);
let next_name = match names.first() {
None => {
let terminal = self.terminal()?;
trace!(
"{}Matched terminal {}",
PadItem(" ", debug_nesting),
terminal.display()
);
return terminal.declarations.clone();
}
Some(next_name) => next_name,
};
let matches = self.find_match(next_name);
trace!("{}Matches: {}", PadItem(" ", debug_nesting), matches);
let mut style: Option<Style> = None;
if let Some(glob) = matches.glob {
style = union(style, glob.find(&names[1..], debug_nesting + 1));
trace!(
"{}matched glob={}",
PadItem(" ", debug_nesting),
DisplayStyle(&style)
);
}
if let Some(star) = matches.star {
style = union(style, star.find(&names[1..], debug_nesting + 1));
trace!(
"{}matched star={}",
PadItem(" ", debug_nesting),
DisplayStyle(&style)
);
}
if let Some(skipped_glob) = matches.skipped_glob {
style = union(style, skipped_glob.find(&names[1..], debug_nesting + 1));
trace!(
"{}matched skipped_glob={}",
PadItem(" ", debug_nesting),
DisplayStyle(&style)
);
}
if let Some(literal) = matches.literal {
style = union(style, literal.find(&names[1..], debug_nesting + 1));
trace!(
"{}matched literal={}",
PadItem(" ", debug_nesting),
DisplayStyle(&style)
);
}
style
}
fn find_match<'a>(&'a self, name: &'static str) -> Match<'a> {
let glob;
let mut skipped_glob = None;
let star = self.children.get(&Segment::Star);
let literal = self.children.get(&Segment::Name(name));
if self.segment == Segment::Glob {
glob = Some(self);
} else {
glob = self.children.get(&Segment::Glob);
if let Some(glob) = glob {
skipped_glob = glob.children.get(&Segment::Name(name));
}
}
Match {
glob,
star,
skipped_glob,
literal,
}
}
}
fn union(left: Option<Style>, right: Option<Style>) -> Option<Style> {
match (left, right) {
(None, None) => None,
(Some(left), None) => Some(left),
(None, Some(right)) => Some(right),
(Some(left), Some(right)) => Some(left.union(right)),
}
}
struct Match<'a> {
glob: Option<&'a Node>,
star: Option<&'a Node>,
skipped_glob: Option<&'a Node>,
literal: Option<&'a Node>,
}
#[derive(Debug)]
pub struct Stylesheet {
styles: Node,
}
impl Stylesheet {
pub fn new() -> Stylesheet {
Stylesheet {
styles: Node::new(Segment::Root),
}
}
pub fn add(mut self, name: impl Into<Selector>, declarations: impl Into<Style>) -> Stylesheet {
self.styles.add(name.into(), declarations);
self
}
pub fn get(&self, names: &[&'static str]) -> Option<Style> {
if log_enabled!(::log::Level::Trace) {
println!("\n");
}
trace!("Searching for `{}`", names.iter().join(" "));
let style = self.styles.find(names, 0);
match &style {
None => trace!("No style found"),
Some(style) => trace!("Found {}", style),
}
style
}
}
#[cfg(test)]
mod tests {
use super::style::Style;
use crate::{Color, Stylesheet};
use pretty_env_logger;
fn init_logger() {
pretty_env_logger::try_init().ok();
}
#[test]
fn test_basic_lookup() {
init_logger();
let stylesheet =
Stylesheet::new().add("message header error code", "fg: red; underline: false");
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(style, Some(Style("fg: red; underline: false")))
}
#[test]
fn test_basic_with_typed_style() {
init_logger();
let stylesheet = Stylesheet::new().add(
"message header error code",
Style::new().bold().fg(Color::Red),
);
assert_eq!(
stylesheet.get(&["message", "header", "error", "code"]),
Some(Style("weight: bold; fg: red"))
)
}
#[test]
fn test_star() {
init_logger();
let stylesheet =
Stylesheet::new().add("message header * code", "fg: red; underline: false");
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(style, Some(Style("fg: red; underline: false")))
}
#[test]
fn test_star_with_typed_style() {
init_logger();
let stylesheet =
Stylesheet::new().add("message header * code", Style::new().bold().fg(Color::Red));
assert_eq!(
stylesheet.get(&["message", "header", "error", "code"]),
Some(Style("weight: bold; fg: red"))
)
}
#[test]
fn test_glob() {
init_logger();
let stylesheet = Stylesheet::new().add("message ** code", "fg: red; underline: false");
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(style, Some(Style("fg: red; underline: false")))
}
#[test]
fn test_glob_with_typed_style() {
init_logger();
let stylesheet =
Stylesheet::new().add("message ** code", Style::new().nounderline().fg(Color::Red));
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(style, Some(Style("fg: red; underline: false")))
}
#[test]
fn test_glob_matches_no_segments() {
init_logger();
let stylesheet =
Stylesheet::new().add("message ** header error code", "fg: red; underline: false");
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(style, Some(Style("fg: red; underline: false")))
}
#[test]
fn test_glob_matches_no_segments_with_typed_style() {
init_logger();
let stylesheet = Stylesheet::new().add(
"message ** header error code",
Style::new().nounderline().fg(Color::Red),
);
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(style, Some(Style("fg: red; underline: false")))
}
#[test]
fn test_trailing_glob_is_terminal() {
init_logger();
let stylesheet = Stylesheet::new().add(
"message header error **",
Style::new().nounderline().fg(Color::Red),
);
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(style, Some(Style("fg: red; underline: false")))
}
#[test]
fn test_trailing_glob_is_terminal_with_typed_styles() {
init_logger();
let stylesheet = Stylesheet::new().add(
"message header error **",
Style::new().nounderline().fg(Color::Red),
);
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(style, Some(Style::new().fg(Color::Red).nounderline()))
}
#[test]
fn test_trailing_glob_is_terminal_and_matches_nothing() {
init_logger();
let stylesheet =
Stylesheet::new().add("message header error code **", "fg: red; underline: false");
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(style, Some(Style::new().fg(Color::Red).nounderline()))
}
#[test]
fn test_trailing_glob_is_terminal_and_matches_nothing_with_typed_style() {
init_logger();
let stylesheet = Stylesheet::new().add(
"message header error code **",
Style::new().nounderline().fg(Color::Red),
);
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(style, Some(Style::new().fg(Color::Red).nounderline()))
}
#[test]
fn test_priority() {
init_logger();
let stylesheet = Stylesheet::new()
.add("message ** code", "fg: blue; weight: bold")
.add("message header * code", "underline: true; bg: black")
.add("message header error code", "fg: red; underline: false");
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(
style,
Some(
Style::new()
.fg(Color::Red)
.bg(Color::Black)
.nounderline()
.bold()
)
)
}
#[test]
fn test_priority_with_typed_style() {
init_logger();
let stylesheet = Stylesheet::new()
.add("message ** code", Style::new().fg(Color::Blue).bold())
.add(
"message header * code",
Style::new().underline().bg(Color::Black),
).add(
"message header error code",
Style::new().fg(Color::Red).nounderline(),
);
let style = stylesheet.get(&["message", "header", "error", "code"]);
assert_eq!(
style,
Some(
Style::new()
.fg(Color::Red)
.bg(Color::Black)
.nounderline()
.bold()
)
)
}
}