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(all(target_arch = "wasm32", feature = "js"))]
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(all(target_arch = "wasm32", feature = "js"))]
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(all(target_arch = "wasm32", feature = "js"))]
213 web_sys::console::error_1(&format!("Failed to parse component {}: {:?}", name, e).into());
214
215 #[cfg(not(all(target_arch = "wasm32", feature = "js")))]
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(all(target_arch = "wasm32", feature = "js"))]
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(all(target_arch = "wasm32", feature = "js"))]
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| Arc::new(self.expand_with_context(&child, child_context_ref)))
300 .collect();
301
302 #[cfg(all(target_arch = "wasm32", feature = "js"))]
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| Arc::new(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| Arc::new(self.expand_with_context(&child, context_path)))
352 .collect();
353 element
354 }
355 }
356
357 pub fn expand_ir_node(&mut self, node: &super::IRNode) -> super::IRNode {
359 self.expand_ir_node_with_context(node, None)
360 }
361
362 fn expand_ir_node_with_context(&mut self, node: &super::IRNode, context_path: Option<&str>) -> super::IRNode {
364 match node {
365 super::IRNode::Element(element) => {
366 let expanded = self.expand_with_context(element, context_path);
368 super::IRNode::Element(expanded)
369 }
370 super::IRNode::ForEach { source, item_name, key_path, template, props } => {
371 let expanded_template: Vec<super::IRNode> = template
373 .iter()
374 .map(|child| self.expand_ir_node_with_context(child, context_path))
375 .collect();
376
377 super::IRNode::ForEach {
378 source: source.clone(),
379 item_name: item_name.clone(),
380 key_path: key_path.clone(),
381 template: expanded_template,
382 props: props.clone(),
383 }
384 }
385 super::IRNode::Conditional { value, branches, fallback } => {
386 let expanded_branches: Vec<super::ConditionalBranch> = branches
388 .iter()
389 .map(|branch| super::ConditionalBranch {
390 pattern: branch.pattern.clone(),
391 children: branch.children
392 .iter()
393 .map(|child| self.expand_ir_node_with_context(child, context_path))
394 .collect(),
395 })
396 .collect();
397
398 let expanded_fallback = fallback.as_ref().map(|fb| {
400 fb.iter()
401 .map(|child| self.expand_ir_node_with_context(child, context_path))
402 .collect()
403 });
404
405 super::IRNode::Conditional {
406 value: value.clone(),
407 branches: expanded_branches,
408 fallback: expanded_fallback,
409 }
410 }
411 }
412 }
413
414 fn replace_children_slots(
417 &self,
418 template_children: &im::Vector<Arc<Element>>,
419 actual_children: &im::Vector<Arc<Element>>,
420 _context_path: Option<&str>,
421 ) -> im::Vector<Arc<Element>> {
422 let mut result = im::Vector::new();
423
424 for child in template_children {
425 if child.element_type == "Children" {
426 let slot_name = self.get_slot_name(&child.props);
429
430 if let Some(slot) = slot_name {
433 for c in actual_children.iter() {
434 if self.get_slot_name(&c.props) == Some(slot) {
435 result.push_back(Arc::clone(c));
436 }
437 }
438 } else {
439 for c in actual_children.iter() {
441 if self.get_slot_name(&c.props).is_none() {
442 result.push_back(Arc::clone(c));
443 }
444 }
445 }
446 } else {
447 let mut new_child = (**child).clone();
449 new_child.children = self.replace_children_slots(&child.children, actual_children, _context_path);
450 result.push_back(Arc::new(new_child));
451 }
452 }
453
454 result
455 }
456
457 fn get_slot_name<'a>(&self, props: &'a super::Props) -> Option<&'a str> {
460 props.get("slot.0")
461 .and_then(|v| {
462 if let super::Value::Static(serde_json::Value::String(s)) = v {
463 Some(s.as_str())
464 } else {
465 None
466 }
467 })
468 }
469}
470
471impl Default for ComponentRegistry {
472 fn default() -> Self {
473 Self::new()
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480 use crate::ir::Value;
481
482 #[test]
483 fn test_dynamic_component_resolution() {
484 let mut registry = ComponentRegistry::new();
485
486 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
488 if name == "Header" {
489 Some(ResolvedComponent {
490 source: r#"Row { Text("Header") }"#.to_string(),
491 path: "/components/Header.hypen".to_string(),
492 passthrough: false,
493 lazy: false,
494 })
495 } else {
496 None
497 }
498 }));
499
500 let element = Element::new("Column")
502 .with_child(Element::new("Header"));
503
504 let expanded = registry.expand(&element);
506
507 assert_eq!(expanded.element_type, "Column");
509 assert_eq!(expanded.children.len(), 1);
510 assert_eq!(expanded.children[0].element_type, "Row");
511 assert_eq!(expanded.children[0].children[0].element_type, "Text");
512 }
513
514 #[test]
515 fn test_component_resolution_with_path_context() {
516 let mut registry = ComponentRegistry::new();
517
518 registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
520 match (name, context) {
521 ("Button", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
522 source: r#"Text("Home Button")"#.to_string(),
523 path: "/components/buttons/HomeButton.hypen".to_string(),
524 passthrough: false,
525 lazy: false,
526 }),
527 ("Button", Some("/pages/About.hypen")) => Some(ResolvedComponent {
528 source: r#"Text("About Button")"#.to_string(),
529 path: "/components/buttons/AboutButton.hypen".to_string(),
530 passthrough: false,
531 lazy: false,
532 }),
533 _ => None,
534 }
535 }));
536
537 let home_element = Element::new("Text").with_prop("0", Value::Static(serde_json::json!("Home")));
539 let home_component = Component::new("Home", move |_| home_element.clone())
540 .with_source_path("/pages/Home.hypen");
541 registry.register(home_component);
542
543 let element = Element::new("Column")
545 .with_child(Element::new("Home").with_child(Element::new("Button")));
546
547 let expanded = registry.expand(&element);
548
549 assert_eq!(expanded.element_type, "Column");
551 }
552
553 #[test]
554 fn test_component_resolution_caching() {
555 let mut registry = ComponentRegistry::new();
556 let call_count = Arc::new(std::sync::Mutex::new(0));
557 let call_count_clone = call_count.clone();
558
559 registry.set_resolver(Arc::new(move |name: &str, _context: Option<&str>| {
561 if name == "Button" {
562 *call_count_clone.lock().unwrap() += 1;
563 Some(ResolvedComponent {
564 source: r#"Text("Click")"#.to_string(),
565 path: "/components/Button.hypen".to_string(),
566 passthrough: false,
567 lazy: false,
568 })
569 } else {
570 None
571 }
572 }));
573
574 let element1 = Element::new("Button");
576 let _ = registry.expand(&element1);
577 assert_eq!(*call_count.lock().unwrap(), 1);
578
579 let element2 = Element::new("Button");
581 let _ = registry.expand(&element2);
582 assert_eq!(*call_count.lock().unwrap(), 1); }
584
585 #[test]
586 fn test_failed_resolution_cached() {
587 let mut registry = ComponentRegistry::new();
588 let call_count = Arc::new(std::sync::Mutex::new(0));
589 let call_count_clone = call_count.clone();
590
591 registry.set_resolver(Arc::new(move |_name: &str, _context: Option<&str>| {
593 *call_count_clone.lock().unwrap() += 1;
594 None
595 }));
596
597 let element1 = Element::new("Unknown");
599 let _ = registry.expand(&element1);
600 assert_eq!(*call_count.lock().unwrap(), 1);
601
602 let element2 = Element::new("Unknown");
604 let _ = registry.expand(&element2);
605 assert_eq!(*call_count.lock().unwrap(), 1); }
607
608 #[test]
609 fn test_passthrough_component_preserves_props() {
610 let mut registry = ComponentRegistry::new();
611
612 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
614 if name == "Router" || name == "Route" {
615 Some(ResolvedComponent {
616 source: String::new(), path: name.to_string(),
618 passthrough: true,
619 lazy: false,
620 })
621 } else if name == "HomePage" {
622 Some(ResolvedComponent {
624 source: "Text(\"Home\")".to_string(),
625 path: name.to_string(),
626 passthrough: false,
627 lazy: false,
628 })
629 } else {
630 None
631 }
632 }));
633
634 let mut router = Element::new("Router");
640
641 let mut route1 = Element::new("Route");
642 route1.props.insert("0".to_string(), Value::Static(serde_json::json!("/")));
643 route1.children.push_back(std::sync::Arc::new(Element::new("HomePage")));
644
645 let mut route2 = Element::new("Route");
646 route2.props.insert("0".to_string(), Value::Static(serde_json::json!("/about")));
647 route2.children.push_back(std::sync::Arc::new(Element::new("HomePage")));
648
649 router.children.push_back(std::sync::Arc::new(route1));
650 router.children.push_back(std::sync::Arc::new(route2));
651
652 let expanded = registry.expand(&router);
654
655 assert_eq!(expanded.element_type, "Router");
657 assert_eq!(expanded.children.len(), 2);
658
659 let expanded_route1 = &expanded.children[0];
661 assert_eq!(expanded_route1.element_type, "Route");
662 if let Some(Value::Static(path)) = expanded_route1.props.get("0") {
663 assert_eq!(path.as_str().unwrap(), "/");
664 } else {
665 panic!("Route 1 missing path prop");
666 }
667
668 let expanded_route2 = &expanded.children[1];
670 assert_eq!(expanded_route2.element_type, "Route");
671 if let Some(Value::Static(path)) = expanded_route2.props.get("0") {
672 assert_eq!(path.as_str().unwrap(), "/about");
673 } else {
674 panic!("Route 2 missing path prop");
675 }
676
677 assert_eq!(expanded_route1.children.len(), 1);
679 assert_eq!(expanded_route1.children[0].element_type, "Text");
680 }
681}