tokmd-format 1.10.0

Output formatting and serialization (Markdown, JSON, CSV) for tokmd.
Documentation
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>tokmd Analysis Report</title>
    <style>
        :root {
            --bg-primary: #1a1a2e;
            --bg-secondary: #16213e;
            --bg-card: #0f3460;
            --text-primary: #e6e6e6;
            --text-secondary: #a0a0a0;
            --accent: #4c9aff;
            --accent-hover: #357abd;
            --success: #4caf50;
            --warning: #ff9800;
            --danger: #f44336;
            --border: #2a2a4a;
        }
        * { box-sizing: border-box; margin: 0; padding: 0; }
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
            background: var(--bg-primary);
            color: var(--text-primary);
            line-height: 1.6;
            min-height: 100vh;
        }
        .container { max-width: 1400px; margin: 0 auto; padding: 20px; }
        header {
            text-align: center;
            padding: 40px 20px;
            background: linear-gradient(135deg, var(--bg-secondary), var(--bg-card));
            border-bottom: 1px solid var(--border);
            margin-bottom: 30px;
        }
        header h1 { font-size: 2.5rem; margin-bottom: 10px; }
        header .timestamp { color: var(--text-secondary); font-size: 0.9rem; }
        .metrics-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }
        .metric-card {
            background: var(--bg-card);
            border-radius: 12px;
            padding: 20px;
            text-align: center;
            border: 1px solid var(--border);
            transition: transform 0.2s, box-shadow 0.2s;
        }
        .metric-card:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 20px rgba(76, 154, 255, 0.2);
        }
        .metric-card .value {
            font-size: 2rem;
            font-weight: bold;
            color: var(--accent);
            display: block;
        }
        .metric-card .label {
            color: var(--text-secondary);
            font-size: 0.85rem;
            text-transform: uppercase;
            letter-spacing: 1px;
        }
        .section {
            background: var(--bg-secondary);
            border-radius: 12px;
            padding: 24px;
            margin-bottom: 24px;
            border: 1px solid var(--border);
        }
        .section h2 {
            font-size: 1.3rem;
            margin-bottom: 20px;
            padding-bottom: 10px;
            border-bottom: 2px solid var(--accent);
            display: inline-block;
        }
        #treemap {
            width: 100%;
            height: 400px;
            background: var(--bg-primary);
            border-radius: 8px;
            overflow: hidden;
        }
        .treemap-cell {
            position: absolute;
            overflow: hidden;
            border: 1px solid var(--bg-primary);
            transition: opacity 0.2s;
            cursor: pointer;
        }
        .treemap-cell:hover { opacity: 0.85; }
        .treemap-label {
            padding: 4px 6px;
            font-size: 11px;
            color: white;
            text-shadow: 0 1px 2px rgba(0,0,0,0.5);
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
        }
        .search-box {
            width: 100%;
            padding: 12px 16px;
            font-size: 1rem;
            background: var(--bg-primary);
            border: 1px solid var(--border);
            border-radius: 8px;
            color: var(--text-primary);
            margin-bottom: 16px;
        }
        .search-box:focus {
            outline: none;
            border-color: var(--accent);
            box-shadow: 0 0 0 3px rgba(76, 154, 255, 0.2);
        }
        table {
            width: 100%;
            border-collapse: collapse;
            font-size: 0.9rem;
        }
        th, td {
            padding: 12px;
            text-align: left;
            border-bottom: 1px solid var(--border);
        }
        th {
            background: var(--bg-card);
            color: var(--accent);
            font-weight: 600;
            text-transform: uppercase;
            font-size: 0.8rem;
            letter-spacing: 0.5px;
            cursor: pointer;
        }
        th:hover { background: var(--accent); color: white; }
        tr:hover { background: rgba(76, 154, 255, 0.1); }
        .num { text-align: right; font-family: 'SF Mono', Monaco, monospace; }
        .path { font-family: 'SF Mono', Monaco, monospace; font-size: 0.85rem; }
        .lang-badge {
            display: inline-block;
            padding: 2px 8px;
            border-radius: 4px;
            font-size: 0.75rem;
            font-weight: 600;
        }
        .hidden { display: none; }
        footer {
            text-align: center;
            padding: 30px;
            color: var(--text-secondary);
            font-size: 0.85rem;
        }
        footer a { color: var(--accent); text-decoration: none; }
        footer a:hover { text-decoration: underline; }
    </style>
