1use std::collections::BTreeMap;
9use std::sync::Arc;
10
11use crate::color::{ColorSystem, SimpleColor as Color};
12
13pub const NULL_STYLE: Style = Style {
17 color: None,
18 bgcolor: None,
19 bold: None,
20 dim: None,
21 italic: None,
22 underline: None,
23 blink: None,
24 blink2: None,
25 reverse: None,
26 conceal: None,
27 strike: None,
28 underline2: None,
29 frame: None,
30 encircle: None,
31 overline: None,
32};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
41pub struct Style {
42 pub color: Option<Color>,
44 pub bgcolor: Option<Color>,
46 pub bold: Option<bool>,
48 pub dim: Option<bool>,
50 pub italic: Option<bool>,
52 pub underline: Option<bool>,
54 pub blink: Option<bool>,
56 pub blink2: Option<bool>,
58 pub reverse: Option<bool>,
60 pub conceal: Option<bool>,
62 pub strike: Option<bool>,
64 pub underline2: Option<bool>,
66 pub frame: Option<bool>,
68 pub encircle: Option<bool>,
70 pub overline: Option<bool>,
72}
73
74impl Style {
75 pub fn new() -> Self {
77 Self::default()
78 }
79
80 pub fn color(color: Color) -> Self {
82 Self {
83 color: Some(color),
84 ..Default::default()
85 }
86 }
87
88 pub fn bgcolor(color: Color) -> Self {
90 Self {
91 bgcolor: Some(color),
92 ..Default::default()
93 }
94 }
95
96 pub fn with_color(mut self, color: Color) -> Self {
98 self.color = Some(color);
99 self
100 }
101
102 pub fn with_bgcolor(mut self, color: Color) -> Self {
104 self.bgcolor = Some(color);
105 self
106 }
107
108 pub fn with_bold(mut self, bold: bool) -> Self {
110 self.bold = Some(bold);
111 self
112 }
113
114 pub fn with_dim(mut self, dim: bool) -> Self {
116 self.dim = Some(dim);
117 self
118 }
119
120 pub fn with_italic(mut self, italic: bool) -> Self {
122 self.italic = Some(italic);
123 self
124 }
125
126 pub fn with_underline(mut self, underline: bool) -> Self {
128 self.underline = Some(underline);
129 self
130 }
131
132 pub fn with_reverse(mut self, reverse: bool) -> Self {
134 self.reverse = Some(reverse);
135 self
136 }
137
138 pub fn with_strike(mut self, strike: bool) -> Self {
140 self.strike = Some(strike);
141 self
142 }
143
144 pub fn with_blink2(mut self, blink2: bool) -> Self {
146 self.blink2 = Some(blink2);
147 self
148 }
149
150 pub fn with_conceal(mut self, conceal: bool) -> Self {
152 self.conceal = Some(conceal);
153 self
154 }
155
156 pub fn with_underline2(mut self, underline2: bool) -> Self {
158 self.underline2 = Some(underline2);
159 self
160 }
161
162 pub fn with_frame(mut self, frame: bool) -> Self {
164 self.frame = Some(frame);
165 self
166 }
167
168 pub fn with_encircle(mut self, encircle: bool) -> Self {
170 self.encircle = Some(encircle);
171 self
172 }
173
174 pub fn with_overline(mut self, overline: bool) -> Self {
176 self.overline = Some(overline);
177 self
178 }
179
180 pub fn combine(&self, other: &Style) -> Self {
184 Style {
185 color: other.color.or(self.color),
186 bgcolor: other.bgcolor.or(self.bgcolor),
187 bold: other.bold.or(self.bold),
188 dim: other.dim.or(self.dim),
189 italic: other.italic.or(self.italic),
190 underline: other.underline.or(self.underline),
191 blink: other.blink.or(self.blink),
192 blink2: other.blink2.or(self.blink2),
193 reverse: other.reverse.or(self.reverse),
194 conceal: other.conceal.or(self.conceal),
195 strike: other.strike.or(self.strike),
196 underline2: other.underline2.or(self.underline2),
197 frame: other.frame.or(self.frame),
198 encircle: other.encircle.or(self.encircle),
199 overline: other.overline.or(self.overline),
200 }
201 }
202
203 pub fn parse(s: &str) -> Option<Self> {
211 let mut style = Style::new();
212 let mut on_background = false;
213
214 let mut words = s.split_whitespace().peekable();
215
216 while let Some(word) = words.next() {
217 let word_lower = word.to_lowercase();
218
219 if word_lower == "on" {
220 on_background = true;
221 continue;
222 }
223
224 if on_background {
225 if let Some(color) = Color::parse(&word_lower) {
226 style.bgcolor = Some(color);
227 on_background = false;
228 continue;
229 }
230 on_background = false;
233 }
234
235 if let Some(named) = crate::theme::get_default_style(&word_lower) {
238 style = style.combine(&named);
239 continue;
240 }
241
242 if word_lower == "not" {
245 if let Some(&next_word) = words.peek() {
246 let next_lower = next_word.to_lowercase();
247 match next_lower.as_str() {
248 "bold" | "b" => {
249 style.bold = Some(false);
250 words.next();
251 continue;
252 }
253 "dim" | "d" => {
254 style.dim = Some(false);
255 words.next();
256 continue;
257 }
258 "italic" | "i" => {
259 style.italic = Some(false);
260 words.next();
261 continue;
262 }
263 "underline" | "u" => {
264 style.underline = Some(false);
265 words.next();
266 continue;
267 }
268 "blink" => {
269 style.blink = Some(false);
270 words.next();
271 continue;
272 }
273 "blink2" => {
274 style.blink2 = Some(false);
275 words.next();
276 continue;
277 }
278 "reverse" | "r" => {
279 style.reverse = Some(false);
280 words.next();
281 continue;
282 }
283 "conceal" | "c" => {
284 style.conceal = Some(false);
285 words.next();
286 continue;
287 }
288 "strike" | "s" => {
289 style.strike = Some(false);
290 words.next();
291 continue;
292 }
293 "underline2" | "uu" => {
294 style.underline2 = Some(false);
295 words.next();
296 continue;
297 }
298 "frame" => {
299 style.frame = Some(false);
300 words.next();
301 continue;
302 }
303 "encircle" => {
304 style.encircle = Some(false);
305 words.next();
306 continue;
307 }
308 "overline" | "o" => {
309 style.overline = Some(false);
310 words.next();
311 continue;
312 }
313 _ => {}
314 }
315 }
316 continue;
318 }
319
320 match word_lower.as_str() {
322 "bold" | "b" => style.bold = Some(true),
323 "dim" | "d" => style.dim = Some(true),
324 "italic" | "i" => style.italic = Some(true),
325 "underline" | "u" => style.underline = Some(true),
326 "blink" => style.blink = Some(true),
327 "blink2" => style.blink2 = Some(true),
328 "reverse" | "r" => style.reverse = Some(true),
329 "conceal" | "c" => style.conceal = Some(true),
330 "strike" | "s" => style.strike = Some(true),
331 "underline2" | "uu" => style.underline2 = Some(true),
332 "frame" => style.frame = Some(true),
333 "encircle" => style.encircle = Some(true),
334 "overline" | "o" => style.overline = Some(true),
335 _ => {
336 if let Some(color) = Color::parse(&word_lower) {
338 style.color = Some(color);
339 }
340 }
341 }
342 }
343
344 Some(style)
345 }
346
347 pub fn is_null(&self) -> bool {
349 *self == NULL_STYLE
350 }
351
352 pub fn render(&self, text: &str, color_system: ColorSystem) -> String {
376 if text.is_empty() {
377 return String::new();
378 }
379
380 let attrs = self.make_ansi_codes(color_system);
381 if attrs.is_empty() {
382 text.to_string()
383 } else {
384 format!("\x1b[{}m{}\x1b[0m", attrs, text)
385 }
386 }
387
388 pub fn render_open(&self, text: &str, color_system: ColorSystem) -> String {
394 if text.is_empty() {
395 return String::new();
396 }
397
398 let attrs = self.make_ansi_codes(color_system);
399 if attrs.is_empty() {
400 text.to_string()
401 } else {
402 format!("\x1b[{}m{}", attrs, text)
403 }
404 }
405
406 fn make_ansi_codes(&self, color_system: ColorSystem) -> String {
410 let mut sgr: Vec<String> = Vec::new();
411
412 if self.bold == Some(false) || self.dim == Some(false) {
427 sgr.push("22".to_string());
428 }
429 if self.italic == Some(false) {
430 sgr.push("23".to_string());
431 }
432 if self.underline == Some(false) || self.underline2 == Some(false) {
433 sgr.push("24".to_string());
434 }
435 if self.blink == Some(false) || self.blink2 == Some(false) {
436 sgr.push("25".to_string());
437 }
438 if self.reverse == Some(false) {
439 sgr.push("27".to_string());
440 }
441 if self.conceal == Some(false) {
442 sgr.push("28".to_string());
443 }
444 if self.strike == Some(false) {
445 sgr.push("29".to_string());
446 }
447 if self.frame == Some(false) || self.encircle == Some(false) {
448 sgr.push("54".to_string());
449 }
450 if self.overline == Some(false) {
451 sgr.push("55".to_string());
452 }
453
454 if self.bold == Some(true) {
459 sgr.push("1".to_string());
460 }
461 if self.dim == Some(true) {
462 sgr.push("2".to_string());
463 }
464 if self.italic == Some(true) {
465 sgr.push("3".to_string());
466 }
467 if self.underline == Some(true) {
468 sgr.push("4".to_string());
469 }
470 if self.blink == Some(true) {
471 sgr.push("5".to_string());
472 }
473 if self.blink2 == Some(true) {
474 sgr.push("6".to_string());
475 }
476 if self.reverse == Some(true) {
477 sgr.push("7".to_string());
478 }
479 if self.conceal == Some(true) {
480 sgr.push("8".to_string());
481 }
482 if self.strike == Some(true) {
483 sgr.push("9".to_string());
484 }
485 if self.underline2 == Some(true) {
486 sgr.push("21".to_string());
487 }
488 if self.frame == Some(true) {
489 sgr.push("51".to_string());
490 }
491 if self.encircle == Some(true) {
492 sgr.push("52".to_string());
493 }
494 if self.overline == Some(true) {
495 sgr.push("53".to_string());
496 }
497
498 if let Some(color) = self.color {
500 let downgraded = color.downgrade(color_system);
501 sgr.extend(downgraded.get_ansi_codes(true));
502 }
503
504 if let Some(bgcolor) = self.bgcolor {
506 let downgraded = bgcolor.downgrade(color_system);
507 sgr.extend(downgraded.get_ansi_codes(false));
508 }
509
510 sgr.join(";")
511 }
512
513 pub fn get_html_style(&self) -> String {
532 let mut css: Vec<String> = Vec::new();
533
534 let (color, bgcolor) = if self.reverse == Some(true) {
536 (self.bgcolor, self.color)
537 } else {
538 (self.color, self.bgcolor)
539 };
540
541 if let Some(c) = color {
543 let hex = c.get_hex();
544 css.push(format!("color: {}", hex));
545 css.push(format!("text-decoration-color: {}", hex));
546 }
547
548 if let Some(c) = bgcolor {
550 let hex = c.get_hex();
551 css.push(format!("background-color: {}", hex));
552 }
553
554 if self.bold == Some(true) {
556 css.push("font-weight: bold".to_string());
557 }
558 if self.italic == Some(true) {
559 css.push("font-style: italic".to_string());
560 }
561
562 let mut decorations = Vec::new();
564 if self.underline == Some(true) {
565 decorations.push("underline");
566 }
567 if self.strike == Some(true) {
568 decorations.push("line-through");
569 }
570 if self.overline == Some(true) {
571 decorations.push("overline");
572 }
573 if !decorations.is_empty() {
574 css.push(format!("text-decoration: {}", decorations.join(" ")));
575 }
576
577 css.join("; ")
578 }
579
580 pub fn chain(styles: &[Style]) -> Self {
584 let mut result = Style::new();
585 for style in styles {
586 result = result.combine(style);
587 }
588 result
589 }
590
591 pub fn from_color(color: Option<Color>, bgcolor: Option<Color>) -> Self {
595 Self {
596 color,
597 bgcolor,
598 ..Default::default()
599 }
600 }
601
602 pub fn on<I, K>(meta: Option<BTreeMap<String, MetaValue>>, handlers: I) -> StyleMeta
607 where
608 I: IntoIterator<Item = (K, MetaValue)>,
609 K: Into<String>,
610 {
611 let mut merged = meta.unwrap_or_default();
612 for (key, value) in handlers {
613 merged.insert(format!("@{}", key.into()), value);
614 }
615
616 StyleMeta {
617 meta: if merged.is_empty() {
618 None
619 } else {
620 Some(Arc::new(merged))
621 },
622 ..Default::default()
623 }
624 }
625
626 pub fn normalize(style: &str) -> String {
631 let normalized = style.trim().to_lowercase();
632 if normalized.is_empty() {
633 return "none".to_string();
634 }
635
636 fn is_attr(word: &str) -> bool {
637 matches!(
638 word,
639 "bold"
640 | "b"
641 | "dim"
642 | "d"
643 | "italic"
644 | "i"
645 | "underline"
646 | "u"
647 | "blink"
648 | "blink2"
649 | "reverse"
650 | "r"
651 | "conceal"
652 | "c"
653 | "strike"
654 | "s"
655 | "underline2"
656 | "uu"
657 | "frame"
658 | "encircle"
659 | "overline"
660 | "o"
661 )
662 }
663
664 let mut words = normalized.split_whitespace().peekable();
665 while let Some(word) = words.next() {
666 match word {
667 "on" => match words.next() {
668 Some(color) if Color::parse(color).is_some() => {}
669 _ => return normalized,
670 },
671 "not" => match words.next() {
672 Some(attr) if is_attr(attr) => {}
673 _ => return normalized,
674 },
675 _ => {
676 if is_attr(word)
677 || Color::parse(word).is_some()
678 || crate::theme::get_default_style(word).is_some()
679 {
680 continue;
681 }
682 return normalized;
683 }
684 }
685 }
686
687 let parsed = Self::parse(&normalized).unwrap_or_default();
688 let canonical = parsed.to_markup_string();
689 if canonical.is_empty() {
690 "none".to_string()
691 } else {
692 canonical
693 }
694 }
695
696 pub fn pick_first(values: &[Option<Style>]) -> Style {
700 values
701 .iter()
702 .flatten()
703 .copied()
704 .next()
705 .expect("expected at least one non-None style")
706 }
707
708 pub fn test(&self, text: &str, color_system: ColorSystem) -> String {
712 self.render(text, color_system)
713 }
714
715 pub fn background_style(&self) -> Self {
719 Style {
720 bgcolor: self.bgcolor,
721 ..Default::default()
722 }
723 }
724
725 pub fn without_color(&self) -> Self {
729 Style {
730 color: None,
731 bgcolor: None,
732 bold: self.bold,
733 dim: self.dim,
734 italic: self.italic,
735 underline: self.underline,
736 blink: self.blink,
737 blink2: self.blink2,
738 reverse: self.reverse,
739 conceal: self.conceal,
740 strike: self.strike,
741 underline2: self.underline2,
742 frame: self.frame,
743 encircle: self.encircle,
744 overline: self.overline,
745 }
746 }
747
748 pub fn has_transparent_background(&self) -> bool {
752 matches!(self.bgcolor, None | Some(Color::Default))
753 }
754
755 pub fn to_markup_string(&self) -> String {
761 use crate::color::ANSI_COLOR_NAMES;
762
763 let mut parts: Vec<&str> = Vec::new();
764 macro_rules! attr {
766 ($field:ident, $name:expr, $neg:expr) => {
767 match self.$field {
768 Some(true) => parts.push($name),
769 Some(false) => parts.push($neg),
770 None => {}
771 }
772 };
773 ($field:ident, $name:expr) => {
774 if self.$field == Some(true) {
775 parts.push($name);
776 }
777 };
778 }
779 attr!(bold, "bold", "not bold");
780 attr!(dim, "dim", "not dim");
781 attr!(italic, "italic", "not italic");
782 attr!(underline, "underline", "not underline");
783 attr!(blink, "blink", "not blink");
784 attr!(blink2, "blink2", "not blink2");
785 attr!(reverse, "reverse", "not reverse");
786 attr!(conceal, "conceal", "not conceal");
787 attr!(strike, "strike", "not strike");
788 attr!(underline2, "underline2", "not underline2");
789 attr!(frame, "frame", "not frame");
790 attr!(encircle, "encircle", "not encircle");
791 attr!(overline, "overline", "not overline");
792
793 let mut owned_parts: Vec<String> = parts.iter().map(|s| s.to_string()).collect();
794
795 if let Some(ref color) = self.color {
797 owned_parts.push(simple_color_name(color, &ANSI_COLOR_NAMES));
798 }
799
800 if let Some(ref bgcolor) = self.bgcolor {
802 owned_parts.push(format!(
803 "on {}",
804 simple_color_name(bgcolor, &ANSI_COLOR_NAMES)
805 ));
806 }
807
808 owned_parts.join(" ")
809 }
810}
811
812fn simple_color_name(color: &Color, color_names: &std::collections::HashMap<&str, u8>) -> String {
814 match color {
815 Color::Default => String::new(),
816 Color::Standard(n) => {
817 for (name, &idx) in color_names.iter() {
818 if idx == *n {
819 return name.to_string();
820 }
821 }
822 format!("color({})", n)
823 }
824 Color::EightBit(n) => format!("color({})", n),
825 Color::Rgb { r, g, b } => format!("#{:02x}{:02x}{:02x}", r, g, b),
826 }
827}
828
829#[derive(Debug, Clone)]
833pub struct StyleStack {
834 _stack: Vec<Style>,
835}
836
837impl StyleStack {
838 pub fn new(default_style: Style) -> Self {
840 StyleStack {
841 _stack: vec![default_style],
842 }
843 }
844
845 pub fn current(&self) -> Style {
847 *self._stack.last().unwrap()
848 }
849
850 pub fn push(&mut self, style: Style) {
852 let combined = self.current().combine(&style);
853 self._stack.push(combined);
854 }
855
856 pub fn pop(&mut self) -> Style {
858 self._stack.pop();
859 self.current()
860 }
861}
862
863impl std::ops::Add for Style {
864 type Output = Self;
865
866 fn add(self, other: Self) -> Self {
867 self.combine(&other)
868 }
869}
870
871#[derive(Debug, Clone, PartialEq, Eq, Default)]
879pub struct StyleMeta {
880 pub link: Option<Arc<str>>,
882 pub link_id: Option<Arc<str>>,
884 pub meta: Option<Arc<BTreeMap<String, MetaValue>>>,
886}
887
888impl StyleMeta {
889 pub fn new() -> Self {
891 Self::default()
892 }
893
894 pub fn with_link(link: impl Into<Arc<str>>) -> Self {
896 StyleMeta {
897 link: Some(link.into()),
898 ..Default::default()
899 }
900 }
901
902 pub fn is_empty(&self) -> bool {
904 self.link.is_none() && self.link_id.is_none() && self.meta.is_none()
905 }
906
907 pub fn combine(&self, other: &StyleMeta) -> Self {
909 StyleMeta {
910 link: other.link.clone().or_else(|| self.link.clone()),
911 link_id: other.link_id.clone().or_else(|| self.link_id.clone()),
912 meta: match (&self.meta, &other.meta) {
913 (Some(a), Some(b)) => {
914 let mut merged = (**a).clone();
915 merged.extend((**b).clone());
916 Some(Arc::new(merged))
917 }
918 (None, Some(b)) => Some(b.clone()),
919 (Some(a), None) => Some(a.clone()),
920 (None, None) => None,
921 },
922 }
923 }
924}
925
926#[derive(Debug, Clone, PartialEq, Eq)]
931pub enum MetaValue {
932 None,
933 Bool(bool),
934 Int(i64),
935 Str(Arc<str>),
936 List(Vec<MetaValue>),
937 Tuple(Vec<MetaValue>),
938 Map(BTreeMap<String, MetaValue>),
939}
940
941impl Default for MetaValue {
942 fn default() -> Self {
943 MetaValue::None
944 }
945}
946
947impl MetaValue {
948 pub fn str(value: impl Into<Arc<str>>) -> Self {
949 MetaValue::Str(value.into())
950 }
951
952 pub fn parse_python_literal(input: &str) -> Option<Self> {
962 struct Parser<'a> {
963 s: &'a str,
964 i: usize,
965 }
966
967 impl<'a> Parser<'a> {
968 fn new(s: &'a str) -> Self {
969 Self { s, i: 0 }
970 }
971
972 fn is_eof(&self) -> bool {
973 self.i >= self.s.len()
974 }
975
976 fn rest(&self) -> &'a str {
977 &self.s[self.i..]
978 }
979
980 fn skip_ws(&mut self) {
981 while let Some(ch) = self.peek() {
982 if ch.is_whitespace() {
983 self.bump(ch);
984 } else {
985 break;
986 }
987 }
988 }
989
990 fn peek(&self) -> Option<char> {
991 self.rest().chars().next()
992 }
993
994 fn bump(&mut self, ch: char) {
995 self.i += ch.len_utf8();
996 }
997
998 fn eat(&mut self, expected: char) -> bool {
999 self.skip_ws();
1000 if self.peek() == Some(expected) {
1001 self.bump(expected);
1002 true
1003 } else {
1004 false
1005 }
1006 }
1007
1008 fn parse_value(&mut self) -> Option<MetaValue> {
1009 self.skip_ws();
1010 let ch = self.peek()?;
1011
1012 match ch {
1013 '\'' | '"' => self.parse_string().map(|s| MetaValue::Str(Arc::from(s))),
1014 '[' => self.parse_list(),
1015 '{' => self.parse_map(),
1016 '(' => self.parse_parens(),
1017 '-' | '0'..='9' => self.parse_int().map(MetaValue::Int),
1018 _ => self.parse_ident_or_keyword(),
1019 }
1020 }
1021
1022 fn parse_ident_or_keyword(&mut self) -> Option<MetaValue> {
1023 self.skip_ws();
1024 let start = self.i;
1025 while let Some(ch) = self.peek() {
1026 if ch.is_ascii_alphanumeric() || ch == '_' {
1027 self.bump(ch);
1028 } else {
1029 break;
1030 }
1031 }
1032 if self.i == start {
1033 return None;
1034 }
1035 let ident = &self.s[start..self.i];
1036 match ident {
1037 "None" => Some(MetaValue::None),
1038 "True" => Some(MetaValue::Bool(true)),
1039 "False" => Some(MetaValue::Bool(false)),
1040 _ => None,
1041 }
1042 }
1043
1044 fn parse_int(&mut self) -> Option<i64> {
1045 self.skip_ws();
1046 let start = self.i;
1047 if self.peek() == Some('-') {
1048 self.bump('-');
1049 }
1050 let mut saw_digit = false;
1051 while let Some(ch) = self.peek() {
1052 if ch.is_ascii_digit() {
1053 saw_digit = true;
1054 self.bump(ch);
1055 } else {
1056 break;
1057 }
1058 }
1059 if !saw_digit {
1060 self.i = start;
1061 return None;
1062 }
1063 self.s[start..self.i].parse::<i64>().ok()
1064 }
1065
1066 fn parse_string(&mut self) -> Option<String> {
1067 self.skip_ws();
1068 let quote = self.peek()?;
1069 if quote != '\'' && quote != '"' {
1070 return None;
1071 }
1072 self.bump(quote);
1073 let mut out = String::new();
1074 while let Some(ch) = self.peek() {
1075 self.bump(ch);
1076 if ch == quote {
1077 return Some(out);
1078 }
1079 if ch == '\\' {
1080 let esc = self.peek()?;
1081 self.bump(esc);
1082 match esc {
1083 'n' => out.push('\n'),
1084 'r' => out.push('\r'),
1085 't' => out.push('\t'),
1086 '\\' => out.push('\\'),
1087 '\'' => out.push('\''),
1088 '"' => out.push('"'),
1089 other => out.push(other),
1090 }
1091 } else {
1092 out.push(ch);
1093 }
1094 }
1095 None
1096 }
1097
1098 fn parse_list(&mut self) -> Option<MetaValue> {
1099 if !self.eat('[') {
1100 return None;
1101 }
1102 let mut items = Vec::new();
1103 loop {
1104 self.skip_ws();
1105 if self.eat(']') {
1106 break;
1107 }
1108 let value = self.parse_value()?;
1109 items.push(value);
1110 self.skip_ws();
1111 if self.eat(']') {
1112 break;
1113 }
1114 if !self.eat(',') {
1115 return None;
1116 }
1117 }
1118 Some(MetaValue::List(items))
1119 }
1120
1121 fn parse_map(&mut self) -> Option<MetaValue> {
1122 if !self.eat('{') {
1123 return None;
1124 }
1125 let mut map: BTreeMap<String, MetaValue> = BTreeMap::new();
1126 loop {
1127 self.skip_ws();
1128 if self.eat('}') {
1129 break;
1130 }
1131 let key = match self.parse_value()? {
1132 MetaValue::Str(s) => s.to_string(),
1133 _ => return None,
1134 };
1135 if !self.eat(':') {
1136 return None;
1137 }
1138 let value = self.parse_value()?;
1139 map.insert(key, value);
1140 self.skip_ws();
1141 if self.eat('}') {
1142 break;
1143 }
1144 if !self.eat(',') {
1145 return None;
1146 }
1147 }
1148 Some(MetaValue::Map(map))
1149 }
1150
1151 fn parse_parens(&mut self) -> Option<MetaValue> {
1152 if !self.eat('(') {
1153 return None;
1154 }
1155 self.skip_ws();
1156 if self.eat(')') {
1157 return Some(MetaValue::Tuple(Vec::new()));
1158 }
1159
1160 let first = self.parse_value()?;
1161 self.skip_ws();
1162
1163 if self.eat(')') {
1165 return Some(first);
1166 }
1167 if !self.eat(',') {
1168 return None;
1169 }
1170
1171 let mut items = vec![first];
1172 loop {
1173 self.skip_ws();
1174 if self.eat(')') {
1175 break;
1176 }
1177 let value = self.parse_value()?;
1178 items.push(value);
1179 self.skip_ws();
1180 if self.eat(')') {
1181 break;
1182 }
1183 if !self.eat(',') {
1184 return None;
1185 }
1186 }
1187
1188 Some(MetaValue::Tuple(items))
1189 }
1190 }
1191
1192 let mut p = Parser::new(input);
1193 let value = p.parse_value()?;
1194 p.skip_ws();
1195 if !p.is_eof() {
1196 return None;
1197 }
1198 Some(value)
1199 }
1200}
1201
1202#[cfg(test)]
1203mod tests {
1204 use super::*;
1205
1206 #[test]
1207 fn test_style_builder() {
1208 let style = Style::new().with_bold(true).with_color(Color::Standard(1));
1209 assert_eq!(style.bold, Some(true));
1210 assert_eq!(style.color, Some(Color::Standard(1)));
1211 }
1212
1213 #[test]
1214 fn test_style_combine() {
1215 let base = Style::new().with_bold(true);
1216 let overlay = Style::new().with_italic(true);
1217 let combined = base.combine(&overlay);
1218 assert_eq!(combined.bold, Some(true));
1219 assert_eq!(combined.italic, Some(true));
1220 }
1221
1222 #[test]
1223 fn test_style_parse() {
1224 let style = Style::parse("bold red").unwrap();
1225 assert_eq!(style.bold, Some(true));
1226 assert_eq!(style.color, Some(Color::Standard(1)));
1227 }
1228
1229 #[test]
1230 fn test_style_parse_background() {
1231 let style = Style::parse("on blue").unwrap();
1232 assert_eq!(style.color, None);
1233 assert_eq!(style.bgcolor, Some(Color::Standard(4)));
1234
1235 let style = Style::parse("bold red on blue").unwrap();
1236 assert_eq!(style.bold, Some(true));
1237 assert_eq!(style.color, Some(Color::Standard(1)));
1238 assert_eq!(style.bgcolor, Some(Color::Standard(4)));
1239 }
1240
1241 #[test]
1244 fn test_null_style_is_default() {
1245 assert_eq!(NULL_STYLE, Style::default());
1246 assert!(NULL_STYLE.is_null());
1247 }
1248
1249 #[test]
1250 fn test_null_style_all_none() {
1251 assert_eq!(NULL_STYLE.color, None);
1252 assert_eq!(NULL_STYLE.bgcolor, None);
1253 assert_eq!(NULL_STYLE.bold, None);
1254 assert_eq!(NULL_STYLE.dim, None);
1255 assert_eq!(NULL_STYLE.italic, None);
1256 assert_eq!(NULL_STYLE.underline, None);
1257 assert_eq!(NULL_STYLE.blink, None);
1258 assert_eq!(NULL_STYLE.blink2, None);
1259 assert_eq!(NULL_STYLE.reverse, None);
1260 assert_eq!(NULL_STYLE.conceal, None);
1261 assert_eq!(NULL_STYLE.strike, None);
1262 assert_eq!(NULL_STYLE.underline2, None);
1263 assert_eq!(NULL_STYLE.frame, None);
1264 assert_eq!(NULL_STYLE.encircle, None);
1265 assert_eq!(NULL_STYLE.overline, None);
1266 }
1267
1268 #[test]
1269 fn test_is_null() {
1270 assert!(Style::new().is_null());
1271 assert!(!Style::new().with_bold(true).is_null());
1272 assert!(!Style::new().with_color(Color::Standard(1)).is_null());
1273 }
1274
1275 #[test]
1278 fn test_render_empty_text() {
1279 let style = Style::new().with_bold(true);
1280 assert_eq!(style.render("", ColorSystem::TrueColor), "");
1281 }
1282
1283 #[test]
1284 fn test_render_null_style() {
1285 let style = Style::new();
1286 assert_eq!(style.render("Hello", ColorSystem::TrueColor), "Hello");
1288 }
1289
1290 #[test]
1291 fn test_render_bold() {
1292 let style = Style::new().with_bold(true);
1293 let rendered = style.render("Hello", ColorSystem::TrueColor);
1294 assert_eq!(rendered, "\x1b[1mHello\x1b[0m");
1295 }
1296
1297 #[test]
1298 fn test_render_multiple_attributes() {
1299 let style = Style::new().with_bold(true).with_italic(true);
1300 let rendered = style.render("Hello", ColorSystem::TrueColor);
1301 assert_eq!(rendered, "\x1b[1;3mHello\x1b[0m");
1302 }
1303
1304 #[test]
1305 fn test_render_all_attributes() {
1306 let style = Style {
1307 bold: Some(true),
1308 dim: Some(true),
1309 italic: Some(true),
1310 underline: Some(true),
1311 blink: Some(true),
1312 reverse: Some(true),
1313 strike: Some(true),
1314 ..Default::default()
1315 };
1316 let rendered = style.render("X", ColorSystem::TrueColor);
1317 assert_eq!(rendered, "\x1b[1;2;3;4;5;7;9mX\x1b[0m");
1319 }
1320
1321 #[test]
1322 fn test_render_with_standard_color() {
1323 let style = Style::new().with_color(Color::Standard(1)); let rendered = style.render("Hi", ColorSystem::TrueColor);
1325 assert_eq!(rendered, "\x1b[31mHi\x1b[0m");
1327 }
1328
1329 #[test]
1330 fn test_render_with_bright_color() {
1331 let style = Style::new().with_color(Color::Standard(9)); let rendered = style.render("Hi", ColorSystem::TrueColor);
1333 assert_eq!(rendered, "\x1b[91mHi\x1b[0m");
1335 }
1336
1337 #[test]
1338 fn test_render_with_256_color() {
1339 let style = Style::new().with_color(Color::EightBit(196));
1340 let rendered = style.render("Hi", ColorSystem::TrueColor);
1341 assert_eq!(rendered, "\x1b[38;5;196mHi\x1b[0m");
1342 }
1343
1344 #[test]
1345 fn test_render_with_rgb_color() {
1346 let style = Style::new().with_color(Color::Rgb {
1347 r: 255,
1348 g: 128,
1349 b: 0,
1350 });
1351 let rendered = style.render("Hi", ColorSystem::TrueColor);
1352 assert_eq!(rendered, "\x1b[38;2;255;128;0mHi\x1b[0m");
1353 }
1354
1355 #[test]
1356 fn test_render_with_bgcolor() {
1357 let style = Style::new().with_bgcolor(Color::Standard(4)); let rendered = style.render("Hi", ColorSystem::TrueColor);
1359 assert_eq!(rendered, "\x1b[44mHi\x1b[0m");
1361 }
1362
1363 #[test]
1364 fn test_render_with_fg_and_bg() {
1365 let style = Style::new()
1366 .with_color(Color::Standard(1)) .with_bgcolor(Color::Standard(7)); let rendered = style.render("Hi", ColorSystem::TrueColor);
1369 assert_eq!(rendered, "\x1b[31;47mHi\x1b[0m");
1370 }
1371
1372 #[test]
1373 fn test_render_bold_and_color() {
1374 let style = Style::new().with_bold(true).with_color(Color::Standard(2)); let rendered = style.render("OK", ColorSystem::TrueColor);
1376 assert_eq!(rendered, "\x1b[1;32mOK\x1b[0m");
1377 }
1378
1379 #[test]
1380 fn test_render_color_downgrade_to_256() {
1381 let style = Style::new().with_color(Color::Rgb { r: 255, g: 0, b: 0 });
1383 let rendered = style.render("X", ColorSystem::EightBit);
1384 assert!(rendered.contains("38;5;"));
1386 assert!(!rendered.contains("38;2;"));
1387 }
1388
1389 #[test]
1390 fn test_render_color_downgrade_to_standard() {
1391 let style = Style::new().with_color(Color::Rgb { r: 255, g: 0, b: 0 });
1393 let rendered = style.render("X", ColorSystem::Standard);
1394 assert!(!rendered.contains("38;5;"));
1396 assert!(!rendered.contains("38;2;"));
1397 }
1398
1399 #[test]
1402 fn test_html_style_empty() {
1403 let style = Style::new();
1404 assert_eq!(style.get_html_style(), "");
1405 }
1406
1407 #[test]
1408 fn test_html_style_bold() {
1409 let style = Style::new().with_bold(true);
1410 assert_eq!(style.get_html_style(), "font-weight: bold");
1411 }
1412
1413 #[test]
1414 fn test_html_style_italic() {
1415 let style = Style::new().with_italic(true);
1416 assert_eq!(style.get_html_style(), "font-style: italic");
1417 }
1418
1419 #[test]
1420 fn test_html_style_underline() {
1421 let style = Style::new().with_underline(true);
1422 assert_eq!(style.get_html_style(), "text-decoration: underline");
1423 }
1424
1425 #[test]
1426 fn test_html_style_strike() {
1427 let style = Style::new().with_strike(true);
1428 assert_eq!(style.get_html_style(), "text-decoration: line-through");
1429 }
1430
1431 #[test]
1432 fn test_with_reverse_builder_sets_flag() {
1433 let style = Style::new().with_reverse(true);
1434 assert_eq!(style.reverse, Some(true));
1435 }
1436
1437 #[test]
1438 fn test_html_style_color_rgb() {
1439 let style = Style::new().with_color(Color::Rgb { r: 255, g: 0, b: 0 });
1440 let css = style.get_html_style();
1441 assert!(css.contains("color: #ff0000"));
1442 assert!(css.contains("text-decoration-color: #ff0000"));
1443 }
1444
1445 #[test]
1446 fn test_html_style_bgcolor() {
1447 let style = Style::new().with_bgcolor(Color::Rgb { r: 0, g: 0, b: 255 });
1448 let css = style.get_html_style();
1449 assert!(css.contains("background-color: #0000ff"));
1450 }
1451
1452 #[test]
1453 fn test_html_style_reverse_swaps_colors() {
1454 let style = Style {
1455 color: Some(Color::Rgb { r: 255, g: 0, b: 0 }),
1456 bgcolor: Some(Color::Rgb { r: 0, g: 0, b: 255 }),
1457 reverse: Some(true),
1458 ..Default::default()
1459 };
1460 let css = style.get_html_style();
1461 assert!(css.contains("color: #0000ff"));
1463 assert!(css.contains("background-color: #ff0000"));
1464 }
1465
1466 #[test]
1467 fn test_html_style_combined() {
1468 let style = Style::new()
1469 .with_bold(true)
1470 .with_italic(true)
1471 .with_color(Color::Rgb {
1472 r: 255,
1473 g: 128,
1474 b: 0,
1475 });
1476 let css = style.get_html_style();
1477 assert!(css.contains("font-weight: bold"));
1478 assert!(css.contains("font-style: italic"));
1479 assert!(css.contains("color: #ff8000"));
1480 }
1481
1482 #[test]
1483 fn test_html_style_standard_color() {
1484 let style = Style::new().with_color(Color::Standard(1));
1486 let css = style.get_html_style();
1487 assert!(css.contains("color: #"));
1489 }
1490
1491 #[test]
1492 fn test_html_style_underline_and_strike_combined() {
1493 let style = Style::new().with_underline(true).with_strike(true);
1495 let css = style.get_html_style();
1496 assert!(css.contains("text-decoration: underline line-through"));
1498 assert_eq!(css.matches("text-decoration").count(), 1);
1500 }
1501
1502 #[test]
1505 fn test_make_ansi_codes_empty() {
1506 let style = Style::new();
1507 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "");
1508 }
1509
1510 #[test]
1511 fn test_make_ansi_codes_attributes_only() {
1512 let style = Style::new().with_bold(true).with_dim(true);
1513 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "1;2");
1514 }
1515
1516 #[test]
1517 fn test_make_ansi_codes_false_attributes_emit_reset() {
1518 let style = Style {
1520 bold: Some(false),
1521 italic: Some(true),
1522 ..Default::default()
1523 };
1524 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "22;3");
1526 }
1527
1528 #[test]
1531 fn test_parse_not_bold() {
1532 let style = Style::parse("not bold").unwrap();
1534 assert_eq!(style.bold, Some(false));
1535 }
1536
1537 #[test]
1538 fn test_parse_not_italic() {
1539 let style = Style::parse("not italic").unwrap();
1540 assert_eq!(style.italic, Some(false));
1541 }
1542
1543 #[test]
1544 fn test_parse_not_underline() {
1545 let style = Style::parse("not underline").unwrap();
1546 assert_eq!(style.underline, Some(false));
1547 }
1548
1549 #[test]
1550 fn test_parse_not_dim() {
1551 let style = Style::parse("not dim").unwrap();
1552 assert_eq!(style.dim, Some(false));
1553 }
1554
1555 #[test]
1556 fn test_parse_not_blink() {
1557 let style = Style::parse("not blink").unwrap();
1558 assert_eq!(style.blink, Some(false));
1559 }
1560
1561 #[test]
1562 fn test_parse_not_reverse() {
1563 let style = Style::parse("not reverse").unwrap();
1564 assert_eq!(style.reverse, Some(false));
1565 }
1566
1567 #[test]
1568 fn test_parse_not_strike() {
1569 let style = Style::parse("not strike").unwrap();
1570 assert_eq!(style.strike, Some(false));
1571 }
1572
1573 #[test]
1574 fn test_parse_mixed_attributes_with_negation() {
1575 let style = Style::parse("bold not italic red").unwrap();
1577 assert_eq!(style.bold, Some(true));
1578 assert_eq!(style.italic, Some(false));
1579 assert_eq!(style.color, Some(Color::Standard(1)));
1580 }
1581
1582 #[test]
1583 fn test_make_ansi_codes_bold_false_emits_22() {
1584 let style = Style {
1586 bold: Some(false),
1587 ..Default::default()
1588 };
1589 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "22");
1590 }
1591
1592 #[test]
1593 fn test_make_ansi_codes_italic_false_emits_23() {
1594 let style = Style {
1595 italic: Some(false),
1596 ..Default::default()
1597 };
1598 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "23");
1599 }
1600
1601 #[test]
1602 fn test_make_ansi_codes_underline_false_emits_24() {
1603 let style = Style {
1604 underline: Some(false),
1605 ..Default::default()
1606 };
1607 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "24");
1608 }
1609
1610 #[test]
1611 fn test_make_ansi_codes_blink_false_emits_25() {
1612 let style = Style {
1613 blink: Some(false),
1614 ..Default::default()
1615 };
1616 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "25");
1617 }
1618
1619 #[test]
1620 fn test_make_ansi_codes_reverse_false_emits_27() {
1621 let style = Style {
1622 reverse: Some(false),
1623 ..Default::default()
1624 };
1625 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "27");
1626 }
1627
1628 #[test]
1629 fn test_make_ansi_codes_strike_false_emits_29() {
1630 let style = Style {
1631 strike: Some(false),
1632 ..Default::default()
1633 };
1634 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "29");
1635 }
1636
1637 #[test]
1638 fn test_make_ansi_codes_dim_false_emits_22() {
1639 let style = Style {
1641 dim: Some(false),
1642 ..Default::default()
1643 };
1644 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "22");
1645 }
1646
1647 #[test]
1648 fn test_make_ansi_codes_bold_false_dim_true() {
1649 let style = Style {
1651 bold: Some(false),
1652 dim: Some(true),
1653 ..Default::default()
1654 };
1655 assert_eq!(style.make_ansi_codes(ColorSystem::TrueColor), "22;2");
1656 }
1657
1658 #[test]
1659 fn test_render_with_false_attribute() {
1660 let style = Style {
1661 bold: Some(false),
1662 ..Default::default()
1663 };
1664 let rendered = style.render("Hi", ColorSystem::TrueColor);
1665 assert_eq!(rendered, "\x1b[22mHi\x1b[0m");
1666 }
1667
1668 #[test]
1671 fn test_parse_shorthand_b_for_bold() {
1672 let style = Style::parse("b").unwrap();
1673 assert_eq!(style.bold, Some(true));
1674 }
1675
1676 #[test]
1677 fn test_parse_shorthand_i_for_italic() {
1678 let style = Style::parse("i").unwrap();
1679 assert_eq!(style.italic, Some(true));
1680 }
1681
1682 #[test]
1683 fn test_parse_shorthand_u_for_underline() {
1684 let style = Style::parse("u").unwrap();
1685 assert_eq!(style.underline, Some(true));
1686 }
1687
1688 #[test]
1689 fn test_parse_shorthand_s_for_strike() {
1690 let style = Style::parse("s").unwrap();
1691 assert_eq!(style.strike, Some(true));
1692 }
1693
1694 #[test]
1695 fn test_parse_shorthand_combined() {
1696 let style = Style::parse("b i red").unwrap();
1697 assert_eq!(style.bold, Some(true));
1698 assert_eq!(style.italic, Some(true));
1699 assert_eq!(style.color, Some(Color::Standard(1)));
1700 }
1701
1702 #[test]
1703 fn test_parse_shorthand_negation() {
1704 let style = Style::parse("not b").unwrap();
1705 assert_eq!(style.bold, Some(false));
1706
1707 let style = Style::parse("not i").unwrap();
1708 assert_eq!(style.italic, Some(false));
1709 }
1710
1711 #[test]
1714 fn test_parse_new_attributes() {
1715 let style = Style::parse("overline").unwrap();
1716 assert_eq!(style.overline, Some(true));
1717
1718 let style = Style::parse("blink2").unwrap();
1719 assert_eq!(style.blink2, Some(true));
1720
1721 let style = Style::parse("conceal").unwrap();
1722 assert_eq!(style.conceal, Some(true));
1723
1724 let style = Style::parse("underline2").unwrap();
1725 assert_eq!(style.underline2, Some(true));
1726
1727 let style = Style::parse("frame").unwrap();
1728 assert_eq!(style.frame, Some(true));
1729
1730 let style = Style::parse("encircle").unwrap();
1731 assert_eq!(style.encircle, Some(true));
1732 }
1733
1734 #[test]
1735 fn test_parse_new_attribute_shorthand() {
1736 let style = Style::parse("o").unwrap();
1737 assert_eq!(style.overline, Some(true));
1738
1739 let style = Style::parse("uu").unwrap();
1740 assert_eq!(style.underline2, Some(true));
1741
1742 let style = Style::parse("c").unwrap();
1743 assert_eq!(style.conceal, Some(true));
1744 }
1745
1746 #[test]
1747 fn test_parse_not_new_attributes() {
1748 let style = Style::parse("not overline").unwrap();
1749 assert_eq!(style.overline, Some(false));
1750
1751 let style = Style::parse("not blink2").unwrap();
1752 assert_eq!(style.blink2, Some(false));
1753
1754 let style = Style::parse("not conceal").unwrap();
1755 assert_eq!(style.conceal, Some(false));
1756
1757 let style = Style::parse("not underline2").unwrap();
1758 assert_eq!(style.underline2, Some(false));
1759
1760 let style = Style::parse("not frame").unwrap();
1761 assert_eq!(style.frame, Some(false));
1762
1763 let style = Style::parse("not encircle").unwrap();
1764 assert_eq!(style.encircle, Some(false));
1765 }
1766
1767 #[test]
1768 fn test_builder_new_attributes() {
1769 let style = Style::new().with_overline(true);
1770 assert_eq!(style.overline, Some(true));
1771
1772 let style = Style::new().with_blink2(true);
1773 assert_eq!(style.blink2, Some(true));
1774
1775 let style = Style::new().with_conceal(true);
1776 assert_eq!(style.conceal, Some(true));
1777
1778 let style = Style::new().with_underline2(true);
1779 assert_eq!(style.underline2, Some(true));
1780
1781 let style = Style::new().with_frame(true);
1782 assert_eq!(style.frame, Some(true));
1783
1784 let style = Style::new().with_encircle(true);
1785 assert_eq!(style.encircle, Some(true));
1786 }
1787
1788 #[test]
1789 fn test_render_new_attributes() {
1790 let style = Style::new().with_overline(true);
1792 assert_eq!(
1793 style.render("X", ColorSystem::TrueColor),
1794 "\x1b[53mX\x1b[0m"
1795 );
1796
1797 let style = Style::new().with_blink2(true);
1799 assert_eq!(style.render("X", ColorSystem::TrueColor), "\x1b[6mX\x1b[0m");
1800
1801 let style = Style::new().with_conceal(true);
1803 assert_eq!(style.render("X", ColorSystem::TrueColor), "\x1b[8mX\x1b[0m");
1804
1805 let style = Style::new().with_underline2(true);
1807 assert_eq!(
1808 style.render("X", ColorSystem::TrueColor),
1809 "\x1b[21mX\x1b[0m"
1810 );
1811
1812 let style = Style::new().with_frame(true);
1814 assert_eq!(
1815 style.render("X", ColorSystem::TrueColor),
1816 "\x1b[51mX\x1b[0m"
1817 );
1818
1819 let style = Style::new().with_encircle(true);
1821 assert_eq!(
1822 style.render("X", ColorSystem::TrueColor),
1823 "\x1b[52mX\x1b[0m"
1824 );
1825 }
1826
1827 #[test]
1828 fn test_render_new_attributes_off() {
1829 let style = Style::new().with_overline(false);
1831 assert_eq!(
1832 style.render("X", ColorSystem::TrueColor),
1833 "\x1b[55mX\x1b[0m"
1834 );
1835
1836 let style = Style::new().with_conceal(false);
1838 assert_eq!(
1839 style.render("X", ColorSystem::TrueColor),
1840 "\x1b[28mX\x1b[0m"
1841 );
1842
1843 let style = Style::new().with_frame(false);
1845 assert_eq!(
1846 style.render("X", ColorSystem::TrueColor),
1847 "\x1b[54mX\x1b[0m"
1848 );
1849 }
1850
1851 #[test]
1852 fn test_combine_new_attributes() {
1853 let a = Style::new().with_overline(true);
1854 let b = Style::new().with_blink2(true);
1855 let combined = a.combine(&b);
1856 assert_eq!(combined.overline, Some(true));
1857 assert_eq!(combined.blink2, Some(true));
1858 }
1859
1860 #[test]
1861 fn test_render_all_new_attributes() {
1862 let style = Style {
1863 blink2: Some(true),
1864 conceal: Some(true),
1865 underline2: Some(true),
1866 frame: Some(true),
1867 encircle: Some(true),
1868 overline: Some(true),
1869 ..Default::default()
1870 };
1871 let rendered = style.render("X", ColorSystem::TrueColor);
1872 assert_eq!(rendered, "\x1b[6;8;21;51;52;53mX\x1b[0m");
1874 }
1875
1876 #[test]
1879 fn test_chain() {
1880 let a = Style::new().with_bold(true);
1881 let b = Style::new()
1882 .with_italic(true)
1883 .with_color(Color::Standard(1));
1884 let c = Style::new().with_underline(true);
1885 let result = Style::chain(&[a, b, c]);
1886 assert_eq!(result.bold, Some(true));
1887 assert_eq!(result.italic, Some(true));
1888 assert_eq!(result.underline, Some(true));
1889 assert_eq!(result.color, Some(Color::Standard(1)));
1890 }
1891
1892 #[test]
1893 fn test_from_color() {
1894 let style = Style::from_color(Some(Color::Standard(1)), Some(Color::Standard(4)));
1895 assert_eq!(style.color, Some(Color::Standard(1)));
1896 assert_eq!(style.bgcolor, Some(Color::Standard(4)));
1897 assert_eq!(style.bold, None);
1898 assert_eq!(style.italic, None);
1899 }
1900
1901 #[test]
1902 fn test_on_builds_meta_with_handlers() {
1903 let meta = Style::on(
1904 None,
1905 [
1906 ("click", MetaValue::str("handler")),
1907 ("focus", MetaValue::Bool(true)),
1908 ],
1909 );
1910
1911 assert!(meta.link.is_none());
1912 assert!(meta.link_id.is_none());
1913 let map = meta.meta.as_ref().expect("meta should be present");
1914 assert_eq!(map.get("@click"), Some(&MetaValue::str("handler")));
1915 assert_eq!(map.get("@focus"), Some(&MetaValue::Bool(true)));
1916 }
1917
1918 #[test]
1919 fn test_on_merges_existing_meta() {
1920 let mut base = BTreeMap::new();
1921 base.insert("existing".to_string(), MetaValue::Int(1));
1922
1923 let meta = Style::on(Some(base), [("blur", MetaValue::Bool(false))]);
1924 let map = meta.meta.as_ref().expect("meta should be present");
1925 assert_eq!(map.get("existing"), Some(&MetaValue::Int(1)));
1926 assert_eq!(map.get("@blur"), Some(&MetaValue::Bool(false)));
1927 }
1928
1929 #[test]
1930 fn test_normalize_valid_style() {
1931 assert_eq!(Style::normalize(" BOLD red ON blue "), "bold red on blue");
1932 assert_eq!(Style::normalize(""), "none");
1933 assert_eq!(Style::normalize(" none "), "none");
1934 }
1935
1936 #[test]
1937 fn test_normalize_invalid_style_returns_trimmed_lower() {
1938 assert_eq!(Style::normalize(" no_such_token "), "no_such_token");
1939 assert_eq!(Style::normalize("foo bold"), "foo bold");
1940 assert_eq!(
1941 Style::normalize("bold on not_a_color"),
1942 "bold on not_a_color"
1943 );
1944 }
1945
1946 #[test]
1947 fn test_pick_first_returns_first_non_none() {
1948 let first = Style::new().with_bold(true);
1949 let second = Style::new().with_italic(true);
1950 let picked = Style::pick_first(&[None, Some(first), Some(second)]);
1951 assert_eq!(picked, first);
1952 }
1953
1954 #[test]
1955 #[should_panic(expected = "expected at least one non-None style")]
1956 fn test_pick_first_panics_when_all_none() {
1957 let _ = Style::pick_first(&[None, None]);
1958 }
1959
1960 #[test]
1961 fn test_background_style() {
1962 let style = Style::new()
1963 .with_bold(true)
1964 .with_color(Color::Standard(1))
1965 .with_bgcolor(Color::Standard(4));
1966 let bg = style.background_style();
1967 assert_eq!(bg.bgcolor, Some(Color::Standard(4)));
1968 assert_eq!(bg.color, None);
1969 assert_eq!(bg.bold, None);
1970 }
1971
1972 #[test]
1973 fn test_without_color() {
1974 let style = Style::new()
1975 .with_bold(true)
1976 .with_color(Color::Standard(1))
1977 .with_bgcolor(Color::Standard(4));
1978 let nc = style.without_color();
1979 assert_eq!(nc.color, None);
1980 assert_eq!(nc.bgcolor, None);
1981 assert_eq!(nc.bold, Some(true));
1982 }
1983
1984 #[test]
1985 fn test_has_transparent_background() {
1986 assert!(Style::new().has_transparent_background());
1987 assert!(
1988 Style::new()
1989 .with_bgcolor(Color::Default)
1990 .has_transparent_background()
1991 );
1992 assert!(
1993 !Style::new()
1994 .with_bgcolor(Color::Standard(1))
1995 .has_transparent_background()
1996 );
1997 }
1998
1999 #[test]
2002 fn test_style_stack_new() {
2003 let stack = StyleStack::new(Style::new().with_bold(true));
2004 assert_eq!(stack.current().bold, Some(true));
2005 }
2006
2007 #[test]
2008 fn test_style_stack_push_pop() {
2009 let mut stack = StyleStack::new(Style::new().with_bold(true));
2010 stack.push(Style::new().with_italic(true));
2011 let current = stack.current();
2012 assert_eq!(current.bold, Some(true));
2013 assert_eq!(current.italic, Some(true));
2014
2015 stack.pop();
2016 let current = stack.current();
2017 assert_eq!(current.bold, Some(true));
2018 assert_eq!(current.italic, None);
2019 }
2020
2021 #[test]
2022 fn test_style_stack_multiple_push() {
2023 let mut stack = StyleStack::new(Style::new());
2024 stack.push(Style::new().with_bold(true));
2025 stack.push(Style::new().with_italic(true));
2026 stack.push(Style::new().with_underline(true));
2027
2028 let current = stack.current();
2029 assert_eq!(current.bold, Some(true));
2030 assert_eq!(current.italic, Some(true));
2031 assert_eq!(current.underline, Some(true));
2032
2033 stack.pop();
2034 let current = stack.current();
2035 assert_eq!(current.bold, Some(true));
2036 assert_eq!(current.italic, Some(true));
2037 assert_eq!(current.underline, None);
2038 }
2039
2040 #[test]
2043 fn test_html_style_overline() {
2044 let style = Style::new().with_overline(true);
2045 let css = style.get_html_style();
2046 assert!(css.contains("text-decoration: overline"));
2047 }
2048
2049 #[test]
2050 fn test_html_style_underline_strike_overline_combined() {
2051 let style = Style::new()
2052 .with_underline(true)
2053 .with_strike(true)
2054 .with_overline(true);
2055 let css = style.get_html_style();
2056 assert!(css.contains("text-decoration: underline line-through overline"));
2057 assert_eq!(css.matches("text-decoration").count(), 1);
2058 }
2059}