1use super::Element;
2use indexmap::IndexMap;
3use std::collections::HashSet;
4use std::sync::Arc;
5
6pub const DEFAULT_PRIMITIVES: &[&str] = &[
12 "Text", "Column", "Row", "Button", "Input", "Textarea", "Image", "Container", "Box",
13 "Center", "List", "Spacer", "Stack", "Divider", "Grid", "Card", "Heading", "Checkbox",
14 "Select", "Switch", "Slider", "Spinner", "Badge", "Avatar", "ProgressBar", "Video", "Audio",
15 "Paragraph",
16];
17
18pub struct ResolvedComponent {
21 pub source: String,
22 pub path: String,
23 pub passthrough: bool,
24 pub lazy: bool,
25}
26
27pub type ComponentResolver =
33 Arc<dyn Fn(&str, Option<&str>) -> Option<ResolvedComponent> + Send + Sync>;
34
35#[derive(Clone)]
37pub struct Component {
38 pub name: String,
40
41 pub template: Arc<dyn Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync>,
44
45 pub default_props: IndexMap<String, serde_json::Value>,
47
48 pub source_path: Option<String>,
50
51 pub passthrough: bool,
54
55 pub lazy: bool,
58}
59
60impl Component {
61 pub fn new(
62 name: impl Into<String>,
63 template: impl Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync + 'static,
64 ) -> Self {
65 Self {
66 name: name.into(),
67 template: Arc::new(template),
68 default_props: IndexMap::new(),
69 source_path: None,
70 passthrough: false,
71 lazy: false,
72 }
73 }
74
75 pub fn with_defaults(mut self, defaults: IndexMap<String, serde_json::Value>) -> Self {
76 self.default_props = defaults;
77 self
78 }
79
80 pub fn with_source_path(mut self, path: impl Into<String>) -> Self {
81 self.source_path = Some(path.into());
82 self
83 }
84
85 pub fn with_passthrough(mut self, passthrough: bool) -> Self {
86 self.passthrough = passthrough;
87 self
88 }
89
90 pub fn with_lazy(mut self, lazy: bool) -> Self {
91 self.lazy = lazy;
92 self
93 }
94
95 pub fn instantiate(&self, props: IndexMap<String, serde_json::Value>) -> Element {
97 let mut merged_props = self.default_props.clone();
98 merged_props.extend(props);
99 (self.template)(merged_props)
100 }
101}
102
103pub struct ComponentRegistry {
105 components: IndexMap<String, Component>,
107 resolver: Option<ComponentResolver>,
109 resolved_cache: IndexMap<String, bool>,
113 primitives: HashSet<String>,
117}
118
119impl ComponentRegistry {
120 pub fn new() -> Self {
121 Self {
122 components: IndexMap::new(),
123 resolver: None,
124 resolved_cache: IndexMap::new(),
125 primitives: HashSet::new(),
126 }
127 }
128
129 pub fn register_primitive(&mut self, name: &str) {
132 self.primitives.insert(name.to_string());
133 }
134
135 pub fn register_default_primitives(&mut self) {
141 for name in DEFAULT_PRIMITIVES {
142 self.register_primitive(name);
143 }
144 }
145
146 pub fn is_primitive(&self, name: &str) -> bool {
148 self.primitives.contains(name)
149 }
150
151 pub fn clear_resolved(&mut self) {
155 self.components.clear();
156 self.resolved_cache.clear();
157 }
158
159 pub fn set_resolver(&mut self, resolver: ComponentResolver) {
161 self.resolver = Some(resolver);
162 }
163
164 pub fn register(&mut self, component: Component) {
165 if let Some(ref path) = component.source_path {
167 let qualified_key = format!("{}:{}", path, component.name);
168 self.components.insert(qualified_key, component.clone());
169 }
170
171 self.components.insert(component.name.clone(), component);
173 }
174
175 pub fn get(&self, name: &str, context_path: Option<&str>) -> Option<&Component> {
177 if let Some(path) = context_path {
179 let qualified_key = format!("{}:{}", path, name);
180 if let Some(component) = self.components.get(&qualified_key) {
181 return Some(component);
182 }
183 }
184
185 self.components.get(name)
187 }
188
189 fn try_resolve(&mut self, name: &str, context_path: Option<&str>) -> bool {
191 if self.primitives.contains(name) {
193 return false;
194 }
195
196 let cache_key = if let Some(path) = context_path {
198 format!("{}:{}", path, name)
199 } else {
200 name.to_string()
201 };
202
203 if let Some(&cached) = self.resolved_cache.get(&cache_key) {
205 return cached;
206 }
207
208 if let Some(ref resolver) = self.resolver {
210 if let Some(resolved) = resolver(name, context_path) {
211 if resolved.lazy {
214 #[cfg(all(target_arch = "wasm32", feature = "js"))]
215 web_sys::console::log_1(
216 &format!("Registering lazy component: {}", name).into(),
217 );
218
219 let dummy_element = Element::new(name);
221 let component = Component::new(name, move |_props| dummy_element.clone())
222 .with_source_path(resolved.path.clone())
223 .with_lazy(true);
224
225 self.register(component);
226 self.resolved_cache.insert(cache_key, true);
227 return true;
228 }
229
230 if resolved.passthrough {
233 #[cfg(all(target_arch = "wasm32", feature = "js"))]
234 web_sys::console::log_1(
235 &format!("Registering passthrough component: {}", name).into(),
236 );
237
238 let dummy_element = Element::new(name);
240 let component = Component::new(name, move |_props| dummy_element.clone())
241 .with_source_path(resolved.path.clone())
242 .with_passthrough(true);
243
244 self.register(component);
245 self.resolved_cache.insert(cache_key, true);
246 return true;
247 }
248
249 match hypen_parser::parse_component(&resolved.source) {
251 Ok(component_spec) => {
252 let ir_node = super::expand::ast_to_ir_node(&component_spec);
254 let element = match ir_node {
255 super::IRNode::Element(e) => e,
256 _ => {
257 #[cfg(all(target_arch = "wasm32", feature = "js"))]
258 web_sys::console::error_1(
259 &format!("Component {} root must be an element", name).into(),
260 );
261 return false;
262 }
263 };
264
265 let component = Component::new(name, move |_props| element.clone())
267 .with_source_path(resolved.path.clone())
268 .with_passthrough(false);
269
270 self.register(component);
271 self.resolved_cache.insert(cache_key, true);
272 return true;
273 }
274 Err(e) => {
275 #[cfg(all(target_arch = "wasm32", feature = "js"))]
276 web_sys::console::error_1(
277 &format!("Failed to parse component {}: {:?}", name, e).into(),
278 );
279
280 #[cfg(not(all(target_arch = "wasm32", feature = "js")))]
281 eprintln!("Failed to parse component {}: {:?}", name, e);
282
283 self.resolved_cache.insert(cache_key, false);
284 return false;
285 }
286 }
287 }
288 }
289
290 self.resolved_cache.insert(cache_key, false);
291 false
292 }
293
294 pub fn expand(&mut self, element: &Element) -> Element {
295 self.expand_with_context(element, None)
296 }
297
298 pub fn expand_children(
301 &mut self,
302 element: &Element,
303 context_path: Option<&str>,
304 ) -> Vec<Element> {
305 element
306 .children
307 .iter()
308 .map(|child| self.expand_with_context(child, context_path))
309 .collect()
310 }
311
312 fn expand_with_context(&mut self, element: &Element, context_path: Option<&str>) -> Element {
314 let component_exists = self.get(&element.element_type, context_path).is_some();
316
317 if !component_exists {
318 self.try_resolve(&element.element_type, context_path);
320 }
321
322 if let Some(component) = self.get(&element.element_type, context_path) {
324 if component.lazy {
326 let mut element = element.clone();
328
329 element.props.insert(
331 "__lazy".to_string(),
332 super::Value::Static(serde_json::json!(true)),
333 );
334
335 #[cfg(all(target_arch = "wasm32", feature = "js"))]
336 web_sys::console::log_1(
337 &format!(
338 "Lazy {} (props: {:?}): {} children kept unexpanded",
339 element.element_type,
340 element.props.keys().collect::<Vec<_>>(),
341 element.children.len()
342 )
343 .into(),
344 );
345
346 return element;
347 }
348
349 if component.passthrough {
351 let mut element = element.clone();
353
354 #[cfg(all(target_arch = "wasm32", feature = "js"))]
355 {
356 let props_str = element
357 .props
358 .iter()
359 .map(|(k, v)| format!("{}={:?}", k, v))
360 .collect::<Vec<_>>()
361 .join(", ");
362 web_sys::console::log_1(
363 &format!(
364 "Passthrough {} (props: [{}]): {} children before expansion",
365 element.element_type,
366 props_str,
367 element.children.len()
368 )
369 .into(),
370 );
371 }
372
373 let child_context = component.source_path.clone();
375 let child_context_ref = child_context.as_deref();
376
377 element.children = element
379 .children
380 .into_iter()
381 .map(|child| Arc::new(self.expand_with_context(&child, child_context_ref)))
382 .collect();
383
384 #[cfg(all(target_arch = "wasm32", feature = "js"))]
385 web_sys::console::log_1(
386 &format!(
387 "Passthrough {}: {} children after expansion",
388 element.element_type,
389 element.children.len()
390 )
391 .into(),
392 );
393
394 element
395 } else {
396 let mut props = IndexMap::new();
399 for (k, v) in &element.props {
400 if let super::Value::Static(val) = v {
401 props.insert(k.clone(), val.clone());
402 }
403 }
404
405 let mut expanded = component.instantiate(props);
406
407 for (k, v) in &element.props {
409 match v {
410 super::Value::Binding(_) | super::Value::Action(_) => {
411 expanded.props.insert(k.clone(), v.clone());
412 }
413 _ => {}
414 }
415 }
416
417 let child_context = component.source_path.clone();
420
421 expanded.children = self.replace_children_slots(
423 &expanded.children,
424 &element.children,
425 context_path,
426 );
427
428 let child_context_ref = child_context.as_deref();
430 expanded.children = expanded
431 .children
432 .into_iter()
433 .map(|child| Arc::new(self.expand_with_context(&child, child_context_ref)))
434 .collect();
435 if !expanded.ir_children.is_empty() {
437 expanded.ir_children = expanded
438 .ir_children
439 .iter()
440 .map(|child| self.expand_ir_node_with_context(child, child_context_ref))
441 .collect();
442 }
443
444 expanded
445 }
446 } else {
447 let mut element = element.clone();
449 element.children = element
450 .children
451 .into_iter()
452 .map(|child| Arc::new(self.expand_with_context(&child, context_path)))
453 .collect();
454 if !element.ir_children.is_empty() {
456 element.ir_children = element
457 .ir_children
458 .iter()
459 .map(|child| self.expand_ir_node_with_context(child, context_path))
460 .collect();
461 }
462 element
463 }
464 }
465
466 pub fn expand_ir_node(&mut self, node: &super::IRNode) -> super::IRNode {
468 self.expand_ir_node_with_context(node, None)
469 }
470
471 fn expand_ir_node_with_context(
473 &mut self,
474 node: &super::IRNode,
475 context_path: Option<&str>,
476 ) -> super::IRNode {
477 match node {
478 super::IRNode::Element(element) => {
479 let mut expanded = self.expand_with_context(element, context_path);
481
482 if !expanded.ir_children.is_empty() {
484 expanded.ir_children = expanded
485 .ir_children
486 .iter()
487 .map(|child| self.expand_ir_node_with_context(child, context_path))
488 .collect();
489 }
490
491 super::IRNode::Element(expanded)
492 }
493 super::IRNode::ForEach {
494 source,
495 item_name,
496 key_path,
497 template,
498 props,
499 } => {
500 let expanded_template: Vec<super::IRNode> = template
502 .iter()
503 .map(|child| self.expand_ir_node_with_context(child, context_path))
504 .collect();
505
506 super::IRNode::ForEach {
507 source: source.clone(),
508 item_name: item_name.clone(),
509 key_path: key_path.clone(),
510 template: expanded_template,
511 props: props.clone(),
512 }
513 }
514 super::IRNode::Conditional {
515 value,
516 branches,
517 fallback,
518 } => {
519 let expanded_branches: Vec<super::ConditionalBranch> = branches
521 .iter()
522 .map(|branch| super::ConditionalBranch {
523 pattern: branch.pattern.clone(),
524 children: branch
525 .children
526 .iter()
527 .map(|child| self.expand_ir_node_with_context(child, context_path))
528 .collect(),
529 })
530 .collect();
531
532 let expanded_fallback = fallback.as_ref().map(|fb| {
534 fb.iter()
535 .map(|child| self.expand_ir_node_with_context(child, context_path))
536 .collect()
537 });
538
539 super::IRNode::Conditional {
540 value: value.clone(),
541 branches: expanded_branches,
542 fallback: expanded_fallback,
543 }
544 }
545 }
546 }
547
548 fn replace_children_slots(
551 &self,
552 template_children: &im::Vector<Arc<Element>>,
553 actual_children: &im::Vector<Arc<Element>>,
554 _context_path: Option<&str>,
555 ) -> im::Vector<Arc<Element>> {
556 let mut result = im::Vector::new();
557
558 for child in template_children {
559 if child.element_type == "Children" {
560 let slot_name = self.get_slot_name(&child.props);
563
564 if let Some(slot) = slot_name {
567 for c in actual_children.iter() {
568 if self.get_slot_name(&c.props) == Some(slot) {
569 result.push_back(Arc::clone(c));
570 }
571 }
572 } else {
573 for c in actual_children.iter() {
575 if self.get_slot_name(&c.props).is_none() {
576 result.push_back(Arc::clone(c));
577 }
578 }
579 }
580 } else {
581 let mut new_child = (**child).clone();
583 new_child.children =
584 self.replace_children_slots(&child.children, actual_children, _context_path);
585 result.push_back(Arc::new(new_child));
586 }
587 }
588
589 result
590 }
591
592 fn get_slot_name<'a>(&self, props: &'a super::Props) -> Option<&'a str> {
595 props.get("slot.0").and_then(|v| {
596 if let super::Value::Static(serde_json::Value::String(s)) = v {
597 Some(s.as_str())
598 } else {
599 None
600 }
601 })
602 }
603}
604
605impl Default for ComponentRegistry {
606 fn default() -> Self {
607 Self::new()
608 }
609}
610
611#[cfg(test)]
612mod tests {
613 use super::*;
614 use crate::ir::Value;
615
616 #[test]
617 fn test_dynamic_component_resolution() {
618 let mut registry = ComponentRegistry::new();
619
620 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
622 if name == "Header" {
623 Some(ResolvedComponent {
624 source: r#"Row { Text("Header") }"#.to_string(),
625 path: "/components/Header.hypen".to_string(),
626 passthrough: false,
627 lazy: false,
628 })
629 } else {
630 None
631 }
632 }));
633
634 let element = Element::new("Column").with_child(Element::new("Header"));
636
637 let expanded = registry.expand(&element);
639
640 assert_eq!(expanded.element_type, "Column");
642 assert_eq!(expanded.children.len(), 1);
643 let row = &expanded.children[0];
644 assert_eq!(row.element_type, "Row");
645 assert_eq!(row.ir_children.len(), 1);
647 match &row.ir_children[0] {
648 crate::ir::IRNode::Element(text) => assert_eq!(text.element_type, "Text"),
649 other => panic!("Expected Element, got {:?}", other),
650 }
651 }
652
653 #[test]
654 fn test_component_resolution_with_path_context() {
655 let mut registry = ComponentRegistry::new();
656
657 registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
659 match (name, context) {
660 ("Button", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
661 source: r#"Text("Home Button")"#.to_string(),
662 path: "/components/buttons/HomeButton.hypen".to_string(),
663 passthrough: false,
664 lazy: false,
665 }),
666 ("Button", Some("/pages/About.hypen")) => Some(ResolvedComponent {
667 source: r#"Text("About Button")"#.to_string(),
668 path: "/components/buttons/AboutButton.hypen".to_string(),
669 passthrough: false,
670 lazy: false,
671 }),
672 _ => None,
673 }
674 }));
675
676 let home_element =
678 Element::new("Text").with_prop("0", Value::Static(serde_json::json!("Home")));
679 let home_component = Component::new("Home", move |_| home_element.clone())
680 .with_source_path("/pages/Home.hypen");
681 registry.register(home_component);
682
683 let element = Element::new("Column")
685 .with_child(Element::new("Home").with_child(Element::new("Button")));
686
687 let expanded = registry.expand(&element);
688
689 assert_eq!(expanded.element_type, "Column");
691 }
692
693 #[test]
694 fn test_component_resolution_caching() {
695 let mut registry = ComponentRegistry::new();
696 let call_count = Arc::new(std::sync::Mutex::new(0));
697 let call_count_clone = call_count.clone();
698
699 registry.set_resolver(Arc::new(move |name: &str, _context: Option<&str>| {
701 if name == "Button" {
702 *call_count_clone.lock().unwrap() += 1;
703 Some(ResolvedComponent {
704 source: r#"Text("Click")"#.to_string(),
705 path: "/components/Button.hypen".to_string(),
706 passthrough: false,
707 lazy: false,
708 })
709 } else {
710 None
711 }
712 }));
713
714 let element1 = Element::new("Button");
716 let _ = registry.expand(&element1);
717 assert_eq!(*call_count.lock().unwrap(), 1);
718
719 let element2 = Element::new("Button");
721 let _ = registry.expand(&element2);
722 assert_eq!(*call_count.lock().unwrap(), 1); }
724
725 #[test]
726 fn test_failed_resolution_cached() {
727 let mut registry = ComponentRegistry::new();
728 let call_count = Arc::new(std::sync::Mutex::new(0));
729 let call_count_clone = call_count.clone();
730
731 registry.set_resolver(Arc::new(move |_name: &str, _context: Option<&str>| {
733 *call_count_clone.lock().unwrap() += 1;
734 None
735 }));
736
737 let element1 = Element::new("Unknown");
739 let _ = registry.expand(&element1);
740 assert_eq!(*call_count.lock().unwrap(), 1);
741
742 let element2 = Element::new("Unknown");
744 let _ = registry.expand(&element2);
745 assert_eq!(*call_count.lock().unwrap(), 1); }
747
748 #[test]
749 fn test_passthrough_component_preserves_props() {
750 let mut registry = ComponentRegistry::new();
751
752 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
754 if name == "Router" || name == "Route" {
755 Some(ResolvedComponent {
756 source: String::new(), path: name.to_string(),
758 passthrough: true,
759 lazy: false,
760 })
761 } else if name == "HomePage" {
762 Some(ResolvedComponent {
764 source: "Text(\"Home\")".to_string(),
765 path: name.to_string(),
766 passthrough: false,
767 lazy: false,
768 })
769 } else {
770 None
771 }
772 }));
773
774 let mut router = Element::new("Router");
780
781 let mut route1 = Element::new("Route");
782 route1
783 .props
784 .insert("0".to_string(), Value::Static(serde_json::json!("/")));
785 route1
786 .children
787 .push_back(std::sync::Arc::new(Element::new("HomePage")));
788
789 let mut route2 = Element::new("Route");
790 route2
791 .props
792 .insert("0".to_string(), Value::Static(serde_json::json!("/about")));
793 route2
794 .children
795 .push_back(std::sync::Arc::new(Element::new("HomePage")));
796
797 router.children.push_back(std::sync::Arc::new(route1));
798 router.children.push_back(std::sync::Arc::new(route2));
799
800 let expanded = registry.expand(&router);
802
803 assert_eq!(expanded.element_type, "Router");
805 assert_eq!(expanded.children.len(), 2);
806
807 let expanded_route1 = &expanded.children[0];
809 assert_eq!(expanded_route1.element_type, "Route");
810 if let Some(Value::Static(path)) = expanded_route1.props.get("0") {
811 assert_eq!(path.as_str().unwrap(), "/");
812 } else {
813 panic!("Route 1 missing path prop");
814 }
815
816 let expanded_route2 = &expanded.children[1];
818 assert_eq!(expanded_route2.element_type, "Route");
819 if let Some(Value::Static(path)) = expanded_route2.props.get("0") {
820 assert_eq!(path.as_str().unwrap(), "/about");
821 } else {
822 panic!("Route 2 missing path prop");
823 }
824
825 assert_eq!(expanded_route1.children.len(), 1);
827 assert_eq!(expanded_route1.children[0].element_type, "Text");
828 }
829
830 #[test]
831 fn test_bare_miss_does_not_block_context_resolve() {
832 let mut registry = ComponentRegistry::new();
835
836 registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
837 match (name, context) {
839 ("Header", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
840 source: r#"Text("Home Header")"#.to_string(),
841 path: "/components/Header.hypen".to_string(),
842 passthrough: false,
843 lazy: false,
844 }),
845 _ => None,
846 }
847 }));
848
849 let element = Element::new("Header");
851 let expanded = registry.expand(&element);
852 assert_eq!(expanded.element_type, "Header");
854
855 let element2 = Element::new("Header");
857 let expanded2 = registry.expand_with_context(&element2, Some("/pages/Home.hypen"));
858 assert_ne!(expanded2.element_type, "Header",
860 "Context-scoped resolve must not be blocked by prior bare-name miss");
861 }
862
863 #[test]
864 fn test_primitives_never_shadowed_by_resolver() {
865 let mut registry = ComponentRegistry::new();
868 registry.register_primitive("Text");
869
870 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
871 if name == "Text" {
872 Some(ResolvedComponent {
873 source: r#"Column { Text("Shadowed!") }"#.to_string(),
874 path: "/evil/Text.hypen".to_string(),
875 passthrough: false,
876 lazy: false,
877 })
878 } else {
879 None
880 }
881 }));
882
883 let element = Element::new("Text");
885 let expanded = registry.expand(&element);
886 assert_eq!(expanded.element_type, "Text");
887 assert!(expanded.children.is_empty());
888
889 let expanded2 = registry.expand_with_context(&element, Some("/some/path"));
891 assert_eq!(expanded2.element_type, "Text");
892 assert!(expanded2.children.is_empty());
893 }
894}