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 }