Hecr
Constants
Class methods
Joins class-name parts for the :class attribute (generated code).
nil/false parts drop (conditional classes), whitespace-separated tokens
within a part dedupe preserving first occurrence — scoped values from
CSS modules repeat shared composes bases.
Writes value HTML-escaped to io. Safe values pass through verbatim,
nil renders as nothing, numbers and booleans need no escaping.
Marks a string as already-safe HTML. raw is the only escape hatch from
escaped-by-default interpolation.
Uniform default-slot protocol: generated component calls without named
slot entries pass a block receiving one object and write rendered
content to Hecr.slot_sink(obj). For IO-protocol components that's the
IO itself; for slot-builder components (Hecr.slots) the content is
captured as the builder's inner entry.
Macros
Renders a template file by appending to an existing IO variable — streaming mode, no intermediate string:
Hecr.embed "page.hecr", response_io
Defines one function component per template file matching glob
(relative to the calling file). Every file must open with a contract
directive — a verbatim Crystal def head whose name matches the file
basename (decision D10):
<%@ def card(title : String, footer : String? = nil) %>
styles: names a CSS modules provider (a module exposing a
.styles(path, name:) macro, e.g. LightningCSSModules). Each template
is then paired with its colocated <basename>.module.css (or the file
named by its <%@ styles "..." %> directive), and the template's
:class attribute resolves class names through the generated styles
module — misspelled classes fail the build (decision D12):
Hecr.embed_templates "components/*.hecr", styles: LightningCSSModules
Renders an inline template (use a RAW heredoc so Crystal doesn't
interpolate #{} in the template — see docs/dialect.md):
def button(label : String) : Hecr::Safe Hecr.render <<-'HECR' <button class="btn">{label}</button> HECR end
Generates a slot-builder struct from a declaration (decision D6).
The builder is yielded to the component's block; each named slot entry
is stored as a typed proc plus its attributes. Generic type parameters
flow from the component's arguments into :let bindings with no
annotations at the call site (spike 04).
Hecr.slots TableSlots(T) do col label : String, let : T footer end
def table(rows : Array(T), & : TableSlots(T) ->) : Hecr::Safe forall T slots = TableSlots(T).new yield slots Hecr.render <<-'HECR' ...<th :for={c <- slots.col}>{c.label}...{c.render(row)}... HECR end
For every slot name the struct gets:
name— getter returning the entries collected so faradd_name(attrs...) { |io, let| }— adder (used by generated code)add_name(attrs...)— body-less adder (<:name attrs/>) and each entry exposes its attrs, itsbodyproc, andrender(let) : Hecr::Safe.
The reserved let parameter declares the type yielded to the entry's
block (use a Tuple type for multiple values). An inner slot (the
default slot) is generated automatically unless declared explicitly —
declare it to give the default slot a let type. render_inner(let)
renders all default-slot entries in order.
Renders a template file (path relative to the calling file) and returns
Hecr::Safe.