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