mirror of
https://github.com/go-i2p/go-gh-page.git
synced 2025-07-02 21:41:55 -04:00
basic template
This commit is contained in:
160
cmd/github-site-gen/main.go
Normal file
160
cmd/github-site-gen/main.go
Normal file
@ -0,0 +1,160 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/go-gh-page/pkg/generator"
|
||||
"github.com/go-i2p/go-gh-page/pkg/git"
|
||||
"github.com/go-i2p/go-gh-page/pkg/templates"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Define command-line flags
|
||||
repoFlag := flag.String("repo", "", "GitHub repository in format 'owner/repo-name'")
|
||||
outputFlag := flag.String("output", "./output", "Output directory for generated site")
|
||||
branchFlag := flag.String("branch", "main", "Branch to use (default: main)")
|
||||
workDirFlag := flag.String("workdir", "", "Working directory for cloning (default: temporary directory)")
|
||||
githost := flag.String("githost", "github.com", "Git host (default: github.com)")
|
||||
mainTemplateOverride := flag.String("main-template", "", "Path to custom main template")
|
||||
docTemplateOverride := flag.String("doc-template", "", "Path to custom documentation template")
|
||||
styleTemplateOverride := flag.String("style-template", "", "Path to custom style template")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Validate repository flag
|
||||
if *repoFlag == "" {
|
||||
fmt.Println("Error: -repo flag is required (format: owner/repo-name)")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
repoParts := strings.Split(*repoFlag, "/")
|
||||
if len(repoParts) != 2 {
|
||||
fmt.Println("Error: -repo flag must be in format 'owner/repo-name'")
|
||||
flag.Usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
// if mainTemplateOverride is not empty, check if a file exists
|
||||
if *mainTemplateOverride != "" {
|
||||
if _, err := os.Stat(*mainTemplateOverride); os.IsNotExist(err) {
|
||||
fmt.Printf("Error: main template file %s does not exist\n", *mainTemplateOverride)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Printf("Using custom main template: %s\n", *mainTemplateOverride)
|
||||
// read the file in and override templates.MainTemplate
|
||||
data, err := os.ReadFile(*mainTemplateOverride)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: failed to read main template file %s: %v\n", *mainTemplateOverride, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
templates.MainTemplate = string(data)
|
||||
}
|
||||
}
|
||||
// if docTemplateOverride is not empty, check if a file exists
|
||||
if *docTemplateOverride != "" {
|
||||
if _, err := os.Stat(*docTemplateOverride); os.IsNotExist(err) {
|
||||
fmt.Printf("Error: doc template file %s does not exist\n", *docTemplateOverride)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Printf("Using custom docs template: %s\n", *docTemplateOverride)
|
||||
// read the file in and override templates.MainTemplate
|
||||
data, err := os.ReadFile(*docTemplateOverride)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: failed to read docs template file %s: %v\n", *docTemplateOverride, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
templates.DocTemplate = string(data)
|
||||
}
|
||||
}
|
||||
// if styleTemplateOverride is not empty, check if a file exists
|
||||
if *styleTemplateOverride != "" {
|
||||
if _, err := os.Stat(*styleTemplateOverride); os.IsNotExist(err) {
|
||||
fmt.Printf("Error: style template file %s does not exist\n", *styleTemplateOverride)
|
||||
os.Exit(1)
|
||||
} else {
|
||||
fmt.Printf("Using custom style template: %s\n", *styleTemplateOverride)
|
||||
// read the file in and override templates.MainTemplate
|
||||
data, err := os.ReadFile(*styleTemplateOverride)
|
||||
if err != nil {
|
||||
fmt.Printf("Error: failed to read style template file %s: %v\n", *styleTemplateOverride, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
templates.StyleTemplate = string(data)
|
||||
}
|
||||
}
|
||||
|
||||
owner, repo := repoParts[0], repoParts[1]
|
||||
repoURL := fmt.Sprintf("https://%s/%s/%s.git", *githost, owner, repo)
|
||||
|
||||
// Create output directory if it doesn't exist
|
||||
if err := os.MkdirAll(*outputFlag, 0755); err != nil {
|
||||
log.Fatalf("Failed to create output directory: %v", err)
|
||||
}
|
||||
|
||||
// Determine working directory
|
||||
workDir := *workDirFlag
|
||||
if workDir == "" {
|
||||
// Create temporary directory
|
||||
tempDir, err := os.MkdirTemp("", "github-site-gen-*")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to create temporary directory: %v", err)
|
||||
}
|
||||
workDir = tempDir
|
||||
defer os.RemoveAll(tempDir) // Clean up when done
|
||||
} else {
|
||||
// Ensure the specified work directory exists
|
||||
if err := os.MkdirAll(workDir, 0755); err != nil {
|
||||
log.Fatalf("Failed to create working directory: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
cloneDir := filepath.Join(workDir, repo)
|
||||
|
||||
// Clone the repository
|
||||
fmt.Printf("Cloning %s/%s into %s...\n", owner, repo, cloneDir)
|
||||
startTime := time.Now()
|
||||
gitRepo, err := git.CloneRepository(repoURL, cloneDir, *branchFlag)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to clone repository: %v", err)
|
||||
}
|
||||
fmt.Printf("Repository cloned in %.2f seconds\n", time.Since(startTime).Seconds())
|
||||
|
||||
// Get repository data
|
||||
repoData, err := git.GetRepositoryData(gitRepo, owner, repo, cloneDir)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to gather repository data: %v", err)
|
||||
}
|
||||
|
||||
// Create generator
|
||||
gen := generator.NewGenerator(repoData, *outputFlag)
|
||||
|
||||
// Generate site
|
||||
fmt.Println("Generating static site...")
|
||||
startGenTime := time.Now()
|
||||
result, err := gen.GenerateSite()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to generate site: %v", err)
|
||||
}
|
||||
|
||||
// Print summary
|
||||
fmt.Printf("\nRepository site for %s/%s successfully generated in %.2f seconds:\n",
|
||||
owner, repo, time.Since(startGenTime).Seconds())
|
||||
fmt.Printf("- Main page: %s\n", filepath.Join(*outputFlag, "index.html"))
|
||||
fmt.Printf("- Documentation pages: %d markdown files converted\n", result.DocsCount)
|
||||
|
||||
if result.ImagesCount > 0 {
|
||||
fmt.Printf("- Images directory: %s/images/\n", *outputFlag)
|
||||
}
|
||||
|
||||
fmt.Printf("\nSite structure:\n%s\n", result.SiteStructure)
|
||||
fmt.Printf("\nYou can open index.html directly in your browser\n")
|
||||
fmt.Printf("or deploy the entire directory to any static web host.\n")
|
||||
|
||||
fmt.Printf("\nTotal time: %.2f seconds\n", time.Since(startTime).Seconds())
|
||||
}
|
30
go.mod
Normal file
30
go.mod
Normal file
@ -0,0 +1,30 @@
|
||||
module github.com/go-i2p/go-gh-page
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
github.com/go-git/go-git/v5 v5.16.0
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b
|
||||
)
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/ProtonMail/go-crypto v1.1.6 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||
github.com/go-git/go-billy/v5 v5.6.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/pjbgf/sha1cd v0.3.2 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/skeema/knownhosts v1.3.1 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
)
|
104
go.sum
Normal file
104
go.sum
Normal file
@ -0,0 +1,104 @@
|
||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
|
||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||
github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=
|
||||
github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
|
||||
github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
|
||||
github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
|
||||
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
|
||||
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
|
||||
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM=
|
||||
github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.16.0 h1:k3kuOEpkc0DeY7xlL6NaaNg39xdgQbtH5mwCafHO9AQ=
|
||||
github.com/go-git/go-git/v5 v5.16.0/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
|
||||
github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b h1:EY/KpStFl60qA17CptGXhwfZ+k1sFNJIUNR8DdbcuUk=
|
||||
github.com/gomarkdown/markdown v0.0.0-20250311123330-531bef5e742b/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
|
||||
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
|
||||
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
|
||||
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
|
||||
github.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=
|
||||
github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=
|
||||
github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
|
||||
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
|
||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
|
||||
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
380
pkg/generator/generator.go
Normal file
380
pkg/generator/generator.go
Normal file
@ -0,0 +1,380 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/go-i2p/go-gh-page/pkg/git"
|
||||
"github.com/go-i2p/go-gh-page/pkg/templates"
|
||||
"github.com/go-i2p/go-gh-page/pkg/utils"
|
||||
|
||||
"github.com/gomarkdown/markdown"
|
||||
"github.com/gomarkdown/markdown/html"
|
||||
"github.com/gomarkdown/markdown/parser"
|
||||
)
|
||||
|
||||
// GenerationResult contains information about the generated site
|
||||
type GenerationResult struct {
|
||||
DocsCount int
|
||||
ImagesCount int
|
||||
SiteStructure string
|
||||
}
|
||||
|
||||
// Generator handles the site generation
|
||||
type Generator struct {
|
||||
repoData *git.RepositoryData
|
||||
outputDir string
|
||||
templateCache map[string]*template.Template
|
||||
}
|
||||
|
||||
// PageData contains the data passed to HTML templates
|
||||
type PageData struct {
|
||||
RepoOwner string
|
||||
RepoName string
|
||||
RepoFullName string
|
||||
Description string
|
||||
CommitCount int
|
||||
LastUpdate string
|
||||
License string
|
||||
RepoURL string
|
||||
|
||||
ReadmeHTML string
|
||||
Contributors []git.Contributor
|
||||
|
||||
// Navigation
|
||||
DocsPages []utils.DocPage
|
||||
|
||||
// Current page info
|
||||
CurrentPage string
|
||||
PageTitle string
|
||||
PageContent string
|
||||
|
||||
// Generation info
|
||||
GeneratedAt string
|
||||
}
|
||||
|
||||
// NewGenerator creates a new site generator
|
||||
func NewGenerator(repoData *git.RepositoryData, outputDir string) *Generator {
|
||||
return &Generator{
|
||||
repoData: repoData,
|
||||
outputDir: outputDir,
|
||||
templateCache: make(map[string]*template.Template),
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateSite generates the complete static site
|
||||
func (g *Generator) GenerateSite() (*GenerationResult, error) {
|
||||
result := &GenerationResult{}
|
||||
|
||||
// Create docs directory
|
||||
docsDir := filepath.Join(g.outputDir, "docs")
|
||||
if err := os.MkdirAll(docsDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create docs directory: %w", err)
|
||||
}
|
||||
|
||||
// Create image directory if needed
|
||||
imagesDir := filepath.Join(g.outputDir, "images")
|
||||
if err := os.MkdirAll(imagesDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create images directory: %w", err)
|
||||
}
|
||||
|
||||
// Parse all templates first
|
||||
if err := g.parseTemplates(); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse templates: %w", err)
|
||||
}
|
||||
|
||||
// Copy image files to output directory
|
||||
for relativePath, sourcePath := range g.repoData.ImageFiles {
|
||||
destPath := filepath.Join(g.outputDir, "images", filepath.Base(relativePath))
|
||||
if err := copyFile(sourcePath, destPath); err != nil {
|
||||
return nil, fmt.Errorf("failed to copy image %s: %w", relativePath, err)
|
||||
}
|
||||
result.ImagesCount++
|
||||
}
|
||||
|
||||
// Prepare the list of documentation pages for navigation
|
||||
var docsPages []utils.DocPage
|
||||
for path := range g.repoData.MarkdownFiles {
|
||||
// Skip README as it's on the main page
|
||||
if isReadmeFile(filepath.Base(path)) {
|
||||
continue
|
||||
}
|
||||
|
||||
title := utils.GetTitleFromMarkdown(g.repoData.MarkdownFiles[path])
|
||||
if title == "" {
|
||||
title = utils.PrettifyFilename(filepath.Base(path))
|
||||
}
|
||||
|
||||
outputPath := utils.GetOutputPath(path, "docs")
|
||||
docsPages = append(docsPages, utils.DocPage{
|
||||
Title: title,
|
||||
Path: outputPath,
|
||||
})
|
||||
}
|
||||
|
||||
// Sort docsPages by title for consistent navigation
|
||||
utils.SortDocPagesByTitle(docsPages)
|
||||
|
||||
// Generate main index page
|
||||
if err := g.generateMainPage(docsPages); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate main page: %w", err)
|
||||
}
|
||||
|
||||
// Generate documentation pages
|
||||
for path, content := range g.repoData.MarkdownFiles {
|
||||
// Skip README as it's on the main page
|
||||
if isReadmeFile(filepath.Base(path)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := g.generateDocPage(path, content, docsPages); err != nil {
|
||||
return nil, fmt.Errorf("failed to generate doc page for %s: %w", path, err)
|
||||
}
|
||||
|
||||
result.DocsCount++
|
||||
}
|
||||
|
||||
// Generate site structure summary
|
||||
var buffer bytes.Buffer
|
||||
buffer.WriteString(g.outputDir + "/\n")
|
||||
buffer.WriteString(" ├── index.html\n")
|
||||
buffer.WriteString(" ├── docs/\n")
|
||||
|
||||
if len(docsPages) > 0 {
|
||||
for i, page := range docsPages {
|
||||
prefix := " │ ├── "
|
||||
if i == len(docsPages)-1 {
|
||||
prefix = " │ └── "
|
||||
}
|
||||
buffer.WriteString(prefix + filepath.Base(page.Path) + "\n")
|
||||
}
|
||||
} else {
|
||||
buffer.WriteString(" │ └── (empty)\n")
|
||||
}
|
||||
|
||||
if result.ImagesCount > 0 {
|
||||
buffer.WriteString(" └── images/\n")
|
||||
buffer.WriteString(" └── ... (" + fmt.Sprintf("%d", result.ImagesCount) + " files)\n")
|
||||
} else {
|
||||
buffer.WriteString(" └── images/\n")
|
||||
buffer.WriteString(" └── (empty)\n")
|
||||
}
|
||||
|
||||
result.SiteStructure = buffer.String()
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// parseTemplates parses all the HTML templates
|
||||
func (g *Generator) parseTemplates() error {
|
||||
// Parse main template
|
||||
mainTmpl, err := template.New("main").Parse(templates.MainTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse main template: %w", err)
|
||||
}
|
||||
g.templateCache["main"] = mainTmpl
|
||||
|
||||
// Parse documentation template
|
||||
docTmpl, err := template.New("doc").Parse(templates.DocTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse doc template: %w", err)
|
||||
}
|
||||
g.templateCache["doc"] = docTmpl
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateMainPage creates the main index.html
|
||||
func (g *Generator) generateMainPage(docsPages []utils.DocPage) error {
|
||||
// Prepare data for template
|
||||
data := PageData{
|
||||
RepoOwner: g.repoData.Owner,
|
||||
RepoName: g.repoData.Name,
|
||||
RepoFullName: g.repoData.Owner + "/" + g.repoData.Name,
|
||||
Description: g.repoData.Description,
|
||||
CommitCount: g.repoData.CommitCount,
|
||||
License: g.repoData.License,
|
||||
RepoURL: g.repoData.URL,
|
||||
LastUpdate: g.repoData.LastCommitDate.Format("January 2, 2006"),
|
||||
|
||||
ReadmeHTML: renderMarkdown(g.repoData.ReadmeContent),
|
||||
Contributors: g.repoData.Contributors,
|
||||
|
||||
DocsPages: docsPages,
|
||||
CurrentPage: "index.html",
|
||||
PageTitle: g.repoData.Owner + "/" + g.repoData.Name,
|
||||
|
||||
GeneratedAt: time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
// Render template
|
||||
var buf bytes.Buffer
|
||||
if err := g.templateCache["main"].Execute(&buf, data); err != nil {
|
||||
return fmt.Errorf("failed to execute main template: %w", err)
|
||||
}
|
||||
|
||||
// Write to file
|
||||
outputPath := filepath.Join(g.outputDir, "index.html")
|
||||
if err := os.WriteFile(outputPath, buf.Bytes(), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write index.html: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateDocPage creates an HTML page for a markdown file
|
||||
func (g *Generator) generateDocPage(path, content string, docsPages []utils.DocPage) error {
|
||||
// Get the title from the markdown content
|
||||
title := utils.GetTitleFromMarkdown(content)
|
||||
if title == "" {
|
||||
title = utils.PrettifyFilename(filepath.Base(path))
|
||||
}
|
||||
|
||||
// Process relative links in the markdown
|
||||
processedContent := utils.ProcessRelativeLinks(content, path, g.repoData.Owner, g.repoData.Name)
|
||||
|
||||
// Process image links to point to our local images
|
||||
processedContent = processImageLinks(processedContent, path)
|
||||
|
||||
// Render markdown to HTML
|
||||
contentHTML := renderMarkdown(processedContent)
|
||||
|
||||
// Create a copy of docsPages with current page marked as active
|
||||
currentDocsPages := make([]utils.DocPage, len(docsPages))
|
||||
copy(currentDocsPages, docsPages)
|
||||
outputPath := utils.GetOutputPath(path, "docs")
|
||||
|
||||
for i := range currentDocsPages {
|
||||
if currentDocsPages[i].Path == outputPath {
|
||||
currentDocsPages[i].IsActive = true
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare data for template
|
||||
data := PageData{
|
||||
RepoOwner: g.repoData.Owner,
|
||||
RepoName: g.repoData.Name,
|
||||
RepoFullName: g.repoData.Owner + "/" + g.repoData.Name,
|
||||
Description: g.repoData.Description,
|
||||
CommitCount: g.repoData.CommitCount,
|
||||
License: g.repoData.License,
|
||||
RepoURL: g.repoData.URL,
|
||||
LastUpdate: g.repoData.LastCommitDate.Format("January 2, 2006"),
|
||||
|
||||
DocsPages: currentDocsPages,
|
||||
CurrentPage: outputPath,
|
||||
PageTitle: title + " - " + g.repoData.Owner + "/" + g.repoData.Name,
|
||||
PageContent: contentHTML,
|
||||
|
||||
GeneratedAt: time.Now().Format("2006-01-02 15:04:05"),
|
||||
}
|
||||
|
||||
// Render template
|
||||
var buf bytes.Buffer
|
||||
if err := g.templateCache["doc"].Execute(&buf, data); err != nil {
|
||||
return fmt.Errorf("failed to execute doc template: %w", err)
|
||||
}
|
||||
|
||||
// Ensure output directory exists
|
||||
outPath := filepath.Join(g.outputDir, outputPath)
|
||||
if err := os.MkdirAll(filepath.Dir(outPath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create directory for %s: %w", outPath, err)
|
||||
}
|
||||
|
||||
// Write to file
|
||||
if err := os.WriteFile(outPath, buf.Bytes(), 0644); err != nil {
|
||||
return fmt.Errorf("failed to write %s: %w", outPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isReadmeFile checks if a file is a README
|
||||
func isReadmeFile(filename string) bool {
|
||||
lowerFilename := strings.ToLower(filename)
|
||||
return strings.HasPrefix(lowerFilename, "readme.")
|
||||
}
|
||||
|
||||
// renderMarkdown converts markdown content to HTML
|
||||
func renderMarkdown(md string) string {
|
||||
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock
|
||||
p := parser.NewWithExtensions(extensions)
|
||||
doc := p.Parse([]byte(md))
|
||||
|
||||
htmlFlags := html.CommonFlags | html.HrefTargetBlank
|
||||
opts := html.RendererOptions{Flags: htmlFlags}
|
||||
renderer := html.NewRenderer(opts)
|
||||
|
||||
return string(markdown.Render(doc, renderer))
|
||||
}
|
||||
|
||||
// processImageLinks updates image links to point to our local images
|
||||
func processImageLinks(content, filePath string) string {
|
||||
// Replace image links with links to our local images directory
|
||||
re := utils.GetImageLinkRegex()
|
||||
|
||||
baseDir := filepath.Dir(filePath)
|
||||
|
||||
return re.ReplaceAllStringFunc(content, func(match string) string {
|
||||
submatch := re.FindStringSubmatch(match)
|
||||
if len(submatch) < 3 {
|
||||
return match
|
||||
}
|
||||
|
||||
altText := submatch[1]
|
||||
imagePath := submatch[2]
|
||||
|
||||
// Skip absolute URLs
|
||||
if strings.HasPrefix(imagePath, "http") {
|
||||
return match
|
||||
}
|
||||
|
||||
// Make the path relative to the root
|
||||
if !strings.HasPrefix(imagePath, "/") {
|
||||
// Handle ./image.jpg style paths
|
||||
if strings.HasPrefix(imagePath, "./") {
|
||||
imagePath = imagePath[2:]
|
||||
}
|
||||
|
||||
// If in a subdirectory, make path relative to root
|
||||
if baseDir != "." {
|
||||
imagePath = filepath.Join(baseDir, imagePath)
|
||||
}
|
||||
} else {
|
||||
// Remove leading slash if any
|
||||
imagePath = strings.TrimPrefix(imagePath, "/")
|
||||
}
|
||||
|
||||
// Create a path to our local images directory
|
||||
localPath := "../images/" + filepath.Base(imagePath)
|
||||
|
||||
return fmt.Sprintf("", altText, localPath)
|
||||
})
|
||||
}
|
||||
|
||||
// copyFile copies a file from src to dst
|
||||
func copyFile(src, dst string) error {
|
||||
// Open source file
|
||||
sourceFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
// Create destination file
|
||||
destFile, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
// Copy the contents
|
||||
_, err = io.Copy(destFile, sourceFile)
|
||||
return err
|
||||
}
|
350
pkg/git/git.go
Normal file
350
pkg/git/git.go
Normal file
@ -0,0 +1,350 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/plumbing/object"
|
||||
)
|
||||
|
||||
// RepositoryData contains all the information about a repository
|
||||
type RepositoryData struct {
|
||||
Owner string
|
||||
Name string
|
||||
Description string
|
||||
URL string
|
||||
|
||||
// Content
|
||||
ReadmeContent string
|
||||
ReadmePath string
|
||||
MarkdownFiles map[string]string // path -> content
|
||||
|
||||
// Stats from git
|
||||
Contributors []Contributor
|
||||
CommitCount int
|
||||
LastCommitDate time.Time
|
||||
|
||||
// License information if available
|
||||
License string
|
||||
|
||||
// Set of image paths in the repository (to copy to output)
|
||||
ImageFiles map[string]string // path -> full path on disk
|
||||
}
|
||||
|
||||
// Contributor represents a repository contributor
|
||||
type Contributor struct {
|
||||
Name string
|
||||
Email string
|
||||
Commits int
|
||||
AvatarURL string
|
||||
}
|
||||
|
||||
// CloneRepository clones a Git repository to the specified directory
|
||||
func CloneRepository(url, destination, branch string) (*git.Repository, error) {
|
||||
// Check if repository already exists
|
||||
if _, err := os.Stat(destination); err == nil {
|
||||
// Directory exists, try to open repository
|
||||
repo, err := git.PlainOpen(destination)
|
||||
if err == nil {
|
||||
fmt.Println("Using existing repository clone")
|
||||
return repo, nil
|
||||
}
|
||||
// If error, remove directory and clone fresh
|
||||
os.RemoveAll(destination)
|
||||
}
|
||||
|
||||
// Clone options
|
||||
options := &git.CloneOptions{
|
||||
URL: url,
|
||||
}
|
||||
|
||||
// Set branch if not default
|
||||
if branch != "main" && branch != "master" {
|
||||
options.ReferenceName = plumbing.NewBranchReferenceName(branch)
|
||||
}
|
||||
|
||||
// Clone the repository
|
||||
return git.PlainClone(destination, false, options)
|
||||
}
|
||||
|
||||
// GetRepositoryData extracts information from a cloned repository
|
||||
func GetRepositoryData(repo *git.Repository, owner, name, repoPath string) (*RepositoryData, error) {
|
||||
repoData := &RepositoryData{
|
||||
Owner: owner,
|
||||
Name: name,
|
||||
URL: fmt.Sprintf("https://github.com/%s/%s", owner, name),
|
||||
MarkdownFiles: make(map[string]string),
|
||||
ImageFiles: make(map[string]string),
|
||||
}
|
||||
|
||||
// Get the repository description from the repository
|
||||
config, err := repo.Config()
|
||||
if err == nil && config != nil {
|
||||
repoData.Description = config.Raw.Section("").Option("description")
|
||||
}
|
||||
|
||||
// Get HEAD reference
|
||||
ref, err := repo.Head()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get HEAD reference: %w", err)
|
||||
}
|
||||
|
||||
// Get commit history
|
||||
cIter, err := repo.Log(&git.LogOptions{From: ref.Hash()})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get commit history: %w", err)
|
||||
}
|
||||
|
||||
// Process commits
|
||||
contributors := make(map[string]*Contributor)
|
||||
err = cIter.ForEach(func(c *object.Commit) error {
|
||||
// Count commits
|
||||
repoData.CommitCount++
|
||||
|
||||
// Update last commit date if needed
|
||||
if repoData.LastCommitDate.IsZero() || c.Author.When.After(repoData.LastCommitDate) {
|
||||
repoData.LastCommitDate = c.Author.When
|
||||
}
|
||||
|
||||
// Track contributors
|
||||
email := c.Author.Email
|
||||
if _, exists := contributors[email]; !exists {
|
||||
contributors[email] = &Contributor{
|
||||
Name: c.Author.Name,
|
||||
Email: email,
|
||||
Commits: 0,
|
||||
// GitHub avatar URL uses MD5 hash of email, which we'd generate here
|
||||
// but for simplicity we'll use a default avatar
|
||||
AvatarURL: fmt.Sprintf("https://avatars.githubusercontent.com/u/0?v=4"),
|
||||
}
|
||||
}
|
||||
contributors[email].Commits++
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to process commits: %w", err)
|
||||
}
|
||||
|
||||
// Convert contributors map to slice and sort by commit count
|
||||
for _, contributor := range contributors {
|
||||
repoData.Contributors = append(repoData.Contributors, *contributor)
|
||||
}
|
||||
|
||||
// Sort contributors by commit count (we'll implement this in utils)
|
||||
sortContributorsByCommits(repoData.Contributors)
|
||||
|
||||
// If we have more than 5 contributors, limit to top 5
|
||||
if len(repoData.Contributors) > 5 {
|
||||
repoData.Contributors = repoData.Contributors[:5]
|
||||
}
|
||||
|
||||
// Walk the repository to find markdown and image files
|
||||
err = filepath.WalkDir(repoPath, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip .git directory
|
||||
if d.IsDir() && d.Name() == ".git" {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
// Skip other common directories we don't want
|
||||
if d.IsDir() && (d.Name() == "node_modules" || d.Name() == "vendor" || d.Name() == ".github") {
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
// Process files
|
||||
if !d.IsDir() {
|
||||
relativePath, err := filepath.Rel(repoPath, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Handle markdown files
|
||||
if isMarkdownFile(d.Name()) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read file %s: %w", path, err)
|
||||
}
|
||||
|
||||
// Store markdown content
|
||||
repoData.MarkdownFiles[relativePath] = string(content)
|
||||
|
||||
// Check if this is a README file
|
||||
if isReadmeFile(d.Name()) && (repoData.ReadmePath == "" || relativePath == "README.md") {
|
||||
repoData.ReadmePath = relativePath
|
||||
repoData.ReadmeContent = string(content)
|
||||
}
|
||||
|
||||
fmt.Printf("Found markdown file: %s\n", relativePath)
|
||||
}
|
||||
|
||||
// Handle image files
|
||||
if isImageFile(d.Name()) {
|
||||
repoData.ImageFiles[relativePath] = path
|
||||
fmt.Printf("Found image file: %s\n", relativePath)
|
||||
}
|
||||
|
||||
// Check for license file
|
||||
if isLicenseFile(d.Name()) && repoData.License == "" {
|
||||
content, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
// Try to determine license type from content
|
||||
licenseType := detectLicenseType(string(content))
|
||||
if licenseType != "" {
|
||||
repoData.License = licenseType
|
||||
} else {
|
||||
repoData.License = "License"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to walk repository: %w", err)
|
||||
}
|
||||
|
||||
// If we didn't find a description, try to extract from README
|
||||
if repoData.Description == "" && repoData.ReadmeContent != "" {
|
||||
repoData.Description = extractDescriptionFromReadme(repoData.ReadmeContent)
|
||||
}
|
||||
|
||||
return repoData, nil
|
||||
}
|
||||
|
||||
// isMarkdownFile checks if a filename has a markdown extension
|
||||
func isMarkdownFile(filename string) bool {
|
||||
extensions := []string{".md", ".markdown", ".mdown", ".mkdn"}
|
||||
lowerFilename := strings.ToLower(filename)
|
||||
for _, ext := range extensions {
|
||||
if strings.HasSuffix(lowerFilename, ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isReadmeFile checks if a file is a README
|
||||
func isReadmeFile(filename string) bool {
|
||||
lowerFilename := strings.ToLower(filename)
|
||||
return strings.HasPrefix(lowerFilename, "readme.") && isMarkdownFile(filename)
|
||||
}
|
||||
|
||||
// isImageFile checks if a filename has an image extension
|
||||
func isImageFile(filename string) bool {
|
||||
extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp"}
|
||||
lowerFilename := strings.ToLower(filename)
|
||||
for _, ext := range extensions {
|
||||
if strings.HasSuffix(lowerFilename, ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// isLicenseFile checks if a file is likely a license file
|
||||
func isLicenseFile(filename string) bool {
|
||||
lowerFilename := strings.ToLower(filename)
|
||||
return lowerFilename == "license" || lowerFilename == "license.md" ||
|
||||
lowerFilename == "license.txt" || lowerFilename == "copying"
|
||||
}
|
||||
|
||||
// detectLicenseType tries to determine the license type from its content
|
||||
func detectLicenseType(content string) string {
|
||||
content = strings.ToLower(content)
|
||||
|
||||
// Check for common license types
|
||||
if strings.Contains(content, "mit license") {
|
||||
return "MIT License"
|
||||
} else if strings.Contains(content, "apache license") {
|
||||
return "Apache License"
|
||||
} else if strings.Contains(content, "gnu general public license") ||
|
||||
strings.Contains(content, "gpl") {
|
||||
return "GPL License"
|
||||
} else if strings.Contains(content, "bsd") {
|
||||
return "BSD License"
|
||||
} else if strings.Contains(content, "mozilla public license") {
|
||||
return "Mozilla Public License"
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractDescriptionFromReadme tries to get a short description from README
|
||||
func extractDescriptionFromReadme(content string) string {
|
||||
// Try to find the first paragraph after the title
|
||||
re := regexp.MustCompile(`(?m)^#\s+.+\n+(.+)`)
|
||||
matches := re.FindStringSubmatch(content)
|
||||
if len(matches) > 1 {
|
||||
// Return first paragraph, up to 150 chars
|
||||
desc := matches[1]
|
||||
if len(desc) > 150 {
|
||||
desc = desc[:147] + "..."
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
// If no match, just take the first non-empty line
|
||||
lines := strings.Split(content, "\n")
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if line != "" && !strings.HasPrefix(line, "#") {
|
||||
if len(line) > 150 {
|
||||
line = line[:147] + "..."
|
||||
}
|
||||
return line
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// sortContributorsByCommits sorts contributors by commit count (descending)
|
||||
func sortContributorsByCommits(contributors []Contributor) {
|
||||
// Simple bubble sort implementation
|
||||
for i := 0; i < len(contributors); i++ {
|
||||
for j := i + 1; j < len(contributors); j++ {
|
||||
if contributors[i].Commits < contributors[j].Commits {
|
||||
contributors[i], contributors[j] = contributors[j], contributors[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetCommitStats gets commit statistics for the repository
|
||||
func GetCommitStats(repo *git.Repository) (int, error) {
|
||||
// Get HEAD reference
|
||||
ref, err := repo.Head()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get HEAD reference: %w", err)
|
||||
}
|
||||
|
||||
// Get commit history
|
||||
cIter, err := repo.Log(&git.LogOptions{From: ref.Hash()})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to get commit history: %w", err)
|
||||
}
|
||||
|
||||
// Count commits
|
||||
count := 0
|
||||
err = cIter.ForEach(func(c *object.Commit) error {
|
||||
count++
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to process commits: %w", err)
|
||||
}
|
||||
|
||||
return count, nil
|
||||
}
|
272
pkg/templates/doc.html
Normal file
272
pkg/templates/doc.html
Normal file
@ -0,0 +1,272 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.PageTitle}}</title>
|
||||
<style>
|
||||
/* Base styles */
|
||||
:root {
|
||||
--primary-color: #0366d6;
|
||||
--secondary-color: #586069;
|
||||
--background-color: #ffffff;
|
||||
--sidebar-bg: #f6f8fa;
|
||||
--border-color: #e1e4e8;
|
||||
--hover-color: #f1f1f1;
|
||||
--text-color: #24292e;
|
||||
--sidebar-width: 260px;
|
||||
--breakpoint: 768px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
background-color: var(--sidebar-bg);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
background-color: rgba(27, 31, 35, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
table tr {
|
||||
background-color: var(--background-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
table tr:nth-child(2n) {
|
||||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.nav-sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background-color: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--border-color);
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.repo-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.repo-info h2 {
|
||||
margin: 0;
|
||||
border: none;
|
||||
font-size: 1.25em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.repo-meta {
|
||||
font-size: 0.9em;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.nav-links li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
display: block;
|
||||
padding: 6px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
background-color: var(--hover-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-links a.active {
|
||||
font-weight: 600;
|
||||
background-color: rgba(3, 102, 214, 0.1);
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
font-weight: 600;
|
||||
margin: 16px 0 8px 0;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.nav-footer {
|
||||
margin-top: 20px;
|
||||
font-size: 0.9em;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Document content */
|
||||
.doc-content {
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
color: var(--secondary-color);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav-sidebar">
|
||||
<div class="repo-info">
|
||||
<h2>
|
||||
<a href="../index.html">{{.RepoFullName}}</a>
|
||||
</h2>
|
||||
<div class="repo-meta">
|
||||
{{if .CommitCount}}📝 {{.CommitCount}} commits{{end}}
|
||||
{{if .License}} • 📜 {{.License}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="nav-links">
|
||||
<li><a href="../index.html">Repository Overview</a></li>
|
||||
|
||||
{{if .DocsPages}}
|
||||
<div class="nav-section-title">Documentation:</div>
|
||||
{{range .DocsPages}}
|
||||
<li><a href="{{.Path}}" {{if .IsActive}}class="active"{{end}}>{{.Title}}</a></li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<div class="nav-footer">
|
||||
<a href="{{.RepoURL}}" target="_blank">View on GitHub</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="main-content">
|
||||
<header class="page-header">
|
||||
<h1>{{.PageTitle}}</h1>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div class="doc-content">
|
||||
{{.PageContent}}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="page-footer">
|
||||
<p>Generated on {{.GeneratedAt}} • <a href="{{.RepoURL}}" target="_blank">View on GitHub</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
381
pkg/templates/main.html
Normal file
381
pkg/templates/main.html
Normal file
@ -0,0 +1,381 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{.PageTitle}}</title>
|
||||
<style>
|
||||
/* Base styles */
|
||||
:root {
|
||||
--primary-color: #0366d6;
|
||||
--secondary-color: #586069;
|
||||
--background-color: #ffffff;
|
||||
--sidebar-bg: #f6f8fa;
|
||||
--border-color: #e1e4e8;
|
||||
--hover-color: #f1f1f1;
|
||||
--text-color: #24292e;
|
||||
--sidebar-width: 260px;
|
||||
--breakpoint: 768px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.5em;
|
||||
padding-bottom: 0.3em;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
pre, code {
|
||||
font-family: SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace;
|
||||
font-size: 85%;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
line-height: 1.45;
|
||||
background-color: var(--sidebar-bg);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 0.2em 0.4em;
|
||||
margin: 0;
|
||||
background-color: rgba(27, 31, 35, 0.05);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
table tr {
|
||||
background-color: var(--background-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
table tr:nth-child(2n) {
|
||||
background-color: var(--sidebar-bg);
|
||||
}
|
||||
|
||||
/* Layout */
|
||||
.nav-sidebar {
|
||||
width: var(--sidebar-width);
|
||||
background-color: var(--sidebar-bg);
|
||||
border-right: 1px solid var(--border-color);
|
||||
overflow-y: auto;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
padding: 40px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
.repo-info {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.repo-info h2 {
|
||||
margin: 0;
|
||||
border: none;
|
||||
font-size: 1.25em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.repo-meta {
|
||||
font-size: 0.9em;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.nav-links {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin: 0 0 20px 0;
|
||||
}
|
||||
|
||||
.nav-links li {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.nav-links a {
|
||||
display: block;
|
||||
padding: 6px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.nav-links a:hover {
|
||||
background-color: var(--hover-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-links a.active {
|
||||
font-weight: 600;
|
||||
background-color: rgba(3, 102, 214, 0.1);
|
||||
}
|
||||
|
||||
.nav-section-title {
|
||||
font-weight: 600;
|
||||
margin: 16px 0 8px 0;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.nav-footer {
|
||||
margin-top: 20px;
|
||||
font-size: 0.9em;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
/* Repository sections */
|
||||
.repo-header {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.repo-stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.repo-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.repo-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.contributors-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.contributor-item {
|
||||
flex: 1 1 calc(33% - 20px);
|
||||
min-width: 200px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 3px;
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
.contributor-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--primary-color);
|
||||
color: white;
|
||||
text-align: center;
|
||||
line-height: 40px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.contributor-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.contributor-name {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.contributor-commits {
|
||||
font-size: 0.9em;
|
||||
color: var(--secondary-color);
|
||||
}
|
||||
|
||||
.page-footer {
|
||||
margin-top: 40px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
color: var(--secondary-color);
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
/* Mobile responsive */
|
||||
@media (max-width: 768px) {
|
||||
body {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.contributor-item {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="nav-sidebar">
|
||||
<div class="repo-info">
|
||||
<h2>
|
||||
<a href="index.html">{{.RepoFullName}}</a>
|
||||
</h2>
|
||||
<div class="repo-meta">
|
||||
{{if .CommitCount}}📝 {{.CommitCount}} commits{{end}}
|
||||
{{if .License}} • 📜 {{.License}}{{end}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="nav-links">
|
||||
<li><a href="index.html" class="active">Repository Overview</a></li>
|
||||
|
||||
{{if .DocsPages}}
|
||||
<div class="nav-section-title">Documentation:</div>
|
||||
{{range .DocsPages}}
|
||||
<li><a href="{{.Path}}">{{.Title}}</a></li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
|
||||
<div class="nav-footer">
|
||||
<a href="{{.RepoURL}}" target="_blank">View on GitHub</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="main-content">
|
||||
<header class="repo-header">
|
||||
<h1>{{.RepoFullName}}</h1>
|
||||
<div class="repo-description">{{.Description}}</div>
|
||||
|
||||
<div class="repo-stats">
|
||||
{{if .CommitCount}}
|
||||
<div class="repo-stat">
|
||||
<span>📝</span> <span>{{.CommitCount}} commits</span>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="repo-stat">
|
||||
<span>📅</span> <span>Last updated: {{.LastUpdate}}</span>
|
||||
</div>
|
||||
|
||||
{{if .License}}
|
||||
<div class="repo-stat">
|
||||
<span>📜</span> <span>{{.License}}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{{if .ReadmeHTML}}
|
||||
<section id="readme" class="repo-section">
|
||||
<h2>README</h2>
|
||||
<div class="readme-content">
|
||||
{{.ReadmeHTML}}
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
|
||||
{{if .Contributors}}
|
||||
<section id="contributors" class="repo-section">
|
||||
<h2>Top Contributors</h2>
|
||||
<div class="contributors-list">
|
||||
{{range .Contributors}}
|
||||
<div class="contributor-item">
|
||||
<!-- Use first letter as avatar if no image available -->
|
||||
<div class="contributor-avatar">
|
||||
{{if .Name}}{{slice .Name 0 1}}{{else}}?{{end}}
|
||||
</div>
|
||||
<div class="contributor-info">
|
||||
<div class="contributor-name">
|
||||
{{.Name}}
|
||||
</div>
|
||||
<div class="contributor-commits">
|
||||
{{.Commits}} commits
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
<a href="{{.RepoURL}}/graphs/contributors" target="_blank">View all contributors on GitHub →</a>
|
||||
</section>
|
||||
{{end}}
|
||||
</main>
|
||||
|
||||
<footer class="page-footer">
|
||||
<p>Generated on {{.GeneratedAt}} • <a href="{{.RepoURL}}" target="_blank">View on GitHub</a></p>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
1
pkg/templates/style.css
Normal file
1
pkg/templates/style.css
Normal file
@ -0,0 +1 @@
|
||||
/*Empty CSS file, expose for customization*/
|
12
pkg/templates/template.go
Normal file
12
pkg/templates/template.go
Normal file
@ -0,0 +1,12 @@
|
||||
package templates
|
||||
|
||||
import _ "embed"
|
||||
|
||||
//go:embed main.html
|
||||
var MainTemplate string
|
||||
|
||||
//go:embed doc.html
|
||||
var DocTemplate string
|
||||
|
||||
//go:embed style.css
|
||||
var StyleTemplate string
|
167
pkg/utils/utils.go
Normal file
167
pkg/utils/utils.go
Normal file
@ -0,0 +1,167 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetOutputPath converts a markdown file path to its HTML output path
|
||||
func GetOutputPath(path, baseDir string) string {
|
||||
// Replace extension with .html
|
||||
baseName := filepath.Base(path)
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
// Remove extension
|
||||
baseName = strings.TrimSuffix(baseName, filepath.Ext(baseName)) + ".html"
|
||||
|
||||
// If it's in root, put it directly in baseDir
|
||||
if dir == "." {
|
||||
return filepath.Join(baseDir, baseName)
|
||||
}
|
||||
|
||||
// Otherwise preserve directory structure
|
||||
return filepath.Join(baseDir, dir, baseName)
|
||||
}
|
||||
|
||||
// GetTitleFromMarkdown extracts the first heading from markdown content
|
||||
func GetTitleFromMarkdown(content string) string {
|
||||
// Look for the first heading
|
||||
re := regexp.MustCompile(`(?m)^#\s+(.+)$`)
|
||||
matches := re.FindStringSubmatch(content)
|
||||
if len(matches) > 1 {
|
||||
return matches[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// PrettifyFilename converts a filename to a more readable title
|
||||
func PrettifyFilename(filename string) string {
|
||||
// Remove extension
|
||||
name := strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||
|
||||
// Replace hyphens and underscores with spaces
|
||||
name = strings.ReplaceAll(name, "-", " ")
|
||||
name = strings.ReplaceAll(name, "_", " ")
|
||||
|
||||
// Capitalize words
|
||||
words := strings.Fields(name)
|
||||
for i, word := range words {
|
||||
if len(word) > 0 {
|
||||
words[i] = strings.ToUpper(word[0:1]) + word[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(words, " ")
|
||||
}
|
||||
|
||||
// ProcessRelativeLinks handles relative links in markdown content
|
||||
func ProcessRelativeLinks(content, filePath, owner, repo string) string {
|
||||
baseDir := filepath.Dir(filePath)
|
||||
|
||||
// Replace relative links to markdown files with links to their HTML versions
|
||||
re := regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
|
||||
|
||||
return re.ReplaceAllStringFunc(content, func(match string) string {
|
||||
submatch := re.FindStringSubmatch(match)
|
||||
if len(submatch) < 3 {
|
||||
return match
|
||||
}
|
||||
|
||||
linkText := submatch[1]
|
||||
linkTarget := submatch[2]
|
||||
|
||||
// Skip absolute URLs and anchors
|
||||
if strings.HasPrefix(linkTarget, "http") || strings.HasPrefix(linkTarget, "#") {
|
||||
return match
|
||||
}
|
||||
|
||||
// Skip image links (we'll handle these separately)
|
||||
if isImageLink(linkTarget) {
|
||||
return match
|
||||
}
|
||||
|
||||
// Handle markdown links - convert to HTML links
|
||||
if isMarkdownLink(linkTarget) {
|
||||
// Remove anchor if present
|
||||
anchor := ""
|
||||
if idx := strings.Index(linkTarget, "#"); idx > -1 {
|
||||
anchor = linkTarget[idx:]
|
||||
linkTarget = linkTarget[:idx]
|
||||
}
|
||||
|
||||
// If the link is relative, resolve it
|
||||
resolvedPath := linkTarget
|
||||
if !strings.HasPrefix(resolvedPath, "/") {
|
||||
// Handle ./file.md style links
|
||||
if strings.HasPrefix(resolvedPath, "./") {
|
||||
resolvedPath = resolvedPath[2:]
|
||||
}
|
||||
|
||||
if baseDir != "." {
|
||||
resolvedPath = filepath.Join(baseDir, resolvedPath)
|
||||
}
|
||||
} else {
|
||||
// Remove leading slash
|
||||
resolvedPath = resolvedPath[1:]
|
||||
}
|
||||
|
||||
htmlPath := "../" + GetOutputPath(resolvedPath, "docs")
|
||||
return "[" + linkText + "](" + htmlPath + anchor + ")"
|
||||
}
|
||||
|
||||
return match
|
||||
})
|
||||
}
|
||||
|
||||
// GetImageLinkRegex returns a regex for matching image links in markdown
|
||||
func GetImageLinkRegex() *regexp.Regexp {
|
||||
return regexp.MustCompile(`!\[([^\]]*)\]\(([^)]+)\)`)
|
||||
}
|
||||
|
||||
// isImageLink checks if a link points to an image
|
||||
func isImageLink(link string) bool {
|
||||
extensions := []string{".jpg", ".jpeg", ".png", ".gif", ".svg", ".webp"}
|
||||
lower := strings.ToLower(link)
|
||||
|
||||
for _, ext := range extensions {
|
||||
if strings.HasSuffix(lower, ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isMarkdownLink checks if a link points to a markdown file
|
||||
func isMarkdownLink(link string) bool {
|
||||
extensions := []string{".md", ".markdown", ".mdown", ".mkdn"}
|
||||
lower := strings.ToLower(link)
|
||||
|
||||
for _, ext := range extensions {
|
||||
if strings.HasSuffix(lower, ext) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// SortDocPagesByTitle sorts doc pages by title
|
||||
func SortDocPagesByTitle(pages []DocPage) {
|
||||
// Simple bubble sort
|
||||
for i := 0; i < len(pages); i++ {
|
||||
for j := i + 1; j < len(pages); j++ {
|
||||
if pages[i].Title > pages[j].Title {
|
||||
pages[i], pages[j] = pages[j], pages[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DocPage represents a documentation page for navigation
|
||||
type DocPage struct {
|
||||
Title string
|
||||
Path string
|
||||
IsActive bool
|
||||
}
|
Reference in New Issue
Block a user