arizona_template (arizona v0.4.0)

View Source

Runtime template API: bindings, descriptors, value rendering, and snapshots.

Most of this module's surface is exposed through the macros in include/arizona_common.hrl (?get, ?html, ?each, ?stateful, ?stateless). Templates themselves are produced by arizona_parse_transform at compile time -- html/1 is only a runtime stub that errors if the parse transform was not applied.

Concepts

  • Bindings -- the map passed to render/1. get/2 and get/3 access bindings while tracking which keys were read, so the differ knows which dynamics depend on which bindings.
  • Templates -- compile-time maps #{s := Statics, d := Dynamics, f := Fp} emitted by the parse transform.
  • Descriptors -- lightweight tuples returned by stateful/2 and stateless/2,3 that tell the renderer how to mount a child component.
  • Snapshots -- cached #{s, d, deps, ...} maps used by the differ to detect what changed between renders.

Example

render(Bindings) ->
    ?html({'div', [{class, ?get(theme)}], [?get(name)]}).

Summary

Functions

Stamps the value a conditional branch produced with that branch's index within its slot, when the value is an az-nodiff template or a ?stateless descriptor (whose template takes the stamp when it is rendered). Two branches can produce the same template -- the same markup, the same helper -- and the stamp is what tells them apart once their az-nodiff elements are no longer evaluated. Any other value is returned as is.

Classifies the escape-exempt content shapes: a raw/1 opt-out ({arizona_raw, V}, returned unwrapped) and an already-encoded effect command or command list ({arizona_effect, _}). Every other value is value.

Extracts the az/0 index from a 2- or 3-tuple dynamic/0.

Wraps a template/0 produced by the parse transform with an each source.

Escape the inner value of an {arizona_esc, _} marker.

Renders a value to its output binary, escaping via Backend (an arizona_renderer): HTML entity-escaping, terminal control-char sanitizing, or JSON string encoding. raw/1-wrapped values and already-encoded arizona_effect commands are classified out and pass through unescaped (the ?raw opt-out and effects are trusted, backend-independent).

Formats compile/runtime error reasons emitted by this module.

Formats runtime errors raised with an error_info annotation pointing at this module. Picked up by erl_error:format_exception/3.

Reads Key from Bindings and tracks the access for diff dependency analysis.

Reads Key from Bindings, returning Default if absent. Tracks the access.

Like get/3 but the default is computed lazily by a 0-arity fun.

Compile-time stub. The parse transform replaces every ?html(...) (and arizona_template:html/1) call with a precomputed template/0 map. If this function runs, the parse transform was not applied -- include arizona_stateful.hrl or arizona_stateless.hrl.

Builds a client-owned slot value. The server renders Init once and never diffs the slot (diff => false); the browser owns the value via Key, updating it client-side with no round-trip. Used via the ?local macro in content or attribute-value position. Key may be a binary or an atom (an atom is normalized to a binary, so it reaches the client as the same string).

Builds a stateful child snapshot, propagating diff => false from the template.

Marks an evaluated value for HTML escaping at output. Only scalars are marked -- nested templates/descriptors and stateless-child snapshots (all maps) and effects pass through untouched so they render structurally (their own inner dynamics carry their own marks). A stateless descriptor returned from a conditional content slot reduces to a bare snapshot map here, so the is_map clause passes it through and scope_slot/2 later namespaces it -- the same treatment a directly-slotted stateless child gets.

Propagates the template-level fields that must survive onto a snapshot: f (fingerprint), the optional diff => false flag, the render backend, and the a attributes the template declares.

Compile-time stub for the native (JSON) render target. The parse transform replaces every ?native(...) (and arizona_template:native/1) call with a precomputed template/0 map whose statics are JSON fragments. If this function runs, the parse transform was not applied.

The value of the az-nodiff attribute on the root of an az-nodiff element: its identity as same_nodiff/3 decides it -- the template's fingerprint F, the conditional branch that produced it and its az_key -- spelled as one opaque token. The client matches az-nodiff elements by it, so a wholesale re-render around a az-nodiff element (a reconnect, a container ?OP_TEXT) carries the existing DOM over to a new render naming the same az-nodiff element, and replaces it otherwise.

Wraps a value to opt out of HTML auto-escaping. Use only for trusted, already-safe HTML fragments -- never for user-controlled data.

Whether two az-nodiff snapshots of the same fingerprint are the same az-nodiff element: the same producing branch and az_key, and the same child view when they are one.

