render_tree/stylesheet/
mod.rs

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
55/// This type statically prevents appending a glob right after another glob,
56/// which is illegal. It shares the `add_star` and `add` methods with
57/// `Selector`, but does not have an `add_glob` method.
58pub 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/// A Segment is one of:
106///
107/// - Root: The root node
108/// - Star: `*`, matches exactly one section names
109/// - Glob: `**`, matches zero or more section names
110/// - Name: A named segment, matches a section name that exactly matches the name
111#[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/// A Node represents a segment, child segments, and an optional associated style.
132#[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    /// Return a terminal node relative to the current node. If the current
153    /// node has no children, it's the terminal node. Otherwise, if the
154    /// current node has a glob child, that child is the terminal node.
155    ///
156    /// Otherwise, this node is not a terminal node.
157    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    /// Add nodes for the segment path, and associate it with the provided style.
169    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    /// Find a style for a section path. The resulting style is the merged result of all
185    /// matches, with literals taking precedence over stars and stars taking precedence
186    /// over globs.
187    ///
188    /// Earlier nodes take precedence over later nodes, so:
189    ///
190    /// `header *` takes precedence over `* code` for the section path `["header", "code"]`.
191    ///
192    /// Styles are merged per attribute, so the style attributes for a lower-precedence rule
193    /// will appear in the merged style as long as they are not overridden by a
194    /// higher-precedence rule.
195    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        // Accumulate styles from matches, in order of precedence.
225        let mut style: Option<Style> = None;
226
227        // A glob match means that a child node of the current node was a glob. Since
228        // globs match zero or more segments, if a node has a glob child, it will
229        // always match.
230        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        // A star matches exactly one segment.
240        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    /// Find a match in the current node for a section name.
271    ///
272    /// - If the current node is a glob, the current node is a match, since a
273    ///   glob node can absorb arbitrarily many section names.alloc
274    /// - If the current node has a glob child, it's a match. These two
275    ///   conditions cannot occur at the same time, since a glob cannot
276    ///   immediately follow a glob.
277    /// - If the current node has a glob child, and it's immediately
278    ///   followed by a literal node that matches the section, that
279    ///   node is a match.
280    /// - If the current node has a star child, it's a match
281    ///
282    /// The matches are applied in precedence order.
283    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        // A glob always matches itself
291        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    /// Construct a new stylesheet
333    pub fn new() -> Stylesheet {
334        Stylesheet {
335            styles: Node::new(Segment::Root),
336        }
337    }
338
339    /// Add a segment to the stylesheet.
340    ///
341    /// Using style strings:
342    ///
343    /// ```
344    /// # use render_tree::{Stylesheet, Style};
345    ///
346    /// let stylesheet = Stylesheet::new()
347    ///     .add("message header * code", "weight: bold; fg: red");
348    ///
349    /// assert_eq!(stylesheet.get(&["message", "header", "error", "code"]),
350    ///     Some(Style("weight: bold; fg: red")))
351    /// ```
352    ///
353    /// Using typed styles:
354    ///
355    /// ```
356    /// # use render_tree::{Color, Style, Stylesheet};
357    /// #
358    /// let stylesheet = Stylesheet::new()
359    ///     .add("message header * code", Style::new().bold().fg(Color::Red));
360    ///
361    /// assert_eq!(stylesheet.get(&["message", "header", "error", "code"]),
362    ///     Some(Style("weight: bold; fg: red")))
363    /// ```
364    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    /// Get the style associated with a nesting.
371    ///
372    /// ```
373    /// # use render_tree::{Stylesheet, Style};
374    ///
375    /// let stylesheet = Stylesheet::new()
376    ///     .add("message ** code", "fg: blue")
377    ///     .add("message header * code", "weight: bold; fg: red");
378    ///
379    /// let style = stylesheet.get(&["message", "header", "error", "code"]);
380    /// ```
381    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}