package html import ( "maps" "slices" "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), } } 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 } 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(`