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_edge_anchor(&cloned.from, namespace);
155 cloned.to = prefix_edge_anchor(&cloned.to, namespace);
156 if let Some(ref mut tc) = cloned.text_child {
157 *tc = prefix_node_id(tc, namespace);
158 }
159 for use_ref in &mut cloned.use_styles {
160 *use_ref = prefix_node_id(use_ref, namespace);
161 }
162 graph.edges.push(cloned);
163 }
164}
165
166fn prefix_node_id(id: &NodeId, namespace: &str) -> NodeId {
169 NodeId::intern(&format!("{namespace}.{}", id.as_str()))
170}
171
172fn prefix_edge_anchor(
175 anchor: &crate::model::EdgeAnchor,
176 namespace: &str,
177) -> crate::model::EdgeAnchor {
178 match anchor {
179 crate::model::EdgeAnchor::Node(id) => {
180 crate::model::EdgeAnchor::Node(prefix_node_id(id, namespace))
181 }
182 point @ crate::model::EdgeAnchor::Point(_, _) => point.clone(),
183 }
184}
185
186#[cfg(test)]
189mod tests {
190 use super::*;
191 use std::collections::HashMap;
192
193 struct MemoryLoader {
195 files: HashMap<String, String>,
196 }
197
198 impl ImportLoader for MemoryLoader {
199 fn load(&self, path: &str) -> Result<String, String> {
200 self.files
201 .get(path)
202 .cloned()
203 .ok_or_else(|| format!("File not found: {path}"))
204 }
205 }
206
207 #[test]
208 fn resolve_namespace_prefixing() {
209 let imported_source = r#"
210style accent { fill: #6C5CE7 }
211rect @button { w: 100 h: 40; fill: #FF0000 }
212"#;
213
214 let main_source = r#"
215import "buttons.fd" as btn
216rect @hero { w: 200 h: 100 }
217"#;
218
219 let mut graph = parse_document(main_source).unwrap();
220 let loader = MemoryLoader {
221 files: HashMap::from([("buttons.fd".to_string(), imported_source.to_string())]),
222 };
223
224 resolve_imports(&mut graph, &loader).unwrap();
225
226 assert!(graph.get_by_id(NodeId::intern("hero")).is_some());
228
229 assert!(graph.get_by_id(NodeId::intern("btn.button")).is_some());
231
232 assert!(graph.styles.contains_key(&NodeId::intern("btn.accent")));
234 }
235
236 #[test]
237 fn resolve_circular_import_error() {
238 let file_a = "import \"b.fd\" as b\n";
239 let file_b = "import \"a.fd\" as a\n";
240
241 let mut graph = parse_document(file_a).unwrap();
242 let loader = MemoryLoader {
243 files: HashMap::from([
244 ("b.fd".to_string(), file_b.to_string()),
245 ("a.fd".to_string(), file_a.to_string()),
246 ]),
247 };
248
249 let result = resolve_imports(&mut graph, &loader);
250 assert!(result.is_err());
251 assert!(result.unwrap_err().contains("Circular import"));
252 }
253
254 #[test]
255 fn resolve_file_not_found_error() {
256 let main_source = "import \"missing.fd\" as m\n";
257 let mut graph = parse_document(main_source).unwrap();
258 let loader = MemoryLoader {
259 files: HashMap::new(),
260 };
261
262 let result = resolve_imports(&mut graph, &loader);
263 assert!(result.is_err());
264 assert!(result.unwrap_err().contains("File not found"));
265 }
266
267 #[test]
268 fn resolve_nested_imports() {
269 let tokens = "style primary { fill: #3B82F6 }\n";
270 let buttons = "import \"tokens.fd\" as tok\nrect @btn { w: 80 h: 32 }\n";
271 let main_source = "import \"buttons.fd\" as ui\n";
272
273 let mut graph = parse_document(main_source).unwrap();
274 let loader = MemoryLoader {
275 files: HashMap::from([
276 ("buttons.fd".to_string(), buttons.to_string()),
277 ("tokens.fd".to_string(), tokens.to_string()),
278 ]),
279 };
280
281 resolve_imports(&mut graph, &loader).unwrap();
282
283 assert!(graph.get_by_id(NodeId::intern("ui.btn")).is_some());
285
286 assert!(graph.styles.contains_key(&NodeId::intern("ui.tok.primary")));
288 }
289
290 #[test]
291 fn resolve_imported_edges() {
292 let imported = r#"
293rect @a { w: 10 h: 10 }
294rect @b { w: 10 h: 10 }
295edge @link { from: @a; to: @b; arrow: end }
296"#;
297 let main_source = "import \"flow.fd\" as flow\n";
298
299 let mut graph = parse_document(main_source).unwrap();
300 let loader = MemoryLoader {
301 files: HashMap::from([("flow.fd".to_string(), imported.to_string())]),
302 };
303
304 resolve_imports(&mut graph, &loader).unwrap();
305
306 assert_eq!(graph.edges.len(), 1);
307 let edge = &graph.edges[0];
308 assert_eq!(edge.id.as_str(), "flow.link");
309 assert_eq!(
310 edge.from,
311 crate::model::EdgeAnchor::Node(NodeId::intern("flow.a"))
312 );
313 assert_eq!(
314 edge.to,
315 crate::model::EdgeAnchor::Node(NodeId::intern("flow.b"))
316 );
317 }
318}