Skip to content

Commit 570e39d

Browse files
authored
Centre star pills with align=absmiddle (#73)
WebKit maps align=middle to baseline-middle, which drops the pill below the text on GitHub; absmiddle maps to vertical-align: middle in WebKit, Blink and Gecko alike. With that the 4px padding is no longer needed, so the pill is 20px tall again.
1 parent 9ac6557 commit 570e39d

4 files changed

Lines changed: 14 additions & 21 deletions

File tree

‎internal/badge/badge.go‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -181,26 +181,17 @@ func textWidth(s string) int {
181181
// up regardless of how many digits the count has.
182182
const StarsWidth = 46
183183

184-
// The pill box is 20px tall like the badge, but the SVG is 24px: the 4px of
185-
// transparent space below the box shifts it up 2px when GitHub centres the
186-
// image on the x-height of the line, which is where the mixed-case text
187-
// around it visually sits.
188-
const (
189-
starsBoxHeight = 20
190-
StarsHeight = 24
191-
)
192-
193184
// Stars renders the star pill that precedes each entry in the README: the
194185
// count in a fixed-width rounded box, in the same style as the badge, so the
195186
// list reads as a single column of numbers.
196187
func Stars(text, title string) []byte {
197188
var b strings.Builder
198189
w := func(format string, args ...any) { fmt.Fprintf(&b, format, args...) }
199-
w(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" role="img" aria-label="%s">`, StarsWidth, StarsHeight, html.EscapeString(title))
190+
w(`<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="20" role="img" aria-label="%s">`, StarsWidth, html.EscapeString(title))
200191
w(`<title>%s</title>`, html.EscapeString(title))
201192
w(`<linearGradient id="s" x2="0" y2="100%%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient>`)
202-
w(`<clipPath id="r"><rect width="%d" height="%d" rx="3" fill="#fff"/></clipPath>`, StarsWidth, starsBoxHeight)
203-
w(`<g clip-path="url(#r)"><rect width="%d" height="%d" fill="#555"/><rect width="%d" height="%d" fill="url(#s)"/></g>`, StarsWidth, starsBoxHeight, StarsWidth, starsBoxHeight)
193+
w(`<clipPath id="r"><rect width="%d" height="20" rx="3" fill="#fff"/></clipPath>`, StarsWidth)
194+
w(`<g clip-path="url(#r)"><rect width="%d" height="20" fill="#555"/><rect width="%d" height="20" fill="url(#s)"/></g>`, StarsWidth, StarsWidth)
204195
w(`<g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" font-size="11">`)
205196
cx := StarsWidth / 2
206197
t := html.EscapeString(text)

‎internal/badge/badge_test.go‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,8 @@ func TestColorIsStableAndVaried(t *testing.T) {
106106
func TestStarsPillHasFixedWidth(t *testing.T) {
107107
for _, text := range []string{"120", "2.1k", "89k", "new"} {
108108
svg := string(Stars(text, "x has stars"))
109-
if !strings.Contains(svg, `width="46" height="24"`) {
110-
t.Errorf("Stars(%q) must be %dx%d:\n%s", text, StarsWidth, StarsHeight, svg)
109+
if !strings.Contains(svg, `width="46" height="20"`) {
110+
t.Errorf("Stars(%q) must be %d wide and 20 tall:\n%s", text, StarsWidth, svg)
111111
}
112112
if !strings.Contains(svg, "★</tspan> "+text+"</text>") {
113113
t.Errorf("Stars(%q) must show the count:\n%s", text, svg)

‎internal/render/readme.go‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,14 +160,16 @@ func README(l *list.List, m *list.Metadata, now time.Time) []byte {
160160
// item renders one entry line. Every line starts with the entry's star pill,
161161
// a fixed-width image, so the names line up in one column. It is an HTML img
162162
// rather than Markdown because GitHub puts Markdown images on the text
163-
// baseline, which leaves a 20px pill floating above the line; align="middle"
164-
// is the one alignment attribute GitHub's sanitizer keeps.
163+
// baseline, which leaves a 20px pill floating above the line. GitHub's
164+
// sanitizer keeps the align attribute; "absmiddle" is the value every engine
165+
// maps to vertical-align: middle, whereas WebKit treats "middle" as
166+
// baseline-middle and drops the pill below the text.
165167
func item(r ranked) string {
166168
alt := "new"
167169
if r.known {
168170
alt = FormatStars(r.stars) + " stars"
169171
}
170-
return fmt.Sprintf(`<img src="%s" alt="%s" align="middle"> [%s](%s) - %s`, StarsPath(r.entry.Repo), alt, r.entry.DisplayName(), r.entry.URL(), r.entry.Description)
172+
return fmt.Sprintf(`<img src="%s" alt="%s" align="absmiddle"> [%s](%s) - %s`, StarsPath(r.entry.Repo), alt, r.entry.DisplayName(), r.entry.URL(), r.entry.Description)
171173
}
172174

173175
// FormatStars renders 1234 as "1.2k" and 80123 as "80k".

‎internal/render/readme_test.go‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,9 @@ func TestREADMEOrderAndPodium(t *testing.T) {
3838
"# Awesome Go\n\n> Curated.\n",
3939
"## Contents\n\n- [Web & HTTP](#web--http)\n",
4040
"## Web & HTTP\n\nServers.\n\n",
41-
"- <img src=\"badges/stars/b--big.svg\" alt=\"46k stars\" align=\"middle\"> [big](https://github.com/b/big) - Big.\n",
42-
"- <img src=\"badges/stars/c--mid.svg\" alt=\"2.1k stars\" align=\"middle\"> [middle](https://github.com/c/mid) - Mid.\n",
43-
"<summary>More (2)</summary>\n\n- <img src=\"badges/stars/a--small.svg\" alt=\"120 stars\" align=\"middle\"> [small](https://github.com/a/small) - Small.\n- <img src=\"badges/stars/d--unknown.svg\" alt=\"new\" align=\"middle\"> [unknown](https://github.com/d/unknown) - Unknown.\n",
41+
"- <img src=\"badges/stars/b--big.svg\" alt=\"46k stars\" align=\"absmiddle\"> [big](https://github.com/b/big) - Big.\n",
42+
"- <img src=\"badges/stars/c--mid.svg\" alt=\"2.1k stars\" align=\"absmiddle\"> [middle](https://github.com/c/mid) - Mid.\n",
43+
"<summary>More (2)</summary>\n\n- <img src=\"badges/stars/a--small.svg\" alt=\"120 stars\" align=\"absmiddle\"> [small](https://github.com/a/small) - Small.\n- <img src=\"badges/stars/d--unknown.svg\" alt=\"new\" align=\"absmiddle\"> [unknown](https://github.com/d/unknown) - Unknown.\n",
4444
"last refreshed on 2026-09-12",
4545
"An entry earns its place by covering something no listed project does, or by doing it better.",
4646
"Search, filter and sort the full list at [x.github.io/y](https://x.github.io/y/).",
@@ -60,7 +60,7 @@ func TestREADMEOrderAndPodium(t *testing.T) {
6060
func TestREADMEPodiumWithoutMore(t *testing.T) {
6161
l, m := fixture(4)
6262
out := string(README(l, m, time.Now()))
63-
if !strings.Contains(out, "- <img src=\"badges/stars/a--small.svg\" alt=\"120 stars\" align=\"middle\"> [small](") || !strings.Contains(out, "- <img src=\"badges/stars/d--unknown.svg\" alt=\"new\" align=\"middle\"> [unknown](") {
63+
if !strings.Contains(out, "- <img src=\"badges/stars/a--small.svg\" alt=\"120 stars\" align=\"absmiddle\"> [small](") || !strings.Contains(out, "- <img src=\"badges/stars/d--unknown.svg\" alt=\"new\" align=\"absmiddle\"> [unknown](") {
6464
t.Fatalf("every podium entry must carry a star pill:\n%s", out)
6565
}
6666
if strings.Contains(out, "<details>") {

0 commit comments

Comments
 (0)