waddling-errors 0.7.3

Structured, secure-by-default diagnostic codes for distributed systems with no_std and role-based documentation
Documentation
//! HTML template for documentation generation (WDP Part 10 Conformant - v3)

use super::{customization::HtmlCustomization, get_default_css, get_default_js};
use std::{format, string::String};

/// Generate HTML template with inline JSON data
/// Uses v3 grid layout with WDP Part 10 colors (light + dark mode)
pub(super) fn generate_template(
    role_badge: &str,
    project_name: &str,
    version: &str,
    data_json: &str,
    languages: &[&str],
    customization: Option<&HtmlCustomization>,
) -> String {
    // Generate Prism.js script tags only for languages actually used
    let prism_scripts = if languages.is_empty() {
        String::new()
    } else {
        let mut scripts = String::from(
            "    <!-- Optional Prism.js for syntax highlighting (loaded async, non-blocking) -->\n\
                 <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css\" \n\
                   crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\" media=\"print\" onload=\"this.media='all'\">\n\
                 <script defer src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js\" \n\
                     crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>\n",
        );

        for lang in languages {
            let normalized = lang.to_lowercase();
            scripts.push_str(&format!(
                "    <script defer src=\"https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-{}.min.js\" \n\
                         crossorigin=\"anonymous\" referrerpolicy=\"no-referrer\"></script>\n",
                normalized
            ));
        }
        scripts
    };

    format!(
        r#"<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{project_name} Error Documentation</title>
    <style>
{css}
    </style>
{prism_scripts}
</head>
<body>
    <div class="app">
        <!-- HEADER -->
        <header class="header">
            {custom_header}
            <div class="header-left">
                {logo}
                <div class="header-brand">
                    <span class="brand-name">waddling-errors</span>
                    <span class="brand-divider">/</span>
                    <span class="project-name">{project_name}</span>
                </div>
                {role_badge}
            </div>
            <div class="header-right">
                <span class="version">v{version}</span>
                <button id="formatToggle" class="format-toggle" title="Toggle numeric/semantic format">🔢 001</button>
                <button id="themeToggle" class="theme-toggle" title="Toggle light/dark theme">🌙</button>
                <button id="hamburgerBtn" class="hamburger-btn" title="Toggle sidebar" aria-label="Toggle sidebar">☰</button>
            </div>
        </header>

        <!-- MAIN CONTENT -->
        <main class="main">
            <!-- Search Section -->
            <section class="search-section">
                <div class="search-input-row">
                    <div class="search-box">
                        <span class="search-icon">🔍</span>
                        <input 
                            type="text" 
                            id="searchInput"
                            class="search-input"
                            placeholder="Search: *.*.*.* or 'authentication' or #hash"
                            value="*.*.*.*"
                            aria-label="Search error codes"
                            autocomplete="off"
                        >
                        <button id="searchClear" class="search-clear-btn" title="Clear search" aria-label="Clear search">❌</button>
                    </div>
                    <button id="queryBuilderToggle" class="query-builder-toggle" title="Build Query">
                        ⚡ Builder
                    </button>
                </div>

                <!-- Active Filter Chips - shows current category/tag filters -->
                <div id="activeFilters" class="active-filters" aria-live="polite"></div>

                <!-- Autocomplete Dropdown -->
                <div id="autocompleteDropdown" class="autocomplete-dropdown" role="listbox"></div>

                <!-- Query Builder Dropdown (inline) -->
                <div id="queryBuilderDropdown" class="query-builder-dropdown">
                    <div class="builder-header">
                        <div class="builder-preview">
                            <span class="builder-preview-label">Pattern:</span>
                            <span id="builderPreview" class="builder-preview-value">*.*.*.*</span>
                        </div>
                        <button id="builderCloseBtn" class="builder-close-btn" title="Close builder" aria-label="Close query builder">×</button>
                    </div>
                    
                    <!-- Mobile tabs for column selection -->
                    <div class="builder-tabs" id="builderTabs">
                        <button class="builder-tab active" data-col="severity">Severity</button>
                        <button class="builder-tab" data-col="component">Component</button>
                        <button class="builder-tab" data-col="primary">Primary</button>
                        <button class="builder-tab" data-col="sequence">Sequence</button>
                    </div>
                    
                    <div class="builder-columns">
                        <div class="builder-column active" data-col="severity">
                            <div class="column-title title-severity"><span>Severity</span><span class="column-current" id="sevCurrent">[*]</span></div>
                            <div class="column-options" id="sevCol"></div>
                        </div>
                        <div class="builder-column" data-col="component">
                            <div class="column-title title-component"><span>Component</span><span class="column-current" id="compCurrent">[*]</span></div>
                            <div class="column-options" id="compCol"></div>
                        </div>
                        <div class="builder-column" data-col="primary">
                            <div class="column-title title-primary"><span>Primary</span><span class="column-current" id="primCurrent">[*]</span></div>
                            <div class="column-options" id="primCol"></div>
                        </div>
                        <div class="builder-column" data-col="sequence">
                            <div class="column-title title-sequence"><span>Sequence</span><span class="column-current" id="seqCurrent">[*]</span></div>
                            <div class="column-options" id="seqCol"></div>
                        </div>
                    </div>
                    <div class="builder-actions">
                        <button id="builderClearBtn" class="btn-clear">Clear</button>
                        <button id="builderSearchBtn" class="btn-search">Search</button>
                    </div>
                </div>

                <!-- Severity Filter Bar -->
                <div id="severityFilterBar" class="severity-filter-bar" role="group" aria-label="Filter by severity"></div>

                <!-- View As Role Switcher (only shown in dev/internal docs) -->
                <div id="visibilityRow" class="visibility-row" role="group" aria-label="View as role"></div>
            </section>

            <!-- Results Section -->
            <section class="results-section">
                <div class="results-header">
                    <span id="resultCount" class="result-count" role="status" aria-live="polite">0 errors</span>
                </div>
                <div id="results" class="results-list" role="region" aria-label="Search results"></div>
            </section>
        </main>

        <!-- SIDEBAR -->
        <aside class="sidebar" id="sidebar">
            <section class="sidebar-section">
                <h2 class="sidebar-title">Overview</h2>
                <div class="stats-row" id="statsRow">
                    <div class="stat-item">
                        <div class="stat-value" id="statErrors">0</div>
                        <div class="stat-label">Errors</div>
                    </div>
                    <div class="stat-item">
                        <div class="stat-value" id="statComponents">0</div>
                        <div class="stat-label">Components</div>
                    </div>
                    <div class="stat-item">
                        <div class="stat-value" id="statDeprecated">0</div>
                        <div class="stat-label">Deprecated</div>
                    </div>
                </div>
            </section>
            <section class="sidebar-section">
                <h2 class="sidebar-title">Browse</h2>
                <div class="browse-tabs" id="browseTabs">
                    <button class="browse-tab active" data-tab="components">Components</button>
                    <button class="browse-tab" data-tab="primaries">Primaries</button>
                    <button class="browse-tab" data-tab="sequences">Sequences</button>
                </div>
                <div class="browse-list" id="browseList"></div>
            </section>
            <section class="sidebar-section" id="detailSection" style="display: none;">
                <h2 class="sidebar-title">Details</h2>
                <div class="detail-panel" id="detailPanel">
                    <div id="detailContent" class="detail-content"></div>
                </div>
            </section>
        </aside>
    </div>

    <!-- Toast Container -->
    <div id="toastContainer" class="toast-container"></div>

    {custom_footer}

    <script>
        // Error database (inline for file:// compatibility)
        const DATA = {data_json};

        const errorDatabase = DATA.errors || [];
        const componentsDatabase = DATA.components || {{}};
        const primariesDatabase = DATA.primaries || {{}};
        const sequencesDatabase = DATA.sequences || {{}};
        const severities = DATA.severities || {{}};

{js}
    </script>
</body>
</html>"#,
        project_name = project_name,
        css = _generate_css(customization),
        prism_scripts = prism_scripts,
        custom_header = _generate_header(customization),
        logo = _generate_logo(customization),
        role_badge = role_badge,
        version = version,
        custom_footer = _generate_footer(customization),
        data_json = data_json,
        js = _generate_js(customization)
    )
}

