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 element = super::expand::ast_to_ir(&component_spec);
206
207 let component = Component::new(name, move |_props| element.clone())
209 .with_source_path(resolved.path.clone())
210 .with_passthrough(false);
211
212 self.register(component);
213 self.resolved_cache.insert(cache_key, true);
214 return true;
215 }
216 Err(e) => {
217 #[cfg(all(target_arch = "wasm32", feature = "js"))]
218 web_sys::console::error_1(
219 &format!("Failed to parse component {}: {:?}", name, e).into(),
220 );
221
222 #[cfg(not(all(target_arch = "wasm32", feature = "js")))]
223 eprintln!("Failed to parse component {}: {:?}", name, e);
224
225 self.resolved_cache.insert(cache_key, false);
226 return false;
227 }
228 }
229 }
230 }
231
232 self.resolved_cache.insert(cache_key, false);
233 false
234 }
235
236 pub fn expand(&mut self, element: &Element) -> Element {
237 self.expand_with_context(element, None)
238 }
239
240 pub fn expand_children(
243 &mut self,
244 element: &Element,
245 context_path: Option<&str>,
246 ) -> Vec<Element> {
247 element
248 .children
249 .iter()
250 .map(|child| self.expand_with_context(child, context_path))
251 .collect()
252 }
253
254 fn expand_with_context(&mut self, element: &Element, context_path: Option<&str>) -> Element {
256 let component_exists = self.get(&element.element_type, context_path).is_some();
258
259 if !component_exists {
260 self.try_resolve(&element.element_type, context_path);
262 }
263
264 if let Some(component) = self.get(&element.element_type, context_path) {
266 if component.lazy {
268 let mut element = element.clone();
270
271 element.props.insert(
273 "__lazy".to_string(),
274 super::Value::Static(serde_json::json!(true)),
275 );
276
277 #[cfg(all(target_arch = "wasm32", feature = "js"))]
278 web_sys::console::log_1(
279 &format!(
280 "Lazy {} (props: {:?}): {} children kept unexpanded",
281 element.element_type,
282 element.props.keys().collect::<Vec<_>>(),
283 element.children.len()
284 )
285 .into(),
286 );
287
288 return element;
289 }
290
291 if component.passthrough {
293 let mut element = element.clone();
295
296 #[cfg(all(target_arch = "wasm32", feature = "js"))]
297 {
298 let props_str = element
299 .props
300 .iter()
301 .map(|(k, v)| format!("{}={:?}", k, v))
302 .collect::<Vec<_>>()
303 .join(", ");
304 web_sys::console::log_1(
305 &format!(
306 "Passthrough {} (props: [{}]): {} children before expansion",
307 element.element_type,
308 props_str,
309 element.children.len()
310 )
311 .into(),
312 );
313 }
314
315 let child_context = component.source_path.clone();
317 let child_context_ref = child_context.as_deref();
318
319 element.children = element
321 .children
322 .into_iter()
323 .map(|child| Arc::new(self.expand_with_context(&child, child_context_ref)))
324 .collect();
325
326 #[cfg(all(target_arch = "wasm32", feature = "js"))]
327 web_sys::console::log_1(
328 &format!(
329 "Passthrough {}: {} children after expansion",
330 element.element_type,
331 element.children.len()
332 )
333 .into(),
334 );
335
336 element
337 } else {
338 let mut props = IndexMap::new();
341 for (k, v) in &element.props {
342 if let super::Value::Static(val) = v {
343 props.insert(k.clone(), val.clone());
344 }
345 }
346
347 let mut expanded = component.instantiate(props);
348
349 for (k, v) in &element.props {
351 match v {
352 super::Value::Binding(_) | super::Value::Action(_) => {
353 expanded.props.insert(k.clone(), v.clone());
354 }
355 _ => {}
356 }
357 }
358
359 let child_context = component.source_path.clone();
362
363 expanded.children = self.replace_children_slots(
365 &expanded.children,
366 &element.children,
367 context_path,
368 );
369
370 let child_context_ref = child_context.as_deref();
372 expanded.children = expanded
373 .children
374 .into_iter()
375 .map(|child| Arc::new(self.expand_with_context(&child, child_context_ref)))
376 .collect();
377
378 expanded
379 }
380 } else {
381 let mut element = element.clone();
383 element.children = element
384 .children
385 .into_iter()
386 .map(|child| Arc::new(self.expand_with_context(&child, context_path)))
387 .collect();
388 element
389 }
390 }
391
392 pub fn expand_ir_node(&mut self, node: &super::IRNode) -> super::IRNode {
394 self.expand_ir_node_with_context(node, None)
395 }
396
397 fn expand_ir_node_with_context(
399 &mut self,
400 node: &super::IRNode,
401 context_path: Option<&str>,
402 ) -> super::IRNode {
403 match node {
404 super::IRNode::Element(element) => {
405 let expanded = self.expand_with_context(element, context_path);
407 super::IRNode::Element(expanded)
408 }
409 super::IRNode::ForEach {
410 source,
411 item_name,
412 key_path,
413 template,
414 props,
415 } => {
416 let expanded_template: Vec<super::IRNode> = template
418 .iter()
419 .map(|child| self.expand_ir_node_with_context(child, context_path))
420 .collect();
421
422 super::IRNode::ForEach {
423 source: source.clone(),
424 item_name: item_name.clone(),
425 key_path: key_path.clone(),
426 template: expanded_template,
427 props: props.clone(),
428 }
429 }
430 super::IRNode::Conditional {
431 value,
432 branches,
433 fallback,
434 } => {
435 let expanded_branches: Vec<super::ConditionalBranch> = branches
437 .iter()
438 .map(|branch| super::ConditionalBranch {
439 pattern: branch.pattern.clone(),
440 children: branch
441 .children
442 .iter()
443 .map(|child| self.expand_ir_node_with_context(child, context_path))
444 .collect(),
445 })
446 .collect();
447
448 let expanded_fallback = fallback.as_ref().map(|fb| {
450 fb.iter()
451 .map(|child| self.expand_ir_node_with_context(child, context_path))
452 .collect()
453 });
454
455 super::IRNode::Conditional {
456 value: value.clone(),
457 branches: expanded_branches,
458 fallback: expanded_fallback,
459 }
460 }
461 }
462 }
463
464 fn replace_children_slots(
467 &self,
468 template_children: &im::Vector<Arc<Element>>,
469 actual_children: &im::Vector<Arc<Element>>,
470 _context_path: Option<&str>,
471 ) -> im::Vector<Arc<Element>> {
472 let mut result = im::Vector::new();
473
474 for child in template_children {
475 if child.element_type == "Children" {
476 let slot_name = self.get_slot_name(&child.props);
479
480 if let Some(slot) = slot_name {
483 for c in actual_children.iter() {
484 if self.get_slot_name(&c.props) == Some(slot) {
485 result.push_back(Arc::clone(c));
486 }
487 }
488 } else {
489 for c in actual_children.iter() {
491 if self.get_slot_name(&c.props).is_none() {
492 result.push_back(Arc::clone(c));
493 }
494 }
495 }
496 } else {
497 let mut new_child = (**child).clone();
499 new_child.children =
500 self.replace_children_slots(&child.children, actual_children, _context_path);
501 result.push_back(Arc::new(new_child));
502 }
503 }
504
505 result
506 }
507
508 fn get_slot_name<'a>(&self, props: &'a super::Props) -> Option<&'a str> {
511 props.get("slot.0").and_then(|v| {
512 if let super::Value::Static(serde_json::Value::String(s)) = v {
513 Some(s.as_str())
514 } else {
515 None
516 }
517 })
518 }
519}
520
521impl Default for ComponentRegistry {
522 fn default() -> Self {
523 Self::new()
524 }
525}
526
527#[cfg(test)]
528mod tests {
529 use super::*;
530 use crate::ir::Value;
531
532 #[test]
533 fn test_dynamic_component_resolution() {
534 let mut registry = ComponentRegistry::new();
535
536 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
538 if name == "Header" {
539 Some(ResolvedComponent {
540 source: r#"Row { Text("Header") }"#.to_string(),
541 path: "/components/Header.hypen".to_string(),
542 passthrough: false,
543 lazy: false,
544 })
545 } else {
546 None
547 }
548 }));
549
550 let element = Element::new("Column").with_child(Element::new("Header"));
552
553 let expanded = registry.expand(&element);
555
556 assert_eq!(expanded.element_type, "Column");
558 assert_eq!(expanded.children.len(), 1);
559 assert_eq!(expanded.children[0].element_type, "Row");
560 assert_eq!(expanded.children[0].children[0].element_type, "Text");
561 }
562
563 #[test]
564 fn test_component_resolution_with_path_context() {
565 let mut registry = ComponentRegistry::new();
566
567 registry.set_resolver(Arc::new(|name: &str, context: Option<&str>| {
569 match (name, context) {
570 ("Button", Some("/pages/Home.hypen")) => Some(ResolvedComponent {
571 source: r#"Text("Home Button")"#.to_string(),
572 path: "/components/buttons/HomeButton.hypen".to_string(),
573 passthrough: false,
574 lazy: false,
575 }),
576 ("Button", Some("/pages/About.hypen")) => Some(ResolvedComponent {
577 source: r#"Text("About Button")"#.to_string(),
578 path: "/components/buttons/AboutButton.hypen".to_string(),
579 passthrough: false,
580 lazy: false,
581 }),
582 _ => None,
583 }
584 }));
585
586 let home_element =
588 Element::new("Text").with_prop("0", Value::Static(serde_json::json!("Home")));
589 let home_component = Component::new("Home", move |_| home_element.clone())
590 .with_source_path("/pages/Home.hypen");
591 registry.register(home_component);
592
593 let element = Element::new("Column")
595 .with_child(Element::new("Home").with_child(Element::new("Button")));
596
597 let expanded = registry.expand(&element);
598
599 assert_eq!(expanded.element_type, "Column");
601 }
602
603 #[test]
604 fn test_component_resolution_caching() {
605 let mut registry = ComponentRegistry::new();
606 let call_count = Arc::new(std::sync::Mutex::new(0));
607 let call_count_clone = call_count.clone();
608
609 registry.set_resolver(Arc::new(move |name: &str, _context: Option<&str>| {
611 if name == "Button" {
612 *call_count_clone.lock().unwrap() += 1;
613 Some(ResolvedComponent {
614 source: r#"Text("Click")"#.to_string(),
615 path: "/components/Button.hypen".to_string(),
616 passthrough: false,
617 lazy: false,
618 })
619 } else {
620 None
621 }
622 }));
623
624 let element1 = Element::new("Button");
626 let _ = registry.expand(&element1);
627 assert_eq!(*call_count.lock().unwrap(), 1);
628
629 let element2 = Element::new("Button");
631 let _ = registry.expand(&element2);
632 assert_eq!(*call_count.lock().unwrap(), 1); }
634
635 #[test]
636 fn test_failed_resolution_cached() {
637 let mut registry = ComponentRegistry::new();
638 let call_count = Arc::new(std::sync::Mutex::new(0));
639 let call_count_clone = call_count.clone();
640
641 registry.set_resolver(Arc::new(move |_name: &str, _context: Option<&str>| {
643 *call_count_clone.lock().unwrap() += 1;
644 None
645 }));
646
647 let element1 = Element::new("Unknown");
649 let _ = registry.expand(&element1);
650 assert_eq!(*call_count.lock().unwrap(), 1);
651
652 let element2 = Element::new("Unknown");
654 let _ = registry.expand(&element2);
655 assert_eq!(*call_count.lock().unwrap(), 1); }
657
658 #[test]
659 fn test_passthrough_component_preserves_props() {
660 let mut registry = ComponentRegistry::new();
661
662 registry.set_resolver(Arc::new(|name: &str, _context: Option<&str>| {
664 if name == "Router" || name == "Route" {
665 Some(ResolvedComponent {
666 source: String::new(), path: name.to_string(),
668 passthrough: true,
669 lazy: false,
670 })
671 } else if name == "HomePage" {
672 Some(ResolvedComponent {
674 source: "Text(\"Home\")".to_string(),
675 path: name.to_string(),
676 passthrough: false,
677 lazy: false,
678 })
679 } else {
680 None
681 }
682 }));
683
684 let mut router = Element::new("Router");
690
691 let mut route1 = Element::new("Route");
692 route1
693 .props
694 .insert("0".to_string(), Value::Static(serde_json::json!("/")));
695 route1
696 .children
697 .push_back(std::sync::Arc::new(Element::new("HomePage")));
698
699 let mut route2 = Element::new("Route");
700 route2
701 .props
702 .insert("0".to_string(), Value::Static(serde_json::json!("/about")));
703 route2
704 .children
705 .push_back(std::sync::Arc::new(Element::new("HomePage")));
706
707 router.children.push_back(std::sync::Arc::new(route1));
708 router.children.push_back(std::sync::Arc::new(route2));
709
710 let expanded = registry.expand(&router);
712
713 assert_eq!(expanded.element_type, "Router");
715 assert_eq!(expanded.children.len(), 2);
716
717 let expanded_route1 = &expanded.children[0];
719 assert_eq!(expanded_route1.element_type, "Route");
720 if let Some(Value::Static(path)) = expanded_route1.props.get("0") {
721 assert_eq!(path.as_str().unwrap(), "/");
722 } else {
723 panic!("Route 1 missing path prop");
724 }
725
726 let expanded_route2 = &expanded.children[1];
728 assert_eq!(expanded_route2.element_type, "Route");
729 if let Some(Value::Static(path)) = expanded_route2.props.get("0") {
730 assert_eq!(path.as_str().unwrap(), "/about");
731 } else {
732 panic!("Route 2 missing path prop");
733 }
734
735 assert_eq!(expanded_route1.children.len(), 1);
737 assert_eq!(expanded_route1.children[0].element_type, "Text");
738 }
739}