#!/usr/bin/env python3 """Generate the Tree of Ascension action-points totals manifest. Scans docs/**/*.md for Markdown tasklist items (`- [ ]` / `- [x]`), counts them per page, and writes docs/javascripts/toa-totals.json mapping each page's site URL -> total task count. The action-points JS (docs/javascripts/extra.js) fetches this manifest once on first load so nav badges show correct totals BEFORE a user has visited every page. Counts are fully static (known from the Markdown), so this has zero runtime cost — it's a build-time artifact. Run before `zensical build`: python3 main.py """ from __future__ import annotations import json import re from pathlib import Path ROOT = Path(__file__).resolve().parent DOCS = ROOT / "docs" OUT = DOCS / "javascripts" / "toa-totals.json" # Matches a Markdown tasklist item: optional indent, list marker, [ ] or [x]. TASK_RE = re.compile(r"^\s*[-*+]\s+\[[ xX]\]\s+", re.MULTILINE) # Matches fenced code blocks (``` or ~~~), incl. language hint, non-greedy. # These contain tasklist *examples* (e.g. the Markdown reference doc) that render # as plain text, NOT interactive checkboxes — so we strip them before counting. FENCE_RE = re.compile(r"^([ \t]*)(`{3,}|~{3,}).*?\n.*?^\1\2[ \t]*$", re.MULTILINE | re.DOTALL) def count_tasks(text: str) -> int: """Count real (interactive) tasklist items, excluding fenced code examples.""" stripped = FENCE_RE.sub("", text) return len(TASK_RE.findall(stripped)) def url_for(md_path: Path) -> str: """Map a docs/*.md path to its built site URL (directory-style URLs). docs/index.md -> / docs/practices/index.md -> /practices/ docs/practices/daily-sadhana.md-> /practices/daily-sadhana/ """ rel = md_path.relative_to(DOCS).with_suffix("") parts = list(rel.parts) if parts and parts[-1] == "index": parts = parts[:-1] path = "/" + "/".join(parts) if not path.endswith("/"): path += "/" return path def main() -> None: totals: dict[str, int] = {} for md in sorted(DOCS.rglob("*.md")): text = md.read_text(encoding="utf-8") count = count_tasks(text) if count: totals[url_for(md)] = count OUT.parent.mkdir(parents=True, exist_ok=True) OUT.write_text(json.dumps(totals, indent=2, sort_keys=True) + "\n", encoding="utf-8") print(f"toa-totals: wrote {len(totals)} page(s) -> {OUT.relative_to(ROOT)}") for url, n in sorted(totals.items()): print(f" {url} {n}") if __name__ == "__main__": main()