#!/usr/bin/env python3
"""Build the SkillHub weekly report site from self-contained report files."""
from __future__ import annotations
import argparse
import json
import shutil
from html import escape
from pathlib import Path
def load_manifest(source: Path) -> tuple[str, list[dict[str, str]]]:
manifest_path = source / "reports.json"
payload = json.loads(manifest_path.read_text(encoding="utf-8"))
latest = payload.get("latest")
reports = payload.get("reports")
if not isinstance(latest, str) or not latest:
raise ValueError("reports.json must define a non-empty latest week")
if not isinstance(reports, list) or not reports:
raise ValueError("reports.json must contain at least one report")
required = {"week", "title", "period", "snapshot", "path"}
normalized: list[dict[str, str]] = []
for index, report in enumerate(reports):
if not isinstance(report, dict) or not required.issubset(report):
missing = required - set(report) if isinstance(report, dict) else required
raise ValueError(f"report #{index + 1} is missing fields: {sorted(missing)}")
normalized.append({key: str(report[key]) for key in required})
weeks = {report["week"] for report in normalized}
if latest not in weeks:
raise ValueError(f"latest week {latest!r} is not present in reports")
return latest, sorted(normalized, key=lambda item: item["week"], reverse=True)
def render_archive(latest: str, reports: list[dict[str, str]]) -> str:
rows = "\n".join(
f"""
{escape(report['week'])}
{'最新' if report['week'] == latest else ''}
{escape(report['title'])}
{escape(report['period'])} · 快照 {escape(report['snapshot'])}
→
"""
for report in reports
)
return f"""
SkillHub 开源周报归档
SkillHub 开源周报归档
按统计周期倒序查看历期开源周报。
当前共收录 {len(reports)} 期,最新一期为 {escape(latest)}。
返回最新周报
"""
def build(source: Path, output: Path) -> None:
latest, reports = load_manifest(source)
latest_report = next(report for report in reports if report["week"] == latest)
latest_source = source / latest_report["path"] / "index.html"
if not latest_source.is_file():
raise FileNotFoundError(f"latest report not found: {latest_source}")
for report in reports:
report_file = source / report["path"] / "index.html"
if not report_file.is_file():
raise FileNotFoundError(f"report not found: {report_file}")
if output.exists():
shutil.rmtree(output)
shutil.copytree(source, output)
latest_html = latest_source.read_text(encoding="utf-8")
latest_html = latest_html.replace('href="../../archive.html"', 'href="./archive.html"')
(output / "index.html").write_text(latest_html, encoding="utf-8")
(output / "archive.html").write_text(
render_archive(latest, reports),
encoding="utf-8",
)
(output / ".nojekyll").write_text("", encoding="utf-8")
print(f"Built {len(reports)} report(s); latest={latest}; output={output}")
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, default=Path("site"))
parser.add_argument("--output", type=Path, default=Path("_site"))
args = parser.parse_args()
build(args.source.resolve(), args.output.resolve())
if __name__ == "__main__":
main()