Whether the template Tmpl, about to fill the slot Az, is the az-nodiff element the slot's snapshot Old already holds: the same scoped fingerprint, the same producing branch and the same az_key.

Namespaces a slot's rendered value by the enclosing slot Az, so a repeated stateless child (or any inline nested template) is an independent diff target. Prefixes the value's fingerprint, its statics' baked az/marker ids, and every inner dynamic az (recursing through inline nested templates) with the slot az.

Splits a list of {Az, Val, Deps} triples into the snapshot d-list and deps list. (arizona_render:render/2 needs the document too and fuses this split into its zip walk, zip_triples/3.)

Builds a stateful child descriptor. The renderer mounts Handler with Props.

Builds a stateless child descriptor from a 1-arity render fun.

Builds a stateless child descriptor from a Handler:Fun/1 reference.

Renders a ?stateless descriptor to its template, passing on the branch stamp the descriptor carries (see branch/2).

Compile-time stub for the terminal (ANSI) render target. The parse transform replaces every ?terminal(...) (and arizona_template:terminal/1) call with a precomputed template/0 map whose statics are ANSI-decorated text. If this function runs, the parse transform was not applied.

Converts a template value to its binary HTML representation.

Records Key as a dependency of the dynamic element currently being rendered.

Materializes an attribute dynamic into its rendered binary through Backend (an arizona_renderer), leaving plain values untouched.

Returns the visible portion of a stream order buffer given a Limit.

Projects Bindings to a sub-map of only Keys, tracking each key as a dependency.

Types

az()

-nominal az() :: binary() | undefined.

bindings()

-type bindings() :: map().

deps()

-nominal deps() :: #{term() => true}.

dynamic()

-nominal dynamic() ::
             {az(), fun(() -> term()), loc()} |
             {az(), {attr, binary(), fun(() -> term())}, loc()} |
             {az(), template(), loc()} |
             {az(), term(), loc()} |
             {az(), fun(() -> term())} |
             {az(), {attr, binary(), fun(() -> term())}} |
             {az(), template()} |
             {az(), term()}.

each_container()

-nominal each_container() :: #{t := 0, source := term(), template := each_template()}.

each_template()

-nominal each_template() ::
             #{t := 0,
               s := [binary()],
               d := fun((term()) -> [dynamic()]) | fun((term(), term()) -> [dynamic()]),
               f := binary(),
               diff => false,
               single_root => true,
               reuse => [boolean()],
               multi_root => true,
               keyless => true,
               backend => module()}.

loc()

-nominal loc() :: {module(), pos_integer()}.

nodiff_identity()

-nominal nodiff_identity() :: {pos_integer() | undefined, term()}.

render_fun()

-nominal render_fun() :: fun((bindings()) -> template()).

snapshot()

-nominal snapshot() ::
             #{s := [binary()],
               d := [{az(), term()}],
               f => binary(),
               deps => [deps()],
               diff => false,
               nodiff => nodiff_identity(),
               view_id => binary(),
               backend => module()}.

stateful_descriptor()

-nominal stateful_descriptor() :: #{stateful := module(), props := map()}.

stateless_descriptor()

-nominal stateless_descriptor() ::
             #{callback := fun((map()) -> template()), props := map(), branch => pos_integer()}.

template()

-nominal template() ::
             #{s := [binary()],
               d := [dynamic()],
               f := binary(),
               diff => false,
               branch => pos_integer(),
               az_key => term(),
               multi_root => true,
               backend => module()}.

Functions

branch(Branch, Value)

-spec branch(Branch, Value) -> Value1 when Branch :: pos_integer(), Value :: term(), Value1 :: term().

Stamps the value a conditional branch produced with that branch's index within its slot, when the value is an az-nodiff template or a ?stateless descriptor (whose template takes the stamp when it is rendered). Two branches can produce the same template -- the same markup, the same helper -- and the stamp is what tells them apart once their az-nodiff elements are no longer evaluated. Any other value is returned as is.

The parse transform stamps a literal template itself and calls this only for a branch value it cannot see at compile time.

classify_trusted(Value)

-spec classify_trusted(Value) -> {raw, term()} | {effect, Effect} | value
                          when Value :: term(), Effect :: term().

Classifies the escape-exempt content shapes: a raw/1 opt-out ({arizona_raw, V}, returned unwrapped) and an already-encoded effect command or command list ({arizona_effect, _}). Every other value is value.

The definition of the shape vocabulary -- escape_value/2, mark_esc/1, and arizona_html:render_attr/2 dispatch on it. to_bin/1 matches the same shapes in its clause heads instead, because it runs right after one of those callers has already classified the value; growing the vocabulary means updating both. Each call site decides the policy for a shape (mark_esc/1 deliberately unwraps and re-escapes a helper-produced raw).

dyn_az(Dynamic)

-spec dyn_az(Dynamic) -> az() when Dynamic :: dynamic().

Extracts the az/0 index from a 2- or 3-tuple dynamic/0.

each(Source, Template)

-spec each(Source, Template) -> each_container() when Source :: term(), Template :: each_template().

Wraps a template/0 produced by the parse transform with an each source.

Called by the parse transform to compile ?each(Fun, Source) -- pairs the per-item template with its source list, stream, or map.

?each(Fun, Source) expands to a call on this same name, which the parse transform replaces with the compiled pairing above. Reached with the macro's own (Fun, Source) argument order, the transform did not run: say so with parse_transform_not_applied rather than a bare function_clause.

escape_marked(Backend, Value)

-spec escape_marked(Backend, Value) -> binary() when Backend :: module(), Value :: term().

Escape the inner value of an {arizona_esc, _} marker.

The marker itself is the classification: mark_esc/1 is its only producer and it wraps only what already came back as a plain value, so re-asking classify_trusted/1 here can only ever get the same answer. Escapable content is most of what a template renders, so that second ask is paid per value per render.

escape_value(Backend, Value)

-spec escape_value(Backend, Value) -> binary() when Backend :: module(), Value :: term().

Renders a value to its output binary, escaping via Backend (an arizona_renderer): HTML entity-escaping, terminal control-char sanitizing, or JSON string encoding. raw/1-wrapped values and already-encoded arizona_effect commands are classified out and pass through unescaped (the ?raw opt-out and effects are trusted, backend-independent).

format_error(Reason)

-spec format_error(Reason) -> string() when Reason :: term().

Formats compile/runtime error reasons emitted by this module.

format_error(Reason, Stacktrace)

-spec format_error(Reason, Stacktrace) -> ErrorInfo
                      when
                          Reason :: term(), Stacktrace :: [tuple()], ErrorInfo :: #{general := iolist()}.

Formats runtime errors raised with an error_info annotation pointing at this module. Picked up by erl_error:format_exception/3.

get(Key, Bindings)

-spec get(Key, Bindings) -> Value when Key :: term(), Bindings :: map(), Value :: term().

Reads Key from Bindings and tracks the access for diff dependency analysis.

Errors with missing_binding (carrying an error_info annotation that routes through format_error/2) if not present. Use get/3 for a default.

get(Key, Bindings, Default)

-spec get(Key, Bindings, Default) -> Value
             when Key :: term(), Bindings :: map(), Default :: term(), Value :: term().

Reads Key from Bindings, returning Default if absent. Tracks the access.

get_lazy(Key, Bindings, DefaultFun)

-spec get_lazy(Key, Bindings, DefaultFun) -> Value
                  when
                      Key :: term(), Bindings :: map(), DefaultFun :: fun(() -> term()), Value :: term().

Like get/3 but the default is computed lazily by a 0-arity fun.

html(Elems)

-spec html(term()) -> no_return().

Compile-time stub. The parse transform replaces every ?html(...) (and arizona_template:html/1) call with a precomputed template/0 map. If this function runs, the parse transform was not applied -- include arizona_stateful.hrl or arizona_stateless.hrl.

local(Key, Init)

-spec local(Key, Init) -> map() when Key :: binary() | atom(), Init :: term().

Builds a client-owned slot value. The server renders Init once and never diffs the slot (diff => false); the browser owns the value via Key, updating it client-side with no round-trip. Used via the ?local macro in content or attribute-value position. Key may be a binary or an atom (an atom is normalized to a binary, so it reaches the client as the same string).

make_child_snap(Template, ChildD, ChildDeps, Id)

-spec make_child_snap(Template, ChildD, ChildDeps, Id) -> snapshot()
                         when
                             Template :: template(),
                             ChildD :: [{az(), term()}],
                             ChildDeps :: [deps()],
                             Id :: binary().

Builds a stateful child snapshot, propagating diff => false from the template.

mark_esc(Value)

-spec mark_esc(Value) -> term() when Value :: term().

