Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added fn from_ansi_str for StyledObject #167

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
target
Cargo.lock
.idea
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an optional dependency. Code that depends on it then should also be feature gated.


[target.'cfg(windows)'.dependencies]
encode_unicode = "0.3"
Expand Down
229 changes: 229 additions & 0 deletions src/ansi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ use std::{
iter::{FusedIterator, Peekable},
str::CharIndices,
};
use std::str::FromStr;
use lazy_static::lazy_static;
use regex::Regex;
use crate::{Attribute, Color, Style, StyledObject};

#[derive(Debug, Clone, Copy)]
enum State {
Expand Down Expand Up @@ -267,6 +271,143 @@ 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>,
}

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<u8> {
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<Attribute> {
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 = (String, Option<Style>);

fn next(&mut self) -> Option<Self::Item> {
let mut style_option: Option<Style> = None;
let mut val: String = "".to_string();

let mut ansi_start = false;
let mut has_next = false;

for (ansi_str, is_ansi) in self.ansi_code_it.by_ref() {
has_next = true;
if !is_ansi {
val.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_option = Some(style_option.unwrap_or(Style::new()).fg(color));
if bright {
style_option = Some(style_option.unwrap_or(Style::new()).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_option = Some(style_option.unwrap_or(Style::new()).bg(color));
if bright {
style_option = Some(style_option.unwrap_or(Style::new()).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_option = Some(style_option.unwrap_or(Style::new()).attr(attr));
}
}
}
}

style_option = style_option.map(|so| so.force_styling(true));

match has_next {
false => None,
true => Some((val, style_option)),
}
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -435,4 +576,92 @@ 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::<Vec<(String, Option<Style>)>>();
let plain_texts = style_parsed.iter().map(|x| &x.0).collect::<Vec<_>>();
let styles = style_parsed.iter().map(|x| x.1.as_ref()).collect::<Vec<_>>();
assert_eq!(
vec!["hello world"],
plain_texts,
);
assert_eq!(
vec![Some(&style_origin1)],
styles,
);

// 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::<Vec<(String, Option<Style>)>>();
let plain_texts = style_parsed.iter().map(|x| &x.0).collect::<Vec<_>>();
let styles = style_parsed.iter().map(|x| x.1.as_ref()).collect::<Vec<_>>();
assert_eq!(
vec!["hello", " ", "world"],
plain_texts
);
assert_eq!(
vec![Some(&style_origin1), None, Some(&style_origin2)],
styles
);

// test for "hello [world]"
let ansi_string = format!("hello {}", style_origin2.apply_to("world"));
let style_parsed = ParsedStyledObjectIterator::new(ansi_string.as_str()).collect::<Vec<(String, Option<Style>)>>();
let plain_texts = style_parsed.iter().map(|x| &x.0).collect::<Vec<_>>();
let styles = style_parsed.iter().map(|x| x.1.as_ref()).collect::<Vec<_>>();
assert_eq!(
vec!["hello ", "world"],
plain_texts
);
assert_eq!(
vec![None, Some(&style_origin2)],
styles
);

// test for "[hello] world"
let ansi_string = format!("{} world", style_origin1.apply_to("hello"));
let style_parsed = ParsedStyledObjectIterator::new(ansi_string.as_str()).collect::<Vec<(String, Option<Style>)>>();
let plain_texts = style_parsed.iter().map(|x| &x.0).collect::<Vec<_>>();
let styles = style_parsed.iter().map(|x| x.1.as_ref()).collect::<Vec<_>>();
assert_eq!(
vec!["hello", " world"],
plain_texts
);
assert_eq!(
vec![Some(&style_origin1), None],
styles
);

// test for "hello world"
let ansi_string = "hello world";
let style_parsed = ParsedStyledObjectIterator::new(ansi_string).collect::<Vec<(String, Option<Style>)>>();
let plain_texts = style_parsed.iter().map(|x| &x.0).collect::<Vec<_>>();
let styles = style_parsed.iter().map(|x| x.1.as_ref()).collect::<Vec<_>>();
assert_eq!(
vec!["hello world"],
plain_texts
);
assert_eq!(
vec![None],
styles
);
}
}
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ pub use crate::utils::{
};

#[cfg(feature = "ansi-parsing")]
pub use crate::ansi::{strip_ansi_codes, AnsiCodeIterator};
pub use crate::ansi::{strip_ansi_codes, AnsiCodeIterator, ParsedStyledObjectIterator};

mod common_term;
mod kb;
Expand Down
20 changes: 20 additions & 0 deletions src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -419,6 +421,23 @@ impl Style {
}
}

impl Style {
pub fn get_fg(&self) -> Option<Color> {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are these APIs added? Let's not add these unless there is a reason (maybe follow up PR), and if they are added, they need docs.

self.fg
}
pub fn get_bg(&self) -> Option<Color> {
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<Attribute> {
&self.attrs
}
}
/// Wraps an object for formatting for styling.
///
/// Example:
Expand Down Expand Up @@ -820,6 +839,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
Expand Down