</head>
<body>
    <header>
        <h1>tokmd Analysis Report</h1>
        <div class="timestamp">Generated: {{TIMESTAMP}}</div>
    </header>

    <div class="container">
        <div class="metrics-grid">
            {{METRICS_CARDS}}
        </div>

        <div class="section">
            <h2>Code Distribution</h2>
            <div id="treemap"></div>
        </div>

        <div class="section">
            <h2>Files</h2>
            <input type="text" class="search-box" id="search" placeholder="Filter by path, module, or language...">
            <table id="files-table">
                <thead>
                    <tr>
                        <th data-sort="path">Path</th>
                        <th data-sort="module">Module</th>
                        <th data-sort="lang">Lang</th>
                        <th data-sort="lines" class="num">Lines</th>
                        <th data-sort="code" class="num">Code</th>
                        <th data-sort="tokens" class="num">Tokens</th>
                        <th data-sort="bytes" class="num">Bytes</th>
                    </tr>
                </thead>
                <tbody>
                    {{TABLE_ROWS}}
                </tbody>
            </table>
        </div>
    </div>

    <footer>
        Generated by <a href="https://github.com/EffortlessMetrics/tokmd">tokmd</a>
    </footer>

    <script>
    const REPORT_DATA = {{REPORT_JSON}};

    // Language colors
    const LANG_COLORS = {
        'Rust': '#dea584',
        'JavaScript': '#f1e05a',
        'TypeScript': '#3178c6',
        'Python': '#3572A5',
        'Go': '#00ADD8',
        'Java': '#b07219',
        'C': '#555555',
        'C++': '#f34b7d',
        'C#': '#178600',
        'Ruby': '#701516',
        'PHP': '#4F5D95',
        'Swift': '#F05138',
        'Kotlin': '#A97BFF',
        'Scala': '#c22d40',
        'HTML': '#e34c26',
        'CSS': '#563d7c',
        'SCSS': '#c6538c',
        'JSON': '#292929',
        'YAML': '#cb171e',
        'TOML': '#9c4221',
        'Markdown': '#083fa1',
        'Shell': '#89e051',
        'SQL': '#e38c00',
    };

    function getLangColor(lang) {
        return LANG_COLORS[lang] || '#' + Math.floor(Math.random()*16777215).toString(16).padStart(6, '0');
    }

    // Treemap implementation (squarify algorithm)
    function squarify(data, x, y, width, height) {
        if (data.length === 0 || width <= 0 || height <= 0) return [];

        const total = data.reduce((sum, d) => sum + d.value, 0);
        if (total === 0) return [];

        const rects = [];
        let remaining = [...data];
        let cx = x, cy = y, cw = width, ch = height;

        while (remaining.length > 0) {
            const vertical = ch > cw;
            const side = vertical ? ch : cw;
            const scale = (cw * ch) / total;

            let row = [];
            let rowArea = 0;
            let worst = Infinity;

            for (const item of remaining) {
                const testRow = [...row, item];
                const testArea = rowArea + item.value * scale;
                const testWorst = getWorst(testRow, testArea, side, scale);

                if (testWorst <= worst) {
                    row = testRow;
                    rowArea = testArea;
                    worst = testWorst;
                } else {
                    break;
                }
            }

            // Layout row
            const rowSide = rowArea / side;
            let offset = 0;

            for (const item of row) {
                const itemSize = (item.value * scale) / rowSide;
                if (vertical) {
                    rects.push({ ...item, x: cx, y: cy + offset, w: rowSide, h: itemSize });
                } else {
                    rects.push({ ...item, x: cx + offset, y: cy, w: itemSize, h: rowSide });
                }
                offset += itemSize;
            }

            // Update remaining area
            if (vertical) {
                cx += rowSide;
                cw -= rowSide;
            } else {
                cy += rowSide;
                ch -= rowSide;
            }

            remaining = remaining.slice(row.length);
        }

        return rects;
    }

    function getWorst(row, area, side, scale) {
        if (row.length === 0) return Infinity;
        const s2 = side * side;
        let min = Infinity, max = 0;
        for (const item of row) {
            const v = item.value * scale;
            min = Math.min(min, v);
            max = Math.max(max, v);
        }
        return Math.max((s2 * max) / (area * area), (area * area) / (s2 * min));
    }

    function renderTreemap() {
        const container = document.getElementById('treemap');
        const width = container.offsetWidth;
        const height = container.offsetHeight;
        container.innerHTML = '';
        container.style.position = 'relative';

        // Aggregate by module
        const moduleData = {};
        for (const file of REPORT_DATA.files || []) {
            const mod = file.module || '(root)';
            if (!moduleData[mod]) moduleData[mod] = { name: mod, value: 0, lang: file.lang };
            moduleData[mod].value += file.code || file.lines || 1;
        }

        const data = Object.values(moduleData).sort((a, b) => b.value - a.value).slice(0, 50);
        const rects = squarify(data, 0, 0, width, height);

        for (const rect of rects) {
            const div = document.createElement('div');
            div.className = 'treemap-cell';
            div.style.left = rect.x + 'px';
            div.style.top = rect.y + 'px';
            div.style.width = rect.w + 'px';
            div.style.height = rect.h + 'px';
            div.style.background = getLangColor(rect.lang);

            const label = document.createElement('div');
            label.className = 'treemap-label';
            label.textContent = rect.name + ' (' + rect.value.toLocaleString() + ')';
            div.appendChild(label);

            div.title = rect.name + ': ' + rect.value.toLocaleString() + ' lines';
            container.appendChild(div);
        }
    }

    // Table filtering
    document.getElementById('search').addEventListener('input', function(e) {
        const filter = e.target.value.toLowerCase();
        const rows = document.querySelectorAll('#files-table tbody tr');
        rows.forEach(row => {
            const text = row.textContent.toLowerCase();
            row.classList.toggle('hidden', filter && !text.includes(filter));
        });
    });

    // Table sorting
    let sortCol = null, sortAsc = true;
    document.querySelectorAll('#files-table th').forEach(th => {
        th.addEventListener('click', function() {
            const col = this.dataset.sort;
            if (sortCol === col) sortAsc = !sortAsc;
            else { sortCol = col; sortAsc = true; }

            const tbody = document.querySelector('#files-table tbody');
            const rows = Array.from(tbody.querySelectorAll('tr'));

            rows.sort((a, b) => {
                const aVal = a.querySelector(`[data-${col}]`)?.dataset[col] || a.cells[getColIndex(col)].textContent;
                const bVal = b.querySelector(`[data-${col}]`)?.dataset[col] || b.cells[getColIndex(col)].textContent;
                const aNum = parseFloat(aVal.replace(/,/g, ''));
                const bNum = parseFloat(bVal.replace(/,/g, ''));
                if (!isNaN(aNum) && !isNaN(bNum)) {
                    return sortAsc ? aNum - bNum : bNum - aNum;
                }
                return sortAsc ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
            });

            rows.forEach(row => tbody.appendChild(row));
        });
    });

    function getColIndex(col) {
        const map = { path: 0, module: 1, lang: 2, lines: 3, code: 4, tokens: 5, bytes: 6 };
        return map[col] || 0;
    }

    // Initialize
    window.addEventListener('load', renderTreemap);
    window.addEventListener('resize', renderTreemap);
    </script>
</body>
</html>