1use super::Element;
2use indexmap::IndexMap;
3use std::collections::HashSet;
4use std::sync::Arc;
5
6pub const DEFAULT_PRIMITIVES: &[&str] = &[
12 "Text",
13 "Column",
14 "Row",
15 "Button",
16 "Input",
17 "Textarea",
18 "Image",
19 "Container",
20 "Box",
21 "Center",
22 "List",
23 "Spacer",
24 "Stack",
25 "Divider",
26 "Grid",
27 "Card",
28 "Heading",
29 "Checkbox",
30 "Select",
31 "Switch",
32 "Slider",
33 "Spinner",
34 "Badge",
35 "Avatar",
36 "ProgressBar",
37 "Video",
38 "Audio",
39 "Paragraph",
40 "Icon",
41];
42
43pub struct ResolvedComponent {
46 pub source: String,
47 pub path: String,
48 pub passthrough: bool,
49 pub lazy: bool,
50}
51
52pub type ComponentResolver =
58 Arc<dyn Fn(&str, Option<&str>) -> Option<ResolvedComponent> + Send + Sync>;
59
60#[derive(Clone)]
62pub struct Component {
63 pub name: String,
65
66 pub template: Arc<dyn Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync>,
69
70 pub default_props: IndexMap<String, serde_json::Value>,
72
73 pub source_path: Option<String>,
75
76 pub passthrough: bool,
79
80 pub lazy: bool,
83
84 pub is_module: bool,
88
89 pub module_name: Option<String>,
91}
92
93impl Component {
94 pub fn new(
95 name: impl Into<String>,
96 template: impl Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync + 'static,
97 ) -> Self {
98 Self {
99 name: name.into(),
100 template: Arc::new(template),
101 default_props: IndexMap::new(),
102 source_path: None,
103 passthrough: false,
104 lazy: false,
105 is_module: false,
106 module_name: None,
107 }
108 }
109
110 pub fn with_defaults(mut self, defaults: IndexMap<String, serde_json::Value>) -> Self {
111 self.default_props = defaults;
112 self
113 }
114
115 pub fn with_source_path(mut self, path: impl Into<String>) -> Self {
116 self.source_path = Some(path.into());
117 self
118 }
119
120 pub fn with_passthrough(mut self, passthrough: bool) -> Self {
121 self.passthrough = passthrough;
122 self
123 }
124
125 pub fn with_lazy(mut self, lazy: bool) -> Self {
126 self.lazy = lazy;
127 self
128 }
129
130 pub fn instantiate(&self, props: IndexMap<String, serde_json::Value>) -> Element {
132 let mut merged_props = self.default_props.clone();
133 merged_props.extend(props);
134 (self.template)(merged_props)
135 }
136}
137
138pub struct ComponentRegistry {
140 components: IndexMap<String, Component>,
142 resolver: Option<ComponentResolver>,
144 resolved_cache: IndexMap<String, bool>,
148 primitives: HashSet<String>,
152}
153
154impl ComponentRegistry {
155 pub fn new() -> Self {
156 Self {
157 components: IndexMap::new(),
158 resolver: None,
159 resolved_cache: IndexMap::new(),
160 primitives: HashSet::new(),
161 }
162 }
163
164 pub fn register_primitive(&mut self, name: &str) {
167 self.primitives.insert(name.to_string());
168 }
169
170 pub fn register_default_primitives(&mut self) {
176 for name in DEFAULT_PRIMITIVES {
177 self.register_primitive(name);
178 }
179 }
180
181 pub fn is_primitive(&self, name: &str) -> bool {
183 self.primitives.contains(name)
184 }
185
186 pub fn clear_resolved(&mut self) {
190 self.components.clear();
191 self.resolved_cache.clear();
192 }
193
194 pub fn set_resolver(&mut self, resolver: ComponentResolver) {
196 self.resolver = Some(resolver);
197 }
198
199 pub fn register(&mut self, component: Component) {
200 if let Some(ref path) = component.source_path {
202 let qualified_key = format!("{}:{}", path, component.name);
203 self.components.insert(qualified_key, component.clone());
204 }
205
206 self.components.insert(component.name.clone(), component);
208 }
209
210 pub fn get(&self, name: &str, context_path: Option<&str>) -> Option<&Component> {
212 if let Some(path) = context_path {
214 let qualified_key = format!("{}:{}", path, name);
215 if let Some(component) = self.components.get(&qualified_key) {
216 return Some(component);
217 }
218 }
219
220 self.components.get(name)
222 }
223
224 fn try_resolve(&mut self, name: &str, context_path: Option<&str>) -> bool {
226 if self.primitives.contains(name) {
228 return false;
229 }
230
231 let cache_key = if let Some(path) = context_path {
233 format!("{}:{}", path, name)
234 } else {
235 name.to_string()
236 };
237
238 if let Some(&cached) = self.resolved_cache.get(&cache_key) {
240 return cached;
241 }
242
243 if let Some(ref resolver) = self.resolver {
245 if let Some(resolved) = resolver(name, context_path) {
246 if resolved.lazy {
249 #[cfg(all(target_arch = "wasm32", feature = "js"))]
250 web_sys::console::log_1(
251 &format!("Registering lazy component: {}", name).into(),
252 );
253
254 let dummy_element = Element::new(name);
256 let component = Component::new(name, move |_props| dummy_element.clone())
257 .with_source_path(resolved.path.clone())
258 .with_lazy(true);
259
260 self.register(component);
261 self.resolved_cache.insert(cache_key, true);
262 return true;
263 }
264
265 if resolved.passthrough {
268 #[cfg(all(target_arch = "wasm32", feature = "js"))]
269 web_sys::console::log_1(
270 &format!("Registering passthrough component: {}", name).into(),
271 );
272
273 let dummy_element = Element::new(name);
275 let component = Component::new(name, move |_props| dummy_element.clone())
276 .with_source_path(resolved.path.clone())
277 .with_passthrough(true);
278
279 self.register(component);
280 self.resolved_cache.insert(cache_key, true);
281 return true;
282 }
283
284 match hypen_parser::parse_component(&resolved.source) {
286 Ok(component_spec) => {
287 let spec_is_module = component_spec.declaration_type
289 == hypen_parser::DeclarationType::Module;
290 let spec_module_name = if spec_is_module {
291 Some(component_spec.name.to_lowercase())
292 } else {
293 None
294 };
295
296 let ir_node = super::expand::ast_to_ir_node(&component_spec);
298 let element = match ir_node {
299 super::IRNode::Element(e) => e,
300 _ => {
301 #[cfg(all(target_arch = "wasm32", feature = "js"))]
302 web_sys::console::error_1(
303 &format!("Component {} root must be an element", name).into(),
304 );
305 return false;
306 }
307 };
308
309 let mut component = Component::new(name, move |_props| element.clone())
311 .with_source_path(resolved.path.clone())
312 .with_passthrough(false);
313
314 if spec_is_module {
316 component.is_module = true;
317 component.module_name = spec_module_name;
318 }
319
320 self.register(component);
321 self.resolved_cache.insert(cache_key, true);
322 return true;
323 }
324 Err(e) => {
325 #[cfg(all(target_arch = "wasm32", feature = "js"))]
326 web_sys::console::error_1(
327 &format!("Failed to parse component {}: {:?}", name, e).into(),
328 );
329
330 #[cfg(not(all(target_arch = "wasm32", feature = "js")))]
331 eprintln!("Failed to parse component {}: {:?}", name, e);
332
333 self.resolved_cache.insert(cache_key, false);
334 return false;
335 }
336 }
337 }
338 }
339
340 self.resolved_cache.insert(cache_key, false);
341 false
342 }
343
344 pub fn expand(&mut self, element: &Element) -> Element {
345 self.expand_with_context(element, None)
346 }
347
348 pub fn expand_children(
351 &mut self,
352 element: &Element,
353 context_path: Option<&str>,
354 ) -> Vec<Element> {
355 element
356 .ir_children
357 .iter()
358 .filter_map(|child_ir| {
359 if let super::IRNode::Element(child) = child_ir {
360 Some(self.expand_with_context(child, context_path))
361 } else {
362 None
363 }
364 })
365 .collect()
366 }
367
368 fn expand_with_context(&mut self, element: &Element, context_path: Option<&str>) -> Element {
370 let component_exists = self.get(&element.element_type, context_path).is_some();
372
373 if !component_exists {
374 self.try_resolve(&element.element_type, context_path);
376 }
377
378 if let Some(component) = self.get(&element.element_type, context_path) {
380 let comp_is_module = component.is_module;
382 let comp_module_name = component.module_name.clone();
383
384 if component.lazy {
386 let mut element = element.clone();
388
389 element.props.insert(
391 "__lazy".to_string(),
392 super::Value::Static(serde_json::json!(true)),
393 );
394
395 #[cfg(all(target_arch = "wasm32", feature = "js"))]
396 web_sys::console::log_1(
397 &format!(
398 "Lazy {} (props: {:?}): {} children kept unexpanded",
399 element.element_type,
400 element.props.keys().collect::<Vec<_>>(),
401 element.ir_children.len()
402 )
403 .into(),
404 );
405
406 return element;
407 }
408
409 if component.passthrough {
411 let mut element = element.clone();
413
414 #[cfg(all(target_arch = "wasm32", feature = "js"))]
415 {
416 let props_str = element
417 .props
418 .iter()
419 .map(|(k, v)| format!("{}={:?}", k, v))
420 .collect::<Vec<_>>()
421 .join(", ");
422 web_sys::console::log_1(
423 &format!(
424 "Passthrough {} (props: [{}]): {} children before expansion",
425 element.element_type,
426 props_str,
427 element.ir_children.len()
428 )
429 .into(),
430 );
431 }
432
433 let child_context = component.source_path.clone();
435 let child_context_ref = child_context.as_deref();
436
437 element.ir_children = element
439 .ir_children
440 .iter()
441 .map(|child| self.expand_ir_node_with_context(child, child_context_ref))
442 .collect();
443
444 #[cfg(all(target_arch = "wasm32", feature = "js"))]
445 web_sys::console::log_1(
446 &format!(
447 "Passthrough {}: {} children after expansion",
448 element.element_type,
449 element.ir_children.len()
450 )
451 .into(),
452 );
453
454 element
455 } else {
456 let mut props = IndexMap::new();
459 for (k, v) in &element.props {
460 if let super::Value::Static(val) = v {
461 props.insert(k.clone(), val.clone());
462 }
463 }
464
465 let mut expanded = component.instantiate(props);
466
467 for (k, v) in &element.props {
469 match v {
470 super::Value::Binding(_) | super::Value::Action(_) => {
471 expanded.props.insert(k.clone(), v.clone());
472 }
473 _ => {}
474 }
475 }
476
477 let child_context = component.source_path.clone();
480
481 expanded.ir_children = self.replace_children_slots(
483 &expanded.ir_children,
484 &element.ir_children,
485 context_path,
486 );
487
488 let child_context_ref = child_context.as_deref();
490 expanded.ir_children = expanded
491 .ir_children
492 .iter()
493 .map(|child| self.expand_ir_node_with_context(child, child_context_ref))
494 .collect();
495
496 if comp_is_module {
500 if let Some(ref scope) = comp_module_name {
501 super::expand::propagate_module_scope_element(&mut expanded, scope);
502 }
503 }
504
505 expanded
506 }
507 } else {
508 let mut element = element.clone();
510 element.ir_children = element
511 .ir_children
512 .iter()
513 .map(|child| self.expand_ir_node_with_context(child, context_path))
514 .collect();
515 element
516 }
517 }
518
519 pub fn expand_ir_node(&mut self, node: &super::IRNode) -> super::IRNode {
521 self.expand_ir_node_with_context(node, None)
522 }
523
524 fn expand_ir_node_with_context(
526 &mut self,
527 node: &super::IRNode,
528 context_path: Option<&str>,
529 ) -> super::IRNode {
530 match node {
531 super::IRNode::Element(element) => {
532 let expanded = self.expand_with_context(element, context_path);
536 super::IRNode::Element(expanded)
537 }
538 super::IRNode::ForEach {
539 source,
540 item_name,
541 key_path,
542 template,
543 props,
544 module_scope,
545 } => {
546 let expanded_template: Vec<super::IRNode> = template
548 .iter()
549 .map(|child| self.expand_ir_node_with_context(child, context_path))
550 .collect();
551
552 super::IRNode::ForEach {
553 source: source.clone(),
554 item_name: item_name.clone(),
555 key_path: key_path.clone(),
556 template: expanded_template,
557 props: props.clone(),
558 module_scope: module_scope.clone(),
559 }
560 }
561 super::IRNode::Conditional {
562 value,
563 branches,
564 fallback,
565 module_scope,
566 } => {
567 let expanded_branches: Vec<super::ConditionalBranch> = branches
569 .iter()
570 .map(|branch| super::ConditionalBranch {
571 pattern: branch.pattern.clone(),
572 children: branch
573 .children
574 .iter()
575 .map(|child| self.expand_ir_node_with_context(child, context_path))
576 .collect(),
577 })
578 .collect();
579
580 let expanded_fallback = fallback.as_ref().map(|fb| {
582 fb.iter()
583 .map(|child| self.expand_ir_node_with_context(child, context_path))
584 .collect()
585 });
586
587 super::IRNode::Conditional {
588 value: value.clone(),
589 branches: expanded_branches,
590 fallback: expanded_fallback,
591 module_scope: module_scope.clone(),
592 }
593 }
594 super::IRNode::Router {
595 location,
596 routes,
597 fallback,
598 module_scope,
599 } => {
600 let expanded_routes: Vec<super::RouterRoute> = routes
602 .iter()
603 .map(|route| super::RouterRoute {
604 path: route.path.clone(),
605 children: route
606 .children
607 .iter()
608 .map(|child| self.expand_ir_node_with_context(child, context_path))
609 .collect(),
610 })
611 .collect();
612
613 let expanded_fallback = fallback.as_ref().map(|fb| {
615 fb.iter()
616 .map(|child| self.expand_ir_node_with_context(child, context_path))
617 .collect()
618 });
619
620 super::IRNode::Router {
621 location: location.clone(),
622 routes: expanded_routes,
623 fallback: expanded_fallback,
624 module_scope: module_scope.clone(),
625 }
626 }
627 }
628 }
629
630 fn replace_children_slots(
633 &self,
634 template_children: &[super::IRNode],
635 actual_children: &[super::IRNode],
636 _context_path: Option<&str>,
637 ) -> Vec<super::IRNode> {
638 let mut result = Vec::new();
639
640 for child_ir in template_children {
641 match child_ir {
642 super::IRNode::Element(child) if child.element_type == "Children" => {
643 let slot_name = self.get_slot_name(&child.props);
645
646 if let Some(slot) = slot_name {
649 for c in actual_children {
650 if let super::IRNode::Element(ce) = c {
651 if self.get_slot_name(&ce.props) == Some(slot) {
652 result.push(c.clone());
653 }
654 }
655 }
656 } else {
657 for c in actual_children {
659 if let super::IRNode::Element(ce) = c {
660 if self.get_slot_name(&ce.props).is_none() {
661 result.push(c.clone());
662 }
663 } else {
664 result.push(c.clone());
666 }
667 }
668 }
669 }
670 super::IRNode::Element(child) => {
671 let mut new_child = child.clone();
673 new_child.ir_children = self.replace_children_slots(
674 &child.ir_children,
675 actual_children,
676 _context_path,
677 );
678 result.push(super::IRNode::Element(new_child));
679 }
680 other => {
681 result.push(other.clone());
683 }
684 }
685 }
686
687 result
688 }
689
690 fn get_slot_name<'a>(&self, props: &'a super::Props) -> Option<&'a str> {
693 props.get("slot.0").and_then(|v| {
694 if let super::Value::Static(serde_json::Value::String(s)) = v {
695 Some(s.as_str())
696 } else {
697 None
698 }
699 })
700 }
701}
702
703impl Default for ComponentRegistry {
704 fn default() -> Self {
705 Self::new()
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use super::*;
712 use crate::ir::Value;
713
714 #[test]
715 fn test_dynamic_component_resolution() {
716 let mut registry = ComponentRegistry::new();
717
718 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
720 if name == "Header" {
721 Some(ResolvedComponent {
722 source: r#"Row { Text("Header") }"#.to_string(),
723 path: "/components/Header.hypen".to_string(),
724 passthrough: false,
725 lazy: false,
726 })
727 } else {
728 None
729 }
730 }));
731
732 let element = Element::new("Column").with_child(Element::new("Header"));
734
735 let expanded = registry.expand(&element);
737
738 assert_eq!(expanded.element_type, "Column");
740 assert_eq!(expanded.ir_children.len(), 1);
741 match &expanded.ir_children[0] {
742 crate::ir::IRNode::Element(row) => {
743 assert_eq!(row.element_type, "Row");
744 assert_eq!(row.ir_children.len(), 1);
745 match &row.ir_children[0] {
746 crate::ir::IRNode::Element(text) => assert_eq!(text.element_type, "Text"),
747 other => panic!("Expected Element, got {:?}", other),
748 }
749 }
750 other => panic!("Expected Element, got {:?}", other),
751 }
752 }
753
754 #[test]
755 fn test_component_resolution_with_path_context() {
756 let mut registry = ComponentRegistry::new();
757
758 registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
760 match (name, context) {
761 ("Button", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
762 source: r#"Text("Home Button")"#.to_string(),
763 path: "/components/buttons/HomeButton.hypen".to_string(),
764 passthrough: false,
765 lazy: false,
766 }),
767 ("Button", Some("/pages/About.hypen")) => Some(ResolvedComponent {
768 source: r#"Text("About Button")"#.to_string(),
769 path: "/components/buttons/AboutButton.hypen".to_string(),
770 passthrough: false,
771 lazy: false,
772 }),
773 _ => None,
774 }
775 }));
776
777 let home_element =
779 Element::new("Text").with_prop("0", Value::Static(serde_json::json!("Home")));
780 let home_component = Component::new("Home", move |_| home_element.clone())
781 .with_source_path("/pages/Home.hypen");
782 registry.register(home_component);
783
784 let element = Element::new("Column")
786 .with_child(Element::new("Home").with_child(Element::new("Button")));
787
788 let expanded = registry.expand(&element);
789
790 assert_eq!(expanded.element_type, "Column");
792 }
793
794 #[test]
795 fn test_component_resolution_caching() {
796 let mut registry = ComponentRegistry::new();
797 let call_count = Arc::new(std::sync::Mutex::new(0));
798 let call_count_clone = call_count.clone();
799
800 registry.set_resolver(Arc::new(move |name: &str, _context: Option<&str>| {
802 if name == "Button" {
803 *call_count_clone.lock().unwrap() += 1;
804 Some(ResolvedComponent {
805 source: r#"Text("Click")"#.to_string(),
806 path: "/components/Button.hypen".to_string(),
807 passthrough: false,
808 lazy: false,
809 })
810 } else {
811 None
812 }
813 }));
814
815 let element1 = Element::new("Button");
817 let _ = registry.expand(&element1);
818 assert_eq!(*call_count.lock().unwrap(), 1);
819
820 let element2 = Element::new("Button");
822 let _ = registry.expand(&element2);
823 assert_eq!(*call_count.lock().unwrap(), 1); }
825
826 #[test]
827 fn test_failed_resolution_cached() {
828 let mut registry = ComponentRegistry::new();
829 let call_count = Arc::new(std::sync::Mutex::new(0));
830 let call_count_clone = call_count.clone();
831
832 registry.set_resolver(Arc::new(move |_name: &str, _context: Option<&str>| {
834 *call_count_clone.lock().unwrap() += 1;
835 None
836 }));
837
838 let element1 = Element::new("Unknown");
840 let _ = registry.expand(&element1);
841 assert_eq!(*call_count.lock().unwrap(), 1);
842
843 let element2 = Element::new("Unknown");
845 let _ = registry.expand(&element2);
846 assert_eq!(*call_count.lock().unwrap(), 1); }
848
849 #[test]
850 fn test_passthrough_component_preserves_props() {
851 let mut registry = ComponentRegistry::new();
852
853 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
855 if name == "Router" || name == "Route" {
856 Some(ResolvedComponent {
857 source: String::new(), path: name.to_string(),
859 passthrough: true,
860 lazy: false,
861 })
862 } else if name == "HomePage" {
863 Some(ResolvedComponent {
865 source: "Text(\"Home\")".to_string(),
866 path: name.to_string(),
867 passthrough: false,
868 lazy: false,
869 })
870 } else {
871 None
872 }
873 }));
874
875 let router = Element::new("Router")
881 .with_child(
882 Element::new("Route")
883 .with_prop("0", Value::Static(serde_json::json!("/")))
884 .with_child(Element::new("HomePage")),
885 )
886 .with_child(
887 Element::new("Route")
888 .with_prop("0", Value::Static(serde_json::json!("/about")))
889 .with_child(Element::new("HomePage")),
890 );
891
892 let expanded = registry.expand(&router);
894
895 assert_eq!(expanded.element_type, "Router");
897 assert_eq!(expanded.ir_children.len(), 2);
898
899 fn unwrap_element(node: &crate::ir::IRNode) -> &Element {
901 match node {
902 crate::ir::IRNode::Element(e) => e,
903 other => panic!("Expected Element, got {:?}", other),
904 }
905 }
906
907 let expanded_route1 = unwrap_element(&expanded.ir_children[0]);
909 assert_eq!(expanded_route1.element_type, "Route");
910 if let Some(Value::Static(path)) = expanded_route1.props.get("0") {
911 assert_eq!(path.as_str().unwrap(), "/");
912 } else {
913 panic!("Route 1 missing path prop");
914 }
915
916 let expanded_route2 = unwrap_element(&expanded.ir_children[1]);
918 assert_eq!(expanded_route2.element_type, "Route");
919 if let Some(Value::Static(path)) = expanded_route2.props.get("0") {
920 assert_eq!(path.as_str().unwrap(), "/about");
921 } else {
922 panic!("Route 2 missing path prop");
923 }
924
925 assert_eq!(expanded_route1.ir_children.len(), 1);
927 assert_eq!(
928 unwrap_element(&expanded_route1.ir_children[0]).element_type,
929 "Text"
930 );
931 }
932
933 #[test]
934 fn test_bare_miss_does_not_block_context_resolve() {
935 let mut registry = ComponentRegistry::new();
938
939 registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
940 match (name, context) {
942 ("Header", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
943 source: r#"Text("Home Header")"#.to_string(),
944 path: "/components/Header.hypen".to_string(),
945 passthrough: false,
946 lazy: false,
947 }),
948 _ => None,
949 }
950 }));
951
952 let element = Element::new("Header");
954 let expanded = registry.expand(&element);
955 assert_eq!(expanded.element_type, "Header");
957
958 let element2 = Element::new("Header");
960 let expanded2 = registry.expand_with_context(&element2, Some("/pages/Home.hypen"));
961 assert_ne!(
963 expanded2.element_type, "Header",
964 "Context-scoped resolve must not be blocked by prior bare-name miss"
965 );
966 }
967
968 #[test]
969 fn test_primitives_never_shadowed_by_resolver() {
970 let mut registry = ComponentRegistry::new();
973 registry.register_primitive("Text");
974
975 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
976 if name == "Text" {
977 Some(ResolvedComponent {
978 source: r#"Column { Text("Shadowed!") }"#.to_string(),
979 path: "/evil/Text.hypen".to_string(),
980 passthrough: false,
981 lazy: false,
982 })
983 } else {
984 None
985 }
986 }));
987
988 let element = Element::new("Text");
990 let expanded = registry.expand(&element);
991 assert_eq!(expanded.element_type, "Text");
992 assert!(expanded.ir_children.is_empty());
993
994 let expanded2 = registry.expand_with_context(&element, Some("/some/path"));
996 assert_eq!(expanded2.element_type, "Text");
997 assert!(expanded2.ir_children.is_empty());
998 }
999}