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 ChainHead(wrappers []Wrapper, leaf Fragment, options ...Option) ([]string, error)
- func CollectChain(w io.Writer, collect Collector, wrappers []Wrapper, leaf Fragment, ...) ([]string, error)
- func CollectChainAsync(ctx context.Context, w io.Writer, collect Collector, rendered func() bool, ...) iter.Seq2[Content, error]
- 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 HasLiveBlock(wrappers []Wrapper, leaf Fragment) bool
- func IsPrivate(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 JSONMember(body, name, encoded string) 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 MergeVary(wrappers []Wrapper, leaf Fragment) []string
- func PrivateSource(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]
- func RenderChainLive(ctx context.Context, w io.Writer, wrappers []Wrapper, leaf Fragment, ...) iter.Seq2[Content, error]
- func RenderHeadNodes(nodes []HeadNode) ([]string, error)
- func RenderLive(ctx context.Context, w io.Writer, leaf Fragment, options ...Option) iter.Seq2[Content, error]
- type Asset
- type AsyncError
- type Boundary
- type Builder
- func (Builder[P]) Attr(name string, value func(P) (string, bool)) Op[P]
- func (Builder[P]) AttrCtx(name string, value func(context.Context, P) (string, bool)) Op[P]
- func (Builder[P]) BoolAttr(name string, value func(P) bool) Op[P]
- func (Builder[P]) BoolAttrCtx(name string, value func(context.Context, P) bool) Op[P]
- func (Builder[P]) BoundaryAttr() Op[P]
- func (Builder[P]) CSRFField(name string) Op[P]
- func (Builder[P]) Component(bind func(P) Fragment) Op[P]
- func (Builder[P]) ComponentCtx(bind func(context.Context, P) Fragment) Op[P]
- func (Builder[P]) If(condition func(P) bool, then, otherwise []Op[P]) Op[P]
- func (Builder[P]) IfCtx(condition func(context.Context, 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]) RawCtx(value func(context.Context, P) string) Op[P]
- func (Builder[P]) Require(check func(P) error) Op[P]
- func (Builder[P]) Slot(value func(P) Fragment, fallback []Op[P]) Op[P]
- func (Builder[P]) SlotCtx(value func(context.Context, 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]
- func (Builder[P]) TextCtx(value func(context.Context, P) string) Op[P]
- func (Builder[P]) URLAttr(name string, value func(P) (string, bool)) Op[P]
- func (Builder[P]) URLAttrCtx(name string, value func(context.Context, P) (string, bool)) Op[P]
- func (Builder[P]) URLListAttr(name, shape string, value func(P) (string, bool)) Op[P]
- func (Builder[P]) URLListAttrCtx(name, shape string, value func(context.Context, P) (string, bool)) Op[P]
- type CachePolicy
- type CacheStore
- type Collector
- type Content
- type Fragment
- func (f Fragment) Assets() []Asset
- func (f Fragment) HasAwaitBlock() bool
- func (f Fragment) HasLiveBlock() bool
- func (f Fragment) Head() []string
- func (f Fragment) HeadSources() []string
- func (f Fragment) InstanceID() string
- func (f Fragment) IsPrivate() bool
- func (f Fragment) Present() bool
- func (f Fragment) PrivateSource() string
- func (f Fragment) Validate() error
- func (f Fragment) Vary() []string
- type HeadAttr
- type HeadNode
- type LiveBinding
- type MemoryCache
- type Op
- func Await[P, S, R any](resolve func(context.Context, P) (S, error), recovery func(P, AsyncError) R, ...) Op[P]
- func For[P, E, S any](items func(P) []E, scope func(P, E, int) S, body []Op[S]) Op[P]
- func ForCtx[P, E, S any](items func(context.Context, P) []E, scope func(P, E, int) S, body []Op[S]) Op[P]
- func Live[P, S, R any](bindings func(context.Context, P) []LiveBinding[S], scope func(P) S, ...) Op[P]
- func Provide[P, V any](element, provider string, fn func(context.Context) (V, error), ...) Op[P]
- func Require[P any](check func(P) error) Op[P]deprecated
- type Option
- func WithAsyncTimeout(timeout time.Duration) Option
- func WithBoundaryPrefix(prefix string) Option
- func WithCSRFToken(token string) Option
- func WithCache(store CacheStore) Option
- func WithCacheScope(scope string) Option
- func WithConcurrencyLimit(limit int) Option
- func WithContext(ctx context.Context) Option
- func WithDataURLMediaTypes(mediaTypes ...string) Option
- func WithErrorReporter(report func(error)) Option
- func WithHead(nodes ...HeadNode) Option
- func WithLiveSubscriptions() Option
- func WithURLSchemes(schemes ...string) Option
- func WithValidatorTag(tag string) Option
- func WithoutCSRFToken() Option
- type Pending
- type Plan
- type PublicError
- type Renderer
- type ScriptJSON
- type Segment
- type SeqKind
- type SeqNode
- type Sequence
- type Signal
- type SignalPayload
- type TrustedCSS
- type TrustedHTML
- type TrustedJavaScript
- type UnrecoveredError
- type UnsetPendingError
- type Wrapper
- func (w Wrapper) Assets() []Asset
- func (w Wrapper) HasAwaitBlock() bool
- func (w Wrapper) HasLiveBlock() bool
- func (w Wrapper) Head() []string
- func (w Wrapper) HeadSources() []string
- func (w Wrapper) IsPrivate() bool
- func (w Wrapper) PrivateSource() string
- func (w Wrapper) Validate() error
- func (w Wrapper) Vary() []string
Constants ¶
const ( AssetTypeStyle = "text/css" AssetTypeScript = "text/javascript" )
Asset media types. They are the two kinds requirement:static-asset-extraction produces, and the module writes no other.
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.
const ( URLListSrcset = "srcset" URLListSpace = "space" )
URLListSrcset and URLListSpace name the two list grammars URLListAttr reads. They travel as strings because they appear in generated source, where a named constant reads better than a bare true or false.
const BlockedURL = "#tb-blocked-url"
BlockedURL is what a URL-bearing attribute renders when the value's scheme is not one this render permits.
It is a fragment, so it resolves to the current document and reaches nothing. Substituting it rather than dropping the attribute is deliberate: a dropped href is indistinguishable from an attribute the template never wrote, so a URL rejected in error would leave no trace to find it by.
const DefaultBoundaryPrefix = "tb"
DefaultBoundaryPrefix names the placeholder element a progressive render writes and the identifiers it allocates: <tb-boundary id="tb-1">.
It matches the generator's default data-attribute prefix, because a document carrying data-tb-id on its boundaries and <tb-boundary> placeholders is one naming system rather than two.
Variables ¶
var DefaultDataURLMediaTypes = []string{
"image/png",
"image/jpeg",
"image/gif",
"image/webp",
"image/avif",
"image/bmp",
"image/x-icon",
}
DefaultDataURLMediaTypes are the media types an inline data URL may carry.
An inline image is ordinary authoring, so the scheme is not refused outright. The roster is an allowlist of exact media types rather than an image/ prefix, which is what keeps image/svg+xml off it: an SVG document carries script, so it is a script sink wearing an image's media type.
var DefaultURLSchemes = []string{"http", "https", "mailto", "tel"}
DefaultURLSchemes are the schemes a URL-bearing attribute renders when the caller configures none.
The set is deliberately small. It can be, because WithURLSchemes exists: an app needing ftp, sms, or its own registered scheme says so, which is cheaper than defaulting schemes in for every app because one app might want them.
var ErrHeadNode = errors.New("htmlbind: invalid head contribution")
ErrHeadNode reports a head contribution a render call could not write.
var ErrNilWrapper = errors.New("htmlbind: chain contains an unset wrapper")
ErrNilWrapper reports a wrapper that was left unset.
var ErrNoCSRFToken = errors.New("htmlbind: form needs a CSRF token")
ErrNoCSRFToken reports a render that reached an unsafe form with no token to put in it.
var ErrNoLeaf = errors.New("htmlbind: chain needs a leaf component")
ErrNoLeaf reports a chain assembled without an innermost component.
var ErrNoRenderContext = errors.New("htmlbind: render needs a context")
ErrNoRenderContext reports a render that reached a per-request builtin element with no context to read from.
var ErrSequenceMismatch = errors.New("htmlbind: values do not match the sequence")
ErrSequenceMismatch reports values that do not fit the sequence they were walked against, which means the two came from different renders or different builds.
var ErrSignal = errors.New("htmlbind: signal")
ErrSignal matches any signal under errors.Is, for a caller that wants the classification without the value. Reading the name or the payload needs AsSignal.
Functions ¶
func ChainHead ¶ added in v0.4.3
ChainHead returns the merged head a render of this chain would write: every member's contributions plus the caller's own, deduplicated in composition order. It renders nothing, so a caller that sends the head ahead of any markup — a streamed delta does — can know it first.
func CollectChain ¶ added in v0.3.0
func CollectChain(w io.Writer, collect Collector, wrappers []Wrapper, leaf Fragment, options ...Option) ([]string, error)
CollectChain renders like RenderChain with collect observing the render, and returns the merged head. Every chain member declaring a boundary opens one around its own output, so the observer sees the layout chain rather than every component call.
Collecting emits the instance attribute on each boundary's root element, so the output differs from RenderChain by exactly those attributes.
func CollectChainAsync ¶ added in v0.4.3
func CollectChainAsync(ctx context.Context, w io.Writer, collect Collector, rendered func() bool, wrappers []Wrapper, leaf Fragment, options ...Option) iter.Seq2[Content, error]
CollectChainAsync is CollectChain for the streaming path: the initial pass renders with every await boundary's fallback in place, and the sequence then yields each boundary as it settles, exactly as RenderChainAsync does.
rendered runs after the initial pass commits and before the first completion, which is the moment the observer's state describes the whole document. Returning false ends the sequence without waiting for the outstanding boundaries. A nil rendered is allowed and skips the call.
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 HasLiveBlock ¶ added in v0.2.7
HasLiveBlock reports whether any member of a chain can open a live boundary.
It answers a different question from HasAwaitBlock. That one decides whether this response needs the client runtime that applies boundaries at all; this one decides whether the screen will keep changing once the document has finished, and so whether a live request is worth issuing. A document with await boundaries and no live ones is complete when its sequence ends.
func IsPrivate ¶ added in v0.4.11
IsPrivate reports whether a whole chain's output belongs to one reader. It is the value a caller turns into a cache policy header, and it is readable before the chain renders, as ChainHead and HasAwaitBlock are.
Two rules decide it, and both follow from what a chain member contains:
Private wins. A member declaring private makes the response private whatever anything else says, because that member's bytes are in the response. This also covers a combination assembled at run time that generation never saw, where the refusal of public over private could not have fired.
Public has to come from the outside in. A wrapper contains everything below it, so a public declaration on the outermost member covers the whole chain — which is what lets one annotation on a layout serve every page beneath it. A declaration further in covers only itself and what it wraps, and says nothing about the markup wrapped around it, so it cannot make the response shared on its own. A page asserting public under an undeclared layout therefore stays private: the layout's own output was never declared, and it is in the response too.
func JSONArray ¶ added in v0.1.16
JSONArray encodes a slice as a JSON array, delegating each element.
func JSONMember ¶ added in v0.5.8
JSONMember appends one member to a JSON object body under assembly, writing the separator only when something precedes it.
It exists so a component's emitted parameters can omit an absent optional rather than writing null: the generated code guards the call, and a member that is never appended leaves no trace. One absence for JavaScript to test beats two, and it matches what the attribute context already does when it omits a whole attribute rather than emitting an empty one.
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 MergeVary ¶ added in v0.3.3
MergeVary is the union of a whole chain's vary axes, deduplicated and in composition order. It takes the same argument pair as MergeHead.
func PrivateSource ¶ added in v0.4.11
PrivateSource names the component whose declaration made a chain private, searching outermost first. It is empty when the chain is private only because nothing declared otherwise, and when the chain is shared.
It is the chain form of the accessor documented on Fragment, and it exists for the reason HeadSources gives: a caller explaining a private response has to be able to name the component to change.
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.
Bindings that fail in a clause with no recover subtree return an UnrecoveredError rather than writing the fallback, because a finished document holding a loading state is a document that lies. This path writes as it goes, so render into a buffer when you want that failure to become an error status.
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.
One of those errors is UnrecoveredError: a boundary's bindings failed in a clause with no recover subtree. Ending the sequence there is the point, since the alternative is a page left showing a fallback that will never be replaced. The caller still owns the response, and what it writes next — an error screen replacing the document, whatever its runtime applies — is its own to frame.
The sequence is single-use and single-consumer. Stopping the range early ends the render without waiting for the outstanding boundaries.
func RenderChainLive ¶ added in v0.2.7
func RenderChainLive(ctx context.Context, w io.Writer, wrappers []Wrapper, leaf Fragment, options ...Option) iter.Seq2[Content, error]
RenderChainLive renders a composed document to w and yields one Content per delivery, for as long as the live boundaries keep producing.
It is RenderChainAsync with one difference: a live boundary stays subscribed. The returned sequence therefore does not end when the await boundaries have settled. It ends when every live source has ended, when the consumer stops ranging, or when ctx is cancelled — which is the shape a screen that updates on the server's clock needs, and the reason the entry is named for the subscription rather than for the transport carrying it.
Pass io.Discard as w to run the render for its deliveries alone. The document bytes are still produced, because evaluating a live clause's source arguments means executing the component that holds them, but nothing is transferred. Boundary ids are allocated in render order, so the same chain rendered again for the same request produces the same ids as the document render did, and a client can address the placeholders already on its screen without being told what they are.
Everything else matches RenderChainAsync: rendering starts on the first pull, only the ranging caller writes the response, and the framing around each Content is the caller's to choose. Deliveries from two boundaries interleave in completion order and carry no ordering guarantee between them.
A source that keeps producing while nobody reads is not a problem here: the sequence pulls, so the source blocks in its own yield until this boundary is ready for the next value. A fast source misses ticks rather than filling a queue.
The sequence is single-use and single-consumer.
func RenderHeadNodes ¶ added in v0.2.10
RenderHeadNodes turns caller-supplied nodes into the ready-to-write tags the merge works in, or reports the first node it cannot write.
A caller answering a fragment request — one with no document shell, and therefore no head to merge into — uses this to decide what to do with its own contributions rather than discovering later that they went nowhere.
Types ¶
type Asset ¶ added in v0.3.3
type Asset struct {
// ID identifies the file by its content. Two components requiring one asset
// carry one ID, and an edited file is a different one, which is what lets an
// immutable cache header stay honest.
ID string
// Type is the media type: text/css for a stylesheet, text/javascript for a
// script.
Type string
// URL is where the reference tag points, which is the generation-time public
// URL base joined to the file name.
URL string
// Scope names the component whose script block declared this file, and is
// empty for every other asset.
//
// Empty is document lifetime: the file evaluates once and is never released,
// which is what a head contribution has always been. A named one binds the
// file to that component's live instances.
//
// The name is the package-qualified declaration identity, pages.counter.Counter
// rather than Counter. It is the same string the generator writes into
// Boundary.ComponentID, and the same one it marks every rendered instance
// with as data-<prefix>-component, so a caller finds the elements an asset
// belongs to by matching this value against that attribute — no mapping and
// no second identity scheme. The short form is not used anywhere, because
// two components named Counter in two directories are one name and two
// declarations.
//
// The marker says which declaration an element came from, not which instance
// it is. It rides the static markup, so it lands on an ordinary component
// call, which opens no update boundary and carries no instance attribute,
// and on a first load, which holds no manifest because the manifest is a
// header the client sends back.
//
// Two instances of one component are marked identically, which is enough to
// run a lifecycle: a caller starts what carries the marker, and releases
// what sits inside the region it is about to replace, which it knows without
// asking because it owns the apply loop. Naming one instance to the server —
// redrawing a single Counter — is what needs the component to be an update
// boundary, and that is a different feature.
//
// The module publishes the owner and calls nothing. What a scoped script
// exports, when it is started, and when it is released are the caller's,
// which is the division decision:client-runtime-ownership already draws.
Scope string
}
Asset is one static file a component requires: a stylesheet or a script the generator extracted and named after the hash of its own bytes.
It is not derivable from Head. A head contribution is a ready-to-write tag, so a caller reading Head gets markup and has to parse it back out to learn what the component needs; and a fragment produced while rendering contributes no head at all. This is the same fact expressed as an identity a caller can act on: decide whether the document already carries it, decide whether a response with no document shell can deliver it, decide whether to preload it.
The division is the one the module keeps everywhere else. It decides what is required and what its identity is; where the bytes are served is the caller's.
func MergeAssets ¶ added in v0.3.3
MergeAssets is the required set of a whole chain, deduplicated by identity, in composition order outermost first.
It takes the same argument pair as MergeHead because it answers the same question one layer down: MergeHead says what tags this document writes, and this says what files those tags stand for.
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 Boundary ¶ added in v0.3.0
type Boundary[P any] struct { // ComponentID is the stable declaration identity of this component, // including its generated version. A template edit changes it, which // invalidates every validator derived from it. ComponentID string // Attr is the data attribute carrying the instance ID on the boundary's // root element. The generator writes the configured prefix into it. Attr string // Input canonically encodes the declared parameters, excluding slot // arguments, which belong to the child boundary rather than this frame. Input func(P) string // Instance returns the id this invocation is addressed by, for a component // that names its own — a reloadable one, whose id an author writes at the // call site. It is nil for a chain member, whose id comes from its position // in the chain instead. // // It is also what decides whether a component call opens a boundary at all. // A reloadable component is an update boundary wherever it renders, so a // delta can compare the region a redraw can replace; every other component // call stays out of the manifest, which is what keeps a manifest the size // of the regions that update rather than the size of the document. Instance func(P) string }
Boundary marks a component as an automatic partial update boundary. Chain members carry one; an ordinary component call does not become an instance, so a manifest stays the size of the layout chain rather than the size of the document.
A boundary is declared by generated code and never by hand, because its identity must change with the generated component version.
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.
The attribute instructions all precompute their ` name="` prefix here, where the plan is built once, so a render concatenates nothing.
func (Builder[P]) AttrCtx ¶ added in v0.2.10
AttrCtx is Attr for a value that needs the render context.
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]) BoolAttrCtx ¶ added in v0.2.10
BoolAttrCtx is BoolAttr for a value that needs the render context.
func (Builder[P]) BoundaryAttr ¶ added in v0.3.0
BoundaryAttr writes the instance attribute of the boundary this component opened. It sits in the attribute position of the component's single root element, which is the reason an update boundary must have exactly one.
The instruction writes nothing during an ordinary render, and nothing when the component was rendered as an ordinary call rather than as a chain member, so a nested component can never claim its parent's instance ID.
func (Builder[P]) CSRFField ¶ added in v0.3.3
CSRFField writes the hidden input carrying the session's CSRF token.
Generation emits it as the first child of every unsafe form, so a later field cannot displace it and an author writes nothing. A GET form never gets one: its fields become the query string, and a token in a URL reaches history, logs, and referrers.
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]) ComponentCtx ¶ added in v0.2.10
ComponentCtx is Component for a binding that needs the render context.
func (Builder[P]) IfCtx ¶ added in v0.2.10
IfCtx is If for a condition that needs the render context.
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]) RawCtx ¶ added in v0.2.10
RawCtx is Raw for a value that needs the render context.
func (Builder[P]) Require ¶ added in v0.5.9
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.
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.
func (Builder[P]) SlotCtx ¶ added in v0.2.10
SlotCtx is Slot for a value that needs the render context. It is also what an html-returning external lowers to when its implementation takes one, so a framework fragment such as a hidden CSRF field renders as a subtree under the ordinary context checks rather than as escaped text.
func (Builder[P]) Static ¶ added in v0.1.15
Static writes literal markup. Adjacent literal output is coalesced at generation time, so one instruction covers a whole run.
func (Builder[P]) Text ¶ added in v0.1.15
Text writes a value into child-node or attribute position with HTML escaping.
func (Builder[P]) TextCtx ¶ added in v0.2.10
TextCtx is Text for a value that needs the render context.
func (Builder[P]) URLAttr ¶ added in v0.4.0
URLAttr writes an attribute a browser resolves as a URL, applying this render's scheme policy before escaping the value.
It exists rather than a wider Escape because the policy is a render option and an Attr value function receives only the parameters, never the renderer. Exec does receive it, so the check lives here — the same reason AttrCtx reaches the boundary context from Exec instead of from its closure.
value returns the assembled attribute text unescaped, because the scheme has to be read before the ampersands and quotes are encoded.
func (Builder[P]) URLAttrCtx ¶ added in v0.4.0
URLAttrCtx is URLAttr for a value that needs the render context.
func (Builder[P]) URLListAttr ¶ added in v0.4.0
URLListAttr writes an attribute holding several URLs, applying the scheme policy to each one and keeping the rest when one is refused.
srcset names the comma-separated form whose entries carry a descriptor, and ping the whitespace-separated form. Dropping only the refused entry matters because these are lists of alternatives: refusing the whole attribute would turn one hostile candidate into a missing image.
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 // Scoped marks a component declared private, whose key is prefixed with the // render's scope value so one key yields a separate entry per reader. // // It is the default for a declared cache: a component that is actually // shared says so with scope: "public", because the cost of getting that // wrong is a miss and the cost of the other mistake is one reader's output // served to another. Scoped bool }
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 Collector ¶ added in v0.4.3
type Collector interface {
// Begin starts one render, carrying the validator tag the render options
// resolved. It is called once, before anything else.
//
// It used to carry the placeholder element name too, so a decomposing
// observer could write the same shape a progressive render writes. The two
// shapes are no longer the same: a hole has no content and is written as a
// template, while an await boundary brackets a visible fallback and is
// written as a comment pair. Neither is nameable by a prefix, so nothing is
// left to pass.
Begin(validatorTag string)
// Write observes one instruction's output, after escaping.
Write(value string)
// Open enters the boundary of one chain member: its instance ID, the
// component's declaration identity, the instance attribute its root
// element will carry, the canonical encoding of its declared inputs, and
// the address of the static half its values are walked against.
Open(id, componentID, attr, input, sequence string)
// Close leaves the innermost open boundary.
Close()
// TakePending consumes the boundary whose root element has not yet written
// its instance attribute, returning that attribute and the instance ID.
// Only the boundary's own root consumes it, so an ordinary component
// nested inside cannot claim its parent's ID.
TakePending() (attr, id string, ok bool)
// Slot brackets one instruction's output, so what it wrote can be separated
// from the static text around it. begin opens and end closes.
Slot(begin bool)
// Choice records what the value stream needs in order to walk the sequence
// tree: which branch a conditional took, how many times a loop ran, and
// whether a called component opened a boundary or rendered inline.
Choice(value string)
}
Collector observes one chain render: every byte an instruction writes, and each chain member's boundary opening and closing around its own output. It is the seam the update machinery hangs from — validators and captured subtrees are derived by the implementation, so a render that collects nothing links none of the hashing that derivation needs.
A collector is driven by the one goroutine walking the plan, so an implementation needs no locking of its own.
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) AppendJSON ¶ added in v0.2.7
AppendJSON appends this delivery as a JSON object with an id and an html field, and returns the extended slice.
It exists because past the initial document there is no parser to feed. The template-and-marker framing requirement:suspense-html-streaming defines is for bytes the HTML parser is consuming; a client reading a fetch stream is not parsing markup, so a record is the natural form and JSON is the ordinary record. A caller streaming completions into a live document wants this; a caller writing into the document response still writes markup.
The fragment is escaped for a script context as well as a JSON one, using the same rules as the generated encoders, so the result stays safe to embed in an inline script element as well as to send as a body. Framing around the record — newline-delimited, an event stream, a length prefix — is still the caller's to choose, because it has to match the client that reads it.
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 Bind
deprecated
added in
v0.1.15
Bind pairs a plan with parameters, producing the value a slot accepts.
Deprecated: use the Bind method on Plan. It carries no type parameter beyond the receiver's own, so the method form was always available; this function remains so no generated or hand-written caller is forced to move.
func (Fragment) Assets ¶ added in v0.3.3
Assets returns every static file this fragment requires, its own and those of every component it can reach, including one supplied through a slot.
It is readable before rendering starts, because it is bound to the value rather than produced by walking one. That timing is the point: a live delivery or a fragment swap can insert a component whose script was not in the first render, and a set known up front lets the document carry every asset a later swap might need. Nothing is then fetched mid-swap, and no client-side loading design has to enter the module.
A member below a slot that never renders still counts. The set is what this value could require, not what one render happened to reach — the same direction HasAwaitBlock already takes, and the conservative one for a caller deciding what to put in a document shell.
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 slot parameter is counted, because generation emits the accessor that reaches it. A fragment a caller holds and has not yet bound in is its own to union, which HasAwaitBlock over a chain does for the ordinary document, layout, and page shape.
func (Fragment) HasLiveBlock ¶ added in v0.2.7
HasLiveBlock reports whether rendering this fragment can open a live boundary: a region the server keeps re-rendering after the document has finished. It follows the same rules as HasAwaitBlock, including how a fragment arriving through a slot parameter is counted.
A caller reads it to decide whether this screen is worth a live request at all. A document whose render owns no live boundary will never produce another delivery, so asking for one costs a page execution and returns nothing.
func (Fragment) Head ¶ added in v0.1.15
Head returns the fragment's head contributions, one entry per tag: its own, plus those of every component supplied to it through a slot parameter.
The slot half matters because a component library's whole shape is a component handed in through a slot. Reporting only the outer component's contributions would drop a library's stylesheet, and drop it before a caller that refuses an undeliverable contribution could ever see it.
func (Fragment) HeadSources ¶ added in v0.2.4
HeadSources names the component that declared each Head entry, in the same order and with the same length. Head and HeadSources are two views of one list, so index i of either describes the same contributed tag.
A caller that cannot deliver a head contribution uses it to report which component to change. The merged head returned by MergeHead has no matching source list, because deduplication drops entries: ask a member for its own.
func (Fragment) InstanceID ¶ added in v0.4.4
InstanceID returns the update-boundary instance this fragment renders as, and empty when it renders as no addressable boundary.
It is set for a component that names its own instance — a reloadable one, whose id an author writes at the call site. A chain member is numbered by its position instead, and reports nothing here because that number is decided by the chain rather than by the fragment.
A redraw reads it to check that the component it just bound is addressable at the id the request asked for, which generated code guarantees and a hand-assembled registration can get wrong.
func (Fragment) IsPrivate ¶ added in v0.4.11
IsPrivate reports whether this fragment's output belongs to one reader, so a caller knows before rendering whether the response may be stored by a shared cache. Reading it renders nothing.
An undeclared fragment reports true. That is the safe direction and it is the framework default rather than a property of the annotation: a component treated as shared that is actually per-reader serves one reader's output to another, while a component treated as per-reader that is actually shared costs a cache miss. Those are not comparable, so forgetting is slow rather than wrong.
func (Fragment) Present ¶ added in v0.1.15
Present reports whether the fragment carries content. An absent optional slot renders its default instead.
func (Fragment) PrivateSource ¶ added in v0.4.11
PrivateSource names the component whose declaration made this fragment private, and is empty when nothing declared it — which includes the ordinary case of a fragment that is private only because nothing said otherwise.
It exists for the same reason HeadSources does: an author who expected shared output and got per-reader output needs to know which component to change, and the answer alone does not say.
func (Fragment) Validate ¶ added in v0.1.19
Validate runs the fragment's parameter check without rendering. Chain assembly calls it so a chain built from unrenderable parameters fails before any member writes.
func (Fragment) Vary ¶ added in v0.3.3
Vary reports the request properties this fragment's output depends on, such as a cookie or a header a builtin element's provider reads.
It exists because nothing else says so. A component reading a cookie through a registered element makes the whole response vary on that cookie, and the template says nothing a caller could read: a caller cannot build a Vary header for a dependency it cannot see, and an output cache cannot refuse to store what it cannot key.
The axes are declared by whoever registered the element, not derived, because only the implementation knows what its provider reads.
type HeadNode ¶ added in v0.2.10
type HeadNode struct {
// contains filtered or unexported fields
}
HeadNode is one head contribution supplied at a render call.
Build one with HeadTitle, HeadMeta, HeadLink, HeadScript, or HeadNoScript. The zero value contributes nothing.
func HeadNoScript ¶ added in v0.2.10
HeadNoScript contributes a noscript element wrapping meta, link, or style children. It is what a page tells a browser with scripting disabled, and the only contributed element with element children.
func HeadScript ¶ added in v0.2.10
HeadScript contributes a script element referencing an external file. It requires a src attribute: an asset is a reference to something served, so no path through this package ever writes inline script, and a policy may keep script-src to self with no nonce.
type LiveBinding ¶ added in v0.2.7
LiveBinding pumps one binding of a live clause. It ranges its own source and calls deliver once per value, passing a function that writes that value into the boundary scope. A non-nil err is a failure delivery rather than the end of the source, mirroring the (value, error) pair the source itself yields.
deliver reports false when the boundary is gone, which is the signal to stop ranging. Returning ends this binding; the boundary lives until every binding has returned.
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, and then a failure becomes an UnrecoveredError for the caller instead of anything on the page.
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 ForCtx ¶ added in v0.2.10
func ForCtx[P, E, S any](items func(context.Context, P) []E, scope func(P, E, int) S, body []Op[S]) Op[P]
ForCtx is For for an item list that needs the render context.
func Live ¶ added in v0.2.7
func Live[P, S, R any]( bindings func(context.Context, P) []LiveBinding[S], scope func(P) S, recovery func(P, AsyncError) R, primary []Op[S], fallback []Op[P], handler []Op[R], ) Op[P]
Live opens a live boundary. bindings subscribes each of the clause's sources, scope builds the boundary scope the primary subtree reads, and 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.
Where Await settles once, this renders the primary subtree again for every delivery, for as long as the subscription lives. The sequence ending is the only terminal signal: a yielded error is a delivery of a failure, so a transient fault shows the recover subtree and the next value replaces it.
A clause with several bindings holds the latest value of each and re-renders whenever any of them moves. Nothing has to say which source fired, because the subtree reads them all: putting every current value on every render is what removes the need for a selector. The first render waits until every binding has produced a value, since the subtree would otherwise read a zero one.
Delivery is pull-based on purpose. A source blocks in its own yield until the boundary is ready for its next value, so a source producing faster than the screen can use it simply misses ticks. That is the coalescing rule with no queue to size and nothing to discard, and it is why a source is a sequence rather than a channel.
On the document entries the boundary takes its first delivery and unsubscribes, so the first paint shows real content rather than a loading state and the response still finishes. Only the live entries keep the subscription open.
func Provide ¶ added in v0.3.3
func Provide[P, V any](element, provider string, fn func(context.Context) (V, error), segments []Segment[P, V]) Op[P]
Provide runs a per-request provider and writes one builtin element's markup, filling each hole from the result.
The provider is called at most once per render, whatever the element's occurrence count, and the result is shared by every occurrence. That is a contract rather than an optimization: a token that reaches the browser twice — once in a response header for script to read, once in a hidden input for a form that must work without script — has to be the same token, and a header carries one value. Two forms on a page holding two different tokens is a bug nobody sees until one of them is submitted.
The memo is keyed by the provider, so two elements backed by one function share one value; and it is scoped to one render, because that is the unit whose output has to agree with itself. A redraw and a live delivery are separate renders and call again, which is correct: each is a separate response with its own header.
What this asks of a provider is that it be a read rather than a mint. See requirement:render-value-provider: a provider returns the value the session already has, so calling it once, twice, or never yields the same answer.
An error from the provider ends the render. During the initial pass that is still before the response commits, so a caller can turn it into an error status rather than a half-written document; from a settled await boundary or a delta rerender it travels as any other member failure does.
func Require
deprecated
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.
Deprecated: use the Require method on Builder. It carries no type parameter beyond the receiver's own, so the method form was always available; this function remains so no generated or hand-written caller is forced to move.
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 WithBoundaryPrefix ¶ added in v0.3.1
WithBoundaryPrefix renames the placeholder element and the boundary identifiers, so a framework's markup carries the framework's own prefix.
It must be the prefix the generator wrote the instance attributes with. A document naming its attributes data-pw-id and its placeholders <tb-boundary> has two naming systems in it, only one of which anything can configure.
The value must be a valid custom element name prefix: lowercase letters and digits, starting with a letter, with no leading or trailing hyphen. Anything else produces markup a browser will not parse as an element.
func WithCSRFToken ¶ added in v0.3.3
WithCSRFToken supplies the session's CSRF token for this render.
It is an option rather than something read from the context because this package cannot read it from a context: the key belongs to whoever owns the session, and there is nothing here to look up. A framework passes it once inside its own render entry, from its own accessor, so no handler changes.
The same value reaches the browser twice — in the hidden field of every unsafe form, and in the request header the runtime sends — because a form must submit with scripting disabled and cannot set a header, while a fetch may not be carrying a form at all. One value per session is what keeps the two agreeing.
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 WithCacheScope ¶ added in v0.4.11
WithCacheScope supplies the value prefixed to the key of every component declared private, so the same parameters yield a separate entry per scope.
The value is opaque: pass whatever identifies the reader a private entry belongs to, and this package never interprets it. It is framed into the key like any other value, so a scope value cannot spell out another key.
Without it a private component stores nothing. That is deliberate: an entry written under an empty scope is a shared entry wearing a private label, and a miss is preferable to serving one reader's output to the next.
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 WithDataURLMediaTypes ¶ added in v0.4.0
WithDataURLMediaTypes replaces the media types an inline data URL may carry.
The data scheme is handled apart from WithURLSchemes because permitting it wholesale would permit text/html, which is a document rather than an asset. Passing no media types refuses every data URL.
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 therefore never surfaced to the caller. Recover subtrees see only the safe AsyncError, so this is where logging and metrics attach.
It is called from each boundary's own goroutine, so a reporter that accumulates rather than logs has to guard its own state.
func WithHead ¶ added in v0.2.10
WithHead adds caller-supplied contributions to this render's merged head.
They merge after every component contribution, as the innermost contributor, and through the same deduplication: a tag a component already declared is not written twice. Supplying none produces the head the render produced before this option existed.
A malformed node fails the render before the first byte, so the response can still carry an error status.
htmlbind.RenderChain(w, chain, page,
htmlbind.WithHead(
htmlbind.HeadTitle(order.Customer),
htmlbind.HeadNoScript(htmlbind.HeadMeta(
htmlbind.HeadAttr{Name: "http-equiv", Value: "refresh"},
htmlbind.HeadAttr{Name: "content", Value: "0; url=/_handoff"},
)),
),
)
This is a channel for the caller, not a way into the byte stream. Nothing here reaches template scope, and a component cannot read what a render contributed.
func WithLiveSubscriptions ¶ added in v0.2.7
func WithLiveSubscriptions() Option
WithLiveSubscriptions keeps live boundaries subscribed instead of taking one delivery and unsubscribing. RenderChainLive sets it; it is exported so a caller assembling its own options can be explicit about which behaviour it is asking for.
func WithURLSchemes ¶ added in v0.4.0
WithURLSchemes replaces the schemes a URL-bearing attribute may carry.
Names are matched case-insensitively against the scheme the browser will read, and a value with no scheme at all — a relative path, a scheme-relative URL, or a bare fragment — is always permitted, because it cannot leave the origin the document already has.
Passing no schemes permits none, which leaves relative URLs as the only form that renders.
func WithValidatorTag ¶ added in v0.3.5
WithValidatorTag seeds every validator this render produces, so two renders that must never be compared cannot produce equal digests.
The transport half passes its build identity. That is the axis that actually moves: it covers a changed template, a changed Go function a template calls, and a changed browser client, none of which a component's own identity sees.
It replaced a protocol version this module owned. Once the browser client belongs to the caller, a version constant here versions a contract this module only half implements, and the caller is the party that can say what a mismatch means. Leaving it empty is allowed and keeps the digests keyed by Key alone, which is what a caller comparing renders within one build wants.
func WithoutCSRFToken ¶ added in v0.3.3
func WithoutCSRFToken() Option
WithoutCSRFToken says this render has no session behind it, so an unsafe form may render with an empty token instead of failing.
It exists for the renders that are not responses: a mail body, a static export, a golden test. It is explicit because the alternative — treating an absent token as "none wanted" — turns a forgotten option into a form that submits and is rejected, with nothing pointing at the cause.
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, one entry per contributed tag. They merge into the shell head // before any body byte. Head []string // Boundary describes this component as a partial update boundary. It is // set only for a component that can be a chain member and renders exactly // one root element; a boundary only becomes an instance when the component // is actually rendered as a chain member. Boundary *Boundary[P] // HeadSources names the component that declared each Head entry, in the same // order and with the same length. It exists so a caller that has to reject a // contribution can say which component to change, instead of printing head // markup a reader then has to grep for. // // It is generated data, so reading it costs nothing and needs no reflection. // It is nil for a component with no contribution, which is most of them. HeadSources []string // Assets names the static files this component and everything it calls // require, deduplicated by identity. Generation computes it over the same // call graph as Head, from the same declarations, and leaves it nil for a // component requiring none. // // It is what Head cannot be: a head entry is markup, and a caller needs an // identity it can compare, refuse, or preload. See Fragment.Assets. Assets []Asset // Vary names the request properties this component's output depends on, // declared by whoever registered the builtin elements it reaches. It is nil // for a component depending on none, which is most of them. See // Fragment.Vary. Vary []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 // HasLiveBlock reports whether this component, or any component it calls, // owns a live boundary. It is computed over the call graph exactly as // HasAwaitBlock is. // // A live boundary is also an await boundary as far as the client runtime is // concerned, so HasAwaitBlock is true wherever this is. The separate flag // exists because a caller decides two different things: whether a response // needs the runtime that applies boundaries, and whether this screen has // anything that will keep updating after the document finishes. HasLiveBlock bool // DeclaresPrivate reports whether this component, or any component it calls, // declared its output per-reader. Generation computes it over the call graph // as HasAwaitBlock is, because a private component's bytes end up inside // whatever renders it. // // It exists so a caller can put a cache policy on the wire before the first // body byte. A private component four levels down renders long after the // header is committed, so anything computed during a render would be // available only on the buffered branch — and a response's cache policy would // then depend on whether streaming was on. DeclaresPrivate bool // DeclaresPublic reports whether this component declared its output shared. // // Unlike DeclaresPrivate it does not fold over the call graph, because the // declaration is an assertion about this component and what it renders rather // than a property it inherits from a caller. Generation refuses the assertion // when the call graph beneath it reaches a declared private component, so a // plan carrying this bit has already been checked. DeclaresPublic bool // PrivateSource names the component whose declaration set DeclaresPrivate, so // a caller that has to explain a private response can say which component to // change. It is empty when nothing declared it. // // It is the courtesy HeadSources provides for a head contribution, for the // same reason: the answer is useless without the position. PrivateSource string // Slots returns the fragments this component's parameters carry, in // declaration order. Generation emits it for a component with an html // parameter and leaves it nil for every other, which is most of them. // // It exists because a slot argument is a whole component the binder cannot // otherwise see: Bind copies this plan's own head, and a fragment arriving // inside a caller's parameter struct is not reachable without reflection. A // component library's whole shape is a component supplied through a slot, so // without this accessor its styles are dropped — and dropped before the guard // that exists to refuse an undeliverable contribution ever hears about them, // which makes the guard silent for exactly the case it was built for. // // An absent optional slot yields an absent Fragment, which contributes // nothing. Slots func(P) []Fragment // 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] // contains filtered or unexported fields }
Plan is a component compiled to instructions. A plan is built once at package initialization and shared by every render.
func (*Plan[P]) Bind ¶ added in v0.5.9
Bind pairs a plan with parameters, producing the value a slot accepts.
func (*Plan[P]) BindWrapper ¶ added in v0.5.9
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.
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.
func (*Renderer) Write ¶ added in v0.1.15
Write emits raw bytes. Instructions call it after applying their own context-appropriate escaping.
func (*Renderer) WriteEscaped ¶ added in v0.4.1
WriteEscaped emits value under Escape's rules, writing clean runs and entities separately so a value that needs escaping never builds an intermediate string.
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 Segment ¶ added in v0.3.3
type Segment[P, V any] struct { // Static is written as-is. It is the markup the definition declared, which // is generation-time input and never a request's. Static string // Hole produces one value. Nil means this segment is Static. Hole func(P, V) string }
Segment is one piece of a builtin element's lowered markup: either fixed bytes or a hole filled from the component's parameters and the provider's result.
A hole is escaped, always. Both of the positions a hole may occupy — element text and an attribute value — take the same escaping, so a provider returning a value with a quote in it cannot close the attribute it sits in. Generation refuses a hole in any other position rather than widening this rule.
type SeqKind ¶ added in v0.4.4
type SeqKind int
SeqKind names what a sequence node contributes.
const ( // SeqStatic is literal output, identical in every render. SeqStatic SeqKind = iota // SeqSlot is one instruction's output, which travels as a value. SeqSlot // SeqIf is a conditional. The value stream says which branch ran. SeqIf // SeqRepeat is a loop body. The value stream says how many times it ran. SeqRepeat // SeqComponent is a called component, which either opened an update // boundary — leaving a placeholder whose only varying part is the id — or // rendered inline, which is opaque because the callee's plan is chosen by a // closure this walk cannot evaluate. The value stream says which. SeqComponent )
type SeqNode ¶ added in v0.4.4
type SeqNode struct {
Kind SeqKind
// Text is the literal output of a static node.
Text string
// Then and Else are the branches of a conditional; Then alone is a loop
// body, and the placeholder frame of a component node.
Then []SeqNode
Else []SeqNode
}
SeqNode is one node of a sequence tree.
type Sequence ¶ added in v0.4.4
type Sequence struct {
// Address identifies this sequence. It is a digest of the tree, computed
// once per plan rather than per request.
Address string
Nodes []SeqNode
}
Sequence is one component's static half, with the address a client caches it under.
func LookupSequence ¶ added in v0.4.4
LookupSequence returns the sequence an address names, for a caller answering a client that holds the address and not the tree.
func (*Sequence) AppendJSON ¶ added in v0.4.4
AppendJSON writes a sequence as the tree a client walks.
func (*Sequence) Reassemble ¶ added in v0.4.4
Reassemble rebuilds a fragment's markup from its sequence and the values one render produced.
It is the reference for what a client does: walk the tree, take the static text as it stands, and consume one value at each hole, each conditional, and each loop. Nothing is escaped here, because the values were escaped by the render that produced them — which is the whole reason a client needs no escaping rules of its own.
It exists on the server so the round trip is testable: a sequence plus its values must reproduce the bytes the render wrote, or the split is not a split.
type Signal ¶ added in v0.5.3
type Signal struct {
// contains filtered or unexported fields
}
Signal is the module half of an application signal: a named instruction the server sends to client code, travelling beside the deliveries a live boundary renders rather than replacing any of them.
An application declares its own type and embeds this one:
type Toast struct{ htmlbind.Signal }
func NewToast(text string) Toast {
return Toast{htmlbind.NewSignal("app.toast", toastPayload{Text: text})}
}
Embedding promotes Error, so the application type satisfies error with no boilerplate, which is what lets a live source yield one in the error position of its sequence without any signature growing a third value. It also promotes an unexported accessor, and that accessor is how the runtime recognizes a signal: an unexported method name is qualified by the package that declared it, so nothing outside this package can claim to be a signal without embedding this struct.
A bare Signal is usable on its own; the embed exists for an application that wants a named Go type per signal, so its emit sites are type-checked.
The runtime never reads an application's own fields. A signal type may hold whatever it likes beside the embed.
func AsSignal ¶ added in v0.5.3
AsSignal reports whether err is a signal, and returns it.
It walks the wrap chain the way findPublicError does, for the same reason: errors.As reflects on its target, and nothing here may link reflect. A joined error reports the first signal in its list, so errors.Join of two signals is read by calling this once per forwarded value rather than once per join.
func NamedSignal ¶ added in v0.5.3
NamedSignal builds a signal with no payload.
func NewRawSignal ¶ added in v0.5.3
NewRawSignal builds a signal from a payload that is already encoded JSON.
Nothing here validates those bytes. A caller passing something that is not one JSON value produces a record a client cannot parse, which is why the encoding path above is the ordinary one.
func NewSignal ¶ added in v0.5.3
func NewSignal(name string, payload SignalPayload) Signal
NewSignal builds a signal carrying an encoded payload.
The payload is encoded here, at the call site, so the value is immutable once yielded and the runtime seam holds bytes rather than something it would have to reflect on to write. A nil payload is legal and means an instruction with no arguments.
func (Signal) AppendJSON ¶ added in v0.5.3
AppendJSON appends the signal as a JSON object with a name and an optional data field, and returns the extended slice.
The name is escaped for a script context as well as a JSON one, using the same rules as every other record this package writes, so the result stays safe to embed in an inline data block as well as to send as a body. The payload is appended as the encoder produced it. Framing around the record is the caller's, as it is for a completion.
func (Signal) Error ¶ added in v0.5.3
Error reports the signal by name. It deliberately omits the payload: an unclassified signal reaches a log or an error page, and error values are printed by code that has no idea what this one is.
func (Signal) Is ¶ added in v0.5.3
Is reports ErrSignal, so errors.Is classifies a signal without this package exporting the accessor that identifies one.
type SignalPayload ¶ added in v0.5.3
SignalPayload is a value that can append itself as one JSON value.
It is the interface a generated encoder already satisfies, and taking it rather than a type parameter is what keeps this package free of the codec registry: the payload encodes itself, so nothing here has to find a codec for a type it only holds as an interface.
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 UnrecoveredError ¶ added in v0.1.21
type UnrecoveredError struct {
// BoundaryID is the placeholder whose fallback is committed to the response.
// It is empty on the synchronous path, which writes no placeholder.
BoundaryID string
// Err is the failure the bindings reported.
Err error
}
UnrecoveredError reports an await boundary whose bindings failed in a clause that declared no recover subtree. The template said nothing about what to show, so the failure leaves the boundary instead of stopping there: the synchronous entries return it, and the streaming sequence yields it and ends.
It carries the original Go error rather than the safe AsyncError projection, because it reaches the caller's Go code and never a template. What a caller puts on the page in response is its own to write, and must not be this text.
func (*UnrecoveredError) Error ¶ added in v0.1.21
func (e *UnrecoveredError) Error() string
func (*UnrecoveredError) Unwrap ¶ added in v0.1.21
func (e *UnrecoveredError) Unwrap() error
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
deprecated
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.
Deprecated: use the BindWrapper method on Plan. It carries no type parameter beyond the receiver's own, so the method form was always available; this function remains so no generated or hand-written caller is forced to move.
func (Wrapper) Assets ¶ added in v0.3.3
Assets returns every static file this wrapper requires. It is the Wrapper form of the accessor documented on Fragment.
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.
func (Wrapper) HasLiveBlock ¶ added in v0.2.7
HasLiveBlock reports whether rendering this wrapper can open a live boundary. It is the Wrapper form of the accessor documented on Fragment.
func (Wrapper) Head ¶ added in v0.1.18
Head returns the wrapper's own head contributions, one entry per tag.
func (Wrapper) HeadSources ¶ added in v0.2.4
HeadSources names the component that declared each Head entry, in the same order and with the same length. It is the Wrapper form of the accessor documented on Fragment.
func (Wrapper) IsPrivate ¶ added in v0.4.11
IsPrivate is the Wrapper form of the accessor documented on Fragment. The child it wraps is counted separately, because a wrapper is bound before it is told what it wraps.
func (Wrapper) PrivateSource ¶ added in v0.4.11
PrivateSource is the Wrapper form of the accessor documented on Fragment.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package delta compares two renders of one chain and expresses the difference as operations a browser applies, so a screen already showing the document reaches the server's fresh render without a full page load.
|
Package delta compares two renders of one chain and expresses the difference as operations a browser applies, so a screen already showing the document reaches the server's fresh render without a full page load. |