91 lines
3.5 KiB
Go
91 lines
3.5 KiB
Go
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 200–599.
|
||
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
|
||
}
|