1mod accumulator;
2mod color;
3mod format;
4mod style;
5
6use self::format::{DisplayStyle, NodeDetails};
7use crate::utils::CommaArray;
8use crate::PadItem;
9use itertools::Itertools;
10use log::*;
11use std::collections::HashMap;
12
13pub use self::accumulator::ColorAccumulator;
14pub use self::color::Color;
15pub use self::style::{Style, WriteStyle};
16
17pub struct Selector {
18 segments: Vec<Segment>,
19}
20
21impl Selector {
22 pub fn new() -> Selector {
23 Selector { segments: vec![] }
24 }
25
26 pub fn glob() -> GlobSelector {
27 Selector::new().add_glob()
28 }
29
30 pub fn star() -> Selector {
31 Selector::new().add_star()
32 }
33
34 pub fn name(name: &'static str) -> Selector {
35 Selector::new().add(name)
36 }
37
38 pub fn add_glob(self) -> GlobSelector {
39 let mut segments = self.segments;
40 segments.push(Segment::Glob);
41 GlobSelector { segments }
42 }
43
44 pub fn add_star(mut self) -> Selector {
45 self.segments.push(Segment::Star);
46 self
47 }
48
49 pub fn add(mut self, segment: &'static str) -> Selector {
50 self.segments.push(Segment::Name(segment));
51 self
52 }
53}
54
55pub struct GlobSelector {
59 segments: Vec<Segment>,
60}
61
62impl GlobSelector {
63 pub fn add_star(self) -> Selector {
64 let mut segments = self.segments;
65 segments.push(Segment::Star);
66 Selector { segments }
67 }
68
69 pub fn add(self, segment: &'static str) -> Selector {
70 let mut segments = self.segments;
71 segments.push(Segment::Name(segment));
72 Selector { segments }
73 }
74}
75
76impl IntoIterator for Selector {
77 type Item = Segment;
78 type IntoIter = ::std::vec::IntoIter<Segment>;
79
80 fn into_iter(self) -> ::std::vec::IntoIter<Segment> {
81 self.segments.into_iter()
82 }
83}
84
85impl IntoIterator for GlobSelector {
86 type Item = Segment;
87 type IntoIter = ::std::vec::IntoIter<Segment>;
88
89 fn into_iter(self) -> ::std::vec::IntoIter<Segment> {
90 self.segments.into_iter()
91 }
92}
93
94impl From<&'static str> for Selector {
95 fn from(from: &'static str) -> Selector {
96 let segments = from.split(' ');
97 let segments = segments.map(|part| part.into());
98
99 Selector {
100 segments: segments.collect(),
101 }
102 }
103}
104
105#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
112pub enum Segment {
113 Root,
114 Star,
115 Glob,
116 Name(&'static str),
117}
118
119impl From<&'static str> for Segment {
120 fn from(from: &'static str) -> Segment {
121 if from == "**" {
122 Segment::Glob
123 } else if from == "*" {
124 Segment::Star
125 } else {
126 Segment::Name(from)
127 }
128 }
129}
130
131#[derive(Debug)]
133struct Node {
134 segment: Segment,
135 children: HashMap<Segment, Node>,
136 declarations: Option<Style>,
137}
138
139impl Node {
140 fn new(segment: Segment) -> Node {
141 Node {
142 segment,
143 children: HashMap::new(),
144 declarations: None,
145 }
146 }
147
148 fn display<'a>(&'a self) -> NodeDetails<'a> {
149 NodeDetails::new(self.segment, &self.declarations)
150 }
151
152 fn terminal(&self) -> Option<&Node> {
158 match self.children.get(&Segment::Glob) {
159 None => if self.children.is_empty() {
160 return Some(self);
161 } else {
162 return None;
163 },
164 Some(glob) => return Some(glob),
165 };
166 }
167
168 fn add(&mut self, selector: impl IntoIterator<Item = Segment>, declarations: impl Into<Style>) {
170 let mut path = selector.into_iter();
171
172 match path.next() {
173 None => {
174 self.declarations = Some(declarations.into());
175 }
176 Some(name) => self
177 .children
178 .entry(name)
179 .or_insert(Node::new(name))
180 .add(path, declarations),
181 }
182 }
183
184 fn find<'a>(&self, names: &[&'static str], debug_nesting: usize) -> Option<Style> {
196 trace!(
197 "{}In {}, finding {:?} (children={})",
198 PadItem(" ", debug_nesting),
199 self,
200 names.join(" "),
201 CommaArray(self.children.keys().map(|k| k.to_string()).collect())
202 );
203
204 let next_name = match names.first() {
205 None => {
206 let terminal = self.terminal()?;
207
208 trace!(
209 "{}Matched terminal {}",
210 PadItem(" ", debug_nesting),
211 terminal.display()
212 );
213
214 return terminal.declarations.clone();
215 }
216
217 Some(next_name) => next_name,
218 };
219
220 let matches = self.find_match(next_name);
221
222 trace!("{}Matches: {}", PadItem(" ", debug_nesting), matches);
223
224 let mut style: Option<Style> = None;
226
227 if let Some(glob) = matches.glob {
231 style = union(style, glob.find(&names[1..], debug_nesting + 1));
232 trace!(
233 "{}matched glob={}",
234 PadItem(" ", debug_nesting),
235 DisplayStyle(&style)
236 );
237 }
238
239 if let Some(star) = matches.star {
241 style = union(style, star.find(&names[1..], debug_nesting + 1));
242 trace!(
243 "{}matched star={}",
244 PadItem(" ", debug_nesting),
245 DisplayStyle(&style)
246 );
247 }
248
249 if let Some(skipped_glob) = matches.skipped_glob {
250 style = union(style, skipped_glob.find(&names[1..], debug_nesting + 1));
251 trace!(
252 "{}matched skipped_glob={}",
253 PadItem(" ", debug_nesting),
254 DisplayStyle(&style)
255 );
256 }
257
258 if let Some(literal) = matches.literal {
259 style = union(style, literal.find(&names[1..], debug_nesting + 1));
260 trace!(
261 "{}matched literal={}",
262 PadItem(" ", debug_nesting),
263 DisplayStyle(&style)
264 );
265 }
266
267 style
268 }
269
270 fn find_match<'a>(&'a self, name: &'static str) -> Match<'a> {
284 let glob;
285
286 let mut skipped_glob = None;
287 let star = self.children.get(&Segment::Star);
288 let literal = self.children.get(&Segment::Name(name));
289
290 if self.segment == Segment::Glob {
292 glob = Some(self);
293 } else {
294 glob = self.children.get(&Segment::Glob);
295
296 if let Some(glob) = glob {
297 skipped_glob = glob.children.get(&Segment::Name(name));
298 }
299 }
300
301 Match {
302 glob,
303 star,
304 skipped_glob,
305 literal,
306 }
307 }
308}
309
310fn union(left: Option<Style>, right: Option<Style>) -> Option<Style> {
311 match (left, right) {
312 (None, None) => None,
313 (Some(left), None) => Some(left),
314 (None, Some(right)) => Some(right),
315 (Some(left), Some(right)) => Some(left.union(right)),
316 }
317}
318
319struct Match<'a> {
320 glob: Option<&'a Node>,
321 star: Option<&'a Node>,
322 skipped_glob: Option<&'a Node>,
323 literal: Option<&'a Node>,
324}
325
326#[derive(Debug)]
327pub struct Stylesheet {
328 styles: Node,
329}
330
331impl Stylesheet {
332 pub fn new() -> Stylesheet {
334 Stylesheet {
335 styles: Node::new(Segment::Root),
336 }
337 }
338
339 pub fn add(mut self, name: impl Into<Selector>, declarations: impl Into<Style>) -> Stylesheet {
365 self.styles.add(name.into(), declarations);
366
367 self
368 }
369
370 pub fn get(&self, names: &[&'static str]) -> Option<Style> {
382 if log_enabled!(::log::Level::Trace) {
383 println!("\n");
384 }
385
386 trace!("Searching for `{}`", names.iter().join(" "));
387 let style = self.styles.find(names, 0);
388
389 match &style {
390 None => trace!("No style found"),
391 Some(style) => trace!("Found {}", style),
392 }
393
394 style
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::style::Style;
401 use crate::{Color, Stylesheet};
402 use pretty_env_logger;
403
404 fn init_logger() {
405 pretty_env_logger::try_init().ok();
406 }
407
408 #[test]
409 fn test_basic_lookup() {
410 init_logger();
411
412 let stylesheet =
413 Stylesheet::new().add("message header error code", "fg: red; underline: false");
414
415 let style = stylesheet.get(&["message", "header", "error", "code"]);
416
417 assert_eq!(style, Some(Style("fg: red; underline: false")))
418 }
419
420 #[test]
421 fn test_basic_with_typed_style() {
422 init_logger();
423
424 let stylesheet = Stylesheet::new().add(
425 "message header error code",
426 Style::new().bold().fg(Color::Red),
427 );
428
429 assert_eq!(
430 stylesheet.get(&["message", "header", "error", "code"]),
431 Some(Style("weight: bold; fg: red"))
432 )
433 }
434
435 #[test]
436 fn test_star() {
437 init_logger();
438
439 let stylesheet =
440 Stylesheet::new().add("message header * code", "fg: red; underline: false");
441
442 let style = stylesheet.get(&["message", "header", "error", "code"]);
443
444 assert_eq!(style, Some(Style("fg: red; underline: false")))
445 }
446
447 #[test]
448 fn test_star_with_typed_style() {
449 init_logger();
450
451 let stylesheet =
452 Stylesheet::new().add("message header * code", Style::new().bold().fg(Color::Red));
453
454 assert_eq!(
455 stylesheet.get(&["message", "header", "error", "code"]),
456 Some(Style("weight: bold; fg: red"))
457 )
458 }
459
460 #[test]
461 fn test_glob() {
462 init_logger();
463
464 let stylesheet = Stylesheet::new().add("message ** code", "fg: red; underline: false");
465
466 let style = stylesheet.get(&["message", "header", "error", "code"]);
467
468 assert_eq!(style, Some(Style("fg: red; underline: false")))
469 }
470
471 #[test]
472 fn test_glob_with_typed_style() {
473 init_logger();
474
475 let stylesheet =
476 Stylesheet::new().add("message ** code", Style::new().nounderline().fg(Color::Red));
477
478 let style = stylesheet.get(&["message", "header", "error", "code"]);
479
480 assert_eq!(style, Some(Style("fg: red; underline: false")))
481 }
482
483 #[test]
484 fn test_glob_matches_no_segments() {
485 init_logger();
486
487 let stylesheet =
488 Stylesheet::new().add("message ** header error code", "fg: red; underline: false");
489
490 let style = stylesheet.get(&["message", "header", "error", "code"]);
491
492 assert_eq!(style, Some(Style("fg: red; underline: false")))
493 }
494
495 #[test]
496 fn test_glob_matches_no_segments_with_typed_style() {
497 init_logger();
498
499 let stylesheet = Stylesheet::new().add(
500 "message ** header error code",
501 Style::new().nounderline().fg(Color::Red),
502 );
503
504 let style = stylesheet.get(&["message", "header", "error", "code"]);
505
506 assert_eq!(style, Some(Style("fg: red; underline: false")))
507 }
508
509 #[test]
510 fn test_trailing_glob_is_terminal() {
511 init_logger();
512
513 let stylesheet = Stylesheet::new().add(
514 "message header error **",
515 Style::new().nounderline().fg(Color::Red),
516 );
517
518 let style = stylesheet.get(&["message", "header", "error", "code"]);
519
520 assert_eq!(style, Some(Style("fg: red; underline: false")))
521 }
522
523 #[test]
524 fn test_trailing_glob_is_terminal_with_typed_styles() {
525 init_logger();
526
527 let stylesheet = Stylesheet::new().add(
528 "message header error **",
529 Style::new().nounderline().fg(Color::Red),
530 );
531
532 let style = stylesheet.get(&["message", "header", "error", "code"]);
533
534 assert_eq!(style, Some(Style::new().fg(Color::Red).nounderline()))
535 }
536
537 #[test]
538 fn test_trailing_glob_is_terminal_and_matches_nothing() {
539 init_logger();
540
541 let stylesheet =
542 Stylesheet::new().add("message header error code **", "fg: red; underline: false");
543
544 let style = stylesheet.get(&["message", "header", "error", "code"]);
545
546 assert_eq!(style, Some(Style::new().fg(Color::Red).nounderline()))
547 }
548
549 #[test]
550 fn test_trailing_glob_is_terminal_and_matches_nothing_with_typed_style() {
551 init_logger();
552
553 let stylesheet = Stylesheet::new().add(
554 "message header error code **",
555 Style::new().nounderline().fg(Color::Red),
556 );
557
558 let style = stylesheet.get(&["message", "header", "error", "code"]);
559
560 assert_eq!(style, Some(Style::new().fg(Color::Red).nounderline()))
561 }
562
563 #[test]
564 fn test_priority() {
565 init_logger();
566
567 let stylesheet = Stylesheet::new()
568 .add("message ** code", "fg: blue; weight: bold")
569 .add("message header * code", "underline: true; bg: black")
570 .add("message header error code", "fg: red; underline: false");
571
572 let style = stylesheet.get(&["message", "header", "error", "code"]);
573
574 assert_eq!(
575 style,
576 Some(
577 Style::new()
578 .fg(Color::Red)
579 .bg(Color::Black)
580 .nounderline()
581 .bold()
582 )
583 )
584 }
585
586 #[test]
587 fn test_priority_with_typed_style() {
588 init_logger();
589
590 let stylesheet = Stylesheet::new()
591 .add("message ** code", Style::new().fg(Color::Blue).bold())
592 .add(
593 "message header * code",
594 Style::new().underline().bg(Color::Black),
595 ).add(
596 "message header error code",
597 Style::new().fg(Color::Red).nounderline(),
598 );
599
600 let style = stylesheet.get(&["message", "header", "error", "code"]);
601
602 assert_eq!(
603 style,
604 Some(
605 Style::new()
606 .fg(Color::Red)
607 .bg(Color::Black)
608 .nounderline()
609 .bold()
610 )
611 )
612 }
613}