rusty_handlebars_parser/
compiler.rs

1//! Handlebars template compilation
2//!
3//! This module provides functionality for compiling Handlebars templates into Rust code.
4//! It handles:
5//! - Variable resolution and scope management
6//! - Block helper compilation
7//! - Expression evaluation
8//! - HTML escaping
9//!
10//! # Compilation Process
11//!
12//! The compilation process involves:
13//! 1. Parsing the template into expressions
14//! 2. Resolving variables and scopes
15//! 3. Compiling block helpers
16//! 4. Generating Rust code
17//!
18//! # Examples
19//!
20//! Basic usage:
21//! ```rust
22//! use rusty_handlebars_parser::compiler::{Compiler, Options};
23//! use rusty_handlebars_parser::block::add_builtins;
24//!
25//! let mut block_map = HashMap::new();
26//! add_builtins(&mut block_map);
27//!
28//! let options = Options {
29//!     root_var_name: Some("data"),
30//!     write_var_name: "write"
31//! };
32//!
33//! let compiler = Compiler::new(options, block_map);
34//! let rust = compiler.compile("Hello {{name}}!")?;
35//! ```
36//!
37//! Complex template example:
38//! ```rust
39//! use rusty_handlebars_parser::compiler::{Compiler, Options};
40//! use rusty_handlebars_parser::block::add_builtins;
41//!
42//! let mut block_map = HashMap::new();
43//! add_builtins(&mut block_map);
44//!
45//! let options = Options {
46//!     root_var_name: Some("data"),
47//!     write_var_name: "write"
48//! };
49//!
50//! let template = r#"
51//! <div class="user-profile">
52//!     {{#if user}}
53//!         <h1>{{user.name}}</h1>
54//!         {{#if user.bio}}
55//!             <p class="bio">{{user.bio}}</p>
56//!         {{else}}
57//!             <p class="no-bio">No bio available</p>
58//!         {{/if}}
59//!         
60//!         {{#if_some user.posts as post}}
61//!             <div class="posts">
62//!                 <h2>Posts</h2>
63//!                 {{#each post as post}}
64//!                     <article class="post">
65//!                         <h3>{{post.title}}</h3>
66//!                         <p>{{post.content}}</p>
67//!                         <div class="meta">
68//!                             <span>Posted on {{post.date}}</span>
69//!                             {{#if post.tags}}
70//!                                 <div class="tags">
71//!                                     {{#each post.tags as tag}}
72//!                                         <span class="tag">{{tag}}</span>
73//!                                     {{/each}}
74//!                                 </div>
75//!                             {{/if}}
76//!                         </div>
77//!                     </article>
78//!                 {{/each}}
79//!             </div>
80//!         {{/if_some}}
81//!     {{else}}
82//!         <p>Please log in to view your profile</p>
83//!     {{/if}}
84//! </div>
85//! "#;
86//!
87//! let compiler = Compiler::new(options, block_map);
88//! let rust = compiler.compile(template)?;
89//! ```
90//!
91//! This example demonstrates:
92//! - Nested conditional blocks with `if` and `else`
93//! - Option handling with `if_some`
94//! - Collection iteration with `each`
95//! - HTML escaping for safe output
96//! - Complex variable resolution
97//! - Block scope management
98//! - Template structure and formatting
99
100use std::{borrow::Cow, collections::{HashMap, HashSet}, fmt::{Display, Write}};
101
102use regex::{Captures, Regex};
103
104use crate::{error::{ParseError, Result}, expression::{Expression, ExpressionType}, expression_tokenizer::{Token, TokenType}};
105
106/// Local variable declaration in a block
107pub enum Local{
108    /// Named local variable: `as name`
109    As(String),
110    /// This context: `this`
111    This,
112    /// No local variable
113    None
114}
115
116/// A scope in the template
117pub struct Scope{
118    /// The block that opened this scope
119    pub opened: Box<dyn Block>,
120    /// The depth of this scope
121    pub depth: usize
122}
123
124/// A pending write operation
125enum PendingWrite<'a>{
126    /// Raw text to write
127    Raw(&'a str),
128    /// Expression to evaluate and write
129    Expression((Expression<'a>, &'static str, &'static str))
130}
131
132/// Rust code generation state
133pub struct Rust{
134    /// Set of used traits
135    pub using: HashSet<String>,
136    /// Generated code
137    pub code: String
138}
139
140/// Trait for HTML escaping
141pub static USE_AS_DISPLAY: &str = "AsDisplay";
142/// Trait for raw HTML output
143pub static USE_AS_DISPLAY_HTML: &str = "AsDisplayHtml";
144
145/// Helper for formatting use statements
146pub struct Uses<'a>{
147    uses: &'a HashSet<String>
148}
149
150impl<'a> Display for Uses<'a>{
151    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
152        match self.uses.len(){
153            0 => (),
154            1 => write!(f, "use rusty_handlebars::{}", self.uses.iter().next().unwrap())?,
155            _ => {
156                f.write_str("use rusty_handlebars::")?;
157                let mut glue = '{';
158                for use_ in self.uses{
159                    f.write_char(glue)?;
160                    f.write_str(use_)?;
161                    glue = ',';
162                }
163                f.write_str("}")?;
164            }
165        }
166        Ok(())
167    }
168}
169
170impl Rust{
171    /// Creates a new Rust code generator
172    pub fn new() -> Self{
173        Self{
174            using: HashSet::new(),
175            code: String::new()
176        }
177    }
178
179    /// Returns a formatter for use statements
180    pub fn uses(&self) -> Uses{
181        Uses{ uses: &self.using}
182    }
183}
184
185/// Trait for block helpers
186pub trait Block{
187    /// Handles block closing
188    fn handle_close<'a>(&self, rust: &mut Rust) {
189        rust.code.push_str("}");
190    }
191
192    /// Resolves a private variable
193    fn resolve_private<'a>(&self, _depth: usize, expression: &'a Expression<'a>, _name: &str, _rust: &mut Rust) -> Result<()>{
194        Err(ParseError::new(&format!("{} not expected ", expression.content), expression))
195    }
196
197    /// Handles else block
198    fn handle_else<'a>(&self, expression: &'a Expression<'a>, _rust: &mut Rust) -> Result<()>{
199        Err(ParseError::new("else not expected here", expression))
200    }
201
202    /// Returns the this context
203    fn this<'a>(&self) -> Option<&str>{
204        None
205    }
206
207    /// Returns the local variable
208    fn local<'a>(&self) -> &Local{
209        &Local::None
210    }
211}
212
213/// Trait for block helper factories
214pub trait BlockFactory{
215    /// Opens a new block
216    fn open<'a>(&self, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Box<dyn Block>>;
217}
218
219/// Map of block helper names to factories
220pub type BlockMap = HashMap<&'static str, &'static dyn BlockFactory>;
221
222/// Compiler state
223pub struct Compile<'a>{
224    /// Stack of open blocks
225    pub open_stack: Vec<Scope>,
226    /// Map of block helpers
227    pub block_map: &'a BlockMap
228}
229
230/// Appends a depth suffix to a variable name
231pub fn append_with_depth(depth: usize, var: &str, buffer: &mut String){
232    buffer.push_str(var);
233    buffer.push('_');
234    buffer.push_str(depth.to_string().as_str());
235}
236
237/// Root block implementation
238struct Root<'a>{
239    this: Option<&'a str>
240}
241
242impl<'a> Block for Root<'a>{
243    fn this<'b>(&self) -> Option<&str>{
244        self.this
245    }
246}
247
248impl<'a> Compile<'a>{
249    /// Creates a new compiler
250    fn new(this: Option<&'static str>, block_map: &'a BlockMap) -> Self{
251        Self{
252            open_stack: vec![Scope{
253                depth: 0,
254                opened: Box::new(Root{this})
255            }],
256            block_map
257        }
258    }
259
260    /// Finds the scope for a variable
261    fn find_scope(&self, var: &'a str) -> Result<(&'a str, &Scope)>{
262        let mut scope = self.open_stack.last().unwrap();
263        let mut local = var;
264        loop {
265            if local.starts_with("../"){
266                match scope.depth{
267                    0 => return Err(ParseError{ message: format!("unable to resolve scope for {}", var)}),
268                    _ => {
269                        local = &var[3 ..];
270                        scope = self.open_stack.get(scope.depth - 1).unwrap();
271                        continue;
272                    }
273                }
274            }
275            return Ok((local, scope));
276        }
277    }
278
279    /// Resolves a local variable
280    fn resolve_local(&self, depth: usize, var: &'a str, local: &'a str, buffer: &mut String) -> bool{
281        if var.starts_with(local){
282            let len = local.len();
283            if var.len() > len{
284                if &var[len .. len + 1] != "."{
285                    return false;
286                }
287                append_with_depth(depth, local, buffer);
288                buffer.push_str(&var[len ..]);
289            }
290            else{
291                append_with_depth(depth, local, buffer);
292            }
293            return true;
294        }
295        return false;
296    }
297
298    /// Resolves a variable in a scope
299    fn resolve_var(&self, var: &'a str, scope: &Scope, buffer: &mut String) -> Result<()>{
300        if scope.depth == 0{
301            if let Some(this) = scope.opened.this(){
302                buffer.push_str(this);
303                buffer.push('.');
304            }
305            buffer.push_str(var);
306            return Ok(());
307        }
308        if match scope.opened.local(){
309            Local::As(local) => self.resolve_local(scope.depth, var, local, buffer),
310            Local::This => {
311                buffer.push_str("this_");
312                buffer.push_str(scope.depth.to_string().as_str());
313                if var != "this"{
314                    buffer.push('.');
315                    buffer.push_str(var);
316                }
317                true
318            },
319            Local::None => false
320        }{
321            return Ok(());
322        }
323        let parent = &self.open_stack[scope.depth - 1];
324        if let Some(this) = scope.opened.this(){
325            self.resolve_var(this, parent, buffer)?;
326            if var != this{
327                buffer.push('.');
328                buffer.push_str(var);
329            }
330        }
331        else{
332            self.resolve_var(var, parent, buffer)?;
333        }
334        return Ok(());
335    }
336
337    /// Resolves a sub-expression
338    fn resolve_sub_expression(&self, raw: &str, value: &str, rust: &mut Rust) -> Result<()>{
339        self.resolve(&Expression { 
340            expression_type: ExpressionType::Raw,
341            prefix: "",
342            content: value,
343            postfix: "", 
344            raw
345        }, rust)
346    }
347
348    /// Writes a variable expression
349    pub fn write_var(&self, expression: &Expression<'a>, rust: &mut Rust, var: &Token<'a>) -> Result<()>{
350        match var.token_type{
351            TokenType::PrivateVariable => {
352                let (name, scope) = self.find_scope(var.value)?;
353                scope.opened.resolve_private(scope.depth, expression, name, rust)?;
354            },
355            TokenType::Literal => {
356                let (name, scope) = self.find_scope(var.value)?;
357                self.resolve_var(name, scope, &mut rust.code)?;
358            },
359            TokenType::SubExpression(raw) => {
360                self.resolve_sub_expression(raw, var.value, rust)?;
361            }
362        }
363        Ok(())
364    }
365
366    /// Handles an else block
367    fn handle_else(&self, expression: &Expression<'a>, rust: &mut Rust) -> Result<()>{
368        match self.open_stack.last() {
369            Some(scope) => scope.opened.handle_else(expression, rust),
370            None => Err(ParseError::new("else not expected here", expression))
371        }
372    }
373
374    /// Resolves a lookup expression
375    fn resolve_lookup(&self, expression: &Expression<'a>, prefix: &str, postfix: char, args: Token<'a>, rust: &mut Rust) -> Result<()>{
376        self.write_var(expression, rust, &args)?;
377        rust.code.push_str(prefix);
378        self.write_var(expression, rust, &args.next()?.ok_or(
379            ParseError::new("lookup expects 2 arguments", &expression))?
380        )?;
381        rust.code.push(postfix);
382        Ok(())
383    }
384
385    /// Resolves a helper expression
386    fn resolve_helper(&self, expression: &Expression<'a>, name: Token<'a>, mut args: Token<'a>, rust: &mut Rust) -> Result<()>{
387        match name.value{
388            "lookup" => self.resolve_lookup(expression, "[", ']', args, rust),
389            "try_lookup" => self.resolve_lookup(expression, ".get(", ')', args, rust),
390            name => {
391                rust.code.push_str(name);
392                rust.code.push('(');
393                self.write_var(expression, rust, &args)?;
394                loop {
395                    args = match args.next()?{
396                        Some(token) => {
397                            rust.code.push_str(", ");
398                            self.write_var(expression, rust, &token)?;
399                            token
400                        },
401                        None => {
402                            rust.code.push(')');
403                            return Ok(());
404                        }
405                    };
406                }
407            }
408        }
409    }
410
411    /// Resolves an expression
412    fn resolve(&self, expression: &Expression<'a>, rust: &mut Rust) -> Result<()>{
413        let token = match Token::first(&expression.content)?{
414            Some(token) => token,
415            None => return Err(ParseError::new("expected token", &expression))
416        };
417        rust.code.push_str(expression.prefix);
418        if let TokenType::SubExpression(raw) = token.token_type{
419            self.resolve_sub_expression(raw, token.value, rust)?;
420        }
421        else if let Some(args) = token.next()?{
422            self.resolve_helper(expression, token, args, rust)?;
423        }
424        else{
425            self.write_var(expression, rust, &token)?;
426        }
427        rust.code.push_str(expression.postfix);
428        Ok(())
429    }
430
431    /// Writes a local variable declaration
432    pub fn write_local(&self, rust: &mut String, local: &Local){
433        append_with_depth(self.open_stack.len(), match local{
434            Local::As(local) => local,
435            _ => "this"
436        }, rust);
437    }
438
439    /// Closes a block
440    fn close(&mut self, expression: Expression<'a>, rust: &mut Rust) -> Result<()>{
441        let scope = self.open_stack.pop().ok_or_else(|| ParseError::new("Mismatched block helper", &expression))?;
442        Ok(scope.opened.handle_close(rust))
443    }
444
445    /// Opens a block
446    fn open(&mut self, expression: Expression<'a>, rust: &mut Rust) -> Result<()>{
447        let token = Token::first(&expression.content)?.ok_or_else(|| ParseError::new("expected token", &expression))?;
448        match self.block_map.get(token.value){
449            Some(block) => {
450                self.open_stack.push(Scope{
451                    opened: block.open(self, token, &expression, rust)?,
452                    depth: self.open_stack.len()
453                });
454                Ok(())
455            },
456            None => Err(ParseError::new(&format!("unsupported block helper {}", token.value), &expression))
457        }
458    }
459}
460
461/// Compiler options
462#[derive(Debug, Clone, Copy)]
463pub struct Options{
464    /// Name of the root variable
465    pub root_var_name: Option<&'static str>,
466    /// Name of the write function
467    pub write_var_name: &'static str
468}
469
470/// Main compiler implementation
471pub struct Compiler{
472    /// Regex for cleaning whitespace
473    clean: Regex,
474    /// Compiler options
475    options: Options,
476    /// Map of block helpers
477    block_map: BlockMap
478}
479
480impl Compiler {
481    /// Creates a new compiler
482    pub fn new(options: Options, block_map: BlockMap) -> Self{
483        Self{
484            clean: Regex::new("[\\\\\"\\{\\}]").unwrap(),
485            options,
486            block_map
487        }
488    }
489
490    /// Escapes HTML content
491    fn escape<'a>(&self, content: &'a str) -> Cow<'a, str> {
492        self.clean.replace_all(
493            &content, |captures: &Captures| match &captures[0]{
494                "{" | "}" => format!("{}{}", &captures[0], &captures[0]),
495                _ => format!("\\{}", &captures[0])
496            }
497        )
498    }
499
500    /// Commits pending writes
501    fn commit_pending<'a>(&self, pending: &mut Vec<PendingWrite<'a>>, compile: &mut Compile<'a>, rust: &mut Rust) -> Result<()>{
502        if pending.is_empty(){
503            return Ok(());
504        }
505        rust.code.push_str("write!(");
506        rust.code.push_str(self.options.write_var_name);
507        rust.code.push_str(", \"");
508        for pending in pending.iter(){
509            match pending{
510                PendingWrite::Raw(raw) => rust.code.push_str(self.escape(raw).as_ref()),
511                PendingWrite::Expression(_) => rust.code.push_str("{}")
512            }
513        }
514        rust.code.push('"');
515        for pending in pending.iter(){
516            if let PendingWrite::Expression((expression, uses, display)) = pending{
517                compile.resolve(&Expression{
518                    expression_type: ExpressionType::Raw,
519                    prefix: ", ",
520                    content: expression.content,
521                    postfix: display,
522                    raw: expression.raw
523                }, rust)?;
524                rust.using.insert(uses.to_string());
525            }
526        }
527        rust.code.push_str(")?;");
528        pending.clear();
529        Ok(())
530    }
531
532    /// Compiles a template
533    pub fn compile(&self, src: &str) -> Result<Rust>{
534        let mut compile = Compile::new(self.options.root_var_name, &self.block_map);
535        let mut rust = Rust::new();
536        let mut pending: Vec<PendingWrite> = Vec::new();
537        let mut rest = src;
538        let mut expression = Expression::from(src)?;
539        while let Some(expr) = expression{
540            let Expression{
541                expression_type,
542                prefix,
543                content,
544                postfix,
545                raw: _
546            } = &expr;
547            rest = postfix; 
548            if !prefix.is_empty(){
549                pending.push(PendingWrite::Raw(prefix));
550            }
551            match expression_type{
552                ExpressionType::Raw => pending.push(PendingWrite::Expression((expr.clone(), USE_AS_DISPLAY, ".as_display()"))),
553                ExpressionType::HtmlEscaped => if *content == "else" {
554                    self.commit_pending(&mut pending, &mut compile, &mut rust)?;
555                    compile.handle_else(&expr, &mut rust)?
556                } else {
557                    pending.push(PendingWrite::Expression((expr.clone(), USE_AS_DISPLAY_HTML, ".as_display_html()")))
558                },
559                ExpressionType::Open => {
560                    self.commit_pending(&mut pending, &mut compile, &mut rust)?;
561                    compile.open(expr, &mut rust)?
562                },
563                ExpressionType::Close => {
564                    self.commit_pending(&mut pending, &mut compile, &mut rust)?;
565                    compile.close(expr, &mut rust)?
566                },
567                ExpressionType::Escaped => pending.push(PendingWrite::Raw(content)),
568                _ => ()
569            };
570            expression = expr.next()?;
571        }
572        if !rest.is_empty(){
573            pending.push(PendingWrite::Raw(rest));
574        }
575        self.commit_pending(&mut pending, &mut compile, &mut rust)?;
576        Ok(rust)
577    }
578}