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",