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