Skip to content

html_reporter

Full name: tenets.core.reporting.html_reporter

html_reporter

HTML report generator module.

This module provides HTML report generation functionality with rich visualizations, interactive charts, and professional styling. It creates standalone HTML reports that can be viewed in any modern web browser.

The HTML reporter generates responsive, interactive reports with embedded JavaScript visualizations and customizable themes.

Classes

HTMLTemplate

Python
HTMLTemplate(theme: str = 'default', custom_css: Optional[str] = None, include_charts: bool = True)

HTML template generator for reports.

Provides template generation for various report components including the main layout, charts, tables, and interactive elements.

ATTRIBUTEDESCRIPTION
theme

Visual theme name

custom_css

Custom CSS styles

include_charts

Whether to include chart libraries

Initialize HTML template.

PARAMETERDESCRIPTION
theme

Theme name

TYPE:strDEFAULT:'default'

custom_css

Custom CSS styles

TYPE:Optional[str]DEFAULT:None

include_charts

Include chart libraries

TYPE:boolDEFAULT:True

Source code in tenets/core/reporting/html_reporter.py
Python
def __init__(
    self, theme: str = "default", custom_css: Optional[str] = None, include_charts: bool = True
):
    """Initialize HTML template.

    Args:
        theme: Theme name
        custom_css: Custom CSS styles
        include_charts: Include chart libraries
    """
    self.theme = theme
    self.custom_css = custom_css
    self.include_charts = include_charts
Functions
get_base_template
Python
get_base_template() -> str

Get base HTML template.

RETURNSDESCRIPTION
str

Base HTML template

TYPE:str

Source code in tenets/core/reporting/html_reporter.py
Python
    def get_base_template(self) -> str:
        """Get base HTML template.

        Returns:
            str: Base HTML template
        """
        return """<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{title}</title>
    {styles}
    {scripts}
</head>
<body>
    <div class="container">
        {header}
        {navigation}
        <main class="content">
            {content}
        </main>
        {footer}
    </div>
    {chart_scripts}
</body>
</html>"""
get_styles
Python
get_styles() -> str

Get CSS styles for the report.

RETURNSDESCRIPTION
str

CSS styles

TYPE:str

Source code in tenets/core/reporting/html_reporter.py
Python
def get_styles(self) -> str:
    """Get CSS styles for the report.

    Returns:
        str: CSS styles
    """
    base_styles = """
<style>
    :root {
        --primary-color: #2563eb;
        --secondary-color: #64748b;
        --success-color: #10b981;
        --warning-color: #f59e0b;
        --danger-color: #ef4444;
        --info-color: #06b6d4;
        --background: #ffffff;
        --surface: #f8fafc;
        --text-primary: #1e293b;
        --text-secondary: #64748b;
        --border: #e2e8f0;
        --shadow: rgba(0, 0, 0, 0.1);
    }

    * {
        margin: 0;
        padding: 0;
        box-sizing: border-box;
    }

    body {
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
        line-height: 1.6;
        color: var(--text-primary);
        background: var(--background);
    }

    .container {
        max-width: 1400px;
        margin: 0 auto;
        padding: 20px;
    }

    /* Header */
    .header {
        background: linear-gradient(135deg, var(--primary-color), #8b5cf6);
        color: white;
        padding: 40px;
        border-radius: 12px;
        margin-bottom: 30px;
        box-shadow: 0 10px 30px var(--shadow);
    }

    .header h1 {
        font-size: 2.5rem;
        margin-bottom: 10px;
    }

    .header .meta {
        opacity: 0.9;
        font-size: 0.95rem;
    }

    .header .score {
        display: inline-block;
        background: rgba(255, 255, 255, 0.2);
        padding: 8px 16px;
        border-radius: 20px;
        margin-top: 15px;
        font-weight: 600;
    }

    /* Navigation */
    .nav {
        background: var(--surface);
        padding: 15px 20px;
        border-radius: 8px;
        margin-bottom: 30px;
        position: sticky;
        top: 20px;
        z-index: 100;
        box-shadow: 0 2px 10px var(--shadow);
    }

    .nav ul {
        list-style: none;
        display: flex;
        gap: 20px;
        flex-wrap: wrap;
    }

    .nav a {
        color: var(--text-primary);
        text-decoration: none;
        padding: 8px 16px;
        border-radius: 6px;
        transition: all 0.3s;
        display: flex;
        align-items: center;
        gap: 8px;
    }

    .nav a:hover {
        background: var(--primary-color);
        color: white;
    }

    .nav a.active {
        background: var(--primary-color);
        color: white;
    }

    /* Sections */
    .section {
        background: white;
        border-radius: 12px;
        padding: 30px;
        margin-bottom: 30px;
        box-shadow: 0 2px 10px var(--shadow);
    }

    .section h2 {
        color: var(--text-primary);
        margin-bottom: 20px;
        padding-bottom: 10px;
        border-bottom: 2px solid var(--border);
        display: flex;
        align-items: center;
        gap: 10px;
    }

    .section h3 {
        color: var(--text-primary);
        margin: 20px 0 15px;
        font-size: 1.2rem;
    }

    /* Tables */
    .table-wrapper {
        overflow-x: auto;
        margin: 20px 0;
    }

    table {
        width: 100%;
        border-collapse: collapse;
        font-size: 0.95rem;
    }

    th {
        background: var(--surface);
        color: var(--text-primary);
        font-weight: 600;
        text-align: left;
        padding: 12px;
        border-bottom: 2px solid var(--border);
    }

    td {
        padding: 12px;
        border-bottom: 1px solid var(--border);
    }

    tr:hover {
        background: var(--surface);
    }

    /* Badges */
    .badge {
        display: inline-block;
        padding: 4px 12px;
        border-radius: 12px;
        font-size: 0.85rem;
        font-weight: 600;
        text-transform: uppercase;
    }

    .badge-critical {
        background: var(--danger-color);
        color: white;
    }

    .badge-high {
        background: #f97316;
        color: white;
    }

    .badge-medium {
        background: var(--warning-color);
        color: white;
    }

    .badge-low {
        background: var(--success-color);
        color: white;
    }

    .badge-info {
        background: var(--info-color);
        color: white;
    }

    /* Charts */
    .chart-container {
        margin: 20px 0;
        padding: 20px;
        background: var(--surface);
        border-radius: 8px;
        min-height: 300px;
    }

    .chart-title {
        font-weight: 600;
        color: var(--text-primary);
        margin-bottom: 15px;
        text-align: center;
    }

    /* Code Snippets */
    .code-snippet {
        background: #1e293b;
        color: #e2e8f0;
        padding: 20px;
        border-radius: 8px;
        margin: 20px 0;
        overflow-x: auto;
        font-family: 'Courier New', monospace;
        font-size: 0.9rem;
        line-height: 1.5;
    }

    .code-snippet .line-number {
        display: inline-block;
        width: 40px;
        color: #64748b;
        text-align: right;
        margin-right: 15px;
        user-select: none;
    }

    .code-snippet .highlight {
        background: rgba(251, 191, 36, 0.2);
        display: block;
    }

    /* Progress Bars */
    .progress {
        height: 24px;
        background: var(--border);
        border-radius: 12px;
        overflow: hidden;
        margin: 10px 0;
    }

    .progress-bar {
        height: 100%;
        background: linear-gradient(90deg, var(--primary-color), #8b5cf6);
        display: flex;
        align-items: center;
        justify-content: center;
        color: white;
        font-size: 0.85rem;
        font-weight: 600;
        transition: width 0.6s ease;
    }

    /* Metrics Grid */
    .metrics-grid {
        display: grid;
        grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
        gap: 20px;
        margin: 20px 0;
    }

    .metric-card {
        background: var(--surface);
        padding: 20px;
        border-radius: 8px;
        text-align: center;
        transition: transform 0.3s;
        position: relative;
    }

    .metric-card:hover {
        transform: translateY(-5px);
        box-shadow: 0 5px 20px var(--shadow);
    }

    /* Tooltip styles */
    .metric-card[data-tooltip]:hover::after {
        content: attr(data-tooltip);
        position: absolute;
        bottom: 100%;
        left: 50%;
        transform: translateX(-50%);
        background: #333;
        color: white;
        padding: 8px 12px;
        border-radius: 6px;
        font-size: 0.85rem;
        z-index: 1000;
        margin-bottom: 10px;
        max-width: 250px;
        white-space: normal;
        text-align: left;
        line-height: 1.4;
    }

    .metric-card[data-tooltip]:hover::before {
        content: "";
        position: absolute;
        bottom: 100%;
        left: 50%;
        transform: translateX(-50%);
        border: 6px solid transparent;
        border-top-color: #333;
        margin-bottom: 4px;
        z-index: 1000;
    }

    .metric-value {
        font-size: 2rem;
        font-weight: 700;
        color: var(--primary-color);
        margin: 10px 0;
    }

    .metric-label {
        color: var(--text-secondary);
        font-size: 0.9rem;
        text-transform: uppercase;
        letter-spacing: 1px;
    }

    /* Collapsible Sections */
    .collapsible {
        cursor: pointer;
        user-select: none;
    }

    .collapsible::before {
        content: '▼';
        display: inline-block;
        margin-right: 8px;
        transition: transform 0.3s;
    }

    .collapsible.collapsed::before {
        transform: rotate(-90deg);
    }

    .collapsible-content {
        max-height: 2000px;
        overflow: hidden;
        transition: max-height 0.3s ease;
    }

    .collapsible-content.collapsed {
        max-height: 0;
    }

    /* Alerts */
    .alert {
        padding: 15px 20px;
        border-radius: 8px;
        margin: 20px 0;
        display: flex;
        align-items: center;
        gap: 15px;
    }

    .alert-success {
        background: #10b98120;
        border-left: 4px solid var(--success-color);
        color: #047857;
    }

    .alert-warning {
        background: #f59e0b20;
        border-left: 4px solid var(--warning-color);
        color: #b45309;
    }

    .alert-danger {
        background: #ef444420;
        border-left: 4px solid var(--danger-color);
        color: #b91c1c;
    }

    .alert-info {
        background: #06b6d420;
        border-left: 4px solid var(--info-color);
        color: #0e7490;
    }

    /* Footer */
    .footer {
        text-align: center;
        padding: 30px;
        color: var(--text-secondary);
        border-top: 1px solid var(--border);
        margin-top: 50px;
    }

    /* Responsive */
    @media (max-width: 768px) {
        .container {
            padding: 10px;
        }

        .header {
            padding: 20px;
        }

        .header h1 {
            font-size: 1.8rem;
        }

        .section {
            padding: 20px;
        }

        .metrics-grid {
            grid-template-columns: 1fr;
        }

        .nav ul {
            flex-direction: column;
            gap: 10px;
        }
    }

    /* Dark Theme */
    @media (prefers-color-scheme: dark) {
        :root {
            --background: #0f172a;
            --surface: #1e293b;
            --text-primary: #f1f5f9;
            --text-secondary: #94a3b8;
            --border: #334155;
            --shadow: rgba(0, 0, 0, 0.3);
        }

        .code-snippet {
            background: #0f172a;
        }
    }

    /* Print Styles */
    @media print {
        .nav {
            display: none;
        }

        .section {
            page-break-inside: avoid;
            box-shadow: none;
            border: 1px solid var(--border);
        }

        .chart-container {
            page-break-inside: avoid;
        }
    }
</style>
"""

    # Add custom CSS if provided
    if self.custom_css:
        base_styles += f"\n<style>\n{self.custom_css}\n</style>"

    # Add theme-specific styles
    if self.theme == "dark":
        base_styles += self._get_dark_theme_styles()
    elif self.theme == "corporate":
        base_styles += self._get_corporate_theme_styles()

    return base_styles
