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/
diff --git a/pkg/ossstats/badge/badge.go b/pkg/ossstats/badge/badge.go
index ad3b64c..bdc2985 100644
--- a/pkg/ossstats/badge/badge.go
+++ b/pkg/ossstats/badge/badge.go
@@ -104,11 +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 },
+ "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)
@@ -205,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/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..5c884db 100644
--- a/pkg/ossstats/badge/badge_test.go
+++ b/pkg/ossstats/badge/badge_test.go
@@ -597,6 +597,196 @@ 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: "…",
+ },
+ }
+
+ 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
+ badgeVariant BadgeVariant
+ }{
+ {
+ name: "short names pass through unchanged",
+ repoName: "short-repo",
+ 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-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-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-repository-na…",
+ wantOwner: "@a-very-long-organization-na…",
+ badgeVariant: VariantDefault,
+ },
+ }
+
+ 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: 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)
+ }
+ if !strings.Contains(svg, tt.wantOwner) {
+ t.Errorf("SVG missing expected owner %q", tt.wantOwner)
+ }
+ })
+ }
+}
+
+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",