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: <script>alert('xss')</script></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
endComponent 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
endbecomes 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.