Skip to main content

gracile_core/
renderer.rs

1//! Template renderer that evaluates the AST against a context to produce output.
2
3use std::collections::HashMap;
4
5use crate::ast::*;
6use crate::error::{Error, Result};
7use crate::value::{Value, html_escape, urlencode};
8
9/// Signature for user-supplied filter functions.
10///
11/// Receives the piped value and any parenthesised arguments, returns the
12/// transformed value or an error.
13///
14/// ```rust
15/// use gracile_core::{Engine, Value};
16///
17/// let engine = Engine::new()
18///     .register_filter("shout", |val, _args| {
19///         match val {
20///             Value::String(s) => Ok(Value::String(format!("{}!!!", s.to_uppercase()))),
21///             other => Ok(other),
22///         }
23///     });
24/// ```
25pub type FilterFn =
26    Box<dyn Fn(Value, Vec<Value>) -> crate::error::Result<Value> + Send + Sync + 'static>;
27
28/// Signature for a template loader function.
29///
30/// Receives a template name and returns the template source, or an error if
31/// the template cannot be found.  This lets you configure an `Engine` to load
32/// templates from the filesystem, a database, a map, or any other source
33/// without having to pre-register every template up front.
34///
35/// ```rust
36/// use gracile_core::{Engine, Value};
37/// use std::collections::HashMap;
38///
39/// let engine = Engine::new()
40///     .with_template_loader(|name| {
41///         match name {
42///             "greeting" => Ok("Hello, {= name}!".to_string()),
43///             other => Err(gracile_core::Error::RenderError {
44///                 message: format!("unknown template '{}'", other),
45///             }),
46///         }
47///     });
48///
49/// let mut ctx = HashMap::new();
50/// ctx.insert("name".to_string(), Value::from("World"));
51/// let out = engine.render_name("greeting", ctx).unwrap();
52/// assert_eq!(out, "Hello, World!");
53/// ```
54pub type LoaderFn = Box<dyn Fn(&str) -> crate::error::Result<String> + Send + Sync + 'static>;
55
56/// The Gracile templating engine.
57///
58/// ```rust
59/// use gracile_core::{Engine, Value};
60/// use std::collections::HashMap;
61///
62/// let mut ctx = HashMap::new();
63/// ctx.insert("name".to_string(), Value::from("World"));
64/// let output = Engine::new().render("Hello, {= name}!", ctx).unwrap();
65/// assert_eq!(output, "Hello, World!");
66/// ```
67pub struct Engine {
68    /// Raise an error on undefined variables / properties instead of returning null.
69    pub strict: bool,
70    /// Registered templates (name → source) available to `{@include}`.
71    templates: HashMap<String, String>,
72    /// User-registered filters, checked before built-ins.
73    filters: HashMap<String, FilterFn>,
74    /// Optional loader called when a template name is not in `templates`.
75    loader: Option<LoaderFn>,
76}
77
78impl Default for Engine {
79    fn default() -> Self {
80        Engine::new()
81    }
82}
83
84impl Engine {
85    pub fn new() -> Self {
86        Engine {
87            strict: false,
88            templates: HashMap::new(),
89            filters: HashMap::new(),
90            loader: None,
91        }
92    }
93
94    /// Enable strict mode (undefined variables are errors, not null).
95    pub fn with_strict(mut self) -> Self {
96        self.strict = true;
97        self
98    }
99
100    /// Register a custom filter function.
101    ///
102    /// User filters take precedence over built-ins, so you can override them if
103    /// needed. The filter receives the piped value and any parenthesised
104    /// arguments evaluated to `Value`.
105    pub fn register_filter<F>(mut self, name: impl Into<String>, f: F) -> Self
106    where
107        F: Fn(Value, Vec<Value>) -> crate::error::Result<Value> + Send + Sync + 'static,
108    {
109        self.filters.insert(name.into(), Box::new(f));
110        self
111    }
112
113    /// Register a named template that can be referenced by `{@include "name"}`.
114    pub fn register_template(mut self, name: impl Into<String>, source: impl Into<String>) -> Self {
115        self.templates.insert(name.into(), source.into());
116        self
117    }
118
119    /// Set a loader function that is called when a template name is not found
120    /// in the pre-registered map.
121    ///
122    /// This lets you lazily load templates from the filesystem, a cache, or
123    /// any other source without having to pre-register them all up front.
124    ///
125    /// ```rust
126    /// use gracile_core::Engine;
127    /// use std::collections::HashMap;
128    ///
129    /// # std::fs::write("/tmp/hello.html", "Hello, {= name}!").unwrap();
130    /// let engine = Engine::new()
131    ///     .with_template_loader(|name| {
132    ///         std::fs::read_to_string(format!("/tmp/{}", name))
133    ///             .map_err(|e| gracile_core::Error::RenderError {
134    ///                 message: format!("cannot load '{}': {}", name, e),
135    ///             })
136    ///     });
137    /// ```
138    pub fn with_template_loader<F>(mut self, loader: F) -> Self
139    where
140        F: Fn(&str) -> crate::error::Result<String> + Send + Sync + 'static,
141    {
142        self.loader = Some(Box::new(loader));
143        self
144    }
145
146    /// Resolve a template by name (pre-registered or via the loader) and render it.
147    ///
148    /// ```rust
149    /// use gracile_core::{Engine, Value};
150    /// use std::collections::HashMap;
151    ///
152    /// let engine = Engine::new()
153    ///     .with_template_loader(|name| match name {
154    ///         "greet" => Ok("Hello, {= who}!".to_string()),
155    ///         other => Err(gracile_core::Error::RenderError {
156    ///             message: format!("unknown template '{}'", other),
157    ///         }),
158    ///     });
159    ///
160    /// let mut ctx = HashMap::new();
161    /// ctx.insert("who".to_string(), Value::from("World"));
162    /// assert_eq!(engine.render_name("greet", ctx).unwrap(), "Hello, World!");
163    /// ```
164    pub fn render_name(&self, name: &str, context: HashMap<String, Value>) -> Result<String> {
165        let source = self.resolve_template(name)?;
166        self.render(&source, context)
167    }
168
169    /// Resolve a template name to its source string.
170    pub(crate) fn resolve_template(&self, name: &str) -> Result<String> {
171        if let Some(src) = self.templates.get(name) {
172            return Ok(src.clone());
173        }
174        if let Some(ref loader) = self.loader {
175            return loader(name);
176        }
177        Err(Error::RenderError {
178            message: format!("Template '{}' not found in engine registry", name),
179        })
180    }
181
182    /// Render `source` against `context` and return the produced string.
183    pub fn render(&self, source: &str, context: HashMap<String, Value>) -> Result<String> {
184        let tokens = crate::lexer::tokenize(source)?;
185        let template = crate::parser::parse(tokens)?;
186        let mut renderer = Renderer::new(self, context);
187        renderer.render_template(&template)
188    }
189
190    /// Pre-compile a source string into a `Template` AST (for repeated rendering).
191    pub fn compile(&self, source: &str) -> Result<Template> {
192        let tokens = crate::lexer::tokenize(source)?;
193        crate::parser::parse(tokens)
194    }
195
196    /// Render a pre-compiled template.
197    pub fn render_template(
198        &self,
199        template: &Template,
200        context: HashMap<String, Value>,
201    ) -> Result<String> {
202        let mut renderer = Renderer::new(self, context);
203        renderer.render_template(template)
204    }
205}
206
207#[cfg(feature = "serde")]
208fn context_from_serialize<S: serde::Serialize>(ctx: &S) -> Result<HashMap<String, Value>> {
209    let json = serde_json::to_value(ctx).map_err(|e| Error::RenderError {
210        message: e.to_string(),
211    })?;
212    match Value::from(json) {
213        Value::Object(map) => Ok(map),
214        other => Err(Error::RenderError {
215            message: format!(
216                "render context must serialise to a JSON object, got {}",
217                other.type_name()
218            ),
219        }),
220    }
221}
222
223#[cfg(feature = "serde")]
224impl Engine {
225    /// Like [`render`][Engine::render] but accepts any [`serde::Serialize`] value as context.
226    ///
227    /// This allows passing plain Rust structs annotated with `#[derive(Serialize)]`,
228    /// or a [`serde_json::json!`] literal, instead of building a
229    /// `HashMap<String, Value>` by hand.
230    ///
231    /// ```rust
232    /// # use gracile_core::Engine;
233    /// # use serde::Serialize;
234    /// #[derive(Serialize)]
235    /// struct Ctx { name: String }
236    ///
237    /// let out = Engine::new()
238    ///     .render_from("{= name}", &Ctx { name: "World".into() })
239    ///     .unwrap();
240    /// assert_eq!(out, "World");
241    /// ```
242    pub fn render_from<S: serde::Serialize>(&self, source: &str, ctx: &S) -> Result<String> {
243        self.render(source, context_from_serialize(ctx)?)
244    }
245
246    /// Like [`render_name`][Engine::render_name] but accepts any [`serde::Serialize`] as context.
247    pub fn render_name_from<S: serde::Serialize>(&self, name: &str, ctx: &S) -> Result<String> {
248        self.render_name(name, context_from_serialize(ctx)?)
249    }
250
251    /// Like [`render_template`][Engine::render_template] but accepts any [`serde::Serialize`] as context.
252    pub fn render_template_from<S: serde::Serialize>(
253        &self,
254        template: &Template,
255        ctx: &S,
256    ) -> Result<String> {
257        self.render_template(template, context_from_serialize(ctx)?)
258    }
259}
260
261/// Internal AST evaluator — walks the template tree and produces rendered output.
262struct Renderer<'e> {
263    engine: &'e Engine,
264    /// Scope stack: innermost scope is last.
265    scopes: Vec<HashMap<String, Value>>,
266    /// Hoisted snippet definitions (name → AST node).
267    snippets: HashMap<String, SnippetBlock>,
268}
269
270impl<'e> Renderer<'e> {
271    fn new(engine: &'e Engine, root_context: HashMap<String, Value>) -> Self {
272        Renderer {
273            engine,
274            scopes: vec![root_context],
275            snippets: HashMap::new(),
276        }
277    }
278
279    fn lookup(&self, name: &str) -> Option<&Value> {
280        for scope in self.scopes.iter().rev() {
281            if let Some(v) = scope.get(name) {
282                return Some(v);
283            }
284        }
285        None
286    }
287
288    fn lookup_value(&self, name: &str) -> Result<Value> {
289        match self.lookup(name) {
290            Some(v) => Ok(v.clone()),
291            None if self.engine.strict => Err(Error::RenderError {
292                message: format!("Undefined variable '{}'", name),
293            }),
294            None => Ok(Value::Null),
295        }
296    }
297
298    fn push_scope(&mut self, scope: HashMap<String, Value>) {
299        self.scopes.push(scope);
300    }
301
302    fn pop_scope(&mut self) {
303        self.scopes.pop();
304    }
305
306    fn render_template(&mut self, template: &Template) -> Result<String> {
307        // Hoist snippet definitions.
308        self.hoist_snippets(&template.nodes);
309        self.render_nodes(&template.nodes)
310    }
311
312    fn hoist_snippets(&mut self, nodes: &[Node]) {
313        for node in nodes {
314            if let Node::SnippetBlock(s) = node {
315                self.snippets.insert(s.name.clone(), s.clone());
316            }
317        }
318    }
319
320    fn render_nodes(&mut self, nodes: &[Node]) -> Result<String> {
321        let mut out = String::new();
322        for node in nodes {
323            out.push_str(&self.render_node(node)?);
324        }
325        Ok(out)
326    }
327
328    fn render_node(&mut self, node: &Node) -> Result<String> {
329        match node {
330            Node::RawText(t) => Ok(t.clone()),
331            Node::Comment(_) => Ok(String::new()),
332            Node::ExprTag(t) => {
333                let val = self.eval_expr(&t.expr)?;
334                if t.raw {
335                    Ok(val.to_display_string())
336                } else {
337                    Ok(val.html_escaped())
338                }
339            }
340            Node::IfBlock(b) => self.render_if(b),
341            Node::EachBlock(b) => self.render_each(b),
342            Node::SnippetBlock(_) => Ok(String::new()), // snippets render only when @render'd
343            Node::RawBlock(content) => Ok(content.clone()),
344            Node::RenderTag(t) => self.render_render_tag(t),
345            Node::ConstTag(t) => {
346                let val = self.eval_expr(&t.expr)?;
347                // Insert into the innermost scope.
348                self.scopes.last_mut().unwrap().insert(t.name.clone(), val);
349                Ok(String::new())
350            }
351            Node::IncludeTag(t) => self.render_include(t),
352            Node::DebugTag(t) => self.render_debug(t),
353        }
354    }
355
356    fn render_if(&mut self, block: &IfBlock) -> Result<String> {
357        for branch in &block.branches {
358            let cond = self.eval_expr(&branch.condition)?;
359            if cond.is_truthy() {
360                return self.render_nodes(&branch.body);
361            }
362        }
363        if let Some(else_body) = &block.else_body {
364            return self.render_nodes(else_body);
365        }
366        Ok(String::new())
367    }
368
369    fn render_each(&mut self, block: &EachBlock) -> Result<String> {
370        let iterable = self.eval_expr(&block.iterable)?;
371        let items = match iterable {
372            Value::Array(arr) => arr,
373            Value::Null => Vec::new(),
374            other => {
375                return Err(Error::RenderError {
376                    message: format!("{{#each}} expects an array, got {}", other.type_name()),
377                });
378            }
379        };
380
381        if items.is_empty() {
382            if let Some(else_body) = &block.else_body {
383                return self.render_nodes(else_body);
384            }
385            return Ok(String::new());
386        }
387
388        let len = items.len();
389        let mut out = String::new();
390        for (i, item) in items.iter().enumerate() {
391            let mut scope = HashMap::new();
392            // Bind the loop variable(s).
393            match &block.pattern {
394                Pattern::Ident(name) => {
395                    scope.insert(name.clone(), item.clone());
396                }
397                Pattern::Destructure(keys) => {
398                    if let Value::Object(map) = item {
399                        for key in keys {
400                            let val = map.get(key).cloned().unwrap_or(Value::Null);
401                            scope.insert(key.clone(), val);
402                        }
403                    } else {
404                        return Err(Error::RenderError {
405                            message: format!(
406                                "Destructuring pattern requires an object, got {}",
407                                item.type_name()
408                            ),
409                        });
410                    }
411                }
412            }
413            if let Some(idx_name) = &block.index_binding {
414                scope.insert(idx_name.clone(), Value::Int(i as i64));
415            }
416            if let Some(loop_name) = &block.loop_binding {
417                let mut meta = HashMap::new();
418                meta.insert("index".to_string(), Value::Int(i as i64));
419                meta.insert("length".to_string(), Value::Int(len as i64));
420                meta.insert("first".to_string(), Value::Bool(i == 0));
421                meta.insert("last".to_string(), Value::Bool(i == len - 1));
422                scope.insert(loop_name.clone(), Value::Object(meta));
423            }
424            self.push_scope(scope);
425            out.push_str(&self.render_nodes(&block.body)?);
426            self.pop_scope();
427        }
428        Ok(out)
429    }
430
431    fn render_render_tag(&mut self, tag: &RenderTag) -> Result<String> {
432        let snippet = self
433            .snippets
434            .get(&tag.name)
435            .cloned()
436            .ok_or_else(|| Error::RenderError {
437                message: format!("Unknown snippet '{}'", tag.name),
438            })?;
439        if snippet.params.len() != tag.args.len() {
440            return Err(Error::RenderError {
441                message: format!(
442                    "Snippet '{}' expects {} argument(s), got {}",
443                    tag.name,
444                    snippet.params.len(),
445                    tag.args.len()
446                ),
447            });
448        }
449        // Evaluate arguments in the current scope before entering the snippet scope.
450        let mut arg_values = Vec::with_capacity(tag.args.len());
451        for arg in &tag.args {
452            arg_values.push(self.eval_expr(arg)?);
453        }
454        let mut scope = HashMap::new();
455        for (name, val) in snippet.params.iter().zip(arg_values) {
456            scope.insert(name.clone(), val);
457        }
458        self.push_scope(scope);
459        let result = self.render_nodes(&snippet.body.clone());
460        self.pop_scope();
461        result
462    }
463
464    fn render_include(&mut self, tag: &IncludeTag) -> Result<String> {
465        let source = self.engine.resolve_template(&tag.path)?;
466        // Collect the current top-scope context snapshot for the included template.
467        let ctx: HashMap<String, Value> = self
468            .scopes
469            .iter()
470            .flat_map(|s| s.iter().map(|(k, v)| (k.clone(), v.clone())))
471            .collect();
472        self.engine
473            .render(&source, ctx)
474            .map_err(|e| Error::RenderError {
475                message: format!("In template '{}': {}", tag.path, e),
476            })
477    }
478
479    fn render_debug(&self, tag: &DebugTag) -> Result<String> {
480        // In a library context we emit nothing; the intent is dev tooling.
481        // A host application can replace this behaviour by post-processing the AST.
482        let _ = tag;
483        Ok(String::new())
484    }
485
486    fn eval_expr(&mut self, expr: &Expr) -> Result<Value> {
487        match expr {
488            Expr::Null => Ok(Value::Null),
489            Expr::Bool(b) => Ok(Value::Bool(*b)),
490            Expr::Int(i) => Ok(Value::Int(*i)),
491            Expr::Float(f) => Ok(Value::Float(*f)),
492            Expr::String(s) => Ok(Value::String(s.clone())),
493            Expr::Array(elements) => {
494                let mut arr = Vec::with_capacity(elements.len());
495                for e in elements {
496                    arr.push(self.eval_expr(e)?);
497                }
498                Ok(Value::Array(arr))
499            }
500            Expr::Ident(name) => self.lookup_value(name),
501            Expr::MemberAccess { object, property } => {
502                let obj = self.eval_expr(object)?;
503                self.get_property(&obj, property)
504            }
505            Expr::IndexAccess { object, index } => {
506                let obj = self.eval_expr(object)?;
507                let idx = self.eval_expr(index)?;
508                self.get_index(&obj, &idx)
509            }
510            Expr::Filter { expr, filters } => {
511                let mut val = self.eval_expr(expr)?;
512                for f in filters {
513                    let mut arg_vals = Vec::with_capacity(f.args.len());
514                    for a in &f.args {
515                        arg_vals.push(self.eval_expr(a)?);
516                    }
517                    val = if let Some(custom) = self.engine.filters.get(f.name.as_str()) {
518                        custom(val, arg_vals)?
519                    } else {
520                        apply_filter(val, &f.name, arg_vals)?
521                    };
522                }
523                Ok(val)
524            }
525            Expr::Ternary {
526                condition,
527                consequent,
528                alternate,
529            } => {
530                let cond = self.eval_expr(condition)?;
531                if cond.is_truthy() {
532                    self.eval_expr(consequent)
533                } else {
534                    self.eval_expr(alternate)
535                }
536            }
537            Expr::Binary { op, left, right } => self.eval_binary(op, left, right),
538            Expr::Unary { op, operand } => self.eval_unary(op, operand),
539            Expr::Test {
540                expr,
541                negated,
542                test_name,
543            } => {
544                let val = self.eval_expr(expr)?;
545                let result = eval_test(&val, test_name, self)?;
546                Ok(Value::Bool(if *negated { !result } else { result }))
547            }
548            Expr::Membership {
549                expr,
550                negated,
551                collection,
552            } => {
553                let val = self.eval_expr(expr)?;
554                let coll = self.eval_expr(collection)?;
555                let member = eval_membership(&val, &coll)?;
556                Ok(Value::Bool(if *negated { !member } else { member }))
557            }
558        }
559    }
560
561    fn get_property(&self, obj: &Value, prop: &str) -> Result<Value> {
562        match obj {
563            Value::Object(map) => Ok(map.get(prop).cloned().unwrap_or(Value::Null)).and_then(|v| {
564                if self.engine.strict
565                    && !obj.is_null()
566                    && let Value::Object(map) = obj
567                    && !map.contains_key(prop)
568                {
569                    return Err(Error::RenderError {
570                        message: format!("Property '{}' not found on object", prop),
571                    });
572                }
573                Ok(v)
574            }),
575            Value::Null => {
576                if self.engine.strict {
577                    Err(Error::RenderError {
578                        message: format!("Cannot access property '{}' on null", prop),
579                    })
580                } else {
581                    Ok(Value::Null)
582                }
583            }
584            other => {
585                if self.engine.strict {
586                    Err(Error::RenderError {
587                        message: format!(
588                            "Cannot access property '{}' on {}",
589                            prop,
590                            other.type_name()
591                        ),
592                    })
593                } else {
594                    Ok(Value::Null)
595                }
596            }
597        }
598    }
599
600    fn get_index(&self, obj: &Value, idx: &Value) -> Result<Value> {
601        match obj {
602            Value::Array(arr) => {
603                let i = match idx {
604                    Value::Int(i) => *i,
605                    other => {
606                        return Err(Error::RenderError {
607                            message: format!(
608                                "Array index must be an integer, got {}",
609                                other.type_name()
610                            ),
611                        });
612                    }
613                };
614                let len = arr.len() as i64;
615                let i = if i < 0 { len + i } else { i };
616                if i < 0 || i >= len {
617                    if self.engine.strict {
618                        Err(Error::RenderError {
619                            message: format!("Array index {} out of bounds (len {})", i, len),
620                        })
621                    } else {
622                        Ok(Value::Null)
623                    }
624                } else {
625                    Ok(arr[i as usize].clone())
626                }
627            }
628            Value::Object(map) => {
629                let key = match idx {
630                    Value::String(s) => s.clone(),
631                    other => other.to_display_string(),
632                };
633                Ok(map.get(&key).cloned().unwrap_or(Value::Null))
634            }
635            Value::Null => {
636                if self.engine.strict {
637                    Err(Error::RenderError {
638                        message: "Cannot index into null".to_string(),
639                    })
640                } else {
641                    Ok(Value::Null)
642                }
643            }
644            other => Err(Error::RenderError {
645                message: format!("Cannot index into {}", other.type_name()),
646            }),
647        }
648    }
649
650    fn eval_binary(&mut self, op: &BinaryOp, left: &Expr, right: &Expr) -> Result<Value> {
651        // Short-circuit operators evaluated first.
652        match op {
653            BinaryOp::Or => {
654                let l = self.eval_expr(left)?;
655                if l.is_truthy() {
656                    return Ok(l);
657                }
658                return self.eval_expr(right);
659            }
660            BinaryOp::And => {
661                let l = self.eval_expr(left)?;
662                if !l.is_truthy() {
663                    return Ok(l);
664                }
665                return self.eval_expr(right);
666            }
667            BinaryOp::NullCoalesce => {
668                let l = self.eval_expr(left)?;
669                if !l.is_null() {
670                    return Ok(l);
671                }
672                return self.eval_expr(right);
673            }
674            _ => {}
675        }
676
677        let l = self.eval_expr(left)?;
678        let r = self.eval_expr(right)?;
679
680        match op {
681            BinaryOp::Eq => Ok(Value::Bool(values_equal(&l, &r))),
682            BinaryOp::Neq => Ok(Value::Bool(!values_equal(&l, &r))),
683            BinaryOp::Lt => {
684                compare_values(&l, &r).map(|o| Value::Bool(o == std::cmp::Ordering::Less))
685            }
686            BinaryOp::Gt => {
687                compare_values(&l, &r).map(|o| Value::Bool(o == std::cmp::Ordering::Greater))
688            }
689            BinaryOp::Lte => {
690                compare_values(&l, &r).map(|o| Value::Bool(o != std::cmp::Ordering::Greater))
691            }
692            BinaryOp::Gte => {
693                compare_values(&l, &r).map(|o| Value::Bool(o != std::cmp::Ordering::Less))
694            }
695            BinaryOp::Add => match (&l, &r) {
696                // At least one operand is a string → string concatenation.
697                (Value::String(_), _) | (_, Value::String(_)) => Ok(Value::String(
698                    l.to_display_string() + &r.to_display_string(),
699                )),
700                // Both are numeric → arithmetic addition.
701                _ => numeric_op(&l, &r, |a, b| a + b, |a, b| a + b),
702            },
703            BinaryOp::Sub => numeric_op(&l, &r, |a, b| a - b, |a, b| a - b),
704            BinaryOp::Mul => numeric_op(&l, &r, |a, b| a * b, |a, b| a * b),
705            BinaryOp::Div => {
706                let is_zero =
707                    matches!(&r, Value::Int(0)) || matches!(&r, Value::Float(f) if *f == 0.0);
708                if is_zero {
709                    Err(Error::RenderError {
710                        message: "Division by zero".to_string(),
711                    })
712                } else {
713                    numeric_op(&l, &r, |a, b| a / b, |a, b| a / b)
714                }
715            }
716            BinaryOp::Mod => numeric_op(&l, &r, |a, b| a % b, |a, b| a % b),
717            BinaryOp::Or | BinaryOp::And | BinaryOp::NullCoalesce => unreachable!(),
718        }
719    }
720
721    fn eval_unary(&mut self, op: &UnaryOp, operand: &Expr) -> Result<Value> {
722        let val = self.eval_expr(operand)?;
723        match op {
724            UnaryOp::Not => Ok(Value::Bool(!val.is_truthy())),
725            UnaryOp::Neg => match val {
726                Value::Int(i) => Ok(Value::Int(-i)),
727                Value::Float(f) => Ok(Value::Float(-f)),
728                other => Err(Error::RenderError {
729                    message: format!("Cannot negate {}", other.type_name()),
730                }),
731            },
732        }
733    }
734}
735
736/// Evaluates an `is [not] test_name` expression against a value.
737fn eval_test(val: &Value, test_name: &str, renderer: &Renderer) -> Result<bool> {
738    match test_name {
739        "defined" => Ok(!matches!(val, Value::Null)),
740        "undefined" => Ok(matches!(val, Value::Null)),
741        "none" => Ok(matches!(val, Value::Null)),
742        "odd" => match val {
743            Value::Int(i) => Ok(i % 2 != 0),
744            other => Err(Error::RenderError {
745                message: format!("Test 'odd' requires a number, got {}", other.type_name()),
746            }),
747        },
748        "even" => match val {
749            Value::Int(i) => Ok(i % 2 == 0),
750            other => Err(Error::RenderError {
751                message: format!("Test 'even' requires a number, got {}", other.type_name()),
752            }),
753        },
754        "empty" => Ok(val.is_empty()),
755        "truthy" => Ok(val.is_truthy()),
756        "falsy" => Ok(!val.is_truthy()),
757        "string" => Ok(matches!(val, Value::String(_))),
758        "number" => Ok(matches!(val, Value::Int(_) | Value::Float(_))),
759        "iterable" => Ok(matches!(val, Value::Array(_))),
760        unknown => {
761            if renderer.engine.strict {
762                Err(Error::RenderError {
763                    message: format!("Unknown test '{}'", unknown),
764                })
765            } else {
766                Ok(false)
767            }
768        }
769    }
770}
771
772/// Evaluates a `[not] in collection` expression.
773fn eval_membership(val: &Value, collection: &Value) -> Result<bool> {
774    match collection {
775        Value::Array(arr) => Ok(arr.contains(val)),
776        Value::Object(map) => {
777            let key = val.to_display_string();
778            Ok(map.contains_key(&key))
779        }
780        Value::String(haystack) => {
781            let needle = val.to_display_string();
782            Ok(haystack.contains(&needle[..]))
783        }
784        other => Err(Error::RenderError {
785            message: format!(
786                "'in' operator requires an array, object, or string, got {}",
787                other.type_name()
788            ),
789        }),
790    }
791}
792
793fn values_equal(a: &Value, b: &Value) -> bool {
794    match (a, b) {
795        (Value::Null, Value::Null) => true,
796        (Value::Bool(x), Value::Bool(y)) => x == y,
797        (Value::Int(x), Value::Int(y)) => x == y,
798        (Value::Float(x), Value::Float(y)) => x == y,
799        (Value::Int(x), Value::Float(y)) => (*x as f64) == *y,
800        (Value::Float(x), Value::Int(y)) => *x == (*y as f64),
801        (Value::String(x), Value::String(y)) => x == y,
802        _ => false,
803    }
804}
805
806fn compare_values(a: &Value, b: &Value) -> Result<std::cmp::Ordering> {
807    match (a, b) {
808        (Value::Int(x), Value::Int(y)) => Ok(x.cmp(y)),
809        (Value::Float(x), Value::Float(y)) => x.partial_cmp(y).ok_or(Error::RenderError {
810            message: "Cannot compare NaN".to_string(),
811        }),
812        (Value::Int(x), Value::Float(y)) => (*x as f64).partial_cmp(y).ok_or(Error::RenderError {
813            message: "Cannot compare NaN".to_string(),
814        }),
815        (Value::Float(x), Value::Int(y)) => x.partial_cmp(&(*y as f64)).ok_or(Error::RenderError {
816            message: "Cannot compare NaN".to_string(),
817        }),
818        (Value::String(x), Value::String(y)) => Ok(x.cmp(y)),
819        _ => Err(Error::RenderError {
820            message: format!("Cannot compare {} and {}", a.type_name(), b.type_name()),
821        }),
822    }
823}
824
825fn numeric_op(
826    l: &Value,
827    r: &Value,
828    int_op: impl Fn(i64, i64) -> i64,
829    float_op: impl Fn(f64, f64) -> f64,
830) -> Result<Value> {
831    match (l, r) {
832        (Value::Int(a), Value::Int(b)) => Ok(Value::Int(int_op(*a, *b))),
833        (Value::Float(a), Value::Float(b)) => Ok(Value::Float(float_op(*a, *b))),
834        (Value::Int(a), Value::Float(b)) => Ok(Value::Float(float_op(*a as f64, *b))),
835        (Value::Float(a), Value::Int(b)) => Ok(Value::Float(float_op(*a, *b as f64))),
836        _ => Err(Error::RenderError {
837            message: format!(
838                "Arithmetic requires numbers, got {} and {}",
839                l.type_name(),
840                r.type_name()
841            ),
842        }),
843    }
844}
845
846/// Dispatch to a built-in filter by name.
847fn apply_filter(val: Value, name: &str, args: Vec<Value>) -> Result<Value> {
848    match name {
849        "upper" => {
850            let s = require_string(&val, "upper")?;
851            Ok(Value::String(s.to_uppercase()))
852        }
853        "lower" => {
854            let s = require_string(&val, "lower")?;
855            Ok(Value::String(s.to_lowercase()))
856        }
857        "capitalize" => {
858            let s = require_string(&val, "capitalize")?;
859            let mut chars = s.chars();
860            let out = match chars.next() {
861                None => String::new(),
862                Some(c) => c.to_uppercase().to_string() + chars.as_str(),
863            };
864            Ok(Value::String(out))
865        }
866        "trim" => {
867            let s = require_string(&val, "trim")?;
868            Ok(Value::String(s.trim().to_string()))
869        }
870        "truncate" => {
871            let s = require_string(&val, "truncate")?;
872            let len = require_int_arg(&args, 0, "truncate")? as usize;
873            if s.chars().count() <= len {
874                Ok(Value::String(s.to_string()))
875            } else {
876                let truncated: String = s.chars().take(len.saturating_sub(3)).collect();
877                Ok(Value::String(truncated + "..."))
878            }
879        }
880        "replace" => {
881            let s = require_string(&val, "replace")?;
882            let from = require_string_arg(&args, 0, "replace")?;
883            let to = require_string_arg(&args, 1, "replace")?;
884            Ok(Value::String(s.replace(&from[..], &to[..])))
885        }
886        "split" => {
887            let s = require_string(&val, "split")?;
888            let sep = require_string_arg(&args, 0, "split")?;
889            let parts: Vec<Value> = s
890                .split(&sep[..])
891                .map(|p| Value::String(p.to_string()))
892                .collect();
893            Ok(Value::Array(parts))
894        }
895
896        "sort" => {
897            let mut arr = require_array(val, "sort")?;
898            arr.sort_by(|a, b| compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal));
899            Ok(Value::Array(arr))
900        }
901        "reverse" => match val {
902            Value::Array(mut arr) => {
903                arr.reverse();
904                Ok(Value::Array(arr))
905            }
906            Value::String(s) => Ok(Value::String(s.chars().rev().collect())),
907            other => Err(Error::RenderError {
908                message: format!(
909                    "Filter 'reverse' expects array or string, got {}",
910                    other.type_name()
911                ),
912            }),
913        },
914        "join" => {
915            let arr = require_array(val, "join")?;
916            let sep = if args.is_empty() {
917                String::new()
918            } else {
919                require_string_arg(&args, 0, "join")?.to_string()
920            };
921            let parts: Vec<String> = arr.iter().map(|v| v.to_display_string()).collect();
922            Ok(Value::String(parts.join(&sep)))
923        }
924        "first" => {
925            let arr = require_array(val, "first")?;
926            Ok(arr.into_iter().next().unwrap_or(Value::Null))
927        }
928        "last" => {
929            let arr = require_array(val, "last")?;
930            Ok(arr.into_iter().next_back().unwrap_or(Value::Null))
931        }
932        "length" => {
933            let len = val.length().ok_or_else(|| Error::RenderError {
934                message: format!(
935                    "Filter 'length' expects string, array, or object, got {}",
936                    val.type_name()
937                ),
938            })?;
939            Ok(Value::Int(len as i64))
940        }
941
942        "default" => {
943            if val.is_null() {
944                Ok(args.into_iter().next().unwrap_or(Value::Null))
945            } else {
946                Ok(val)
947            }
948        }
949        "json" => Ok(Value::String(val.to_json_string())),
950        "round" => {
951            let precision = if args.is_empty() {
952                0usize
953            } else {
954                require_int_arg(&args, 0, "round")? as usize
955            };
956            match val {
957                Value::Int(i) => Ok(Value::Int(i)),
958                Value::Float(f) => {
959                    let factor = 10f64.powi(precision as i32);
960                    Ok(Value::Float((f * factor).round() / factor))
961                }
962                other => Err(Error::RenderError {
963                    message: format!("Filter 'round' expects a number, got {}", other.type_name()),
964                }),
965            }
966        }
967
968        "urlencode" => {
969            let s = require_string(&val, "urlencode")?;
970            Ok(Value::String(urlencode(s)))
971        }
972        "escape" => {
973            let s = val.to_display_string();
974            Ok(Value::String(html_escape(&s)))
975        }
976
977        unknown => Err(Error::RenderError {
978            message: format!("Unknown filter '{}'", unknown),
979        }),
980    }
981}
982
983fn require_string<'a>(val: &'a Value, filter: &str) -> Result<&'a str> {
984    match val {
985        Value::String(s) => Ok(s),
986        other => Err(Error::RenderError {
987            message: format!(
988                "Filter '{}' expects a string, got {}",
989                filter,
990                other.type_name()
991            ),
992        }),
993    }
994}
995
996fn require_array(val: Value, filter: &str) -> Result<Vec<Value>> {
997    match val {
998        Value::Array(arr) => Ok(arr),
999        other => Err(Error::RenderError {
1000            message: format!(
1001                "Filter '{}' expects an array, got {}",
1002                filter,
1003                other.type_name()
1004            ),
1005        }),
1006    }
1007}
1008
1009fn require_string_arg(args: &[Value], idx: usize, filter: &str) -> Result<String> {
1010    args.get(idx)
1011        .and_then(|v| {
1012            if let Value::String(s) = v {
1013                Some(s.clone())
1014            } else {
1015                None
1016            }
1017        })
1018        .ok_or_else(|| Error::RenderError {
1019            message: format!("Filter '{}' argument {} must be a string", filter, idx + 1),
1020        })
1021}
1022
1023fn require_int_arg(args: &[Value], idx: usize, filter: &str) -> Result<i64> {
1024    match args.get(idx) {
1025        Some(Value::Int(i)) => Ok(*i),
1026        Some(Value::Float(f)) => Ok(*f as i64),
1027        _ => Err(Error::RenderError {
1028            message: format!("Filter '{}' argument {} must be a number", filter, idx + 1),
1029        }),
1030    }
1031}