1use crate::id::NodeId;
8use crate::model::{Import, SceneGraph};
9use crate::parser::parse_document;
10use std::collections::HashSet;
11
12pub trait ImportLoader {
21 fn load(&self, path: &str) -> Result<String, String>;
23}
24
25pub fn resolve_imports(graph: &mut SceneGraph, loader: &dyn ImportLoader) -> Result<(), String> {
35 let imports = graph.imports.clone();
36 let mut visited = HashSet::new();
37 resolve_imports_recursive(graph, &imports, loader, &mut visited)
38}
39
40fn resolve_imports_recursive(
41 graph: &mut SceneGraph,
42 imports: &[Import],
43 loader: &dyn ImportLoader,
44 visited: &mut HashSet<String>,
45) -> Result<(), String> {
46 for import in imports {
47 if !visited.insert(import.path.clone()) {
49 return Err(format!(
50 "Circular import detected: \"{}\" was already imported",
51 import.path
52 ));
53 }
54
55 let source = loader.load(&import.path)?;
57 let mut imported = parse_document(&source)
58 .map_err(|e| format!("Error parsing \"{}\": {e}", import.path))?;
59
60 let nested_imports = imported.imports.clone();
62 if !nested_imports.is_empty() {
63 resolve_imports_recursive(&mut imported, &nested_imports, loader, visited)?;
64 }
65
66 merge_namespaced_styles(graph, &imported, &import.namespace)?;
68
69 merge_namespaced_nodes(graph, &imported, &import.namespace)?;
71
72 merge_namespaced_edges(graph, &imported, &import.namespace);
74 }
75
76 Ok(())
77}
78
79fn merge_namespaced_styles(
81 graph: &mut SceneGraph,
82 imported: &SceneGraph,
83 namespace: &str,
84) -> Result<(), String> {
85 for (name, style) in &imported.styles {
86 let ns_name = NodeId::intern(&format!("{namespace}.{}", name.as_str()));
87 if graph.styles.contains_key(&ns_name) {
88 return Err(format!(
89 "Style conflict: \"{namespace}.{}\" already exists",
90 name.as_str()
91 ));
92 }
93 graph.define_style(ns_name, style.clone());
94 }
95 Ok(())
96}
97
98fn merge_namespaced_nodes(
100 graph: &mut SceneGraph,
101 imported: &SceneGraph,
102 namespace: &str,
103) -> Result<(), String> {
104 let children = imported.children(imported.root);
105 for child_idx in children {
106 merge_node_recursive(graph, graph.root, imported, child_idx, namespace)?;
107 }
108 Ok(())
109}
110
111fn merge_node_recursive(
113 graph: &mut SceneGraph,
114 parent: petgraph::graph::NodeIndex,
115 imported: &SceneGraph,
116 source_idx: petgraph::graph::NodeIndex,
117 namespace: &str,
118) -> Result<(), String> {
119 let source_node = &imported.graph[source_idx];
120
121 let ns_id = prefix_node_id(&source_node.id, namespace);
123 if graph.id_index.contains_key(&ns_id) {
124 return Err(format!(
125 "Node ID conflict: \"{}\" already exists",
126 ns_id.as_str()
127 ));
128 }
129
130 let mut cloned = source_node.clone();
131 cloned.id = ns_id;
132
133 for use_ref in &mut cloned.use_styles {
135 *use_ref = prefix_node_id(use_ref, namespace);
136 }
137
138 let new_idx = graph.add_node(parent, cloned);
139
140 let children = imported.children(source_idx);
142 for child_idx in children {
143 merge_node_recursive(graph, new_idx, imported, child_idx, namespace)?;
144 }
145
146 Ok(())
147}
148
149fn merge_namespaced_edges(graph: &mut SceneGraph, imported: &SceneGraph, namespace: &str) {
151 for edge in &imported.edges {
152 let mut cloned = edge.clone();
153 cloned.id = prefix_node_id(&cloned.id, namespace);
154 cloned.from = prefix_node_id(&cloned.from, namespace);
155 cloned.to = prefix_node_id(&cloned.to, namespace);
156 for use_ref in &mut cloned.use_styles {
157 *use_ref = prefix_node_id(use_ref, namespace);
158 }
159 graph.edges.push(cloned);
160 }
161}
162
163fn prefix_node_id(id: &NodeId, namespace: &str) -> NodeId {
166 NodeId::intern(&format!("{namespace}.{}", id.as_str()))
167}
168
169#[cfg(test)]
172mod tests {
173 use super::*;
174 use std::collections::HashMap;
175
176 struct MemoryLoader {
178 files: HashMap<String, String>,
179 }
180
181 impl ImportLoader for MemoryLoader {
182 fn load(&self, path: &str) -> Result<String, String> {
183 self.files
184 .get(path)
185 .cloned()
186 .ok_or_else(|| format!("File not found: {path}"))
187 }
188 }
189
190 #[test]
191 fn resolve_namespace_prefixing() {
192 let imported_source = r#"
193style accent { fill: #6C5CE7 }
194rect @button { w: 100 h: 40; fill: #FF0000 }
195"#;
196
197 let main_source = r#"
198import "buttons.fd" as btn
199rect @hero { w: 200 h: 100 }
200"#;
201
202 let mut graph = parse_document(main_source).unwrap();
203 let loader = MemoryLoader {
204 files: HashMap::from([("buttons.fd".to_string(), imported_source.to_string())]),
205 };
206
207 resolve_imports(&mut graph, &loader).unwrap();
208
209 assert!(graph.get_by_id(NodeId::intern("hero")).is_some());
211
212 assert!(graph.get_by_id(NodeId::intern("btn.button")).is_some());
214
215 assert!(graph.styles.contains_key(&NodeId::intern("btn.accent")));
217 }
218
219 #[test]
220 fn resolve_circular_import_error() {
221 let file_a = "import \"b.fd\" as b\n";
222 let file_b = "import \"a.fd\" as a\n";
223
224 let mut graph = parse_document(file_a).unwrap();
225 let loader = MemoryLoader {
226 files: HashMap::from([
227 ("b.fd".to_string(), file_b.to_string()),
228 ("a.fd".to_string(), file_a.to_string()),
229 ]),
230 };
231
232 let result = resolve_imports(&mut graph, &loader);
233 assert!(result.is_err());
234 assert!(result.unwrap_err().contains("Circular import"));
235 }
236
237 #[test]
238 fn resolve_file_not_found_error() {
239 let main_source = "import \"missing.fd\" as m\n";
240 let mut graph = parse_document(main_source).unwrap();
241 let loader = MemoryLoader {
242 files: HashMap::new(),
243 };
244
245 let result = resolve_imports(&mut graph, &loader);
246 assert!(result.is_err());
247 assert!(result.unwrap_err().contains("File not found"));
248 }
249
250 #[test]
251 fn resolve_nested_imports() {
252 let tokens = "style primary { fill: #3B82F6 }\n";
253 let buttons = "import \"tokens.fd\" as tok\nrect @btn { w: 80 h: 32 }\n";
254 let main_source = "import \"buttons.fd\" as ui\n";
255
256 let mut graph = parse_document(main_source).unwrap();
257 let loader = MemoryLoader {
258 files: HashMap::from([
259 ("buttons.fd".to_string(), buttons.to_string()),
260 ("tokens.fd".to_string(), tokens.to_string()),
261 ]),
262 };
263
264 resolve_imports(&mut graph, &loader).unwrap();
265
266 assert!(graph.get_by_id(NodeId::intern("ui.btn")).is_some());
268
269 assert!(graph.styles.contains_key(&NodeId::intern("ui.tok.primary")));
271 }
272
273 #[test]
274 fn resolve_imported_edges() {
275 let imported = r#"
276rect @a { w: 10 h: 10 }
277rect @b { w: 10 h: 10 }
278edge @link { from: @a; to: @b; arrow: end }
279"#;
280 let main_source = "import \"flow.fd\" as flow\n";
281
282 let mut graph = parse_document(main_source).unwrap();
283 let loader = MemoryLoader {
284 files: HashMap::from([("flow.fd".to_string(), imported.to_string())]),
285 };
286
287 resolve_imports(&mut graph, &loader).unwrap();
288
289 assert_eq!(graph.edges.len(), 1);
290 let edge = &graph.edges[0];
291 assert_eq!(edge.id.as_str(), "flow.link");
292 assert_eq!(edge.from.as_str(), "flow.a");
293 assert_eq!(edge.to.as_str(), "flow.b");
294 }
295}