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] }

Search Document