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", "Icon",
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 pub is_module: bool,
63
64 pub module_name: Option<String>,
66}
67
68impl Component {
69 pub fn new(
70 name: impl Into<String>,
71 template: impl Fn(IndexMap<String, serde_json::Value>) -> Element + Send + Sync + 'static,
72 ) -> Self {
73 Self {
74 name: name.into(),
75 template: Arc::new(template),
76 default_props: IndexMap::new(),
77 source_path: None,
78 passthrough: false,
79 lazy: false,
80 is_module: false,
81 module_name: None,
82 }
83 }
84
85 pub fn with_defaults(mut self, defaults: IndexMap<String, serde_json::Value>) -> Self {
86 self.default_props = defaults;
87 self
88 }
89
90 pub fn with_source_path(mut self, path: impl Into<String>) -> Self {
91 self.source_path = Some(path.into());
92 self
93 }
94
95 pub fn with_passthrough(mut self, passthrough: bool) -> Self {
96 self.passthrough = passthrough;
97 self
98 }
99
100 pub fn with_lazy(mut self, lazy: bool) -> Self {
101 self.lazy = lazy;
102 self
103 }
104
105 pub fn instantiate(&self, props: IndexMap<String, serde_json::Value>) -> Element {
107 let mut merged_props = self.default_props.clone();
108 merged_props.extend(props);
109 (self.template)(merged_props)
110 }
111}
112
113pub struct ComponentRegistry {
115 components: IndexMap<String, Component>,
117 resolver: Option<ComponentResolver>,
119 resolved_cache: IndexMap<String, bool>,
123 primitives: HashSet<String>,
127}
128
129impl ComponentRegistry {
130 pub fn new() -> Self {
131 Self {
132 components: IndexMap::new(),
133 resolver: None,
134 resolved_cache: IndexMap::new(),
135 primitives: HashSet::new(),
136 }
137 }
138
139 pub fn register_primitive(&mut self, name: &str) {
142 self.primitives.insert(name.to_string());
143 }
144
145 pub fn register_default_primitives(&mut self) {
151 for name in DEFAULT_PRIMITIVES {
152 self.register_primitive(name);
153 }
154 }
155
156 pub fn is_primitive(&self, name: &str) -> bool {
158 self.primitives.contains(name)
159 }
160
161 pub fn clear_resolved(&mut self) {
165 self.components.clear();
166 self.resolved_cache.clear();
167 }
168
169 pub fn set_resolver(&mut self, resolver: ComponentResolver) {
171 self.resolver = Some(resolver);
172 }
173
174 pub fn register(&mut self, component: Component) {
175 if let Some(ref path) = component.source_path {
177 let qualified_key = format!("{}:{}", path, component.name);
178 self.components.insert(qualified_key, component.clone());
179 }
180
181 self.components.insert(component.name.clone(), component);
183 }
184
185 pub fn get(&self, name: &str, context_path: Option<&str>) -> Option<&Component> {
187 if let Some(path) = context_path {
189 let qualified_key = format!("{}:{}", path, name);
190 if let Some(component) = self.components.get(&qualified_key) {
191 return Some(component);
192 }
193 }
194
195 self.components.get(name)
197 }
198
199 fn try_resolve(&mut self, name: &str, context_path: Option<&str>) -> bool {
201 if self.primitives.contains(name) {
203 return false;
204 }
205
206 let cache_key = if let Some(path) = context_path {
208 format!("{}:{}", path, name)
209 } else {
210 name.to_string()
211 };
212
213 if let Some(&cached) = self.resolved_cache.get(&cache_key) {
215 return cached;
216 }
217
218 if let Some(ref resolver) = self.resolver {
220 if let Some(resolved) = resolver(name, context_path) {
221 if resolved.lazy {
224 #[cfg(all(target_arch = "wasm32", feature = "js"))]
225 web_sys::console::log_1(
226 &format!("Registering lazy component: {}", name).into(),
227 );
228
229 let dummy_element = Element::new(name);
231 let component = Component::new(name, move |_props| dummy_element.clone())
232 .with_source_path(resolved.path.clone())
233 .with_lazy(true);
234
235 self.register(component);
236 self.resolved_cache.insert(cache_key, true);
237 return true;
238 }
239
240 if resolved.passthrough {
243 #[cfg(all(target_arch = "wasm32", feature = "js"))]
244 web_sys::console::log_1(
245 &format!("Registering passthrough component: {}", name).into(),
246 );
247
248 let dummy_element = Element::new(name);
250 let component = Component::new(name, move |_props| dummy_element.clone())
251 .with_source_path(resolved.path.clone())
252 .with_passthrough(true);
253
254 self.register(component);
255 self.resolved_cache.insert(cache_key, true);
256 return true;
257 }
258
259 match hypen_parser::parse_component(&resolved.source) {
261 Ok(component_spec) => {
262 let spec_is_module = component_spec.declaration_type
264 == hypen_parser::DeclarationType::Module;
265 let spec_module_name = if spec_is_module {
266 Some(component_spec.name.to_lowercase())
267 } else {
268 None
269 };
270
271 let ir_node = super::expand::ast_to_ir_node(&component_spec);
273 let element = match ir_node {
274 super::IRNode::Element(e) => e,
275 _ => {
276 #[cfg(all(target_arch = "wasm32", feature = "js"))]
277 web_sys::console::error_1(
278 &format!("Component {} root must be an element", name).into(),
279 );
280 return false;
281 }
282 };
283
284 let mut component = Component::new(name, move |_props| element.clone())
286 .with_source_path(resolved.path.clone())
287 .with_passthrough(false);
288
289 if spec_is_module {
291 component.is_module = true;
292 component.module_name = spec_module_name;
293 }
294
295 self.register(component);
296 self.resolved_cache.insert(cache_key, true);
297 return true;
298 }
299 Err(e) => {
300 #[cfg(all(target_arch = "wasm32", feature = "js"))]
301 web_sys::console::error_1(
302 &format!("Failed to parse component {}: {:?}", name, e).into(),
303 );
304
305 #[cfg(not(all(target_arch = "wasm32", feature = "js")))]
306 eprintln!("Failed to parse component {}: {:?}", name, e);
307
308 self.resolved_cache.insert(cache_key, false);
309 return false;
310 }
311 }
312 }
313 }
314
315 self.resolved_cache.insert(cache_key, false);
316 false
317 }
318
319 pub fn expand(&mut self, element: &Element) -> Element {
320 self.expand_with_context(element, None)
321 }
322
323 pub fn expand_children(
326 &mut self,
327 element: &Element,
328 context_path: Option<&str>,
329 ) -> Vec<Element> {
330 element
331 .ir_children
332 .iter()
333 .filter_map(|child_ir| {
334 if let super::IRNode::Element(child) = child_ir {
335 Some(self.expand_with_context(child, context_path))
336 } else {
337 None
338 }
339 })
340 .collect()
341 }
342
343 fn expand_with_context(&mut self, element: &Element, context_path: Option<&str>) -> Element {
345 let component_exists = self.get(&element.element_type, context_path).is_some();
347
348 if !component_exists {
349 self.try_resolve(&element.element_type, context_path);
351 }
352
353 if let Some(component) = self.get(&element.element_type, context_path) {
355 let comp_is_module = component.is_module;
357 let comp_module_name = component.module_name.clone();
358
359 if component.lazy {
361 let mut element = element.clone();
363
364 element.props.insert(
366 "__lazy".to_string(),
367 super::Value::Static(serde_json::json!(true)),
368 );
369
370 #[cfg(all(target_arch = "wasm32", feature = "js"))]
371 web_sys::console::log_1(
372 &format!(
373 "Lazy {} (props: {:?}): {} children kept unexpanded",
374 element.element_type,
375 element.props.keys().collect::<Vec<_>>(),
376 element.ir_children.len()
377 )
378 .into(),
379 );
380
381 return element;
382 }
383
384 if component.passthrough {
386 let mut element = element.clone();
388
389 #[cfg(all(target_arch = "wasm32", feature = "js"))]
390 {
391 let props_str = element
392 .props
393 .iter()
394 .map(|(k, v)| format!("{}={:?}", k, v))
395 .collect::<Vec<_>>()
396 .join(", ");
397 web_sys::console::log_1(
398 &format!(
399 "Passthrough {} (props: [{}]): {} children before expansion",
400 element.element_type,
401 props_str,
402 element.ir_children.len()
403 )
404 .into(),
405 );
406 }
407
408 let child_context = component.source_path.clone();
410 let child_context_ref = child_context.as_deref();
411
412 element.ir_children = element
414 .ir_children
415 .iter()
416 .map(|child| self.expand_ir_node_with_context(child, child_context_ref))
417 .collect();
418
419 #[cfg(all(target_arch = "wasm32", feature = "js"))]
420 web_sys::console::log_1(
421 &format!(
422 "Passthrough {}: {} children after expansion",
423 element.element_type,
424 element.ir_children.len()
425 )
426 .into(),
427 );
428
429 element
430 } else {
431 let mut props = IndexMap::new();
434 for (k, v) in &element.props {
435 if let super::Value::Static(val) = v {
436 props.insert(k.clone(), val.clone());
437 }
438 }
439
440 let mut expanded = component.instantiate(props);
441
442 for (k, v) in &element.props {
444 match v {
445 super::Value::Binding(_) | super::Value::Action(_) => {
446 expanded.props.insert(k.clone(), v.clone());
447 }
448 _ => {}
449 }
450 }
451
452 let child_context = component.source_path.clone();
455
456 expanded.ir_children = self.replace_children_slots(
458 &expanded.ir_children,
459 &element.ir_children,
460 context_path,
461 );
462
463 let child_context_ref = child_context.as_deref();
465 expanded.ir_children = expanded
466 .ir_children
467 .iter()
468 .map(|child| self.expand_ir_node_with_context(child, child_context_ref))
469 .collect();
470
471 if comp_is_module {
475 if let Some(ref scope) = comp_module_name {
476 super::expand::propagate_module_scope_element(&mut expanded, scope);
477 }
478 }
479
480 expanded
481 }
482 } else {
483 let mut element = element.clone();
485 element.ir_children = element
486 .ir_children
487 .iter()
488 .map(|child| self.expand_ir_node_with_context(child, context_path))
489 .collect();
490 element
491 }
492 }
493
494 pub fn expand_ir_node(&mut self, node: &super::IRNode) -> super::IRNode {
496 self.expand_ir_node_with_context(node, None)
497 }
498
499 fn expand_ir_node_with_context(
501 &mut self,
502 node: &super::IRNode,
503 context_path: Option<&str>,
504 ) -> super::IRNode {
505 match node {
506 super::IRNode::Element(element) => {
507 let expanded = self.expand_with_context(element, context_path);
511 super::IRNode::Element(expanded)
512 }
513 super::IRNode::ForEach {
514 source,
515 item_name,
516 key_path,
517 template,
518 props,
519 module_scope,
520 } => {
521 let expanded_template: Vec<super::IRNode> = template
523 .iter()
524 .map(|child| self.expand_ir_node_with_context(child, context_path))
525 .collect();
526
527 super::IRNode::ForEach {
528 source: source.clone(),
529 item_name: item_name.clone(),
530 key_path: key_path.clone(),
531 template: expanded_template,
532 props: props.clone(),
533 module_scope: module_scope.clone(),
534 }
535 }
536 super::IRNode::Conditional {
537 value,
538 branches,
539 fallback,
540 module_scope,
541 } => {
542 let expanded_branches: Vec<super::ConditionalBranch> = branches
544 .iter()
545 .map(|branch| super::ConditionalBranch {
546 pattern: branch.pattern.clone(),
547 children: branch
548 .children
549 .iter()
550 .map(|child| self.expand_ir_node_with_context(child, context_path))
551 .collect(),
552 })
553 .collect();
554
555 let expanded_fallback = fallback.as_ref().map(|fb| {
557 fb.iter()
558 .map(|child| self.expand_ir_node_with_context(child, context_path))
559 .collect()
560 });
561
562 super::IRNode::Conditional {
563 value: value.clone(),
564 branches: expanded_branches,
565 fallback: expanded_fallback,
566 module_scope: module_scope.clone(),
567 }
568 }
569 super::IRNode::Router {
570 location,
571 routes,
572 fallback,
573 module_scope,
574 } => {
575 let expanded_routes: Vec<super::RouterRoute> = routes
577 .iter()
578 .map(|route| super::RouterRoute {
579 path: route.path.clone(),
580 children: route
581 .children
582 .iter()
583 .map(|child| self.expand_ir_node_with_context(child, context_path))
584 .collect(),
585 })
586 .collect();
587
588 let expanded_fallback = fallback.as_ref().map(|fb| {
590 fb.iter()
591 .map(|child| self.expand_ir_node_with_context(child, context_path))
592 .collect()
593 });
594
595 super::IRNode::Router {
596 location: location.clone(),
597 routes: expanded_routes,
598 fallback: expanded_fallback,
599 module_scope: module_scope.clone(),
600 }
601 }
602 }
603 }
604
605 fn replace_children_slots(
608 &self,
609 template_children: &[super::IRNode],
610 actual_children: &[super::IRNode],
611 _context_path: Option<&str>,
612 ) -> Vec<super::IRNode> {
613 let mut result = Vec::new();
614
615 for child_ir in template_children {
616 match child_ir {
617 super::IRNode::Element(child) if child.element_type == "Children" => {
618 let slot_name = self.get_slot_name(&child.props);
620
621 if let Some(slot) = slot_name {
624 for c in actual_children {
625 if let super::IRNode::Element(ce) = c {
626 if self.get_slot_name(&ce.props) == Some(slot) {
627 result.push(c.clone());
628 }
629 }
630 }
631 } else {
632 for c in actual_children {
634 if let super::IRNode::Element(ce) = c {
635 if self.get_slot_name(&ce.props).is_none() {
636 result.push(c.clone());
637 }
638 } else {
639 result.push(c.clone());
641 }
642 }
643 }
644 }
645 super::IRNode::Element(child) => {
646 let mut new_child = child.clone();
648 new_child.ir_children =
649 self.replace_children_slots(&child.ir_children, actual_children, _context_path);
650 result.push(super::IRNode::Element(new_child));
651 }
652 other => {
653 result.push(other.clone());
655 }
656 }
657 }
658
659 result
660 }
661
662 fn get_slot_name<'a>(&self, props: &'a super::Props) -> Option<&'a str> {
665 props.get("slot.0").and_then(|v| {
666 if let super::Value::Static(serde_json::Value::String(s)) = v {
667 Some(s.as_str())
668 } else {
669 None
670 }
671 })
672 }
673}
674
675impl Default for ComponentRegistry {
676 fn default() -> Self {
677 Self::new()
678 }
679}
680
681#[cfg(test)]
682mod tests {
683 use super::*;
684 use crate::ir::Value;
685
686 #[test]
687 fn test_dynamic_component_resolution() {
688 let mut registry = ComponentRegistry::new();
689
690 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
692 if name == "Header" {
693 Some(ResolvedComponent {
694 source: r#"Row { Text("Header") }"#.to_string(),
695 path: "/components/Header.hypen".to_string(),
696 passthrough: false,
697 lazy: false,
698 })
699 } else {
700 None
701 }
702 }));
703
704 let element = Element::new("Column").with_child(Element::new("Header"));
706
707 let expanded = registry.expand(&element);
709
710 assert_eq!(expanded.element_type, "Column");
712 assert_eq!(expanded.ir_children.len(), 1);
713 match &expanded.ir_children[0] {
714 crate::ir::IRNode::Element(row) => {
715 assert_eq!(row.element_type, "Row");
716 assert_eq!(row.ir_children.len(), 1);
717 match &row.ir_children[0] {
718 crate::ir::IRNode::Element(text) => assert_eq!(text.element_type, "Text"),
719 other => panic!("Expected Element, got {:?}", other),
720 }
721 }
722 other => panic!("Expected Element, got {:?}", other),
723 }
724 }
725
726 #[test]
727 fn test_component_resolution_with_path_context() {
728 let mut registry = ComponentRegistry::new();
729
730 registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
732 match (name, context) {
733 ("Button", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
734 source: r#"Text("Home Button")"#.to_string(),
735 path: "/components/buttons/HomeButton.hypen".to_string(),
736 passthrough: false,
737 lazy: false,
738 }),
739 ("Button", Some("/pages/About.hypen")) => Some(ResolvedComponent {
740 source: r#"Text("About Button")"#.to_string(),
741 path: "/components/buttons/AboutButton.hypen".to_string(),
742 passthrough: false,
743 lazy: false,
744 }),
745 _ => None,
746 }
747 }));
748
749 let home_element =
751 Element::new("Text").with_prop("0", Value::Static(serde_json::json!("Home")));
752 let home_component = Component::new("Home", move |_| home_element.clone())
753 .with_source_path("/pages/Home.hypen");
754 registry.register(home_component);
755
756 let element = Element::new("Column")
758 .with_child(Element::new("Home").with_child(Element::new("Button")));
759
760 let expanded = registry.expand(&element);
761
762 assert_eq!(expanded.element_type, "Column");
764 }
765
766 #[test]
767 fn test_component_resolution_caching() {
768 let mut registry = ComponentRegistry::new();
769 let call_count = Arc::new(std::sync::Mutex::new(0));
770 let call_count_clone = call_count.clone();
771
772 registry.set_resolver(Arc::new(move |name: &str, _context: Option<&str>| {
774 if name == "Button" {
775 *call_count_clone.lock().unwrap() += 1;
776 Some(ResolvedComponent {
777 source: r#"Text("Click")"#.to_string(),
778 path: "/components/Button.hypen".to_string(),
779 passthrough: false,
780 lazy: false,
781 })
782 } else {
783 None
784 }
785 }));
786
787 let element1 = Element::new("Button");
789 let _ = registry.expand(&element1);
790 assert_eq!(*call_count.lock().unwrap(), 1);
791
792 let element2 = Element::new("Button");
794 let _ = registry.expand(&element2);
795 assert_eq!(*call_count.lock().unwrap(), 1); }
797
798 #[test]
799 fn test_failed_resolution_cached() {
800 let mut registry = ComponentRegistry::new();
801 let call_count = Arc::new(std::sync::Mutex::new(0));
802 let call_count_clone = call_count.clone();
803
804 registry.set_resolver(Arc::new(move |_name: &str, _context: Option<&str>| {
806 *call_count_clone.lock().unwrap() += 1;
807 None
808 }));
809
810 let element1 = Element::new("Unknown");
812 let _ = registry.expand(&element1);
813 assert_eq!(*call_count.lock().unwrap(), 1);
814
815 let element2 = Element::new("Unknown");
817 let _ = registry.expand(&element2);
818 assert_eq!(*call_count.lock().unwrap(), 1); }
820
821 #[test]
822 fn test_passthrough_component_preserves_props() {
823 let mut registry = ComponentRegistry::new();
824
825 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
827 if name == "Router" || name == "Route" {
828 Some(ResolvedComponent {
829 source: String::new(), path: name.to_string(),
831 passthrough: true,
832 lazy: false,
833 })
834 } else if name == "HomePage" {
835 Some(ResolvedComponent {
837 source: "Text(\"Home\")".to_string(),
838 path: name.to_string(),
839 passthrough: false,
840 lazy: false,
841 })
842 } else {
843 None
844 }
845 }));
846
847 let router = Element::new("Router")
853 .with_child(
854 Element::new("Route")
855 .with_prop("0", Value::Static(serde_json::json!("/")))
856 .with_child(Element::new("HomePage")),
857 )
858 .with_child(
859 Element::new("Route")
860 .with_prop("0", Value::Static(serde_json::json!("/about")))
861 .with_child(Element::new("HomePage")),
862 );
863
864 let expanded = registry.expand(&router);
866
867 assert_eq!(expanded.element_type, "Router");
869 assert_eq!(expanded.ir_children.len(), 2);
870
871 fn unwrap_element(node: &crate::ir::IRNode) -> &Element {
873 match node {
874 crate::ir::IRNode::Element(e) => e,
875 other => panic!("Expected Element, got {:?}", other),
876 }
877 }
878
879 let expanded_route1 = unwrap_element(&expanded.ir_children[0]);
881 assert_eq!(expanded_route1.element_type, "Route");
882 if let Some(Value::Static(path)) = expanded_route1.props.get("0") {
883 assert_eq!(path.as_str().unwrap(), "/");
884 } else {
885 panic!("Route 1 missing path prop");
886 }
887
888 let expanded_route2 = unwrap_element(&expanded.ir_children[1]);
890 assert_eq!(expanded_route2.element_type, "Route");
891 if let Some(Value::Static(path)) = expanded_route2.props.get("0") {
892 assert_eq!(path.as_str().unwrap(), "/about");
893 } else {
894 panic!("Route 2 missing path prop");
895 }
896
897 assert_eq!(expanded_route1.ir_children.len(), 1);
899 assert_eq!(unwrap_element(&expanded_route1.ir_children[0]).element_type, "Text");
900 }
901
902 #[test]
903 fn test_bare_miss_does_not_block_context_resolve() {
904 let mut registry = ComponentRegistry::new();
907
908 registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
909 match (name, context) {
911 ("Header", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
912 source: r#"Text("Home Header")"#.to_string(),
913 path: "/components/Header.hypen".to_string(),
914 passthrough: false,
915 lazy: false,
916 }),
917 _ => None,
918 }
919 }));
920
921 let element = Element::new("Header");
923 let expanded = registry.expand(&element);
924 assert_eq!(expanded.element_type, "Header");
926
927 let element2 = Element::new("Header");
929 let expanded2 = registry.expand_with_context(&element2, Some("/pages/Home.hypen"));
930 assert_ne!(expanded2.element_type, "Header",
932 "Context-scoped resolve must not be blocked by prior bare-name miss");
933 }
934
935 #[test]
936 fn test_primitives_never_shadowed_by_resolver() {
937 let mut registry = ComponentRegistry::new();
940 registry.register_primitive("Text");
941
942 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
943 if name == "Text" {
944 Some(ResolvedComponent {
945 source: r#"Column { Text("Shadowed!") }"#.to_string(),
946 path: "/evil/Text.hypen".to_string(),
947 passthrough: false,
948 lazy: false,
949 })
950 } else {
951 None
952 }
953 }));
954
955 let element = Element::new("Text");
957 let expanded = registry.expand(&element);
958 assert_eq!(expanded.element_type, "Text");
959 assert!(expanded.ir_children.is_empty());
960
961 let expanded2 = registry.expand_with_context(&element, Some("/some/path"));
963 assert_eq!(expanded2.element_type, "Text");
964 assert!(expanded2.ir_children.is_empty());
965 }
966}
967