feat(wasmguest): PageDocument envelope; renderTemplate returns TemplateDocument
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
a43ef7c242
commit
80328eddb1
@ -17,6 +17,7 @@ import (
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/caps"
|
||||
"git.dev.alexdunmow.com/block/pluginsdk/templates"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@ -331,11 +332,44 @@ func (g *guest) renderTemplate(ctx context.Context, payload []byte) (proto.Messa
|
||||
if component == nil {
|
||||
return nil, internalError("template " + req.GetTemplateKey() + " returned nil component")
|
||||
}
|
||||
tdoc := &abiv1.TemplateDocument{}
|
||||
renderPart := func(c templates.HTMLComponent) ([]byte, error) {
|
||||
if c == nil {
|
||||
return nil, nil
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := component.Render(ctx, &buf); err != nil {
|
||||
if err := c.Render(ctx, &buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
if pd, ok := component.(*templates.PageDocument); ok {
|
||||
body, err := renderPart(pd.Body)
|
||||
if err != nil {
|
||||
return nil, internalError("render template " + req.GetTemplateKey() + ": " + err.Error())
|
||||
}
|
||||
return &abiv1.RenderTemplateResponse{Html: buf.Bytes()}, nil
|
||||
head, err := renderPart(pd.HeadExtra)
|
||||
if err != nil {
|
||||
return nil, internalError("render template " + req.GetTemplateKey() + " head extra: " + err.Error())
|
||||
}
|
||||
end, err := renderPart(pd.BodyEndExtra)
|
||||
if err != nil {
|
||||
return nil, internalError("render template " + req.GetTemplateKey() + " body-end extra: " + err.Error())
|
||||
}
|
||||
tdoc.BodyHtml = body
|
||||
tdoc.BodyClass = pd.BodyClass
|
||||
tdoc.Lang = pd.Lang
|
||||
tdoc.HtmlAttrs = pd.HTMLAttrs
|
||||
tdoc.HeadExtraHtml = head
|
||||
tdoc.BodyEndExtraHtml = end
|
||||
} else {
|
||||
body, err := renderPart(component)
|
||||
if err != nil {
|
||||
return nil, internalError("render template " + req.GetTemplateKey() + ": " + err.Error())
|
||||
}
|
||||
tdoc.BodyHtml = body
|
||||
}
|
||||
return &abiv1.RenderTemplateResponse{Document: tdoc}, nil
|
||||
}
|
||||
|
||||
func (g *guest) handleHTTP(ctx context.Context, payload []byte) (proto.Message, *abiv1.AbiError) {
|
||||
|
||||
@ -334,8 +334,90 @@ func TestRenderTemplate(t *testing.T) {
|
||||
if err := proto.Unmarshal(resp.GetPayload(), rr); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if string(rr.GetHtml()) != "<html>My Page</html>" {
|
||||
t.Errorf("html = %q", rr.GetHtml())
|
||||
if string(rr.GetDocument().GetBodyHtml()) != "<html>My Page</html>" {
|
||||
t.Errorf("body_html = %q", rr.GetDocument().GetBodyHtml())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderTemplatePlainComponentBecomesBodyOnly(t *testing.T) {
|
||||
g := newGuest(plugin.PluginRegistration{
|
||||
Name: "tpltest",
|
||||
Register: func(tr templates.TemplateRegistry, _ blocks.BlockRegistry) error {
|
||||
return tr.Register("plain", func(_ context.Context, _ map[string]any) templates.HTMLComponent {
|
||||
return textComponent("<h1>hi</h1>")
|
||||
})
|
||||
},
|
||||
})
|
||||
resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_TEMPLATE, &abiv1.RenderTemplateRequest{
|
||||
TemplateKey: "plain",
|
||||
})
|
||||
if resp.GetError() != nil {
|
||||
t.Fatalf("render error: %v", resp.GetError())
|
||||
}
|
||||
rr := &abiv1.RenderTemplateResponse{}
|
||||
if err := proto.Unmarshal(resp.GetPayload(), rr); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
doc := rr.GetDocument()
|
||||
if doc == nil {
|
||||
t.Fatal("nil document")
|
||||
}
|
||||
if string(doc.GetBodyHtml()) != "<h1>hi</h1>" {
|
||||
t.Errorf("body_html = %q", doc.GetBodyHtml())
|
||||
}
|
||||
if doc.GetBodyClass() != "" || doc.GetLang() != "" || len(doc.GetHtmlAttrs()) != 0 ||
|
||||
len(doc.GetHeadExtraHtml()) != 0 || len(doc.GetBodyEndExtraHtml()) != 0 {
|
||||
t.Errorf("plain component should set only body_html, got %+v", doc)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderTemplatePageDocumentFillsEnvelope(t *testing.T) {
|
||||
g := newGuest(plugin.PluginRegistration{
|
||||
Name: "tpltest",
|
||||
Register: func(tr templates.TemplateRegistry, _ blocks.BlockRegistry) error {
|
||||
return tr.Register("page", func(_ context.Context, _ map[string]any) templates.HTMLComponent {
|
||||
return &templates.PageDocument{
|
||||
Body: textComponent("<main>body</main>"),
|
||||
BodyClass: "cls",
|
||||
Lang: "fr",
|
||||
HTMLAttrs: map[string]string{"data-theme": "x"},
|
||||
HeadExtra: textComponent(`<meta name="e">`),
|
||||
BodyEndExtra: textComponent("<script>1</script>"),
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
resp := invokeHook(t, g, abiv1.Hook_HOOK_RENDER_TEMPLATE, &abiv1.RenderTemplateRequest{
|
||||
TemplateKey: "page",
|
||||
})
|
||||
if resp.GetError() != nil {
|
||||
t.Fatalf("render error: %v", resp.GetError())
|
||||
}
|
||||
rr := &abiv1.RenderTemplateResponse{}
|
||||
if err := proto.Unmarshal(resp.GetPayload(), rr); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
doc := rr.GetDocument()
|
||||
if doc == nil {
|
||||
t.Fatal("nil document")
|
||||
}
|
||||
if string(doc.GetBodyHtml()) != "<main>body</main>" {
|
||||
t.Errorf("body_html = %q", doc.GetBodyHtml())
|
||||
}
|
||||
if doc.GetBodyClass() != "cls" {
|
||||
t.Errorf("body_class = %q", doc.GetBodyClass())
|
||||
}
|
||||
if doc.GetLang() != "fr" {
|
||||
t.Errorf("lang = %q", doc.GetLang())
|
||||
}
|
||||
if doc.GetHtmlAttrs()["data-theme"] != "x" {
|
||||
t.Errorf("html_attrs = %v", doc.GetHtmlAttrs())
|
||||
}
|
||||
if string(doc.GetHeadExtraHtml()) != `<meta name="e">` {
|
||||
t.Errorf("head_extra_html = %q", doc.GetHeadExtraHtml())
|
||||
}
|
||||
if string(doc.GetBodyEndExtraHtml()) != "<script>1</script>" {
|
||||
t.Errorf("body_end_extra_html = %q", doc.GetBodyEndExtraHtml())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -183,8 +183,8 @@ func TestWasmFixtureDescribeRoundTrip(t *testing.T) {
|
||||
if err := proto.Unmarshal(resp.GetPayload(), rt); err != nil {
|
||||
t.Fatalf("unmarshal RenderTemplateResponse: %v", err)
|
||||
}
|
||||
if string(rt.GetHtml()) != "<!doctype html><title>Wazero</title>" {
|
||||
t.Errorf("template html = %q", rt.GetHtml())
|
||||
if string(rt.GetDocument().GetBodyHtml()) != "<!doctype html><title>Wazero</title>" {
|
||||
t.Errorf("template body_html = %q", rt.GetDocument().GetBodyHtml())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
27
templates/pagedoc.go
Normal file
27
templates/pagedoc.go
Normal file
@ -0,0 +1,27 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
)
|
||||
|
||||
// PageDocument is a full-page template's contribution to the document. The
|
||||
// HOST owns the envelope (doctype/<html>/<head>/<body> and the bn chrome);
|
||||
// the guest supplies the body interior and envelope attributes. PageDocument
|
||||
// is itself an HTMLComponent (renders Body), so TemplateFunc's signature is
|
||||
// unchanged and non-page callers need no adaptation.
|
||||
type PageDocument struct {
|
||||
Body HTMLComponent
|
||||
BodyClass string
|
||||
Lang string // <html lang>; empty means host default "en"
|
||||
HTMLAttrs map[string]string // extra <html> attributes (e.g. data-theme)
|
||||
HeadExtra HTMLComponent // appended inside <head> after bn.Head
|
||||
BodyEndExtra HTMLComponent // appended before </body> after bn.BodyEnd
|
||||
}
|
||||
|
||||
func (p *PageDocument) Render(ctx context.Context, w io.Writer) error {
|
||||
if p.Body == nil {
|
||||
return nil
|
||||
}
|
||||
return p.Body.Render(ctx, w)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user