Core Concepts

What the macros do with a template, and why one behaves the way it does.

Macro-Based DSL Philosophy

Every element is a macro. A template is ordinary Julia code that writes HTML as it runs.

Compile-Time Optimization

Tag names and literal attributes are known while the macro expands. They become constants there, and the render writes them out.

using HypertextTemplates
using HypertextTemplates.Elements

html = @render @div {class = "container"} @p "Hello"

<div class="container">
  <p>Hello</p>
</div>

Native Julia Integration

A loop or a branch in a template is the loop or branch you would write anywhere else:

using HypertextTemplates
using HypertextTemplates.Elements

@render @ul begin
    for i in 1:5
        if isodd(i)
            @li {class = "odd"} "Item " $i
        else
            @li {class = "even"} "Item " $i
        end
    end
end

<ul>
  <li class="odd">Item 1</li>
  <li class="even">Item 2</li>
  <li class="odd">Item 3</li>
  <li class="even">Item 4</li>
  <li class="odd">Item 5</li>
</ul>

Typed Props

Props are keyword arguments. Annotate one and Julia checks it when the component is called, raising a TypeError that names the prop.

using HypertextTemplates
using HypertextTemplates.Elements

@component function typed_list(; items::Vector{String})
    @ul begin
        for item in items
            @li $item
        end
    end
end

@deftag macro typed_list end

items = ["Apple", "Banana", "Cherry"]
html = @render @typed_list {items}

<ul>
  <li>Apple</li>
  <li>Banana</li>
  <li>Cherry</li>
</ul>

The {} Attribute Syntax

Building on the macro foundation, attributes use a special {} syntax that resembles Julia's NamedTuple syntax:

Basic Attributes

using HypertextTemplates
using HypertextTemplates.Elements

# Simple attributes
@render @div {id = "main", class = "container"} "Content"

<div id="main" class="container">Content</div>

using HypertextTemplates
using HypertextTemplates.Elements

# Computed attributes
width = 100
@render @img {src = "/logo.png", width = width * 2}

<img src="/logo.png" width="200">

Attribute Name Shortcuts

When variable names match attribute names:

using HypertextTemplates
using HypertextTemplates.Elements

class = "active"
disabled = true

# Instead of {class = class, disabled = disabled}
@render @button {class, disabled} "Click me"

<button class="active" disabled>Click me</button>

Attribute Spreading

Spread multiple attributes from a collection:

using HypertextTemplates
using HypertextTemplates.Elements

common_attrs = (class = "btn", type = "button")
@render @button {id = "submit", common_attrs...} "Submit"

<button id="submit" class="btn" type="button">Submit</button>

Boolean Attributes

Boolean handling follows HTML5 semantics:

using HypertextTemplates
using HypertextTemplates.Elements

# true renders the attribute name only
@render @input {type = "checkbox", checked = true}

<input type="checkbox" checked>

# false omits the attribute entirely
@render @input {type = "checkbox", checked = false}

<input type="checkbox">

Text Rendering and Interpolation

Variable Interpolation with $

The $ syntax marks expressions for rendering with automatic escaping:

using HypertextTemplates
using HypertextTemplates.Elements

user_input = "<script>alert('xss')</script>"
html = @render @p "User said: " $user_input

<p>User said: &lt;script&gt;alert('xss')&lt;/script&gt;</p>

The @text Macro

The $ syntax is actually shorthand for @text:

using HypertextTemplates
using HypertextTemplates.Elements

value = 42

# These are equivalent
html1 = @render @p "\$ Value: " $value

<p>$ Value: 42</p>

html2 = @render @p "@text Value: " @text value

<p>@text Value: 42</p>

a, b = 10, 20
# @text can handle complex expressions
html3 = @render @p @text "The sum is $(a + b)"

<p>The sum is 30</p>

Mixed Content

You can mix different content types:

using HypertextTemplates
using HypertextTemplates.Elements

dynamic_var = "dynamic content"

html = @render @div begin
    @span "Static text "   # String literal
    @code $dynamic_var     # Escaped variable
    @p "bold"              # Nested element
    @strong " more text"   # Another literal
end

<div><span>Static text </span><code>dynamic content</code><p>bold</p><strong> more text</strong></div>

Streaming Design

The macro expansion described above feeds a rendering pipeline that writes straight to an IO stream. There is no DOM and no string concatenation, so a render allocates a few hundred bytes of context and nothing per element, however long the document runs, and the first byte reaches the client before the last one is computed. Rendering to a String fills an append-only buffer and hands you its contents; rendering to an IO writes through to it. Rendering & Performance covers the pipeline, the streaming API, and how to keep a template on the fast path.

Control Flow Integration

Since templates are Julia code, all control flow constructs work naturally:

Loops

using HypertextTemplates
using HypertextTemplates.Elements

# for loops
collection = ["Apple", "Banana", "Cherry"]
html1 = @render @ul for item in collection
    @li $item
end

<ul>
  <li>Apple</li>
  <li>Banana</li>
  <li>Cherry</li>
</ul>

# while loops
count = 0
html2 = @render @div begin
    while count < 3
        @p "Count: " $count
        global count += 1
    end
end

