From 946ea8d0aa355176cb01f45167ca3d8381e7e0ba Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Feb 2026 23:39:31 +0000 Subject: [PATCH] feat: add HLCRF Layout type with semantic elements Co-Authored-By: Claude Opus 4.6 --- layout.go | 112 +++++++++++++++++++++++++++++++++++++++++++++++ layout_test.go | 116 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 layout.go create mode 100644 layout_test.go diff --git a/layout.go b/layout.go new file mode 100644 index 0000000..70ef3ac --- /dev/null +++ b/layout.go @@ -0,0 +1,112 @@ +package html + +import ( + "fmt" + "strings" +) + +// slotMeta holds the semantic HTML mapping for each HLCRF slot. +type slotMeta struct { + tag string + role string +} + +// slotRegistry maps slot letters to their semantic HTML elements and ARIA roles. +var slotRegistry = map[byte]slotMeta{ + 'H': {tag: "header", role: "banner"}, + 'L': {tag: "aside", role: "complementary"}, + 'C': {tag: "main", role: "main"}, + 'R': {tag: "aside", role: "complementary"}, + 'F': {tag: "footer", role: "contentinfo"}, +} + +// Layout is an HLCRF compositor. Arranges nodes into semantic HTML regions +// with deterministic path-based IDs. +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 +} + +// NewLayout creates a new Layout with the given variant string. +// The variant determines which slots are rendered (e.g., "HLCRF", "HCF", "C"). +func NewLayout(variant string) *Layout { + return &Layout{ + variant: variant, + slots: make(map[byte][]Node), + } +} + +// H appends nodes to the Header slot. +func (l *Layout) H(nodes ...Node) *Layout { + l.slots['H'] = append(l.slots['H'], nodes...) + return l +} + +// L appends nodes to the Left aside slot. +func (l *Layout) L(nodes ...Node) *Layout { + l.slots['L'] = append(l.slots['L'], nodes...) + return l +} + +// C appends nodes to the Content (main) slot. +func (l *Layout) C(nodes ...Node) *Layout { + l.slots['C'] = append(l.slots['C'], nodes...) + return l +} + +// R appends nodes to the Right aside slot. +func (l *Layout) R(nodes ...Node) *Layout { + l.slots['R'] = append(l.slots['R'], nodes...) + return l +} + +// F appends nodes to the Footer slot. +func (l *Layout) F(nodes ...Node) *Layout { + l.slots['F'] = append(l.slots['F'], nodes...) + return l +} + +// blockID returns the deterministic data-block attribute value for a slot. +func (l *Layout) blockID(slot byte) string { + return fmt.Sprintf("%s%c-0", l.path, slot) +} + +// Render produces the semantic HTML for this layout. +// Only slots present in the variant string are rendered. +func (l *Layout) Render(ctx *Context) string { + var b strings.Builder + + for i := 0; i < len(l.variant); i++ { + slot := l.variant[i] + children := l.slots[slot] + if len(children) == 0 { + continue + } + + meta, ok := slotRegistry[slot] + if !ok { + continue + } + + bid := l.blockID(slot) + + b.WriteByte('<') + b.WriteString(meta.tag) + b.WriteString(` role="`) + b.WriteString(meta.role) + b.WriteString(`" data-block="`) + b.WriteString(bid) + b.WriteString(`">`) + + for _, child := range children { + b.WriteString(child.Render(ctx)) + } + + b.WriteString("') + } + + return b.String() +} diff --git a/layout_test.go b/layout_test.go new file mode 100644 index 0000000..53532f4 --- /dev/null +++ b/layout_test.go @@ -0,0 +1,116 @@ +package html + +import ( + "strings" + "testing" +) + +func TestLayout_HLCRF(t *testing.T) { + ctx := NewContext() + layout := NewLayout("HLCRF"). + H(Raw("header")).L(Raw("left")).C(Raw("main")).R(Raw("right")).F(Raw("footer")) + got := layout.Render(ctx) + + // Must contain semantic elements + for _, want := range []string{"