fix(core): harden nil-safe rendering paths
All checks were successful
Security Scan / security (push) Successful in 15s
Test / test (push) Successful in 43s

- guard nil receivers and nodes in core render flows
- make Render() safe for nil input
- add compile-time Node contract for Responsive

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-29 23:10:48 +00:00
parent 8c7a9de546
commit 0318d73a12
4 changed files with 54 additions and 2 deletions

View file

@ -81,6 +81,10 @@ func (l *Layout) blockID(slot byte) string {
// Usage example: html := NewLayout("C").C(Text("body")).Render(NewContext())
// Only slots present in the variant string are rendered.
func (l *Layout) Render(ctx *Context) string {
if l == nil {
return ""
}
b := newTextBuilder()
for i := range len(l.variant) {
@ -106,8 +110,12 @@ func (l *Layout) Render(ctx *Context) string {
b.WriteString(`">`)
for _, child := range children {
if child == nil {
continue
}
// Clone nested layouts before setting path (thread-safe).
if inner, ok := child.(*Layout); ok {
if inner, ok := child.(*Layout); ok && inner != nil {
clone := *inner
clone.path = bid + "-"
b.WriteString(clone.Render(ctx))

32
node.go
View file

@ -85,6 +85,10 @@ func El(tag string, children ...Node) Node {
// Usage example: Attr(El("a", Text("docs")), "href", "/docs")
// It recursively traverses through wrappers like If, Unless, and Entitled.
func Attr(n Node, key, value string) Node {
if n == nil {
return n
}
switch t := n.(type) {
case *elNode:
t.attrs[key] = value
@ -171,6 +175,9 @@ 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)
}
@ -191,6 +198,9 @@ 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)
}
@ -212,6 +222,9 @@ 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 ""
}
@ -232,8 +245,17 @@ func Switch(selector func(*Context) string, cases map[string]Node) Node {
}
func (n *switchNode) Render(ctx *Context) string {
if n == nil || n.selector == nil {
return ""
}
key := n.selector(ctx)
if n.cases == nil {
return ""
}
if node, ok := n.cases[key]; ok {
if node == nil {
return ""
}
return node.Render(ctx)
}
return ""
@ -259,9 +281,17 @@ 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.fn == nil || n.items == nil {
return ""
}
b := newTextBuilder()
for item := range n.items {
b.WriteString(n.fn(item).Render(ctx))
child := n.fn(item)
if child == nil {
continue
}
b.WriteString(child.Render(ctx))
}
return b.String()
}

View file

@ -3,6 +3,9 @@ package html
// Render is a convenience function that renders a node tree to HTML.
// Usage example: html := Render(El("main", Text("welcome")), NewContext())
func Render(node Node, ctx *Context) string {
if node == nil {
return ""
}
if ctx == nil {
ctx = NewContext()
}

View file

@ -1,5 +1,8 @@
package html
// Compile-time interface check.
var _ Node = (*Responsive)(nil)
// Responsive wraps multiple Layout variants for breakpoint-aware rendering.
// Usage example: r := NewResponsive().Variant("mobile", NewLayout("C"))
// Each variant is rendered inside a container with data-variant for CSS targeting.
@ -29,8 +32,16 @@ func (r *Responsive) Variant(name string, layout *Layout) *Responsive {
// Render produces HTML with each variant in a data-variant container.
// Usage example: html := NewResponsive().Variant("mobile", NewLayout("C")).Render(NewContext())
func (r *Responsive) Render(ctx *Context) string {
if r == nil {
return ""
}
b := newTextBuilder()
for _, v := range r.variants {
if v.layout == nil {
continue
}
b.WriteString(`<div data-variant="`)
b.WriteString(escapeAttr(v.name))
b.WriteString(`">`)