Documentation
¶
Overview ¶
Package htmlbind is the rendering runtime for generated HTML templates.
Generation produces one immutable Plan per component: an ordered instruction list typed by that component's parameter struct. A shared coordinator walks the plan and writes HTML, so composition concerns such as slots, chain nesting, and document head merging live here rather than in generated code.
The package depends on the standard library only, and never on net/http, so generated code stays usable on TinyGo and WebAssembly targets. Response concerns such as content type and content encoding belong to the caller.
Index ¶
- Constants
- Variables
- func Concurrent(ctx context.Context, tasks ...func() error) error
- func ErrUnsetPending(path string) error
- func Escape(value string) string
- func Flush(w io.Writer)
- func FormatBool(value bool) string
- func FormatFloat(value float64) string
- func FormatInt(value int) string
- func HasAwaitBlock(wrappers []Wrapper, leaf Fragment) bool
- func JSONArray[T any](values []T, encode func(T) string) string
- func JSONBool(value bool) string
- func JSONFloat(value float64) string
- func JSONInt(value int) string
- func JSONOptional[T any](value *T, encode func(T) string) string
- func JSONString[T ~string](value T) string
- func KeyArray[T any](values []T, encode func(T) string) string
- func KeyBool(value bool) string
- func KeyBytes(value []byte) string
- func KeyFloat(value float64) string
- func KeyInt(value int) string
- func KeyOptional[T any](value *T, encode func(T) string) string
- func KeyString[T ~string](value T) string
- func KeyTime(value time.Time) string
- func MergeHead(wrappers []Wrapper, leaf Fragment) []string
- func Render(w io.Writer, leaf Fragment, options ...Option) error
- func RenderAsync(ctx context.Context, w io.Writer, leaf Fragment, options ...Option) iter.Seq2[Content, error]
- func RenderChain(w io.Writer, wrappers []Wrapper, leaf Fragment, options ...Option) error
- func RenderChainAsync(ctx context.Context, w io.Writer, wrappers []Wrapper, leaf Fragment, ...) iter.Seq2[Content, error]
- type AsyncError
- type Builder
- func (Builder[P]) Attr(name string, value func(P) (string, bool)) Op[P]
- func (Builder[P]) BoolAttr(name string, value func(P) bool) Op[P]
- func (Builder[P]) Component(bind func(P) Fragment) Op[P]
- func (Builder[P]) If(condition func(P) bool, then, otherwise []Op[P]) Op[P]
- func (Builder[P]) MergedHead() Op[P]
- func (Builder[P]) Raw(value func(P) string) Op[P]
- func (Builder[P]) Slot(value func(P) Fragment, fallback []Op[P]) Op[P]
- func (Builder[P]) Static(text string) Op[P]
- func (Builder[P]) Text(value func(P) string) Op[P]
- type CachePolicy
- type CacheStore
- type Content
- type Fragment
- type MemoryCache
- type Op
- type Option
- type Pending
- type Plan
- type PublicError
- type Renderer
- type ScriptJSON
- type TrustedCSS
- type TrustedHTML
- type TrustedJavaScript
- type UnsetPendingError
- type Wrapper
Constants ¶
const ( // ErrorCodeInternal is the code for any failure that supplied no public // projection of its own. Raw Go errors never reach a recover subtree. ErrorCodeInternal = "internal" // ErrorCodeTimeout is the code for a boundary that exceeded its deadline. ErrorCodeTimeout = "timeout" )
Error codes carried by AsyncError. Generated templates compare against them through the built-in error type's code field.
Variables ¶
var ErrNilWrapper = errors.New("htmlbind: chain contains an unset wrapper")
ErrNilWrapper reports a wrapper that was left unset.
var ErrNoLeaf = errors.New("htmlbind: chain needs a leaf component")
ErrNoLeaf reports a chain assembled without an innermost component.
Functions ¶
func Concurrent ¶ added in v0.1.16
Concurrent runs every task in its own goroutine and reports the first failure in task order. Generated await clauses call it with one task per binding.
This is the whole reason an async external stays an ordinary blocking Go function: the function knows how to fetch its value, and the runtime owns running it off the render path and joining the results. Each task assigns its own field of the boundary scope, so the tasks share no memory and need no lock.
ctx bounds the wait, not the work. When it is cancelled Concurrent returns straight away and the still-running tasks are abandoned: their results are discarded and the caller must not read the scope they were writing. A task that wants to stop early has to take a context of its own.
func ErrUnsetPending ¶ added in v0.1.19
ErrUnsetPending builds the error generated code raises for an unset required async value.
func Escape ¶ added in v0.1.15
Escape applies HTML text and attribute escaping. It is exported so generated helpers can reuse exactly the runtime's rules.
func Flush ¶ added in v0.1.16
Flush pushes buffered bytes toward the client when the writer can. io.Writer has no flush, so the capability is discovered by interface assertion rather than reflection, and a writer without it still produces correct output, only without progressive delivery.
The render entries flush after the initial pass. Call this after writing each Content, because a completion sitting in a buffer defeats the point of having sent it early.
func FormatBool ¶ added in v0.1.16
FormatBool renders a bool as template text.
func FormatFloat ¶ added in v0.1.16
FormatFloat renders a float64 as template text.
func HasAwaitBlock ¶ added in v0.1.18
HasAwaitBlock reports whether any member of a chain can open an await boundary. It answers the one question a caller has to settle before rendering: whether this response needs the client runtime that applies settled boundaries.
Ask once for the whole chain rather than per member, so a chain whose layout and page both await still contributes one runtime script.
func JSONArray ¶ added in v0.1.16
JSONArray encodes a slice as a JSON array, delegating each element.
func JSONOptional ¶ added in v0.1.16
JSONOptional encodes a nil pointer as JSON null and otherwise delegates.
func JSONString ¶ added in v0.1.16
JSONString encodes any string-kinded value as a JSON string, covering plain strings, generated enums, and the trusted string types above. It escapes the characters that could close a script element or break a JavaScript line, so the result is safe to embed in inline script content.
func KeyArray ¶ added in v0.1.16
KeyArray frames a slice as its element count followed by its framed elements, so a slice of one two-element string cannot collide with two one-element ones.
func KeyOptional ¶ added in v0.1.16
KeyOptional frames a pointer, distinguishing absence from any present value.
func KeyString ¶ added in v0.1.16
KeyString frames a string for a cache key. It is generic over ~string so a generated enum or trusted string type needs no conversion at the call site.
func KeyTime ¶ added in v0.1.16
KeyTime frames a time for a cache key. It uses a fixed layout with nanosecond precision so two equal instants in different locations encode identically.
func MergeHead ¶ added in v0.1.15
MergeHead collects head contributions in composition order, outermost first, dropping later duplicates so two components declaring the same stylesheet emit one tag.
func RenderAsync ¶ added in v0.1.16
func RenderAsync(ctx context.Context, w io.Writer, leaf Fragment, options ...Option) iter.Seq2[Content, error]
RenderAsync renders one component and yields each await boundary as it settles.
func RenderChain ¶ added in v0.1.15
RenderChain writes a composed document to w. Wrappers apply outermost first, so RenderChain(w, []Wrapper{document, layout}, page) renders page inside layout inside document. An empty wrapper list renders the leaf alone.
Head contributions are merged before the first byte is written, so the shell can emit them without buffering the body. Assembly is validated up front, so a malformed chain cannot leave a partial response behind.
An await boundary reached on this path blocks and emits its settled subtree in place, so one template renders correctly with or without progressive delivery. Use RenderChainAsync to send fallbacks first instead.
func RenderChainAsync ¶ added in v0.1.16
func RenderChainAsync(ctx context.Context, w io.Writer, wrappers []Wrapper, leaf Fragment, options ...Option) iter.Seq2[Content, error]
RenderChainAsync renders a composed document to w and yields one Content per settled await boundary, in completion order.
Rendering starts on the first pull. The initial pass writes the document with every boundary's fallback inside its placeholder and flushes, so a slow dependency does not delay the first bytes. Each later item is the replacement for one placeholder, and the ranging caller writes it, because only the caller may touch the response:
for content, err := range htmlbind.RenderChainAsync(ctx, w, wrappers, page) {
if err != nil {
log.Printf("render failed: %v", err)
break
}
if err := writeCompletion(w, content); err != nil {
break
}
htmlbind.Flush(w)
}
A yielded item is the bare fragment plus the id of the placeholder it belongs to. How that pair travels — an inert template and a marker element, a JSON record, anything else — is the caller's choice, because it has to match the client runtime the caller ships. Nothing on this path writes script, and the merged head carries component contributions only.
There is no variant that hides this loop. How many boundaries a render produces is not knowable up front, least of all for a chain assembled at request time, so a handler that streams has to be written against the sequence anyway.
Once the initial pass commits, the response status can no longer change, so a later error is for logging rather than for rewriting the response.
The sequence is single-use and single-consumer. Stopping the range early ends the render without waiting for the outstanding boundaries.
Types ¶
type AsyncError ¶ added in v0.1.16
type AsyncError struct {
// Code is a stable classification, either an application code or one of the
// ErrorCode constants above.
Code string
// Message is optional presentation-safe text. It is empty unless the failing
// error supplied one.
Message string
// Retryable reports whether the UI may offer a retry.
Retryable bool
// Timeout reports whether the configured async deadline expired.
Timeout bool
}
AsyncError is the presentation-safe failure value an await boundary's recover clause receives. It carries only fields a template may render; the original Go error stays server-side and reaches the caller through WithErrorReporter.
type Builder ¶ added in v0.1.15
type Builder[P any] struct{}
Builder constructs instructions for one component. Generated code declares one per component so the parameter type is written once instead of on every instruction.
func (Builder[P]) Attr ¶ added in v0.1.15
Attr writes one attribute. The value arrives already escaped, because a mixed value concatenates author literals with escaped expressions and only the expressions may be escaped. present reports whether an optional value exists; an absent value omits the whole attribute.
func (Builder[P]) BoolAttr ¶ added in v0.1.15
BoolAttr writes a bare attribute name when the value is true and omits it otherwise.
func (Builder[P]) Component ¶ added in v0.1.15
Component renders another component. bind pairs the callee's plan with arguments derived from the caller's parameters.
func (Builder[P]) MergedHead ¶ added in v0.1.15
MergedHead writes every chain member's head contributions. The document shell places it inside its own head element.
func (Builder[P]) Raw ¶ added in v0.1.15
Raw writes a value that the template already marked trusted for its context.
func (Builder[P]) Slot ¶ added in v0.1.15
Slot inserts a bound slot argument. When the argument is absent the fallback instructions run, which is how default slot content is expressed. An absent slot with no fallback emits nothing at all.
type CachePolicy ¶ added in v0.1.16
type CachePolicy[P any] struct { // ID is the component identity plus a fingerprint of its generated plan, so // regenerated code cannot read entries written by the previous code. ID string // TTL is how long an entry may be reused. TTL time.Duration // Key appends the canonical encoding of every declared parameter. Key func(P) string }
CachePolicy is the cache configuration compiled into a component's plan. Generated code builds one; application code never does.
type CacheStore ¶ added in v0.1.16
type CacheStore interface {
// Get returns previously stored output for key. An expired or absent entry
// reports false. The returned bytes are written unmodified, so a store must
// not reuse or mutate the slice it hands back.
Get(ctx context.Context, key string) ([]byte, bool)
// Set stores output for at most ttl. It returns nothing: a cache write
// failure must not fail a response that already rendered correctly, so an
// implementation reports its own failures.
Set(ctx context.Context, key string, value []byte, ttl time.Duration)
}
CacheStore holds rendered component output for components declared with the cache annotation. The caller supplies one per render through WithCache, so a store is an ordinary caller resource rather than package state.
An implementation is used from several goroutines during one render and must be safe for concurrent use.
type Content ¶ added in v0.1.16
type Content struct {
// BoundaryID matches the placeholder written during the initial pass.
BoundaryID string
// HTML is the already escaped and context-checked replacement fragment.
// Consumers must not escape it again.
HTML []byte
}
Content is one settled await boundary: the placeholder it replaces and the HTML that replaces it. The runtime yields these after the initial document write, and the ranging caller is the only code that writes them.
func (Content) WriteTo ¶ added in v0.1.16
WriteTo writes the settled fragment and nothing else: no wrapper element, no marker, no script.
htmlbind does not pick a wire format for completions. The framing that carries a fragment and the client code that acts on it are one design, and it belongs to whoever ships the runtime — a framework built on htmlbind, or the handler itself. BoundaryID is what ties this fragment back to its placeholder, so the caller has both halves and writes the framing around this call.
type Fragment ¶ added in v0.1.15
type Fragment struct {
// contains filtered or unexported fields
}
Fragment is a component with its parameters already bound. It is the runtime value behind the html template type, so a slot argument can be passed between components and across template files without either side naming the other's parameter struct.
The zero Fragment is absent, which is how an optional slot with no argument is represented.
func (Fragment) HasAwaitBlock ¶ added in v0.1.18
HasAwaitBlock reports whether rendering this fragment can open an await boundary, so a caller knows whether a response will need the client runtime that applies settled boundaries. Reading it renders nothing.
A fragment passed in through a parameter is not counted, because the binder cannot look inside a caller's parameter struct without reflection. That fragment is in the caller's own hand, so a caller composing slots unions the flag across the values it holds. HasAwaitBlock over a chain does that for the ordinary document, layout, and page shape.
type MemoryCache ¶ added in v0.1.16
type MemoryCache struct {
// contains filtered or unexported fields
}
MemoryCache is an in-process CacheStore with TTL expiry and a maximum entry count. It is the default a single-process server needs; a shared store is an adapter the caller writes.
func NewMemoryCache ¶ added in v0.1.16
func NewMemoryCache(maxEntries int) *MemoryCache
NewMemoryCache returns a store holding at most maxEntries entries. A non-positive maxEntries means unbounded.
func (*MemoryCache) Len ¶ added in v0.1.16
func (c *MemoryCache) Len() int
Len reports how many entries the cache currently holds, including entries that have expired but not yet been evicted.
type Op ¶ added in v0.1.15
type Op[P any] interface { // Exec writes this instruction's output. Implementations are immutable and // safe to share across goroutines. Exec(r *Renderer, params P) error }
Op is one instruction of a render plan. P is the parameter struct of the component the instruction belongs to, so every step stays statically typed and no reflection is needed.
func Await ¶ added in v0.1.16
func Await[P, S, R any]( resolve func(context.Context, P) (S, error), recovery func(P, AsyncError) R, primary []Op[S], fallback []Op[P], handler []Op[R], ) Op[P]
Await opens an await boundary. resolve runs the clause's bindings and builds the primary subtree's scope; recovery builds the recover subtree's scope from the outer parameters and the safe error. handler is nil when the clause declared no recover subtree.
It is a free function rather than a Builder method because the primary and recover subtrees each read their own generated scope type.
func For ¶ added in v0.1.15
For repeats body once per item. scope builds the body's parameter value from the enclosing parameters, the item, and its index, so the loop variable stays statically typed instead of becoming an untyped lookup.
func Require ¶ added in v0.1.19
Require fails the render when check rejects the parameters. Generation emits it ahead of an await boundary that binds a required async parameter, so a caller who left one unset gets an error before the boundary commits its fallback and fixes the response status.
It writes nothing, which is the point: the check has to run on the initial pass, where a failure can still become an error response, rather than in the boundary goroutine that runs after the response is already committed.
type Option ¶ added in v0.1.16
type Option func(*renderOptions)
Option configures one render. Options are variadic so a call that needs nothing beyond a writer and a component stays two arguments long.
func WithAsyncTimeout ¶ added in v0.1.16
WithAsyncTimeout bounds how long one await boundary's bindings may run. An expired boundary fails with ErrorCodeTimeout. Zero means no deadline beyond the request context.
func WithCache ¶ added in v0.1.16
func WithCache(store CacheStore) Option
WithCache supplies the store used by components declared with the cache annotation. Without it those components render normally, so caching is a deployment choice rather than a template rewrite.
The store belongs to the caller: passing it per render keeps two servers in one process from sharing entries through package state.
func WithConcurrencyLimit ¶ added in v0.1.16
WithConcurrencyLimit bounds how many await boundaries may have work running at once across the whole render. Zero or less means unbounded.
func WithContext ¶ added in v0.1.16
WithContext supplies the context for the synchronous entries, which take no ctx parameter of their own. A cache store and a blocking await boundary both use it, so a cancelled request stops doing work. The async entries take their context directly and ignore this option.
func WithErrorReporter ¶ added in v0.1.16
WithErrorReporter receives the original Go error behind every await boundary failure, including failures a recover clause handled and failures a clause without recover left as a committed fallback. Recover subtrees see only the safe AsyncError, so this is where logging and metrics attach.
type Pending ¶ added in v0.1.19
type Pending[T any] struct { // contains filtered or unexported fields }
Pending is one value the caller started before rendering and a template waits for in an await clause. It is the Go form of the template's `async T`.
It settles once and stays readable, which is the whole reason it is a handle and not a channel: a layout and the page inside it may hold the same value, and a channel would deliver it to whichever of them received first while the other blocked until the boundary deadline. Every await of one handle sees the same settled result, and the work behind it runs once.
The zero value is unset. Awaiting an unset handle is legal exactly where the template declared the awaited type optional, and yields an absent value rather than a failure; generation rejects an unset handle anywhere else before the response commits. Nothing about an unset handle panics or blocks.
This is a render parameter type, not a general future. It has no combinators and no caller-facing wait: code that wants to compose work does so with ordinary goroutines before handing the result over.
func Failed ¶ added in v0.1.19
Failed returns a handle that has already settled to err. The boundary that awaits it renders its recover subtree.
func Go ¶ added in v0.1.19
Go starts work in its own goroutine and returns the handle to pass as a template parameter. The work receives the caller's context, so bounding or cancelling it stays the caller's business; a render only bounds how long it waits.
A panic in the work becomes the handle's error, the way an async external's panic does, so a boundary reports it through its recover clause instead of taking the process down.
func Resolved ¶ added in v0.1.19
Resolved returns a handle that has already settled to value. It is what a caller passes when it computed the value itself, and what a test passes instead of starting a goroutine.
func (Pending[T]) IsSet ¶ added in v0.1.19
IsSet reports whether this handle carries work. Generated code calls it before the response commits, so a caller that forgot to supply a required value gets an error response rather than a boundary that waits for nothing.
func (Pending[T]) Wait ¶ added in v0.1.19
Wait returns the settled value. ctx bounds the wait, never the work.
An unset handle returns the zero value and no error, because absence is data rather than failure wherever the template allowed it. It is deliberately not a panic and deliberately not an infinite wait: the zero value of a struct field is exactly what a caller who supplied nothing left behind.
type Plan ¶ added in v0.1.15
type Plan[P any] struct { // Head holds this component's document head contributions as ready to // write HTML. They merge into the shell head before any body byte. Head []string // HasAwaitBlock reports whether this component, or any component it calls, // owns an await boundary. Generation computes it over the call graph, so a // component that only calls an async one still reports true. // // It exists so a caller can decide before rendering whether a response needs // the client runtime that applies settled boundaries, instead of that // decision being made for it inside the render entry points. HasAwaitBlock bool // Check rejects parameters this component cannot render, before it writes // anything. Generation emits it for a required async parameter, whose // absence has to be reported while the response can still carry an error // status rather than a half written document. // // It is nil for a component with nothing to check, which is most of them. Check func(P) error // Ops is the instruction list executed in order. Ops []Op[P] // Cache is set for a component declared with the cache annotation. It is // consulted only when the caller supplied a store through WithCache, so the // same generated code runs cached or uncached. Cache *CachePolicy[P] }
Plan is a component compiled to instructions. A plan is built once at package initialization and shared by every render.
type PublicError ¶ added in v0.1.16
type PublicError interface {
error
PublicError() AsyncError
}
PublicError is implemented by an error that supplies its own safe projection. Any other error is exposed to a recover clause as ErrorCodeInternal with no message, so error text cannot leak into a page by accident.
type Renderer ¶ added in v0.1.15
type Renderer struct {
// contains filtered or unexported fields
}
Renderer is the coordinator walking plans. It owns the output stream and the merged head, so instructions never touch either directly.
func (*Renderer) MergedHead ¶ added in v0.1.15
MergedHead returns the head contributions collected for this render.
type ScriptJSON ¶ added in v0.1.16
type ScriptJSON string
ScriptJSON is a JSON document destined for a script element. It is produced by the JSON encoders below, which escape the characters that would otherwise terminate the element.
type TrustedCSS ¶ added in v0.1.16
type TrustedCSS string
TrustedCSS is stylesheet text the template author vouched for.
type TrustedHTML ¶ added in v0.1.16
type TrustedHTML string
TrustedHTML is markup the template author vouched for. It is written without escaping, so it must never carry unvalidated input.
type TrustedJavaScript ¶ added in v0.1.16
type TrustedJavaScript string
TrustedJavaScript is script text the template author vouched for.
type UnsetPendingError ¶ added in v0.1.19
type UnsetPendingError struct {
// Path names the parameter or field as the template declared it.
Path string
}
UnsetPendingError reports a required async parameter or record field the caller left unset. It is raised during the initial pass, before any byte commits, so a handler can still turn it into an error response.
func (*UnsetPendingError) Error ¶ added in v0.1.19
func (e *UnsetPendingError) Error() string
type Wrapper ¶ added in v0.1.15
type Wrapper struct {
// contains filtered or unexported fields
}
Wrapper is a component that renders another one into its unnamed slot. Generated code returns one from Bind<Name> for a component with a children parameter.
func BindWrapper ¶ added in v0.1.15
BindWrapper pairs a plan with parameters and the setter that installs the child fragment. Generated code supplies the setter because only it knows which field the unnamed slot binds to.
func (Wrapper) HasAwaitBlock ¶ added in v0.1.18
HasAwaitBlock reports whether rendering this wrapper can open an await boundary. The child it wraps is counted separately, because a wrapper is bound before it is told what it wraps.