use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::atomic::{AtomicU32, Ordering};
use crate::color::{Color, ColorType, EIGHT_BIT_PALETTE, STANDARD_COLOR_NAMES, STANDARD_PALETTE};
static NEXT_ID: AtomicU32 = AtomicU32::new(0);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Attributes(u32);
impl Attributes {
pub const BOLD: u32 = 1 << 0;
pub const DIM: u32 = 1 << 1;
pub const ITALIC: u32 = 1 << 2;
pub const UNDERLINE: u32 = 1 << 3;
pub const BLINK: u32 = 1 << 4;
pub const REVERSE: u32 = 1 << 5;
pub const STRIKE: u32 = 1 << 6;
pub const UNDERLINE2: u32 = 1 << 7;
pub const FRAME: u32 = 1 << 8;
pub const ENCIRCLE: u32 = 1 << 9;
pub const OVERLINE: u32 = 1 << 10;
pub const BLINK2: u32 = 1 << 11;
pub const CONCEAL: u32 = 1 << 12;
pub const fn empty() -> Self {
Self(0)
}
pub fn set(&mut self, bit: u32, value: bool) {
if value {
self.0 |= bit;
} else {
self.0 &= !bit;
}
}
pub fn get(&self, bit: u32) -> bool {
self.0 & bit != 0
}
pub const fn bits(&self) -> u32 {
self.0
}
}
pub const STYLE_BITS: &[u32] = &[
Attributes::BOLD, Attributes::DIM, Attributes::ITALIC,
Attributes::UNDERLINE, Attributes::BLINK, Attributes::REVERSE,
Attributes::STRIKE, Attributes::UNDERLINE2, Attributes::FRAME,
Attributes::ENCIRCLE, Attributes::OVERLINE, Attributes::BLINK2,
Attributes::CONCEAL,
];
pub const STYLE_ATTRIBUTES: &[(&str, u32)] = &[
("bold", Attributes::BOLD),
("dim", Attributes::DIM),
("italic", Attributes::ITALIC),
("underline", Attributes::UNDERLINE),
("blink", Attributes::BLINK),
("reverse", Attributes::REVERSE),
("strike", Attributes::STRIKE),
("underline2", Attributes::UNDERLINE2),
("frame", Attributes::FRAME),
("encircle", Attributes::ENCIRCLE),
("overline", Attributes::OVERLINE),
("blink2", Attributes::BLINK2),
("conceal", Attributes::CONCEAL),
];
impl fmt::Display for Attributes {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut parts: Vec<&str> = Vec::new();
if self.get(Self::BOLD) { parts.push("bold"); }
if self.get(Self::DIM) { parts.push("dim"); }
if self.get(Self::ITALIC) { parts.push("italic"); }
if self.get(Self::UNDERLINE) { parts.push("underline"); }
if self.get(Self::BLINK) { parts.push("blink"); }
if self.get(Self::REVERSE) { parts.push("reverse"); }
if self.get(Self::CONCEAL) { parts.push("conceal"); }
if self.get(Self::STRIKE) { parts.push("strike"); }
if self.get(Self::OVERLINE) { parts.push("overline"); }
if parts.is_empty() {
write!(f, "none")
} else {
write!(f, "{}", parts.join(" "))
}
}
}
#[derive(Debug, Clone)]
pub struct Style {
pub(crate) color: Option<Color>,
pub(crate) bgcolor: Option<Color>,
pub(crate) attributes: Attributes,
pub(crate) set_attributes: u32,
pub(crate) link: Option<String>,
pub(crate) link_id: u32,
pub(crate) is_null: bool,
pub(crate) meta: Option<Vec<u8>>,
}
impl Style {
pub fn null() -> Self {
Self {
color: None,
bgcolor: None,
attributes: Attributes::empty(),
set_attributes: 0,
link: None,
link_id: 0,
is_null: true,
meta: None,
}
}
pub fn new() -> Self {
Self {
color: None,
bgcolor: None,
attributes: Attributes::empty(),
set_attributes: 0,
link: None,
link_id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
is_null: false,
meta: None,
}
}
pub fn color(mut self, color: impl Into<Option<Color>>) -> Self {
self.color = color.into();
self
}
pub fn bgcolor(mut self, bgcolor: impl Into<Option<Color>>) -> Self {
self.bgcolor = bgcolor.into();
self
}
pub fn bold(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::BOLD;
self.attributes.set(Attributes::BOLD, value);
self
}
pub fn dim(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::DIM;
self.attributes.set(Attributes::DIM, value);
self
}
pub fn italic(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::ITALIC;
self.attributes.set(Attributes::ITALIC, value);
self
}
pub fn underline(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::UNDERLINE;
self.attributes.set(Attributes::UNDERLINE, value);
self
}
pub fn blink(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::BLINK;
self.attributes.set(Attributes::BLINK, value);
self
}
pub fn reverse(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::REVERSE;
self.attributes.set(Attributes::REVERSE, value);
self
}
pub fn strike(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::STRIKE;
self.attributes.set(Attributes::STRIKE, value);
self
}
pub fn blink2(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::BLINK2;
self.attributes.set(Attributes::BLINK2, value);
self
}
pub fn conceal(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::CONCEAL;
self.attributes.set(Attributes::CONCEAL, value);
self
}
pub fn underline2(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::UNDERLINE2;
self.attributes.set(Attributes::UNDERLINE2, value);
self
}
pub fn frame(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::FRAME;
self.attributes.set(Attributes::FRAME, value);
self
}
pub fn encircle(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::ENCIRCLE;
self.attributes.set(Attributes::ENCIRCLE, value);
self
}
pub fn overline(mut self, value: bool) -> Self {
self.set_attributes |= Attributes::OVERLINE;
self.attributes.set(Attributes::OVERLINE, value);
self
}
pub fn without_color(&self) -> Self {
let mut s = self.clone();
s.color = None;
s.bgcolor = None;
s
}
pub fn background_style(&self) -> Self {
let mut s = Self::new();
s.bgcolor = self.color.clone();
s
}
pub fn transparent_background(&self) -> bool {
self.bgcolor.is_none()
}
pub fn link(mut self, url: impl Into<String>) -> Self {
self.link = Some(url.into());
self.link_id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
self
}
pub fn from_str(definition: &str) -> Self {
let mut style = Self::new();
for part in definition.split_whitespace() {
match part {
"bold" | "b" => { style.set_attributes |= Attributes::BOLD; style.attributes.set(Attributes::BOLD, true); }
"dim" | "d" => { style.set_attributes |= Attributes::DIM; style.attributes.set(Attributes::DIM, true); }
"italic" | "i" => { style.set_attributes |= Attributes::ITALIC; style.attributes.set(Attributes::ITALIC, true); }
"underline" | "u" => { style.set_attributes |= Attributes::UNDERLINE; style.attributes.set(Attributes::UNDERLINE, true); }
"blink" => { style.set_attributes |= Attributes::BLINK; style.attributes.set(Attributes::BLINK, true); }
"reverse" | "r" => { style.set_attributes |= Attributes::REVERSE; style.attributes.set(Attributes::REVERSE, true); }
"strike" | "s" => { style.set_attributes |= Attributes::STRIKE; style.attributes.set(Attributes::STRIKE, true); }
"not bold" | "!bold" | "nobold" => { style.set_attributes |= Attributes::BOLD; style.attributes.set(Attributes::BOLD, false); }
"not italic" | "!italic" | "noitalic" => { style.set_attributes |= Attributes::ITALIC; style.attributes.set(Attributes::ITALIC, false); }
"not underline" | "!underline" | "nounderline" => { style.set_attributes |= Attributes::UNDERLINE; style.attributes.set(Attributes::UNDERLINE, false); }
"none" | "default" => {}
"on" => { }
part if part.starts_with("on ") => {
if let Ok(c) = Color::parse(&part[3..]) {
style.bgcolor = Some(c);
}
}
part if part.starts_with("link=") => {
style.link = Some(part[5..].to_string());
}
part => {
if let Ok(c) = Color::parse(part) {
if style.bgcolor.is_some() && style.color.is_none() {
} else {
style.color = Some(c);
}
}
}
}
}
style
}
pub fn is_null(&self) -> bool {
self.is_null
}
pub fn is_plain(&self) -> bool {
self.color.is_none()
&& self.bgcolor.is_none()
&& self.set_attributes == 0
&& self.link.is_none()
}
pub fn get_bold(&self) -> Option<bool> {
if self.set_attributes & Attributes::BOLD != 0 {
Some(self.attributes.get(Attributes::BOLD))
} else {
None
}
}
pub fn get_italic(&self) -> Option<bool> {
if self.set_attributes & Attributes::ITALIC != 0 {
Some(self.attributes.get(Attributes::ITALIC))
} else {
None
}
}
pub fn combine(&self, other: &Style) -> Style {
if other.is_null {
return self.clone();
}
if self.is_null {
return other.clone();
}
let mut combined = self.clone();
if other.color.is_some() {
combined.color = other.color.clone();
}
if other.bgcolor.is_some() {
combined.bgcolor = other.bgcolor.clone();
}
for &bit in STYLE_BITS {
if other.set_attributes & bit != 0 {
combined.set_attributes |= bit;
combined.attributes.set(bit, other.attributes.get(bit));
}
}
if other.link.is_some() {
combined.link = other.link.clone();
combined.link_id = other.link_id;
}
if other.meta.is_some() {
combined.meta = other.meta.clone();
}
combined.is_null = false;
combined
}
pub fn to_ansi(&self) -> String {
if self.is_null {
return String::new();
}
let mut codes: Vec<String> = Vec::new();
if let Some(ref c) = self.color {
match c.color_type {
crate::color::ColorType::Default => codes.push("39".into()),
crate::color::ColorType::Standard => {
if let Some(n) = c.number {
if n < 8 {
codes.push((30 + n).to_string());
} else {
codes.push((82 + n).to_string()); }
}
}
crate::color::ColorType::EightBit => {
if let Some(n) = c.number {
codes.push(format!("38;5;{n}"));
}
}
crate::color::ColorType::TrueColor => {
if let Some((r, g, b)) = c.triplet {
codes.push(format!("38;2;{r};{g};{b}"));
}
}
}
}
if let Some(ref c) = self.bgcolor {
match c.color_type {
crate::color::ColorType::Default => codes.push("49".into()),
crate::color::ColorType::Standard => {
if let Some(n) = c.number {
if n < 8 {
codes.push((40 + n).to_string());
} else {
codes.push((92 + n).to_string()); }
}
}
crate::color::ColorType::EightBit => {
if let Some(n) = c.number {
codes.push(format!("48;5;{n}"));
}
}
crate::color::ColorType::TrueColor => {
if let Some((r, g, b)) = c.triplet {
codes.push(format!("48;2;{r};{g};{b}"));
}
}
}
}
if self.set_attributes & Attributes::BOLD != 0 {
codes.push(if self.attributes.get(Attributes::BOLD) { "1" } else { "22" }.into());
}
if self.set_attributes & Attributes::DIM != 0 {
codes.push(if self.attributes.get(Attributes::DIM) { "2" } else { "22" }.into());
}
if self.set_attributes & Attributes::ITALIC != 0 {
codes.push(if self.attributes.get(Attributes::ITALIC) { "3" } else { "23" }.into());
}
if self.set_attributes & Attributes::UNDERLINE != 0 {
codes.push(if self.attributes.get(Attributes::UNDERLINE) { "4" } else { "24" }.into());
}
if self.set_attributes & Attributes::BLINK != 0 {
codes.push(if self.attributes.get(Attributes::BLINK) { "5" } else { "25" }.into());
}
if self.set_attributes & Attributes::REVERSE != 0 {
codes.push(if self.attributes.get(Attributes::REVERSE) { "7" } else { "27" }.into());
}
if self.set_attributes & Attributes::CONCEAL != 0 {
codes.push(if self.attributes.get(Attributes::CONCEAL) { "8" } else { "28" }.into());
}
if self.set_attributes & Attributes::STRIKE != 0 {
codes.push(if self.attributes.get(Attributes::STRIKE) { "9" } else { "29" }.into());
}
if self.set_attributes & Attributes::CONCEAL != 0 {
codes.push(if self.attributes.get(Attributes::CONCEAL) { "8" } else { "28" }.into());
}
if self.set_attributes & Attributes::UNDERLINE2 != 0 {
codes.push(if self.attributes.get(Attributes::UNDERLINE2) { "21" } else { "24" }.into());
}
if self.set_attributes & Attributes::BLINK2 != 0 {
codes.push(if self.attributes.get(Attributes::BLINK2) { "6" } else { "25" }.into());
}
if self.set_attributes & Attributes::FRAME != 0 {
codes.push(if self.attributes.get(Attributes::FRAME) { "51" } else { "54" }.into());
}
if self.set_attributes & Attributes::ENCIRCLE != 0 {
codes.push(if self.attributes.get(Attributes::ENCIRCLE) { "52" } else { "54" }.into());
}
if self.set_attributes & Attributes::OVERLINE != 0 {
codes.push(if self.attributes.get(Attributes::OVERLINE) { "53" } else { "55" }.into());
}
if codes.is_empty() {
String::new()
} else {
format!("\x1b[{}m", codes.join(";"))
}
}
pub fn reset_ansi(&self) -> &'static str {
"\x1b[0m"
}
pub fn chain(&self, other: &Style) -> Style {
let mut result = Style::new();
result.color = self.color.clone().or_else(|| other.color.clone());
result.bgcolor = self.bgcolor.clone().or_else(|| other.bgcolor.clone());
result.link = self.link.clone().or_else(|| other.link.clone());
result.meta = self.meta.clone().or_else(|| other.meta.clone());
for &bit in STYLE_BITS {
if self.set_attributes & bit != 0 {
result.set_attributes |= bit;
result.attributes.set(bit, self.attributes.get(bit));
} else if other.set_attributes & bit != 0 {
result.set_attributes |= bit;
result.attributes.set(bit, other.attributes.get(bit));
}
}
result
}
pub fn copy(&self) -> Style {
self.clone()
}
pub fn clear_meta_and_links(&mut self) -> &mut Self {
self.meta = None;
self.link = None;
self
}
pub fn from_color(color: Color) -> Self {
Self::new().color(color)
}
pub fn from_meta(meta: Vec<u8>) -> Self {
let mut s = Self::new();
s.meta = Some(meta);
s
}
pub fn get_html_style(&self, _theme: Option<&crate::export::ExportTheme>) -> String {
if self.is_null {
return String::new();
}
let mut parts: Vec<String> = Vec::new();
if let Some(ref c) = self.color {
let hex = color_to_css_hex(c);
if !hex.is_empty() {
parts.push(format!("color: {}", hex));
}
}
if let Some(ref c) = self.bgcolor {
let hex = color_to_css_hex(c);
if !hex.is_empty() {
parts.push(format!("background-color: {}", hex));
}
}
if self.set_attributes & Attributes::BOLD != 0 && self.attributes.get(Attributes::BOLD) {
parts.push("font-weight: bold".into());
}
if self.set_attributes & Attributes::ITALIC != 0 && self.attributes.get(Attributes::ITALIC) {
parts.push("font-style: italic".into());
}
let mut decor: Vec<&str> = Vec::new();
if self.set_attributes & Attributes::UNDERLINE != 0
&& self.attributes.get(Attributes::UNDERLINE)
{
decor.push("underline");
}
if self.set_attributes & Attributes::UNDERLINE2 != 0
&& self.attributes.get(Attributes::UNDERLINE2)
{
decor.push("underline");
}
if self.set_attributes & Attributes::STRIKE != 0
&& self.attributes.get(Attributes::STRIKE)
{
decor.push("line-through");
}
if !decor.is_empty() {
parts.push(format!("text-decoration: {}", decor.join(" ")));
}
if parts.is_empty() {
String::new()
} else {
parts.join("; ")
}
}
pub fn normalize(&self) -> Style {
let mut s = Style::new();
s.color = self.color.clone();
s.bgcolor = self.bgcolor.clone();
s.link = self.link.clone();
s.link_id = self.link_id;
s.meta = self.meta.clone();
for &bit in STYLE_BITS {
if self.set_attributes & bit != 0 && self.attributes.get(bit) {
s.set_attributes |= bit;
s.attributes.set(bit, true);
}
}
s
}
pub fn pick_first(&self) -> Option<&'static str> {
if let Some(ref c) = self.color {
if let Some(name) = color_to_name(c) {
return Some(name);
}
}
if let Some(ref c) = self.bgcolor {
if let Some(name) = color_to_name(c) {
return Some(name);
}
}
None
}
pub fn render(&self, text: &str) -> String {
format!("{}{}{}", self.to_ansi(), text, self.reset_ansi())
}
pub fn test(&self, text: Option<&str>) -> String {
let t = text.unwrap_or("Lorem ipsum");
self.render(t)
}
pub fn update_link(&mut self, url: Option<String>) -> &mut Self {
self.link = url;
if self.link.is_some() {
self.link_id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
}
self
}
pub fn meta(&self) -> Option<&Vec<u8>> {
self.meta.as_ref()
}
pub fn meta_mut(&mut self) -> Option<&mut Vec<u8>> {
self.meta.as_mut()
}
pub fn set_meta(&mut self, meta: Option<Vec<u8>>) -> &mut Self {
self.meta = meta;
self
}
pub fn link_id(&self) -> u32 {
self.link_id
}
pub fn on(self, color: impl Into<Option<Color>>) -> Self {
self.bgcolor(color)
}
pub fn color_ref(&self) -> Option<&Color> {
self.color.as_ref()
}
pub fn bgcolor_ref(&self) -> Option<&Color> {
self.bgcolor.as_ref()
}
}
impl Default for Style {
fn default() -> Self {
Self::new()
}
}
impl PartialEq for Style {
fn eq(&self, other: &Self) -> bool {
self.color == other.color
&& self.bgcolor == other.bgcolor
&& self.attributes == other.attributes
&& self.set_attributes == other.set_attributes
&& self.link == other.link
}
}
impl Eq for Style {}
impl Hash for Style {
fn hash<H: Hasher>(&self, state: &mut H) {
self.color.hash(state);
self.bgcolor.hash(state);
self.attributes.hash(state);
self.set_attributes.hash(state);
self.link.hash(state);
}
}
impl fmt::Display for Style {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_null {
return write!(f, "null");
}
let mut parts: Vec<String> = Vec::new();
if let Some(ref c) = self.color {
parts.push(c.to_string());
}
if let Some(ref c) = self.bgcolor {
parts.push(format!("on {}", c));
}
let attrs = self.attributes.to_string();
if attrs != "none" {
parts.push(attrs);
}
if parts.is_empty() {
write!(f, "none")
} else {
write!(f, "{}", parts.join(" "))
}
}
}
pub type StyleType = Style;
fn color_to_css_hex(c: &Color) -> String {
match c.color_type {
ColorType::Default => String::new(),
ColorType::Standard => {
if let Some(n) = c.number {
let (r, g, b) = STANDARD_PALETTE[n as usize];
format!("#{:02x}{:02x}{:02x}", r, g, b)
} else {
String::new()
}
}
ColorType::EightBit => {
if let Some(n) = c.number {
let [r, g, b] = EIGHT_BIT_PALETTE[n as usize];
format!("#{:02x}{:02x}{:02x}", r, g, b)
} else {
String::new()
}
}
ColorType::TrueColor => {
if let Some((r, g, b)) = c.triplet {
format!("#{:02x}{:02x}{:02x}", r, g, b)
} else {
String::new()
}
}
}
}
fn color_to_name(c: &Color) -> Option<&'static str> {
match c.color_type {
ColorType::Standard => {
if let Some(n) = c.number {
Some(STANDARD_COLOR_NAMES[n as usize])
} else {
None
}
}
_ => None,
}
}
#[derive(Debug, Clone)]
pub struct StyleStack {
stack: Vec<Style>,
default_style: Style,
}
impl StyleStack {
pub fn new(default_style: Style) -> Self {
Self {
stack: Vec::new(),
default_style,
}
}
pub fn current(&self) -> Style {
let mut combined = self.default_style.clone();
for s in &self.stack {
combined = combined.combine(s);
}
combined
}
pub fn push(&mut self, style: Style) {
self.stack.push(style);
}
pub fn pop(&mut self) -> Option<Style> {
self.stack.pop()
}
pub fn len(&self) -> usize {
self.stack.len()
}
pub fn is_empty(&self) -> bool {
self.stack.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_style_parse() {
let s = Style::from_str("bold red");
assert_eq!(s.get_bold(), Some(true));
assert!(s.color.is_some());
}
#[test]
fn test_style_combine() {
let base = Style::from_str("red");
let over = Style::from_str("bold");
let combined = base.combine(&over);
assert_eq!(combined.get_bold(), Some(true));
assert!(combined.color.is_some());
}
#[test]
fn test_ansi_output() {
let s = Style::new().color(Color::parse("red").unwrap()).bold(true);
let ansi = s.to_ansi();
assert!(ansi.contains("31")); assert!(ansi.contains("1")); }
#[test]
fn test_chain() {
let a = Style::new().bold(true);
let b = Style::new().color(Color::parse("red").unwrap()).italic(true);
let chained = a.chain(&b);
assert_eq!(chained.get_bold(), Some(true));
assert!(chained.attributes.get(Attributes::ITALIC));
assert!(chained.set_attributes & Attributes::ITALIC != 0);
assert!(chained.color.is_some());
}
#[test]
fn test_chain_precedence() {
let a = Style::new().bold(true).color(Color::parse("red").unwrap());
let b = Style::new().bold(false).color(Color::parse("blue").unwrap());
let chained = a.chain(&b);
assert_eq!(chained.get_bold(), Some(true));
let c = chained.color.as_ref().unwrap();
let name = color_to_name(c);
assert_eq!(name, Some("red"));
}
#[test]
fn test_copy() {
let s = Style::new().bold(true).color(Color::parse("red").unwrap());
let c = s.copy();
assert_eq!(s, c);
}
#[test]
fn test_clear_meta_and_links() {
let mut s = Style::new().link("https://example.com");
s.meta = Some(vec![1, 2, 3]);
s.clear_meta_and_links();
assert!(s.link.is_none());
assert!(s.meta.is_none());
}
#[test]
fn test_from_color() {
let s = Style::from_color(Color::parse("red").unwrap());
assert!(s.color.is_some());
assert!(s.bgcolor.is_none());
}
#[test]
fn test_from_meta() {
let s = Style::from_meta(vec![10, 20, 30]);
assert_eq!(s.meta(), Some(&vec![10, 20, 30]));
}
#[test]
fn test_get_html_style() {
let s = Style::new()
.color(Color::parse("red").unwrap())
.bold(true)
.italic(true);
let css = s.get_html_style(None);
assert!(css.contains("color:"));
assert!(css.contains("font-weight: bold"));
assert!(css.contains("font-style: italic"));
}
#[test]
fn test_get_html_style_underline_strike() {
let s = Style::new()
.color(Color::parse("red").unwrap())
.underline(true)
.strike(true);
let css = s.get_html_style(None);
assert!(css.contains("text-decoration:"));
assert!(css.contains("underline"));
assert!(css.contains("line-through"));
}
#[test]
fn test_get_html_style_null() {
let s = Style::null();
let css = s.get_html_style(None);
assert!(css.is_empty());
}
#[test]
fn test_normalize() {
let s = Style::new().bold(true).italic(false);
let n = s.normalize();
assert_eq!(n.get_bold(), Some(true));
assert!(n.set_attributes & Attributes::ITALIC == 0);
}
#[test]
fn test_pick_first() {
let s = Style::new().color(Color::parse("red").unwrap());
assert_eq!(s.pick_first(), Some("red"));
}
#[test]
fn test_pick_first_fallback() {
let s = Style::new().bgcolor(Color::parse("blue").unwrap());
assert_eq!(s.pick_first(), Some("blue"));
}
#[test]
fn test_pick_first_none() {
let s = Style::new();
assert_eq!(s.pick_first(), None);
}
#[test]
fn test_render() {
let s = Style::new().bold(true).color(Color::parse("red").unwrap());
let rendered = s.render("hello");
assert!(rendered.starts_with("\x1b["));
assert!(rendered.contains("hello"));
assert!(rendered.ends_with("\x1b[0m"));
}
#[test]
fn test_test_with_text() {
let s = Style::new().bold(true);
let out = s.test(Some("custom"));
assert!(out.contains("custom"));
}
#[test]
fn test_test_default() {
let s = Style::new().bold(true);
let out = s.test(None);
assert!(out.contains("Lorem ipsum"));
}
#[test]
fn test_update_link() {
let mut s = Style::new();
s.update_link(Some("https://example.com".into()));
assert!(s.link.is_some());
let first_id = s.link_id;
s.update_link(None);
assert!(s.link.is_none());
assert_eq!(s.link_id, first_id);
}
#[test]
fn test_link_id() {
let s = Style::new().link("https://example.com");
assert!(s.link_id() > 0);
}
#[test]
fn test_meta_methods() {
let mut s = Style::new();
s.set_meta(Some(vec![1, 2, 3]));
assert_eq!(s.meta(), Some(&vec![1, 2, 3]));
if let Some(m) = s.meta_mut() {
m.push(4);
}
assert_eq!(s.meta(), Some(&vec![1, 2, 3, 4]));
}
#[test]
fn test_on() {
let s = Style::new().on(Color::parse("red").unwrap());
assert!(s.bgcolor.is_some());
let b = Color::parse("red").unwrap();
assert_eq!(s.bgcolor.unwrap(), b);
}
#[test]
fn test_references() {
let s = Style::new()
.color(Color::parse("red").unwrap())
.bgcolor(Color::parse("blue").unwrap());
assert!(s.color_ref().is_some());
assert!(s.bgcolor_ref().is_some());
}
#[test]
fn test_color_to_css_hex() {
let c = Color::parse("red").unwrap();
let hex = color_to_css_hex(&c);
assert_eq!(hex, "#800000"); }
#[test]
fn test_color_to_css_hex_truecolor() {
let c = Color::from_rgb(255, 0, 128);
let hex = color_to_css_hex(&c);
assert_eq!(hex, "#ff0080");
}
#[test]
fn test_static_attributes() {
assert!(!STYLE_ATTRIBUTES.is_empty());
let names: Vec<&str> = STYLE_ATTRIBUTES.iter().map(|(n, _)| *n).collect();
assert!(names.contains(&"bold"));
assert!(names.contains(&"italic"));
assert!(names.contains(&"underline"));
assert!(!names.contains(&"notexist"));
}
}