Marks an evaluated value for HTML escaping at output. Only scalars are marked -- nested templates/descriptors and stateless-child snapshots (all maps) and effects pass through untouched so they render structurally (their own inner dynamics carry their own marks). A stateless descriptor returned from a conditional content slot reduces to a bare snapshot map here, so the is_map clause passes it through and scope_slot/2 later namespaces it -- the same treatment a directly-slotted stateless child gets.

mark_esc/1 is reached only for an escape-tagged content slot ({esc, Fun}), which the parse transform emits for every content interpolation that is not a literal block -- a literal raw/1 at the template site compiles to a bare (un-esc-tagged) slot and never reaches here. So a {arizona_raw, V} arriving here was produced by a helper returning raw/1 into an escaping slot, which the documented rule forbids ("the parse transform only recognizes the opt-out when the raw call is literal at the template site"). Honor that rule: unwrap the raw and escape its inner value like any plain scalar, so a helper wrapping user data in raw/1 cannot smuggle trusted markup past the escaper (on SSR or across the live diff).

maybe_propagate(Template, Snapshot)

-spec maybe_propagate(Template, Snapshot) -> Snapshot1
                         when Template :: template(), Snapshot :: snapshot(), Snapshot1 :: snapshot().

Propagates the template-level fields that must survive onto a snapshot: f (fingerprint), the optional diff => false flag, the render backend, and the a attributes the template declares.

Used by both the render path (arizona_render) and the diff path (arizona_diff), so a diffed snapshot carries the same backend as a freshly rendered one -- the field is a uniform invariant on every snapshot.

native(Elems)

-spec native(term()) -> no_return().

Compile-time stub for the native (JSON) render target. The parse transform replaces every ?native(...) (and arizona_template:native/1) call with a precomputed template/0 map whose statics are JSON fragments. If this function runs, the parse transform was not applied.

nodiff_token(F, Branch, Key)

-spec nodiff_token(F, Branch, Key) -> binary()
                      when F :: binary(), Branch :: pos_integer() | undefined, Key :: term().

The value of the az-nodiff attribute on the root of an az-nodiff element: its identity as same_nodiff/3 decides it -- the template's fingerprint F, the conditional branch that produced it and its az_key -- spelled as one opaque token. The client matches az-nodiff elements by it, so a wholesale re-render around a az-nodiff element (a reconnect, a container ?OP_TEXT) carries the existing DOM over to a new render naming the same az-nodiff element, and replaces it otherwise.

The token is not scoped: where the az-nodiff element sits (its slot, the item it is in) is read off the DOM by the client, which is where those addresses already are.

raw(Value)

-spec raw(Value) -> {arizona_raw, term()} when Value :: term().

Wraps a value to opt out of HTML auto-escaping. Use only for trusted, already-safe HTML fragments -- never for user-controlled data.

This is an HTML-target feature: on the live diff a ?raw value is tagged so the client innerHTMLs it (vs text-noding a plain ?get value -- the escaping boundary). The ?native and ?terminal targets do not HTML-escape, so ?raw there is a no-op with no defined meaning; using it in a ?native/?terminal template is unsupported.

same_nodiff(New, Old)

-spec same_nodiff(New, Old) -> boolean() when New :: snapshot(), Old :: snapshot().

Whether two az-nodiff snapshots of the same fingerprint are the same az-nodiff element: the same producing branch and az_key, and the same child view when they are one.

same_nodiff(Az, Tmpl, Old)

-spec same_nodiff(Az, Tmpl, Old) -> boolean() when Az :: binary(), Tmpl :: term(), Old :: snapshot().

Whether the template Tmpl, about to fill the slot Az, is the az-nodiff element the slot's snapshot Old already holds: the same scoped fingerprint, the same producing branch and the same az_key.

scope_slot(Az, Value)

-spec scope_slot(Az, Value) -> term() when Az :: az(), Value :: term().

Namespaces a slot's rendered value by the enclosing slot Az, so a repeated stateless child (or any inline nested template) is an independent diff target. Prefixes the value's fingerprint, its statics' baked az/marker ids, and every inner dynamic az (recursing through inline nested templates) with the slot az.

A stateless component has no view_id boundary of its own, and its inner az ids derive only from its own template fingerprint (<Fp>-<n>). So the same render function rendered more than once in a view produces byte-identical az targets; on the client every diff op resolves with querySelector (first match) and lands on the first instance. Namespacing by the slot az -- applied here, where the slot is in scope -- keeps each instance distinct. The same prefixing makes two structurally identical inline nested templates (e.g. same-shaped conditional branches) in different slots distinct too.

