refactor(html): share deterministic attribute rendering
Some checks are pending
Security Scan / security (push) Waiting to run
Test / test (push) Waiting to run

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-04 01:01:12 +00:00
parent 2e8886bbd7
commit cc75f3b533
3 changed files with 29 additions and 40 deletions

View file

@ -3,7 +3,6 @@ package html
import (
"errors"
"maps"
"slices"
"strconv"
"strings"
)
@ -224,20 +223,9 @@ func (l *Layout) Render(ctx *Context) string {
b.WriteByte('<')
b.WriteString(escapeHTML(meta.tag))
if len(l.attrs) > 0 {
keys := slices.Collect(maps.Keys(l.attrs))
slices.Sort(keys)
for _, key := range keys {
if key == "role" || key == "data-block" {
continue
}
b.WriteByte(' ')
b.WriteString(escapeHTML(key))
b.WriteString(`="`)
b.WriteString(escapeAttr(l.attrs[key]))
b.WriteByte('"')
}
}
writeSortedAttrs(&b, l.attrs, func(key string) bool {
return key == "role" || key == "data-block"
})
b.WriteString(` role="`)
b.WriteString(escapeAttr(meta.role))
b.WriteString(`" data-block="`)

33
node.go
View file

@ -117,6 +117,28 @@ func escapeAttr(s string) string {
return html.EscapeString(s)
}
// writeSortedAttrs renders a deterministic attribute list to the builder.
// An optional skip callback can omit reserved keys while preserving ordering
// for the remaining attributes.
func writeSortedAttrs(b *strings.Builder, attrs map[string]string, skip func(string) bool) {
if len(attrs) == 0 {
return
}
keys := slices.Collect(maps.Keys(attrs))
slices.Sort(keys)
for _, key := range keys {
if skip != nil && skip(key) {
continue
}
b.WriteByte(' ')
b.WriteString(escapeHTML(key))
b.WriteString(`="`)
b.WriteString(escapeAttr(attrs[key]))
b.WriteByte('"')
}
}
// --- rawNode ---
type rawNode struct {
@ -622,16 +644,7 @@ func (n *elNode) renderWithPath(ctx *Context, path string) string {
b.WriteByte('<')
b.WriteString(escapeHTML(n.tag))
// Sort attribute keys for deterministic output.
keys := slices.Collect(maps.Keys(n.attrs))
slices.Sort(keys)
for _, key := range keys {
b.WriteByte(' ')
b.WriteString(escapeHTML(key))
b.WriteString(`="`)
b.WriteString(escapeAttr(n.attrs[key]))
b.WriteByte('"')
}
writeSortedAttrs(&b, n.attrs, nil)
b.WriteByte('>')

View file

@ -2,7 +2,6 @@ package html
import (
"maps"
"slices"
"strconv"
"strings"
)
@ -232,20 +231,9 @@ func (r *Responsive) renderWithPath(ctx *Context, path string) string {
var b strings.Builder
for _, v := range r.variants {
b.WriteString(`<div`)
if len(r.attrs) > 0 {
keys := slices.Collect(maps.Keys(r.attrs))
slices.Sort(keys)
for _, key := range keys {
if key == "data-variant" {
continue
}
b.WriteByte(' ')
b.WriteString(escapeHTML(key))
b.WriteString(`="`)
b.WriteString(escapeAttr(r.attrs[key]))
b.WriteByte('"')
}
}
writeSortedAttrs(&b, r.attrs, func(key string) bool {
return key == "data-variant"
})
b.WriteString(` data-variant="`)
b.WriteString(escapeAttr(v.name))
b.WriteString(`">`)