rusty_handlebars_parser/
block.rs

1//! Handlebars block parsing and compilation
2//!
3//! This module provides functionality for parsing and compiling Handlebars block helpers.
4//! It supports various block types including:
5//! - `if`/`unless` for conditional rendering
6//! - `if_some`/`if_some_ref` for handling Option types
7//! - `with`/`with_ref` for changing context
8//! - `each`/`each_ref` for iterating over collections
9//!
10//! # Block Types
11//!
12//! ## Conditional Blocks
13//! - `{{#if value}}...{{/if}}` - Renders content if value is truthy
14//! - `{{#unless value}}...{{/unless}}` - Renders content if value is falsy
15//!
16//! ## Option Handling
17//! - `{{#if_some value as item}}...{{/if_some}}` - Handles Option types
18//! - `{{#if_some_ref value as item}}...{{/if_some_ref}}` - Handles Option references
19//!
20//! ## Context Blocks
21//! - `{{#with value as item}}...{{/with}}` - Changes context to value
22//! - `{{#with_ref value as item}}...{{/with_ref}}` - Changes context to value reference
23//!
24//! ## Iteration Blocks
25//! - `{{#each items as item}}...{{/each}}` - Iterates over collection
26//! - `{{#each_ref items as item}}...{{/each_ref}}` - Iterates over collection references
27//! - Supports `@index` for accessing current index
28//! - Supports `else` block for empty collections
29//!
30//! # Examples
31//!
32//! ```rust
33//! use rusty_handlebars_parser::block::{Block, BlockFactory};
34//! use rusty_handlebars_parser::expression::{Expression, ExpressionType};
35//!
36//! let template = "{{#if user}}Hello {{user.name}}!{{/if}}";
37//! let expr = Expression::from(template).unwrap().unwrap();
38//! assert_eq!(expr.expression_type, ExpressionType::Open);
39//! ```
40
41use crate::{compiler::{append_with_depth, Block, BlockFactory, BlockMap, Compile, Local, Rust}, error::{ParseError, Result}, expression::{Expression, ExpressionType}, expression_tokenizer::Token};
42
43/// Strips pipe characters from a token value
44fn strip_pipes<'a>(token: Token<'a>, expression: &Expression<'a>) -> Result<&'a str> {
45    loop {
46        match token.next()? {
47            Some(token) => {
48                if token.value == "|" {
49                    continue;
50                }
51                return Ok(token.value.trim_matches('|'));
52            },
53            None => return Err(ParseError::new("expected variable after as", expression))
54        }
55    }
56}
57
58/// Reads a local variable declaration from a token
59fn read_local<'a>(token: &Token<'a>, expression: &Expression<'a>) -> Result<Local> {
60    match token.next()? {
61        Some(token) => {
62            match token.value {
63                "as" => Ok(Local::As(strip_pipes(token, expression)?.to_string())),
64                token => Err(ParseError::new(&format!("unexpected token {}", token), expression))
65            }
66        },
67        None => Ok(Local::This)
68    }
69}
70
71/// Handles if/unless block compilation
72struct IfOrUnless {}
73
74impl IfOrUnless {
75    /// Creates a new if/unless block
76    pub fn new<'a>(label: &str, prefix: &str, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<IfOrUnless> {
77        match token.next()? {
78            Some(var) => {
79                rust.using.insert("AsBool".to_string());
80                rust.code.push_str(prefix);
81                compile.write_var(expression, rust, &var)?;
82                rust.code.push_str(".as_bool(){");
83                Ok(Self{})
84            },
85            None => Err(ParseError::new(&format!("expected variable after {}", label), expression))
86        }
87    }
88}
89
90impl Block for IfOrUnless {
91    /// Handles else block compilation
92    fn handle_else<'a>(&self, _expression: &'a Expression<'a>, rust: &mut Rust) -> Result<()> {
93        rust.code.push_str("}else{");
94        Ok(())
95    }
96}
97
98/// Factory for if blocks
99struct IfFty {}
100
101impl BlockFactory for IfFty {
102    /// Opens an if block
103    fn open<'a>(&self, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Box<dyn Block>> {
104        Ok(Box::new(IfOrUnless::new("if", "if ", compile, token, expression, rust)?))
105    }
106}
107
108/// Factory for unless blocks
109struct UnlessFty {}
110
111impl BlockFactory for UnlessFty {
112    /// Opens an unless block
113    fn open<'a>(&self, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Box<dyn Block>> {
114        Ok(Box::new(IfOrUnless::new("unless", "if !", compile, token, expression, rust)?))
115    }
116}
117
118/// Handles if_some block compilation
119struct IfSome {
120    local: Local
121}
122
123impl IfSome {
124    /// Creates a new if_some block
125    fn new<'a>(by_ref: bool, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Self> {
126        let next = token.next()?.ok_or_else(|| ParseError::new(
127            &format!("expected variable after if_some{}", if by_ref {"_ref"} else {""}), expression
128        ))?;
129        let local = read_local(&next, expression)?;
130        rust.code.push_str("if let Some(");
131        compile.write_local(&mut rust.code, &local);
132        rust.code.push_str(") = ");
133        if by_ref {
134            rust.code.push('&');
135        }
136        compile.write_var(expression, rust, &next)?;
137        rust.code.push('{');
138        Ok(Self{local})
139    }
140}
141
142impl Block for IfSome {
143    /// Handles else block compilation
144    fn handle_else<'a>(&self, _expression: &'a Expression<'a>, rust: &mut Rust) -> Result<()> {
145        rust.code.push_str("}else{");
146        Ok(())
147    }
148
149    /// Returns the local variable
150    fn local<'a>(&self) -> &Local {
151        &self.local
152    }
153}
154
155/// Factory for if_some blocks
156struct IfSomeFty {}
157
158impl BlockFactory for IfSomeFty {
159    /// Opens an if_some block
160    fn open<'a>(&self, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Box<dyn Block>> {
161        Ok(Box::new(IfSome::new(false, compile, token, expression, rust)?))
162    }
163}
164
165/// Factory for if_some_ref blocks
166struct IfSomeRefFty {}
167
168impl BlockFactory for IfSomeRefFty {
169    /// Opens an if_some_ref block
170    fn open<'a>(&self, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Box<dyn Block>> {
171        Ok(Box::new(IfSome::new(true, compile, token, expression, rust)?))
172    }
173}
174
175/// Handles with block compilation
176struct With {
177    local: Local
178}
179
180impl With {
181    /// Creates a new with block
182    pub fn new<'a>(by_ref: bool, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Self> {
183        let next = token.next()?.ok_or_else(|| ParseError::new(
184            &format!("expected variable after with{}", if by_ref {"_ref"} else {""}), expression
185        ))?;
186        let local = read_local(&next, expression)?;
187        rust.code.push_str("{let ");
188        compile.write_local(&mut rust.code, &local);
189        rust.code.push_str(" = ");
190        if by_ref {
191            rust.code.push('&');
192        }
193        compile.write_var(expression, rust, &next)?;
194        rust.code.push(';');
195        Ok(Self{local})
196    }
197}
198
199impl Block for With {
200    /// Returns the local variable
201    fn local<'a>(&self) -> &Local {
202        &self.local
203    }
204}
205
206/// Factory for with blocks
207struct WithFty {}
208
209impl BlockFactory for WithFty {
210    /// Opens a with block
211    fn open<'a>(&self, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Box<dyn Block>> {
212        Ok(Box::new(With::new(false, compile, token, expression, rust)?))
213    }
214}
215
216/// Factory for with_ref blocks
217struct WithRefFty {}
218
219impl BlockFactory for WithRefFty {
220    /// Opens a with_ref block
221    fn open<'a>(&self, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Box<dyn Block>> {
222        Ok(Box::new(With::new(true, compile, token, expression, rust)?))
223    }
224}
225
226/// Handles each block compilation
227struct Each {
228    local: Local,
229    indexer: Option<String>,
230    has_else: bool
231}
232
233/// Checks if a string contains an indexer expression at the given depth
234fn contains_indexer(src: &str, mut depth: i32) -> bool {
235    match src.find("index") {
236        Some(pos) => {
237            match src[..pos].rfind('@') {
238                Some(start) => {
239                    let mut prefix = &src[start + 1 .. pos];
240                    while prefix.starts_with("../") {
241                        depth -= 1;
242                        prefix = &prefix[3 ..];
243                    }
244                    return depth == 0;
245                },
246                None => return false
247            }
248        },
249        None => return false
250    }
251}
252
253/// Checks if a block contains an indexer expression
254fn check_for_indexer(src: &str) -> Result<bool> {
255    let mut exp = Expression::from(src)?;
256    let mut depth = 1;
257    while let Some(expr) = &exp {
258        match expr.expression_type {
259            ExpressionType::Comment | ExpressionType::Escaped => continue,
260            ExpressionType::Open => if contains_indexer(expr.content, depth - 1) {
261                return Ok(true);
262            } else {
263                depth += 1;
264            },
265            ExpressionType::Close => {
266                depth -= 1;
267                if depth == 0 {
268                    return Ok(false);
269                }
270            },
271            _ => if contains_indexer(expr.content, depth - 1) {
272                return Ok(true);
273            }
274        }
275        exp = expr.next()?;
276    }
277    Ok(false)
278}
279
280/// Checks if a block contains an else block
281fn check_for_else(src: &str) -> Result<bool> {
282    let mut exp = Expression::from(src)?;
283    let mut depth = 1;
284    while let Some(expr) = &exp {
285        match expr.expression_type {
286            ExpressionType::Comment | ExpressionType::Escaped => continue,
287            ExpressionType::Open => depth += 1,
288            ExpressionType::Close => {
289                depth -= 1;
290                if depth == 0 {
291                    return Ok(false);
292                }
293            },
294            _ => if expr.content == "else" && depth == 1 {
295                return Ok(true);
296            }
297        }
298        exp = expr.next()?;
299    }
300    Ok(false)
301}
302
303impl Each {
304    /// Creates a new each block
305    pub fn new<'a>(by_ref: bool, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Self>{
306        let next = match token.next()?{
307            Some(next) => next,
308            None => return Err(ParseError::new(&format!("expected variable after {}", if by_ref{"each_ref"}else{"each"}), expression))
309        };
310        let indexer = check_for_indexer(&expression.postfix).map(|found| match found{
311            true => {
312                let indexer = format!("i_{}", compile.open_stack.len());
313                rust.code.push_str("let mut ");
314                rust.code.push_str(indexer.as_str());
315                rust.code.push_str(" = 0;");
316                Some(indexer)
317            },
318            false => None
319        })?;
320        let local = read_local(&next, expression)?;
321        let has_else = check_for_else(&expression.postfix)?;
322        if has_else{
323            rust.code.push_str("{let mut empty = true;");
324        }
325        rust.code.push_str("for ");
326        compile.write_local(&mut rust.code, &local);
327        rust.code.push_str(" in ");
328        if by_ref{
329            rust.code.push('&');
330        }
331        compile.write_var(expression, rust, &next)?;
332        rust.code.push('{');
333        if has_else{
334            rust.code.push_str("empty = false;");
335        }
336        Ok(Self{
337            local,
338            indexer,
339            has_else
340        })
341    }
342    /// Writes a map variable access
343    fn write_map_var<'a>(&self, depth: usize, suffix: &str, rust: &mut Rust) {
344        append_with_depth(depth, if let Local::As(name) = &self.local {
345            name.as_str()
346        } else {
347            "this"
348        }, &mut rust.code);
349        rust.code.push_str(suffix)
350    }
351
352    /// Writes an indexer increment
353    fn write_indexer<'a>(&self, rust: &mut Rust) {
354        if let Some(indexer) = &self.indexer {
355            rust.code.push_str(indexer);
356            rust.code.push_str("+=1;");
357        }
358    }
359}
360
361impl Block for Each{
362    fn handle_else<'a>(&self, _expression: &'a Expression<'a>, rust: &mut Rust) -> Result<()> {
363        self.write_indexer(rust);
364        rust.code.push_str("} if empty {");
365        Ok(())
366    }
367
368    fn resolve_private<'a>(&self, depth: usize, expression: &'a Expression<'a>, name: &str, rust: &mut Rust) -> Result<()>{
369        Ok(match name{
370            "index" => rust.code.push_str(&self.indexer.as_ref().unwrap()),
371            "key" => self.write_map_var(depth, ".0", rust),
372            "value" => self.write_map_var(depth, ".1", rust),
373            _ => Err(ParseError::new(&format!("unexpected variable {}", name), expression))?
374        })
375    }
376
377    fn handle_close<'a>(&self, rust: &mut Rust) {
378        if self.has_else{
379            rust.code.push_str("}}");
380        }
381        else{
382            self.write_indexer(rust);
383            rust.code.push('}');
384        }
385    }
386
387    fn local<'a>(&self) -> &Local {
388        &self.local
389    }
390}
391
392/// Factory for each blocks
393struct EachFty {}
394
395impl BlockFactory for EachFty {
396    /// Opens an each block
397    fn open<'a>(&self, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Box<dyn Block>> {
398        Ok(Box::new(Each::new(false, compile, token, expression, rust)?))
399    }
400}
401
402/// Factory for each_ref blocks
403struct EachRefFty {}
404
405impl BlockFactory for EachRefFty {
406    /// Opens an each_ref block
407    fn open<'a>(&self, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Box<dyn Block>> {
408        Ok(Box::new(Each::new(true, compile, token, expression, rust)?))
409    }
410}
411
412const IF: IfFty = IfFty{};
413const UNLESS: UnlessFty = UnlessFty{};
414const IF_SOME: IfSomeFty = IfSomeFty{};
415const IF_SOME_REF: IfSomeRefFty = IfSomeRefFty{};
416const WITH: WithFty = WithFty{};
417const WITH_REF: WithRefFty = WithRefFty{};
418const EACH: EachFty = EachFty{};
419const EACH_REF: EachRefFty = EachRefFty{};
420
421/// Adds built-in block helpers to the block map
422pub fn add_builtins(map: &mut BlockMap) {
423    map.insert("if", &IF);
424    map.insert("unless", &UNLESS);
425    map.insert("if_some", &IF_SOME);
426    map.insert("if_some_ref", &IF_SOME_REF);
427    map.insert("with", &WITH);
428    map.insert("with_ref", &WITH_REF);
429    map.insert("each", &EACH);
430    map.insert("each_ref", &EACH_REF);
431}