Skip to content

scorredoira/prose

Repository files navigation

Prose Template System

A lightweight template system for Go with support for variables, filters, conditionals, loops, and translations.

Installation

go get github.com/scorredoira/prose

Quick Start

package main

import (
    "fmt"
    "github.com/scorredoira/prose"
)

func main() {
    template := "Hello {{name|capitalize}}!"
    context := map[string]any{"name": "john"}

    tmpl := prose.MustParse(template)
    result, _ := tmpl.Render(context, nil)
    fmt.Println(result) // Output: Hello John!
}

Complete Syntax Reference

1. Variables

Syntax: {{variable_name}}

Features:

  • Simple variables: {{name}}
  • Nested properties: {{user.address.city}}
  • Array access by index: {{products[0].name}}
  • Array access with variable: {{items[i]}}

Examples:

Hello {{customer_name}}
Order: {{order.number}}
Price: {{items[0].price}}
Dynamic: {{products[index].name}}

Behavior:

  • Missing variables return empty string (no error)

2. Filters

Syntax: {{variable|filter}} or {{variable|filter1|filter2}}

Standard Filters

Filter Description Input Example Output Example
uppercase Convert to uppercase hello HELLO
lowercase Convert to lowercase HELLO hello
capitalize Capitalize each word hello world Hello World
format:c Currency format 1234.56 $1,234.56
format:d Short date 2025-10-15T10:30:00Z 10/15/2025
format:g Date + time 2025-10-15T10:30:00Z 10/15/2025 10:30 AM
format:f Float (2 decimals) 1234.5678 1234.57
format:i Integer with separators 1234567 1,234,567

Chaining filters:

{{name|lowercase|capitalize}}
{{price|format:c|uppercase}}

Custom filters:

opts := &prose.RenderOptions{
    CustomFilters: map[string]prose.FilterFunc{
        "reverse": func(value any, args ...string) (string, error) {
            s := fmt.Sprintf("%v", value)
            runes := []rune(s)
            for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
                runes[i], runes[j] = runes[j], runes[i]
            }
            return string(runes), nil
        },
    },
}

Behavior:

  • Filters apply left to right
  • Unknown filters are silently ignored
  • Filters handle nil/empty values safely

3. Translations

Syntax: {{t:translation_key}}

Behavior:

  • Looks up translation_key in the translation dictionary for current language
  • Language specified in render options
  • Missing keys return [translation_key]
  • Missing language falls back to default (typically en)
  • Translations can contain variables resolved after translation

Setup:

import "github.com/scorredoira/locale"

translations := locale.NewTranslationStore()
translations.Set("en", "welcome", "Welcome {{name}}!")
translations.Set("es", "welcome", "¡Bienvenido {{name}}!")

opts := &prose.RenderOptions{
    Language:     "es",
    Translations: translations,
}
result, _ := tmpl.Render(context, opts)

Translation format:

{
  "welcome_message": {
    "en": "Thank you for your purchase, {{customer_name}}!",
    "es": "¡Gracias por tu compra, {{customer_name}}!",
    "fr": "Merci pour votre achat, {{customer_name}}!"
  },
  "payment_due": {
    "en": "Payment due: {{amount|format:c}}",
    "es": "Pago pendiente: {{amount|format:c}}"
  }
}

Examples:

{{t:welcome_message}}
{{t:footer_copyright}}
{{t:payment_reminder}}

4. Conditionals

Basic syntax:

[IF condition]
content
[END]

With negation:

[IF NOT condition]
content
[END]

With else:

[IF condition]
content if true
[ELSE]
content if false
[END]

Condition Types

Variable existence:

[IF variable_name]

True if variable exists and is not false, null, 0, or empty string.

Comparisons:

Operator Description Example
= Equal [IF status = "paid"]
!= Not equal [IF status != "pending"]
> Greater than [IF count > 5]
>= Greater or equal [IF count >= 5]
< Less than [IF count < 10]
<= Less or equal [IF count <= 10]

Value types in comparisons:

  • Numbers: [IF age > 18]
  • Strings: [IF status = "paid"] (quotes required)
  • Booleans: [IF is_active = true]
  • Variables: [IF total > minimum]

Examples:

[IF has_discount]
Discount: {{discount_percentage}}%
[END]

[IF NOT is_paid]
Please complete payment.
[END]

