fix(core): harden nil-safe rendering paths
- 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:
parent
8c7a9de546
commit
0318d73a12
4 changed files with 54 additions and 2 deletions
10
layout.go
10
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))
|
||||
|
|
|
|||
32
node.go
32
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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(`">`)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue