Point instance badge sources at the PNG !41

merged merged by cmc on 2026-08-31 23:28 UTC · krz/gitbay-ios:badge-png-source into main

4 files changed, +66 −4

Layout: unified · split

gitbay.xcodeproj/project.pbxproj +1 −1
@@ -591,7 +591,7 @@
591591 repositoryURL = "ssh://git@gitbay.org/krz/org-swift.git";
592592 requirement = {
593593 kind = upToNextMinorVersion;
594 minimumVersion = 0.3.1;
594 minimumVersion = 0.4.0;
595595 };
596596 };
597597 8B2898B0303FF3E5001BF44A /* XCRemoteSwiftPackageReference "Highlightr" */ = {
gitbay.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +2 −2
@@ -15,8 +15,8 @@
1515 "kind" : "remoteSourceControl",
1616 "location" : "ssh://git@gitbay.org/krz/org-swift.git",
1717 "state" : {
18 "revision" : "6e2ef80a16a9eb61bff7f9928c230f4c5dff8240",
19 "version" : "0.3.1"
18 "revision" : "a5dc89631cf65a183edae1bfa22ef147bd8f1686",
19 "version" : "0.4.0"
2020 }
2121 }
2222 ],
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}