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: `
header
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: `
header
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 +}