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"
|
||||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm"
|
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/bnwasm"
|
||||||
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/caps"
|
"git.dev.alexdunmow.com/block/pluginsdk/plugin/wasmguest/caps"
|
||||||
|
"git.dev.alexdunmow.com/block/pluginsdk/templates"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
@ -331,11 +332,44 @@ func (g *guest) renderTemplate(ctx context.Context, payload []byte) (proto.Messa
|
|||||||
if component == nil {
|
if component == nil {
|
||||||
return nil, internalError("template " + req.GetTemplateKey() + " returned nil component")
|
return nil, internalError("template " + req.GetTemplateKey() + " returned nil component")
|
||||||
}
|
}
|
||||||
var buf bytes.Buffer
|
tdoc := &abiv1.TemplateDocument{}
|
||||||
if err := component.Render(ctx, &buf); err != nil {
|
renderPart := func(c templates.HTMLComponent) ([]byte, error) {
|
||||||
return nil, internalError("render template " + req.GetTemplateKey() + ": " + err.Error())
|
if c == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
if err := c.Render(ctx, &buf); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buf.Bytes(), nil
|
||||||
}
|
}
|
||||||
return &abiv1.RenderTemplateResponse{Html: 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())
|
||||||
|
}
|
||||||
|
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) {
|
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 {
|
if err := proto.Unmarshal(resp.GetPayload(), rr); err != nil {
|
||||||
t.Fatalf("unmarshal: %v", err)
|
t.Fatalf("unmarshal: %v", err)
|
||||||
}
|
}
|
||||||
if string(rr.GetHtml()) != "<html>My Page</html>" {
|
if string(rr.GetDocument().GetBodyHtml()) != "<html>My Page</html>" {
|
||||||
t.Errorf("html = %q", rr.GetHtml())
|
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 {
|
if err := proto.Unmarshal(resp.GetPayload(), rt); err != nil {
|
||||||
t.Fatalf("unmarshal RenderTemplateResponse: %v", err)
|
t.Fatalf("unmarshal RenderTemplateResponse: %v", err)
|
||||||
}
|
}
|
||||||
if string(rt.GetHtml()) != "<!doctype html><title>Wazero</title>" {
|
if string(rt.GetDocument().GetBodyHtml()) != "<!doctype html><title>Wazero</title>" {
|
||||||
t.Errorf("template html = %q", rt.GetHtml())
|
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