fix(html): isolate wrapper attrs from shared nodes
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
b5d170817c
commit
8900d25cfe
4 changed files with 159 additions and 4 deletions
22
layout.go
22
layout.go
|
|
@ -161,6 +161,28 @@ func (l *Layout) VariantValid() bool {
|
|||
return l == nil || l.variantErr == nil
|
||||
}
|
||||
|
||||
func (l *Layout) cloneNode() Node {
|
||||
if l == nil {
|
||||
return (*Layout)(nil)
|
||||
}
|
||||
|
||||
clone := *l
|
||||
if l.attrs != nil {
|
||||
clone.attrs = maps.Clone(l.attrs)
|
||||
}
|
||||
if l.slots != nil {
|
||||
clone.slots = make(map[byte][]Node, len(l.slots))
|
||||
for slot, children := range l.slots {
|
||||
clonedChildren := make([]Node, len(children))
|
||||
for i := range children {
|
||||
clonedChildren[i] = cloneNode(children[i])
|
||||
}
|
||||
clone.slots[slot] = clonedChildren
|
||||
}
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
||||
// layout.go: Render produces the semantic HTML for this layout.
|
||||
// Example: NewLayout("C").C(Raw("body")).Render(NewContext()).
|
||||
// Only slots present in the variant string are rendered.
|
||||
|
|
|
|||
96
node.go
96
node.go
|
|
@ -15,6 +15,10 @@ type Node interface {
|
|||
Render(ctx *Context) string
|
||||
}
|
||||
|
||||
type cloneableNode interface {
|
||||
cloneNode() Node
|
||||
}
|
||||
|
||||
// Compile-time interface checks.
|
||||
var (
|
||||
_ Node = (*rawNode)(nil)
|
||||
|
|
@ -160,21 +164,33 @@ func Attr(n Node, key, value string) Node {
|
|||
case *elNode:
|
||||
t.attrs[key] = value
|
||||
case *ifNode:
|
||||
Attr(t.node, key, value)
|
||||
t.node = Attr(cloneNode(t.node), key, value)
|
||||
case *unlessNode:
|
||||
Attr(t.node, key, value)
|
||||
t.node = Attr(cloneNode(t.node), key, value)
|
||||
case *entitledNode:
|
||||
Attr(t.node, key, value)
|
||||
t.node = Attr(cloneNode(t.node), key, value)
|
||||
case *switchNode:
|
||||
cloned := make(map[string]Node, len(t.cases))
|
||||
for caseKey, caseNode := range t.cases {
|
||||
t.cases[caseKey] = Attr(caseNode, key, value)
|
||||
cloned[caseKey] = Attr(cloneNode(caseNode), key, value)
|
||||
}
|
||||
t.cases = cloned
|
||||
case interface{ setAttr(string, string) }:
|
||||
t.setAttr(key, value)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func cloneNode(n Node) Node {
|
||||
if n == nil {
|
||||
return nil
|
||||
}
|
||||
if cloner, ok := n.(cloneableNode); ok {
|
||||
return cloner.cloneNode()
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// node.go: AriaLabel sets the aria-label attribute on an element node.
|
||||
// Example: AriaLabel(El("button"), "Open menu").
|
||||
func AriaLabel(n Node, label string) Node {
|
||||
|
|
@ -472,6 +488,24 @@ func (n *elNode) renderWithPath(ctx *Context, path string) string {
|
|||
return b.String()
|
||||
}
|
||||
|
||||
func (n *elNode) cloneNode() Node {
|
||||
if n == nil {
|
||||
return (*elNode)(nil)
|
||||
}
|
||||
|
||||
clone := *n
|
||||
if len(n.children) > 0 {
|
||||
clone.children = make([]Node, len(n.children))
|
||||
for i := range n.children {
|
||||
clone.children[i] = cloneNode(n.children[i])
|
||||
}
|
||||
}
|
||||
if n.attrs != nil {
|
||||
clone.attrs = maps.Clone(n.attrs)
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
||||
// --- escapeHTML ---
|
||||
|
||||
// escapeHTML escapes a string for safe inclusion in HTML text content.
|
||||
|
|
@ -526,6 +560,16 @@ func (n *ifNode) Render(ctx *Context) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (n *ifNode) cloneNode() Node {
|
||||
if n == nil {
|
||||
return (*ifNode)(nil)
|
||||
}
|
||||
|
||||
clone := *n
|
||||
clone.node = cloneNode(n.node)
|
||||
return &clone
|
||||
}
|
||||
|
||||
// --- unlessNode ---
|
||||
|
||||
type unlessNode struct {
|
||||
|
|
@ -551,6 +595,16 @@ func (n *unlessNode) Render(ctx *Context) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (n *unlessNode) cloneNode() Node {
|
||||
if n == nil {
|
||||
return (*unlessNode)(nil)
|
||||
}
|
||||
|
||||
clone := *n
|
||||
clone.node = cloneNode(n.node)
|
||||
return &clone
|
||||
}
|
||||
|
||||
// --- entitledNode ---
|
||||
|
||||
type entitledNode struct {
|
||||
|
|
@ -578,6 +632,16 @@ func (n *entitledNode) Render(ctx *Context) string {
|
|||
return renderNodeWithPath(n.node, ctx, "")
|
||||
}
|
||||
|
||||
func (n *entitledNode) cloneNode() Node {
|
||||
if n == nil {
|
||||
return (*entitledNode)(nil)
|
||||
}
|
||||
|
||||
clone := *n
|
||||
clone.node = cloneNode(n.node)
|
||||
return &clone
|
||||
}
|
||||
|
||||
// --- switchNode ---
|
||||
|
||||
type switchNode struct {
|
||||
|
|
@ -604,6 +668,21 @@ func (n *switchNode) Render(ctx *Context) string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (n *switchNode) cloneNode() Node {
|
||||
if n == nil {
|
||||
return (*switchNode)(nil)
|
||||
}
|
||||
|
||||
clone := *n
|
||||
if n.cases != nil {
|
||||
clone.cases = make(map[string]Node, len(n.cases))
|
||||
for caseKey, caseNode := range n.cases {
|
||||
clone.cases[caseKey] = cloneNode(caseNode)
|
||||
}
|
||||
}
|
||||
return &clone
|
||||
}
|
||||
|
||||
// --- eachNode ---
|
||||
|
||||
type eachNode[T any] struct {
|
||||
|
|
@ -658,3 +737,12 @@ func (n *eachNode[T]) setAttr(key, value string) {
|
|||
return Attr(prev(item), key, value)
|
||||
}
|
||||
}
|
||||
|
||||
func (n *eachNode[T]) cloneNode() Node {
|
||||
if n == nil {
|
||||
return (*eachNode[T])(nil)
|
||||
}
|
||||
|
||||
clone := *n
|
||||
return &clone
|
||||
}
|
||||
|
|
|
|||
22
node_test.go
22
node_test.go
|
|
@ -753,6 +753,28 @@ func TestAttr_ThroughSwitchNode(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAttr_ThroughSwitchNode_IsolatedFromSharedCases(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
shared := El("div", Raw("content"))
|
||||
cases := map[string]Node{
|
||||
"dark": shared,
|
||||
"light": shared,
|
||||
}
|
||||
|
||||
first := Switch(func(*Context) string { return "dark" }, cases)
|
||||
second := Switch(func(*Context) string { return "light" }, cases)
|
||||
|
||||
Attr(first, "class", "theme")
|
||||
|
||||
if got := first.Render(ctx); got != `<div class="theme">content</div>` {
|
||||
t.Fatalf("first switch render = %q, want %q", got, `<div class="theme">content</div>`)
|
||||
}
|
||||
|
||||
if got := second.Render(ctx); got != `<div>content</div>` {
|
||||
t.Fatalf("shared switch cases should not leak attrs, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttr_ThroughEachNode(t *testing.T) {
|
||||
ctx := NewContext()
|
||||
node := Each([]string{"a", "b"}, func(item string) Node {
|
||||
|
|
|
|||
|
|
@ -176,6 +176,29 @@ 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 ""
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue