use std::collections::HashSet; use std::str::FromStr; use crate::error::*; #[non_exhaustive] #[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)] pub enum StyleComponent { Auto, #[cfg(feature = "git")] Changes, Grid, Rule, Header, HeaderFilename, HeaderFilesize, LineNumbers, Snip, Full, Default, Plain, } impl StyleComponent { pub fn components(self, interactive_terminal: bool) -> &'static [StyleComponent] { match self { StyleComponent::Auto => { if interactive_terminal { StyleComponent::Default.components(interactive_terminal) } else { StyleComponent::Plain.components(interactive_terminal) } } #[cfg(feature = "git")] StyleComponent::Changes => &[StyleComponent::Changes], StyleComponent::Grid => &[StyleComponent::Grid], StyleComponent::Rule => &[StyleComponent::Rule], StyleComponent::Header => &[StyleComponent::HeaderFilename], StyleComponent::HeaderFilename => &[StyleComponent::HeaderFilename], StyleComponent::HeaderFilesize => &[StyleComponent::HeaderFilesize], StyleComponent::LineNumbers => &[StyleComponent::LineNumbers], StyleComponent::Snip => &[StyleComponent::Snip], StyleComponent::Full => &[ #[cfg(feature = "git")] StyleComponent::Changes, StyleComponent::Grid, StyleComponent::HeaderFilename, StyleComponent::HeaderFilesize, StyleComponent::LineNumbers, StyleComponent::Snip, ], StyleComponent::Default => &[ #[cfg(feature = "git")] StyleComponent::Changes, StyleComponent::Grid, StyleComponent::HeaderFilename, StyleComponent::LineNumbers, StyleComponent::Snip, ], StyleComponent::Plain => &[], } } } impl FromStr for StyleComponent { type Err = Error; fn from_str(s: &str) -> Result { match s { "auto" => Ok(StyleComponent::Auto), #[cfg(feature = "git")] "changes" => Ok(StyleComponent::Changes), "grid" => Ok(StyleComponent::Grid), "rule" => Ok(StyleComponent::Rule), "header" => Ok(StyleComponent::Header), "header-filename" => Ok(StyleComponent::HeaderFilename), "header-filesize" => Ok(StyleComponent::HeaderFilesize), "numbers" => Ok(StyleComponent::LineNumbers), "snip" => Ok(StyleComponent::Snip), "full" => Ok(StyleComponent::Full), "default" => Ok(StyleComponent::Default), "plain" => Ok(StyleComponent::Plain), _ => Err(format!("Unknown style '{s}'").into()), } } } #[derive(Debug, Clone, Default)] pub struct StyleComponents(pub HashSet); impl StyleComponents { pub fn new(components: &[StyleComponent]) -> StyleComponents { StyleComponents(components.iter().cloned().collect()) } #[cfg(feature = "git")] pub fn changes(&self) -> bool { self.0.contains(&StyleComponent::Changes) } pub fn grid(&self) -> bool { self.0.contains(&StyleComponent::Grid) } pub fn rule(&self) -> bool { self.0.contains(&StyleComponent::Rule) } pub fn header(&self) -> bool { self.header_filename() || self.header_filesize() } pub fn header_filename(&self) -> bool { self.0.contains(&StyleComponent::HeaderFilename) } pub fn header_filesize(&self) -> bool { self.0.contains(&StyleComponent::HeaderFilesize) } pub fn numbers(&self) -> bool { self.0.contains(&StyleComponent::LineNumbers) } pub fn snip(&self) -> bool { self.0.contains(&StyleComponent::Snip) } pub fn plain(&self) -> bool { self.0.iter().all(|c| c == &StyleComponent::Plain) } pub fn insert(&mut self, component: StyleComponent) { self.0.insert(component); } pub fn clear(&mut self) { self.0.clear(); } } #[derive(Debug, PartialEq)] enum ComponentAction { Override, Add, Remove, } impl ComponentAction { fn extract_from_str(string: &str) -> (ComponentAction, &str) { match string.chars().next() { Some('-') => (ComponentAction::Remove, string.strip_prefix('-').unwrap()), Some('+') => (ComponentAction::Add, string.strip_prefix('+').unwrap()), _ => (ComponentAction::Override, string), } } } /// A list of [StyleComponent] that can be parsed from a string. pub struct StyleComponentList(Vec<(ComponentAction, StyleComponent)>); impl StyleComponentList { fn expand_into(&self, components: &mut HashSet, interactive_terminal: bool) { for (action, component) in self.0.iter() { let subcomponents = component.components(interactive_terminal); use ComponentAction::*; match action { Override | Add => components.extend(subcomponents), Remove => components.retain(|c| !subcomponents.contains(c)), } } } /// Returns `true` if any component in the list was not prefixed with `+` or `-`. fn contains_override(&self) -> bool { self.0.iter().any(|(a, _)| *a == ComponentAction::Override) } /// Combines multiple [StyleComponentList]s into a single [StyleComponents] set. /// /// ## Precedence /// The most recent list will take precedence and override all previous lists /// unless it only contains components prefixed with `-` or `+`. When this /// happens, the list's components will be merged into the previous list. /// /// ## Example /// ```text /// [numbers,grid] + [header,changes] -> [header,changes] /// [numbers,grid] + [+header,-grid] -> [numbers,header] /// ``` /// /// ## Parameters /// - `with_default`: If true, the styles lists will build upon the StyleComponent::Auto style. pub fn to_components( lists: impl IntoIterator, interactive_terminal: bool, with_default: bool, ) -> StyleComponents { let mut components: HashSet = HashSet::new(); if with_default { components.extend(StyleComponent::Auto.components(interactive_terminal)) } StyleComponents(lists.into_iter().fold(components, |mut components, list| { if list.contains_override() { components.clear(); } list.expand_into(&mut components, interactive_terminal); components })) } } impl Default for StyleComponentList { fn default() -> Self { StyleComponentList(vec![(ComponentAction::Override, StyleComponent::Default)]) } } impl FromStr for StyleComponentList { type Err = Error; fn from_str(s: &str) -> Result { Ok(StyleComponentList( s.split(",") .map(|s| ComponentAction::extract_from_str(s)) // If the component starts with "-", it's meant to be removed .map(|(a, s)| Ok((a, StyleComponent::from_str(s)?))) .collect::>>()?, )) } } #[cfg(test)] mod test { use std::collections::HashSet; use std::str::FromStr; use super::ComponentAction::*; use super::StyleComponent; use super::StyleComponent::*; use super::StyleComponentList; #[test] pub fn style_component_list_parse() { assert_eq!( StyleComponentList::from_str("grid,+numbers,snip,-snip,header") .expect("no error") .0, vec![ (Override, Grid), (Add, LineNumbers), (Override, Snip), (Remove, Snip), (Override, Header), ] ); assert!(StyleComponentList::from_str("not-a-component").is_err()); assert!(StyleComponentList::from_str("grid,not-a-component").is_err()); assert!(StyleComponentList::from_str("numbers,-not-a-component").is_err()); } #[test] pub fn style_component_list_to_components() { assert_eq!( StyleComponentList::to_components( vec![StyleComponentList::from_str("grid,numbers").expect("no error")], false, false ) .0, HashSet::from([Grid, LineNumbers]) ); } #[test] pub fn style_component_list_to_components_removes_negated() { assert_eq!( StyleComponentList::to_components( vec![StyleComponentList::from_str("grid,numbers,-grid").expect("no error")], false, false ) .0, HashSet::from([LineNumbers]) ); } #[test] pub fn style_component_list_to_components_expands_subcomponents() { assert_eq!( StyleComponentList::to_components( vec![StyleComponentList::from_str("full").expect("no error")], false, false ) .0, HashSet::from_iter(Full.components(true).to_owned()) ); } #[test] pub fn style_component_list_expand_negates_subcomponents() { assert!(!StyleComponentList::to_components( vec![StyleComponentList::from_str("full,-numbers").expect("no error")], true, false ) .numbers()); } #[test] pub fn style_component_list_to_components_precedence_overrides_previous_lists() { assert_eq!( StyleComponentList::to_components( vec![ StyleComponentList::from_str("grid").expect("no error"), StyleComponentList::from_str("numbers").expect("no error"), ], false, false ) .0, HashSet::from([LineNumbers]) ); } #[test] pub fn style_component_list_to_components_precedence_merges_previous_lists() { assert_eq!( StyleComponentList::to_components( vec![ StyleComponentList::from_str("grid,header").expect("no error"), StyleComponentList::from_str("-grid").expect("no error"), StyleComponentList::from_str("+numbers").expect("no error"), ], false, false ) .0, HashSet::from([HeaderFilename, LineNumbers]) ); } #[test] pub fn style_component_list_default_builds_on_auto() { assert_eq!( StyleComponentList::to_components( vec![StyleComponentList::from_str("-numbers").expect("no error"),], true, true ) .0, { let mut expected: HashSet = HashSet::new(); expected.extend(Auto.components(true)); expected.remove(&LineNumbers); expected } ); } }