get_scripts
Python
get_scripts() -> str

Get JavaScript libraries and scripts.

RETURNSDESCRIPTION
str

Script tags

TYPE:str

Source code in tenets/core/reporting/html_reporter.py
Python
def get_scripts(self) -> str:
    """Get JavaScript libraries and scripts.

    Returns:
        str: Script tags
    """
    scripts = []

    if self.include_charts:
        # Include Chart.js for charts
        scripts.append(
            '<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>'
        )

        # Include Prism.js for code highlighting
        scripts.append(
            '<link href="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css" rel="stylesheet">'
        )
        scripts.append(
            '<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js"></script>'
        )
        scripts.append(
            '<script src="https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js"></script>'
        )

    return "\n    ".join(scripts)
get_navigation
Python
get_navigation(sections: List[ReportSection]) -> str

Generate navigation menu.

PARAMETERDESCRIPTION
sections

Report sections

TYPE:List[ReportSection]

RETURNSDESCRIPTION
str

Navigation HTML

TYPE:str

Source code in tenets/core/reporting/html_reporter.py
Python
def get_navigation(self, sections: List[ReportSection]) -> str:
    """Generate navigation menu.

    Args:
        sections: Report sections

    Returns:
        str: Navigation HTML
    """
    nav_items = []

    for section in sections:
        if section.visible:
            icon = section.icon if section.icon else ""
            # Preserve emoji/icons as-is; HTML is written as UTF-8
            nav_items.append(f'<li><a href="#{section.id}">{icon} {section.title}</a></li>')

    return f"""
<nav class="nav">
    <ul>
        {" ".join(nav_items)}
    </ul>
</nav>
"""

HTMLReporter

Python
HTMLReporter(config: TenetsConfig)

HTML report generator.

Generates standalone HTML reports with rich visualizations and interactive elements from analysis results.

ATTRIBUTEDESCRIPTION
config

Configuration object

logger

Logger instance

template

HTML template generator

Initialize HTML reporter.

PARAMETERDESCRIPTION
config

TenetsConfig instance

TYPE:TenetsConfig

