pluginsdk/blocks/powered.go

91 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package blocks
import (
"encoding/json"
"strings"
"git.dev.alexdunmow.com/block/pluginsdk/document"
)
// poweredBlockSentinel prefixes a PoweredBlock marker string. It uses NUL
// bytes so it can never collide with real block HTML: a BlockFunc returns a
// plain HTML string today, and this marker is a distinct, out-of-band signal
// that the block is "powered" (template + data, rendered host-side).
const poweredBlockSentinel = "\x00bn:powered\x00"
// PoweredResult is the decoded payload of a PoweredBlock marker: the template
// source and its data map. The wasm guest's RENDER_BLOCK handler decodes it
// and forwards it as abiv1.PoweredBlock so the HOST renders the template with
// pongo2 — after the block-invoke has returned, keeping the guest free.
type PoweredResult struct {
Template string `json:"template"`
Data map[string]any `json:"data"`
StatusCode int32 `json:"statusCode,omitempty"`
Metadata *document.Metadata `json:"metadata,omitempty"`
}
// PoweredOptions controls host-owned response data for a powered block.
// StatusCode is transported as declared; the host accepts only 200599.
type PoweredOptions struct {
StatusCode int32
Metadata *document.Metadata
}
// PoweredBlock marks a block's return value as "powered": instead of final
// HTML, the block hands back a template string plus a data map, and the host
// renders it (pongo2/ninjatpl) host-side. Return its result directly from a
// BlockFunc:
//
// func MyBlock(ctx context.Context, content map[string]any) string {
// posts := loadPosts(ctx) // build data via capabilities
// return blocks.PoweredBlock(tmpl, map[string]any{"posts": posts})
// }
//
// This is the guest-safe replacement for calling blocks.RenderTemplate inside
// a block: pongo2 never crosses the wasm boundary, so the guest cannot render
// itself — it defers rendering to the host. Because the host renders only
// after RENDER_BLOCK returns, any plugin-declared tag/filter the template
// hits ({% mytag %} / |myfilter) is a fresh RENDER_TAG / APPLY_FILTER invoke,
// never a re-entrant one.
func PoweredBlock(template string, data map[string]any) string {
return poweredBlockMarker(PoweredResult{Template: template, Data: data})
}
// PoweredBlockWithOptions is PoweredBlock with structured document metadata
// and an optional HTTP response status for the host to accumulate before it
// writes response headers.
func PoweredBlockWithOptions(template string, data map[string]any, options PoweredOptions) string {
return poweredBlockMarker(PoweredResult{
Template: template,
Data: data,
StatusCode: options.StatusCode,
Metadata: options.Metadata,
})
}
func poweredBlockMarker(result PoweredResult) string {
payload, err := json.Marshal(result)
if err != nil {
// A non-serializable data map is a programming error; fall back to an
// empty-data result while preserving response options.
result.Data = nil
payload, _ = json.Marshal(result)
}
return poweredBlockSentinel + string(payload)
}
// DecodePoweredBlock reports whether s is a PoweredBlock marker and, if so,
// returns the decoded template + data. The wasm guest uses it to distinguish a
// powered result from plain HTML. A non-marker (ordinary HTML) returns ok=false.
func DecodePoweredBlock(s string) (PoweredResult, bool) {
rest, ok := strings.CutPrefix(s, poweredBlockSentinel)
if !ok {
return PoweredResult{}, false
}
var pr PoweredResult
if err := json.Unmarshal([]byte(rest), &pr); err != nil {
return PoweredResult{}, false
}
return pr, true
}