diff --git a/layout.go b/layout.go index 2d8c5ba..b2ab489 100644 --- a/layout.go +++ b/layout.go @@ -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="`) diff --git a/node.go b/node.go index cbd4f45..3571527 100644 --- a/node.go +++ b/node.go @@ -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('>') diff --git a/responsive.go b/responsive.go index 8c23d56..4a41858 100644 --- a/responsive.go +++ b/responsive.go @@ -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(` 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(`">`)