Source code in tenets/core/reporting/html_reporter.py
Python
def __init__(self, config: TenetsConfig):
    """Initialize HTML reporter.

    Args:
        config: TenetsConfig instance
    """
    self.config = config
    self.logger = get_logger(__name__)
    self.template = HTMLTemplate()
Functions
generate
Python
generate(sections: List[ReportSection], metadata: Dict[str, Any], output_path: Path, report_config: ReportConfig) -> Path

Generate HTML report.

PARAMETERDESCRIPTION
sections

Report sections

TYPE:List[ReportSection]

metadata

Report metadata

TYPE:Dict[str, Any]

output_path

Output file path

TYPE:Path

report_config

Report configuration

TYPE:ReportConfig

RETURNSDESCRIPTION
Path

Path to generated report

TYPE:Path

Source code in tenets/core/reporting/html_reporter.py
Python
def generate(
    self,
    sections: List[ReportSection],
    metadata: Dict[str, Any],
    output_path: Path,
    report_config: ReportConfig,
) -> Path:
    """Generate HTML report.

    Args:
        sections: Report sections
        metadata: Report metadata
        output_path: Output file path
        report_config: Report configuration

    Returns:
        Path: Path to generated report
    """
    self.logger.debug(f"Generating HTML report to {output_path}")

    # Set template configuration
    self.template = HTMLTemplate(
        theme=report_config.theme,
        custom_css=self._load_custom_css(report_config.custom_css),
        include_charts=report_config.include_charts,
    )

    # Generate HTML content
    html_content = self._generate_html(sections, metadata, report_config)

    # Ensure output is ASCII-safe for environments that read with
    # platform default encodings (e.g., cp1252 on Windows). Convert
    # non-ASCII characters to HTML entities to avoid decode errors
    # when tests read the file without specifying encoding.
    try:
        safe_content = html_content.encode("ascii", "xmlcharrefreplace").decode("ascii")
    except Exception:
        safe_content = html_content  # Fallback; still write as-is

    # Write to file
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(safe_content)

    self.logger.info(f"HTML report generated: {output_path}")
    return output_path

Functions

create_html_report

Python
create_html_report(sections: List[ReportSection], output_path: Path, title: str = 'Code Analysis Report', config: Optional[TenetsConfig] = None) -> Path

Convenience function to create HTML report.

PARAMETERDESCRIPTION
sections

Report sections

TYPE:List[ReportSection]

output_path

Output path

TYPE:Path

title

Report title

TYPE:strDEFAULT:'Code Analysis Report'

config

Optional configuration

TYPE:Optional[TenetsConfig]DEFAULT:None

RETURNSDESCRIPTION
Path

Path to generated report

TYPE:Path

Source code in tenets/core/reporting/html_reporter.py
Python
def create_html_report(
    sections: List[ReportSection],
    output_path: Path,
    title: str = "Code Analysis Report",
    config: Optional[TenetsConfig] = None,
) -> Path:
    """Convenience function to create HTML report.

    Args:
        sections: Report sections
        output_path: Output path
        title: Report title
        config: Optional configuration

    Returns:
        Path: Path to generated report
    """
    if config is None:
        config = TenetsConfig()

    reporter = HTMLReporter(config)
    report_config = ReportConfig(title=title, format="html")
    metadata = {"title": title, "generated_at": datetime.now().isoformat()}

    return reporter.generate(sections, metadata, output_path, report_config)

create_dashboard

Python
create_dashboard(analysis_results: Dict[str, Any], output_path: Path, config: Optional[TenetsConfig] = None) -> Path

Create an interactive dashboard.

PARAMETERDESCRIPTION
analysis_results

Analysis results

TYPE:Dict[str, Any]

output_path

Output path

TYPE:Path

config

Optional configuration

TYPE:Optional[TenetsConfig]DEFAULT:None

RETURNSDESCRIPTION
Path

Path to dashboard

TYPE:Path

Source code in tenets/core/reporting/html_reporter.py
Python
def create_dashboard(
    analysis_results: Dict[str, Any], output_path: Path, config: Optional[TenetsConfig] = None
) -> Path:
    """Create an interactive dashboard.

    Args:
        analysis_results: Analysis results
        output_path: Output path
        config: Optional configuration

    Returns:
        Path: Path to dashboard
    """
    # Dashboard is a specialized HTML report
    if config is None:
        config = TenetsConfig()

    # Use module-level ReportGenerator symbol so tests can patch it via this module
    generator = ReportGenerator(config) if ReportGenerator is not None else None
    if generator is None:
        # Fallback import if re-export failed for any reason
        from .generator import ReportGenerator as _RG  # type: ignore

        generator = _RG(config)
    report_config = ReportConfig(
        title="Code Analysis Dashboard",
        format="html",
        include_charts=True,
        include_toc=False,
    )

    return generator.generate(analysis_results, output_path, report_config)