1use console::Style;
24
25use super::color::ColorDef;
26use super::error::StylesheetError;
27
28#[derive(Debug, Clone, Default, PartialEq, Eq)]
33pub struct StyleAttributes {
34 pub fg: Option<ColorDef>,
36 pub bg: Option<ColorDef>,
38 pub bold: Option<bool>,
40 pub dim: Option<bool>,
42 pub italic: Option<bool>,
44 pub underline: Option<bool>,
46 pub blink: Option<bool>,
48 pub reverse: Option<bool>,
50 pub hidden: Option<bool>,
52 pub strikethrough: Option<bool>,
54}
55
56impl StyleAttributes {
57 pub fn new() -> Self {
59 Self::default()
60 }
61
62 pub fn parse_mapping(
66 map: &serde_yaml::Mapping,
67 style_name: &str,
68 ) -> Result<Self, StylesheetError> {
69 let mut attrs = StyleAttributes::new();
70
71 for (key, value) in map {
72 let key_str = key
73 .as_str()
74 .ok_or_else(|| StylesheetError::InvalidDefinition {
75 style: style_name.to_string(),
76 message: format!("Non-string key in style definition: {:?}", key),
77 path: None,
78 })?;
79
80 if key_str == "light" || key_str == "dark" {
82 continue;
83 }
84
85 attrs.set_attribute(key_str, value, style_name)?;
86 }
87
88 Ok(attrs)
89 }
90
91 fn set_attribute(
93 &mut self,
94 name: &str,
95 value: &serde_yaml::Value,
96 style_name: &str,
97 ) -> Result<(), StylesheetError> {
98 match name {
99 "fg" => {
100 self.fg = Some(ColorDef::parse_value(value).map_err(|e| {
101 StylesheetError::InvalidColor {
102 style: style_name.to_string(),
103 value: e,
104 path: None,
105 }
106 })?);
107 }
108 "bg" => {
109 self.bg = Some(ColorDef::parse_value(value).map_err(|e| {
110 StylesheetError::InvalidColor {
111 style: style_name.to_string(),
112 value: e,
113 path: None,
114 }
115 })?);
116 }
117 "bold" => {
118 self.bold = Some(parse_bool(value, name, style_name)?);
119 }
120 "dim" => {
121 self.dim = Some(parse_bool(value, name, style_name)?);
122 }
123 "italic" => {
124 self.italic = Some(parse_bool(value, name, style_name)?);
125 }
126 "underline" => {
127 self.underline = Some(parse_bool(value, name, style_name)?);
128 }
129 "blink" => {
130 self.blink = Some(parse_bool(value, name, style_name)?);
131 }
132 "reverse" => {
133 self.reverse = Some(parse_bool(value, name, style_name)?);
134 }
135 "hidden" => {
136 self.hidden = Some(parse_bool(value, name, style_name)?);
137 }
138 "strikethrough" => {
139 self.strikethrough = Some(parse_bool(value, name, style_name)?);
140 }
141 _ => {
142 return Err(StylesheetError::UnknownAttribute {
143 style: style_name.to_string(),
144 attribute: name.to_string(),
145 path: None,
146 });
147 }
148 }
149
150 Ok(())
151 }
152
153 pub fn merge(&self, other: &StyleAttributes) -> StyleAttributes {
160 StyleAttributes {
161 fg: other.fg.clone().or_else(|| self.fg.clone()),
162 bg: other.bg.clone().or_else(|| self.bg.clone()),
163 bold: other.bold.or(self.bold),
164 dim: other.dim.or(self.dim),
165 italic: other.italic.or(self.italic),
166 underline: other.underline.or(self.underline),
167 blink: other.blink.or(self.blink),
168 reverse: other.reverse.or(self.reverse),
169 hidden: other.hidden.or(self.hidden),
170 strikethrough: other.strikethrough.or(self.strikethrough),
171 }
172 }
173
174 pub fn is_empty(&self) -> bool {
176 self.fg.is_none()
177 && self.bg.is_none()
178 && self.bold.is_none()
179 && self.dim.is_none()
180 && self.italic.is_none()
181 && self.underline.is_none()
182 && self.blink.is_none()
183 && self.reverse.is_none()
184 && self.hidden.is_none()
185 && self.strikethrough.is_none()
186 }
187
188 pub fn to_style(&self) -> Style {
190 let mut style = Style::new();
191
192 if let Some(ref fg) = self.fg {
193 style = style.fg(fg.to_console_color());
194 }
195 if let Some(ref bg) = self.bg {
196 style = style.bg(bg.to_console_color());
197 }
198 if self.bold == Some(true) {
199 style = style.bold();
200 }
201 if self.dim == Some(true) {
202 style = style.dim();
203 }
204 if self.italic == Some(true) {
205 style = style.italic();
206 }
207 if self.underline == Some(true) {
208 style = style.underlined();
209 }
210 if self.blink == Some(true) {
211 style = style.blink();
212 }
213 if self.reverse == Some(true) {
214 style = style.reverse();
215 }
216 if self.hidden == Some(true) {
217 style = style.hidden();
218 }
219 if self.strikethrough == Some(true) {
220 style = style.strikethrough();
221 }
222
223 style
224 }
225}
226
227fn parse_bool(
229 value: &serde_yaml::Value,
230 attr: &str,
231 style_name: &str,
232) -> Result<bool, StylesheetError> {
233 value
234 .as_bool()
235 .ok_or_else(|| StylesheetError::InvalidDefinition {
236 style: style_name.to_string(),
237 message: format!("'{}' must be a boolean, got {:?}", attr, value),
238 path: None,
239 })
240}
241
242pub fn parse_shorthand(s: &str, style_name: &str) -> Result<StyleAttributes, StylesheetError> {
252 let mut attrs = StyleAttributes::new();
253
254 let parts: Vec<&str> = s
256 .split(|c: char| c == ',' || c.is_whitespace())
257 .filter(|s| !s.is_empty())
258 .collect();
259
260 for part in parts {
261 match part.to_lowercase().as_str() {
262 "bold" => attrs.bold = Some(true),
263 "dim" => attrs.dim = Some(true),
264 "italic" => attrs.italic = Some(true),
265 "underline" => attrs.underline = Some(true),
266 "blink" => attrs.blink = Some(true),
267 "reverse" => attrs.reverse = Some(true),
268 "hidden" => attrs.hidden = Some(true),
269 "strikethrough" => attrs.strikethrough = Some(true),
270 _ => {
272 if attrs.fg.is_some() {
273 return Err(StylesheetError::InvalidShorthand {
274 style: style_name.to_string(),
275 value: format!(
276 "Multiple colors in shorthand: already have fg, got '{}'",
277 part
278 ),
279 path: None,
280 });
281 }
282 attrs.fg = Some(ColorDef::parse_string(part).map_err(|e| {
283 StylesheetError::InvalidShorthand {
284 style: style_name.to_string(),
285 value: e,
286 path: None,
287 }
288 })?);
289 }
290 }
291 }
292
293 if attrs.is_empty() {
294 return Err(StylesheetError::InvalidShorthand {
295 style: style_name.to_string(),
296 value: format!("Empty or invalid shorthand: '{}'", s),
297 path: None,
298 });
299 }
300
301 Ok(attrs)
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use console::Color;
308 use serde_yaml::{Mapping, Value};
309
310 #[test]
315 fn test_parse_mapping_fg_only() {
316 let mut map = Mapping::new();
317 map.insert(Value::String("fg".into()), Value::String("red".into()));
318
319 let attrs = StyleAttributes::parse_mapping(&map, "test").unwrap();
320 assert_eq!(attrs.fg, Some(ColorDef::Named(Color::Red)));
321 assert!(attrs.bg.is_none());
322 assert!(attrs.bold.is_none());
323 }
324
325 #[test]
326 fn test_parse_mapping_full() {
327 let mut map = Mapping::new();
328 map.insert(Value::String("fg".into()), Value::String("cyan".into()));
329 map.insert(Value::String("bg".into()), Value::String("black".into()));
330 map.insert(Value::String("bold".into()), Value::Bool(true));
331 map.insert(Value::String("dim".into()), Value::Bool(false));
332 map.insert(Value::String("italic".into()), Value::Bool(true));
333
334 let attrs = StyleAttributes::parse_mapping(&map, "test").unwrap();
335 assert_eq!(attrs.fg, Some(ColorDef::Named(Color::Cyan)));
336 assert_eq!(attrs.bg, Some(ColorDef::Named(Color::Black)));
337 assert_eq!(attrs.bold, Some(true));
338 assert_eq!(attrs.dim, Some(false));
339 assert_eq!(attrs.italic, Some(true));
340 }
341
342 #[test]
343 fn test_parse_mapping_ignores_light_dark() {
344 let mut map = Mapping::new();
345 map.insert(Value::String("fg".into()), Value::String("red".into()));
346 map.insert(
347 Value::String("light".into()),
348 Value::Mapping(Mapping::new()),
349 );
350 map.insert(Value::String("dark".into()), Value::Mapping(Mapping::new()));
351
352 let attrs = StyleAttributes::parse_mapping(&map, "test").unwrap();
353 assert_eq!(attrs.fg, Some(ColorDef::Named(Color::Red)));
354 }
356
357 #[test]
358 fn test_parse_mapping_unknown_attribute() {
359 let mut map = Mapping::new();
360 map.insert(
361 Value::String("unknown".into()),
362 Value::String("value".into()),
363 );
364
365 let result = StyleAttributes::parse_mapping(&map, "test");
366 assert!(matches!(
367 result,
368 Err(StylesheetError::UnknownAttribute { attribute, .. }) if attribute == "unknown"
369 ));
370 }
371
372 #[test]
373 fn test_parse_mapping_hex_color() {
374 let mut map = Mapping::new();
375 map.insert(Value::String("fg".into()), Value::String("#ff6b35".into()));
376
377 let attrs = StyleAttributes::parse_mapping(&map, "test").unwrap();
378 assert_eq!(attrs.fg, Some(ColorDef::Rgb(255, 107, 53)));
379 }
380
381 #[test]
386 fn test_merge_empty_onto_full() {
387 let base = StyleAttributes {
388 fg: Some(ColorDef::Named(Color::Red)),
389 bold: Some(true),
390 ..Default::default()
391 };
392 let empty = StyleAttributes::new();
393
394 let merged = base.merge(&empty);
395 assert_eq!(merged.fg, Some(ColorDef::Named(Color::Red)));
396 assert_eq!(merged.bold, Some(true));
397 }
398
399 #[test]
400 fn test_merge_full_onto_empty() {
401 let empty = StyleAttributes::new();
402 let full = StyleAttributes {
403 fg: Some(ColorDef::Named(Color::Blue)),
404 italic: Some(true),
405 ..Default::default()
406 };
407
408 let merged = empty.merge(&full);
409 assert_eq!(merged.fg, Some(ColorDef::Named(Color::Blue)));
410 assert_eq!(merged.italic, Some(true));
411 }
412
413 #[test]
414 fn test_merge_override() {
415 let base = StyleAttributes {
416 fg: Some(ColorDef::Named(Color::Red)),
417 bold: Some(true),
418 ..Default::default()
419 };
420 let override_attrs = StyleAttributes {
421 fg: Some(ColorDef::Named(Color::Blue)),
422 ..Default::default()
423 };
424
425 let merged = base.merge(&override_attrs);
426 assert_eq!(merged.fg, Some(ColorDef::Named(Color::Blue)));
428 assert_eq!(merged.bold, Some(true));
430 }
431
432 #[test]
433 fn test_merge_preserves_unset() {
434 let base = StyleAttributes {
435 fg: Some(ColorDef::Named(Color::Red)),
436 bg: Some(ColorDef::Named(Color::White)),
437 bold: Some(true),
438 dim: Some(true),
439 ..Default::default()
440 };
441 let override_attrs = StyleAttributes {
442 fg: Some(ColorDef::Named(Color::Blue)),
443 bold: Some(false),
444 ..Default::default()
445 };
446
447 let merged = base.merge(&override_attrs);
448 assert_eq!(merged.fg, Some(ColorDef::Named(Color::Blue))); assert_eq!(merged.bg, Some(ColorDef::Named(Color::White))); assert_eq!(merged.bold, Some(false)); assert_eq!(merged.dim, Some(true)); }
453
454 #[test]
459 fn test_to_style_empty() {
460 let attrs = StyleAttributes::new();
461 let style = attrs.to_style();
462 let _ = style.apply_to("test");
464 }
465
466 #[test]
467 fn test_to_style_with_attributes() {
468 let attrs = StyleAttributes {
469 fg: Some(ColorDef::Named(Color::Red)),
470 bold: Some(true),
471 italic: Some(true),
472 ..Default::default()
473 };
474 let style = attrs.to_style().force_styling(true);
475 let output = style.apply_to("test").to_string();
476 assert!(output.contains("\x1b["));
478 assert!(output.contains("test"));
479 }
480
481 #[test]
486 fn test_parse_shorthand_single_attribute() {
487 let attrs = parse_shorthand("bold", "test").unwrap();
488 assert_eq!(attrs.bold, Some(true));
489 assert!(attrs.fg.is_none());
490 }
491
492 #[test]
493 fn test_parse_shorthand_single_color() {
494 let attrs = parse_shorthand("cyan", "test").unwrap();
495 assert_eq!(attrs.fg, Some(ColorDef::Named(Color::Cyan)));
496 assert!(attrs.bold.is_none());
497 }
498
499 #[test]
500 fn test_parse_shorthand_color_and_attribute() {
501 let attrs = parse_shorthand("cyan bold", "test").unwrap();
502 assert_eq!(attrs.fg, Some(ColorDef::Named(Color::Cyan)));
503 assert_eq!(attrs.bold, Some(true));
504 }
505
506 #[test]
507 fn test_parse_shorthand_multiple_attributes() {
508 let attrs = parse_shorthand("bold italic underline", "test").unwrap();
509 assert_eq!(attrs.bold, Some(true));
510 assert_eq!(attrs.italic, Some(true));
511 assert_eq!(attrs.underline, Some(true));
512 assert!(attrs.fg.is_none());
513 }
514
515 #[test]
516 fn test_parse_shorthand_color_with_multiple_attributes() {
517 let attrs = parse_shorthand("yellow bold italic", "test").unwrap();
518 assert_eq!(attrs.fg, Some(ColorDef::Named(Color::Yellow)));
519 assert_eq!(attrs.bold, Some(true));
520 assert_eq!(attrs.italic, Some(true));
521 }
522
523 #[test]
524 fn test_parse_shorthand_multiple_colors_error() {
525 let result = parse_shorthand("red blue", "test");
526 assert!(matches!(
527 result,
528 Err(StylesheetError::InvalidShorthand { .. })
529 ));
530 }
531
532 #[test]
533 fn test_parse_shorthand_empty_error() {
534 let result = parse_shorthand("", "test");
535 assert!(matches!(
536 result,
537 Err(StylesheetError::InvalidShorthand { .. })
538 ));
539 }
540
541 #[test]
542 fn test_parse_shorthand_invalid_token_error() {
543 let result = parse_shorthand("boldx", "test");
544 assert!(matches!(
545 result,
546 Err(StylesheetError::InvalidShorthand { .. })
547 ));
548 }
549
550 #[test]
551 fn test_parse_shorthand_case_insensitive() {
552 let attrs = parse_shorthand("BOLD ITALIC", "test").unwrap();
553 assert_eq!(attrs.bold, Some(true));
554 assert_eq!(attrs.italic, Some(true));
555 }
556
557 #[test]
558 fn test_parse_shorthand_comma_separated() {
559 let attrs = parse_shorthand("bold, italic, cyan", "test").unwrap();
560 assert_eq!(attrs.bold, Some(true));
561 assert_eq!(attrs.italic, Some(true));
562 assert_eq!(attrs.fg, Some(ColorDef::Named(Color::Cyan)));
563 }
564
565 #[test]
566 fn test_parse_shorthand_mixed_separators() {
567 let attrs = parse_shorthand("bold, italic underline", "test").unwrap();
568 assert_eq!(attrs.bold, Some(true));
569 assert_eq!(attrs.italic, Some(true));
570 assert_eq!(attrs.underline, Some(true));
571 }
572
573 #[test]
578 fn test_is_empty_true() {
579 assert!(StyleAttributes::new().is_empty());
580 }
581
582 #[test]
583 fn test_is_empty_false() {
584 let attrs = StyleAttributes {
585 bold: Some(true),
586 ..Default::default()
587 };
588 assert!(!attrs.is_empty());
589 }
590}