Skip to content
← Back to Examples

ERB Template Parser

RustRubycode templateintermediate

Parse ERB (Embedded Ruby) templates, extracting text content, expressions (<%= %>), code blocks (<% %>), and comments (<%# %>).

Key Concepts

  • template syntax
  • embedded expressions
  • code extraction
  • comment handling
  • variable substitution

Why Parse This Format?

ERB templates are widely used in Ruby on Rails, configuration management tools, and static site generators. Parsing templates enables analysis, transformation, and rendering.

Use Cases

  • Ruby on Rails view processing
  • Configuration template rendering
  • Static site generation
  • Code generation templates
  • Email template processing

Input Format

ERB templates mix text with Ruby code in special tags: <%= expr %> for expressions, <% code %> for statements, <%# comment %> for comments.

text | <%= expression %> | <% code %> | <%# comment %>

Examples

The value of x is <%= x %>.VALID

Simple expression embedding

<% 1 + 2 %>VALID

Code block (no output)

<%# commented out %>VALID

Comment (not rendered)

Hello <%= name %>, you have <%= count %> messages.VALID

Multiple expressions

Text before <% code %> text after.VALID

Code with surrounding text

<html><body><%= content %></body></html>VALID

HTML template

<% unclosedINVALID

Invalid: missing closing %>

Output Format

The parser returns a list of ErbElement enum values distinguishing between text, expressions, code, and comments.

Structure

ErbElement

Enum: Text(String), Expression(String), Code(String), Comment(String)

rendered

Can be rendered with variable bindings

Visual Overview

ERB Template Parser diagram

How the Code Works

This section explains how the parsing rules map to the format semantics.

ERB Tag Types

.rule(
    "expression",  // <%= ... %>  - Output expression value
    seq(vec![dynamic(str("<%=")), dynamic(re("[^%]*")), dynamic(str("%>"))]),
)
.rule(
    "comment",     // <%# ... %>  - Comment (not rendered)
    seq(vec![dynamic(str("<%#")), dynamic(re("[^%]*")), dynamic(str("%>"))]),
)
.rule(
    "code",        // <% ... %>   - Execute code
    seq(vec![dynamic(str("<%")), dynamic(re("[^%]*")), dynamic(str("%>"))]),
)

ERB has three tag types with different purposes:

Template Parsing

while pos < chars.len() {
    if pos + 1 < chars.len() && chars[pos] == '<' && chars[pos + 1] == '%' {
        pos += 2;  // Skip <%

        // Determine tag type
        let tag_type = if chars[pos] == '=' {
            pos += 1;
            ErbTagType::Expression
        } else if chars[pos] == '#' {
            pos += 1;
            ErbTagType::Comment
        } else {
            ErbTagType::Code
        };

        // Find closing %>
        while pos + 1 < chars.len() && !(chars[pos] == '%' && chars[pos + 1] == '>') {
            pos += 1;
        }
        // ...
    } else {
        // Collect text until next ERB tag
    }
}

Templates are parsed by scanning for `<%` markers:

Template Rendering

pub fn render_template(
    elements: &[ErbElement],
    vars: &std::collections::HashMap<&str, &str>,
) -> String {
    let mut result = String::new();

    for element in elements {
        match element {
            ErbElement::Text(t) => result.push_str(t),
            ErbElement::Expression(e) => {
                if let Some(value) = vars.get(e.as_str()) {
                    result.push_str(value);
                } else {
                    result.push_str(&format!("[{}]", e));
                }
            }
            ErbElement::Code(_) => { /* Not implemented */ }
            ErbElement::Comment(_) => { /* Not rendered */ }
        }
    }
    result
}

Expressions are substituted with variable values:

Template Rendering

pub fn render_template(
    elements: &[ErbElement],
    vars: &std::collections::HashMap<&str, &str>,
) -> String {
    let mut result = String::new();

    for element in elements {
        match element {
            ErbElement::Text(t) => result.push_str(t),
            ErbElement::Expression(e) => {
                if let Some(value) = vars.get(e.as_str()) {
                    result.push_str(value);
                } else {
                    result.push_str(&format!("[{}]", e));
                }
            }
            ErbElement::Code(_) => { /* Not implemented */ }
            ErbElement::Comment(_) => { /* Not rendered */ }
        }
    }
    result
}

Expressions are substituted with variable values:

Source Code

Rust
//! ERB Template Parser Example
//!
//! This example demonstrates parsing ERB-style templates.
//! Shows handling of mixed text and embedded code sections.
//! Based on the Parslet erb.rb example.
//!
//! Run with: cargo run --example erb_parser --no-default-features

#![allow(clippy::print_literal)]

use parsanol::portable::{
    parser_dsl::{choice, dynamic, re, seq, str, GrammarBuilder},
    AstArena, Grammar, PortableParser,
};

/// Build an ERB grammar
fn build_erb_grammar() -> Grammar {
    GrammarBuilder::new()
        // ERB expression: <%= ... %>
        .rule(
            "expression",
            seq(vec![
                dynamic(str("<%=")),
                dynamic(re("[^%]*")), // Content (simplified)
                dynamic(str("%>")),
            ]),
        )
        // ERB comment: <%# ... %>
        .rule(
            "comment",
            seq(vec![
                dynamic(str("<%#")),
                dynamic(re("[^%]*")),
                dynamic(str("%>")),
            ]),
        )
        // ERB code: <% ... %>
        .rule(
            "code",
            seq(vec![
                dynamic(str("<%")),
                dynamic(re("[^%]*")),
                dynamic(str("%>")),
            ]),
        )
        // Any ERB tag
        .rule(
            "erb_tag",
            choice(vec![
                dynamic(seq(vec![
                    dynamic(str("<%=")),
                    dynamic(re("[^%]*")),
                    dynamic(str("%>")),
                ])),
                dynamic(seq(vec![
                    dynamic(str("<%#")),
                    dynamic(re("[^%]*")),
                    dynamic(str("%>")),
                ])),
                dynamic(seq(vec![
                    dynamic(str("<%")),
                    dynamic(re("[^%]*")),
                    dynamic(str("%>")),
                ])),
            ]),
        )
        // Text: anything until ERB tag
        .rule("text", re("[^<]+"))
        // Template: text and tags mixed
        .rule("template", re(".*"))
        .build()
}

/// ERB element types
#[derive(Debug, Clone)]
pub enum ErbElement {
    /// Plain text
    Text(String),
    /// Expression: <%= ... %>
    Expression(String),
    /// Code: <% ... %>
    Code(String),
    /// Comment: <%# ... %>
    Comment(String),
}

impl std::fmt::Display for ErbElement {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ErbElement::Text(t) => write!(f, "TEXT: {}", t),
            ErbElement::Expression(e) => write!(f, "EXPR: {}", e),
            ErbElement::Code(c) => write!(f, "CODE: {}", c),
            ErbElement::Comment(c) => write!(f, "COMMENT: {}", c),
        }
    }
}

