up/main.py
Tolaria 69f69be5e4 fix(action-points): correct nav badges, first-load totals, legibility
- Fix nav badge mis-targeting: scope to .md-nav--primary, skip #anchor
  (TOC) links so 'On this page' entries no longer show the page total;
  detect active-leaf sections via md-nav__item--nested instead of the
  secondary (TOC) nav, so the active page gets its own badge.
- Add build-time totals manifest (main.py -> docs/javascripts/toa-totals.json)
  so nav badges show correct totals on first load before a user has
  visited each page. Fetched once, browser-cached, zero runtime cost.
  Wired into CI before zensical build.
- Exclude fenced code blocks when counting tasks, so the Markdown
  reference doc's example checkboxes don't produce a phantom 0/3 badge.
- Force white text (#fff !important) on completed green pills/badges,
  incl. badge-inside-complete-link, to beat the nav link's green color.
- Convert prose practice/strategy steps into interactive tasklist items.
- Add action-points PRD under planning/.
2026-06-03 23:03:17 +08:00

75 lines
2.5 KiB
Python

#!/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()