Skip to main content

atom_engine/
lib.rs

1//! Atom Engine - A component-oriented template engine for Rust
2//! 
3//! # Overview
4//! 
5//! Atom Engine is a high-performance template engine built on Tera with additional
6//! features like components, props, slots, and provide/inject context.
7//! 
8//! # Features
9//! 
10//! - Built on Tera for robust template parsing and rendering
11//! - Component system with props, slots, and validation
12//! - Provide/Inject context (React-style)
13//! - Stack system for content accumulation
14//! - 50+ built-in filters
15//! - Helper directives (@map, @filter, @each, @reduce)
16//! - Async and parallel rendering support
17//! - Component caching
18//! 
19//! # Quick Start
20//! 
21//! ```rust
22//! use atom_engine::Atom;
23//! use serde_json::json;
24//! 
25//! let mut engine = Atom::new();
26//! engine.add_template("hello.html", "Hello, {{ name }}!").unwrap();
27//! let result = engine.render("hello.html", &json!({"name": "World"})).unwrap();
28//! assert_eq!(result, "Hello, World!");
29//! ```
30
31pub mod filters;
32
33use glob::glob;
34use serde_json::Value;
35use std::cell::RefCell;
36use std::rc::Rc;
37use tera::{Context, Filter, Function, Tera};
38
39mod components;
40mod context;
41mod error;
42mod pool;
43
44pub use components::{
45    compute_cache_key, compute_props_hash, Component, ComponentCache, ComponentRegistry,
46    ComponentRenderer, PropDef, PropType, ScopedSlotDef, SlotData,
47};
48pub use context::ContextChain;
49pub use error::Error;
50pub use pool::{MemoryPool, PooledString, StringPool};
51
52thread_local! {
53    static COMPONENT_RENDERER: RefCell<Option<Rc<RefCell<ComponentRenderer>>>> = const { RefCell::new(None) };
54}
55
56/// The main template engine struct.
57///
58/// Atom provides methods for creating templates, registering components,
59/// rendering templates, and managing context.
60///
61/// # Example
62///
63/// ```rust
64/// use atom_engine::Atom;
65/// use serde_json::json;
66///
67/// let mut engine = Atom::new();
68/// engine.add_template("greeting.html", "Hello, {{ name }}!").unwrap();
69/// let result = engine.render("greeting.html", &json!({"name": "Alice"})).unwrap();
70/// assert_eq!(result, "Hello, Alice!");
71/// ```
72#[derive(Clone)]
73pub struct Atom {
74    tera: Tera,
75    components: ComponentRegistry,
76    context_chain: ContextChain,
77    max_loop_iter: usize,
78    debug: bool,
79    use_parallel: bool,
80}
81
82impl Atom {
83    /// Creates a new Atom engine instance with all built-in filters and functions registered.
84    ///
85    /// # Example
86    ///
87    /// ```rust
88    /// use atom_engine::Atom;
89    ///
90    /// let engine = Atom::new();
91    /// ```
92    pub fn new() -> Self {
93        let mut tera = Tera::default();
94
95        // JSON
96        tera.register_filter("json_encode", filters::json_encode);
97
98        // String filters
99        tera.register_filter("upper", filters::upper);
100        tera.register_filter("lower", filters::lower);
101        tera.register_filter("capitalize", filters::capitalize);
102        tera.register_filter("title", filters::title);
103        tera.register_filter("camel_case", filters::camel_case);
104        tera.register_filter("pascal_case", filters::pascal_case);
105        tera.register_filter("snake_case", filters::snake_case);
106        tera.register_filter("kebab_case", filters::kebab_case);
107        tera.register_filter("truncate", filters::truncate);
108        tera.register_filter("slugify", filters::slugify);
109        tera.register_filter("pluralize", filters::pluralize);
110        tera.register_filter("replace", filters::replace);
111        tera.register_filter("remove", filters::remove);
112        tera.register_filter("prepend", filters::prepend);
113        tera.register_filter("append", filters::append);
114        tera.register_filter("strip", filters::strip);
115        tera.register_filter("nl2br", filters::nl2br);
116        tera.register_filter("word_count", filters::word_count);
117        tera.register_filter("char_count", filters::char_count);
118        tera.register_filter("starts_with", filters::starts_with);
119        tera.register_filter("ends_with", filters::ends_with);
120        tera.register_filter("contains", filters::contains);
121
122        // Collection filters
123        tera.register_filter("first", filters::first);
124        tera.register_filter("last", filters::last);
125        tera.register_filter("length", filters::length);
126        tera.register_filter("reverse", filters::reverse);
127        tera.register_filter("sort", filters::sort);
128        tera.register_filter("group_by", filters::group_by);
129        tera.register_filter("where", filters::where_filter);
130        tera.register_filter("pluck", filters::pluck);
131        tera.register_filter("join", filters::join);
132        tera.register_filter("slice", filters::slice);
133        tera.register_filter("uniq", filters::uniq);
134        tera.register_filter("shuffle", filters::shuffle);
135        tera.register_filter("map", filters::map_filter);
136        tera.register_filter("filter", filters::filter_filter);
137        tera.register_filter("each", filters::each_filter);
138        tera.register_filter("reduce", filters::reduce_filter);
139        tera.register_filter("flatten", filters::flatten_filter);
140        tera.register_filter("partition", filters::partition_filter);
141
142        // Number filters
143        tera.register_filter("round", filters::round);
144        tera.register_filter("abs", filters::abs);
145        tera.register_filter("format", filters::format_number);
146        tera.register_filter("min", filters::min_filter);
147        tera.register_filter("max", filters::max_filter);
148        tera.register_filter("sum", filters::sum);
149        tera.register_filter("avg", filters::avg);
150        tera.register_filter("ceil", filters::ceil);
151        tera.register_filter("floor", filters::floor);
152
153        // Date filters
154        tera.register_filter("date", filters::date_format);
155
156        // HTML filters
157        tera.register_filter("escape_html", filters::escape_html);
158        tera.register_filter("safe", filters::safe);
159
160        // Encoding filters
161        tera.register_filter("json_decode", filters::json_decode);
162        tera.register_filter("urlescape", filters::urlescape);
163        tera.register_filter("urlunescape", filters::urlunescape);
164        tera.register_filter("strip_tags", filters::strip_tags);
165        tera.register_filter("base64_encode", filters::base64_encode);
166        tera.register_filter("base64_decode", filters::base64_decode);
167
168        // Slot helpers
169        tera.register_filter("slot", filters::slot_filter);
170        tera.register_filter("has_slot", filters::has_slot_filter);
171        tera.register_filter("scoped_slot", filters::scoped_slot_filter);
172        tera.register_filter("with_scoped_data", filters::with_scoped_data_filter);
173
174        // Stack filter
175        tera.register_filter("stack", filters::stack_filter);
176
177        // Conditional filters
178        tera.register_filter("when", filters::when);
179        tera.register_filter("default", filters::default_filter);
180        tera.register_filter("coalesce", filters::coalesce);
181        tera.register_filter("defined", filters::defined);
182        tera.register_filter("undefined", filters::undefined);
183        tera.register_filter("empty", filters::empty);
184        tera.register_filter("not_empty", filters::not_empty);
185
186        // Global functions
187        tera.register_function("dump", filters::DumpFn);
188        tera.register_function("log", filters::LogFn);
189        tera.register_function("range", filters::RangeFn);
190        tera.register_function("now", filters::NowFn);
191        tera.register_function("cycle", filters::CycleFn::new());
192        tera.register_function("uuid", filters::UuidFn);
193        tera.register_function("random", filters::RandomFn);
194        tera.register_function("choice", filters::ChoiceFn);
195        tera.register_function("file_exists", filters::FileExistsFn);
196        tera.register_function("env", filters::EnvFn);
197        tera.register_function("md5", filters::Md5Fn);
198        tera.register_function("sha256", filters::Sha256Fn);
199        tera.register_function("repeat", filters::RepeatFn);
200        tera.register_function("times", filters::TimesFn);
201        tera.register_function("loop", filters::LoopFn);
202        tera.register_function("iterate", filters::IterateFn);
203        tera.register_function("object", filters::ObjectFn);
204        tera.register_function("merge", filters::MergeFn);
205        tera.register_function("chunk", filters::ChunkFn);
206        tera.register_function("zip", filters::ZipFn);
207        tera.register_function("compact", filters::CompactFn);
208
209        // Component functions
210        tera.register_function("push", filters::PushFn);
211        tera.register_function("prepend", filters::PrependFn);
212        tera.register_function("set_slot", filters::SetSlotFn);
213        tera.register_function("once", filters::OnceFn);
214
215        Atom {
216            tera,
217            components: ComponentRegistry::new(),
218            context_chain: ContextChain::new(),
219            max_loop_iter: 10000,
220            debug: false,
221            use_parallel: false,
222        }
223    }
224
225    /// Loads templates from the filesystem using a glob pattern.
226    ///
227    /// # Arguments
228    ///
229    /// * `glob_pattern` - A glob pattern to match template files (e.g., "templates/**/*.html")
230    ///
231    /// # Example
232    ///
233    /// ```rust,ignore
234    /// let mut engine = atom_engine::Atom::new();
235    /// engine.load_templates("templates/**/*.html").unwrap();
236    /// ```
237    pub fn load_templates(&mut self, glob_pattern: &str) -> std::result::Result<(), Error> {
238        let template_files: Vec<(std::path::PathBuf, Option<String>)> = glob(glob_pattern)
239            .map_err(|e| Error::TemplateLoad {
240                path: glob_pattern.to_string(),
241                message: e.to_string(),
242            })?
243            .filter_map(|p| p.ok())
244            .map(|p| {
245                let name = p
246                    .file_name()
247                    .and_then(|n| n.to_str())
248                    .map(|s| s.to_string())
249                    .unwrap_or_default();
250                (p, Some(name))
251            })
252            .collect();
253
254        self.tera
255            .add_template_files(template_files.into_iter())
256            .map_err(|e| Error::TemplateLoad {
257                path: glob_pattern.to_string(),
258                message: e.to_string(),
259            })?;
260        Ok(())
261    }
262
263    /// Adds a raw template to the engine.
264    ///
265    /// # Arguments
266    ///
267    /// * `name` - The template name (e.g., "index.html")
268    /// * `content` - The template content
269    ///
270    /// # Example
271    ///
272    /// ```rust,ignore
273    /// let mut engine = atom_engine::Atom::new();
274    /// engine.add_template("hello.html", "Hello, {{ name }}!").unwrap();
275    /// ```
276    pub fn add_template(&mut self, name: &str, content: &str) -> std::result::Result<(), Error> {
277        self.tera
278            .add_raw_template(name, content)
279            .map_err(|e| Error::TemplateParse {
280                name: name.to_string(),
281                message: e.to_string(),
282            })?;
283        Ok(())
284    }
285
286    /// Registers a reusable component.
287    ///
288    /// # Arguments
289    ///
290    /// * `path` - Component path/name (e.g., "button")
291    /// * `template` - Component template content
292    ///
293    /// # Example
294    ///
295    /// ```rust,ignore
296    /// let mut engine = atom_engine::Atom::new();
297    /// engine.register_component("button", "<button>{{ text }}</button>").unwrap();
298    /// ```
299    pub fn register_component(
300        &mut self,
301        path: &str,
302        template: &str,
303    ) -> std::result::Result<(), Error> {
304        self.components.register(path, template)
305    }
306
307    /// Registers a custom filter.
308    ///
309    /// # Arguments
310    ///
311    /// * `name` - Filter name
312    /// * `filter` - The filter function
313    pub fn register_filter<F>(&mut self, name: &str, filter: F)
314    where
315        F: Filter + Send + Sync + 'static,
316    {
317        self.tera.register_filter(name, filter);
318    }
319
320    /// Registers a custom global function.
321    ///
322    /// # Arguments
323    ///
324    /// * `name` - Function name
325    /// * `function` - The function to register
326    pub fn register_function<F>(&mut self, name: &str, function: F)
327    where
328        F: Function + Send + Sync + 'static,
329    {
330        self.tera.register_function(name, function);
331    }
332
333    /// Sets the maximum number of iterations for loops.
334    ///
335    /// This prevents infinite loops in templates.
336    pub fn set_max_loop_iter(&mut self, max: usize) {
337        self.max_loop_iter = max;
338    }
339
340    /// Sets debug mode for the engine.
341    ///
342    /// When enabled, additional debugging information may be logged.
343    pub fn set_debug(&mut self, debug: bool) {
344        self.debug = debug;
345    }
346
347    /// Renders a template with the given context.
348    ///
349    /// # Arguments
350    ///
351    /// * `template` - The template name to render
352    /// * `context` - The context data as a JSON value
353    ///
354    /// # Example
355    ///
356    /// ```rust
357    /// use atom_engine::Atom;
358    /// use serde_json::json;
359    ///
360    /// let mut engine = Atom::new();
361    /// engine.add_template("greeting.html", "Hello, {{ name }}!").unwrap();
362    /// let result = engine.render("greeting.html", &json!({"name": "World"})).unwrap();
363    /// assert_eq!(result, "Hello, World!");
364    /// ```
365    pub fn render(&self, template: &str, context: &Value) -> Result<String, Error> {
366        let mut ctx = Context::from_serialize(context).map_err(|e| Error::Context {
367            message: e.to_string(),
368        })?;
369
370        for (key, value) in self.context_chain.all() {
371            ctx.insert(key, &value);
372        }
373
374        ctx.insert("__atom_components", &self.components.list_components());
375
376        self.tera.render(template, &ctx).map_err(|e| Error::Render {
377            template: template.to_string(),
378            message: e.to_string(),
379        })
380    }
381
382    /// Renders a template with component data included in the context.
383    ///
384    /// This is useful when you want to pass additional component-specific data
385    /// alongside the regular context.
386    ///
387    /// # Arguments
388    ///
389    /// * `template` - The template name to render
390    /// * `context` - The context data as a JSON value
391    /// * `component_data` - Additional component-specific data
392    pub fn render_with_components(
393        &self,
394        template: &str,
395        context: &Value,
396        component_data: &Value,
397    ) -> std::result::Result<String, Error> {
398        let mut ctx = Context::from_serialize(context).map_err(|e| Error::Context {
399            message: e.to_string(),
400        })?;
401
402        if let Some(obj) = component_data.as_object() {
403            for (key, value) in obj {
404                ctx.insert(key, &value);
405            }
406        }
407
408        self.tera.render(template, &ctx).map_err(|e| Error::Render {
409            template: template.to_string(),
410            message: e.to_string(),
411        })
412    }
413
414    /// Provides a value to the context chain.
415    ///
416    /// This implements a provide/inject pattern (similar to Vue.js) where
417    /// values can be provided at a higher level and injected in child components.
418    ///
419    /// # Arguments
420    ///
421    /// * `key` - The context key
422    /// * `value` - The value to provide
423    ///
424    /// # Example
425    ///
426    /// ```rust,ignore
427    /// use atom_engine::Atom;
428    /// use serde_json::json;
429    ///
430    /// let mut engine = Atom::new();
431    /// engine.add_template("child.html", "{{ theme }}").unwrap();
432    /// engine.provide("theme", json!("dark"));
433    /// let result = engine.render("child.html", &json!({})).unwrap();
434    /// assert_eq!(result, "dark");
435    /// ```
436    pub fn provide(&mut self, key: &str, value: Value) {
437        self.context_chain.provide(key, value);
438    }
439
440    /// Reloads all templates from the filesystem.
441    ///
442    /// This is useful during development when templates change on disk.
443    pub fn reload(&mut self) -> std::result::Result<(), Error> {
444        self.tera.full_reload().map_err(|e| Error::TemplateLoad {
445            path: "reload".to_string(),
446            message: e.to_string(),
447        })
448    }
449
450    /// Checks if a template exists in the engine.
451    ///
452    /// # Arguments
453    ///
454    /// * `name` - The template name to check
455    ///
456    /// # Returns
457    ///
458    /// `true` if the template exists, `false` otherwise
459    pub fn template_exists(&self, name: &str) -> bool {
460        self.tera.get_template(name).is_ok()
461    }
462
463    /// Gets a list of all registered template names.
464    ///
465    /// # Returns
466    ///
467    /// A vector of template names
468    pub fn get_registered_templates(&self) -> Vec<String> {
469        self.tera
470            .get_template_names()
471            .map(|s| s.to_string())
472            .collect()
473    }
474
475    /// Clears the template cache.
476    ///
477    /// This forces templates to be re-parsed on next render.
478    pub fn clear_cache(&mut self) {
479        self.tera.templates.clear();
480    }
481
482    /// Enables or disables parallel rendering.
483    ///
484    /// When enabled with the `parallel` feature, multiple templates can be
485    /// rendered concurrently using Rayon.
486    pub fn set_parallel(&mut self, enabled: bool) {
487        self.use_parallel = enabled;
488    }
489
490    /// Returns whether parallel rendering is enabled.
491    pub fn is_parallel(&self) -> bool {
492        self.use_parallel
493    }
494
495    /// Enables or disables component caching.
496    ///
497    /// When enabled, rendered components are cached based on their props hash.
498    pub fn enable_component_cache(&mut self, enabled: bool) {
499        self.components.enable_cache(enabled);
500    }
501
502    /// Returns whether component caching is enabled.
503    pub fn is_component_cache_enabled(&self) -> bool {
504        self.components.is_cache_enabled()
505    }
506
507    /// Clears the component cache.
508    pub fn clear_component_cache(&mut self) {
509        self.components.clear_cache();
510    }
511
512    /// Returns the number of cached component renders.
513    pub fn component_cache_len(&self) -> usize {
514        self.components.cache_len()
515    }
516
517    /// Renders multiple templates in parallel (when the `parallel` feature is enabled).
518    ///
519    /// Requires the `parallel` feature to be enabled. Without it, templates
520    /// are rendered sequentially.
521    ///
522    /// # Arguments
523    ///
524    /// * `templates` - A slice of template name and context pairs
525    ///
526    /// # Returns
527    ///
528    /// A vector of (template_name, rendered_output) pairs
529    #[cfg(feature = "parallel")]
530    pub fn render_many(
531        &self,
532        templates: &[(&str, &Value)],
533    ) -> std::result::Result<Vec<(String, String)>, Error> {
534        use rayon::prelude::*;
535
536        let results: Vec<std::result::Result<(String, String), Error>> = templates
537            .par_iter()
538            .map(|(name, context)| {
539                let rendered = self.render(name, context)?;
540                Ok((name.to_string(), rendered))
541            })
542            .collect();
543
544        let mut output = Vec::new();
545        for result in results {
546            output.push(result?);
547        }
548        Ok(output)
549    }
550
551    /// Renders multiple templates sequentially.
552    ///
553    /// This is the fallback implementation when the `parallel` feature is not enabled.
554    #[cfg(not(feature = "parallel"))]
555    pub fn render_many(
556        &self,
557        templates: &[(&str, &Value)],
558    ) -> std::result::Result<Vec<(String, String)>, Error> {
559        let mut results = Vec::new();
560        for (name, context) in templates {
561            let rendered = self.render(name, context)?;
562            results.push((name.to_string(), rendered));
563        }
564        Ok(results)
565    }
566
567    /// Renders a template asynchronously.
568    ///
569    /// Requires the `async` feature to be enabled.
570    ///
571    /// # Arguments
572    ///
573    /// * `template` - The template name to render
574    /// * `context` - The context data as a JSON value
575    ///
576    /// # Example
577    ///
578    /// ```rust
579    /// #[cfg(feature = "async")]
580    /// async fn render_template() {
581    ///     use atom_engine::Atom;
582    ///     use serde_json::json;
583    ///
584    ///     let engine = Atom::new();
585    ///     let result = engine.render_async("hello.html", &json!({"name": "World"})).await;
586    /// }
587    /// ```
588    #[cfg(feature = "async")]
589    pub async fn render_async(&self, template: &str, context: &Value) -> Result<String, Error> {
590        let template = template.to_string();
591        let context = context.clone();
592
593        tokio::task::spawn_blocking(move || {
594            let mut tera = Tera::default();
595            tera.register_filter("json_encode", filters::json_encode);
596            tera.render(
597                &template,
598                &Context::from_serialize(&context).map_err(|e| Error::Context {
599                    message: e.to_string(),
600                })?,
601            )
602            .map_err(|e| Error::Render {
603                template,
604                message: e.to_string(),
605            })
606        })
607        .await
608        .map_err(|e| Error::Render {
609            template: "async".to_string(),
610            message: e.to_string(),
611        })?
612    }
613
614    /// Renders multiple templates asynchronously.
615    ///
616    /// Requires the `async` feature to be enabled.
617    ///
618    /// # Arguments
619    ///
620    /// * `templates` - A slice of template name and context pairs
621    ///
622    /// # Returns
623    ///
624    /// A vector of (template_name, rendered_output) pairs
625    #[cfg(feature = "async")]
626    pub async fn render_many_async(
627        &self,
628        templates: &[(&str, &Value)],
629    ) -> std::result::Result<Vec<(String, String)>, Error> {
630        use tokio::task::JoinSet;
631
632        let mut join_set = JoinSet::new();
633
634        for (name, context) in templates {
635            let name = name.to_string();
636            let context = context.clone();
637            let filters = filters::Filters::new();
638
639            join_set.spawn(async move {
640                tokio::task::spawn_blocking(move || {
641                    let mut tera = Tera::default();
642                    tera.register_filter("json_encode", filters::json_encode);
643                    let mut ctx =
644                        Context::from_serialize(&context).map_err(|e| Error::Context {
645                            message: e.to_string(),
646                        })?;
647                    tera.render(&name, &ctx)
648                        .map(|r| (name, r))
649                        .map_err(|e| Error::Render {
650                            template: name,
651                            message: e.to_string(),
652                        })
653                })
654                .await
655                .map_err(|e| Error::Render {
656                    template: "async".to_string(),
657                    message: e.to_string(),
658                })?
659            });
660        }
661
662        let mut results = Vec::new();
663        while let Some(result) = join_set.join_next().await {
664            results.push(result??);
665        }
666        Ok(results)
667    }
668}
669
670impl Default for Atom {
671    fn default() -> Self {
672        Self::new()
673    }
674}