package reporter import ( "encoding/csv" "encoding/json" "fmt" "io" "strings" "time" "git.nakama.town/fmartingr/dharma/pkg/scraper" "github.com/fatih/color" ) // Reporter is an interface for report generators type Reporter interface { Generate(results *scraper.Results, writer io.Writer) error } // New creates a new reporter based on the format func New(format string) (Reporter, error) { switch strings.ToLower(format) { case "pretty": return &PrettyReporter{}, nil case "json": return &JSONReporter{}, nil case "csv": return &CSVReporter{}, nil default: return nil, fmt.Errorf("unsupported format: %s", format) } } // PrettyReporter generates a human-readable report for terminal type PrettyReporter struct{} // Generate generates a pretty report func (r *PrettyReporter) Generate(results *scraper.Results, writer io.Writer) error { red := color.New(color.FgRed).SprintFunc() green := color.New(color.FgGreen).SprintFunc() yellow := color.New(color.FgYellow).SprintFunc() blue := color.New(color.FgBlue).SprintFunc() cyan := color.New(color.FgCyan).SprintFunc() // Count internal vs external links countInternalSuccess := 0 countInternalErrors := 0 countExternalSuccess := 0 countExternalErrors := 0 for _, result := range results.Successes { if result.IsExternal { countExternalSuccess++ } else { countInternalSuccess++ } } for _, result := range results.Errors { if result.IsExternal { countExternalErrors++ } else { countInternalErrors++ } } fmt.Fprintf(writer, "Website scan report for: %s\n", blue(results.BaseURL)) fmt.Fprintf(writer, "Scanned at: %s\n", time.Now().Format(time.RFC1123)) fmt.Fprintf(writer, "Total resources checked: %d\n", results.Total) fmt.Fprintf(writer, "Success: %s, Errors: %s\n", green(len(results.Successes)), red(len(results.Errors))) fmt.Fprintf(writer, "Internal links: %s success, %s errors\n", green(countInternalSuccess), red(countInternalErrors)) fmt.Fprintf(writer, "External links: %s success, %s errors\n\n", green(countExternalSuccess), red(countExternalErrors)) if len(results.Errors) == 0 { fmt.Fprintf(writer, "%s No errors found!\n", green("✓")) return nil } // Group errors by internal/external internalErrors := []scraper.Result{} externalErrors := []scraper.Result{} for _, result := range results.Errors { if result.IsExternal { externalErrors = append(externalErrors, result) } else { internalErrors = append(internalErrors, result) } } // Print internal errors first if we have any if len(internalErrors) > 0 { fmt.Fprintln(writer, "Errors found:") for _, result := range internalErrors { status := fmt.Sprintf("%d", result.Status) if result.Status == 0 { status = "ERR" } fmt.Fprintf(writer, "%-6s (%-10s) %s [from: %s]\n", red(status), yellow(result.Type), result.URL, result.SourceURL, ) } } // Print external errors if we have any if len(externalErrors) > 0 { if len(internalErrors) > 0 { fmt.Fprintln(writer, "") } fmt.Fprintln(writer, "External Errors:") fmt.Fprintln(writer, strings.Repeat("-", 80)) fmt.Fprintf(writer, "%-6s | %-10s | %s | %s\n", "Status", "Type", "URL", "Source") fmt.Fprintln(writer, strings.Repeat("-", 80)) for _, result := range externalErrors { status := fmt.Sprintf("%d", result.Status) if result.Status == 0 { status = "ERR" } fmt.Fprintf(writer, "%-6s | %-10s | %s | %s\n", red(status), cyan(result.Type), result.URL, result.SourceURL, ) } } return nil } // JSONReporter generates a JSON report type JSONReporter struct{} // Generate generates a JSON report func (r *JSONReporter) Generate(results *scraper.Results, writer io.Writer) error { return json.NewEncoder(writer).Encode(results) } // CSVReporter generates a CSV report type CSVReporter struct{} // Generate generates a CSV report func (r *CSVReporter) Generate(results *scraper.Results, writer io.Writer) error { csvWriter := csv.NewWriter(writer) defer csvWriter.Flush() // Write header if err := csvWriter.Write([]string{"Status", "Type", "URL", "Source URL", "Error"}); err != nil { return err } // Write errors for _, result := range results.Errors { status := fmt.Sprintf("%d", result.Status) if result.Status == 0 { status = "ERROR" } if err := csvWriter.Write([]string{ status, result.Type, result.URL, result.SourceURL, result.Error, }); err != nil { return err } } return nil } // Helper function to truncate strings func truncate(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen-3] + "..." }