glimr/loom/runtime
Template Runtime
Generated Loom templates compile down to string concatenation expressions, but they need a shared set of helpers for escaping, conditional rendering, loop iteration, and attribute management. This module provides those building blocks so generated code stays minimal and focused on template structure while the runtime handles the messy details of safe HTML output.
Types
HTML has two fundamentally different attribute types: standard name=“value” pairs and boolean attributes (like disabled, checked) that render only their name when true and are omitted entirely when false. A sum type lets render_attributes handle both correctly.
pub type Attribute {
Attribute(name: String, value: String)
BoolAttribute(name: String, condition: Bool)
}
Constructors
-
Attribute(name: String, value: String) -
BoolAttribute(name: String, condition: Bool)
Template authors frequently need to style the first/last items differently, apply zebra striping, or display counts. Pre-computing all loop metadata into a record lets templates access these values without manual index arithmetic or length checks in the template itself.
pub type Loop {
Loop(
index: Int,
iteration: Int,
first: Bool,
last: Bool,
even: Bool,
odd: Bool,
count: Int,
remaining: Int,
)
}
Constructors
-
Loop( index: Int, iteration: Int, first: Bool, last: Bool, even: Bool, odd: Bool, count: Int, remaining: Int, )
Values
pub fn append(acc: String, value: String) -> String
Deprecated: Re-compile your templates with `./glimr loom_compile`
Generated templates build HTML via a chain of pipe operations. This function provides a named entry point for string concatenation that integrates cleanly with Gleam’s pipe syntax, keeping generated code readable and consistent.
pub fn append_each(
acc: String,
items: List(item),
render_fn: fn(String, item) -> String,
) -> String
Deprecated: Re-compile your templates with `./glimr loom_compile`
l-for loops need to render the same template body for each item while threading the accumulator through. A fold-based approach builds the output in a single pass without allocating intermediate string lists that would need joining afterward.
pub fn append_each_tree(
acc: string_tree.StringTree,
items: List(item),
render_fn: fn(string_tree.StringTree, item) -> string_tree.StringTree,
) -> string_tree.StringTree
Deprecated: Re-compile your templates with `./glimr loom_compile`
Transitional fold-based each for StringTree, now replaced by
concat_each which maps instead of folding. Kept for
templates compiled during the intermediate refactor.
pub fn append_each_with_loop(
acc: String,
items: List(item),
render_fn: fn(String, item, Loop) -> String,
) -> String
Deprecated: Re-compile your templates with `./glimr loom_compile`
When a template uses loop in its l-for body, it needs
metadata like index and first/last flags. Computing the Loop
record once per iteration and passing it to the render
callback avoids repeated list.length calls and keeps the
metadata fresh per item.
pub fn append_each_with_loop_tree(
acc: string_tree.StringTree,
items: List(item),
render_fn: fn(string_tree.StringTree, item, Loop) -> string_tree.StringTree,
) -> string_tree.StringTree
Deprecated: Re-compile your templates with `./glimr loom_compile`
Transitional fold-based each with loop metadata for
StringTree, now replaced by concat_each_with_loop. Kept
for templates compiled during the intermediate refactor.
pub fn append_if(
acc: String,
condition: Bool,
render_fn: fn(String) -> String,
) -> String
Deprecated: Re-compile your templates with `./glimr loom_compile`
l-if directives generate conditional blocks that either render content or skip it entirely. Using a callback for the true branch lets generated code defer rendering — the template body only executes when the condition holds, avoiding unnecessary work.
pub fn append_if_tree(
acc: string_tree.StringTree,
condition: Bool,
render_fn: fn(string_tree.StringTree) -> string_tree.StringTree,
) -> string_tree.StringTree
Deprecated: Re-compile your templates with `./glimr loom_compile`
Transitional helper from the intermediate StringTree refactor that still used fold-based accumulation. Now superseded by the concat code path which generates inline case expressions instead. Keeping it around so templates compiled during the transition don’t break.
pub fn build_classes(items: List(#(String, Bool))) -> String
Templates often need to toggle CSS classes based on state (e.g., “active” when selected, “disabled” when locked). A list of #(name, Bool) tuples lets authors express this declaratively, and this function handles the filtering and space-joining at render time.
pub fn build_styles(items: List(#(String, Bool))) -> String
Same pattern as build_classes but for inline styles. Templates may need to toggle style rules based on state (e.g., “display: none” when hidden). Filtering by boolean and joining produces a valid style attribute value without manual string manipulation.
pub fn class(value: String) -> #(String, Bool)
:class lists expect uniform #(String, Bool) tuples, but static classes that are always present shouldn’t need a redundant True flag. This helper lets authors write class(“btn”) instead of #(“btn”, True), keeping the template syntax clean for the common case.
pub fn concat_each(
items: List(item),
render_fn: fn(item) -> string_tree.StringTree,
) -> string_tree.StringTree
The old fold-based append_each forced every iteration to
flatten into a single String, defeating BEAM iodata. Mapping
each item to a StringTree and concatenating them lets the
runtime keep everything as iodata until the final response
write — a big win for lists of any size.
pub fn concat_each_with_loop(
items: List(item),
render_fn: fn(item, Loop) -> string_tree.StringTree,
) -> string_tree.StringTree
Same idea as concat_each but for templates that use
loop.index, loop.first, etc. in their l-for body. Builds
the Loop record once per iteration so the render function
can reference metadata without computing it itself.
pub fn diff_tree_json(
old_json: String,
new_json: String,
) -> String
Compare two JSON-serialized trees and return a JSON diff containing only the changed dynamics, keyed by index. Returns “{}” if nothing changed.
pub fn display(value: a) -> String
Fallback converter for {{ variable }} expressions whose
type the generator can’t determine at compile time (props
without declared types in the view file, loop properties of
unknown types, etc.). When the type IS known, codegen emits
runtime.escape for Strings, int.to_string for Ints, and
so on — skipping this function entirely. Declaring prop
types in view files is therefore the fast path; this helper
exists so templates without full type coverage still render
safely.
pub fn escape(value: String) -> String
User-provided data rendered into HTML can contain characters that would be interpreted as markup, enabling XSS attacks. Escaping through houdini neutralizes these characters so template output is safe by default without author intervention.
pub fn flatten_tree(tree: loom.LiveTree) -> String
Interleave statics and dynamics into a single HTML string. statics[0] + flatten(dynamics[0]) + statics[1] + …
pub fn inject_live_wrapper(
html: String,
module_name: String,
props_json: String,
) -> String
Live templates render through layout components that produce the /
structure. The live container div and script tag must be injected inside the body rather than wrapping the entire output, otherwise the HTML structure would be invalid with nested body tags.pub fn inject_live_wrapper_tree(
html: string_tree.StringTree,
module_name: String,
props_json: String,
) -> string_tree.StringTree
Live templates now produce StringTree, but injecting the
live container still needs to find the <body> tag via
string splitting. Converting to String here is unavoidable
for the split, but the result goes straight back to
StringTree so the rest of the response stays iodata.
pub fn live_component_wrapper(
html: String,
module_name: String,
props_json: String,
) -> String
Wraps a live component’s rendered HTML in a data-l-live container with a signed token. Used when a live component is embedded inside a page — gives the component its own independent WebSocket actor via multiplexing.
pub fn live_ws_url() -> String
Dev environments use a proxy (e.g., Vite) that doesn’t forward WebSocket connections properly. Detecting the DEV_PROXY_PORT env var lets live templates connect directly to the app port in dev while using a relative path in production that works behind any reverse proxy.
pub fn map_each(
items: List(item),
render_fn: fn(item) -> loom.LiveTree,
) -> loom.Dynamic
Tree mode’s equivalent of concat_each. Each item renders
to its own LiveTree, and the list of trees becomes a DynList
dynamic so the diff algorithm can compare items individually
on re-render.
pub fn map_each_with_loop(
items: List(item),
render_fn: fn(item, Loop) -> loom.LiveTree,
) -> loom.Dynamic
Tree mode’s equivalent of concat_each_with_loop. Same as
map_each but also computes loop metadata per item for
templates that reference loop.index, loop.first, etc.
inside their l-for body.
pub fn merge_attributes(
base: List(Attribute),
extra: List(Attribute),
) -> List(Attribute)
Parent-provided attributes must combine with a component’s base attributes, but the merge rules differ by attribute type: classes and styles should concatenate (so both parent and component classes apply), while other attributes should override (parent wins for id, etc.).
pub fn render_attributes(attrs: List(Attribute)) -> String
Generated code builds attribute lists as runtime values, but the final HTML needs a flat string. Rendering them here with proper escaping and boolean attribute handling centralizes the HTML output rules so generated code doesn’t need to know about escaping or attribute syntax.
pub fn style(value: String) -> #(String, Bool)
Mirrors the class() helper for :style lists. Static styles that are always applied can be written as style(“color: red”) instead of #(“color: red”, True), keeping template syntax consistent between :class and :style.
pub fn to_string(value: a) -> String
Fallback converter for raw variables ({!! expr !!}) whose
type the generator can’t determine at compile time. When
prop types are declared in the view file, codegen emits
specialized conversions (int.to_string, bool.to_string,
etc.) directly and skips this helper. It remains the path
for loop properties with dynamic types and any expression
the generator can’t resolve statically.
pub fn tree_to_json(tree: loom.LiveTree) -> String
Serialize a LiveTree to the JSON wire format for initial send. Format: { “s”: […statics], “d”: […dynamics] }