mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-12 16:50:57 +01:00
Ranks the codecrafters build-your-own-x projects by difficulty and emits two offline, self-contained HTML pages: a per-guide ladder (~360 tutorials) and a per-category ladder (30 categories), grouped into Beginner/Intermediate/Advanced/ Expert tiers plus an Unsorted bucket. A guide inherits its category's tier; language only nudges order within a tier. Includes a Makefile pipeline (fetch -> parse -> build) and 100%-branch-covered tests. Excludes the generated guide page from codespell since it inlines upstream titles verbatim. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QNRTPNYvoBTQoTEWkYtCJD
198 lines
6.3 KiB
Python
Executable File
198 lines
6.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Parse the build-your-own-x README into a ranked, tiered list of guides.
|
|
|
|
Reads a local copy of the upstream README (``byox-readme.md`` — fetch it with the
|
|
command documented in this folder's ``README.md``, or run ``make build``) and
|
|
writes ``guides.json``.
|
|
|
|
Difficulty model: every guide inherits its category's tier — the dominant
|
|
signal — while its language only nudges the ordering *within* that tier
|
|
(lower-level languages such as C or Rust sort a little harder than Python or
|
|
JavaScript). See ``README.md`` for the full rationale.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
import re
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
HERE = Path(__file__).resolve().parent
|
|
README = HERE / "byox-readme.md"
|
|
OUTPUT = HERE / "guides.json"
|
|
|
|
# Category display name -> (global difficulty rank 1..30, tier). The rank order
|
|
# mirrors the approved per-category ladder; keys match the exact backtick names
|
|
# used in the README headers.
|
|
CATEGORIES: dict[str, tuple[int, str]] = {
|
|
"Command-Line Tool": (1, "beginner"),
|
|
"Bot": (2, "beginner"),
|
|
"Template Engine": (3, "beginner"),
|
|
"Text Editor": (4, "beginner"),
|
|
"Git": (5, "beginner"),
|
|
"Shell": (6, "intermediate"),
|
|
"Web Server": (7, "intermediate"),
|
|
"Search Engine": (8, "intermediate"),
|
|
"BitTorrent Client": (9, "intermediate"),
|
|
"Front-end Framework / Library": (10, "intermediate"),
|
|
"Regex Engine": (11, "intermediate"),
|
|
"Game": (12, "intermediate"),
|
|
"Programming Language": (13, "advanced"),
|
|
"Emulator / Virtual Machine": (14, "advanced"),
|
|
"Neural Network": (15, "advanced"),
|
|
"Memory Allocator": (16, "advanced"),
|
|
"Blockchain / Cryptocurrency": (17, "advanced"),
|
|
"Database": (18, "advanced"),
|
|
"3D Renderer": (19, "advanced"),
|
|
"Physics Engine": (20, "advanced"),
|
|
"Docker": (21, "advanced"),
|
|
"Voxel Engine": (22, "advanced"),
|
|
"Network Stack": (23, "expert"),
|
|
"AI Model": (24, "expert"),
|
|
"Visual Recognition System": (25, "expert"),
|
|
"Augmented Reality": (26, "expert"),
|
|
"Web Browser": (27, "expert"),
|
|
"Processor": (28, "expert"),
|
|
"Operating System": (29, "expert"),
|
|
"Distributed Systems": (30, "expert"),
|
|
}
|
|
|
|
# Rank used for the repo's "Uncategorized" bucket, sorted after every real tier.
|
|
UNSORTED_RANK = 99
|
|
|
|
# Per-language ordering nudge (0.0 = high-level, higher = lower-level / more
|
|
# effort). Only orders guides *within* a category; it never crosses a tier.
|
|
LANG_EFFORT: dict[str, float] = {
|
|
"assembly": 0.9,
|
|
"asm": 0.9,
|
|
"c": 0.8,
|
|
"c++": 0.8,
|
|
"cpp": 0.8,
|
|
"rust": 0.75,
|
|
"zig": 0.75,
|
|
"d": 0.7,
|
|
"nim": 0.6,
|
|
"haskell": 0.6,
|
|
"ocaml": 0.6,
|
|
"go": 0.55,
|
|
"scala": 0.55,
|
|
"c#": 0.5,
|
|
"java": 0.5,
|
|
"kotlin": 0.5,
|
|
"swift": 0.5,
|
|
"f#": 0.5,
|
|
"clojure": 0.45,
|
|
"elixir": 0.4,
|
|
"shell": 0.4,
|
|
"bash": 0.4,
|
|
"r": 0.4,
|
|
"typescript": 0.35,
|
|
"php": 0.35,
|
|
"perl": 0.35,
|
|
"dart": 0.35,
|
|
"javascript": 0.3,
|
|
"node.js": 0.3,
|
|
"node": 0.3,
|
|
"ruby": 0.3,
|
|
"python": 0.3,
|
|
"lua": 0.3,
|
|
"any": 0.25,
|
|
"pseudocode": 0.2,
|
|
}
|
|
|
|
# Default nudge for a language not present in ``LANG_EFFORT``.
|
|
DEFAULT_EFFORT = 0.5
|
|
|
|
HEADER_RE = re.compile(r"^####\s+Build your own\s+`([^`]+)`")
|
|
UNCAT_RE = re.compile(r"^####\s+Uncategorized")
|
|
GUIDE_RE = re.compile(r"^\*\s+\[(.+)\]\((\S+?)\)(.*)$")
|
|
LABEL_RE = re.compile(r"^\*\*(.+?)\*\*:\s*(.*)$")
|
|
|
|
|
|
def split_langs(raw: str) -> list[str]:
|
|
"""Split a ``C# / TypeScript`` language blob into a clean list of names."""
|
|
parts = re.split(r"[/,&]| or ", raw)
|
|
names = [part.strip().strip("*").strip() for part in parts]
|
|
cleaned = [name for name in names if name]
|
|
return cleaned or ["Various"]
|
|
|
|
|
|
def lang_effort(langs: list[str]) -> float:
|
|
"""Return the easiest listed language's nudge (you pick your best one)."""
|
|
values = [LANG_EFFORT.get(lang.lower(), DEFAULT_EFFORT) for lang in langs]
|
|
return min(values) if values else DEFAULT_EFFORT
|
|
|
|
|
|
def clean_title(rest: str) -> str:
|
|
"""Strip surrounding markdown italics/emphasis from a guide title."""
|
|
title = rest.strip()
|
|
title = re.sub(r"^_+", "", title)
|
|
title = re.sub(r"_+$", "", title)
|
|
return title.strip().strip("*").strip()
|
|
|
|
|
|
def parse_guide_line(line: str, category: str) -> dict[str, object] | None:
|
|
"""Parse one ``* [**Lang**: _Title_](url)`` line into a guide record."""
|
|
match = GUIDE_RE.match(line.strip())
|
|
if match is None:
|
|
return None
|
|
label, url, trailer = match.group(1), match.group(2), match.group(3)
|
|
label_match = LABEL_RE.match(label)
|
|
if label_match is not None:
|
|
langs = split_langs(label_match.group(1))
|
|
title = clean_title(label_match.group(2))
|
|
else:
|
|
langs = ["Various"]
|
|
title = clean_title(label)
|
|
rank, tier = CATEGORIES.get(category, (UNSORTED_RANK, "unsorted"))
|
|
return {
|
|
"title": title,
|
|
"url": url,
|
|
"langs": langs,
|
|
"category": category,
|
|
"cat_rank": rank,
|
|
"tier": tier,
|
|
"video": "[video]" in trailer.lower(),
|
|
"score": round(rank * 10 + lang_effort(langs), 2),
|
|
}
|
|
|
|
|
|
def parse_readme(text: str) -> list[dict[str, object]]:
|
|
"""Parse the whole README text into a score-sorted list of guides."""
|
|
guides: list[dict[str, object]] = []
|
|
current: str | None = None
|
|
for line in text.splitlines():
|
|
header = HEADER_RE.match(line)
|
|
if header is not None:
|
|
current = header.group(1).strip()
|
|
continue
|
|
if UNCAT_RE.match(line):
|
|
current = "Uncategorized"
|
|
continue
|
|
if current is None:
|
|
continue
|
|
guide = parse_guide_line(line, current)
|
|
if guide is not None:
|
|
guides.append(guide)
|
|
guides.sort(key=lambda item: (item["score"], str(item["title"]).lower()))
|
|
return guides
|
|
|
|
|
|
def main() -> int:
|
|
"""Parse ``byox-readme.md`` and write ``guides.json``; return an exit code."""
|
|
if not README.exists():
|
|
logger.error("missing %s — fetch it first (see README.md / make build)", README)
|
|
return 1
|
|
guides = parse_readme(README.read_text(encoding="utf-8"))
|
|
OUTPUT.write_text(json.dumps(guides, indent=1), encoding="utf-8")
|
|
logger.info("wrote %s (%d guides)", OUTPUT.name, len(guides))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
raise SystemExit(main())
|