/// Parse an ERB template
pub fn parse_erb(input: &str) -> Result<Vec<ErbElement>, String> {
    let grammar = build_erb_grammar();
    let mut arena = AstArena::for_input(input.len());
    let mut parser = PortableParser::new(&grammar, input, &mut arena);

    parser
        .parse()
        .map_err(|e| format!("Parse error: {:?}", e))?;

    // Manual parsing for element extraction
    let mut elements = Vec::new();
    let mut pos = 0;
    let chars: Vec<char> = input.chars().collect();

    while pos < chars.len() {
        // Check for ERB tag
        if pos + 1 < chars.len() && chars[pos] == '<' && chars[pos + 1] == '%' {
            // Determine tag type
            pos += 2; // Skip <%

            let tag_type = if pos < chars.len() && chars[pos] == '=' {
                pos += 1;
                ErbTagType::Expression
            } else if pos < chars.len() && chars[pos] == '#' {
                pos += 1;
                ErbTagType::Comment
            } else {
                ErbTagType::Code
            };

            // Find closing %>
            let content_start = pos;
            while pos + 1 < chars.len() && !(chars[pos] == '%' && chars[pos + 1] == '>') {
                pos += 1;
            }

            let content: String = chars[content_start..pos].iter().collect();
            pos += 2; // Skip %>

            match tag_type {
                ErbTagType::Expression => {
                    elements.push(ErbElement::Expression(content.trim().to_string()))
                }
                ErbTagType::Code => elements.push(ErbElement::Code(content.trim().to_string())),
                ErbTagType::Comment => {
                    elements.push(ErbElement::Comment(content.trim().to_string()))
                }
            }
        } else {
            // Collect text until next ERB tag
            let text_start = pos;
            while pos < chars.len()
                && !(pos + 1 < chars.len() && chars[pos] == '<' && chars[pos + 1] == '%')
            {
                pos += 1;
            }
            let text: String = chars[text_start..pos].iter().collect();
            if !text.is_empty() {
                elements.push(ErbElement::Text(text));
            }
        }
    }

    Ok(elements)
}

#[derive(Debug, Clone, Copy)]
enum ErbTagType {
    Expression,
    Code,
    Comment,
}

