fix(core): harden layout and responsive nil chains
Some checks failed
Security Scan / security (push) Successful in 8s
Test / test (push) Failing after 31s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-30 00:28:03 +00:00
parent c6fd135239
commit 911071d2b0
4 changed files with 70 additions and 5 deletions

View file

@ -37,38 +37,63 @@ func NewLayout(variant string) *Layout {
}
}
func (l *Layout) slotsForSlot(slot byte) []Node {
if l == nil {
return nil
}
if l.slots == nil {
l.slots = make(map[byte][]Node)
}
return l.slots[slot]
}
// H appends nodes to the Header slot.
// Usage example: NewLayout("HCF").H(Text("title"))
func (l *Layout) H(nodes ...Node) *Layout {
l.slots['H'] = append(l.slots['H'], nodes...)
if l == nil {
return nil
}
l.slots['H'] = append(l.slotsForSlot('H'), nodes...)
return l
}
// L appends nodes to the Left aside slot.
// Usage example: NewLayout("LC").L(Text("nav"))
func (l *Layout) L(nodes ...Node) *Layout {
l.slots['L'] = append(l.slots['L'], nodes...)
if l == nil {
return nil
}
l.slots['L'] = append(l.slotsForSlot('L'), nodes...)
return l
}
// C appends nodes to the Content (main) slot.
// Usage example: NewLayout("C").C(Text("body"))
func (l *Layout) C(nodes ...Node) *Layout {
l.slots['C'] = append(l.slots['C'], nodes...)
if l == nil {
return nil
}
l.slots['C'] = append(l.slotsForSlot('C'), nodes...)
return l
}
// R appends nodes to the Right aside slot.
// Usage example: NewLayout("CR").R(Text("ads"))
func (l *Layout) R(nodes ...Node) *Layout {
l.slots['R'] = append(l.slots['R'], nodes...)
if l == nil {
return nil
}
l.slots['R'] = append(l.slotsForSlot('R'), nodes...)
return l
}
// F appends nodes to the Footer slot.
// Usage example: NewLayout("CF").F(Text("footer"))
func (l *Layout) F(nodes ...Node) *Layout {
l.slots['F'] = append(l.slots['F'], nodes...)
if l == nil {
return nil
}
l.slots['F'] = append(l.slotsForSlot('F'), nodes...)
return l
}

View file

@ -113,3 +113,27 @@ func TestLayout_IgnoresInvalidSlots_Good(t *testing.T) {
t.Errorf("C variant should ignore R slot content, got:\n%s", got)
}
}
func TestLayout_Methods_NilLayout_Ugly(t *testing.T) {
var layout *Layout
if layout.H(Raw("h")) != nil {
t.Fatal("expected nil layout from H on nil receiver")
}
if layout.L(Raw("l")) != nil {
t.Fatal("expected nil layout from L on nil receiver")
}
if layout.C(Raw("c")) != nil {
t.Fatal("expected nil layout from C on nil receiver")
}
if layout.R(Raw("r")) != nil {
t.Fatal("expected nil layout from R on nil receiver")
}
if layout.F(Raw("f")) != nil {
t.Fatal("expected nil layout from F on nil receiver")
}
if got := layout.Render(NewContext()); got != "" {
t.Fatalf("nil layout render should be empty, got %q", got)
}
}

View file

@ -25,6 +25,9 @@ func NewResponsive() *Responsive {
// Usage example: NewResponsive().Variant("desktop", NewLayout("HLCRF"))
// Variants render in insertion order.
func (r *Responsive) Variant(name string, layout *Layout) *Responsive {
if r == nil {
r = NewResponsive()
}
r.variants = append(r.variants, responsiveVariant{name: name, layout: layout})
return r
}

View file

@ -86,3 +86,16 @@ func TestResponsive_VariantsIndependent_Good(t *testing.T) {
func TestResponsive_ImplementsNode_Ugly(t *testing.T) {
var _ Node = NewResponsive()
}
func TestResponsive_Variant_NilResponsive_Ugly(t *testing.T) {
var r *Responsive
got := r.Variant("mobile", NewLayout("C").C(Raw("content")))
if got == nil {
t.Fatal("expected non-nil responsive from Variant on nil receiver")
}
if output := got.Render(NewContext()); output != `<div data-variant="mobile"><main data-block="C-0">content</main></div>` {
t.Fatalf("unexpected output from nil receiver Variant path: %q", output)
}
}