From f49ddbf3743c29bde616471f6b74af444eaa5a61 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 17 Feb 2026 00:08:17 +0000 Subject: [PATCH] feat: add Attr() helper for setting element attributes Co-Authored-By: Claude Opus 4.6 --- node.go | 9 +++++++++ node_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/node.go b/node.go index 2ade2f2..ee72d9a 100644 --- a/node.go +++ b/node.go @@ -71,6 +71,15 @@ func El(tag string, children ...Node) Node { } } +// Attr sets an attribute on an El node. Returns the node for chaining. +// If the node is not an *elNode, returns it unchanged. +func Attr(n Node, key, value string) Node { + if el, ok := n.(*elNode); ok { + el.attrs[key] = value + } + return n +} + func (n *elNode) Render(ctx *Context) string { var b strings.Builder diff --git a/node_test.go b/node_test.go index cfca0c1..c1df25b 100644 --- a/node_test.go +++ b/node_test.go @@ -155,6 +155,34 @@ func TestEachNode_Empty(t *testing.T) { } } +func TestElNode_Attr(t *testing.T) { + ctx := NewContext() + node := Attr(El("div", Raw("content")), "class", "container") + got := node.Render(ctx) + want := `
content
` + if got != want { + t.Errorf("Attr() = %q, want %q", got, want) + } +} + +func TestElNode_AttrEscaping(t *testing.T) { + ctx := NewContext() + node := Attr(El("img"), "alt", `he said "hello"`) + got := node.Render(ctx) + if !strings.Contains(got, `alt="he said "hello""`) { + t.Errorf("Attr should escape attribute values, got %q", got) + } +} + +func TestElNode_MultipleAttrs(t *testing.T) { + ctx := NewContext() + node := Attr(Attr(El("a", Raw("link")), "href", "/home"), "class", "nav") + got := node.Render(ctx) + if !strings.Contains(got, `class="nav"`) || !strings.Contains(got, `href="/home"`) { + t.Errorf("multiple Attr() calls should stack, got %q", got) + } +} + func TestSwitchNode(t *testing.T) { ctx := NewContext() cases := map[string]Node{