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}