From 6318a10bf45b072aee421e1730ec764de7a4585c Mon Sep 17 00:00:00 2001 From: eyedeekay Date: Mon, 5 May 2025 21:55:21 -0400 Subject: [PATCH] basic template --- cmd/github-site-gen/main.go | 160 +++++++++++++++ go.mod | 30 +++ go.sum | 104 ++++++++++ pkg/generator/generator.go | 380 +++++++++++++++++++++++++++++++++++ pkg/git/git.go | 350 +++++++++++++++++++++++++++++++++ pkg/templates/doc.html | 272 +++++++++++++++++++++++++ pkg/templates/main.html | 381 ++++++++++++++++++++++++++++++++++++ pkg/templates/style.css | 1 + pkg/templates/template.go | 12 ++ pkg/utils/utils.go | 167 ++++++++++++++++ 10 files changed, 1857 insertions(+) create mode 100644 cmd/github-site-gen/main.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 pkg/generator/generator.go create mode 100644 pkg/git/git.go create mode 100644 pkg/templates/doc.html create mode 100644 pkg/templates/main.html create mode 100644 pkg/templates/style.css create mode 100644 pkg/templates/template.go create mode 100644 pkg/utils/utils.go diff --git a/cmd/github-site-gen/main.go b/cmd/github-site-gen/main.go new file mode 100644 index 0000000..048a041 --- /dev/null +++ b/cmd/github-site-gen/main.go @@ -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()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..007093f --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8804b5e --- /dev/null +++ b/go.sum @@ -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= diff --git a/pkg/generator/generator.go b/pkg/generator/generator.go new file mode 100644 index 0000000..6337084 --- /dev/null +++ b/pkg/generator/generator.go @@ -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("![%s](%s)", 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 +} diff --git a/pkg/git/git.go b/pkg/git/git.go new file mode 100644 index 0000000..f75f9ae --- /dev/null +++ b/pkg/git/git.go @@ -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 +} diff --git a/pkg/templates/doc.html b/pkg/templates/doc.html new file mode 100644 index 0000000..980e6bc --- /dev/null +++ b/pkg/templates/doc.html @@ -0,0 +1,272 @@ + + + + + + {{.PageTitle}} + + + + + + +
+ + +
+
+ {{.PageContent}} +
+
+ + +
+ + \ No newline at end of file diff --git a/pkg/templates/main.html b/pkg/templates/main.html new file mode 100644 index 0000000..fb7a16e --- /dev/null +++ b/pkg/templates/main.html @@ -0,0 +1,381 @@ + + + + + + {{.PageTitle}} + + + + + + +
+
+

{{.RepoFullName}}

+
{{.Description}}
+ +
+ {{if .CommitCount}} +
+ 📝 {{.CommitCount}} commits +
+ {{end}} + +
+ 📅 Last updated: {{.LastUpdate}} +
+ + {{if .License}} +
+ 📜 {{.License}} +
+ {{end}} +
+
+ +
+ {{if .ReadmeHTML}} +
+

README

+
+ {{.ReadmeHTML}} +
+
+ {{end}} + + {{if .Contributors}} +
+

Top Contributors

+
+ {{range .Contributors}} +
+ +
+ {{if .Name}}{{slice .Name 0 1}}{{else}}?{{end}} +
+
+
+ {{.Name}} +
+
+ {{.Commits}} commits +
+
+
+ {{end}} +
+ View all contributors on GitHub → +
+ {{end}} +
+ + +
+ + \ No newline at end of file diff --git a/pkg/templates/style.css b/pkg/templates/style.css new file mode 100644 index 0000000..91a2116 --- /dev/null +++ b/pkg/templates/style.css @@ -0,0 +1 @@ +/*Empty CSS file, expose for customization*/ \ No newline at end of file diff --git a/pkg/templates/template.go b/pkg/templates/template.go new file mode 100644 index 0000000..db1f89f --- /dev/null +++ b/pkg/templates/template.go @@ -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 diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go new file mode 100644 index 0000000..20da2c1 --- /dev/null +++ b/pkg/utils/utils.go @@ -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 +}