1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
|
package showcase
import (
"fmt"
"sort"
"strings"
"time"
)
type showcaseOverviewStats struct {
totalProjects int
totalCommits int
totalLOC int
totalDocs int
releasedCount int
languageStats []LanguageStats
docStats []LanguageStats
}
// formatGemtext formats the summaries as Gemini Gemtext.
func (g *Generator) formatGemtext(summaries []ProjectSummary) string {
var builder strings.Builder
writeGemtextHeader(&builder)
stats := collectShowcaseOverviewStats(summaries)
writeShowcaseOverviewStats(&builder, stats)
g.writeProjectsGemtext(&builder, summaries)
return builder.String()
}
func writeGemtextHeader(builder *strings.Builder) {
// Header
builder.WriteString("# Project Showcase\n\n")
// Generated date at the top
builder.WriteString(fmt.Sprintf("Generated on: %s\n\n", time.Now().Format("2006-01-02")))
// Link to the interactive SVG rank history graph โ placed at the very top
// so it is the first thing a reader sees after the date line.
builder.WriteString("=> showcase-rank-history.svg Interactive Project Rank History Graph (SVG)\n\n")
// Introduction paragraph
builder.WriteString("This page showcases my side projects, providing an overview of what each project does, its technical implementation, and key metrics. Each project summary includes information about the programming languages used, development activity, releases, and licensing. The projects are ranked by score, which combines recent activity, project size, tag history, and whether the project has shipped a release.\n\n")
// Template inline TOC
builder.WriteString("<< template::inline::toc\n\n")
}
func collectShowcaseOverviewStats(summaries []ProjectSummary) showcaseOverviewStats {
stats := showcaseOverviewStats{
totalProjects: len(summaries),
}
languageTotals := make(map[string]int)
docTotals := make(map[string]int)
for _, summary := range summaries {
if summary.Metadata == nil {
continue
}
stats.totalCommits += summary.Metadata.CommitCount
stats.totalLOC += summary.Metadata.LinesOfCode
stats.totalDocs += summary.Metadata.LinesOfDocs
if summary.Metadata.HasReleases {
stats.releasedCount++
}
for _, lang := range summary.Metadata.Languages {
languageTotals[lang.Name] += lang.Lines
}
for _, doc := range summary.Metadata.Documentation {
docTotals[doc.Name] += doc.Lines
}
}
stats.languageStats = buildSortedPercentageStats(languageTotals, stats.totalLOC)
stats.docStats = buildSortedPercentageStats(docTotals, stats.totalDocs)
return stats
}
func buildSortedPercentageStats(lineTotalsByName map[string]int, totalLines int) []LanguageStats {
stats := make([]LanguageStats, 0, len(lineTotalsByName))
for name, lines := range lineTotalsByName {
percentage := 0.0
if totalLines > 0 {
percentage = float64(lines) * 100.0 / float64(totalLines)
}
stats = append(stats, LanguageStats{
Name: name,
Lines: lines,
Percentage: percentage,
})
}
sort.Slice(stats, func(i, j int) bool {
return stats[i].Percentage > stats[j].Percentage
})
return stats
}
func writeShowcaseOverviewStats(builder *strings.Builder, stats showcaseOverviewStats) {
builder.WriteString("## Overall Statistics\n\n")
builder.WriteString(fmt.Sprintf("* ๐ฆ Total Projects: %d\n", stats.totalProjects))
builder.WriteString(fmt.Sprintf("* ๐ Total Commits: %s\n", formatNumber(stats.totalCommits)))
builder.WriteString(fmt.Sprintf("* ๐ Total Lines of Code: %s\n", formatNumber(stats.totalLOC)))
if stats.totalDocs > 0 {
builder.WriteString(fmt.Sprintf("* ๐ Total Lines of Documentation: %s\n", formatNumber(stats.totalDocs)))
}
if len(stats.languageStats) > 0 {
builder.WriteString(fmt.Sprintf("* ๐ป Languages: %s\n", FormatLanguagesWithPercentages(stats.languageStats)))
}
if len(stats.docStats) > 0 {
builder.WriteString(fmt.Sprintf("* ๐ Documentation: %s\n", FormatLanguagesWithPercentages(stats.docStats)))
}
experimentalCount, releasedPercentage, experimentalPercentage := releaseStatusBreakdown(stats.totalProjects, stats.releasedCount)
builder.WriteString(fmt.Sprintf("* ๐ Release Status: %d released, %d experimental (%.1f%% with releases, %.1f%% experimental)\n",
stats.releasedCount, experimentalCount, releasedPercentage, experimentalPercentage))
builder.WriteString("\n")
}
func releaseStatusBreakdown(totalProjects, releasedCount int) (experimentalCount int, releasedPercentage, experimentalPercentage float64) {
experimentalCount = totalProjects - releasedCount
if totalProjects == 0 {
return experimentalCount, 0, 0
}
releasedPercentage = float64(releasedCount) * 100 / float64(totalProjects)
experimentalPercentage = float64(experimentalCount) * 100 / float64(totalProjects)
return experimentalCount, releasedPercentage, experimentalPercentage
}
func (g *Generator) writeProjectsGemtext(builder *strings.Builder, summaries []ProjectSummary) {
builder.WriteString("## Projects\n\n")
for i, summary := range summaries {
if i > 0 {
builder.WriteString("\n---\n\n")
}
g.writeProjectGemtext(builder, i, summary)
}
}
func (g *Generator) writeProjectGemtext(builder *strings.Builder, index int, summary ProjectSummary) {
builder.WriteString(fmt.Sprintf("### %d. %s%s\n\n", index+1, summary.Name, formatRankHistoryForHeader(summary.RankHistory)))
writeProjectMetadata(builder, summary.Metadata)
writeProjectSummaryContent(builder, summary)
writeProjectLinks(builder, summary)
}
func writeProjectMetadata(builder *strings.Builder, metadata *RepoMetadata) {
if metadata == nil {
return
}
if len(metadata.Languages) > 0 {
builder.WriteString(fmt.Sprintf("* ๐ป Languages: %s\n", FormatLanguagesWithPercentages(metadata.Languages)))
}
if len(metadata.Documentation) > 0 {
builder.WriteString(fmt.Sprintf("* ๐ Documentation: %s\n", FormatLanguagesWithPercentages(metadata.Documentation)))
}
builder.WriteString(fmt.Sprintf("* ๐ Commits: %d\n", metadata.CommitCount))
builder.WriteString(fmt.Sprintf("* ๐ Lines of Code: %d\n", metadata.LinesOfCode))
if metadata.LinesOfDocs > 0 {
builder.WriteString(fmt.Sprintf("* ๐ Lines of Documentation: %d\n", metadata.LinesOfDocs))
}
builder.WriteString(fmt.Sprintf("* ๐ท๏ธ Tags: %d\n", metadata.TagCount))
builder.WriteString(fmt.Sprintf("* ๐
Development Period: %s to %s\n", metadata.FirstCommitDate, metadata.LastCommitDate))
builder.WriteString(fmt.Sprintf("* ๐ Score: %.1f (combines recent activity, code size, tags, and release status)\n", metadata.Score))
builder.WriteString(fmt.Sprintf("* โ๏ธ License: %s\n", metadata.License))
// Add release information or experimental status.
if metadata.HasReleases && metadata.LatestTag != "" {
if metadata.LatestTagDate != "" {
builder.WriteString(fmt.Sprintf("* ๐ท๏ธ Latest Release: %s (%s)\n", metadata.LatestTag, metadata.LatestTagDate))
} else {
builder.WriteString(fmt.Sprintf("* ๐ท๏ธ Latest Release: %s\n", metadata.LatestTag))
}
} else {
builder.WriteString("* ๐งช Status: Experimental (no releases yet)\n")
}
// Mark as inactive when the average age of the last 42 commits exceeds
// 730 days (~2 years). A single recent commit (e.g. a deprecation
// notice) does not rescue a dormant project โ ~42 recent commits are
// required to move the average below the threshold. This matches the
// grey-line rule used in the interactive rank-history SVG.
if metadata.AvgCommitAge > 730 {
builder.WriteString("\nโ ๏ธ **Notice**: This project appears to be inactive or no longer maintained. The average age of its last 42 commits exceeds 2 years. Use at your own risk.")
}
builder.WriteString("\n\n")
}
func writeProjectSummaryContent(builder *strings.Builder, summary ProjectSummary) {
paragraphs := splitSummaryParagraphs(sanitizeSummaryForGemtext(summary.Summary))
if len(summary.Images) > 0 {
// First image after metadata, before text.
builder.WriteString(fmt.Sprintf("=> %s %s screenshot\n\n", summary.Images[0], summary.Name))
// First paragraph.
if len(paragraphs) > 0 {
builder.WriteString(fmt.Sprintf("%s\n\n", strings.TrimSpace(paragraphs[0])))
}
// Second image after first paragraph (if we have 2 images and multiple paragraphs).
if len(summary.Images) > 1 && len(paragraphs) > 1 {
builder.WriteString(fmt.Sprintf("=> %s %s screenshot\n\n", summary.Images[1], summary.Name))
}
// Remaining paragraphs.
for i := 1; i < len(paragraphs); i++ {
builder.WriteString(fmt.Sprintf("%s\n\n", strings.TrimSpace(paragraphs[i])))
}
return
}
// No images - just add all paragraphs.
for _, para := range paragraphs {
builder.WriteString(fmt.Sprintf("%s\n\n", strings.TrimSpace(para)))
}
}
func writeProjectLinks(builder *strings.Builder, summary ProjectSummary) {
if summary.CodebergURL != "" {
builder.WriteString(fmt.Sprintf("=> %s View on Codeberg\n", summary.CodebergURL))
}
if summary.GitHubURL != "" {
builder.WriteString(fmt.Sprintf("=> %s View on GitHub\n", summary.GitHubURL))
}
if summary.CgitURL != "" {
builder.WriteString(fmt.Sprintf("For cgit access go to c-git dot f3s dot buetow dot org slash %s\n", summary.Name))
}
}
|