diff --git a/layout.go b/layout.go index abeeeba..2d8c5ba 100644 --- a/layout.go +++ b/layout.go @@ -42,6 +42,20 @@ type Layout struct { variantErr error } +// Clone returns a deep copy of the layout tree. +// Example: next := layout.Clone(). +func (l *Layout) Clone() *Layout { + if l == nil { + return nil + } + + clone, ok := l.cloneNode().(*Layout) + if !ok { + return nil + } + return clone +} + // layout.go: NewLayout creates a new Layout with the given variant string. // Example: page := NewLayout("HCF"). // The variant determines which slots are rendered (e.g., "HLCRF", "HCF", "C"). diff --git a/layout_test.go b/layout_test.go index 2617831..563b423 100644 --- a/layout_test.go +++ b/layout_test.go @@ -99,6 +99,34 @@ func TestLayout_FluentAPI(t *testing.T) { } } +func TestLayout_CloneReturnsIndependentCopy(t *testing.T) { + original := NewLayout("HLCRF"). + H(Raw("header")). + C(Raw("main")). + F(Raw("footer")) + + clone := original.Clone() + if clone == nil { + t.Fatal("Clone should return a layout") + } + if clone == original { + t.Fatal("Clone should return a distinct layout instance") + } + + clone.H(Raw("cloned-header")) + clone.C(Raw("cloned-main")) + + originalGot := original.Render(NewContext()) + cloneGot := clone.Render(NewContext()) + + if strings.Contains(originalGot, "cloned-header") || strings.Contains(originalGot, "cloned-main") { + t.Fatalf("Clone should not mutate original layout, got:\n%s", originalGot) + } + if !strings.Contains(cloneGot, "cloned-header") || !strings.Contains(cloneGot, "cloned-main") { + t.Fatalf("Clone should preserve mutations on the copy, got:\n%s", cloneGot) + } +} + func TestLayout_IgnoresInvalidSlots(t *testing.T) { ctx := NewContext() // "C" variant: populating L and R should have no effect diff --git a/responsive.go b/responsive.go index c9e4e3e..8c23d56 100644 --- a/responsive.go +++ b/responsive.go @@ -28,6 +28,20 @@ func NewResponsive() *Responsive { } } +// Clone returns a deep copy of the responsive compositor. +// Example: next := responsive.Clone(). +func (r *Responsive) Clone() *Responsive { + if r == nil { + return nil + } + + clone, ok := r.cloneNode().(*Responsive) + if !ok { + return nil + } + return clone +} + func (r *Responsive) setAttr(key, value string) { if r == nil { return diff --git a/responsive_test.go b/responsive_test.go index 7600481..d67232b 100644 --- a/responsive_test.go +++ b/responsive_test.go @@ -54,6 +54,36 @@ func TestResponsive_VariantOrder(t *testing.T) { } } +func TestResponsive_CloneReturnsIndependentCopy(t *testing.T) { + original := NewResponsive(). + Variant("desktop", NewLayout("C").C(Raw("desktop"))). + Variant("mobile", NewLayout("C").C(Raw("mobile"))) + + clone := original.Clone() + if clone == nil { + t.Fatal("Clone should return a responsive compositor") + } + if clone == original { + t.Fatal("Clone should return a distinct responsive instance") + } + + clone.Variant("tablet", NewLayout("C").C(Raw("tablet"))) + clone.setAttr("class", "cloned-responsive") + + originalGot := original.Render(NewContext()) + cloneGot := clone.Render(NewContext()) + + if strings.Contains(originalGot, "tablet") || strings.Contains(originalGot, "cloned-responsive") { + t.Fatalf("Clone should not mutate original responsive compositor, got:\n%s", originalGot) + } + if !strings.Contains(cloneGot, `class="cloned-responsive"`) { + t.Fatalf("Clone should preserve attributes on the copy, got:\n%s", cloneGot) + } + if !strings.Contains(cloneGot, `data-variant="tablet"`) { + t.Fatalf("Clone should preserve new variants on the copy, got:\n%s", cloneGot) + } +} + func TestResponsive_NestedPaths(t *testing.T) { ctx := NewContext() inner := NewLayout("HCF").H(Raw("ih")).C(Raw("ic")).F(Raw("if"))