runjucks_core/
ast.rs

1//! Abstract syntax tree for templates after [`crate::parser::parse`].
2//!
3//! [`Expr`] covers outputs (`{{ … }}`) including literals, variables, and operators
4//! (aligned with Nunjucks [`parseExpression`](https://github.com/mozilla/nunjucks/blob/master/nunjucks/src/parser.js)).
5
6use serde_json::Value;
7use std::sync::Arc;
8
9/// One branch of an `{% if %}` / `{% elif %}` / `{% else %}` chain.
10#[derive(Debug, Clone, PartialEq)]
11pub struct IfBranch {
12    /// Condition; [`None`] for `{% else %}`.
13    pub cond: Option<Expr>,
14    pub body: Vec<Node>,
15}
16
17/// One formal parameter in a `{% macro %}` header (`a` or `a = expr`).
18#[derive(Debug, Clone, PartialEq)]
19pub struct MacroParam {
20    pub name: String,
21    /// Default expression evaluated in the **caller's** scope when the argument is omitted.
22    pub default: Option<Expr>,
23}
24
25/// A template macro definition (`{% macro name(args) %}…{% endmacro %}`).
26#[derive(Debug, Clone, PartialEq)]
27pub struct MacroDef {
28    pub name: String,
29    pub params: Vec<MacroParam>,
30    pub body: Vec<Node>,
31}
32
33/// Loop binding list after `for` (`x` or `k, v` or `a, b, c`).
34#[derive(Debug, Clone, PartialEq)]
35pub enum ForVars {
36    Single(String),
37    Multi(Vec<String>),
38}
39
40/// One `{% case expr %}…` branch inside `{% switch %}`.
41#[derive(Debug, Clone, PartialEq)]
42pub struct SwitchCase {
43    pub cond: Expr,
44    pub body: Vec<Node>,
45}
46
47/// Template structure produced by the parser.
48///
49/// - [`Node::Root`]: sequence of top-level fragments (text + outputs).
50/// - [`Node::Text`]: literal characters from the template.
51/// - [`Node::Output`]: one or more [`Expr`] values to evaluate and concatenate (filters etc. are future work).
52#[derive(Debug, Clone, PartialEq)]
53pub enum Node {
54    /// Container for sibling template fragments.
55    Root(Vec<Node>),
56    /// Raw text between (or outside) template tags (shared across clones of the AST).
57    Text(Arc<str>),
58    /// Content inside `{{` … `}}` after expression parsing.
59    Output(Vec<Expr>),
60    /// `{% if %}`, optional `{% elif %}`, optional `{% else %}`, `{% endif %}`.
61    If { branches: Vec<IfBranch> },
62    /// `{% for var in iter %} … {% endfor %}` (optional `{% else %}` before `endfor`).
63    For {
64        vars: ForVars,
65        iter: Expr,
66        body: Vec<Node>,
67        else_body: Option<Vec<Node>>,
68    },
69    /// `{% set a = expr %}`, `{% set a, b = expr %}`, or `{% set a %}…{% endset %}`.
70    Set {
71        targets: Vec<String>,
72        value: Option<Expr>,
73        body: Option<Vec<Node>>,
74    },
75    /// `{% include expr %}` with optional `ignore missing` — template name from expression.
76    ///
77    /// `with_context`: `None` / `Some(true)` — current frame stack; `Some(false)` — isolated root
78    /// context (template globals only; matches Jinja `without context` / Nunjucks-style isolation).
79    Include {
80        template: Expr,
81        ignore_missing: bool,
82        with_context: Option<bool>,
83    },
84    /// `{% switch expr %}{% case c %}…{% default %}…{% endswitch %}`.
85    Switch {
86        expr: Expr,
87        cases: Vec<SwitchCase>,
88        default_body: Option<Vec<Node>>,
89    },
90    /// `{% extends expr %}` — template name from any expression (e.g. quoted string or variable); must appear before meaningful child content.
91    Extends { parent: Expr },
92    /// `{% block name %}…{% endblock %}` — default body when used in a base layout.
93    Block { name: String, body: Vec<Node> },
94    /// `{% macro name(a, b) %}…{% endmacro %}` — emits no output; registers a macro for the current template.
95    MacroDef(MacroDef),
96    /// `{% import expr as alias %}` — loads a template and exposes its top-level macros under `alias` (`alias.macro()`).
97    Import {
98        template: Expr,
99        alias: String,
100        /// `Some(true)` = `with context` (parent context when evaluating top-level `{% set %}` and macro bodies);
101        /// `Some(false)` or omitted = isolated (Nunjucks default).
102        with_context: Option<bool>,
103    },
104    /// `{% from expr import a, b as c %}` — imports named macros into the current macro scope.
105    FromImport {
106        template: Expr,
107        /// `(exported_name, alias)` where `alias` is the local name (same as exported if `None`).
108        names: Vec<(String, Option<String>)>,
109        with_context: Option<bool>,
110    },
111    /// `{% filter name %}…{% endfilter %}` — render body to a string, then apply a builtin filter.
112    FilterBlock {
113        name: String,
114        args: Vec<Expr>,
115        body: Vec<Node>,
116    },
117    /// `{% call macro(args) %}…{% endcall %}` — macro may invoke `caller()` to render this body.
118    /// Optional `{% call(a, b) m() %}` — `caller(x, y)` passes arguments into the call body scope.
119    CallBlock {
120        caller_params: Vec<MacroParam>,
121        callee: Expr,
122        body: Vec<Node>,
123    },
124    /// `{% asyncEach var in iter %} … {% endeach %}` — sequential async iteration.
125    /// Each iteration body is awaited sequentially; async filters/globals within the body can suspend.
126    AsyncEach {
127        vars: ForVars,
128        iter: Expr,
129        body: Vec<Node>,
130        else_body: Option<Vec<Node>>,
131    },
132    /// `{% asyncAll var in iter %} … {% endall %}` — parallel async iteration.
133    /// All iteration bodies are rendered concurrently; results are concatenated in iteration order.
134    AsyncAll {
135        vars: ForVars,
136        iter: Expr,
137        body: Vec<Node>,
138        else_body: Option<Vec<Node>>,
139    },
140    /// `{% ifAsync cond %} … {% endif %}` — async condition evaluation.
141    IfAsync { branches: Vec<IfBranch> },
142    /// Custom extension tag (`addExtension` / [`Environment::register_extension`](crate::environment::Environment::register_extension)).
143    ExtensionTag {
144        extension_name: String,
145        /// Opening tag name (e.g. `echo`).
146        tag: String,
147        /// Raw source after the tag name until `%}` (trimmed).
148        args: String,
149        /// Block body when the extension declares an end tag.
150        body: Option<Vec<Node>>,
151    },
152}
153
154/// Comparison operators in a Nunjucks-style chain (`a == b < c`).
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum CompareOp {
157    Eq,
158    StrictEq,
159    Ne,
160    StrictNe,
161    Lt,
162    Gt,
163    Le,
164    Ge,
165}
166
167/// Binary operators (arithmetic, logical short-circuit forms use these in the AST).
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum BinOp {
170    Add,
171    Sub,
172    Mul,
173    Div,
174    FloorDiv,
175    Mod,
176    Pow,
177    /// String concatenation (`~`), compiled as `+ "" +` in Nunjucks JS output.
178    Concat,
179    And,
180    Or,
181    /// Membership ([`runtime.inOperator` in Nunjucks](https://github.com/mozilla/nunjucks/blob/master/nunjucks/src/lib.js)).
182    In,
183    /// `is` test (right-hand side is typically a variable naming the test).
184    Is,
185}
186
187#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum UnaryOp {
189    Not,
190    Neg,
191    Pos,
192}
193
194/// Expression inside a variable tag or tag body (when wired up).
195#[derive(Debug, Clone, PartialEq)]
196pub enum Expr {
197    /// JSON literal (numbers, strings, booleans, null).
198    Literal(Value),
199    /// Context key; non-existent keys render as empty (Nunjucks-style).
200    Variable(String),
201    /// Attribute access: `base.attr` (Nunjucks `LookupVal`).
202    GetAttr {
203        base: Box<Expr>,
204        attr: String,
205    },
206    /// Subscript: `base[index]` or Jinja-style `base[start:stop:step]`.
207    GetItem {
208        base: Box<Expr>,
209        /// Single index, or [`Expr::Slice`] for `:` subscripts.
210        index: Box<Expr>,
211    },
212    /// Slice inside `[ … ]` (`start:stop:step`; any part may be omitted).
213    Slice {
214        start: Option<Box<Expr>>,
215        stop: Option<Box<Expr>>,
216        step: Option<Box<Expr>>,
217    },
218    /// Function-style call `callee(args…)` (runtime may be limited until globals exist).
219    Call {
220        callee: Box<Expr>,
221        args: Vec<Expr>,
222        /// `name = value` arguments (applied after positionals; unknown names ignored, Nunjucks-style).
223        kwargs: Vec<(String, Expr)>,
224    },
225    /// Filter pipeline step: `input | name` or `input | name(arg, …)`.
226    Filter {
227        name: String,
228        input: Box<Expr>,
229        args: Vec<Expr>,
230    },
231    /// Array literal `[a, b, …]` with arbitrary expressions.
232    List(Vec<Expr>),
233    /// Object literal `{ key: val, …}` (keys are expressions, usually strings or identifiers).
234    Dict(Vec<(Expr, Expr)>),
235    /// Python-style conditional: `body if cond else else_`.
236    InlineIf {
237        cond: Box<Expr>,
238        then_expr: Box<Expr>,
239        else_expr: Option<Box<Expr>>,
240    },
241    Unary {
242        op: UnaryOp,
243        expr: Box<Expr>,
244    },
245    Binary {
246        op: BinOp,
247        left: Box<Expr>,
248        right: Box<Expr>,
249    },
250    /// Chained comparisons (`a == b < c` → left-associative JS-style emit in Nunjucks).
251    Compare {
252        head: Box<Expr>,
253        rest: Vec<(CompareOp, Expr)>,
254    },
255    /// JavaScript-style regex literal `r/pattern/flags` ([Nunjucks lexer](https://github.com/mozilla/nunjucks/blob/master/nunjucks/src/lexer.js)).
256    RegexLiteral {
257        /// Raw pattern body between slashes (may contain `\/` escapes).
258        pattern: String,
259        /// Flag letters `g`, `i`, `m`, `y` (subset of ECMAScript).
260        flags: String,
261    },
262}