Source code for report

"""Build an offline HTML report from TextLab JSON records."""

import argparse
import json
from html import escape
from pathlib import Path


[docs] def render_report(records: list[dict]) -> str: """Render statistics as an HTML table and per-document bar charts. Input must follow the schema produced by analyze_file. All textual values are escaped. Word totals include duplicates between documents. :param records: Analysis records in the desired display order. :returns: A complete, self-contained HTML page. """ total = sum(record["total_words"] for record in records) rows = [] cards = [] for record in records: name = escape(record["source"]) rows.append( f"<tr><td>{name}</td><td>{record['total_words']}</td>" f"<td>{record['unique_words']}</td></tr>" ) maximum = max((item["count"] for item in record["top_words"]), default=1) bars = [] for item in record["top_words"]: width = item["count"] / maximum * 100 word = escape(item["word"]) bars.append( f'<li><span>{word}</span><div class="track">' f'<div class="bar" style="width:{width:.2f}%"></div></div>' f'<strong>{item["count"]}</strong></li>' ) chart = "<ul>" + "".join(bars) + "</ul>" if bars else "<p>No words found.</p>" cards.append(f"<section><h2>{name}</h2>{chart}</section>") return """<!doctype html> <html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"> <title>TextLab — document report</title><style> body{font:17px/1.6 system-ui,sans-serif;color:#153b45;background:#f2f5f3;margin:0} main{max-width:1050px;margin:auto;padding:40px 24px}h1{font-size:42px;margin:0} .lead{color:#4d666c}table{width:100%;border-collapse:collapse;background:white} th,td{text-align:left;padding:12px;border-bottom:1px solid #dae4df} .cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:20px;margin-top:24px} section{background:white;border:1px solid #dae4df;border-radius:12px;padding:18px} h2{font-size:21px}ul{list-style:none;padding:0}li{display:grid;grid-template-columns:90px 1fr 30px;gap:8px;align-items:center;margin:9px 0} li span{overflow-wrap:anywhere}.track{background:#edf2ef;border-radius:4px}.bar{height:13px;background:#16806c;border-radius:4px} .note{font-size:14px;color:#4d666c;margin-top:24px} </style><main><p>TEXTLAB / REPRODUCIBLE ANALYSIS</p><h1>What is in these documents?</h1> """ + f"""<p class="lead">{len(records)} documents · {total} total word occurrences</p> <table><thead><tr><th>Document</th><th>Words</th><th>Unique words</th></tr></thead> <tbody>{''.join(rows)}</tbody></table><div class="cards">{''.join(cards)}</div> <p class="note">Raw Markdown analysis: links and code are included. Common words are not excluded. Bars use a separate scale for each document. Frequency is not a measure of document quality.</p> </main></html>"""
[docs] def write_report(input_dir: Path, output: Path) -> int: """Load all JSON files in a directory and write an HTML report. :param input_dir: Directory containing TextLab JSON files only. :param output: Destination HTML file. :returns: Number of documents included. :raises ValueError: If no JSON files exist or a schema version is unknown. """ files = sorted(input_dir.glob("*.json")) if not files: raise ValueError(f"No JSON results found in {input_dir}; run analyze.py first") records = [json.loads(path.read_text(encoding="utf-8")) for path in files] if any(record.get("schema_version") != 1 for record in records): raise ValueError("Unsupported analysis schema; regenerate results with analyze.py") html = render_report(records) output.parent.mkdir(parents=True, exist_ok=True) output.write_text(html, encoding="utf-8") return len(records)
def main() -> None: parser = argparse.ArgumentParser(description="Build an HTML report from TextLab JSON files.") parser.add_argument("--input", type=Path, required=True) parser.add_argument("--output", type=Path, required=True) args = parser.parse_args() try: count = write_report(args.input, args.output) except (OSError, UnicodeError, ValueError, KeyError, TypeError) as error: parser.exit(1, f"Report failed: {error}\n") print(f"Report: {count} documents -> {args.output}") if __name__ == "__main__": main()