1use super::Element;
2use indexmap::IndexMap;
3use std::sync::Arc;
4
5pub struct ResolvedComponent {
8 pub source: String,
9 pub path: String,
10 pub passthrough: bool,
11 pub lazy: bool,
12}
13
14pub type ComponentResolver = Arc<dyn Fn(&str, Option<&str>) -> Option<ResolvedComponent> + Send + Sync>;
20
21#[derive(Clone)]
23pub struct Component {
24 pub name: String,
26
27 pub template: Arc<dyn Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync>,
30
31 pub default_props: IndexMap<String, serde_json::Value>,
33
34 pub source_path: Option<String>,
36
37 pub passthrough: bool,
40
41 pub lazy: bool,
44}
45
46impl Component {
47 pub fn new(
48 name: impl Into<String>,
49 template: impl Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync + 'static,
50 ) -> Self {
51 Self {
52 name: name.into(),
53 template: Arc::new(template),
54 default_props: IndexMap::new(),
55 source_path: None,
56 passthrough: false,
57 lazy: false,
58 }
59 }
60
61 pub fn with_defaults(mut self, defaults: IndexMap<String, serde_json::Value>) -> Self {
62 self.default_props = defaults;
63 self
64 }
65
66 pub fn with_source_path(mut self, path: impl Into<String>) -> Self {
67 self.source_path = Some(path.into());
68 self
69 }
70
71 pub fn with_passthrough(mut self, passthrough: bool) -> Self {
72 self.passthrough = passthrough;
73 self
74 }
75
76 pub fn with_lazy(mut self, lazy: bool) -> Self {
77 self.lazy = lazy;
78 self
79 }
80
81 pub fn instantiate(&self, props: IndexMap<String, serde_json::Value>) -> Element {
83 let mut merged_props = self.default_props.clone();
84 merged_props.extend(props);
85 (self.template)(merged_props)
86 }
87}
88
89pub struct ComponentRegistry {
91 components: IndexMap<String, Component>,
93 resolver: Option<ComponentResolver>,
95 resolved_cache: IndexMap<String, bool>,
98}
99
100impl ComponentRegistry {
101 pub fn new() -> Self {
102 Self {
103 components: IndexMap::new(),
104 resolver: None,
105 resolved_cache: IndexMap::new(),
106 }
107 }
108
109 pub fn register_primitive(&mut self, name: &str) {
112 self.resolved_cache.insert(name.to_string(), false);
113 }
114
115 pub fn set_resolver(&mut self, resolver: ComponentResolver) {
117 self.resolver = Some(resolver);
118 }
119
120 pub fn register(&mut self, component: Component) {
121 if let Some(ref path) = component.source_path {
123 let qualified_key = format!("{}:{}", path, component.name);
124 self.components.insert(qualified_key, component.clone());
125 }
126
127 self.components.insert(component.name.clone(), component);
129 }
130
131 pub fn get(&self, name: &str, context_path: Option<&str>) -> Option<&Component> {
133 if let Some(path) = context_path {
135 let qualified_key = format!("{}:{}", path, name);
136 if let Some(component) = self.components.get(&qualified_key) {
137 return Some(component);
138 }
139 }
140
141 self.components.get(name)
143 }
144
145 fn try_resolve(&mut self, name: &str, context_path: Option<&str>) -> bool {
147 let cache_key = if let Some(path) = context_path {
149 format!("{}:{}", path, name)
150 } else {
151 name.to_string()
152 };
153
154 if let Some(&cached) = self.resolved_cache.get(&cache_key) {
156 return cached;
157 }
158
159 if let Some(ref resolver) = self.resolver {
161 if let Some(resolved) = resolver(name, context_path) {
162 if resolved.lazy {
165 #[cfg(target_arch = "wasm32")]
166 web_sys::console::log_1(&format!("📦 Registering lazy component: {}", name).into());
167
168 let dummy_element = Element::new(name);
170 let component = Component::new(name, move |_props| dummy_element.clone())
171 .with_source_path(resolved.path.clone())
172 .with_lazy(true);
173
174 self.register(component);
175 self.resolved_cache.insert(cache_key, true);
176 return true;
177 }
178
179 if resolved.passthrough {
182 #[cfg(target_arch = "wasm32")]
183 web_sys::console::log_1(&format!("📦 Registering passthrough component: {}", name).into());
184
185 let dummy_element = Element::new(name);
187 let component = Component::new(name, move |_props| dummy_element.clone())
188 .with_source_path(resolved.path.clone())
189 .with_passthrough(true);
190
191 self.register(component);
192 self.resolved_cache.insert(cache_key, true);
193 return true;
194 }
195
196 match hypen_parser::parse_component(&resolved.source) {
198 Ok(component_spec) => {
199 let element = super::expand::ast_to_ir(&component_spec);
201
202 let component = Component::new(name, move |_props| element.clone())
204 .with_source_path(resolved.path.clone())
205 .with_passthrough(false);
206
207 self.register(component);
208 self.resolved_cache.insert(cache_key, true);
209 return true;
210 }
211 Err(e) => {
212 #[cfg(target_arch = "wasm32")]
213 web_sys::console::error_1(&format!("Failed to parse component {}: {:?}", name, e).into());
214
215 #[cfg(not(target_arch = "wasm32"))]
216 eprintln!("Failed to parse component {}: {:?}", name, e);
217
218 self.resolved_cache.insert(cache_key, false);
219 return false;
220 }
221 }
222 }
223 }
224
225 self.resolved_cache.insert(cache_key, false);
226 false
227 }
228
229 pub fn expand(&mut self, element: &Element) -> Element {
230 self.expand_with_context(element, None)
231 }
232
233 pub fn expand_children(&mut self, element: &Element, context_path: Option<&str>) -> Vec<Element> {
236 element.children.iter()
237 .map(|child| self.expand_with_context(child, context_path))
238 .collect()
239 }
240
241 fn expand_with_context(&mut self, element: &Element, context_path: Option<&str>) -> Element {
243 let component_exists = self.get(&element.element_type, context_path).is_some();
245
246 if !component_exists {
247 self.try_resolve(&element.element_type, context_path);
249 }
250
251 if let Some(component) = self.get(&element.element_type, context_path) {
253 if component.lazy {
255 let mut element = element.clone();
257
258 element.props.insert("__lazy".to_string(), super::Value::Static(serde_json::json!(true)));
260
261 #[cfg(target_arch = "wasm32")]
262 web_sys::console::log_1(&format!(
263 "💤 Lazy {} (props: {:?}): {} children kept unexpanded",
264 element.element_type,
265 element.props.keys().collect::<Vec<_>>(),
266 element.children.len()
267 ).into());
268
269 return element;
270 }
271
272 if component.passthrough {
274 let mut element = element.clone();
276
277 #[cfg(target_arch = "wasm32")]
278 {
279 let props_str = element.props.iter()
280 .map(|(k, v)| format!("{}={:?}", k, v))
281 .collect::<Vec<_>>()
282 .join(", ");
283 web_sys::console::log_1(&format!(
284 "🔄 Passthrough {} (props: [{}]): {} children before expansion",
285 element.element_type,
286 props_str,
287 element.children.len()
288 ).into());
289 }
290
291 let child_context = component.source_path.clone();
293 let child_context_ref = child_context.as_deref();
294
295 element.children = element
297 .children
298 .into_iter()
299 .map(|child| self.expand_with_context(&child, child_context_ref))
300 .collect();
301
302 #[cfg(target_arch = "wasm32")]
303 web_sys::console::log_1(&format!("🔄 Passthrough {}: {} children after expansion", element.element_type, element.children.len()).into());
304
305 element
306 } else {
307 let mut props = IndexMap::new();
310 for (k, v) in &element.props {
311 if let super::Value::Static(val) = v {
312 props.insert(k.clone(), val.clone());
313 }
314 }
315
316 let mut expanded = component.instantiate(props);
317
318 for (k, v) in &element.props {
320 match v {
321 super::Value::Binding(_) | super::Value::Action(_) => {
322 expanded.props.insert(k.clone(), v.clone());
323 }
324 _ => {}
325 }
326 }
327
328 let child_context = component.source_path.clone();
331
332 expanded.children = self.replace_children_slots(&expanded.children, &element.children, context_path);
334
335 let child_context_ref = child_context.as_deref();
337 expanded.children = expanded
338 .children
339 .into_iter()
340 .map(|child| self.expand_with_context(&child, child_context_ref))
341 .collect();
342
343 expanded
344 }
345 } else {
346 let mut element = element.clone();
348 element.children = element
349 .children
350 .into_iter()
351 .map(|child| self.expand_with_context(&child, context_path))
352 .collect();
353 element
354 }
355 }
356
357 fn replace_children_slots(&self, template_children: &[Element], actual_children: &[Element], _context_path: Option<&str>) -> Vec<Element> {
360 let mut result = Vec::new();
361
362 for child in template_children {
363 if child.element_type == "Children" {
364 let slot_name = self.get_slot_name(&child.props);
367
368 let children_to_insert: Vec<Element> = if let Some(slot) = slot_name {
371 actual_children.iter()
372 .filter(|c| {
373 self.get_slot_name(&c.props)
374 .map_or(false, |s| s == slot)
375 })
376 .cloned()
377 .collect()
378 } else {
379 actual_children.iter()
381 .filter(|c| self.get_slot_name(&c.props).is_none())
382 .cloned()
383 .collect()
384 };
385
386 result.extend(children_to_insert);
387 } else {
388 let mut new_child = child.clone();
390 new_child.children = self.replace_children_slots(&child.children, actual_children, _context_path);
391 result.push(new_child);
392 }
393 }
394
395 result
396 }
397
398 fn get_slot_name<'a>(&self, props: &'a super::Props) -> Option<&'a str> {
401 props.get("slot.0")
402 .and_then(|v| {
403 if let super::Value::Static(serde_json::Value::String(s)) = v {
404 Some(s.as_str())
405 } else {
406 None
407 }
408 })
409 }
410}
411
412impl Default for ComponentRegistry {
413 fn default() -> Self {
414 Self::new()
415 }
416}
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421 use crate::ir::Value;
422
423 #[test]
424 fn test_dynamic_component_resolution() {
425 let mut registry = ComponentRegistry::new();
426
427 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
429 if name == "Header" {
430 Some(ResolvedComponent {
431 source: r#"Row { Text("Header") }"#.to_string(),
432 path: "/components/Header.hypen".to_string(),
433 passthrough: false,
434 lazy: false,
435 })
436 } else {
437 None
438 }
439 }));
440
441 let element = Element::new("Column")
443 .with_child(Element::new("Header"));
444
445 let expanded = registry.expand(&element);
447
448 assert_eq!(expanded.element_type, "Column");
450 assert_eq!(expanded.children.len(), 1);
451 assert_eq!(expanded.children[0].element_type, "Row");
452 assert_eq!(expanded.children[0].children[0].element_type, "Text");
453 }
454
455 #[test]
456 fn test_component_resolution_with_path_context() {
457 let mut registry = ComponentRegistry::new();
458
459 registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
461 match (name, context) {
462 ("Button", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
463 source: r#"Text("Home Button")"#.to_string(),
464 path: "/components/buttons/HomeButton.hypen".to_string(),
465 passthrough: false,
466 lazy: false,
467 }),
468 ("Button", Some("/pages/About.hypen")) => Some(ResolvedComponent {
469 source: r#"Text("About Button")"#.to_string(),
470 path: "/components/buttons/AboutButton.hypen".to_string(),
471 passthrough: false,
472 lazy: false,
473 }),
474 _ => None,
475 }
476 }));
477
478 let home_element = Element::new("Text").with_prop("0", Value::Static(serde_json::json!("Home")));
480 let home_component = Component::new("Home", move |_| home_element.clone())
481 .with_source_path("/pages/Home.hypen");
482 registry.register(home_component);
483
484 let element = Element::new("Column")
486 .with_child(Element::new("Home").with_child(Element::new("Button")));
487
488 let expanded = registry.expand(&element);
489
490 assert_eq!(expanded.element_type, "Column");
492 }
493
494 #[test]
495 fn test_component_resolution_caching() {
496 let mut registry = ComponentRegistry::new();
497 let call_count = Arc::new(std::sync::Mutex::new(0));
498 let call_count_clone = call_count.clone();
499
500 registry.set_resolver(Arc::new(move |name: &str, _context: Option<&str>| {
502 if name == "Button" {
503 *call_count_clone.lock().unwrap() += 1;
504 Some(ResolvedComponent {
505 source: r#"Text("Click")"#.to_string(),
506 path: "/components/Button.hypen".to_string(),
507 passthrough: false,
508 lazy: false,
509 })
510 } else {
511 None
512 }
513 }));
514
515 let element1 = Element::new("Button");
517 let _ = registry.expand(&element1);
518 assert_eq!(*call_count.lock().unwrap(), 1);
519
520 let element2 = Element::new("Button");
522 let _ = registry.expand(&element2);
523 assert_eq!(*call_count.lock().unwrap(), 1); }
525
526 #[test]
527 fn test_failed_resolution_cached() {
528 let mut registry = ComponentRegistry::new();
529 let call_count = Arc::new(std::sync::Mutex::new(0));
530 let call_count_clone = call_count.clone();
531
532 registry.set_resolver(Arc::new(move |_name: &str, _context: Option<&str>| {
534 *call_count_clone.lock().unwrap() += 1;
535 None
536 }));
537
538 let element1 = Element::new("Unknown");
540 let _ = registry.expand(&element1);
541 assert_eq!(*call_count.lock().unwrap(), 1);
542
543 let element2 = Element::new("Unknown");
545 let _ = registry.expand(&element2);
546 assert_eq!(*call_count.lock().unwrap(), 1); }
548
549 #[test]
550 fn test_passthrough_component_preserves_props() {
551 let mut registry = ComponentRegistry::new();
552
553 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
555 if name == "Router" || name == "Route" {
556 Some(ResolvedComponent {
557 source: String::new(), path: name.to_string(),
559 passthrough: true,
560 lazy: false,
561 })
562 } else if name == "HomePage" {
563 Some(ResolvedComponent {
565 source: "Text(\"Home\")".to_string(),
566 path: name.to_string(),
567 passthrough: false,
568 lazy: false,
569 })
570 } else {
571 None
572 }
573 }));
574
575 let mut router = Element::new("Router");
581
582 let mut route1 = Element::new("Route");
583 route1.props.insert("0".to_string(), Value::Static(serde_json::json!("/")));
584 route1.children.push(Element::new("HomePage"));
585
586 let mut route2 = Element::new("Route");
587 route2.props.insert("0".to_string(), Value::Static(serde_json::json!("/about")));
588 route2.children.push(Element::new("HomePage"));
589
590 router.children.push(route1);
591 router.children.push(route2);
592
593 let expanded = registry.expand(&router);
595
596 assert_eq!(expanded.element_type, "Router");
598 assert_eq!(expanded.children.len(), 2);
599
600 let expanded_route1 = &expanded.children[0];
602 assert_eq!(expanded_route1.element_type, "Route");
603 if let Some(Value::Static(path)) = expanded_route1.props.get("0") {
604 assert_eq!(path.as_str().unwrap(), "/");
605 } else {
606 panic!("Route 1 missing path prop");
607 }
608
609 let expanded_route2 = &expanded.children[1];
611 assert_eq!(expanded_route2.element_type, "Route");
612 if let Some(Value::Static(path)) = expanded_route2.props.get("0") {
613 assert_eq!(path.as_str().unwrap(), "/about");
614 } else {
615 panic!("Route 2 missing path prop");
616 }
617
618 assert_eq!(expanded_route1.children.len(), 1);
620 assert_eq!(expanded_route1.children[0].element_type, "Text");
621 }
622}