2026-02-16 23:39:31 +00:00
|
|
|
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 {
|
2026-02-16 23:53:31 +00:00
|
|
|
// Clone nested layouts before setting path (thread-safe).
|
2026-02-16 23:40:40 +00:00
|
|
|
if inner, ok := child.(*Layout); ok {
|
2026-02-16 23:53:31 +00:00
|
|
|
clone := *inner
|
|
|
|
|
clone.path = bid + "-"
|
|
|
|
|
b.WriteString(clone.Render(ctx))
|
|
|
|
|
continue
|
2026-02-16 23:40:40 +00:00
|
|
|
}
|
2026-02-16 23:39:31 +00:00
|
|
|
b.WriteString(child.Render(ctx))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
b.WriteString("</")
|
|
|
|
|
b.WriteString(meta.tag)
|
|
|
|
|
b.WriteByte('>')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return b.String()
|
|
|
|
|
}
|