package generator import ( "bytes" "fmt" "html/template" "log" "os" "path/filepath" "strings" "github.com/go-i2p/go-github-dashboard/pkg/types" "github.com/russross/blackfriday/v2" ) // HTMLGenerator handles the generation of HTML files type HTMLGenerator struct { outputDir string template *template.Template verbose bool } // NewHTMLGenerator creates a new HTMLGenerator func NewHTMLGenerator(config *types.Config) (*HTMLGenerator, error) { // Create the template indexTmpl := ` GitHub Dashboard {{if .Username}}for @{{.Username}}{{else}}for {{.Organization}}{{end}}

GitHub Dashboard {{if .Username}}for @{{.Username}}{{else}}for {{.Organization}}{{end}}

{{len .Repositories}} repositories {{.TotalPRs}} open pull requests {{.TotalIssues}} open issues {{.TotalDiscussions}} recent discussions

Generated on {{.GeneratedAt.Format "January 2, 2006 at 15:04"}}

Repositories

{{range .Repositories}}

{{if .Description}}{{.Description}}{{else}}No description provided.{{end}}

View on GitHub ⭐ {{.Stars}} 🍴 {{.Forks}} Updated: {{.LastUpdated.Format "2006-01-02"}}
{{if .PullRequests}}
{{range .PullRequests}} {{end}}
Title Author Updated Labels
{{.Title}} @{{.Author}} {{.UpdatedAt.Format "2006-01-02"}} {{range $i, $label := .Labels}}{{if $i}}, {{end}}{{$label.Name}}{{else}}none{{end}}
{{end}} {{if .Issues}}
{{range .Issues}} {{end}}
Title Author Updated Labels
{{.Title}} @{{.Author}} {{.UpdatedAt.Format "2006-01-02"}} {{range $i, $label := .Labels}}{{if $i}}, {{end}}{{$label.Name}}{{else}}none{{end}}
{{end}} {{if .Discussions}}
{{range .Discussions}} {{end}}
Title Started By Last Activity Category
{{.Title}} @{{.Author}} {{.LastUpdated.Format "2006-01-02"}} {{.Category}}
{{end}}
{{end}}
` tmpl, err := template.New("index.html").Parse(indexTmpl) if err != nil { return nil, fmt.Errorf("error parsing HTML template: %w", err) } return &HTMLGenerator{ outputDir: config.OutputDir, template: tmpl, verbose: config.Verbose, }, nil } // GenerateCSS generates the CSS file for the dashboard func (g *HTMLGenerator) GenerateCSS() error { css := `/* Base styles */ :root { --primary-color: #0366d6; --secondary-color: #586069; --background-color: #ffffff; --border-color: #e1e4e8; --pr-color: #28a745; --issue-color: #d73a49; --discussion-color: #6f42c1; --hover-color: #f6f8fa; --font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; } * { box-sizing: border-box; margin: 0; padding: 0; } body { font-family: var(--font-family); line-height: 1.5; color: #24292e; background-color: var(--background-color); padding: 20px; max-width: 1200px; margin: 0 auto; } /* Header styles */ header { margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid var(--border-color); } header h1 { margin-bottom: 10px; } .dashboard-stats { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 10px; } .dashboard-stats span { background-color: #f1f8ff; border-radius: 20px; padding: 5px 12px; font-size: 14px; } .generated-at { font-size: 14px; color: var(--secondary-color); } /* Repository styles */ .repositories { margin-bottom: 30px; } .repositories h2 { margin-bottom: 20px; } .repository { margin-bottom: 15px; border: 1px solid var(--border-color); border-radius: 6px; overflow: hidden; } .repo-details { padding: 15px; border-bottom: 1px solid var(--border-color); } .repo-description { margin-bottom: 10px; } .repo-meta { display: flex; flex-wrap: wrap; gap: 15px; font-size: 14px; color: var(--secondary-color); } .repo-links { padding: 10px 15px; font-size: 14px; border-top: 1px solid var(--border-color); } /* Collapsible sections */ .collapsible { width: 100%; } .toggle { position: absolute; opacity: 0; z-index: -1; } .toggle-label { display: flex; justify-content: space-between; align-items: center; padding: 12px 15px; font-weight: 600; cursor: pointer; background-color: #f6f8fa; position: relative; } .section-label { border-top: 1px solid var(--border-color); font-weight: 500; } .pr-label { color: var(--pr-color); } .issue-label { color: var(--issue-color); } .discussion-label { color: var(--discussion-color); } .toggle-label::after { content: '+'; font-size: 18px; transition: transform 0.3s ease; } .toggle:checked ~ .toggle-label::after { content: '−'; } .collapsible-content { max-height: 0; overflow: hidden; transition: max-height 0.35s ease; } .toggle:checked ~ .collapsible-content { max-height: 100vh; } /* Table styles */ .data-table { width: 100%; border-collapse: collapse; font-size: 14px; } .data-table th, .data-table td { padding: 8px 15px; text-align: left; border-bottom: 1px solid var(--border-color); } .data-table th { background-color: #f6f8fa; font-weight: 600; } .data-table tr:hover { background-color: var(--hover-color); } /* Links */ a { color: var(--primary-color); text-decoration: none; } a:hover { text-decoration: underline; } /* Repository name and stats */ .repo-name { font-size: 16px; } .repo-stats { display: flex; gap: 10px; } .stat { font-size: 12px; padding: 2px 8px; border-radius: 12px; background-color: #f1f8ff; color: var(--primary-color); } /* Footer */ footer { margin-top: 40px; padding-top: 20px; border-top: 1px solid var(--border-color); font-size: 14px; color: var(--secondary-color); text-align: center; } /* Responsive adjustments */ @media (max-width: 768px) { .toggle-label { flex-direction: column; align-items: flex-start; gap: 5px; } .repo-stats { align-self: flex-start; } .data-table { display: block; overflow-x: auto; } .dashboard-stats { flex-direction: column; align-items: flex-start; gap: 5px; } }` err := os.WriteFile(filepath.Join(g.outputDir, "style.css"), []byte(css), 0644) if err != nil { return fmt.Errorf("error writing CSS file: %w", err) } return nil } // GenerateHTML generates the main HTML dashboard func (g *HTMLGenerator) GenerateHTML(dashboard types.Dashboard) error { if g.verbose { log.Println("Generating HTML dashboard") } // Render the template var buf bytes.Buffer err := g.template.Execute(&buf, dashboard) if err != nil { return fmt.Errorf("error executing template: %w", err) } // Write the file outputPath := filepath.Join(g.outputDir, "index.html") err = os.WriteFile(outputPath, buf.Bytes(), 0644) if err != nil { return fmt.Errorf("error writing HTML file: %w", err) } // Generate the CSS file err = g.GenerateCSS() if err != nil { return err } return nil } // ConvertMarkdownToHTML converts a markdown file to HTML func (g *HTMLGenerator) ConvertMarkdownToHTML(markdownPath string) (string, error) { if g.verbose { log.Printf("Converting markdown to HTML: %s", markdownPath) } // Read the markdown file markdownContent, err := os.ReadFile(markdownPath) if err != nil { return "", fmt.Errorf("error reading markdown file: %w", err) } // Convert the markdown to HTML htmlContent := blackfriday.Run(markdownContent) // Determine the output filename baseName := filepath.Base(markdownPath) htmlFileName := strings.TrimSuffix(baseName, filepath.Ext(baseName)) + ".html" htmlPath := filepath.Join(g.outputDir, "repositories", htmlFileName) // Create a simple HTML wrapper htmlPage := fmt.Sprintf(` %s ← Back to Dashboard
%s
`, strings.TrimSuffix(baseName, filepath.Ext(baseName)), string(htmlContent)) // Write the HTML file err = os.WriteFile(htmlPath, []byte(htmlPage), 0644) if err != nil { return "", fmt.Errorf("error writing HTML file: %w", err) } return htmlPath, nil } // ConvertAllMarkdownToHTML converts all markdown files to HTML func (g *HTMLGenerator) ConvertAllMarkdownToHTML(markdownPaths []string) ([]string, error) { var htmlPaths []string for _, markdownPath := range markdownPaths { htmlPath, err := g.ConvertMarkdownToHTML(markdownPath) if err != nil { return htmlPaths, err } htmlPaths = append(htmlPaths, htmlPath) } return htmlPaths, nil }