pongo2 stays host-side and is never compiled into a guest; plugins render
templates by handing the host a {template, data} pair, and the host calls
back into the free guest instance for plugin-declared tags/filters.
ABI (additive, buf-breaking clean):
- RenderBlockResponse gains a `powered` PoweredBlock{template, data_json};
a block returns EITHER html OR powered.
- New hooks HOOK_RENDER_TAG (10) / HOOK_APPLY_FILTER (11) with
RenderTag{Request,Response} and ApplyFilter{Request,Response}.
- PluginManifest gains repeated declared_tags / declared_filters (31/32).
Guest SDK (core/blocks, core/plugin/wasmguest):
- blocks.PoweredBlock(template, data) / DecodePoweredBlock: NUL-sentinel
marker so BlockFunc's string signature is unchanged (smallest additive
change — no ripple to existing blocks or the host guest-side).
- blocks.RegisterTag / RegisterFilter (+ RenderContext = context.Context)
write a package-level registry; runRegister resets it per registration
for deterministic DESCRIBE + dispatch.
- DESCRIBE emits declared_tags/filters; dispatch handles RENDER_TAG /
APPLY_FILTER (fn errors → response.error; panics → AbiError INTERNAL,
instance stays callable).
Docs: core/docs/wasm-abi.md gains the render-as-a-host-capability model,
the powered-block flow (re-entrancy-free), the plugin API, and the
HOST-SIDE CONTRACT the cms phase implements.
Tests: unit round-trips for powered/RENDER_TAG/APPLY_FILTER (dispatch +
error + unknown + panic + per-guest registry isolation) plus a real
wazero round-trip through the compiled fixture module. Verified no
guest-reachable package imports pongo2 (go list -deps on the wasip1
fixture build is clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
63 lines
2.6 KiB
Go
63 lines
2.6 KiB
Go
package blocks
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
)
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// 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 {
|
|
payload, err := json.Marshal(PoweredResult{Template: template, Data: data})
|
|
if err != nil {
|
|
// A non-serializable data map is a programming error; fall back to an
|
|
// empty-data powered result so the template still renders.
|
|
payload, _ = json.Marshal(PoweredResult{Template: template})
|
|
}
|
|
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
|
|
}
|