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 =
20 Arc<dyn Fn(&str, Option<&str>) -> Option<ResolvedComponent> + Send + Sync>;
21
22#[derive(Clone)]
24pub struct Component {
25 pub name: String,
27
28 pub template: Arc<dyn Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync>,
31
32 pub default_props: IndexMap<String, serde_json::Value>,
34
35 pub source_path: Option<String>,
37
38 pub passthrough: bool,
41
42 pub lazy: bool,
45}
46
47impl Component {
48 pub fn new(
49 name: impl Into<String>,
50 template: impl Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync + 'static,
51 ) -> Self {
52 Self {
53 name: name.into(),
54 template: Arc::new(template),
55 default_props: IndexMap::new(),
56 source_path: None,
57 passthrough: false,
58 lazy: false,
59 }
60 }
61
62 pub fn with_defaults(mut self, defaults: IndexMap<String, serde_json::Value>) -> Self {
63 self.default_props = defaults;
64 self
65 }
66
67 pub fn with_source_path(mut self, path: impl Into<String>) -> Self {
68 self.source_path = Some(path.into());
69 self
70 }
71
72 pub fn with_passthrough(mut self, passthrough: bool) -> Self {
73 self.passthrough = passthrough;
74 self
75 }
76
77 pub fn with_lazy(mut self, lazy: bool) -> Self {
78 self.lazy = lazy;
79 self
80 }
81
82 pub fn instantiate(&self, props: IndexMap<String, serde_json::Value>) -> Element {
84 let mut merged_props = self.default_props.clone();
85 merged_props.extend(props);
86 (self.template)(merged_props)
87 }
88}
89
90pub struct ComponentRegistry {
92 components: IndexMap<String, Component>,
94 resolver: Option<ComponentResolver>,
96 resolved_cache: IndexMap<String, bool>,
99}
100
101impl ComponentRegistry {
102 pub fn new() -> Self {
103 Self {
104 components: IndexMap::new(),
105 resolver: None,
106 resolved_cache: IndexMap::new(),
107 }
108 }
109
110 pub fn register_primitive(&mut self, name: &str) {
113 self.resolved_cache.insert(name.to_string(), false);
114 }
115
116 pub fn set_resolver(&mut self, resolver: ComponentResolver) {
118 self.resolver = Some(resolver);
119 }
120
121 pub fn register(&mut self, component: Component) {
122 if let Some(ref path) = component.source_path {
124 let qualified_key = format!("{}:{}", path, component.name);
125 self.components.insert(qualified_key, component.clone());
126 }
127
128 self.components.insert(component.name.clone(), component);
130 }
131
132 pub fn get(&self, name: &str, context_path: Option<&str>) -> Option<&Component> {
134 if let Some(path) = context_path {
136 let qualified_key = format!("{}:{}", path, name);
137 if let Some(component) = self.components.get(&qualified_key) {
138 return Some(component);
139 }
140 }
141
142 self.components.get(name)
144 }
145
146 fn try_resolve(&mut self, name: &str, context_path: Option<&str>) -> bool {
148 let cache_key = if let Some(path) = context_path {
150 format!("{}:{}", path, name)
151 } else {
152 name.to_string()
153 };
154
155 if let Some(&cached) = self.resolved_cache.get(&cache_key) {
157 return cached;
158 }
159
160 if let Some(ref resolver) = self.resolver {
162 if let Some(resolved) = resolver(name, context_path) {
163 if resolved.lazy {
166 #[cfg(all(target_arch = "wasm32", feature = "js"))]
167 web_sys::console::log_1(
168 &format!("Registering lazy component: {}", name).into(),
169 );
170
171 let dummy_element = Element::new(name);
173 let component = Component::new(name, move |_props| dummy_element.clone())
174 .with_source_path(resolved.path.clone())
175 .with_lazy(true);
176
177 self.register(component);
178 self.resolved_cache.insert(cache_key, true);
179 return true;
180 }
181
182 if resolved.passthrough {
185 #[cfg(all(target_arch = "wasm32", feature = "js"))]
186 web_sys::console::log_1(
187 &format!("Registering passthrough component: {}", name).into(),
188 );
189
190 let dummy_element = Element::new(name);
192 let component = Component::new(name, move |_props| dummy_element.clone())
193 .with_source_path(resolved.path.clone())
194 .with_passthrough(true);
195
196 self.register(component);
197 self.resolved_cache.insert(cache_key, true);
198 return true;
199 }
200
201 match hypen_parser::parse_component(&resolved.source) {
203 Ok(component_spec) => {
204 let ir_node = super::expand::ast_to_ir_node(&component_spec);
206 let element = match ir_node {
207 super::IRNode::Element(e) => e,
208 _ => {
209 #[cfg(all(target_arch = "wasm32", feature = "js"))]
210 web_sys::console::error_1(
211 &format!("Component {} root must be an element", name).into(),
212 );
213 return false;
214 }
215 };
216
217 let component = Component::new(name, move |_props| element.clone())
219 .with_source_path(resolved.path.clone())
220 .with_passthrough(false);
221
222 self.register(component);
223 self.resolved_cache.insert(cache_key, true);
224 return true;
225 }
226 Err(e) => {
227 #[cfg(all(target_arch = "wasm32", feature = "js"))]
228 web_sys::console::error_1(
229 &format!("Failed to parse component {}: {:?}", name, e).into(),
230 );
231
232 #[cfg(not(all(target_arch = "wasm32", feature = "js")))]
233 eprintln!("Failed to parse component {}: {:?}", name, e);
234
235 self.resolved_cache.insert(cache_key, false);
236 return false;
237 }
238 }
239 }
240 }
241
242 self.resolved_cache.insert(cache_key, false);
243 false
244 }
245
246 pub fn expand(&mut self, element: &Element) -> Element {
247 self.expand_with_context(element, None)
248 }
249
250 pub fn expand_children(
253 &mut self,
254 element: &Element,
255 context_path: Option<&str>,
256 ) -> Vec<Element> {
257 element
258 .children
259 .iter()
260 .map(|child| self.expand_with_context(child, context_path))
261 .collect()
262 }
263
264 fn expand_with_context(&mut self, element: &Element, context_path: Option<&str>) -> Element {
266 let component_exists = self.get(&element.element_type, context_path).is_some();
268
269 if !component_exists {
270 self.try_resolve(&element.element_type, context_path);
272 }
273
274 if let Some(component) = self.get(&element.element_type, context_path) {
276 if component.lazy {
278 let mut element = element.clone();
280
281 element.props.insert(
283 "__lazy".to_string(),
284 super::Value::Static(serde_json::json!(true)),
285 );
286
287 #[cfg(all(target_arch = "wasm32", feature = "js"))]
288 web_sys::console::log_1(
289 &format!(
290 "Lazy {} (props: {:?}): {} children kept unexpanded",
291 element.element_type,
292 element.props.keys().collect::<Vec<_>>(),
293 element.children.len()
294 )
295 .into(),
296 );
297
298 return element;
299 }
300
301 if component.passthrough {
303 let mut element = element.clone();
305
306 #[cfg(all(target_arch = "wasm32", feature = "js"))]
307 {
308 let props_str = element
309 .props
310 .iter()
311 .map(|(k, v)| format!("{}={:?}", k, v))
312 .collect::<Vec<_>>()
313 .join(", ");
314 web_sys::console::log_1(
315 &format!(
316 "Passthrough {} (props: [{}]): {} children before expansion",
317 element.element_type,
318 props_str,
319 element.children.len()
320 )
321 .into(),
322 );
323 }
324
325 let child_context = component.source_path.clone();
327 let child_context_ref = child_context.as_deref();
328
329 element.children = element
331 .children
332 .into_iter()
333 .map(|child| Arc::new(self.expand_with_context(&child, child_context_ref)))
334 .collect();
335
336 #[cfg(all(target_arch = "wasm32", feature = "js"))]
337 web_sys::console::log_1(
338 &format!(
339 "Passthrough {}: {} children after expansion",
340 element.element_type,
341 element.children.len()
342 )
343 .into(),
344 );
345
346 element
347 } else {
348 let mut props = IndexMap::new();
351 for (k, v) in &element.props {
352 if let super::Value::Static(val) = v {
353 props.insert(k.clone(), val.clone());
354 }
355 }
356
357 let mut expanded = component.instantiate(props);
358
359 for (k, v) in &element.props {
361 match v {
362 super::Value::Binding(_) | super::Value::Action(_) => {
363 expanded.props.insert(k.clone(), v.clone());
364 }
365 _ => {}
366 }
367 }
368
369 let child_context = component.source_path.clone();
372
373 expanded.children = self.replace_children_slots(
375 &expanded.children,
376 &element.children,
377 context_path,
378 );
379
380 let child_context_ref = child_context.as_deref();
382 expanded.children = expanded
383 .children
384 .into_iter()
385 .map(|child| Arc::new(self.expand_with_context(&child, child_context_ref)))
386 .collect();
387 if !expanded.ir_children.is_empty() {
389 expanded.ir_children = expanded
390 .ir_children
391 .iter()
392 .map(|child| self.expand_ir_node_with_context(child, child_context_ref))
393 .collect();
394 }
395
396 expanded
397 }
398 } else {
399 let mut element = element.clone();
401 element.children = element
402 .children
403 .into_iter()
404 .map(|child| Arc::new(self.expand_with_context(&child, context_path)))
405 .collect();
406 if !element.ir_children.is_empty() {
408 element.ir_children = element
409 .ir_children
410 .iter()
411 .map(|child| self.expand_ir_node_with_context(child, context_path))
412 .collect();
413 }
414 element
415 }
416 }
417
418 pub fn expand_ir_node(&mut self, node: &super::IRNode) -> super::IRNode {
420 self.expand_ir_node_with_context(node, None)
421 }
422
423 fn expand_ir_node_with_context(
425 &mut self,
426 node: &super::IRNode,
427 context_path: Option<&str>,
428 ) -> super::IRNode {
429 match node {
430 super::IRNode::Element(element) => {
431 let mut expanded = self.expand_with_context(element, context_path);
433
434 if !expanded.ir_children.is_empty() {
436 expanded.ir_children = expanded
437 .ir_children
438 .iter()
439 .map(|child| self.expand_ir_node_with_context(child, context_path))
440 .collect();
441 }
442
443 super::IRNode::Element(expanded)
444 }
445 super::IRNode::ForEach {
446 source,
447 item_name,
448 key_path,
449 template,
450 props,
451 } => {
452 let expanded_template: Vec<super::IRNode> = template
454 .iter()
455 .map(|child| self.expand_ir_node_with_context(child, context_path))
456 .collect();
457
458 super::IRNode::ForEach {
459 source: source.clone(),
460 item_name: item_name.clone(),
461 key_path: key_path.clone(),
462 template: expanded_template,
463 props: props.clone(),
464 }
465 }
466 super::IRNode::Conditional {
467 value,
468 branches,
469 fallback,
470 } => {
471 let expanded_branches: Vec<super::ConditionalBranch> = branches
473 .iter()
474 .map(|branch| super::ConditionalBranch {
475 pattern: branch.pattern.clone(),
476 children: branch
477 .children
478 .iter()
479 .map(|child| self.expand_ir_node_with_context(child, context_path))
480 .collect(),
481 })
482 .collect();
483
484 let expanded_fallback = fallback.as_ref().map(|fb| {
486 fb.iter()
487 .map(|child| self.expand_ir_node_with_context(child, context_path))
488 .collect()
489 });
490
491 super::IRNode::Conditional {
492 value: value.clone(),
493 branches: expanded_branches,
494 fallback: expanded_fallback,
495 }
496 }
497 }
498 }
499
500 fn replace_children_slots(
503 &self,
504 template_children: &im::Vector<Arc<Element>>,
505 actual_children: &im::Vector<Arc<Element>>,
506 _context_path: Option<&str>,
507 ) -> im::Vector<Arc<Element>> {
508 let mut result = im::Vector::new();
509
510 for child in template_children {
511 if child.element_type == "Children" {
512 let slot_name = self.get_slot_name(&child.props);
515
516 if let Some(slot) = slot_name {
519 for c in actual_children.iter() {
520 if self.get_slot_name(&c.props) == Some(slot) {
521 result.push_back(Arc::clone(c));
522 }
523 }
524 } else {
525 for c in actual_children.iter() {
527 if self.get_slot_name(&c.props).is_none() {
528 result.push_back(Arc::clone(c));
529 }
530 }
531 }
532 } else {
533 let mut new_child = (**child).clone();
535 new_child.children =
536 self.replace_children_slots(&child.children, actual_children, _context_path);
537 result.push_back(Arc::new(new_child));
538 }
539 }
540
541 result
542 }
543
544 fn get_slot_name<'a>(&self, props: &'a super::Props) -> Option<&'a str> {
547 props.get("slot.0").and_then(|v| {
548 if let super::Value::Static(serde_json::Value::String(s)) = v {
549 Some(s.as_str())
550 } else {
551 None
552 }
553 })
554 }
555}
556
557impl Default for ComponentRegistry {
558 fn default() -> Self {
559 Self::new()
560 }
561}
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use crate::ir::Value;
567
568 #[test]
569 fn test_dynamic_component_resolution() {
570 let mut registry = ComponentRegistry::new();
571
572 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
574 if name == "Header" {
575 Some(ResolvedComponent {
576 source: r#"Row { Text("Header") }"#.to_string(),
577 path: "/components/Header.hypen".to_string(),
578 passthrough: false,
579 lazy: false,
580 })
581 } else {
582 None
583 }
584 }));
585
586 let element = Element::new("Column").with_child(Element::new("Header"));
588
589 let expanded = registry.expand(&element);
591
592 assert_eq!(expanded.element_type, "Column");
594 assert_eq!(expanded.children.len(), 1);
595 let row = &expanded.children[0];
596 assert_eq!(row.element_type, "Row");
597 assert_eq!(row.ir_children.len(), 1);
599 match &row.ir_children[0] {
600 crate::ir::IRNode::Element(text) => assert_eq!(text.element_type, "Text"),
601 other => panic!("Expected Element, got {:?}", other),
602 }
603 }
604
605 #[test]
606 fn test_component_resolution_with_path_context() {
607 let mut registry = ComponentRegistry::new();
608
609 registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
611 match (name, context) {
612 ("Button", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
613 source: r#"Text("Home Button")"#.to_string(),
614 path: "/components/buttons/HomeButton.hypen".to_string(),
615 passthrough: false,
616 lazy: false,
617 }),
618 ("Button", Some("/pages/About.hypen")) => Some(ResolvedComponent {
619 source: r#"Text("About Button")"#.to_string(),
620 path: "/components/buttons/AboutButton.hypen".to_string(),
621 passthrough: false,
622 lazy: false,
623 }),
624 _ => None,
625 }
626 }));
627
628 let home_element =
630 Element::new("Text").with_prop("0", Value::Static(serde_json::json!("Home")));
631 let home_component = Component::new("Home", move |_| home_element.clone())
632 .with_source_path("/pages/Home.hypen");
633 registry.register(home_component);
634
635 let element = Element::new("Column")
637 .with_child(Element::new("Home").with_child(Element::new("Button")));
638
639 let expanded = registry.expand(&element);
640
641 assert_eq!(expanded.element_type, "Column");
643 }
644
645 #[test]
646 fn test_component_resolution_caching() {
647 let mut registry = ComponentRegistry::new();
648 let call_count = Arc::new(std::sync::Mutex::new(0));
649 let call_count_clone = call_count.clone();
650
651 registry.set_resolver(Arc::new(move |name: &str, _context: Option<&str>| {
653 if name == "Button" {
654 *call_count_clone.lock().unwrap() += 1;
655 Some(ResolvedComponent {
656 source: r#"Text("Click")"#.to_string(),
657 path: "/components/Button.hypen".to_string(),
658 passthrough: false,
659 lazy: false,
660 })
661 } else {
662 None
663 }
664 }));
665
666 let element1 = Element::new("Button");
668 let _ = registry.expand(&element1);
669 assert_eq!(*call_count.lock().unwrap(), 1);
670
671 let element2 = Element::new("Button");
673 let _ = registry.expand(&element2);
674 assert_eq!(*call_count.lock().unwrap(), 1); }
676
677 #[test]
678 fn test_failed_resolution_cached() {
679 let mut registry = ComponentRegistry::new();
680 let call_count = Arc::new(std::sync::Mutex::new(0));
681 let call_count_clone = call_count.clone();
682
683 registry.set_resolver(Arc::new(move |_name: &str, _context: Option<&str>| {
685 *call_count_clone.lock().unwrap() += 1;
686 None
687 }));
688
689 let element1 = Element::new("Unknown");
691 let _ = registry.expand(&element1);
692 assert_eq!(*call_count.lock().unwrap(), 1);
693
694 let element2 = Element::new("Unknown");
696 let _ = registry.expand(&element2);
697 assert_eq!(*call_count.lock().unwrap(), 1); }
699
700 #[test]
701 fn test_passthrough_component_preserves_props() {
702 let mut registry = ComponentRegistry::new();
703
704 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
706 if name == "Router" || name == "Route" {
707 Some(ResolvedComponent {
708 source: String::new(), path: name.to_string(),
710 passthrough: true,
711 lazy: false,
712 })
713 } else if name == "HomePage" {
714 Some(ResolvedComponent {
716 source: "Text(\"Home\")".to_string(),
717 path: name.to_string(),
718 passthrough: false,
719 lazy: false,
720 })
721 } else {
722 None
723 }
724 }));
725
726 let mut router = Element::new("Router");
732
733 let mut route1 = Element::new("Route");
734 route1
735 .props
736 .insert("0".to_string(), Value::Static(serde_json::json!("/")));
737 route1
738 .children
739 .push_back(std::sync::Arc::new(Element::new("HomePage")));
740
741 let mut route2 = Element::new("Route");
742 route2
743 .props
744 .insert("0".to_string(), Value::Static(serde_json::json!("/about")));
745 route2
746 .children
747 .push_back(std::sync::Arc::new(Element::new("HomePage")));
748
749 router.children.push_back(std::sync::Arc::new(route1));
750 router.children.push_back(std::sync::Arc::new(route2));
751
752 let expanded = registry.expand(&router);
754
755 assert_eq!(expanded.element_type, "Router");
757 assert_eq!(expanded.children.len(), 2);
758
759 let expanded_route1 = &expanded.children[0];
761 assert_eq!(expanded_route1.element_type, "Route");
762 if let Some(Value::Static(path)) = expanded_route1.props.get("0") {
763 assert_eq!(path.as_str().unwrap(), "/");
764 } else {
765 panic!("Route 1 missing path prop");
766 }
767
768 let expanded_route2 = &expanded.children[1];
770 assert_eq!(expanded_route2.element_type, "Route");
771 if let Some(Value::Static(path)) = expanded_route2.props.get("0") {
772 assert_eq!(path.as_str().unwrap(), "/about");
773 } else {
774 panic!("Route 2 missing path prop");
775 }
776
777 assert_eq!(expanded_route1.children.len(), 1);
779 assert_eq!(expanded_route1.children[0].element_type, "Text");
780 }
781}