vexy_vsvg/css/
variables.rs1use std::collections::HashMap;
4use std::sync::OnceLock;
5
6use regex::Regex;
7
8use crate::ast::Element;
9
10pub struct CssVariableResolver {
17 }
20
21impl CssVariableResolver {
22 fn _parse_inline_style(style: &str) -> HashMap<String, String> {
24 static VAR_DECL_RE: OnceLock<Regex> = OnceLock::new();
25
26 let mut vars = HashMap::new();
27 for cap in VAR_DECL_RE
28 .get_or_init(|| Regex::new(r"(?m)(--[\w-]+)\s*:\s*([^;]+)").unwrap())
29 .captures_iter(style)
30 {
31 if let (Some(name), Some(value)) = (cap.get(1), cap.get(2)) {
32 vars.insert(
33 name.as_str().trim().to_string(),
34 value.as_str().trim().to_string(),
35 );
36 }
37 }
38 vars
39 }
40
41 pub fn resolve_value(name: &str, computed_vars: &HashMap<String, String>) -> Option<String> {
44 computed_vars.get(name).cloned()
45 }
46}
47
48#[derive(Debug, Clone, Default)]
50pub struct CssScope {
51 variables: HashMap<String, String>,
53}
54
55impl CssScope {
56 pub fn new() -> Self {
57 Self::default()
58 }
59
60 pub fn child_scope(&self, element: &Element) -> Self {
62 let mut new_scope = self.clone();
63
64 if let Some(style) = element.attr("style") {
65 static VAR_DECL_RE: OnceLock<Regex> = OnceLock::new();
66
67 for cap in VAR_DECL_RE
68 .get_or_init(|| Regex::new(r"(--[\w-]+)\s*:\s*([^;]+)").unwrap())
69 .captures_iter(style)
70 {
71 if let (Some(name), Some(value)) = (cap.get(1), cap.get(2)) {
72 new_scope.variables.insert(
73 name.as_str().trim().to_string(),
74 value.as_str().trim().to_string(),
75 );
76 }
77 }
78 }
79
80 new_scope
81 }
82
83 pub fn get(&self, name: &str) -> Option<&str> {
85 self.variables.get(name).map(|s| s.as_str())
86 }
87
88 pub fn resolve(&self, value: &str) -> String {
90 static VAR_REF_RE: OnceLock<Regex> = OnceLock::new();
91
92 if !value.contains("var(--") {
93 return value.to_string();
94 }
95
96 let mut result = value.to_string();
97
98 let match_info = if let Some(caps) = VAR_REF_RE
101 .get_or_init(|| Regex::new(r"var\((--[\w-]+)(?:,\s*([^)]+))?\)").unwrap())
102 .captures(&result)
103 {
104 let full_match = caps.get(0).unwrap();
105 let var_name = caps.get(1).unwrap().as_str().to_string();
106 let fallback = caps.get(2).map(|m| m.as_str().to_string());
107 Some((full_match.range(), var_name, fallback))
108 } else {
109 None
110 };
111
112 if let Some((range, var_name, fallback)) = match_info {
113 let resolved = self.get(&var_name).map(String::from).or(fallback);
114
115 if let Some(replacement) = resolved {
116 result.replace_range(range, &replacement);
117 }
118 }
119
120 result
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 use super::*;
127 use crate::ast::Element;
128
129 #[test]
130 fn test_scope_inheritance() {
131 let _root = Element::new("root"); let root_scope = CssScope::new();
133
134 let mut parent = Element::new("g");
135 parent.set_attr("style", "--color: red; --size: 10px;");
136 let parent_scope = root_scope.child_scope(&parent);
137
138 assert_eq!(parent_scope.get("--color"), Some("red"));
139
140 let mut child = Element::new("rect");
141 child.set_attr("style", "--color: blue;"); let child_scope = parent_scope.child_scope(&child);
143
144 assert_eq!(child_scope.get("--color"), Some("blue"));
145 assert_eq!(child_scope.get("--size"), Some("10px")); }
147
148 #[test]
149 fn test_resolve_var() {
150 let mut el = Element::new("g");
151 el.set_attr("style", "--main-bg: #fff;");
152 let scope = CssScope::new().child_scope(&el);
153
154 assert_eq!(scope.resolve("var(--main-bg)"), "#fff");
155 assert_eq!(scope.resolve("1px solid var(--main-bg)"), "1px solid #fff");
156 assert_eq!(scope.resolve("var(--missing, black)"), "black");
157 }
158}