1use std::collections::HashMap;
7
8use yog_data::Diagnostic;
9use yog_ids::sep::TYPED;
10use yog_ids::{yui, yui_attr};
11
12pub 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#[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
33fn problem(line: usize, message: impl std::fmt::Display) -> Diagnostic {
35 Diagnostic::error(yog_ids::build_problem::SCREEN, message).at_line(line)
36}
37
38pub 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
45const STYLE_ONLY: [&str; 3] = [yui_attr::X_KEY, yui_attr::TARGET, yui_attr::BASED_ON];
47
48impl Resources {
49 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 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}