From 40da0d8b1dec5fb0caf7fbb18029839420850fdf Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Feb 2026 23:53:31 +0000 Subject: [PATCH] fix: deterministic attributes and thread-safe nested layouts Sort attribute keys for deterministic HTML output (I-1). Clone nested layouts before mutating path in Render (I-3), preventing data races when the same tree is rendered concurrently. Co-Authored-By: Claude Opus 4.6 --- layout.go | 7 +++++-- node.go | 11 +++++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/layout.go b/layout.go index 5d59fa1..ab53b11 100644 --- a/layout.go +++ b/layout.go @@ -100,9 +100,12 @@ func (l *Layout) Render(ctx *Context) string { b.WriteString(`">`) for _, child := range children { - // Propagate path to nested layouts. + // Clone nested layouts before setting path (thread-safe). if inner, ok := child.(*Layout); ok { - inner.path = bid + "-" + clone := *inner + clone.path = bid + "-" + b.WriteString(clone.Render(ctx)) + continue } b.WriteString(child.Render(ctx)) } diff --git a/node.go b/node.go index 725cc86..2ade2f2 100644 --- a/node.go +++ b/node.go @@ -1,6 +1,7 @@ package html import ( + "sort" "strings" i18n "forge.lthn.ai/core/go-i18n" @@ -76,11 +77,17 @@ func (n *elNode) Render(ctx *Context) string { b.WriteByte('<') b.WriteString(n.tag) - for key, val := range n.attrs { + // Sort attribute keys for deterministic output. + keys := make([]string, 0, len(n.attrs)) + for k := range n.attrs { + keys = append(keys, k) + } + sort.Strings(keys) + for _, key := range keys { b.WriteByte(' ') b.WriteString(key) b.WriteString(`="`) - b.WriteString(escapeAttr(val)) + b.WriteString(escapeAttr(n.attrs[key])) b.WriteByte('"') }