2026-06-03 15:03:17 +00:00
|
|
|
#!/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}")
|
2026-06-02 07:54:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|