The prefix is made colon-free (: -> -): the client treats : as the content-slot-index separator (a base element az never contains one) and recovers a base element by stripping at the first colon, so an internal colon in the prefix would misroute multi-slot and ?each-among-siblings ops. A stateful child (view_id boundary), an ?each container (items are addressed relative to the container, not by global az), and a client-owned ?local slot are left untouched; a scalar (escaped or bare) has no az to prefix and passes through. This is exactly the structural discrimination scope_val/2 applies inside an already-scoped snapshot, reused here for the top-level slot value -- so a stateless child evaluates to a plain snapshot map (no wrapper tag) and is scoped by the same #{s, d} path as an inline nested template.

split_triples(Triples)

-spec split_triples(Triples) -> {DList, DepsList}
                       when
                           Triples :: [{az(), term(), deps()}],
                           DList :: [{az(), term()}],
                           DepsList :: [deps()].

Splits a list of {Az, Val, Deps} triples into the snapshot d-list and deps list. (arizona_render:render/2 needs the document too and fuses this split into its zip walk, zip_triples/3.)

stateful(Handler, Props)

-spec stateful(Handler, Props) -> stateful_descriptor() when Handler :: module(), Props :: map().

Builds a stateful child descriptor. The renderer mounts Handler with Props.

stateless(Callback, Props)

-spec stateless(Callback, Props) -> stateless_descriptor()
                   when Callback :: fun((map()) -> template()), Props :: map().

Builds a stateless child descriptor from a 1-arity render fun.

stateless(Handler, Fun, Props)

-spec stateless(Handler, Fun, Props) -> stateless_descriptor()
                   when Handler :: module(), Fun :: atom(), Props :: map().

Builds a stateless child descriptor from a Handler:Fun/1 reference.

stateless_template(Descriptor)

-spec stateless_template(Descriptor) -> term() when Descriptor :: stateless_descriptor().

Renders a ?stateless descriptor to its template, passing on the branch stamp the descriptor carries (see branch/2).

terminal(Elems)

-spec terminal(term()) -> no_return().

Compile-time stub for the terminal (ANSI) render target. The parse transform replaces every ?terminal(...) (and arizona_template:terminal/1) call with a precomputed template/0 map whose statics are ANSI-decorated text. If this function runs, the parse transform was not applied.

to_bin(Value)

-spec to_bin(Value) -> binary() when Value :: term().

Converts a template value to its binary HTML representation.

Errors with {bad_template_value, V} for unsupported types.

track(Key)

-spec track(Key) -> ok when Key :: term().

Records Key as a dependency of the dynamic element currently being rendered.

Called automatically by get/2,3 and get_lazy/3. Public so render code can attribute dependencies that bypass binding access.

It is a no-op outside a dynamic's evaluation, since that is where the dependency scope is opened. Do not read that as "a ?get in the body of render/1 records nothing" -- what a slot depends on is decided at compile time, not here. The parse transform inlines a fresh read into the slot's own closure, so the body-level call is the one that no-ops while the inlined one records. erlc -P shows which reads ended up inside which closure, and is the only reliable way to answer "why does this slot not update". See "The invariant" in docs/architecture.md.

unwrap_val(Backend, Value)

-spec unwrap_val(Backend, Value) -> term() when Backend :: module(), Value :: term().

Materializes an attribute dynamic into its rendered binary through Backend (an arizona_renderer), leaving plain values untouched.

visible_keys(Order, Limit)

-spec visible_keys(Order, Limit) -> Order1
                      when
                          Order :: {[term()], [term()]},
                          Limit :: pos_integer() | infinity,
                          Order1 :: [term()].

Returns the visible portion of a stream order buffer given a Limit.

The order is the arizona_stream record's {Front, BackRev} 2-list buffer (Back holds recently-appended keys NEWEST-first). This function flushes the buffer to a flat oldest-first list and applies the limit. infinity returns the full order; an integer truncates via lists:sublist/2.

with(Keys, Bindings)

-spec with(Keys, Bindings) -> Bindings1 when Keys :: [term()], Bindings :: map(), Bindings1 :: map().

Projects Bindings to a sub-map of only Keys, tracking each key as a dependency.

Use it to hand a bindings subset to a sub-context (a helper, a child, a passed-through map) while declaring exactly which keys it depends on: the projection means the sub-context cannot silently read an untracked key (an omitted key fails loudly with missing_binding rather than freezing), and the tracking makes the enclosing slot re-render when any listed key changes.

Equivalent to track/1 on each key followed by maps:with(Keys, Bindings).