From e5ffb8257fb73e484360c9d1e0de19d180b175db Mon Sep 17 00:00:00 2001 From: tannevaled Date: Sun, 18 Oct 2026 15:48:15 +0200 Subject: [PATCH] fix(css): map width/height attributes to CSS lengths MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On an outermost , width and height are presentation attributes for the CSS width/height properties (SVG 2 §7.8), so they take any CSS . They were read only by the image loader's attrDim, which kept the leading digits: width="2em" became 2px and width="100%" was ignored, so the SVG fell back to its viewBox size (100x100 instead of 40x40 in a 2em box). They are now presentation hints (zero specificity, before author rules, like ): a bare number is px, any other valid non-negative length or percentage is kept, and auto/negative/invalid values are dropped, so the cascade resolves em/rem/% like any other length. Co-Authored-By: Claude Opus 5.5 --- css/presentational.go | 36 +++++++++++++++++++++ css/presentational_test.go | 25 ++++++++++++++ fixtures_svg_attr_size_test.go | 59 ++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+) create mode 100644 fixtures_svg_attr_size_test.go diff --git a/css/presentational.go b/css/presentational.go index 149b464..f310595 100644 --- a/css/presentational.go +++ b/css/presentational.go @@ -43,6 +43,22 @@ func presentationalHints(n *dom.Node) []Declaration { } } + // An 's width/height are not HTML length hints but SVG presentation + // attributes for the CSS width/height properties (SVG 2 §7.8), so they + // take any CSS — 100%, 2em, 3rem — and auto (which + // keeps the intrinsic, viewBox-derived size). Mapped here so the cascade + // resolves them like any other length; nested elements are SVG + // content, never laid out as boxes, so only the outermost one matters. + if tag == "svg" { + for _, name := range []string{"width", "height"} { + if v, ok := attr(name); ok { + if p := svgLengthHint(v); p != "" { + d = append(d, Declaration{Property: name, Value: p}) + } + } + } + } + // bgcolor → background-color (table, cells, rows, body, and legacy others). if v, ok := attr("bgcolor"); ok && v != "" { d = append(d, Declaration{Property: "background-color", Value: v}) @@ -156,6 +172,26 @@ func lengthHint(v string) string { return "" } +// svgLengthHint turns an SVG width/height presentation attribute into a CSS +// length: a bare number is user units (px), anything else must already be a +// valid non-negative CSS length or percentage. auto, negative or unparseable +// values are dropped (SVG 2 §7.8: a negative value is invalid and behaves as +// auto). +func svgLengthHint(v string) string { + v = strings.TrimSpace(v) + if f, err := strconv.ParseFloat(v, 64); err == nil { + if f < 0 { + return "" + } + return v + "px" + } + l, ok := parseLength(v, 16) + if !ok || l.Auto || l.Px < 0 || l.Percent < 0 { + return "" + } + return v +} + // fontSizeAttr maps a legacy value to a CSS font-size. Absolute // sizes 1..7 map to the HTML size table; a leading + or - is a relative step // from the size-3 baseline (approximated, since the real base is the inherited diff --git a/css/presentational_test.go b/css/presentational_test.go index 24d6696..b19762f 100644 --- a/css/presentational_test.go +++ b/css/presentational_test.go @@ -217,6 +217,31 @@ func TestFontSizeAttr(t *testing.T) { } } +// TestSVGLengthHint: an 's width/height are SVG presentation +// attributes taking any CSS length (SVG 2 §7.8). +func TestSVGLengthHint(t *testing.T) { + for _, tc := range []struct{ in, want string }{ + {"40", "40px"}, + {"2em", "2em"}, + {"100%", "100%"}, + {"3rem", "3rem"}, + {"12pt", "12pt"}, + {"auto", ""}, + {"-5", ""}, + {"-2em", ""}, + {"-10%", ""}, + {"bogus", ""}, + } { + if got := svgLengthHint(tc.in); got != tc.want { + t.Errorf("svgLengthHint(%q) = %q, want %q", tc.in, got, tc.want) + } + } + st := styleOf(t, ``, "svg") + if st.Width.Px != 40 || !st.Height.IsPercent || st.Height.Percent != 0.5 { + t.Errorf("svg width=2em height=50%% → %+v %+v", st.Width, st.Height) + } +} + // TestLengthHintEmptyAndBadPercent covers the empty-string and malformed-percent // branches of lengthHint via width attributes. func TestLengthHintEmptyAndBadPercent(t *testing.T) { diff --git a/fixtures_svg_attr_size_test.go b/fixtures_svg_attr_size_test.go new file mode 100644 index 0000000..fab823d --- /dev/null +++ b/fixtures_svg_attr_size_test.go @@ -0,0 +1,59 @@ +// Copyright (c) the go-webengine/engine authors. +// SPDX-License-Identifier: BSD-3-Clause + +package engine + +import ( + "image" + "testing" +) + +// redBBox returns the bounding box of the pure-red pixels of img. +func redBBox(img *image.RGBA) image.Rectangle { + r := image.Rectangle{} + first := true + b := img.Bounds() + for y := b.Min.Y; y < b.Max.Y; y++ { + for x := b.Min.X; x < b.Max.X; x++ { + if c := img.RGBAAt(x, y); c.R == 0xff && c.G == 0 && c.B == 0 { + p := image.Rect(x, y, x+1, y+1) + if first { + r, first = p, false + } else { + r = r.Union(p) + } + } + } + } + return r +} + +// TestSVGWidthHeightAttributes: on an outermost , width and height are +// presentation attributes for the CSS width/height properties (SVG 2 §7.8), +// so they take any CSS length — %, em, rem, px — not just a bare number. +func TestSVGWidthHeightAttributes(t *testing.T) { + const rect = `viewBox="0 0 100 100">` + page := func(body string) string { + return `` + body + `` + } + for _, tc := range []struct { + name, body string + w, h int + }{ + {"percent in a sized box", ``, 40, 40}, + {"em", `