Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions css/presentational.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,22 @@ func presentationalHints(n *dom.Node) []Declaration {
}
}

// An <svg>'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 <length-percentage> — 100%, 2em, 3rem — and auto (which
// keeps the intrinsic, viewBox-derived size). Mapped here so the cascade
// resolves them like any other length; nested <svg> 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})
Expand Down Expand Up @@ -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 <font size> 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
Expand Down
25 changes: 25 additions & 0 deletions css/presentational_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,31 @@ func TestFontSizeAttr(t *testing.T) {
}
}

// TestSVGLengthHint: an <svg>'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, `<html><body style="font-size:20px"><svg width="2em" height="50%"></svg></body></html>`, "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) {
Expand Down
59 changes: 59 additions & 0 deletions fixtures_svg_attr_size_test.go
Original file line number Diff line number Diff line change
@@ -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 <svg>, 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"><rect width="100" height="100" fill="#ff0000"/></svg>`
page := func(body string) string {
return `<!DOCTYPE html><html><head><style>html,body{margin:0}body{font-size:20px}</style></head><body>` + body + `</body></html>`
}
for _, tc := range []struct {
name, body string
w, h int
}{
{"percent in a sized box", `<span style="display:inline-block;width:2em;height:2em"><svg width="100%" height="100%" ` + rect + `</span>`, 40, 40},
{"em", `<svg style="display:block" width="2em" height="2em" ` + rect, 40, 40},
{"rem", `<svg style="display:block" width="3rem" height="1rem" preserveAspectRatio="none" ` + rect, 48, 16},
{"px unit", `<svg style="display:block" width="30px" height="30px" ` + rect, 30, 30},
{"bare number (unchanged)", `<svg style="display:block" width="40" height="40" ` + rect, 40, 40},
{"style wins over attribute", `<svg style="display:block;width:25px;height:25px" width="2em" height="2em" ` + rect, 25, 25},
{"auto keeps the viewBox size", `<svg style="display:block" width="auto" height="auto" ` + rect, 100, 100},
} {
t.Run(tc.name, func(t *testing.T) {
img := renderString(t, page(tc.body), 200, 200)
bb := redBBox(img)
if bb.Dx() != tc.w || bb.Dy() != tc.h {
t.Errorf("red box %v (%dx%d), want %dx%d", bb, bb.Dx(), bb.Dy(), tc.w, tc.h)
}
})
}
}
Loading