feat(html): add layout variant validation sentinel
Expose VariantError() on Layout and ErrInvalidLayoutVariant for invalid variant strings while preserving current render behaviour. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
8386c7e57d
commit
1f98026d04
2 changed files with 108 additions and 4 deletions
59
edge_test.go
59
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 role="banner" data-block="H-0">header</header><main role="main" data-block="C-0">main</main><footer role="contentinfo" data-block="F-0">footer</footer>`,
|
||||
},
|
||||
{
|
||||
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 role="banner" data-block="H-0">header</header><main role="main" data-block="C-0">main</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()
|
||||
|
||||
|
|
|
|||
53
layout.go
53
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
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue