From c34ddee0446a3c389c9f7f8d3aa81fe94025565d Mon Sep 17 00:00:00 2001 From: BppleMan Date: Thu, 18 May 2023 16:59:40 +0800 Subject: [PATCH 1/5] Added fn from_ansi_str for StyledObject this is method can parse a string with ansi escape code and return a vector of StyledObject --- .gitignore | 1 + Cargo.toml | 1 + src/utils.rs | 178 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 180 insertions(+) diff --git a/.gitignore b/.gitignore index a9d37c56..64f40ab2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target Cargo.lock +.idea diff --git a/Cargo.toml b/Cargo.toml index 4a5c713a..19f735ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ ansi-parsing = [] libc = "0.2.30" unicode-width = { version = "0.1", optional = true } lazy_static = "1.4.0" +regex = "1.4.2" [target.'cfg(windows)'.dependencies] encode_unicode = "0.3" diff --git a/src/utils.rs b/src/utils.rs index 9e6b942f..2ebabfd4 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -2,9 +2,11 @@ use std::borrow::Cow; use std::collections::BTreeSet; use std::env; use std::fmt; +use std::str::FromStr; use std::sync::atomic::{AtomicBool, Ordering}; use lazy_static::lazy_static; +use regex::Regex; use crate::term::{wants_emoji, Term}; @@ -447,6 +449,139 @@ pub struct StyledObject { } impl StyledObject { + /// parse a string with ansi escape code and return a vector of StyledObject + pub fn from_ansi_str(ansi_str: &str) -> Vec> { + let fg_color256_or_bright = Regex::new("\x1b\\[38;5;[1-9]\\d?m").unwrap(); + let fg_color = Regex::new("\x1b\\[3\\dm").unwrap(); + + let bg_color256_or_bright = Regex::new("\x1b\\[48;5;[1-9]\\d?m").unwrap(); + let bg_color = Regex::new("\x1b\\[4\\dm").unwrap(); + + let attr = Regex::new("\x1b\\[[1-9]m").unwrap(); + + let reset = "\x1b[0m"; + + let mut style = Style::new(); + let mut val = String::new(); + let mut results = vec![]; + + // use ansi_start and ansi_end to determine whether the current ansi_str is surrounded by ansi code + // the judgment only makes sense when is_ansi is false + // if ansi_start is false and ansi_end is true, it means that the current ansi_str is not surrounded by ansi code, it's a normal string + // then we can push the current string with empty style to results + // more details can be found in the following test code in this file + // such as "[hello] [world]", the space between "hello" and "world" is not surrounded by ansi code + let mut ansi_start = false; + let mut ansi_end = false; + + for (ansi_str, is_ansi) in AnsiCodeIterator::new(ansi_str) { + if !is_ansi { + val.push_str(ansi_str); + if !ansi_start && ansi_end { + results.push(StyledObject { + style, + val, + }); + style = Style::new(); + val = String::new(); + ansi_start = false; + ansi_end = false; + } + continue; + } + if ansi_str == reset { + results.push(StyledObject { + style, + val, + }); + style = Style::new(); + val = String::new(); + // if is_ansi == true and ansi_str is reset, it means that ansi code is end + ansi_start = false; + ansi_end = true; + continue; + } + // if is_ansi == true and ansi_str is not reset, it means that ansi code is start + ansi_start = true; + ansi_end = false; + if fg_color.is_match(ansi_str) || fg_color256_or_bright.is_match(ansi_str) { + if let Some(n) = Self::parse_ansi_num(ansi_str) { + let (color, bright) = Self::convert_to_color(&n); + style = style.fg(color); + if bright { + style = style.bright(); + } + } + } else if bg_color.is_match(ansi_str) || bg_color256_or_bright.is_match(ansi_str) { + if let Some(n) = Self::parse_ansi_num(ansi_str) { + let (color, bright) = Self::convert_to_color(&n); + style = style.bg(color); + if bright { + style = style.on_bright(); + } + } + } else if attr.is_match(ansi_str) { + if let Some(n) = Self::parse_ansi_num(ansi_str) { + if let Some(attr) = Self::convert_to_attr(&n) { + style = style.attr(attr) + } + } + } + } + results + } + + /// parse a ansi code string to u8 + fn parse_ansi_num(ansi_str: &str) -> Option { + let number = Regex::new("[1-9]\\d?m").unwrap(); + // find first str which matched xxm, such as 1m, 2m, 31m + number.find(ansi_str).map(|r| { + let r_str = r.as_str(); + // trim the 'm' and convert to u8 + u8::from_str(&r_str[0..r_str.len() - 1]).unwrap() + }) + } + + /// convert ansi_num to color + /// return (color: Color, bright: bool) + fn convert_to_color(ansi_num: &u8) -> (Color, bool) { + let mut bright = false; + let ansi_num = if (40u8..47u8).contains(ansi_num) { + ansi_num - 40 + } else if (30u8..37u8).contains(ansi_num) { + ansi_num - 30 + } else if (8u8..15u8).contains(ansi_num) { + bright = true; + ansi_num - 8 + } else { + *ansi_num + }; + match ansi_num { + 0 => (Color::Black, bright), + 1 => (Color::Red, bright), + 2 => (Color::Green, bright), + 3 => (Color::Yellow, bright), + 4 => (Color::Blue, bright), + 5 => (Color::Magenta, bright), + 6 => (Color::Cyan, bright), + 7 => (Color::White, bright), + _ => (Color::Color256(ansi_num), bright), + } + } + + fn convert_to_attr(ansi_num: &u8) -> Option { + match ansi_num { + 1 => Some(Attribute::Bold), + 2 => Some(Attribute::Dim), + 3 => Some(Attribute::Italic), + 4 => Some(Attribute::Underlined), + 5 => Some(Attribute::Blink), + 7 => Some(Attribute::Reverse), + 8 => Some(Attribute::Hidden), + _ => None, + } + } + /// Forces styling on or off. /// /// This overrides the automatic detection. @@ -820,6 +955,7 @@ pub fn pad_str<'a>( ) -> Cow<'a, str> { pad_str_with(s, width, align, truncate, ' ') } + /// Pads a string with specific padding to fill a certain number of characters. /// /// This will honor ansi codes correctly and allows you to align a string @@ -960,3 +1096,45 @@ fn test_pad_str_with() { "foo..." ); } + +#[test] +fn test_parse_to_style() { + let style_origin = Style::new() + .force_styling(true) + .red() + .on_blue() + .on_bright() + .bold() + .italic(); + let ansi_string = style_origin.apply_to("hello world").to_string(); + let mut style_parsed = StyledObject::::from_ansi_str(ansi_string.as_str()); + assert_eq!(style_origin, style_parsed.pop().unwrap().style.force_styling(true)); +} + +#[test] +fn test_parse_to_style_for_multi_text() { + let style_origin1 = Style::new() + .force_styling(true) + .red() + .on_blue() + .on_bright() + .bold() + .italic(); + let style_origin2 = Style::new() + .force_styling(true) + .blue() + .on_yellow() + .on_bright() + .blink() + .italic(); + let ansi_string = format!("{} {}", style_origin1.apply_to("hello"), style_origin2.apply_to("world")); + let style_parsed = StyledObject::::from_ansi_str(ansi_string.as_str()); + assert_eq!( + vec!["hello", " ", "world"], + style_parsed.iter().map(|x| x.val.as_str()).collect::>() + ); + assert_eq!( + vec![style_origin1, Style::new().force_styling(true), style_origin2], + style_parsed.into_iter().map(|x| x.style.force_styling(true)).collect::>() + ); +} From abc2b2e288414d0efcbacec63394a859b1b73258 Mon Sep 17 00:00:00 2001 From: BppleMan Date: Thu, 18 May 2023 17:37:06 +0800 Subject: [PATCH 2/5] Added more test for parse ansi_str --- src/utils.rs | 57 ++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 8 deletions(-) diff --git a/src/utils.rs b/src/utils.rs index 2ebabfd4..821609eb 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -472,12 +472,11 @@ impl StyledObject { // more details can be found in the following test code in this file // such as "[hello] [world]", the space between "hello" and "world" is not surrounded by ansi code let mut ansi_start = false; - let mut ansi_end = false; for (ansi_str, is_ansi) in AnsiCodeIterator::new(ansi_str) { if !is_ansi { val.push_str(ansi_str); - if !ansi_start && ansi_end { + if !ansi_start { results.push(StyledObject { style, val, @@ -485,7 +484,6 @@ impl StyledObject { style = Style::new(); val = String::new(); ansi_start = false; - ansi_end = false; } continue; } @@ -498,12 +496,10 @@ impl StyledObject { val = String::new(); // if is_ansi == true and ansi_str is reset, it means that ansi code is end ansi_start = false; - ansi_end = true; continue; } // if is_ansi == true and ansi_str is not reset, it means that ansi code is start ansi_start = true; - ansi_end = false; if fg_color.is_match(ansi_str) || fg_color256_or_bright.is_match(ansi_str) { if let Some(n) = Self::parse_ansi_num(ansi_str) { let (color, bright) = Self::convert_to_color(&n); @@ -1113,6 +1109,7 @@ fn test_parse_to_style() { #[test] fn test_parse_to_style_for_multi_text() { + // test for "[hello] [world]" let style_origin1 = Style::new() .force_styling(true) .red() @@ -1129,12 +1126,56 @@ fn test_parse_to_style_for_multi_text() { .italic(); let ansi_string = format!("{} {}", style_origin1.apply_to("hello"), style_origin2.apply_to("world")); let style_parsed = StyledObject::::from_ansi_str(ansi_string.as_str()); + let plain_texts = style_parsed.iter().map(|x| x.val.as_str()).collect::>(); + let styles = style_parsed.iter().map(|x| x.style.clone().force_styling(true)).collect::>(); assert_eq!( vec!["hello", " ", "world"], - style_parsed.iter().map(|x| x.val.as_str()).collect::>() + plain_texts ); assert_eq!( - vec![style_origin1, Style::new().force_styling(true), style_origin2], - style_parsed.into_iter().map(|x| x.style.force_styling(true)).collect::>() + vec![&style_origin1, &Style::new().force_styling(true), &style_origin2], + styles.iter().collect::>() + ); + + // test for "hello [world]" + let ansi_string = format!("hello {}", style_origin2.apply_to("world")); + let style_parsed = StyledObject::::from_ansi_str(ansi_string.as_str()); + let plain_texts = style_parsed.iter().map(|x| x.val.as_str()).collect::>(); + let styles = style_parsed.iter().map(|x| x.style.clone().force_styling(true)).collect::>(); + assert_eq!( + vec!["hello ", "world"], + plain_texts + ); + assert_eq!( + vec![&Style::new().force_styling(true), &style_origin2], + styles.iter().collect::>() + ); + + // test for "[hello] world" + let ansi_string = format!("{} world", style_origin1.apply_to("hello")); + let style_parsed = StyledObject::::from_ansi_str(ansi_string.as_str()); + let plain_texts = style_parsed.iter().map(|x| x.val.as_str()).collect::>(); + let styles = style_parsed.iter().map(|x| x.style.clone().force_styling(true)).collect::>(); + assert_eq!( + vec!["hello", " world"], + plain_texts + ); + assert_eq!( + vec![&style_origin1, &Style::new().force_styling(true)], + styles.iter().collect::>() + ); + + // test for "hello world" + let ansi_string = "hello world"; + let style_parsed = StyledObject::::from_ansi_str(ansi_string); + let plain_texts = style_parsed.iter().map(|x| x.val.as_str()).collect::>(); + let styles = style_parsed.iter().map(|x| x.style.clone().force_styling(true)).collect::>(); + assert_eq!( + vec!["hello world"], + plain_texts + ); + assert_eq!( + vec![&Style::new().force_styling(true)], + styles.iter().collect::>() ); } From 3e1a8346def9c0e9b0dd3d6555199ba83987d0c9 Mon Sep 17 00:00:00 2001 From: BppleMan Date: Fri, 19 May 2023 16:00:57 +0800 Subject: [PATCH 3/5] Refactor parse ansi str to iterator --- src/ansi.rs | 211 +++++++++++++++++++++++++++++++++++++++++++++ src/utils.rs | 238 ++++++--------------------------------------------- 2 files changed, 235 insertions(+), 214 deletions(-) diff --git a/src/ansi.rs b/src/ansi.rs index 3a3c96c3..b2afc766 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -3,6 +3,11 @@ use std::{ iter::{FusedIterator, Peekable}, str::CharIndices, }; +use std::str::FromStr; +use lazy_static::lazy::Lazy; +use lazy_static::lazy_static; +use regex::Regex; +use crate::{Attribute, Color, Style, StyledObject}; #[derive(Debug, Clone, Copy)] enum State { @@ -267,6 +272,133 @@ impl<'a> Iterator for AnsiCodeIterator<'a> { impl<'a> FusedIterator for AnsiCodeIterator<'a> {} +pub struct ParsedStyledObjectIterator<'a> { + ansi_code_it: AnsiCodeIterator<'a>, +} + +impl<'a> ParsedStyledObjectIterator<'a> { + pub fn new(s: &'a str) -> ParsedStyledObjectIterator<'a> { + ParsedStyledObjectIterator { + ansi_code_it: AnsiCodeIterator::new(s), + } + } + + /// parse a ansi code string to u8 + fn parse_ansi_num(ansi_str: &str) -> Option { + let number = Regex::new("[1-9]\\d?m").unwrap(); + // find first str which matched xxm, such as 1m, 2m, 31m + number.find(ansi_str).map(|r| { + let r_str = r.as_str(); + // trim the 'm' and convert to u8 + u8::from_str(&r_str[0..r_str.len() - 1]).unwrap() + }) + } + + /// convert ansi_num to color + /// return (color: Color, bright: bool) + fn convert_to_color(ansi_num: &u8) -> (Color, bool) { + let mut bright = false; + let ansi_num = if (40u8..47u8).contains(ansi_num) { + ansi_num - 40 + } else if (30u8..37u8).contains(ansi_num) { + ansi_num - 30 + } else if (8u8..15u8).contains(ansi_num) { + bright = true; + ansi_num - 8 + } else { + *ansi_num + }; + match ansi_num { + 0 => (Color::Black, bright), + 1 => (Color::Red, bright), + 2 => (Color::Green, bright), + 3 => (Color::Yellow, bright), + 4 => (Color::Blue, bright), + 5 => (Color::Magenta, bright), + 6 => (Color::Cyan, bright), + 7 => (Color::White, bright), + _ => (Color::Color256(ansi_num), bright), + } + } + + fn convert_to_attr(ansi_num: &u8) -> Option { + match ansi_num { + 1 => Some(Attribute::Bold), + 2 => Some(Attribute::Dim), + 3 => Some(Attribute::Italic), + 4 => Some(Attribute::Underlined), + 5 => Some(Attribute::Blink), + 7 => Some(Attribute::Reverse), + 8 => Some(Attribute::Hidden), + _ => None, + } + } +} + +lazy_static! { +static ref FG_COLOR256_OR_BRIGHT_REG: Regex = Regex::new("\x1b\\[38;5;[1-9]\\d?m").unwrap(); +static ref FG_COLOR_REG: Regex = Regex::new("\x1b\\[3\\dm").unwrap(); + +static ref BG_COLOR256_OR_BRIGHT_REG: Regex = Regex::new("\x1b\\[48;5;[1-9]\\d?m").unwrap(); +static ref BG_COLOR_REG: Regex = Regex::new("\x1b\\[4\\dm").unwrap(); + +static ref ATTR_REG: Regex = Regex::new("\x1b\\[[1-9]m").unwrap(); +} + +static RESET_STR: &str = "\x1b[0m"; + +impl<'a> Iterator for ParsedStyledObjectIterator<'a> { + type Item = StyledObject; + + fn next(&mut self) -> Option { + let mut style: Style = Style::new(); + let mut val: Option = None; + + let mut ansi_start = false; + + for (ansi_str, is_ansi) in self.ansi_code_it.by_ref() { + if !is_ansi { + val.get_or_insert(String::new()).push_str(ansi_str); + if !ansi_start { + break; + } + continue; + } + if ansi_str == RESET_STR { + // if is_ansi == true and ansi_str is reset, it means that ansi code is end + break; + } + // if is_ansi == true and ansi_str is not reset, it means that ansi code is start + ansi_start = true; + if FG_COLOR_REG.is_match(ansi_str) || FG_COLOR256_OR_BRIGHT_REG.is_match(ansi_str) { + if let Some(n) = Self::parse_ansi_num(ansi_str) { + let (color, bright) = Self::convert_to_color(&n); + style = style.fg(color); + if bright { + style = style.bright(); + } + } + } else if BG_COLOR_REG.is_match(ansi_str) || BG_COLOR256_OR_BRIGHT_REG.is_match(ansi_str) { + if let Some(n) = Self::parse_ansi_num(ansi_str) { + let (color, bright) = Self::convert_to_color(&n); + style = style.bg(color); + if bright { + style = style.on_bright(); + } + } + } else if ATTR_REG.is_match(ansi_str) { + if let Some(n) = Self::parse_ansi_num(ansi_str) { + if let Some(attr) = Self::convert_to_attr(&n) { + style = style.attr(attr); + } + } + } + } + + val.map(|v| style.apply_to(v)) + } +} + #[cfg(test)] mod tests { use super::*; @@ -435,4 +567,83 @@ mod tests { assert_eq!(iter.rest_slice(), ""); assert_eq!(iter.next(), None); } + + #[test] + fn test_parse_to_style_for_multi_text() { + let style_origin1 = Style::new() + .force_styling(true) + .red() + .on_blue() + .on_bright() + .bold() + .italic(); + let style_origin2 = Style::new() + .force_styling(true) + .blue() + .on_yellow() + .on_bright() + .blink() + .italic(); + + // test for "[hello world]" + let ansi_string = style_origin1.apply_to("hello world").to_string(); + let style_parsed = ParsedStyledObjectIterator::new(ansi_string.as_str()).collect::>>(); + assert_eq!(&style_origin1, &style_parsed.first().unwrap().get_style().clone().force_styling(true)); + + // test for "[hello] [world]" + let ansi_string = format!("{} {}", style_origin1.apply_to("hello"), style_origin2.apply_to("world")); + let style_parsed = ParsedStyledObjectIterator::new(ansi_string.as_str()).collect::>>(); + let plain_texts = style_parsed.iter().map(|x| x.get_val()).collect::>(); + let styles = style_parsed.iter().map(|x| x.get_style().clone().force_styling(true)).collect::>(); + assert_eq!( + vec!["hello", " ", "world"], + plain_texts + ); + assert_eq!( + vec![&style_origin1, &Style::new().force_styling(true), &style_origin2], + styles.iter().collect::>() + ); + + // test for "hello [world]" + let ansi_string = format!("hello {}", style_origin2.apply_to("world")); + let style_parsed = ParsedStyledObjectIterator::new(ansi_string.as_str()).collect::>>(); + let plain_texts = style_parsed.iter().map(|x| x.get_val().as_str()).collect::>(); + let styles = style_parsed.iter().map(|x| x.get_style().clone().force_styling(true)).collect::>(); + assert_eq!( + vec!["hello ", "world"], + plain_texts + ); + assert_eq!( + vec![&Style::new().force_styling(true), &style_origin2], + styles.iter().collect::>() + ); + + // test for "[hello] world" + let ansi_string = format!("{} world", style_origin1.apply_to("hello")); + let style_parsed = ParsedStyledObjectIterator::new(ansi_string.as_str()).collect::>>(); + let plain_texts = style_parsed.iter().map(|x| x.get_val().as_str()).collect::>(); + let styles = style_parsed.iter().map(|x| x.get_style().clone().force_styling(true)).collect::>(); + assert_eq!( + vec!["hello", " world"], + plain_texts + ); + assert_eq!( + vec![&style_origin1, &Style::new().force_styling(true)], + styles.iter().collect::>() + ); + + // test for "hello world" + let ansi_string = "hello world"; + let style_parsed = ParsedStyledObjectIterator::new(ansi_string).collect::>>(); + let plain_texts = style_parsed.iter().map(|x| x.get_val().as_str()).collect::>(); + let styles = style_parsed.iter().map(|x| x.get_style().clone().force_styling(true)).collect::>(); + assert_eq!( + vec!["hello world"], + plain_texts + ); + assert_eq!( + vec![&Style::new().force_styling(true)], + styles.iter().collect::>() + ); + } } diff --git a/src/utils.rs b/src/utils.rs index 821609eb..418f6a0e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -421,6 +421,23 @@ impl Style { } } +impl Style { + pub fn get_fg(&self) -> Option { + self.fg + } + pub fn get_bg(&self) -> Option { + self.bg + } + pub fn get_fg_bright(&self) -> bool { + self.fg_bright + } + pub fn get_bg_bright(&self) -> bool { + self.bg_bright + } + pub fn get_attrs(&self) -> &BTreeSet { + &self.attrs + } +} /// Wraps an object for formatting for styling. /// /// Example: @@ -448,136 +465,16 @@ pub struct StyledObject { val: D, } -impl StyledObject { - /// parse a string with ansi escape code and return a vector of StyledObject - pub fn from_ansi_str(ansi_str: &str) -> Vec> { - let fg_color256_or_bright = Regex::new("\x1b\\[38;5;[1-9]\\d?m").unwrap(); - let fg_color = Regex::new("\x1b\\[3\\dm").unwrap(); - - let bg_color256_or_bright = Regex::new("\x1b\\[48;5;[1-9]\\d?m").unwrap(); - let bg_color = Regex::new("\x1b\\[4\\dm").unwrap(); - - let attr = Regex::new("\x1b\\[[1-9]m").unwrap(); - - let reset = "\x1b[0m"; - - let mut style = Style::new(); - let mut val = String::new(); - let mut results = vec![]; - - // use ansi_start and ansi_end to determine whether the current ansi_str is surrounded by ansi code - // the judgment only makes sense when is_ansi is false - // if ansi_start is false and ansi_end is true, it means that the current ansi_str is not surrounded by ansi code, it's a normal string - // then we can push the current string with empty style to results - // more details can be found in the following test code in this file - // such as "[hello] [world]", the space between "hello" and "world" is not surrounded by ansi code - let mut ansi_start = false; - - for (ansi_str, is_ansi) in AnsiCodeIterator::new(ansi_str) { - if !is_ansi { - val.push_str(ansi_str); - if !ansi_start { - results.push(StyledObject { - style, - val, - }); - style = Style::new(); - val = String::new(); - ansi_start = false; - } - continue; - } - if ansi_str == reset { - results.push(StyledObject { - style, - val, - }); - style = Style::new(); - val = String::new(); - // if is_ansi == true and ansi_str is reset, it means that ansi code is end - ansi_start = false; - continue; - } - // if is_ansi == true and ansi_str is not reset, it means that ansi code is start - ansi_start = true; - if fg_color.is_match(ansi_str) || fg_color256_or_bright.is_match(ansi_str) { - if let Some(n) = Self::parse_ansi_num(ansi_str) { - let (color, bright) = Self::convert_to_color(&n); - style = style.fg(color); - if bright { - style = style.bright(); - } - } - } else if bg_color.is_match(ansi_str) || bg_color256_or_bright.is_match(ansi_str) { - if let Some(n) = Self::parse_ansi_num(ansi_str) { - let (color, bright) = Self::convert_to_color(&n); - style = style.bg(color); - if bright { - style = style.on_bright(); - } - } - } else if attr.is_match(ansi_str) { - if let Some(n) = Self::parse_ansi_num(ansi_str) { - if let Some(attr) = Self::convert_to_attr(&n) { - style = style.attr(attr) - } - } - } - } - results - } - - /// parse a ansi code string to u8 - fn parse_ansi_num(ansi_str: &str) -> Option { - let number = Regex::new("[1-9]\\d?m").unwrap(); - // find first str which matched xxm, such as 1m, 2m, 31m - number.find(ansi_str).map(|r| { - let r_str = r.as_str(); - // trim the 'm' and convert to u8 - u8::from_str(&r_str[0..r_str.len() - 1]).unwrap() - }) - } - - /// convert ansi_num to color - /// return (color: Color, bright: bool) - fn convert_to_color(ansi_num: &u8) -> (Color, bool) { - let mut bright = false; - let ansi_num = if (40u8..47u8).contains(ansi_num) { - ansi_num - 40 - } else if (30u8..37u8).contains(ansi_num) { - ansi_num - 30 - } else if (8u8..15u8).contains(ansi_num) { - bright = true; - ansi_num - 8 - } else { - *ansi_num - }; - match ansi_num { - 0 => (Color::Black, bright), - 1 => (Color::Red, bright), - 2 => (Color::Green, bright), - 3 => (Color::Yellow, bright), - 4 => (Color::Blue, bright), - 5 => (Color::Magenta, bright), - 6 => (Color::Cyan, bright), - 7 => (Color::White, bright), - _ => (Color::Color256(ansi_num), bright), - } +impl StyledObject { + pub fn get_style(&self) -> &Style { + &self.style } - - fn convert_to_attr(ansi_num: &u8) -> Option { - match ansi_num { - 1 => Some(Attribute::Bold), - 2 => Some(Attribute::Dim), - 3 => Some(Attribute::Italic), - 4 => Some(Attribute::Underlined), - 5 => Some(Attribute::Blink), - 7 => Some(Attribute::Reverse), - 8 => Some(Attribute::Hidden), - _ => None, - } + pub fn get_val(&self) -> &D { + &self.val } +} +impl StyledObject { /// Forces styling on or off. /// /// This overrides the automatic detection. @@ -1092,90 +989,3 @@ fn test_pad_str_with() { "foo..." ); } - -#[test] -fn test_parse_to_style() { - let style_origin = Style::new() - .force_styling(true) - .red() - .on_blue() - .on_bright() - .bold() - .italic(); - let ansi_string = style_origin.apply_to("hello world").to_string(); - let mut style_parsed = StyledObject::::from_ansi_str(ansi_string.as_str()); - assert_eq!(style_origin, style_parsed.pop().unwrap().style.force_styling(true)); -} - -#[test] -fn test_parse_to_style_for_multi_text() { - // test for "[hello] [world]" - let style_origin1 = Style::new() - .force_styling(true) - .red() - .on_blue() - .on_bright() - .bold() - .italic(); - let style_origin2 = Style::new() - .force_styling(true) - .blue() - .on_yellow() - .on_bright() - .blink() - .italic(); - let ansi_string = format!("{} {}", style_origin1.apply_to("hello"), style_origin2.apply_to("world")); - let style_parsed = StyledObject::::from_ansi_str(ansi_string.as_str()); - let plain_texts = style_parsed.iter().map(|x| x.val.as_str()).collect::>(); - let styles = style_parsed.iter().map(|x| x.style.clone().force_styling(true)).collect::>(); - assert_eq!( - vec!["hello", " ", "world"], - plain_texts - ); - assert_eq!( - vec![&style_origin1, &Style::new().force_styling(true), &style_origin2], - styles.iter().collect::>() - ); - - // test for "hello [world]" - let ansi_string = format!("hello {}", style_origin2.apply_to("world")); - let style_parsed = StyledObject::::from_ansi_str(ansi_string.as_str()); - let plain_texts = style_parsed.iter().map(|x| x.val.as_str()).collect::>(); - let styles = style_parsed.iter().map(|x| x.style.clone().force_styling(true)).collect::>(); - assert_eq!( - vec!["hello ", "world"], - plain_texts - ); - assert_eq!( - vec![&Style::new().force_styling(true), &style_origin2], - styles.iter().collect::>() - ); - - // test for "[hello] world" - let ansi_string = format!("{} world", style_origin1.apply_to("hello")); - let style_parsed = StyledObject::::from_ansi_str(ansi_string.as_str()); - let plain_texts = style_parsed.iter().map(|x| x.val.as_str()).collect::>(); - let styles = style_parsed.iter().map(|x| x.style.clone().force_styling(true)).collect::>(); - assert_eq!( - vec!["hello", " world"], - plain_texts - ); - assert_eq!( - vec![&style_origin1, &Style::new().force_styling(true)], - styles.iter().collect::>() - ); - - // test for "hello world" - let ansi_string = "hello world"; - let style_parsed = StyledObject::::from_ansi_str(ansi_string); - let plain_texts = style_parsed.iter().map(|x| x.val.as_str()).collect::>(); - let styles = style_parsed.iter().map(|x| x.style.clone().force_styling(true)).collect::>(); - assert_eq!( - vec!["hello world"], - plain_texts - ); - assert_eq!( - vec![&Style::new().force_styling(true)], - styles.iter().collect::>() - ); -} From d2bb8c12f960660469d85cfd7fc775c4161e44c8 Mon Sep 17 00:00:00 2001 From: BppleMan Date: Fri, 19 May 2023 16:50:13 +0800 Subject: [PATCH 4/5] Refactor parse ansi str to iterator --- src/ansi.rs | 84 +++++++++++++++++++++++++++++++--------------------- src/utils.rs | 9 ------ 2 files changed, 51 insertions(+), 42 deletions(-) diff --git a/src/ansi.rs b/src/ansi.rs index b2afc766..a7397b1b 100644 --- a/src/ansi.rs +++ b/src/ansi.rs @@ -4,7 +4,6 @@ use std::{ str::CharIndices, }; use std::str::FromStr; -use lazy_static::lazy::Lazy; use lazy_static::lazy_static; use regex::Regex; use crate::{Attribute, Color, Style, StyledObject}; @@ -272,6 +271,9 @@ impl<'a> Iterator for AnsiCodeIterator<'a> { impl<'a> FusedIterator for AnsiCodeIterator<'a> {} +/// An iterator over styled objects in a string. +/// +/// This type can be used to scan over styled objects in a string. pub struct ParsedStyledObjectIterator<'a> { ansi_code_it: AnsiCodeIterator<'a>, } @@ -348,17 +350,19 @@ static ref ATTR_REG: Regex = Regex::new("\x1b\\[[1-9]m").unwrap(); static RESET_STR: &str = "\x1b[0m"; impl<'a> Iterator for ParsedStyledObjectIterator<'a> { - type Item = StyledObject; + type Item = (String, Option