use crate::error::{IoError, IoErrorKind, KrikError, KrikResult};
use crate::lint::link_checker::BrokenLink;
use chrono::{DateTime, Utc};
use std::fs;
use std::path::PathBuf;
#[derive(Debug, Default)]
pub struct LintReport {
pub files_scanned: usize,
pub errors: Vec<String>,
pub warnings: Vec<String>,
pub broken_links: Vec<BrokenLink>,
}
impl LintReport {
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn has_warnings(&self) -> bool {
!self.warnings.is_empty()
}
pub fn has_broken_links(&self) -> bool {
!self.broken_links.is_empty()
}
}
pub fn generate_html_report(report: &LintReport, check_links: bool) -> KrikResult<String> {
let timestamp = Utc::now();
let filename = format!(
"krik-report-{}.html",
timestamp.format("%Y-%m-%dT%H-%M-%SZ")
);
let html_content = create_html_report_content(report, check_links, ×tamp);
fs::write(&filename, html_content).map_err(|e| {
KrikError::Io(Box::new(IoError {
kind: IoErrorKind::WriteFailed(e),
path: PathBuf::from(&filename),
context: "Failed to write HTML report".to_string(),
}))
})?;
Ok(filename)
}
fn create_html_report_content(
report: &LintReport,
check_links: bool,
timestamp: &DateTime<Utc>,
) -> String {
let iso_timestamp = timestamp.format("%Y-%m-%dT%H:%M:%SZ");
let total_issues = report.errors.len() + report.warnings.len() + report.broken_links.len();
let status = if report.has_errors() || report.has_broken_links() {
("ERROR", "status-error", "❌")
} else if report.has_warnings() {
("WARNING", "status-warning", "⚠️")
} else {
("SUCCESS", "status-success", "✅")
};
format!(
r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Krik Lint Report - {}</title>
<style>
{}
</style>
</head>
<body>
<div class="header">
<h1>🔍 Krik Lint Report</h1>
<p>Generated on {}</p>
<div class="status-badge {}">
{} {}
</div>
</div>
<div class="summary">
<div class="summary-card">
<h3>📁 Files Scanned</h3>
<div class="number">{}</div>
</div>
<div class="summary-card">
<h3>❌ Errors</h3>
<div class="number error">{}</div>
</div>
<div class="summary-card">
<h3>⚠️ Warnings</h3>
<div class="number warning">{}</div>
</div>
{}
<div class="summary-card">
<h3>📊 Total Issues</h3>
<div class="number">{}</div>
</div>
</div>
{}
{}
{}
<div class="footer">
<p>Report generated by <strong>Krik Static Site Generator</strong></p>
<p>Timestamp: {} (UTC)</p>
</div>
</body>
</html>"#,
iso_timestamp,
get_css_styles(),
iso_timestamp,
status.1,
status.2,
status.0,
report.files_scanned,
report.errors.len(),
report.warnings.len(),
if check_links {
format!(
r#"<div class="summary-card">
<h3>🔗 Broken Links</h3>
<div class="number error">{}</div>
</div>"#,
report.broken_links.len()
)
} else {
String::new()
},
total_issues,
create_errors_section(&report.errors),
create_warnings_section(&report.warnings),
if check_links {
create_links_section(&report.broken_links)
} else {
String::new()
},
iso_timestamp
)
}
fn get_css_styles() -> &'static str {
r#"body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
max-width: 1200px;
margin: 0 auto;
padding: 20px;
background-color: #f8f9fa;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 10px;
margin-bottom: 30px;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 2.5em;
}
.header p {
margin: 10px 0 0 0;
opacity: 0.9;
}
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
margin-top: 15px;
}
.status-success { background-color: #28a745; }
.status-warning { background-color: #ffc107; color: #212529; }
.status-error { background-color: #dc3545; }
.summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.summary-card {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
text-align: center;
}
.summary-card h3 {
margin: 0 0 10px 0;
color: #495057;
}
.summary-card .number {
font-size: 2em;
font-weight: bold;
margin: 10px 0;
}
.number.success { color: #28a745; }
.number.warning { color: #ffc107; }
.number.error { color: #dc3545; }
.section {
background: white;
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.section-header {
padding: 15px 20px;
font-weight: bold;
display: flex;
align-items: center;
}
.section-header.errors { background-color: #f8d7da; color: #721c24; }
.section-header.warnings { background-color: #fff3cd; color: #856404; }
.section-header.links { background-color: #d1ecf1; color: #0c5460; }
.section-content {
padding: 0;
}
.issue-item {
padding: 15px 20px;
border-bottom: 1px solid #e9ecef;
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
font-size: 0.9em;
line-height: 1.4;
}
.issue-item:last-child {
border-bottom: none;
}
.link-item {
padding: 15px 20px;
border-bottom: 1px solid #e9ecef;
}
.link-item:last-child {
border-bottom: none;
}
.link-url {
font-family: monospace;
background-color: #f8f9fa;
padding: 2px 6px;
border-radius: 3px;
word-break: break-all;
}
.link-location {
color: #6c757d;
font-size: 0.9em;
margin-top: 5px;
}
.link-error {
color: #dc3545;
font-style: italic;
margin-top: 5px;
}
.footer {
text-align: center;
color: #6c757d;
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid #dee2e6;
}
.empty-section {
padding: 30px;
text-align: center;
color: #6c757d;
font-style: italic;
}"#
}
fn create_errors_section(errors: &[String]) -> String {
if errors.is_empty() {
return String::new();
}
let items = errors
.iter()
.map(|error| format!(r#"<div class="issue-item">{}</div>"#, html_escape(error)))
.collect::<Vec<_>>()
.join("");
format!(
r#"<div class="section">
<div class="section-header errors">
❌ Errors ({})
</div>
<div class="section-content">
{}
</div>
</div>"#,
errors.len(),
items
)
}
fn create_warnings_section(warnings: &[String]) -> String {
if warnings.is_empty() {
return String::new();
}
let items = warnings
.iter()
.map(|warning| format!(r#"<div class="issue-item">{}</div>"#, html_escape(warning)))
.collect::<Vec<_>>()
.join("");
format!(
r#"<div class="section">
<div class="section-header warnings">
⚠️ Warnings ({})
</div>
<div class="section-content">
{}
</div>
</div>"#,
warnings.len(),
items
)
}
fn create_links_section(broken_links: &[BrokenLink]) -> String {
if broken_links.is_empty() {
return String::new();
}
let items = broken_links
.iter()
.map(|link| {
format!(
r#"<div class="link-item">
<div class="link-url">{}</div>
<div class="link-location">📍 {}:{}</div>
<div class="link-error">💥 {}</div>
</div>"#,
html_escape(&link.url),
html_escape(&link.file_path.display().to_string()),
link.line_number,
html_escape(&link.error)
)
})
.collect::<Vec<_>>()
.join("");
format!(
r#"<div class="section">
<div class="section-header links">
🔗 Broken Links ({})
</div>
<div class="section-content">
{}
</div>
</div>"#,
broken_links.len(),
items
)
}
fn html_escape(text: &str) -> String {
text.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}