247 lines
5.4 KiB
Go
247 lines
5.4 KiB
Go
package html
|
|
|
|
import (
|
|
"maps"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// responsive.go: Responsive wraps multiple Layout variants for breakpoint-aware rendering.
|
|
// Example: NewResponsive().Variant("desktop", NewLayout("C").C(Raw("main"))).
|
|
// Each variant is rendered inside a container with data-variant for CSS targeting.
|
|
type Responsive struct {
|
|
variants []responsiveVariant
|
|
attrs map[string]string
|
|
}
|
|
|
|
type responsiveVariant struct {
|
|
name string
|
|
layout *Layout
|
|
}
|
|
|
|
// responsive.go: NewResponsive creates a new multi-variant responsive compositor.
|
|
// Example: r := NewResponsive().
|
|
func NewResponsive() *Responsive {
|
|
return &Responsive{
|
|
attrs: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
// Clone returns a deep copy of the responsive compositor.
|
|
// Example: next := responsive.Clone().
|
|
func (r *Responsive) Clone() *Responsive {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
|
|
clone, ok := r.cloneNode().(*Responsive)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
return clone
|
|
}
|
|
|
|
func (r *Responsive) setAttr(key, value string) {
|
|
if r == nil {
|
|
return
|
|
}
|
|
if r.attrs == nil {
|
|
r.attrs = make(map[string]string)
|
|
}
|
|
r.attrs[key] = value
|
|
}
|
|
|
|
// escapeCSSString escapes a string for safe use inside a double-quoted CSS
|
|
// attribute selector.
|
|
func escapeCSSString(s string) string {
|
|
var b strings.Builder
|
|
for _, r := range s {
|
|
switch r {
|
|
case '\\', '"':
|
|
b.WriteByte('\\')
|
|
b.WriteRune(r)
|
|
case '\n':
|
|
b.WriteString(`\a `)
|
|
case '\r':
|
|
b.WriteString(`\d `)
|
|
case '\f':
|
|
b.WriteString(`\c `)
|
|
default:
|
|
if r < 0x20 || r == 0x7f {
|
|
b.WriteByte('\\')
|
|
b.WriteString(strings.ToLower(strconv.FormatInt(int64(r), 16)))
|
|
b.WriteByte(' ')
|
|
continue
|
|
}
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// responsive.go: VariantSelector returns a CSS attribute selector for a named responsive variant.
|
|
// Example: VariantSelector("desktop") returns [data-variant="desktop"].
|
|
func VariantSelector(name string) string {
|
|
return `[data-variant="` + escapeCSSString(name) + `"]`
|
|
}
|
|
|
|
// responsive.go: ScopeVariant prefixes a selector so it only matches elements inside the
|
|
// named responsive variant.
|
|
// Example: ScopeVariant("desktop", ".nav").
|
|
func ScopeVariant(name, selector string) string {
|
|
scope := VariantSelector(name)
|
|
if selector == "" {
|
|
return scope
|
|
}
|
|
|
|
parts := splitSelectorList(selector)
|
|
scoped := make([]string, 0, len(parts))
|
|
for i := range parts {
|
|
part := strings.TrimSpace(parts[i])
|
|
if part == "" {
|
|
continue
|
|
}
|
|
scoped = append(scoped, scope+" "+part)
|
|
}
|
|
if len(scoped) == 0 {
|
|
return scope
|
|
}
|
|
return strings.Join(scoped, ", ")
|
|
}
|
|
|
|
// splitSelectorList splits a CSS selector list on top-level commas only.
|
|
// Commas inside brackets, parentheses, braces, or quoted strings are preserved.
|
|
func splitSelectorList(selector string) []string {
|
|
if selector == "" {
|
|
return nil
|
|
}
|
|
|
|
parts := make([]string, 0, 1)
|
|
var b strings.Builder
|
|
var quote rune
|
|
escaped := false
|
|
depthParen := 0
|
|
depthBracket := 0
|
|
depthBrace := 0
|
|
|
|
for _, r := range selector {
|
|
if escaped {
|
|
b.WriteRune(r)
|
|
escaped = false
|
|
continue
|
|
}
|
|
|
|
if r == '\\' {
|
|
b.WriteRune(r)
|
|
escaped = true
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case quote != 0:
|
|
b.WriteRune(r)
|
|
if r == quote {
|
|
quote = 0
|
|
}
|
|
case r == '"' || r == '\'':
|
|
quote = r
|
|
b.WriteRune(r)
|
|
case r == '(':
|
|
depthParen++
|
|
b.WriteRune(r)
|
|
case r == ')':
|
|
if depthParen > 0 {
|
|
depthParen--
|
|
}
|
|
b.WriteRune(r)
|
|
case r == '[':
|
|
depthBracket++
|
|
b.WriteRune(r)
|
|
case r == ']':
|
|
if depthBracket > 0 {
|
|
depthBracket--
|
|
}
|
|
b.WriteRune(r)
|
|
case r == '{':
|
|
depthBrace++
|
|
b.WriteRune(r)
|
|
case r == '}':
|
|
if depthBrace > 0 {
|
|
depthBrace--
|
|
}
|
|
b.WriteRune(r)
|
|
case r == ',' && depthParen == 0 && depthBracket == 0 && depthBrace == 0:
|
|
parts = append(parts, b.String())
|
|
b.Reset()
|
|
default:
|
|
b.WriteRune(r)
|
|
}
|
|
}
|
|
|
|
parts = append(parts, b.String())
|
|
return parts
|
|
}
|
|
|
|
// responsive.go: Variant adds a named layout variant (e.g., "desktop", "tablet", "mobile").
|
|
// Example: r.Variant("mobile", NewLayout("C").C(Raw("body"))).
|
|
// Variants render in insertion order.
|
|
func (r *Responsive) Variant(name string, layout *Layout) *Responsive {
|
|
if r == nil {
|
|
return nil
|
|
}
|
|
if layout != nil {
|
|
layout = layout.Clone()
|
|
}
|
|
r.variants = append(r.variants, responsiveVariant{name: name, layout: layout})
|
|
return r
|
|
}
|
|
|
|
// responsive.go: Render produces HTML with each variant in a data-variant container.
|
|
// Example: NewResponsive().Variant("desktop", NewLayout("C")).Render(NewContext()).
|
|
func (r *Responsive) Render(ctx *Context) string {
|
|
return r.renderWithPath(ctx, "")
|
|
}
|
|
|
|
func (r *Responsive) cloneNode() Node {
|
|
if r == nil {
|
|
return (*Responsive)(nil)
|
|
}
|
|
|
|
clone := *r
|
|
if r.attrs != nil {
|
|
clone.attrs = maps.Clone(r.attrs)
|
|
}
|
|
if r.variants != nil {
|
|
clone.variants = make([]responsiveVariant, len(r.variants))
|
|
for i := range r.variants {
|
|
clone.variants[i] = r.variants[i]
|
|
if r.variants[i].layout != nil {
|
|
if layout, ok := cloneNode(r.variants[i].layout).(*Layout); ok {
|
|
clone.variants[i].layout = layout
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return &clone
|
|
}
|
|
|
|
func (r *Responsive) renderWithPath(ctx *Context, path string) string {
|
|
if r == nil {
|
|
return ""
|
|
}
|
|
ctx = normaliseContext(ctx)
|
|
|
|
var b strings.Builder
|
|
for _, v := range r.variants {
|
|
b.WriteString(`<div`)
|
|
writeSortedAttrs(&b, r.attrs, func(key string) bool {
|
|
return key == "data-variant"
|
|
})
|
|
b.WriteString(` data-variant="`)
|
|
b.WriteString(escapeAttr(v.name))
|
|
b.WriteString(`">`)
|
|
b.WriteString(renderNodeWithPath(v.layout, ctx, path))
|
|
b.WriteString(`</div>`)
|
|
}
|
|
return b.String()
|
|
}
|