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