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    Format((&'a str, &'a str, &'a str))
131}
132
133/// Rust code generation state
134pub struct Rust{
135    /// Set of used traits
136    pub using: HashSet<String>,
137    /// Generated code
138    pub code: String
139}
140
141/// Trait for HTML escaping
142pub static USE_AS_DISPLAY: &str = "AsDisplay";
143/// Trait for raw HTML output
144pub static USE_AS_DISPLAY_HTML: &str = "AsDisplayHtml";
145
146/// Helper for formatting use statements
147pub struct Uses<'a>{
148    uses: &'a HashSet<String>,
149    crate_name: &'a str
150}
151
152impl<'a> Display for Uses<'a>{
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        match self.uses.len(){
155            0 => (),
156            1 => write!(f, "use {}::{}", self.crate_name, self.uses.iter().next().unwrap())?,
157            _ => {
158                f.write_str("use ")?;
159                f.write_str(&self.crate_name)?;
160                f.write_str("::")?;
161                let mut glue = '{';
162                for use_ in self.uses{
163                    f.write_char(glue)?;
164                    f.write_str(use_)?;
165                    glue = ',';
166                }
167                f.write_str("}")?;
168            }
169        }
170        Ok(())
171    }
172}
173
174impl Rust{
175    /// Creates a new Rust code generator
176    pub fn new() -> Self{
177        Self{
178            using: HashSet::new(),
179            code: String::new()
180        }
181    }
182
183    /// Returns a formatter for use statements
184    pub fn uses<'a>(&'a self, crate_name: &'a str) -> Uses<'a>{
185        Uses{ uses: &self.using, crate_name}
186    }
187}
188
189/// Trait for block helpers
190pub trait Block{
191    /// Handles block closing
192    fn handle_close<'a>(&self, rust: &mut Rust) {
193        rust.code.push_str("}");
194    }
195
196    /// Resolves a private variable
197    fn resolve_private<'a>(&self, _depth: usize, expression: &'a Expression<'a>, _name: &str, _rust: &mut Rust) -> Result<()>{
198        Err(ParseError::new(&format!("{} not expected ", expression.content), expression))
199    }
200
201    /// Handles else block
202    fn handle_else<'a>(&self, expression: &'a Expression<'a>, _rust: &mut Rust) -> Result<()>{
203        Err(ParseError::new("else not expected here", expression))
204    }
205
206    /// Returns the this context
207    fn this<'a>(&self) -> Option<&str>{
208        None
209    }
210
211    /// Returns the local variable
212    fn local<'a>(&self) -> &Local{
213        &Local::None
214    }
215}
216
217/// Trait for block helper factories
218pub trait BlockFactory{
219    /// Opens a new block
220    fn open<'a>(&self, compile: &'a Compile<'a>, token: Token<'a>, expression: &'a Expression<'a>, rust: &mut Rust) -> Result<Box<dyn Block>>;
221}
222
223/// Map of block helper names to factories
224pub type BlockMap = HashMap<&'static str, &'static dyn BlockFactory>;
225
226/// Compiler state
227pub struct Compile<'a>{
228    /// Stack of open blocks
229    pub open_stack: Vec<Scope>,
230    /// Map of block helpers
231    pub block_map: &'a BlockMap
232}
233
234/// Appends a depth suffix to a variable name
235pub fn append_with_depth(depth: usize, var: &str, buffer: &mut String){
236    buffer.push_str(var);
237    buffer.push('_');
238    buffer.push_str(depth.to_string().as_str());
239}
240
241/// Root block implementation
242struct Root<'a>{
243    this: Option<&'a str>
244}
245
246impl<'a> Block for Root<'a>{
247    fn this<'b>(&self) -> Option<&str>{
248        self.this
249    }
250}
251
252impl<'a> Compile<'a>{
253    /// Creates a new compiler
254    fn new(this: Option<&'static str>, block_map: &'a BlockMap) -> Self{
255        Self{
256            open_stack: vec![Scope{
257                depth: 0,
258                opened: Box::new(Root{this})
259            }],
260            block_map
261        }
262    }
263
264    /// Finds the scope for a variable
265    fn find_scope(&self, var: &'a str) -> Result<(&'a str, &Scope)>{
266        let mut scope = self.open_stack.last().unwrap();
267        let mut local = var;
268        while local.starts_with("../"){
269            match scope.depth{
270                0 => return Err(ParseError{ message: format!("unable to resolve scope for {}", var)}),
271                _ => {
272                    local = &local[3 ..];
273                    scope = self.open_stack.get(scope.depth - 1).unwrap();
274                }
275            }
276        }
277        return Ok((local, scope));
278    }
279
280    /// Resolves a local variable
281    fn resolve_local(&self, depth: usize, var: &'a str, local: &'a str, buffer: &mut String) -> bool{
282        if var.starts_with(local){
283            let len = local.len();
284            if var.len() > len{
285                if &var[len .. len + 1] != "."{
286                    return false;
287                }
288                append_with_depth(depth, local, buffer);
289                buffer.push_str(&var[len ..]);
290            }
291            else{
292                append_with_depth(depth, local, buffer);
293            }
294            return true;
295        }
296        return false;
297    }
298
299    /// Resolves a variable in a scope
300    fn resolve_var(&self, var: &'a str, scope: &Scope, buffer: &mut String) -> Result<()>{
301        if scope.depth == 0{
302            if let Some(this) = scope.opened.this(){
303                buffer.push_str(this);
304                buffer.push('.');
305            }
306            buffer.push_str(var);
307            return Ok(());
308        }
309        if match scope.opened.local(){
310            Local::As(local) => self.resolve_local(scope.depth, var, local, buffer),
311            Local::This => {
312                buffer.push_str("this_");
313                buffer.push_str(scope.depth.to_string().as_str());
314                if var != "this"{
315                    buffer.push('.');
316                    buffer.push_str(var);
317                }
318                true
319            },
320            Local::None => false
321        }{
322            return Ok(());
323        }
324        let parent = &self.open_stack[scope.depth - 1];
325        if let Some(this) = scope.opened.this(){
326            self.resolve_var(this, parent, buffer)?;
327            if var != this{
328                buffer.push('.');
329                buffer.push_str(var);
330            }
331        }
332        else{
333            self.resolve_var(var, parent, buffer)?;
334        }
335        Ok(())
336    }
337
338    /// Resolves a sub-expression
339    fn resolve_sub_expression(&self, raw: &str, value: &str, rust: &mut Rust) -> Result<()>{
340        self.resolve(&Expression { 
341            expression_type: ExpressionType::Raw,
342            prefix: "",
343            content: value,
344            postfix: "", 
345            raw
346        }, rust)
347    }
348
349    /// Writes a variable expression
350    pub fn write_var(&self, expression: &Expression<'a>, rust: &mut Rust, var: &Token<'a>) -> Result<()>{
351        match var.token_type{
352            TokenType::PrivateVariable => {
353                let (name, scope) = self.find_scope(var.value)?;
354                scope.opened.resolve_private(scope.depth, expression, name, rust)?;
355            },
356            TokenType::Variable => {
357                let (name, scope) = self.find_scope(var.value)?;
358                self.resolve_var(name, scope, &mut rust.code)?;
359            },
360            TokenType::Literal => {
361                rust.code.push_str(var.value);
362            },
363            TokenType::SubExpression(raw) => {
364                self.resolve_sub_expression(raw, var.value, rust)?;
365            }
366        }
367        Ok(())
368    }
369
370    /// Handles an else block
371    fn handle_else(&self, expression: &Expression<'a>, rust: &mut Rust) -> Result<()>{
372        match self.open_stack.last() {
373            Some(scope) => scope.opened.handle_else(expression, rust),
374            None => Err(ParseError::new("else not expected here", expression))
375        }
376    }
377
378    /// Resolves a lookup expression
379    fn resolve_lookup(&self, expression: &Expression<'a>, prefix: &str, postfix: char, args: Token<'a>, rust: &mut Rust) -> Result<()>{
380        self.write_var(expression, rust, &args)?;
381        rust.code.push_str(prefix);
382        self.write_var(expression, rust, &args.next()?.ok_or(
383            ParseError::new("lookup expects 2 arguments", &expression))?
384        )?;
385        rust.code.push(postfix);
386        Ok(())
387    }
388
389    /// Resolves a helper expression
390    fn resolve_helper(&self, expression: &Expression<'a>, name: Token<'a>, mut args: Token<'a>, rust: &mut Rust) -> Result<()>{
391        match name.value{
392            "lookup" => self.resolve_lookup(expression, "[", ']', args, rust),
393            "try_lookup" => self.resolve_lookup(expression, ".get(", ')', args, rust),
394            name => {
395                rust.code.push_str(name);
396                rust.code.push('(');
397                self.write_var(expression, rust, &args)?;
398                loop {
399                    args = match args.next()?{
400                        Some(token) => {
401                            rust.code.push_str(", ");
402                            self.write_var(expression, rust, &token)?;
403                            token
404                        },
405                        None => {
406                            rust.code.push(')');
407                            return Ok(());
408                        }
409                    };
410                }
411            }
412        }
413    }
414
415    /// Resolves an expression
416    fn resolve(&self, expression: &Expression<'a>, rust: &mut Rust) -> Result<()>{
417        let token = match Token::first(&expression.content)?{
418            Some(token) => token,
419            None => return Err(ParseError::new("expected token", &expression))
420        };
421        rust.code.push_str(expression.prefix);
422        if let TokenType::SubExpression(raw) = token.token_type{
423            self.resolve_sub_expression(raw, token.value, rust)?;
424        }
425        else if let Some(args) = token.next()?{
426            self.resolve_helper(expression, token, args, rust)?;
427        }
428        else{
429            self.write_var(expression, rust, &token)?;
430        }
431        rust.code.push_str(expression.postfix);
432        Ok(())
433    }
434
435    /// Writes a local variable declaration
436    pub fn write_local(&self, rust: &mut String, local: &Local){
437        append_with_depth(self.open_stack.len(), match local{
438            Local::As(local) => local,
439            _ => "this"
440        }, rust);
441    }
442
443    /// Closes a block
444    fn close(&mut self, expression: Expression<'a>, rust: &mut Rust) -> Result<()>{
445        let scope = self.open_stack.pop().ok_or_else(|| ParseError::new("Mismatched block helper", &expression))?;
446        Ok(scope.opened.handle_close(rust))
447    }
448
449    /// Opens a block
450    fn open(&mut self, expression: Expression<'a>, rust: &mut Rust) -> Result<()>{
451        let token = Token::first(&expression.content)?.ok_or_else(|| ParseError::new("expected token", &expression))?;
452        match self.block_map.get(token.value){
453            Some(block) => {
454                self.open_stack.push(Scope{
455                    opened: block.open(self, token, &expression, rust)?,
456                    depth: self.open_stack.len()
457                });
458                Ok(())
459            },
460            None => Err(ParseError::new(&format!("unsupported block helper {}", token.value), &expression))
461        }
462    }
463}
464
465/// Compiler options
466#[derive(Debug, Clone, Copy)]
467pub struct Options{
468    /// Name of the root variable
469    pub root_var_name: Option<&'static str>,
470    /// Name of the write function
471    pub write_var_name: &'static str
472}
473
474/// Main compiler implementation
475pub struct Compiler{
476    /// Regex for cleaning whitespace
477    clean: Regex,
478    /// Compiler options
479    options: Options,
480    /// Map of block helpers
481    block_map: BlockMap
482}
483
484impl Compiler {
485    /// Creates a new compiler
486    pub fn new(options: Options, block_map: BlockMap) -> Self{
487        Self{
488            clean: Regex::new("[\\\\\"\\{\\}]").unwrap(),
489            options,
490            block_map
491        }
492    }
493
494    /// Escapes HTML content
495    fn escape<'a>(&self, content: &'a str) -> Cow<'a, str> {
496        self.clean.replace_all(
497            &content, |captures: &Captures| match &captures[0]{
498                "{" | "}" => format!("{}{}", &captures[0], &captures[0]),
499                _ => format!("\\{}", &captures[0])
500            }
501        )
502    }
503
504    /// Commits pending writes
505    fn commit_pending<'a>(&self, pending: &mut Vec<PendingWrite<'a>>, compile: &mut Compile<'a>, rust: &mut Rust) -> Result<()>{
506        if pending.is_empty(){
507            return Ok(());
508        }
509        rust.code.push_str("write!(");
510        rust.code.push_str(self.options.write_var_name);
511        rust.code.push_str(", \"");
512        for pending in pending.iter(){
513            match pending{
514                PendingWrite::Raw(raw) => rust.code.push_str(self.escape(raw).as_ref()),
515                PendingWrite::Expression(_) => rust.code.push_str("{}"),
516                PendingWrite::Format((_, format, _)) => rust.code.push_str(format)
517            }
518        }
519        rust.code.push('"');
520        for pending in pending.iter(){
521            match pending{
522                PendingWrite::Expression((expression, uses, display)) => {
523                    compile.resolve(&Expression{
524                        expression_type: ExpressionType::Raw,
525                        prefix: ", ",
526                        content: expression.content,
527                        postfix: display,
528                        raw: expression.raw
529                    }, rust)?;
530                    rust.using.insert(uses.to_string());
531                },
532                PendingWrite::Format((raw, _, content)) => {
533                    compile.resolve(&Expression{
534                        expression_type: ExpressionType::Raw,
535                        prefix: ", ",
536                        content,
537                        postfix: "",
538                        raw
539                    }, rust)?;
540                },
541                _ => ()
542            }
543        }
544        rust.code.push_str(")?;");
545        pending.clear();
546        Ok(())
547    }
548
549    fn select_write<'a>(expression: &Expression<'a>, uses: &'static str, postfix: &'static str) -> Result<PendingWrite<'a>>{
550        if let Some(token) = Token::first(&expression.content)?{
551            if let TokenType::Variable = token.token_type{
552                if token.value != "format"{
553                    return Ok(PendingWrite::Expression((expression.clone(), uses, postfix)));
554                }
555                let pattern = match token.next()?{
556                    Some(token) => token,
557                    _ => return Ok(PendingWrite::Expression((expression.clone(), uses, postfix)))
558                };
559                let value = match pattern.next(){
560                    Ok(Some(token)) => token,
561                    _ => return Err(ParseError::new("format requires 2 arguments", expression))
562                };
563                if let TokenType::Literal = pattern.token_type{
564                    if pattern.value.starts_with('"') && pattern.value.ends_with('"'){
565                        return Ok(PendingWrite::Format((expression.raw, &pattern.value[1..pattern.value.len() - 1], value.value)));
566                    }
567                }
568                return Err(ParseError::new("first argument of format must be a string literal", expression));
569            }
570        }
571        Ok(PendingWrite::Expression((expression.clone(), uses, postfix)))
572    }
573
574    /// Compiles a template
575    pub fn compile(&self, src: &str) -> Result<Rust>{
576        let mut compile = Compile::new(self.options.root_var_name, &self.block_map);
577        let mut rust = Rust::new();
578        let mut pending: Vec<PendingWrite> = Vec::new();
579        let mut rest = src;
580        let mut expression = Expression::from(src)?;
581        while let Some(expr) = expression{
582            let Expression{
583                expression_type,
584                prefix,
585                content,
586                postfix,
587                raw: _
588            } = &expr;
589            rest = postfix; 
590            if !prefix.is_empty(){
591                pending.push(PendingWrite::Raw(prefix));
592            }
593            match expression_type{
594                ExpressionType::Raw => pending.push(Self::select_write(&expr, USE_AS_DISPLAY, ".as_display()")?),
595                ExpressionType::HtmlEscaped => if *content == "else" {
596                    self.commit_pending(&mut pending, &mut compile, &mut rust)?;
597                    compile.handle_else(&expr, &mut rust)?
598                } else {
599                    pending.push(Self::select_write(&expr, USE_AS_DISPLAY_HTML, ".as_display_html()")?)
600                },
601                ExpressionType::Open => {
602                    self.commit_pending(&mut pending, &mut compile, &mut rust)?;
603                    compile.open(expr, &mut rust)?
604                },
605                ExpressionType::Close => {
606                    self.commit_pending(&mut pending, &mut compile, &mut rust)?;
607                    compile.close(expr, &mut rust)?
608                },
609                ExpressionType::Escaped => pending.push(PendingWrite::Raw(content)),
610                _ => ()
611            };
612            expression = expr.next()?;
613        }
614        if !rest.is_empty(){
615            pending.push(PendingWrite::Raw(rest));
616        }
617        self.commit_pending(&mut pending, &mut compile, &mut rust)?;
618        Ok(rust)
619    }
620}