[IF total > 100]
Free shipping!
[ELSE]
Shipping: $10
[END]

[IF status = "shipped"]
Your order is on the way!
[END]

[IF quantity >= minimum_order]
  [IF has_express_shipping]
    Ships today!
  [ELSE]
    Ships within 3 days
  [END]
[END]

Behavior:

  • IF blocks can be nested
  • Spaces around operators are optional
  • Blocks must close with [END]
  • [ELSE] is optional

5. Loops

Array Loops

Syntax:

[FOR item IN array]
content
[END]

Behavior:

  • Empty arrays render nothing (no error)
  • Missing variables render nothing (no error)
  • Loops can be nested
  • Access item properties: {{item.property}}

Numeric Range Loops

Syntax:

[FOR i IN start..end]
content
[END]

Behavior:

  • Ranges include both endpoints: 1..3 produces 1, 2, 3
  • Start must be ≤ end
  • Only accepts integers

Loop Variables

Available automatically inside any loop:

Variable Description Type
{{index}} 1-based index Number
{{index0}} 0-based index Number
{{is_first}} True on first iteration Boolean
{{is_last}} True on last iteration Boolean

Examples:

[FOR product IN products]
- {{product.name}}: {{product.price|format:c}}
[END]

[FOR i IN 1..5]
Day {{i}}: {{schedule[i]}}
[END]

[FOR item IN cart]
{{index}}. {{item.name}}[IF NOT is_last], [END]
[END]

[FOR order IN orders]
  Order #{{order.id}}
  [FOR item IN order.items]
    {{index}}. {{item.name}}
  [END]
[END]

[FOR day IN 1..7]
  [IF day = 1]
    Monday schedule
  [ELSE]
    Regular schedule
  [END]
[END]

6. General Syntax

Whitespace

  • Spaces and line breaks from original template are preserved
  • No extra spaces added around substituted values

