Commit 90fb344898

90fb3448988db9d64cc321b6d556f99dd7fbdea2

parent: 19bbcd6de2

Verified · cmc

cmc <hello@cleberg.net> · 2026-08-31 23:09 UTC

Point instance badge sources at the PNG

No Apple image decoder reads SVG outside an asset catalog, so the SVG
badge a README names cannot render at all. Badge URLs on the instance
being browsed are rewritten to the PNG the forge also serves; badges
from anywhere else have no PNG to offer and are left as authored.

Layout: unified · split

gitbay/Views/Repos/ReadmeView.swift +20 −1
@@ -22,7 +22,7 @@ struct ReadmeView: View {
2222 if isOrg {
2323 // Qualified: `OrgView` here is org-mode, not the organization view.
2424 OrgSwiftUI.OrgView(
25 content,
25 Self.preferringPNGBadges(content, host: host),
2626 options: orgOptions,
2727 styler: OrgSyntaxStyler(colorScheme: colorScheme),
2828 // org's TODO/DONE are the same open/done distinction the forge already
@@ -34,6 +34,25 @@ struct ReadmeView: View {
3434 }
3535 }
3636
37 /// Point badge sources on this instance at the PNG the forge also serves.
38 ///
39 /// No Apple image decoder reads SVG outside an asset catalog, so the SVG badge a README
40 /// names cannot render at all. Only badges on the instance being browsed are rewritten —
41 /// a badge from anywhere else has no PNG to offer, and is left as authored.
42 static func preferringPNGBadges(_ content: String, host: String) -> String {
43 guard !host.isEmpty,
44 let pattern = try? NSRegularExpression(
45 pattern: "(https://\(NSRegularExpression.escapedPattern(for: host))"
46 + "/[^/\\s\\]]+/[^/\\s\\]]+/badge/[\\w.-]+)\\.svg"
47 )
48 else { return content }
49 return pattern.stringByReplacingMatches(
50 in: content,
51 range: NSRange(content.startIndex..., in: content),
52 withTemplate: "$1.png"
53 )
54 }
55
3756 /// gitbay serves raw bytes at `/{owner}/{repo}/raw/{ref}/…` and the file page at
3857 /// `/blob/…`, so relative images resolve against `raw` and links against `blob`.
3958 private var orgOptions: OrgRenderOptions {
gitbayTests/ReadmeBadgeTests.swift added +43
@@ -0,0 +1,43 @@
1import Testing
2@testable import gitbay
3
4@MainActor
5struct ReadmeBadgeSourceTests {
6
7 private func rewrite(_ content: String, host: String = "gitbay.org") -> String {
8 ReadmeView.preferringPNGBadges(content, host: host)
9 }
10
11 @Test func aBadgeOnThisInstancePointsAtThePNG() {
12 #expect(rewrite("[[https://gitbay.org/krz/gitbay/builds][file:https://gitbay.org/krz/gitbay/badge/build.svg]]")
13 == "[[https://gitbay.org/krz/gitbay/builds][file:https://gitbay.org/krz/gitbay/badge/build.png]]")
14 }
15
16 @Test func aBadgeForAnotherRepoOnTheSameInstanceIsRewritten() {
17 #expect(rewrite("file:https://gitbay.org/krz/orgo/badge/build.svg")
18 == "file:https://gitbay.org/krz/orgo/badge/build.png")
19 }
20
21 /// Somewhere else's badge has no PNG to offer, so it is left as authored.
22 @Test func aBadgeOnAnotherHostIsLeftAlone() {
23 let shields = "file:https://img.shields.io/badge/build-passing.svg"
24 #expect(rewrite(shields) == shields)
25 }
26
27 /// Only the badge path is rewritten; an SVG committed to the repo is not a badge.
28 @Test func anSVGThatIsNotABadgeIsLeftAlone() {
29 let diagram = "file:https://gitbay.org/krz/gitbay/raw/HEAD/docs/diagram.svg"
30 #expect(rewrite(diagram) == diagram)
31 }
32
33 @Test func anEmptyHostRewritesNothing() {
34 let badge = "file:https://gitbay.org/krz/gitbay/badge/build.svg"
35 #expect(rewrite(badge, host: "") == badge)
36 }
37
38 /// The host is matched literally: its dots are not wildcards.
39 @Test func theHostIsNotTreatedAsAPattern() {
40 let impostor = "file:https://gitbayxorg/krz/gitbay/badge/build.svg"
41 #expect(rewrite(impostor) == impostor)
42 }
43}