From ba384aeb12889bc38bbac95781737b582f76b402 Mon Sep 17 00:00:00 2001 From: Virgil Date: Fri, 3 Apr 2026 16:55:48 +0000 Subject: [PATCH] fix: add nil-safe rendering paths Co-Authored-By: Virgil --- layout.go | 4 ++++ layout_test.go | 8 ++++++++ node.go | 31 +++++++++++++++++++++++++++++++ pipeline.go | 3 +++ pipeline_test.go | 7 +++++++ render.go | 3 +++ render_test.go | 7 +++++++ responsive.go | 4 ++++ responsive_test.go | 8 ++++++++ 9 files changed, 75 insertions(+) diff --git a/layout.go b/layout.go index cb11610..165c842 100644 --- a/layout.go +++ b/layout.go @@ -111,6 +111,10 @@ func (l *Layout) VariantError() error { // Render produces the semantic HTML for this layout. // Only slots present in the variant string are rendered. func (l *Layout) Render(ctx *Context) string { + if l == nil { + return "" + } + var b strings.Builder for i := range len(l.variant) { diff --git a/layout_test.go b/layout_test.go index 7b9eb3d..b986761 100644 --- a/layout_test.go +++ b/layout_test.go @@ -211,3 +211,11 @@ func TestLayout_VariantError(t *testing.T) { }) } } + +func TestLayout_RenderNilReceiver(t *testing.T) { + var layout *Layout + got := layout.Render(NewContext()) + if got != "" { + t.Fatalf("nil Layout should render empty string, got %q", got) + } +} diff --git a/node.go b/node.go index 3db5cc5..8ae5395 100644 --- a/node.go +++ b/node.go @@ -96,6 +96,9 @@ func Raw(content string) Node { } func (n *rawNode) Render(_ *Context) string { + if n == nil { + return "" + } return n.content } @@ -156,6 +159,10 @@ func TabIndex(n Node, index int) Node { } func (n *elNode) Render(ctx *Context) string { + if n == nil { + return "" + } + var b strings.Builder b.WriteByte('<') @@ -210,6 +217,10 @@ func Text(key string, args ...any) Node { } func (n *textNode) Render(ctx *Context) string { + if n == nil { + return "" + } + var text string if ctx != nil && ctx.service != nil { text = ctx.service.T(n.key, n.args...) @@ -232,6 +243,10 @@ func If(cond func(*Context) bool, node Node) Node { } func (n *ifNode) Render(ctx *Context) string { + if n == nil || n.cond == nil || n.node == nil { + return "" + } + if n.cond(ctx) { return n.node.Render(ctx) } @@ -251,6 +266,10 @@ func Unless(cond func(*Context) bool, node Node) Node { } func (n *unlessNode) Render(ctx *Context) string { + if n == nil || n.cond == nil || n.node == nil { + return "" + } + if !n.cond(ctx) { return n.node.Render(ctx) } @@ -271,6 +290,10 @@ func Entitled(feature string, node Node) Node { } func (n *entitledNode) Render(ctx *Context) string { + if n == nil || n.node == nil { + return "" + } + if ctx == nil || ctx.Entitlements == nil || !ctx.Entitlements(n.feature) { return "" } @@ -290,6 +313,10 @@ func Switch(selector func(*Context) string, cases map[string]Node) Node { } func (n *switchNode) Render(ctx *Context) string { + if n == nil || n.selector == nil || n.cases == nil { + return "" + } + key := n.selector(ctx) if node, ok := n.cases[key]; ok { return node.Render(ctx) @@ -315,6 +342,10 @@ func EachSeq[T any](items iter.Seq[T], fn func(T) Node) Node { } func (n *eachNode[T]) Render(ctx *Context) string { + if n == nil || n.items == nil || n.fn == nil { + return "" + } + var b strings.Builder for item := range n.items { b.WriteString(n.fn(item).Render(ctx)) diff --git a/pipeline.go b/pipeline.go index c400dd3..5ed47e5 100644 --- a/pipeline.go +++ b/pipeline.go @@ -62,6 +62,9 @@ func CompareVariants(r *Responsive, ctx *Context) map[string]float64 { if ctx == nil { ctx = NewContext() } + if r == nil { + return map[string]float64{} + } type named struct { name string diff --git a/pipeline_test.go b/pipeline_test.go index c896859..11f3bd8 100644 --- a/pipeline_test.go +++ b/pipeline_test.go @@ -128,3 +128,10 @@ func TestCompareVariants(t *testing.T) { t.Errorf("same content in different variants should score >= 0.8, got %f", sim) } } + +func TestCompareVariants_NilResponsive(t *testing.T) { + scores := CompareVariants(nil, NewContext()) + if len(scores) != 0 { + t.Fatalf("CompareVariants(nil, ctx) = %v, want empty map", scores) + } +} diff --git a/render.go b/render.go index 3d3a7e3..93a2529 100644 --- a/render.go +++ b/render.go @@ -5,5 +5,8 @@ func Render(node Node, ctx *Context) string { if ctx == nil { ctx = NewContext() } + if node == nil { + return "" + } return node.Render(ctx) } diff --git a/render_test.go b/render_test.go index 02ebb8e..8cdd076 100644 --- a/render_test.go +++ b/render_test.go @@ -95,3 +95,10 @@ func TestRender_XSSPrevention(t *testing.T) { t.Errorf("XSS prevention: expected escaped script tag, got:\n%s", got) } } + +func TestRender_NilNode(t *testing.T) { + got := Render(nil, NewContext()) + if got != "" { + t.Fatalf("Render(nil, ctx) = %q, want empty string", got) + } +} diff --git a/responsive.go b/responsive.go index 540d7b1..8ed8447 100644 --- a/responsive.go +++ b/responsive.go @@ -65,6 +65,10 @@ func (r *Responsive) Variant(name string, layout *Layout) *Responsive { // Render produces HTML with each variant in a data-variant container. func (r *Responsive) Render(ctx *Context) string { + if r == nil { + return "" + } + var b strings.Builder for _, v := range r.variants { b.WriteString(`