diff --git a/layout.go b/layout.go index c156739..12792b0 100644 --- a/layout.go +++ b/layout.go @@ -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)) diff --git a/node.go b/node.go index 921c53c..b9e6bac 100644 --- a/node.go +++ b/node.go @@ -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() } diff --git a/render.go b/render.go index 728ca7f..ad14109 100644 --- a/render.go +++ b/render.go @@ -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() } diff --git a/responsive.go b/responsive.go index fa2c479..b6fca16 100644 --- a/responsive.go +++ b/responsive.go @@ -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(`
`)