diff options
author | Dan Davison <dandavison7@gmail.com> | 2020-06-26 18:50:47 -0400 |
---|---|---|
committer | Dan Davison <dandavison7@gmail.com> | 2020-06-26 18:50:47 -0400 |
commit | 472c5a509d5cc9e57bad476e4cd852e4b9459a01 (patch) | |
tree | ee97403ba99466e065f50a32ae9e9196e143300f /src/parse_style.rs | |
parent | b83cee3707159c4b6bc0c5070a84d78f73017578 (diff) |
Refactor: parse_style module
Diffstat (limited to 'src/parse_style.rs')
-rw-r--r-- | src/parse_style.rs | 702 |
1 files changed, 702 insertions, 0 deletions
diff --git a/src/parse_style.rs b/src/parse_style.rs new file mode 100644 index 00000000..ffe514d5 --- /dev/null +++ b/src/parse_style.rs @@ -0,0 +1,702 @@ +use std::process; + +use bitflags::bitflags; + +use crate::color; +use crate::config::unreachable; +use crate::style::{DecorationStyle, Style}; + +impl Style { + /// Construct Style from style and decoration-style strings supplied on command line, together + /// with defaults. A style string is a space-separated string containing 0, 1, or 2 colors + /// (foreground and then background) and an arbitrary number of style attributes. See `delta + /// --help` for more precise spec. + pub fn from_str( + style_string: &str, + foreground_default: Option<ansi_term::Color>, + background_default: Option<ansi_term::Color>, + decoration_style_string: Option<&str>, + true_color: bool, + is_emph: bool, + ) -> Self { + let (ansi_term_style, is_omitted, is_raw, is_syntax_highlighted) = parse_ansi_term_style( + &style_string, + foreground_default, + background_default, + true_color, + ); + let decoration_style = + DecorationStyle::from_str(decoration_style_string.unwrap_or(""), true_color); + Self { + ansi_term_style, + is_emph, + is_omitted, + is_raw, + is_syntax_highlighted, + decoration_style, + } + } + + /// Construct Style but interpreting 'ul', 'box', etc as applying to the decoration style. + fn from_str_with_handling_of_special_decoration_attributes( + style_string: &str, + foreground_default: Option<ansi_term::Color>, + background_default: Option<ansi_term::Color>, + decoration_style_string: Option<&str>, + true_color: bool, + is_emph: bool, + ) -> Self { + let (special_attributes_from_style_string, style_string) = + extract_special_decoration_attributes_from_non_decoration_style_string(style_string); + let mut style = Style::from_str( + &style_string, + foreground_default, + background_default, + decoration_style_string.as_deref(), + true_color, + is_emph, + ); + // TODO: box in this context resulted in box-with-underline for commit and file + style.decoration_style = DecorationStyle::apply_special_decoration_attributes( + &mut style, + special_attributes_from_style_string, + ); + style + } + + /// As from_str_with_handling_of_special_decoration_attributes but respecting an optional + /// foreground color which has precedence when present. + pub fn from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg( + style_string: &str, + foreground_default: Option<ansi_term::Color>, + background_default: Option<ansi_term::Color>, + decoration_style_string: Option<&str>, + deprecated_foreground_color_arg: Option<&str>, + true_color: bool, + is_emph: bool, + ) -> Self { + let mut style = Self::from_str_with_handling_of_special_decoration_attributes( + style_string, + foreground_default, + background_default, + decoration_style_string, + true_color, + is_emph, + ); + if let Some(s) = deprecated_foreground_color_arg { + // The deprecated --{commit,file,hunk}-color args functioned to set the decoration + // foreground color. In the case of file, it set the text foreground color also. + let foreground_from_deprecated_arg = parse_ansi_term_style(s, None, None, true_color) + .0 + .foreground; + style.ansi_term_style.foreground = foreground_from_deprecated_arg; + style.decoration_style = match style.decoration_style { + DecorationStyle::Box(mut ansi_term_style) => { + ansi_term_style.foreground = foreground_from_deprecated_arg; + DecorationStyle::Box(ansi_term_style) + } + DecorationStyle::Underline(mut ansi_term_style) => { + ansi_term_style.foreground = foreground_from_deprecated_arg; + DecorationStyle::Underline(ansi_term_style) + } + DecorationStyle::Overline(mut ansi_term_style) => { + ansi_term_style.foreground = foreground_from_deprecated_arg; + DecorationStyle::Overline(ansi_term_style) + } + DecorationStyle::UnderOverline(mut ansi_term_style) => { + ansi_term_style.foreground = foreground_from_deprecated_arg; + DecorationStyle::UnderOverline(ansi_term_style) + } + DecorationStyle::BoxWithUnderline(mut ansi_term_style) => { + ansi_term_style.foreground = foreground_from_deprecated_arg; + DecorationStyle::BoxWithUnderline(ansi_term_style) + } + DecorationStyle::BoxWithOverline(mut ansi_term_style) => { + ansi_term_style.foreground = foreground_from_deprecated_arg; + DecorationStyle::BoxWithOverline(ansi_term_style) + } + DecorationStyle::BoxWithUnderOverline(mut ansi_term_style) => { + ansi_term_style.foreground = foreground_from_deprecated_arg; + DecorationStyle::BoxWithUnderOverline(ansi_term_style) + } + DecorationStyle::NoDecoration => style.decoration_style, + }; + } + style + } +} + +bitflags! { + struct DecorationAttributes: u8 { + const EMPTY = 0b00000000; + const BOX = 0b00000001; + const OVERLINE = 0b00000010; + const UNDERLINE = 0b00000100; + } +} + +impl DecorationStyle { + pub fn from_str(style_string: &str, true_color: bool) -> Self { + let (special_attributes, style_string) = + extract_special_decoration_attributes(&style_string); + let (style, is_omitted, is_raw, is_syntax_highlighted) = + parse_ansi_term_style(&style_string, None, None, true_color); + if is_raw { + eprintln!("'raw' may not be used in a decoration style."); + process::exit(1); + }; + if is_syntax_highlighted { + eprintln!("'syntax' may not be used in a decoration style."); + process::exit(1); + }; + #[allow(non_snake_case)] + let (BOX, UL, OL, EMPTY) = ( + DecorationAttributes::BOX, + DecorationAttributes::UNDERLINE, + DecorationAttributes::OVERLINE, + DecorationAttributes::EMPTY, + ); + match special_attributes { + bits if bits == EMPTY => DecorationStyle::NoDecoration, + bits if bits == BOX => DecorationStyle::Box(style), + bits if bits == UL => DecorationStyle::Underline(style), + bits if bits == OL => DecorationStyle::Overline(style), + bits if bits == UL | OL => DecorationStyle::UnderOverline(style), + bits if bits == BOX | UL => DecorationStyle::BoxWithUnderline(style), + bits if bits == BOX | OL => DecorationStyle::BoxWithOverline(style), + bits if bits == BOX | UL | OL => DecorationStyle::BoxWithUnderOverline(style), + _ if is_omitted => DecorationStyle::NoDecoration, + _ => unreachable("Unreachable code path reached in parse_decoration_style."), + } + } + + fn apply_special_decoration_attributes( + style: &mut Style, + special_attributes: DecorationAttributes, + ) -> DecorationStyle { + let ansi_term_style = match style.decoration_style { + DecorationStyle::Box(ansi_term_style) => ansi_term_style, + DecorationStyle::Underline(ansi_term_style) => ansi_term_style, + DecorationStyle::Overline(ansi_term_style) => ansi_term_style, + DecorationStyle::UnderOverline(ansi_term_style) => ansi_term_style, + DecorationStyle::BoxWithUnderline(ansi_term_style) => ansi_term_style, + DecorationStyle::BoxWithOverline(ansi_term_style) => ansi_term_style, + DecorationStyle::BoxWithUnderOverline(ansi_term_style) => ansi_term_style, + DecorationStyle::NoDecoration => ansi_term::Style::new(), + }; + #[allow(non_snake_case)] + let (BOX, UL, OL, EMPTY) = ( + DecorationAttributes::BOX, + DecorationAttributes::UNDERLINE, + DecorationAttributes::OVERLINE, + DecorationAttributes::EMPTY, + ); + match special_attributes { + bits if bits == EMPTY => style.decoration_style, + bits if bits == BOX => DecorationStyle::Box(ansi_term_style), + bits if bits == UL => DecorationStyle::Underline(ansi_term_style), + bits if bits == OL => DecorationStyle::Overline(ansi_term_style), + bits if bits == UL | OL => DecorationStyle::UnderOverline(ansi_term_style), + bits if bits == BOX | UL => DecorationStyle::BoxWithUnderline(ansi_term_style), + bits if bits == BOX | OL => DecorationStyle::BoxWithOverline(ansi_term_style), + bits if bits == BOX | UL | OL => DecorationStyle::BoxWithUnderOverline(ansi_term_style), + _ => DecorationStyle::NoDecoration, + } + } +} + +fn parse_ansi_term_style( + s: &str, + foreground_default: Option<ansi_term::Color>, + background_default: Option<ansi_term::Color>, + true_color: bool, +) -> (ansi_term::Style, bool, bool, bool) { + let mut style = ansi_term::Style::new(); + let mut seen_foreground = false; + let mut seen_background = false; + let mut is_omitted = false; + let mut is_raw = false; + let mut is_syntax_highlighted = false; + for word in s + .to_lowercase() + .split_whitespace() + .map(|word| word.trim_matches(|c| c == '"' || c == '\'')) + { + if word == "blink" { + style.is_blink = true; + } else if word == "bold" { + style.is_bold = true; + } else if word == "dim" { + style.is_dimmed = true; + } else if word == "hidden" { + style.is_hidden = true; + } else if word == "italic" { + style.is_italic = true; + } else if word == "omit" { + is_omitted = true; + } else if word == "reverse" { + style.is_reverse = true; + } else if word == "raw" { + is_raw = true; + } else if word == "strike" { + style.is_strikethrough = true; + } else if word == "ul" || word == "underline" { + style.is_underline = true; + } else if !seen_foreground { + if word == "syntax" { + is_syntax_highlighted = true; + } else { + style.foreground = color::color_from_rgb_or_ansi_code_with_default( + word, + foreground_default, + true_color, + ); + } + seen_foreground = true; + } else if !seen_background { + if word == "syntax" { + eprintln!( + "You have used the special color 'syntax' as a background color \ + (second color in a style string). It may only be used as a foreground \ + color (first color in a style string)." + ); + process::exit(1); + } else { + style.background = color::color_from_rgb_or_ansi_code_with_default( + word, + background_default, + true_color, + ); + } + seen_background = true; + } else { + eprintln!( + "Invalid style string: {}. See the STYLES section of delta --help.", + s + ); + process::exit(1); + } + } + (style, is_omitted, is_raw, is_syntax_highlighted) +} + +/// Extract set of 'special decoration attributes' and return it along with modified style string. +fn extract_special_decoration_attributes(style_string: &str) -> (DecorationAttributes, String) { + _extract_special_decoration_attributes(style_string, true) +} + +fn extract_special_decoration_attributes_from_non_decoration_style_string( + style_string: &str, +) -> (DecorationAttributes, String) { + _extract_special_decoration_attributes(style_string, false) +} + +// If this is being called in the context of processing a decoration style string then we treat +// ul/ol as a request for an underline/overline decoration respectively. Otherwise they are +// conventional character style attributes. +fn _extract_special_decoration_attributes( + style_string: &str, + is_decoration_style_string: bool, +) -> (DecorationAttributes, String) { + let mut attributes = DecorationAttributes::EMPTY; + let mut new_style_string = Vec::new(); + let style_string = style_string.to_lowercase(); + for token in style_string + .split_whitespace() + .map(|word| word.trim_matches(|c| c == '"' || c == '\'')) + { + match token { + "box" => attributes |= DecorationAttributes::BOX, + token if token == "overline" || is_decoration_style_string && token == "ol" => { + attributes |= DecorationAttributes::OVERLINE + } + token if token == "underline" || is_decoration_style_string && token == "ul" => { + attributes |= DecorationAttributes::UNDERLINE + } + token if token == "none" || token == "plain" => {} + _ => new_style_string.push(token), + } + } + (attributes, new_style_string.join(" ")) +} + +#[cfg(test)] +mod tests { + use super::*; + + use ansi_term; + + use crate::color::ansi_color_name_to_number; + + #[test] + fn test_parse_ansi_term_style() { + assert_eq!( + parse_ansi_term_style("", None, None, false), + (ansi_term::Style::new(), false, false, false) + ); + assert_eq!( + parse_ansi_term_style("red", None, None, false), + ( + ansi_term::Style { + foreground: Some(ansi_term::Color::Fixed( + ansi_color_name_to_number("red").unwrap() + )), + ..ansi_term::Style::new() + }, + false, + false, + false + ) + ); + assert_eq!( + parse_ansi_term_style("red green", None, None, false), + ( + ansi_term::Style { + foreground: Some(ansi_term::Color::Fixed( + ansi_color_name_to_number("red").unwrap() + )), + background: Some(ansi_term::Color::Fixed( + ansi_color_name_to_number("green").unwrap() + )), + ..ansi_term::Style::new() + }, + false, + false, + false + ) + ); + assert_eq!( + parse_ansi_term_style("bold red underline green blink", None, None, false), + ( + ansi_term::Style { + foreground: Some(ansi_term::Color::Fixed( + ansi_color_name_to_number("red").unwrap() + )), + background: Some(ansi_term::Color::Fixed( + ansi_color_name_to_number("green").unwrap() + )), + is_blink: true, + is_bold: true, + is_underline: true, + ..ansi_term::Style::new() + }, + false, + false, + false + ) + ); + } + + #[test] + fn test_parse_ansi_term_style_with_special_syntax_color() { + assert_eq!( + parse_ansi_term_style("syntax", None, None, false), + (ansi_term::Style::new(), false, false, true) + ); + assert_eq!( + parse_ansi_term_style("syntax italic white hidden", None, None, false), + ( + ansi_term::Style { + background: Some(ansi_term::Color::Fixed( + ansi_color_name_to_number("white").unwrap() + )), + is_italic: true, + is_hidden: true, + ..ansi_term::Style::new() + }, + false, + false, + true + ) + ); + assert_eq!( + parse_ansi_term_style("bold syntax italic white hidden", None, None, false), + ( + ansi_term::Style { + background: Some(ansi_term::Color::Fixed( + ansi_color_name_to_number("white").unwrap() + )), + is_bold: true, + is_italic: true, + is_hidden: true, + ..ansi_term::Style::new() + }, + false, + false, + true + ) + ); + } + + #[test] + fn test_parse_ansi_term_style_with_special_omit_attribute() { + assert_eq!( + parse_ansi_term_style("omit", None, None, false), + (ansi_term::Style::new(), true, false, false) + ); + // It doesn't make sense for omit to be combined with anything else, but it is not an error. + assert_eq!( + parse_ansi_term_style("omit syntax italic white hidden", None, None, false), + ( + ansi_term::Style { + background: Some(ansi_term::Color::Fixed( + ansi_color_name_to_number("white").unwrap() + )), + is_italic: true, + is_hidden: true, + ..ansi_term::Style::new() + }, + true, + false, + true + ) + ); + } + + #[test] + fn test_parse_ansi_term_style_with_special_raw_attribute() { + assert_eq!( + parse_ansi_term_style("raw", None, None, false), + (ansi_term::Style::new(), false, true, false) + ); + // It doesn't make sense for raw to be combined with anything else, but it is not an error. + assert_eq!( + parse_ansi_term_style("raw syntax italic white hidden", None, None, false), + ( + ansi_term::Style { + background: Some(ansi_term::Color::Fixed( + ansi_color_name_to_number("white").unwrap() + )), + is_italic: true, + is_hidden: true, + ..ansi_term::Style::new() + }, + false, + true, + true + ) + ); + } + + #[test] + fn test_extract_special_decoration_attribute() { + #[allow(non_snake_case)] + let (BOX, UL, OL, EMPTY) = ( + DecorationAttributes::BOX, + DecorationAttributes::UNDERLINE, + DecorationAttributes::OVERLINE, + DecorationAttributes::EMPTY, + ); + assert_eq!( + extract_special_decoration_attributes(""), + (EMPTY, "".to_string(),) + ); + assert_eq!( + extract_special_decoration_attributes("box"), + (BOX, "".to_string()) + ); + assert_eq!( + extract_special_decoration_attributes("ul"), + (UL, "".to_string()) + ); + assert_eq!( + extract_special_decoration_attributes("ol"), + (OL, "".to_string()) + ); + assert_eq!( + extract_special_decoration_attributes("box ul"), + (BOX | UL, "".to_string()) + ); + assert_eq!( + extract_special_decoration_attributes("box ol"), + (BOX | OL, "".to_string()) + ); + assert_eq!( + extract_special_decoration_attributes("ul box ol"), + (BOX | UL | OL, "".to_string()) + ); + assert_eq!( + extract_special_decoration_attributes("ol ul"), + (UL | OL, "".to_string()) + ); + } + + #[test] + fn test_decoration_style_from_str_empty_string() { + assert_eq!( + DecorationStyle::from_str("", true), + DecorationStyle::NoDecoration, + ) + } + + #[test] + fn test_decoration_style_from_str() { + assert_eq!( + DecorationStyle::from_str("ol red box bold green ul", true), + DecorationStyle::BoxWithUnderOverline(ansi_term::Style { + foreground: Some(ansi_term::Color::Fixed(1)), + background: Some(ansi_term::Color::Fixed(2)), + is_bold: true, + ..ansi_term::Style::new() + }) + ) + } + + #[test] + fn test_style_from_str() { + let actual_style = Style::from_str( + "red green bold", + None, + None, + Some("ol red box bold green ul"), + true, + false, + ); + let red_green_bold = ansi_term::Style { + foreground: Some(ansi_term::Color::Fixed(1)), + background: Some(ansi_term::Color::Fixed(2)), + is_bold: true, + ..ansi_term::Style::new() + }; + assert_eq!( + actual_style, + Style { + ansi_term_style: red_green_bold, + decoration_style: DecorationStyle::BoxWithUnderOverline(red_green_bold), + ..Style::new() + } + ) + } + + #[test] + fn test_style_from_str_raw_with_box() { + let actual_style = Style::from_str("raw", None, None, Some("box"), true, false); + let empty_ansi_term_style = ansi_term::Style::new(); + assert_eq!( + actual_style, + Style { + ansi_term_style: empty_ansi_term_style, + decoration_style: DecorationStyle::Box(empty_ansi_term_style), + is_raw: true, + ..Style::new() + } + ) + } + + #[test] + fn test_style_from_str_decoration_style_only() { + let actual_style = Style::from_str( + "", + None, + None, + Some("ol red box bold green ul"), + true, + false, + ); + let red_green_bold = ansi_term::Style { + foreground: Some(ansi_term::Color::Fixed(1)), + background: Some(ansi_term::Color::Fixed(2)), + is_bold: true, + ..ansi_term::Style::new() + }; + assert_eq!( + actual_style, + Style { + decoration_style: DecorationStyle::BoxWithUnderOverline(red_green_bold), + ..Style::new() + } + ) + } + + #[test] + fn test_style_from_str_with_handling_of_special_decoration_attributes() { + let actual_style = Style::from_str_with_handling_of_special_decoration_attributes( + "", + None, + None, + Some("ol red box bold green ul"), + true, + false, + ); + let expected_decoration_style = DecorationStyle::BoxWithUnderOverline(ansi_term::Style { + foreground: Some(ansi_term::Color::Fixed(1)), + background: Some(ansi_term::Color::Fixed(2)), + is_bold: true, + ..ansi_term::Style::new() + }); + assert_eq!( + actual_style, + Style { + decoration_style: expected_decoration_style, + ..Style::new() + } + ) + } + + #[test] + fn test_style_from_str_with_handling_of_special_decoration_attributes_raw_with_box() { + let actual_style = Style::from_str_with_handling_of_special_decoration_attributes( + "raw", + None, + None, + Some("box"), + true, + false, + ); + let empty_ansi_term_style = ansi_term::Style::new(); + assert_eq!( + actual_style, + Style { + ansi_term_style: empty_ansi_term_style, + decoration_style: DecorationStyle::Box(empty_ansi_term_style), + is_raw: true, + ..Style::new() + } + ) + } + + #[test] + fn test_style_from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg( + ) { + let expected_decoration_style = DecorationStyle::BoxWithUnderOverline(ansi_term::Style { + foreground: Some(ansi_term::Color::Fixed(1)), + background: Some(ansi_term::Color::Fixed(2)), + is_bold: true, + ..ansi_term::Style::new() + }); + let actual_style = Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg( + "", None, None, Some("ol red box bold green ul"), None, true, false + ); + assert_eq!( + actual_style, + Style { + decoration_style: expected_decoration_style, + ..Style::new() + } + ) + } + + #[test] + fn test_style_from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg_raw_with_box( + ) { + let actual_style = Style::from_str_with_handling_of_special_decoration_attributes_and_respecting_deprecated_foreground_color_arg( + "raw", + None, + None, + Some("box"), + None, + true, + false, + ); + let empty_ansi_term_style = ansi_term::Style::new(); + assert_eq!( + actual_style, + Style { + ansi_term_style: empty_ansi_term_style, + decoration_style: DecorationStyle::Box(empty_ansi_term_style), + is_raw: true, + ..Style::new() + } + ) + } +} |