1use console::Style;
4use std::collections::HashMap;
5
6use super::error::StyleValidationError;
7use super::value::StyleValue;
8
9pub const DEFAULT_MISSING_STYLE_INDICATOR: &str = "(!?)";
11
12#[derive(Debug, Clone)]
46pub struct Styles {
47 styles: HashMap<String, StyleValue>,
48 missing_indicator: String,
49}
50
51impl Default for Styles {
52 fn default() -> Self {
53 Self {
54 styles: HashMap::new(),
55 missing_indicator: DEFAULT_MISSING_STYLE_INDICATOR.to_string(),
56 }
57 }
58}
59
60impl Styles {
61 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn missing_indicator(mut self, indicator: &str) -> Self {
84 self.missing_indicator = indicator.to_string();
85 self
86 }
87
88 pub fn add<V: Into<StyleValue>>(mut self, name: &str, value: V) -> Self {
106 self.styles.insert(name.to_string(), value.into());
107 self
108 }
109
110 pub(crate) fn resolve(&self, name: &str) -> Option<&Style> {
115 let mut current = name;
116 let mut visited = std::collections::HashSet::new();
117
118 loop {
119 if !visited.insert(current) {
120 return None; }
122 match self.styles.get(current)? {
123 StyleValue::Concrete(style) => return Some(style),
124 StyleValue::Alias(next) => current = next,
125 }
126 }
127 }
128
129 fn can_resolve(&self, name: &str) -> bool {
131 self.resolve(name).is_some()
132 }
133
134 pub fn validate(&self) -> Result<(), StyleValidationError> {
169 for (name, value) in &self.styles {
170 if let StyleValue::Alias(target) = value {
171 self.validate_alias_chain(name, target)?;
172 }
173 }
174 Ok(())
175 }
176
177 fn validate_alias_chain(&self, name: &str, target: &str) -> Result<(), StyleValidationError> {
179 let mut current = target;
180 let mut path = vec![name.to_string()];
181
182 loop {
183 let value =
185 self.styles
186 .get(current)
187 .ok_or_else(|| StyleValidationError::UnresolvedAlias {
188 from: path.last().unwrap().clone(),
189 to: current.to_string(),
190 })?;
191
192 path.push(current.to_string());
193
194 if path[..path.len() - 1].contains(¤t.to_string()) {
196 return Err(StyleValidationError::CycleDetected { path });
197 }
198
199 match value {
200 StyleValue::Concrete(_) => return Ok(()),
201 StyleValue::Alias(next) => current = next,
202 }
203 }
204 }
205
206 pub fn apply(&self, name: &str, text: &str) -> String {
211 match self.resolve(name) {
212 Some(style) => style.apply_to(text).to_string(),
213 None if self.missing_indicator.is_empty() => text.to_string(),
214 None => format!("{} {}", self.missing_indicator, text),
215 }
216 }
217
218 pub fn apply_plain(&self, name: &str, text: &str) -> String {
223 if self.can_resolve(name) || self.missing_indicator.is_empty() {
224 text.to_string()
225 } else {
226 format!("{} {}", self.missing_indicator, text)
227 }
228 }
229
230 pub fn apply_with_mode(&self, name: &str, text: &str, use_color: bool) -> String {
239 if use_color {
240 self.apply(name, text)
241 } else {
242 self.apply_plain(name, text)
243 }
244 }
245
246 pub fn apply_debug(&self, name: &str, text: &str) -> String {
271 if self.can_resolve(name) {
272 format!("[{}]{}[/{}]", name, text, name)
273 } else if self.missing_indicator.is_empty() {
274 text.to_string()
275 } else {
276 format!("{} {}", self.missing_indicator, text)
277 }
278 }
279
280 pub fn has(&self, name: &str) -> bool {
282 self.styles.contains_key(name)
283 }
284
285 pub fn len(&self) -> usize {
287 self.styles.len()
288 }
289
290 pub fn is_empty(&self) -> bool {
292 self.styles.is_empty()
293 }
294
295 pub fn to_resolved_map(&self) -> HashMap<String, Style> {
317 let mut result = HashMap::new();
318 for name in self.styles.keys() {
319 if let Some(style) = self.resolve(name) {
320 result.insert(name.clone(), style.clone());
321 }
322 }
323 result
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 #[test]
332 fn test_styles_new_is_empty() {
333 let styles = Styles::new();
334 assert!(styles.is_empty());
335 assert_eq!(styles.len(), 0);
336 }
337
338 #[test]
339 fn test_styles_add_and_has() {
340 let styles = Styles::new()
341 .add("error", Style::new().red())
342 .add("ok", Style::new().green());
343
344 assert!(styles.has("error"));
345 assert!(styles.has("ok"));
346 assert!(!styles.has("warning"));
347 assert_eq!(styles.len(), 2);
348 }
349
350 #[test]
351 fn test_styles_apply_unknown_shows_indicator() {
352 let styles = Styles::new();
353 let result = styles.apply("nonexistent", "hello");
354 assert_eq!(result, "(!?) hello");
355 }
356
357 #[test]
358 fn test_styles_apply_unknown_with_empty_indicator() {
359 let styles = Styles::new().missing_indicator("");
360 let result = styles.apply("nonexistent", "hello");
361 assert_eq!(result, "hello");
362 }
363
364 #[test]
365 fn test_styles_apply_unknown_with_custom_indicator() {
366 let styles = Styles::new().missing_indicator("[MISSING]");
367 let result = styles.apply("nonexistent", "hello");
368 assert_eq!(result, "[MISSING] hello");
369 }
370
371 #[test]
372 fn test_styles_apply_plain_known_style() {
373 let styles = Styles::new().add("bold", Style::new().bold());
374 let result = styles.apply_plain("bold", "hello");
375 assert_eq!(result, "hello");
377 }
378
379 #[test]
380 fn test_styles_apply_plain_unknown_shows_indicator() {
381 let styles = Styles::new();
382 let result = styles.apply_plain("nonexistent", "hello");
383 assert_eq!(result, "(!?) hello");
384 }
385
386 #[test]
387 fn test_styles_apply_known_style() {
388 let styles = Styles::new().add("bold", Style::new().bold().force_styling(true));
389 let result = styles.apply("bold", "hello");
390 assert!(result.contains("hello"));
392 assert!(result.contains("\x1b[1m"));
394 }
395
396 #[test]
397 fn test_styles_can_be_replaced() {
398 let styles = Styles::new()
399 .add("x", Style::new().red())
400 .add("x", Style::new().green()); assert_eq!(styles.len(), 1);
404 assert!(styles.has("x"));
405 }
406
407 #[test]
408 fn test_styles_apply_with_mode_color() {
409 let styles = Styles::new().add("bold", Style::new().bold().force_styling(true));
410 let result = styles.apply_with_mode("bold", "hello", true);
411 assert!(result.contains("\x1b[1m"));
413 assert!(result.contains("hello"));
414 }
415
416 #[test]
417 fn test_styles_apply_with_mode_no_color() {
418 let styles = Styles::new().add("bold", Style::new().bold());
419 let result = styles.apply_with_mode("bold", "hello", false);
420 assert_eq!(result, "hello");
422 }
423
424 #[test]
425 fn test_styles_apply_with_mode_missing_style() {
426 let styles = Styles::new();
427 let result = styles.apply_with_mode("nonexistent", "hello", true);
429 assert_eq!(result, "(!?) hello");
430 let result = styles.apply_with_mode("nonexistent", "hello", false);
432 assert_eq!(result, "(!?) hello");
433 }
434
435 #[test]
436 fn test_styles_apply_debug_known_style() {
437 let styles = Styles::new().add("bold", Style::new().bold());
438 let result = styles.apply_debug("bold", "hello");
439 assert_eq!(result, "[bold]hello[/bold]");
440 }
441
442 #[test]
443 fn test_styles_apply_debug_unknown_style() {
444 let styles = Styles::new();
445 let result = styles.apply_debug("unknown", "hello");
446 assert_eq!(result, "(!?) hello");
447 }
448
449 #[test]
450 fn test_styles_apply_debug_unknown_empty_indicator() {
451 let styles = Styles::new().missing_indicator("");
452 let result = styles.apply_debug("unknown", "hello");
453 assert_eq!(result, "hello");
454 }
455
456 #[test]
459 fn test_resolve_concrete_style() {
460 let styles = Styles::new().add("bold", Style::new().bold());
461 assert!(styles.resolve("bold").is_some());
462 }
463
464 #[test]
465 fn test_resolve_nonexistent_style() {
466 let styles = Styles::new();
467 assert!(styles.resolve("nonexistent").is_none());
468 }
469
470 #[test]
471 fn test_resolve_single_alias() {
472 let styles = Styles::new()
473 .add("base", Style::new().dim())
474 .add("alias", "base");
475
476 assert!(styles.resolve("alias").is_some());
477 assert!(styles.resolve("base").is_some());
478 }
479
480 #[test]
481 fn test_resolve_chained_aliases() {
482 let styles = Styles::new()
483 .add("visual", Style::new().cyan())
484 .add("presentation", "visual")
485 .add("semantic", "presentation");
486
487 assert!(styles.resolve("visual").is_some());
489 assert!(styles.resolve("presentation").is_some());
490 assert!(styles.resolve("semantic").is_some());
491 }
492
493 #[test]
494 fn test_resolve_deep_alias_chain() {
495 let styles = Styles::new()
496 .add("level0", Style::new().bold())
497 .add("level1", "level0")
498 .add("level2", "level1")
499 .add("level3", "level2")
500 .add("level4", "level3");
501
502 assert!(styles.resolve("level4").is_some());
503 }
504
505 #[test]
506 fn test_resolve_dangling_alias_returns_none() {
507 let styles = Styles::new().add("orphan", "nonexistent");
508 assert!(styles.resolve("orphan").is_none());
509 }
510
511 #[test]
512 fn test_resolve_cycle_returns_none() {
513 let styles = Styles::new().add("a", "b").add("b", "a");
514
515 assert!(styles.resolve("a").is_none());
516 assert!(styles.resolve("b").is_none());
517 }
518
519 #[test]
520 fn test_resolve_self_referential_returns_none() {
521 let styles = Styles::new().add("self", "self");
522 assert!(styles.resolve("self").is_none());
523 }
524
525 #[test]
526 fn test_resolve_three_way_cycle() {
527 let styles = Styles::new().add("a", "b").add("b", "c").add("c", "a");
528
529 assert!(styles.resolve("a").is_none());
530 assert!(styles.resolve("b").is_none());
531 assert!(styles.resolve("c").is_none());
532 }
533
534 #[test]
537 fn test_validate_empty_styles() {
538 let styles = Styles::new();
539 assert!(styles.validate().is_ok());
540 }
541
542 #[test]
543 fn test_validate_only_concrete_styles() {
544 let styles = Styles::new()
545 .add("a", Style::new().bold())
546 .add("b", Style::new().dim())
547 .add("c", Style::new().red());
548
549 assert!(styles.validate().is_ok());
550 }
551
552 #[test]
553 fn test_validate_valid_alias() {
554 let styles = Styles::new()
555 .add("base", Style::new().dim())
556 .add("alias", "base");
557
558 assert!(styles.validate().is_ok());
559 }
560
561 #[test]
562 fn test_validate_valid_alias_chain() {
563 let styles = Styles::new()
564 .add("visual", Style::new().cyan())
565 .add("presentation", "visual")
566 .add("semantic", "presentation");
567
568 assert!(styles.validate().is_ok());
569 }
570
571 #[test]
572 fn test_validate_dangling_alias_error() {
573 let styles = Styles::new().add("orphan", "nonexistent");
574
575 let result = styles.validate();
576 assert!(result.is_err());
577
578 match result.unwrap_err() {
579 StyleValidationError::UnresolvedAlias { from, to } => {
580 assert_eq!(from, "orphan");
581 assert_eq!(to, "nonexistent");
582 }
583 _ => panic!("Expected UnresolvedAlias error"),
584 }
585 }
586
587 #[test]
588 fn test_validate_dangling_in_chain() {
589 let styles = Styles::new()
590 .add("level1", "level2")
591 .add("level2", "missing");
592
593 let result = styles.validate();
594 assert!(result.is_err());
595
596 match result.unwrap_err() {
597 StyleValidationError::UnresolvedAlias { from: _, to } => {
598 assert_eq!(to, "missing");
599 }
600 _ => panic!("Expected UnresolvedAlias error"),
601 }
602 }
603
604 #[test]
605 fn test_validate_cycle_error() {
606 let styles = Styles::new().add("a", "b").add("b", "a");
607
608 let result = styles.validate();
609 assert!(result.is_err());
610
611 match result.unwrap_err() {
612 StyleValidationError::CycleDetected { path } => {
613 assert!(path.contains(&"a".to_string()));
614 assert!(path.contains(&"b".to_string()));
615 }
616 _ => panic!("Expected CycleDetected error"),
617 }
618 }
619
620 #[test]
621 fn test_validate_self_referential_cycle() {
622 let styles = Styles::new().add("self", "self");
623
624 let result = styles.validate();
625 assert!(result.is_err());
626
627 match result.unwrap_err() {
628 StyleValidationError::CycleDetected { path } => {
629 assert!(path.contains(&"self".to_string()));
630 }
631 _ => panic!("Expected CycleDetected error"),
632 }
633 }
634
635 #[test]
636 fn test_validate_three_way_cycle() {
637 let styles = Styles::new().add("a", "b").add("b", "c").add("c", "a");
638
639 let result = styles.validate();
640 assert!(result.is_err());
641
642 match result.unwrap_err() {
643 StyleValidationError::CycleDetected { path } => {
644 assert!(path.len() >= 3);
645 }
646 _ => panic!("Expected CycleDetected error"),
647 }
648 }
649
650 #[test]
651 fn test_validate_mixed_valid_and_invalid() {
652 let styles = Styles::new()
653 .add("valid1", Style::new().bold())
654 .add("valid2", "valid1")
655 .add("invalid", "missing");
656
657 assert!(styles.validate().is_err());
658 }
659
660 #[test]
663 fn test_apply_through_alias() {
664 let styles = Styles::new()
665 .add("base", Style::new().bold().force_styling(true))
666 .add("alias", "base");
667
668 let result = styles.apply("alias", "text");
669 assert!(result.contains("\x1b[1m"));
670 assert!(result.contains("text"));
671 }
672
673 #[test]
674 fn test_apply_through_chain() {
675 let styles = Styles::new()
676 .add("visual", Style::new().red().force_styling(true))
677 .add("presentation", "visual")
678 .add("semantic", "presentation");
679
680 let result = styles.apply("semantic", "error");
681 assert!(result.contains("\x1b[31m"));
682 assert!(result.contains("error"));
683 }
684
685 #[test]
686 fn test_apply_dangling_alias_shows_indicator() {
687 let styles = Styles::new().add("orphan", "missing");
688 let result = styles.apply("orphan", "text");
689 assert_eq!(result, "(!?) text");
690 }
691
692 #[test]
693 fn test_apply_cycle_shows_indicator() {
694 let styles = Styles::new().add("a", "b").add("b", "a");
695
696 let result = styles.apply("a", "text");
697 assert_eq!(result, "(!?) text");
698 }
699
700 #[test]
701 fn test_apply_plain_through_alias() {
702 let styles = Styles::new()
703 .add("base", Style::new().bold())
704 .add("alias", "base");
705
706 let result = styles.apply_plain("alias", "text");
707 assert_eq!(result, "text");
708 }
709
710 #[test]
711 fn test_apply_debug_through_alias() {
712 let styles = Styles::new()
713 .add("base", Style::new().bold())
714 .add("alias", "base");
715
716 let result = styles.apply_debug("alias", "text");
717 assert_eq!(result, "[alias]text[/alias]");
718 }
719
720 #[test]
721 fn test_apply_debug_dangling_alias() {
722 let styles = Styles::new().add("orphan", "missing");
723 let result = styles.apply_debug("orphan", "text");
724 assert_eq!(result, "(!?) text");
725 }
726}