Skip to main content

yog_cli/
yui_resources.rs

1//! A screen's `<Screen.Resources>`: values by key (`<Color x:Key="Accent"
2//! value="#FFD700"/>`), and styles (`<Style x:Key based_on>`: their
3//! attributes over their base's, `@Name` values put in) — read once for
4//! the build and the designer alike.
5
6use std::collections::HashMap;
7
8use yog_data::Diagnostic;
9use yog_ids::sep::TYPED;
10use yog_ids::{yui, yui_attr};
11
12/// One element of the resources: its kind, line and attributes.
13pub struct Entry<'a> {
14    pub kind: &'a str,
15    pub line: usize,
16    pub attrs: Vec<(&'a str, &'a str)>,
17}
18
19impl Entry<'_> {
20    fn attr(&self, key: &str) -> Option<&str> {
21        self.attrs.iter().find(|(k, _)| *k == key).map(|(_, v)| *v)
22    }
23}
24
25/// A screen's values and styles; the problems reading them.
26#[derive(Default)]
27pub struct Resources {
28    pub values: HashMap<String, String>,
29    pub styles: HashMap<String, Vec<(String, String)>>,
30    pub problems: Vec<Diagnostic>,
31}
32
33/// A problem with the resources at `line`.
34fn problem(line: usize, message: impl std::fmt::Display) -> Diagnostic {
35    Diagnostic::error(yog_ids::build_problem::SCREEN, message).at_line(line)
36}
37
38/// The resource `v` names (`@Accent` → `Accent`); none for anything else
39/// (`@@x`, `@Set.key`, words).
40pub fn local_name(v: &str) -> Option<&str> {
41    v.strip_prefix(TYPED)
42        .filter(|n| !n.is_empty() && !n.starts_with(TYPED) && !n.contains('.'))
43}
44
45/// A style's own attributes that aren't for its elements.
46const STYLE_ONLY: [&str; 3] = [yui_attr::X_KEY, yui_attr::TARGET, yui_attr::BASED_ON];
47
48impl Resources {
49    /// The resources of `entries`: values first, then styles, their
50    /// `based_on` chains in any order.
51    pub fn read(entries: &[Entry]) -> Resources {
52        let mut out = Resources::default();
53        let mut pending: Vec<&Entry> = Vec::new();
54        for r in entries {
55            let Some(key) = r.attr(yui_attr::X_KEY) else {
56                out.problems
57                    .push(problem(r.line, format!("<{}> needs an x:Key", r.kind)));
58                continue;
59            };
60            if r.kind == yui::STYLE {
61                pending.push(r);
62            } else if let Some(v) = r.attr(yui_attr::VALUE) {
63                out.values.insert(key.to_string(), v.to_string());
64            } else {
65                out.problems.push(problem(
66                    r.line,
67                    format!(
68                        "<{}> needs a value (e.g. <Color x:Key=\"Accent\" value=\"#FFD700\"/>)",
69                        r.kind
70                    ),
71                ));
72            }
73        }
74        while !pending.is_empty() {
75            let before = pending.len();
76            pending.retain(|s| !out.style(s));
77            if pending.len() == before {
78                out.problems.push(problem(
79                    pending[0].line,
80                    "<Style> based_on names an unknown (or circular) style",
81                ));
82                break;
83            }
84        }
85        out
86    }
87
88    /// Style `s` read when its base is (true); its attributes over its base's.
89    fn style(&mut self, s: &Entry) -> bool {
90        let mut attrs = match s
91            .attr(yui_attr::BASED_ON)
92            .map(|b| b.trim().trim_start_matches(TYPED))
93        {
94            Some(b) => match self.styles.get(b) {
95                Some(a) => a.clone(),
96                None => return false,
97            },
98            None => Vec::new(),
99        };
100        for (k, v) in s.attrs.iter().filter(|(k, _)| !STYLE_ONLY.contains(k)) {
101            let v = match local_name(v) {
102                Some(n) => match self.values.get(n) {
103                    Some(r) => r.clone(),
104                    None => {
105                        self.problems.push(problem(
106                            s.line,
107                            format!("<Style> {k}=\"{v}\": no resource `{n}`"),
108                        ));
109                        v.to_string()
110                    }
111                },
112                None => v.to_string(),
113            };
114            attrs.retain(|(ek, _)| ek != k);
115            attrs.push((k.to_string(), v));
116        }
117        let key = s.attr(yui_attr::X_KEY).unwrap_or_default().to_string();
118        self.styles.insert(key, attrs);
119        true
120    }
121}