diff --git a/edge_test.go b/edge_test.go
index 0611a23..aaaaa12 100644
--- a/edge_test.go
+++ b/edge_test.go
@@ -1,6 +1,7 @@
package html
import (
+ "errors"
"testing"
i18n "dappco.re/go/core/i18n"
@@ -341,6 +342,64 @@ func TestLayout_InvalidVariantChars_Bad(t *testing.T) {
}
}
+func TestLayout_VariantError_Bad(t *testing.T) {
+ tests := []struct {
+ name string
+ variant string
+ wantInvalid bool
+ wantErrString string
+ build func(*Layout)
+ wantRender string
+ }{
+ {
+ name: "valid variant",
+ variant: "HCF",
+ wantInvalid: false,
+ build: func(layout *Layout) {
+ layout.H(Raw("header")).C(Raw("main")).F(Raw("footer"))
+ },
+ wantRender: `main`,
+ },
+ {
+ name: "mixed invalid variant",
+ variant: "HXC",
+ wantInvalid: true,
+ wantErrString: "html: invalid layout variant HXC",
+ build: func(layout *Layout) {
+ layout.H(Raw("header")).C(Raw("main"))
+ },
+ wantRender: `main`,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ layout := NewLayout(tt.variant)
+ if tt.build != nil {
+ tt.build(layout)
+ }
+ if tt.wantInvalid {
+ if layout.VariantError() == nil {
+ t.Fatalf("VariantError() = nil, want sentinel error for %q", tt.variant)
+ }
+ if !errors.Is(layout.VariantError(), ErrInvalidLayoutVariant) {
+ t.Fatalf("VariantError() = %v, want errors.Is(..., ErrInvalidLayoutVariant)", layout.VariantError())
+ }
+ if got := layout.VariantError().Error(); got != tt.wantErrString {
+ t.Fatalf("VariantError().Error() = %q, want %q", got, tt.wantErrString)
+ }
+ } else if layout.VariantError() != nil {
+ t.Fatalf("VariantError() = %v, want nil", layout.VariantError())
+ }
+
+ got := layout.Render(NewContext())
+ if got != tt.wantRender {
+ t.Fatalf("Render() = %q, want %q", got, tt.wantRender)
+ }
+ })
+ }
+}
+
func TestLayout_InvalidVariantMixedValidInvalid_Bad(t *testing.T) {
ctx := NewContext()
diff --git a/layout.go b/layout.go
index 06e3318..ea17221 100644
--- a/layout.go
+++ b/layout.go
@@ -1,8 +1,14 @@
package html
+import "errors"
+
// Compile-time interface check.
var _ Node = (*Layout)(nil)
+// ErrInvalidLayoutVariant reports that a layout variant string contains at least
+// one unrecognised slot character.
+var ErrInvalidLayoutVariant = errors.New("html: invalid layout variant")
+
// slotMeta holds the semantic HTML mapping for each HLCRF slot.
type slotMeta struct {
tag string
@@ -22,9 +28,10 @@ var slotRegistry = map[byte]slotMeta{
// with deterministic path-based IDs.
// Usage example: page := NewLayout("HCF").H(Text("title")).C(Text("body"))
type Layout struct {
- variant string // "HLCRF", "HCF", "C", etc.
- path string // "" for root, "L-0-" for nested
- slots map[byte][]Node // H, L, C, R, F → children
+ variant string // "HLCRF", "HCF", "C", etc.
+ path string // "" for root, "L-0-" for nested
+ slots map[byte][]Node // H, L, C, R, F → children
+ variantErr error
}
func renderWithLayoutPath(node Node, ctx *Context, path string) string {
@@ -73,10 +80,27 @@ func renderWithLayoutPath(node Node, ctx *Context, path string) string {
// Usage example: page := NewLayout("HLCRF")
// The variant determines which slots are rendered (e.g., "HLCRF", "HCF", "C").
func NewLayout(variant string) *Layout {
- return &Layout{
+ l := &Layout{
variant: variant,
slots: make(map[byte][]Node),
}
+ l.variantErr = validateLayoutVariant(variant)
+ return l
+}
+
+func validateLayoutVariant(variant string) error {
+ var invalid bool
+ for i := range len(variant) {
+ if _, ok := slotRegistry[variant[i]]; ok {
+ continue
+ }
+ invalid = true
+ break
+ }
+ if !invalid {
+ return nil
+ }
+ return &layoutVariantError{variant: variant}
}
func (l *Layout) slotsForSlot(slot byte) []Node {
@@ -144,6 +168,15 @@ func (l *Layout) blockID(slot byte) string {
return l.path + string(slot) + "-0"
}
+// VariantError reports whether the layout variant string contained any invalid
+// slot characters when the layout was constructed.
+func (l *Layout) VariantError() error {
+ if l == nil {
+ return nil
+ }
+ return l.variantErr
+}
+
// Render produces the semantic HTML for this layout.
// Usage example: html := NewLayout("C").C(Text("body")).Render(NewContext())
// Only slots present in the variant string are rendered.
@@ -193,3 +226,15 @@ func (l *Layout) Render(ctx *Context) string {
return b.String()
}
+
+type layoutVariantError struct {
+ variant string
+}
+
+func (e *layoutVariantError) Error() string {
+ return "html: invalid layout variant " + e.variant
+}
+
+func (e *layoutVariantError) Unwrap() error {
+ return ErrInvalidLayoutVariant
+}