<div>
  <p>Count: 0</p>
  <p>Count: 1</p>
  <p>Count: 2</p>
</div>

# comprehensions
html3 = @render @select begin
    [@option {value = i} "Option " $i for i in 1:5]
end

<select><option value="1">Option 1</option><option value="2">Option 2</option><option value="3">Option 3</option><option value="4">Option 4</option><option value="5">Option 5</option></select>

Conditionals

All conditional forms are supported:

using HypertextTemplates
using HypertextTemplates.Elements

# if-else
condition = true
html1 = @render @div begin
    if condition
        @p "True branch"
    else
        @p "False branch"
    end
end

<div>
  <p>True branch</p>
</div>

# ternary operator
isactive = false
html2 = @render @p {class = isactive ? "active" : "inactive"} "Status"

<p class="inactive">Status</p>

# short-circuit evaluation
hasdata = false
html3 = @render @div begin
    hasdata && @p "Data is available"
    !hasdata && @p "No data available"
end

<div>
  <p>No data available</p>
</div>

Pattern Matching

Works with any macro-based control flow:

# With Match.jl (example)
@div begin
    @match value begin
        1 => @p "One"
        2 => @p "Two"
        _ => @p "Other"
    end
end

Component Architecture

The macro system and control flow integration come together in HypertextTemplates' component architecture:

Function-Based Components

using HypertextTemplates
using HypertextTemplates.Elements

@component function alert(; type = "info", message)
    classes = "alert alert-" * type
    @div {class = classes, role = "alert"} $message
end

@deftag macro alert end

# Use the component
html = @render @alert {type = "warning", message = "This is a warning!"}

<div class="alert alert-warning" role="alert">This is a warning!</div>

Composition

Components compose naturally:

using HypertextTemplates
using HypertextTemplates.Elements

# Reuse the alert component from above
@component function alert(; type = "info", message)
    classes = "alert alert-" * type
    @div {class = classes, role = "alert"} $message
end

@deftag macro alert end

@component function alert_list(; alerts)
    @div {class = "alert-container"} begin
        for alert in alerts
            @alert {type = alert.type, message = alert.message}
        end
    end
end

@deftag macro alert_list end

# Use the composed component
alerts = [
    (type = "info", message = "Information message"),
    (type = "warning", message = "Warning message"),
    (type = "error", message = "Error message")
]

html = @render @alert_list {alerts}

<div class="alert-container">
  <div class="alert alert-info" role="alert">Information message</div>
  <div class="alert alert-warning" role="alert">Warning message</div>
  <div class="alert alert-error" role="alert">Error message</div>
</div>

Component Transformation

@component rewrites the function definition. This component:

@component function example(; prop)
    @div $prop
end

becomes a function that takes two keyword arguments alongside prop: the stream to write to, and the slot content the caller passed. Both carry hidden names that user code cannot collide with, and the body also binds the definition's file and line so data-htloc can point back at it. @macroexpand shows the whole expansion.

Because the stream is a plain argument, a component writes wherever it is told to: an IOBuffer, a socket, or the batching writer behind StreamingRender.

Performance Considerations

Macro expansion and the streaming design each cut work out of a render:

Compile-Time Work

using HypertextTemplates
using HypertextTemplates.Elements

# This template structure is analyzed at compile time
@component function static_heavy()
    @div {class = "wrapper"} begin
        @header begin
            @nav begin
                @ul begin
                    @li @a {href = "/"} "Home"
                    @li @a {href = "/about"} "About"
                end
            end
        end
    end
end

@deftag macro static_heavy end

# The structure is compiled, not interpreted at runtime
html = @render @static_heavy

<div class="wrapper">
  <header>
    <nav>
      <ul>
        <li><a href="/">Home</a></li>
        <li><a href="/about">About</a></li>
      </ul>
    </nav>
  </header>
</div>

Runtime Efficiency

Only dynamic parts are computed at runtime:

using HypertextTemplates
using HypertextTemplates.Elements

@component function dynamic_list(; items)
    # Static structure compiled, only loop runs at runtime
    @ul {class = "list"} begin
        for item in items  # Only this loop runs at runtime
            @li $item
        end
    end
end

@deftag macro dynamic_list end

# Only the loop execution is runtime work
items = ["Dynamic 1", "Dynamic 2", "Dynamic 3"]
html = @render @dynamic_list {items}

<ul class="list">
  <li>Dynamic 1</li>
  <li>Dynamic 2</li>
  <li>Dynamic 3</li>
</ul>

HTML Escaping Strategy

Anything interpolated with $ or @text is escaped before it reaches the output, so a value that came from a user cannot close a tag or open a script. String literals written directly in a template are escaped too, during macro expansion, so what the render writes is the already-escaped text and nothing is escaped twice. A script and a style hold raw text rather than markup, so what is written directly in one goes out unescaped and that element's own end tag is neutralised instead, in a SafeString as much as in anything else. The innermost element decides, so an element nested inside a script starts markup again and its children are escaped, which is what a <script type="text/template"> block needs from a value the page will later parse as HTML. HTML Elements & Attributes sets out the full rules, the SafeString escape hatch, and @esc_str for escaping a value once at macro expansion.