Special Characters

  • {{ and }} always mark expression start/end
  • [ always marks control block start
  • No escape characters in this version

Nesting

  • [IF] and [FOR] blocks can nest indefinitely
  • Each block must have its own [END]
  • [END] associates with nearest unclosed block

Comments

HTML/XML comments are preserved in output:

<!-- This is a comment -->

Complete Examples

Example 1: Order Confirmation Email

template := `{{t:email_header}}

Hello {{customer.name}},

[IF order.is_shipped]
Great news! Your order #{{order.number}} has been shipped.
Tracking: {{order.tracking_number}}
[ELSE]
Your order #{{order.number}} is being prepared.
[END]

Your items:
[FOR item IN order.items]
{{index}}. {{item.name}} - {{item.price|format:c}} x{{item.quantity}}
[END]

Total: {{order.total|format:c}}

[IF order.has_discount]
You saved {{order.discount_amount|format:c}} with your discount code!
[END]

{{t:email_footer}}`

Example 2: Invoice

template := `Invoice #{{invoice.number}}
Date: {{invoice.date|format:d}}
[IF invoice.due_date]
Due: {{invoice.due_date|format:d}}
[END]

Bill to:
{{customer.name}}
{{customer.address}}

Items:
[FOR line IN invoice.lines]
{{index}}. {{line.description}}
   {{line.quantity}} x {{line.unit_price|format:c}} = {{line.total|format:c}}
[END]

Subtotal: {{invoice.subtotal|format:c}}
[FOR tax IN invoice.taxes]
{{tax.name}} ({{tax.rate}}%): {{tax.amount|format:c}}
[END]
Total: {{invoice.total|format:c}}`

context := map[string]any{
    "invoice": map[string]any{
        "number": "INV-001",
        "date":   time.Now(),
        "total":  150.00,
        "subtotal": 140.00,
        "lines": []map[string]any{
            {"description": "Widget", "quantity": 2, "unit_price": 50.00, "total": 100.00},
            {"description": "Gadget", "quantity": 1, "unit_price": 40.00, "total": 40.00},
        },
        "taxes": []map[string]any{
            {"name": "Sales Tax", "rate": 7.5, "amount": 10.00},
        },
    },
    "customer": map[string]any{
        "name":    "John Doe",
        "address": "123 Main St, City, State 12345",
    },
}

tmpl := prose.MustParse(template)
result, _ := tmpl.Render(context, nil)
fmt.Println(result)

Example 3: Payment Reminder with Translations

template := `{{t:reminder_greeting}}

[IF days_overdue > 0]
  {{t:payment_overdue}}
  [IF days_overdue > 30]
    {{t:urgent_notice}}
  [END]
[ELSE]
  {{t:payment_upcoming}}
[END]

{{t:amount_label}}: {{amount|format:c}}
{{t:due_date_label}}: {{due_date|format:d}}

[IF payment_methods]
{{t:payment_methods_label}}:
[FOR method IN payment_methods]
- {{method.name}}: {{method.details}}
[END]
[END]

{{t:footer_signature}}`

Example 4: Weekly Report with Numeric Ranges

template := `Weekly Report: {{week_start|format:d}} - {{week_end|format:d}}

Daily Summary:
[FOR day IN 1..7]
Day {{day}}: {{daily_sales[day]|format:c}}
[END]

Top 5 Products:
[FOR i IN 1..5]
{{i}}. {{top_products[i].name}} - {{top_products[i].sales|format:c}}
[END]`

Example 5: Complex Nested Loops

template := `[FOR category IN categories]
  {{category.name}}:
  [FOR product IN category.products]
    [IF product.in_stock]
      {{index}}. {{product.name}} - {{product.price|format:c}}
      [IF product.discount > 0]
        SALE: {{product.discount}}% off!
      [END]
    [END]
  [END]
[END]`

Error Handling

Parse Errors

The system rejects templates with:

  • Unclosed blocks ([IF] without [END])
  • [END] without corresponding opening block
  • Invalid condition syntax
  • Invalid loop syntax

Runtime Behavior (No Errors)

The system does NOT generate errors for:

  • Missing variables → returns empty string
  • Missing translations → returns [key]
  • Empty arrays → skips loop
  • Unknown filters → ignored

Only critical issues like infinite recursion or memory problems cause errors.


Security

Built-in protection against:

  • Stack overflow: Configurable MaxDepth limit (default: 100)
    opts := &prose.RenderOptions{MaxDepth: 50}
  • Translation recursion: Maximum depth of 10 levels
  • Malformed input: Parse-time validation catches issues early
  • Thread-safe: Templates can be rendered concurrently
  • Array bounds: Out-of-bounds access returns empty string (no panic)
  • Nil values: Safely handled without crashes

Advanced Features

Custom Localizer

Implement your own number/date formatting:

type MyLocalizer struct{}

func (l *MyLocalizer) Format(format string, value any) string {
    // Your custom formatting logic
}

opts := &prose.RenderOptions{
    Localizer: &MyLocalizer{},
}

Recursion Depth Control

// For deeply nested templates
opts := &prose.RenderOptions{
    MaxDepth: 200, // Increase from default 100
}

Syntax Summary Table

Element Syntax Example
Variable {{name}} {{customer_name}}
Nested property {{obj.prop}} {{order.total}}
Array index {{arr[0]}} {{items[0].name}}
Variable index {{arr[i]}} {{products[index]}}
Filter {{var|filter}} {{price|format:c}}
Multiple filters {{var|f1|f2}} {{name|lowercase|capitalize}}
Custom filter {{var|format:name}} {{date|format:iso}}
Translation {{t:key}} {{t:welcome}}
Conditional [IF cond]...[END] [IF paid]Yes[END]
If-else [IF cond]...[ELSE]...[END] [IF paid]Yes[ELSE]No[END]
Negation [IF NOT cond]...[END] [IF NOT active]Inactive[END]
Comparison [IF var = val] [IF status = "ok"]
Array loop [FOR x IN arr]...[END] [FOR p IN products]{{p.name}}[END]
Range loop [FOR i IN n..m]...[END] [FOR i IN 1..10]Day {{i}}[END]
Loop index {{index}} or {{index0}} {{index}}. {{item}}
First iteration {{is_first}} [IF is_first]First![END]
Last iteration {{is_last}} [IF is_last]Last![END]

Testing

Run tests:

go test -v

Run benchmarks:

go test -bench=. -benchmem

See complete examples:

cd example
go run main.go

Performance

Benchmarks on Apple M4 Pro:

  • Parse: ~1.8 μs/op
  • Simple render: ~240 ns/op
  • Complex render (loops, conditionals): ~4.3 μs/op
  • Parse + render: ~800 ns/op

Suitable for high-throughput applications.


License

MIT

About

A lightweight template system for Go

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages