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 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 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 Plan
- type PublicError
- type Renderer
- type ScriptJSON
- type TrustedCSS
- type TrustedHTML
- type TrustedJavaScript
- 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 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 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 := content.WriteTo(w); err != nil {
break
}
htmlbind.Flush(w)
}
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 content as an inert template element followed by the marker that commits it. It never emits script, so a completion needs no CSP nonce.
The trailing marker is what makes the swap safe. An HTML parser inserts an element when it reads the start tag, so a runtime that reacted to the template's insertion could read a template whose content had not arrived yet and replace the placeholder with nothing. The marker comes after the closing tag in the byte stream, so by the time it exists the template is complete, however the bytes were chunked.
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.
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.
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 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 // 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 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.