From 73dac1235a344ab563a821655ee159474bfc9553 Mon Sep 17 00:00:00 2001 From: MABD Date: Sun, 12 Apr 2026 11:44:42 +0300 Subject: [PATCH 1/4] truncate repo name + owner in detailed badge style --- pkg/ossstats/badge/badge.go | 7 + pkg/ossstats/badge/badgeTemplates/defaults.go | 4 +- .../badge/badgeTemplates/textBased.go | 2 +- pkg/ossstats/badge/badge_test.go | 149 ++++++++++++++++++ 4 files changed, 159 insertions(+), 3 deletions(-) diff --git a/pkg/ossstats/badge/badge.go b/pkg/ossstats/badge/badge.go index ad3b64c..d2be5e1 100644 --- a/pkg/ossstats/badge/badge.go +++ b/pkg/ossstats/badge/badge.go @@ -109,6 +109,13 @@ func RenderSVG(stats *ossstats.Stats, opts BadgeOptions) (string, error) { "mul": func(a, b int) int { return a * b }, "mod": func(a, b int) int { return a % b }, "div": func(a, b int) int { return a / b }, + "truncate": func(maxLen int, s string) string { + runes := []rune(s) + if len(runes) <= maxLen { + return s + } + return string(runes[:maxLen-1]) + "…" + }, }).Parse(tmplStr) if err != nil { return "", fmt.Errorf("failed to parse template: %w", err) diff --git a/pkg/ossstats/badge/badgeTemplates/defaults.go b/pkg/ossstats/badge/badgeTemplates/defaults.go index 4b8d8b0..c96e75d 100644 --- a/pkg/ossstats/badge/badgeTemplates/defaults.go +++ b/pkg/ossstats/badge/badgeTemplates/defaults.go @@ -262,7 +262,7 @@ const DefaultDetailed = ` font-size="16" font-weight="bold" class="repo-name" - letter-spacing="0em">{{$r.RepoName}} + letter-spacing="0em">{{truncate 26 $r.RepoName}} @{{$r.Owner}} + letter-spacing="0em">@{{truncate 28 $r.Owner}} - {{$r.RepoName}} + @{{truncate 62 $r.RepoName }} ★ {{$r.Stars}} diff --git a/pkg/ossstats/badge/badge_test.go b/pkg/ossstats/badge/badge_test.go index 489eb1d..91377de 100644 --- a/pkg/ossstats/badge/badge_test.go +++ b/pkg/ossstats/badge/badge_test.go @@ -597,6 +597,155 @@ func TestGetThemeColors(t *testing.T) { } } +func TestTruncate(t *testing.T) { + tests := []struct { + name string + maxLen int + input string + want string + }{ + { + name: "short string unchanged", + maxLen: 10, + input: "hello", + want: "hello", + }, + { + name: "exact length unchanged", + maxLen: 5, + input: "hello", + want: "hello", + }, + { + name: "over limit gets ellipsis", + maxLen: 5, + input: "hello world", + want: "hell…", + }, + { + name: "empty string unchanged", + maxLen: 10, + input: "", + want: "", + }, + { + name: "multibyte runes truncated correctly", + maxLen: 5, + input: "日本語テスト長い名前", + want: "日本語テ…", + }, + { + name: "limit 1 yields only ellipsis", + maxLen: 1, + input: "ab", + want: "…", + }, + } + + // truncate is exercised via RenderSVG; test it inline with the same logic + truncate := func(maxLen int, s string) string { + runes := []rune(s) + if len(runes) <= maxLen { + return s + } + return string(runes[:maxLen-1]) + "…" + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := truncate(tt.maxLen, tt.input) + if got != tt.want { + t.Errorf("truncate(%d, %q) = %q, want %q", tt.maxLen, tt.input, got, tt.want) + } + }) + } +} + +func TestRenderSVG_DetailedTextTruncation(t *testing.T) { + tests := []struct { + name string + repoName string + owner string + wantRepoName string + wantOwner string + }{ + { + name: "short names pass through unchanged", + repoName: "short-repo", + owner: "shortowner", + wantRepoName: "short-repo", + wantOwner: "@shortowner", + }, + { + name: "long repo name is truncated", + repoName: "this-repo-name-is-way-too-long-to-fit", + owner: "owner", + wantRepoName: "this-repo-name-is-way…", + wantOwner: "@owner", + }, + { + name: "long owner name is truncated", + repoName: "repo", + owner: "this-owner-name-is-way-too-long-to-fit", + wantRepoName: "repo", + wantOwner: "@this-owner-name-is-way-to…", + }, + { + name: "both long names are truncated", + repoName: "a-very-long-repository-name-here", + owner: "a-very-long-organization-name-here", + wantRepoName: "a-very-long-repositor…", + wantOwner: "@a-very-long-organization-…", + }, + // --- FAIL: TestRenderSVG_DetailedTextTruncation/long_repo_name_is_truncated (0.00s) + // badge_test.go:733: SVG missing expected repo name "this-repo-name-is-way…" + // --- FAIL: TestRenderSVG_DetailedTextTruncation/long_owner_name_is_truncated (0.00s) + // badge_test.go:736: SVG missing expected owner "@this-owner-name-is-way-to…" + // --- FAIL: TestRenderSVG_DetailedTextTruncation/both_long_names_are_truncated (0.00s) + // badge_test.go:733: SVG missing expected repo name "a-very-long-repositor…" + // badge_test.go:736: SVG missing expected owner "@a-very-long-organization-…" + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stats := &ossstats.Stats{ + Username: "testuser", + Summary: ossstats.Summary{ + TotalProjects: 1, + TotalPRsMerged: 1, + }, + Contributions: []ossstats.Contribution{ + { + RepoName: tt.repoName, + Owner: tt.owner, + Stars: 100, + PRsMerged: 1, + }, + }, + } + + opts := BadgeOptions{ + Style: StyleDetailed, + Variant: VariantDefault, + Theme: ThemeGithubDark, + Limit: 1, + } + + svg, err := RenderSVG(stats, opts) + if err != nil { + t.Fatalf("RenderSVG() unexpected error: %v", err) + } + + if !strings.Contains(svg, tt.wantRepoName) { + t.Errorf("SVG missing expected repo name %q", tt.wantRepoName) + } + if !strings.Contains(svg, tt.wantOwner) { + t.Errorf("SVG missing expected owner %q", tt.wantOwner) + } + }) + } +} + func TestRenderSVG_CompactBadgeContent(t *testing.T) { stats := &ossstats.Stats{ Username: "testuser", From 0a733f395123c748da32d3ac130ec0e079f59fba Mon Sep 17 00:00:00 2001 From: MABD Date: Sun, 12 Apr 2026 11:57:27 +0300 Subject: [PATCH 2/4] made truncate function reusable in test and svg template --- pkg/ossstats/badge/badge.go | 26 ++++++++++++++------------ pkg/ossstats/badge/badge_test.go | 17 +---------------- 2 files changed, 15 insertions(+), 28 deletions(-) diff --git a/pkg/ossstats/badge/badge.go b/pkg/ossstats/badge/badge.go index d2be5e1..bdc2985 100644 --- a/pkg/ossstats/badge/badge.go +++ b/pkg/ossstats/badge/badge.go @@ -104,18 +104,12 @@ func RenderSVG(stats *ossstats.Stats, opts BadgeOptions) (string, error) { // Parse and execute template with custom functions tmpl, err := template.New("badge").Funcs(template.FuncMap{ - "add": func(a, b int) int { return a + b }, - "sub": func(a, b int) int { return a - b }, - "mul": func(a, b int) int { return a * b }, - "mod": func(a, b int) int { return a % b }, - "div": func(a, b int) int { return a / b }, - "truncate": func(maxLen int, s string) string { - runes := []rune(s) - if len(runes) <= maxLen { - return s - } - return string(runes[:maxLen-1]) + "…" - }, + "add": func(a, b int) int { return a + b }, + "sub": func(a, b int) int { return a - b }, + "mul": func(a, b int) int { return a * b }, + "mod": func(a, b int) int { return a % b }, + "div": func(a, b int) int { return a / b }, + "truncate": truncate, }).Parse(tmplStr) if err != nil { return "", fmt.Errorf("failed to parse template: %w", err) @@ -212,3 +206,11 @@ func getTemplateStr( err := fmt.Errorf("unsupported badge variant: %s, and style: %s combinations", variant, style) return "", err } + +func truncate(maxLen int, s string) string { + runes := []rune(s) + if len(runes) <= maxLen { + return s + } + return string(runes[:maxLen-1]) + "…" +} diff --git a/pkg/ossstats/badge/badge_test.go b/pkg/ossstats/badge/badge_test.go index 91377de..17b64d0 100644 --- a/pkg/ossstats/badge/badge_test.go +++ b/pkg/ossstats/badge/badge_test.go @@ -642,15 +642,6 @@ func TestTruncate(t *testing.T) { }, } - // truncate is exercised via RenderSVG; test it inline with the same logic - truncate := func(maxLen int, s string) string { - runes := []rune(s) - if len(runes) <= maxLen { - return s - } - return string(runes[:maxLen-1]) + "…" - } - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := truncate(tt.maxLen, tt.input) @@ -659,6 +650,7 @@ func TestTruncate(t *testing.T) { } }) } + } func TestRenderSVG_DetailedTextTruncation(t *testing.T) { @@ -697,13 +689,6 @@ func TestRenderSVG_DetailedTextTruncation(t *testing.T) { wantRepoName: "a-very-long-repositor…", wantOwner: "@a-very-long-organization-…", }, - // --- FAIL: TestRenderSVG_DetailedTextTruncation/long_repo_name_is_truncated (0.00s) - // badge_test.go:733: SVG missing expected repo name "this-repo-name-is-way…" - // --- FAIL: TestRenderSVG_DetailedTextTruncation/long_owner_name_is_truncated (0.00s) - // badge_test.go:736: SVG missing expected owner "@this-owner-name-is-way-to…" - // --- FAIL: TestRenderSVG_DetailedTextTruncation/both_long_names_are_truncated (0.00s) - // badge_test.go:733: SVG missing expected repo name "a-very-long-repositor…" - // badge_test.go:736: SVG missing expected owner "@a-very-long-organization-…" } for _, tt := range tests { From d16f09f9c615943e15fe6eb6e6faa026086429ae Mon Sep 17 00:00:00 2001 From: MABD Date: Sun, 12 Apr 2026 13:30:30 +0300 Subject: [PATCH 3/4] fix unit tests --- pkg/ossstats/badge/badge_test.go | 66 +++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/pkg/ossstats/badge/badge_test.go b/pkg/ossstats/badge/badge_test.go index 17b64d0..5c884db 100644 --- a/pkg/ossstats/badge/badge_test.go +++ b/pkg/ossstats/badge/badge_test.go @@ -660,6 +660,7 @@ func TestRenderSVG_DetailedTextTruncation(t *testing.T) { owner string wantRepoName string wantOwner string + badgeVariant BadgeVariant }{ { name: "short names pass through unchanged", @@ -667,27 +668,31 @@ func TestRenderSVG_DetailedTextTruncation(t *testing.T) { owner: "shortowner", wantRepoName: "short-repo", wantOwner: "@shortowner", + badgeVariant: VariantDefault, }, { name: "long repo name is truncated", repoName: "this-repo-name-is-way-too-long-to-fit", owner: "owner", - wantRepoName: "this-repo-name-is-way…", + wantRepoName: "this-repo-name-is-way-too…", wantOwner: "@owner", + badgeVariant: VariantDefault, }, { name: "long owner name is truncated", repoName: "repo", owner: "this-owner-name-is-way-too-long-to-fit", wantRepoName: "repo", - wantOwner: "@this-owner-name-is-way-to…", + wantOwner: "@this-owner-name-is-way-too-…", + badgeVariant: VariantDefault, }, { name: "both long names are truncated", repoName: "a-very-long-repository-name-here", owner: "a-very-long-organization-name-here", - wantRepoName: "a-very-long-repositor…", - wantOwner: "@a-very-long-organization-…", + wantRepoName: "a-very-long-repository-na…", + wantOwner: "@a-very-long-organization-na…", + badgeVariant: VariantDefault, }, } @@ -711,7 +716,7 @@ func TestRenderSVG_DetailedTextTruncation(t *testing.T) { opts := BadgeOptions{ Style: StyleDetailed, - Variant: VariantDefault, + Variant: tt.badgeVariant, Theme: ThemeGithubDark, Limit: 1, } @@ -731,6 +736,57 @@ func TestRenderSVG_DetailedTextTruncation(t *testing.T) { } } +func TestRenderSVG_TextBasedDetailedTextTruncation(t *testing.T) { + tests := []struct { + name string + repoName string + wantRepoName string + badgeVariant BadgeVariant + }{ + { + name: "both long names are truncated", + repoName: "a-veryyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy-very-very-very-long-repository-name-here", + wantRepoName: "a-veryyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy-very-very-very-long-rep…", + badgeVariant: VariantTextBased, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + stats := &ossstats.Stats{ + Username: "testuser", + Summary: ossstats.Summary{ + TotalProjects: 1, + TotalPRsMerged: 1, + }, + Contributions: []ossstats.Contribution{ + { + RepoName: tt.repoName, + Stars: 100, + PRsMerged: 1, + }, + }, + } + + opts := BadgeOptions{ + Style: StyleDetailed, + Variant: tt.badgeVariant, + Theme: ThemeGithubDark, + Limit: 1, + } + + svg, err := RenderSVG(stats, opts) + if err != nil { + t.Fatalf("RenderSVG() unexpected error: %v", err) + } + + if !strings.Contains(svg, tt.wantRepoName) { + t.Errorf("SVG missing expected repo name %q", tt.wantRepoName) + } + }) + } +} + func TestRenderSVG_CompactBadgeContent(t *testing.T) { stats := &ossstats.Stats{ Username: "testuser", From 4c3bb4268950c5be1489dae6208bd04a36cfff3a Mon Sep 17 00:00:00 2001 From: MABD Date: Sun, 12 Apr 2026 13:31:18 +0300 Subject: [PATCH 4/4] updated gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 3ef7b65..005c643 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ .claude .serena +.mcp.json commands.nu /gh-oss-stats scripts/