/// Generate CSS (default + customizations)
fn _generate_css(customization: Option<&HtmlCustomization>) -> String {
    if let Some(custom) = customization {
        if let Some(ref replace) = custom.replace_css {
            replace.clone()
        } else {
            let mut css = get_default_css();
            let vars = custom.generate_css_variables();
            if !vars.is_empty() {
                css.push_str("\n/* Custom Theme Variables */\n");
                css.push_str(&vars);
            }
            if let Some(ref custom_css) = custom.custom_css {
                css.push_str("\n/* Custom User Styles */\n");
                css.push_str(custom_css);
            }
            css
        }
    } else {
        get_default_css()
    }
}

/// Generate JavaScript (default + customizations)
fn _generate_js(customization: Option<&HtmlCustomization>) -> String {
    if let Some(custom) = customization {
        if let Some(ref replace) = custom.replace_js {
            replace.clone()
        } else {
            let mut js = get_default_js();
            if let Some(ref custom_js) = custom.custom_js {
                js.push_str("\n/* Custom User JavaScript */\n");
                js.push_str(custom_js);
            }
            js
        }
    } else {
        get_default_js()
    }
}

/// Generate logo HTML if provided
fn _generate_logo(customization: Option<&HtmlCustomization>) -> String {
    if let Some(custom) = customization
        && let Some(ref logo_url) = custom.logo_url
    {
        let alt = custom.logo_alt.as_deref().unwrap_or("Project Logo");
        return format!(
            "<img src=\"{}\" alt=\"{}\" style=\"height: 40px; width: auto; vertical-align: middle; margin-right: 10px;\">",
            logo_url, alt
        );
    }
    String::new()
}

/// Generate custom header HTML if provided
fn _generate_header(customization: Option<&HtmlCustomization>) -> String {
    if let Some(custom) = customization
        && let Some(ref header) = custom.header_html
    {
        return header.clone();
    }
    String::new()
}

/// Generate custom footer HTML if provided
fn _generate_footer(customization: Option<&HtmlCustomization>) -> String {
    if let Some(custom) = customization
        && let Some(ref footer) = custom.footer_html
    {
        return footer.clone();
    }
    String::new()
}