From ae286563fd788d38fc8a5cf5363cacebb3861b49 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 18:05:06 +0000 Subject: [PATCH] fix(html): normalise nil render context Co-Authored-By: Virgil --- context.go | 9 +++++++++ layout.go | 1 + layout_test.go | 12 ++++++++++++ node.go | 25 ++++++++++++++++--------- pipeline.go | 8 ++------ render.go | 4 +--- responsive.go | 1 + responsive_test.go | 14 ++++++++++++++ 8 files changed, 56 insertions(+), 18 deletions(-) diff --git a/context.go b/context.go index b8492f9..2807b40 100644 --- a/context.go +++ b/context.go @@ -33,6 +33,15 @@ func applyLocaleToService(svc Translator, locale string) { } } +// normaliseContext ensures render paths always have a usable context. +// A nil input is replaced with a fresh default context. +func normaliseContext(ctx *Context) *Context { + if ctx != nil { + return ctx + } + return NewContext() +} + // context.go: NewContext creates a new rendering context with sensible defaults. // Example: ctx := NewContext("en-GB"). // An optional locale may be provided as the first argument. diff --git a/layout.go b/layout.go index 1decd98..3041a7f 100644 --- a/layout.go +++ b/layout.go @@ -133,6 +133,7 @@ func (l *Layout) Render(ctx *Context) string { if l == nil { return "" } + ctx = normaliseContext(ctx) var b strings.Builder diff --git a/layout_test.go b/layout_test.go index 64b6b7d..b1d6926 100644 --- a/layout_test.go +++ b/layout_test.go @@ -226,3 +226,15 @@ func TestLayout_RenderNilReceiver(t *testing.T) { t.Fatalf("nil Layout should render empty string, got %q", got) } } + +func TestLayout_RenderNilContext(t *testing.T) { + layout := NewLayout("C").C(Raw("content")) + got := layout.Render(nil) + + if !strings.Contains(got, `data-block="C-0"`) { + t.Fatalf("Layout.Render(nil) should still render the block ID, got:\n%s", got) + } + if !strings.Contains(got, "content") { + t.Fatalf("Layout.Render(nil) should still render content, got:\n%s", got) + } +} diff --git a/node.go b/node.go index cff0a0a..99c2d10 100644 --- a/node.go +++ b/node.go @@ -34,7 +34,7 @@ func renderNode(n Node, ctx *Context) string { if n == nil { return "" } - return n.Render(ctx) + return n.Render(normaliseContext(ctx)) } // renderNodeWithPath renders a node while preserving layout path prefixes for @@ -43,6 +43,7 @@ func renderNodeWithPath(n Node, ctx *Context, path string) string { if n == nil { return "" } + ctx = normaliseContext(ctx) switch t := n.(type) { case *Layout: @@ -246,7 +247,7 @@ func (n *elNode) Render(ctx *Context) string { return "" } - return n.renderWithPath(ctx, "") + return n.renderWithPath(normaliseContext(ctx), "") } func (n *elNode) renderWithPath(ctx *Context, path string) string { @@ -312,9 +313,10 @@ func (n *textNode) Render(ctx *Context) string { if n == nil { return "" } + ctx = normaliseContext(ctx) var text string - if ctx != nil && ctx.service != nil { + if ctx.service != nil { text = ctx.service.T(n.key, n.args...) } else { text = i18n.T(n.key, n.args...) @@ -339,9 +341,10 @@ func (n *ifNode) Render(ctx *Context) string { if n == nil || n.cond == nil || n.node == nil { return "" } + ctx = normaliseContext(ctx) if n.cond(ctx) { - return n.node.Render(ctx) + return renderNodeWithPath(n.node, ctx, "") } return "" } @@ -363,9 +366,10 @@ func (n *unlessNode) Render(ctx *Context) string { if n == nil || n.cond == nil || n.node == nil { return "" } + ctx = normaliseContext(ctx) if !n.cond(ctx) { - return n.node.Render(ctx) + return renderNodeWithPath(n.node, ctx, "") } return "" } @@ -389,11 +393,12 @@ func (n *entitledNode) Render(ctx *Context) string { if n == nil || n.node == nil { return "" } + ctx = normaliseContext(ctx) - if ctx == nil || ctx.Entitlements == nil || !ctx.Entitlements(n.feature) { + if ctx.Entitlements == nil || !ctx.Entitlements(n.feature) { return "" } - return n.node.Render(ctx) + return renderNodeWithPath(n.node, ctx, "") } // --- switchNode --- @@ -413,10 +418,11 @@ func (n *switchNode) Render(ctx *Context) string { if n == nil || n.selector == nil || n.cases == nil { return "" } + ctx = normaliseContext(ctx) key := n.selector(ctx) if node, ok := n.cases[key]; ok { - return renderNode(node, ctx) + return renderNodeWithPath(node, ctx, "") } return "" } @@ -444,10 +450,11 @@ func (n *eachNode[T]) Render(ctx *Context) string { if n == nil || n.items == nil || n.fn == nil { return "" } + ctx = normaliseContext(ctx) var b strings.Builder for item := range n.items { - b.WriteString(renderNode(n.fn(item), ctx)) + b.WriteString(renderNodeWithPath(n.fn(item), ctx, "")) } return b.String() } diff --git a/pipeline.go b/pipeline.go index a675c03..2f71cd3 100644 --- a/pipeline.go +++ b/pipeline.go @@ -48,9 +48,7 @@ func StripTags(html string) string { // and returns a GrammarImprint — the full render-reverse pipeline. // Example: Imprint(NewLayout("C").C(Text("page.body")), NewContext()). func Imprint(node Node, ctx *Context) reversal.GrammarImprint { - if ctx == nil { - ctx = NewContext() - } + ctx = normaliseContext(ctx) rendered := Render(node, ctx) text := StripTags(rendered) tok := reversal.NewTokeniser() @@ -62,9 +60,7 @@ func Imprint(node Node, ctx *Context) reversal.GrammarImprint { // and returns pairwise similarity scores. Key format: "name1:name2". // Example: CompareVariants(NewResponsive().Variant("desktop", NewLayout("C")), NewContext()). func CompareVariants(r *Responsive, ctx *Context) map[string]float64 { - if ctx == nil { - ctx = NewContext() - } + ctx = normaliseContext(ctx) if r == nil { return map[string]float64{} } diff --git a/render.go b/render.go index e7c90a3..e7d1904 100644 --- a/render.go +++ b/render.go @@ -3,9 +3,7 @@ package html // render.go: Render is a convenience function that renders a node tree to HTML. // Example: Render(NewLayout("C").C(Raw("body")), NewContext()). func Render(node Node, ctx *Context) string { - if ctx == nil { - ctx = NewContext() - } + ctx = normaliseContext(ctx) if node == nil { return "" } diff --git a/responsive.go b/responsive.go index e2feb5e..e7ca77b 100644 --- a/responsive.go +++ b/responsive.go @@ -106,6 +106,7 @@ func (r *Responsive) Render(ctx *Context) string { if r == nil { return "" } + ctx = normaliseContext(ctx) var b strings.Builder for _, v := range r.variants { diff --git a/responsive_test.go b/responsive_test.go index 73d4d36..64dfe96 100644 --- a/responsive_test.go +++ b/responsive_test.go @@ -96,6 +96,20 @@ func TestResponsive_RenderNilReceiver(t *testing.T) { } } +func TestResponsive_RenderNilContext(t *testing.T) { + r := NewResponsive(). + Variant("desktop", NewLayout("C").C(Raw("main"))) + + got := r.Render(nil) + + if !strings.Contains(got, `data-variant="desktop"`) { + t.Fatalf("Responsive.Render(nil) should still render the variant wrapper, got:\n%s", got) + } + if !strings.Contains(got, `data-block="C-0"`) { + t.Fatalf("Responsive.Render(nil) should still render the layout block, got:\n%s", got) + } +} + func TestResponsive_NilLayoutVariant(t *testing.T) { ctx := NewContext() r := NewResponsive().