krik 0.1.27

A fast static site generator written in Rust with internationalization, theming, and modern web features
Documentation
use crate::error::{IoError, IoErrorKind, KrikError, KrikResult};
use crate::lint::link_checker::BrokenLink;
use chrono::{DateTime, Utc};
use std::fs;
use std::path::PathBuf;

/// Report produced by the content linter
#[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()
    }
}

/// Generate HTML report from lint results
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, &timestamp);

    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)
}

/// Create the HTML content for the report
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");

    // Calculate totals
    let total_issues = report.errors.len() + report.warnings.len() + report.broken_links.len();

    // Determine overall status
    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
    )
}

/// Get the CSS styles for the HTML report
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;
        }"#
}

/// Create the errors section HTML
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
    )
}

/// Create the warnings section HTML
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
    )
}

/// Create the broken links section HTML
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
    )
}

/// Simple HTML escape function
fn html_escape(text: &str) -> String {
    text.replace('&', "&amp;")
        .replace('<', "&lt;")
        .replace('>', "&gt;")
        .replace('"', "&quot;")
        .replace('\'', "&#x27;")
}