/// Render template with variable bindings (simplified)
pub fn render_template(
    elements: &[ErbElement],
    vars: &std::collections::HashMap<&str, &str>,
) -> String {
    let mut result = String::new();

    for element in elements {
        match element {
            ErbElement::Text(t) => result.push_str(t),
            ErbElement::Expression(e) => {
                // Simple variable substitution
                if let Some(value) = vars.get(e.as_str()) {
                    result.push_str(value);
                } else {
                    result.push_str(&format!("[{}]", e));
                }
            }
            ErbElement::Code(_) => {
                // Code execution not implemented (would need embedded interpreter)
            }
            ErbElement::Comment(_) => {
                // Comments are not rendered
            }
        }
    }

    result
}

fn main() {
    println!("ERB Template Parser Example");
    println!("===========================\n");

    let templates = [
        "The value of x is <%= x %>.",
        "<% 1 + 2 %>",
        "<%# commented %>",
        "Hello <%= name %>, you have <%= count %> messages.",
        "Text before <% code %> text after.",
    ];

    println!("=== Parsing Examples ===\n");

    for template in templates {
        println!("Template: {:?}", template);
        match parse_erb(template) {
            Ok(elements) => {
                println!("Elements:");
                for element in &elements {
                    println!("  {}", element);
                }
            }
            Err(e) => println!("Error: {}", e),
        }
        println!();
    }

    // Demonstrate rendering
    println!("=== Rendering Example ===\n");

    let template = "Hello <%= name %>, welcome to <%= place %>!";
    println!("Template: {:?}", template);

    use std::collections::HashMap;
    let mut vars = HashMap::new();
    vars.insert("name", "World");
    vars.insert("place", "Rust");

    match parse_erb(template) {
        Ok(elements) => {
            println!("Parsed:");
            for element in &elements {
                println!("  {}", element);
            }
            println!("\nRendered: {:?}", render_template(&elements, &vars));
        }
        Err(e) => println!("Error: {}", e),
    }

    // Complex example
    println!("\n=== Complex Template ===\n");

    let complex = r#"<!DOCTYPE html>
<html>
<head><title><%= title %></title></head>
<body>
  <%# This is a comment %>
  <h1><%= heading %></h1>
  <p>Welcome, <%= user %>!</p>
</body>
</html>"#;

    println!("Template:");
    println!("{}", complex);
    println!();

    let mut vars2 = HashMap::new();
    vars2.insert("title", "My Page");
    vars2.insert("heading", "Welcome");
    vars2.insert("user", "Alice");

    match parse_erb(complex) {
        Ok(elements) => {
            println!("Found {} elements:", elements.len());
            for (i, element) in elements.iter().enumerate() {
                println!("  {}: {}", i + 1, element);
            }
        }
        Err(e) => println!("Error: {}", e),
    }
}
Ruby
# frozen_string_literal: true

# Example that demonstrates how a simple erb-like parser could be constructed.

$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"

require "parsanol/parslet"

class ErbParser < Parsanol::Parser
  rule(:ruby) { (str("%>").absent? >> any).repeat.as(:ruby) }

  rule(:expression) { (str("=") >> ruby).as(:expression) }
  rule(:comment) { (str("#") >> ruby).as(:comment) }
  rule(:code) { ruby.as(:code) }
  rule(:erb) { expression | comment | code }

  rule(:erb_with_tags) { str("<%") >> erb >> str("%>") }
  rule(:text) { (str("<%").absent? >> any).repeat(1) }

  rule(:text_with_ruby) { (text.as(:text) | erb_with_tags).repeat.as(:text) }
  root(:text_with_ruby)
end

parser = ErbParser.new
p parser.parse "The value of x is <%= x %>."
p parser.parse "<% 1 + 2 %>"
p parser.parse "<%# commented %>"

evaluator = Parsanol::Transform.new do
  erb_binding = binding

  rule(code: { ruby: simple(:ruby) }) do
    eval(ruby, erb_binding)
    ""
  end
  rule(expression: { ruby: simple(:ruby) }) { eval(ruby, erb_binding) }
  rule(comment: { ruby: simple(:ruby) }) { "" }

  rule(text: simple(:text)) { text }
  rule(text: sequence(:texts)) { texts.join }
end

puts evaluator.apply(parser.parse(<<~ERB,
  The <% a = 2 %>not printed result of "a = 2".
  The <%# a = 1 %>not printed non-evaluated comment "a = 1", see the value of a below.
  The <%= 'nicely' %> printed result.
  The <% b = 3 %>value of a is <%= a %>, and b is <%= b %>.
ERB
                                 ))