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