1use std::collections::HashMap;
11use std::hash::{Hash, Hasher};
12use std::sync::Arc;
13
14use indexmap::IndexMap;
15use serde_json::Value;
16
17use crate::error::Result;
18
19pub fn compute_props_hash(props: &Value) -> u64 {
23 let mut hasher = std::collections::hash_map::DefaultHasher::new();
24 let json_str = serde_json::to_string(props).unwrap_or_default();
25 json_str.hash(&mut hasher);
26 hasher.finish()
27}
28
29pub fn compute_cache_key(path: &str, props_hash: u64) -> u64 {
31 let mut hasher = std::collections::hash_map::DefaultHasher::new();
32 path.hash(&mut hasher);
33 props_hash.hash(&mut hasher);
34 hasher.finish()
35}
36
37#[derive(Debug, Clone, PartialEq)]
41pub enum PropType {
42 String,
43 Number,
44 Boolean,
45 Array,
46 Object,
47 Any,
48}
49
50impl PropType {
51 #[allow(clippy::should_implement_trait)]
55 pub fn from_str(s: &str) -> Self {
56 match s.to_lowercase().as_str() {
57 "string" => PropType::String,
58 "number" => PropType::Number,
59 "boolean" => PropType::Boolean,
60 "array" => PropType::Array,
61 "object" => PropType::Object,
62 _ => PropType::Any,
63 }
64 }
65
66 pub fn matches(&self, value: &Value) -> bool {
68 match self {
69 PropType::Any => true,
70 PropType::String => value.is_string(),
71 PropType::Number => value.is_number(),
72 PropType::Boolean => value.is_boolean(),
73 PropType::Array => value.is_array(),
74 PropType::Object => value.is_object(),
75 }
76 }
77}
78
79#[derive(Debug, Clone)]
81pub struct PropDef {
82 pub name: String,
83 pub prop_type: PropType,
84 pub required: bool,
85 pub default: Option<Value>,
86}
87
88impl PropDef {
89 pub fn validate(&self, value: &Value) -> std::result::Result<(), String> {
95 if value.is_null() && self.required {
96 return Err(format!("Required prop '{}' is null", self.name));
97 }
98 if !self.prop_type.matches(value) {
99 return Err(format!(
100 "Prop '{}' type mismatch: expected {:?}, got {}",
101 self.name,
102 self.prop_type,
103 match value {
104 Value::Null => "null",
105 Value::Bool(_) => "boolean",
106 Value::Number(_) => "number",
107 Value::String(_) => "string",
108 Value::Array(_) => "array",
109 Value::Object(_) => "object",
110 }
111 ));
112 }
113 Ok(())
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct Component {
120 pub path: String,
121 pub props: Vec<PropDef>,
122 pub template: String,
123 pub slots: Vec<String>,
124 pub optional_slots: Vec<String>,
125 pub scoped_slots: Vec<ScopedSlotDef>,
126}
127
128#[derive(Debug, Clone)]
130pub struct ScopedSlotDef {
131 pub name: String,
132 pub props: Vec<String>,
133}
134
135#[derive(Debug, Clone, Default)]
137pub struct SlotData {
138 pub fills: IndexMap<String, String>,
139 pub default: Option<String>,
140 pub scoped_data: HashMap<String, Value>,
141}
142
143#[derive(Clone, Default)]
147pub struct ComponentRenderer {
148 stack_buffers: HashMap<String, Vec<String>>,
149 slot_data: HashMap<String, SlotData>,
150 once_rendered: std::collections::HashSet<u64>,
151}
152
153impl ComponentRenderer {
154 pub fn new() -> Self {
156 ComponentRenderer {
157 stack_buffers: HashMap::new(),
158 slot_data: HashMap::new(),
159 once_rendered: std::collections::HashSet::new(),
160 }
161 }
162
163 pub fn push(&mut self, name: &str, content: String) {
165 self.stack_buffers
166 .entry(name.to_string())
167 .or_default()
168 .push(content);
169 }
170
171 pub fn prepend(&mut self, name: &str, content: String) {
173 self.stack_buffers
174 .entry(name.to_string())
175 .or_default()
176 .insert(0, content);
177 }
178
179 pub fn drain(&mut self, name: &str) -> String {
181 self.stack_buffers
182 .remove(name)
183 .map(|v| v.join("\n"))
184 .unwrap_or_default()
185 }
186
187 pub fn peek(&self, name: &str) -> Option<String> {
189 self.stack_buffers.get(name).map(|v| v.join("\n"))
190 }
191
192 pub fn set_slot_fill(&mut self, slot_name: &str, content: String) {
194 let slot_name = slot_name.trim_start_matches('$').to_string();
195 let slot = self.slot_data.entry(slot_name.clone()).or_default();
196 if slot_name == "default" || slot_name.is_empty() {
197 slot.default = Some(content);
198 } else {
199 slot.fills.insert(slot_name, content);
200 }
201 }
202
203 pub fn get_slot(&self, name: &str) -> Option<String> {
205 let name = name.trim_start_matches('$').to_string();
206 if name == "default" || name.is_empty() {
207 self.slot_data
208 .get("default")
209 .and_then(|s| s.default.clone())
210 } else {
211 self.slot_data
212 .get(&name)
213 .and_then(|s| s.fills.get(&name).cloned())
214 }
215 }
216
217 pub fn has_slot(&self, name: &str) -> bool {
219 let name = name.trim_start_matches('$').to_string();
220 if name == "default" || name.is_empty() {
221 self.slot_data
222 .get("default")
223 .map(|s| s.default.is_some())
224 .unwrap_or(false)
225 } else {
226 self.slot_data
227 .get(&name)
228 .map(|s| s.fills.contains_key(&name))
229 .unwrap_or(false)
230 }
231 }
232
233 pub fn set_scoped_data(&mut self, slot_name: &str, key: &str, value: Value) {
235 let slot_name = slot_name.trim_start_matches('$').to_string();
236 let slot = self.slot_data.entry(slot_name).or_default();
237 slot.scoped_data.insert(key.to_string(), value);
238 }
239
240 pub fn get_scoped_data(&self, slot_name: &str) -> Option<HashMap<String, Value>> {
242 let name = slot_name.trim_start_matches('$').to_string();
243 self.slot_data.get(&name).map(|s| s.scoped_data.clone())
244 }
245
246 pub fn once(&mut self, key: u64) -> bool {
250 if self.once_rendered.contains(&key) {
251 return false;
252 }
253 self.once_rendered.insert(key);
254 true
255 }
256
257 pub fn reset(&mut self) {
259 self.stack_buffers.clear();
260 self.slot_data.clear();
261 }
262}
263
264#[derive(Clone, Default)]
266pub struct ComponentRegistry {
267 components: HashMap<String, Component>,
268 cache: Arc<std::sync::RwLock<ComponentCache>>,
269 cache_enabled: bool,
270}
271
272#[derive(Default)]
274pub struct ComponentCache {
275 entries: HashMap<u64, CachedRender>,
276}
277
278#[derive(Clone)]
280pub struct CachedRender {
281 pub html: String,
282 #[allow(dead_code)]
283 pub props_hash: u64,
284}
285
286impl ComponentCache {
287 pub fn new() -> Self {
289 ComponentCache {
290 entries: HashMap::new(),
291 }
292 }
293
294 pub fn get(&self, key: &u64) -> Option<String> {
296 self.entries.get(key).map(|c| c.html.clone())
297 }
298
299 pub fn insert(&mut self, key: u64, html: String, props_hash: u64) {
301 self.entries.insert(key, CachedRender { html, props_hash });
302 }
303
304 pub fn clear(&mut self) {
306 self.entries.clear();
307 }
308
309 pub fn len(&self) -> usize {
311 self.entries.len()
312 }
313
314 pub fn is_empty(&self) -> bool {
316 self.entries.is_empty()
317 }
318}
319
320impl ComponentRegistry {
321 pub fn new() -> Self {
323 ComponentRegistry {
324 components: HashMap::new(),
325 cache: Arc::new(std::sync::RwLock::new(ComponentCache::new())),
326 cache_enabled: false,
327 }
328 }
329
330 pub fn register(&mut self, path: &str, template: &str) -> Result<()> {
332 let props = Self::parse_props(template);
333 let (slots, optional_slots) = Self::parse_slots(template);
334 let scoped_slots = Self::parse_scoped_slots(template);
335
336 self.components.insert(
337 path.to_string(),
338 Component {
339 path: path.to_string(),
340 props,
341 template: template.to_string(),
342 slots,
343 optional_slots,
344 scoped_slots,
345 },
346 );
347
348 Ok(())
349 }
350
351 pub fn get(&self, path: &str) -> Option<&Component> {
353 self.components.get(path)
354 }
355
356 pub fn resolve_tag(&self, tag: &str) -> Option<String> {
361 if self.components.contains_key(tag) {
362 return Some(tag.to_string());
363 }
364
365 let with_prefix = format!("components/{}", tag);
366 if self.components.contains_key(&with_prefix) {
367 return Some(with_prefix);
368 }
369
370 let with_ext = format!("{}.html", tag);
371 if self.components.contains_key(&with_ext) {
372 return Some(with_ext);
373 }
374
375 let parts: Vec<&str> = tag.split('.').collect();
376 if parts.len() >= 2 {
377 let nested = format!("components/{}.html", parts.join("/"));
378 if self.components.contains_key(&nested) {
379 return Some(nested);
380 }
381 }
382
383 None
384 }
385
386 pub fn list_components(&self) -> Vec<String> {
388 self.components.keys().cloned().collect()
389 }
390
391 pub fn validate_props(
397 &self,
398 path: &str,
399 provided: &Value,
400 ) -> std::result::Result<HashMap<String, Value>, String> {
401 let component = self
402 .components
403 .get(path)
404 .ok_or_else(|| format!("Component '{}' not found", path))?;
405
406 let mut result = HashMap::new();
407 let provided_obj = provided.as_object().ok_or("Props must be an object")?;
408
409 for prop_def in &component.props {
410 if let Some(value) = provided_obj.get(&prop_def.name) {
411 prop_def.validate(value)?;
412 result.insert(prop_def.name.clone(), value.clone());
413 } else if let Some(default) = &prop_def.default {
414 result.insert(prop_def.name.clone(), default.clone());
415 } else if prop_def.required {
416 return Err(format!("Required prop '{}' not provided", prop_def.name));
417 }
418 }
419
420 Ok(result)
421 }
422
423 pub fn enable_cache(&mut self, enabled: bool) {
425 self.cache_enabled = enabled;
426 }
427
428 pub fn is_cache_enabled(&self) -> bool {
430 self.cache_enabled
431 }
432
433 pub fn get_cached(&self, key: u64) -> Option<String> {
435 if !self.cache_enabled {
436 return None;
437 }
438 self.cache.read().ok()?.get(&key)
439 }
440
441 pub fn set_cached(&self, key: u64, html: String, props_hash: u64) {
443 if !self.cache_enabled {
444 return;
445 }
446 if let Ok(mut cache) = self.cache.write() {
447 cache.insert(key, html, props_hash);
448 }
449 }
450
451 pub fn clear_cache(&self) {
453 if let Ok(mut cache) = self.cache.write() {
454 cache.clear();
455 }
456 }
457
458 pub fn cache_len(&self) -> usize {
460 self.cache.read().map(|c| c.len()).unwrap_or(0)
461 }
462
463 fn parse_scoped_slots(template: &str) -> Vec<ScopedSlotDef> {
464 let mut scoped_slots = Vec::new();
465
466 if let Some(start) = template.find("{%-- atom:") {
467 if let Some(end) = template[start..].find("--%}") {
468 let atom_block = &template[start + 11..start + end];
469 if let Some(slots_start) = atom_block.find("@scoped_slots(") {
470 let slots_end = atom_block[slots_start..]
471 .find(')')
472 .map(|i| slots_start + i + 1);
473 if let Some(end_idx) = slots_end {
474 let slots_str = &atom_block[slots_start + 14..end_idx];
475 for part in slots_str.split(',') {
476 let part = part.trim();
477 if part.is_empty() {
478 continue;
479 }
480 let parts: Vec<&str> = part.split(':').collect();
481 let name = parts[0].trim().to_string();
482 let props = if parts.len() > 1 {
483 parts[1]
484 .trim()
485 .split(',')
486 .map(|s| s.trim().to_string())
487 .collect()
488 } else {
489 vec![]
490 };
491 scoped_slots.push(ScopedSlotDef { name, props });
492 }
493 }
494 }
495 }
496 }
497
498 scoped_slots
499 }
500
501 fn parse_props(template: &str) -> Vec<PropDef> {
502 let mut props = Vec::new();
503
504 if let Some(start) = template.find("{%-- atom:") {
505 if let Some(end) = template[start..].find("--%}") {
506 let atom_block = &template[start + 11..start + end];
507 if let Some(props_start) = atom_block.find("@props(") {
508 let props_end = atom_block[props_start..]
509 .find(')')
510 .map(|i| props_start + i + 1);
511 if let Some(end_idx) = props_end {
512 let props_str = &atom_block[props_start + 7..end_idx];
513 props = Self::parse_props_string(props_str);
514 }
515 }
516 }
517 }
518
519 props
520 }
521
522 fn parse_props_string(s: &str) -> Vec<PropDef> {
523 let mut props = Vec::new();
524 let s = s.trim();
525 let s = s.trim_start_matches('{').trim_end_matches('}');
526
527 for part in s.split(',') {
528 let part = part.trim();
529 if part.is_empty() {
530 continue;
531 }
532
533 let parts: Vec<&str> = part.split(':').collect();
534 if parts.is_empty() {
535 continue;
536 }
537
538 let mut name = parts[0].trim().to_string();
539 let prop_type = if parts.len() > 1 {
540 PropType::from_str(parts[1].trim())
541 } else {
542 PropType::Any
543 };
544
545 let optional = name.ends_with('?');
546 if optional {
547 name = name.trim_end_matches('?').to_string();
548 }
549
550 let (required, default) = if let Some(eq_pos) = name.find('=') {
551 let default_str = &name[eq_pos + 1..];
552 let default = serde_json::Value::from(default_str.trim());
553 (false, Some(default))
554 } else {
555 (true, None)
556 };
557
558 name = name.split('=').next().unwrap_or(&name).trim().to_string();
559
560 let required = required && !optional;
561
562 props.push(PropDef {
563 name,
564 prop_type,
565 required,
566 default,
567 });
568 }
569
570 props
571 }
572
573 fn parse_slots(template: &str) -> (Vec<String>, Vec<String>) {
574 let mut slots = Vec::new();
575 let mut optional_slots = Vec::new();
576
577 let mut search = template;
578 while let Some(start) = search.find("slot_") {
579 search = &search[start + 5..];
580 if let Some(end) = search.find("()") {
581 let raw_name = &search[..end];
582 if raw_name.is_empty() {
583 continue;
584 }
585 let is_optional = raw_name.ends_with('?');
586 let name = if is_optional {
587 raw_name.trim_end_matches('?').to_string()
588 } else {
589 raw_name.to_string()
590 };
591
592 if is_optional {
593 if !optional_slots.contains(&name) {
594 optional_slots.push(name);
595 }
596 } else if !slots.contains(&name) {
597 slots.push(name);
598 }
599 }
600 }
601
602 (slots, optional_slots)
603 }
604}