feat(byox_ladder): add build-your-own-x difficulty ladder generator
Some checks are pending
Pre-commit checks / pre-commit (push) Waiting to run
Python tests / test (push) Waiting to run

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
This commit is contained in:
Krzysztof Rudnicki 2026-07-17 18:31:21 +02:00
parent 678098e7e5
commit 6342eaf0e2
16 changed files with 1993 additions and 1 deletions

View File

@ -293,7 +293,9 @@ repos:
args:
- --skip=*.json,*.lock,*-lock.yaml,*.min.js,*.min.css,.git,__pycache__,.venv,*.txt
- --ignore-words-list=als,ans,ect,nd,som,sur,te,nam,numer,lew,sie,wil,postion,clen,ther,folow,derrive,ony,tje,noe,theses,crate,doubleclick,wile,tabel,pary,blok,bloc,proces,serwer,parametr,adres,hart,dout,metod,tekst,synonim,grup,mosty,lokal,skalar,milion,nowe,tre,hel,alph,iif
exclude: ^(Bash/ffmpeg-build/|LaTeX/|.*\.geojson$)
# guide-ladder.html inlines upstream build-your-own-x tutorial titles
# verbatim (third-party data, incl. real source typos) — not authored text.
exclude: ^(Bash/ffmpeg-build/|LaTeX/|.*\.geojson$|python_pkg/byox_ladder/guide-ladder\.html$)
# ===========================================================================
# DOCFORMATTER - Format docstrings (disabled - causes recursion errors)

View File

@ -0,0 +1,17 @@
{
"title": "Add byox_ladder difficulty-ladder generator",
"objective": "Add a self-contained tool that ranks the codecrafters build-your-own-x projects by difficulty and emits two offline 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.",
"acceptance_criteria": [
"python_pkg/byox_ladder parses the upstream README into guides.json via parse_guides.py",
"build_pages.py emits standalone guide-ladder.html and category-ladder.html that open via file:// with no network",
"make build regenerates the whole pipeline (fetch -> parse -> build)",
"Tests reach 100% branch coverage on parse_guides.py and build_pages.py",
"All pre-commit hooks pass (ruff ALL, mypy, pylint, bandit, codespell, check-python-location)"
],
"out_of_scope": [
"Vendoring the upstream README (fetched at build time, gitignored)",
"Any change to existing python_pkg packages or repo tooling",
"A per-guide exact 1..N ranking (difficulty is tier-based by design)"
],
"verifier": "pre-commit run --all-files (pre-push stage) and python3 -m pytest python_pkg/byox_ladder/tests"
}

View File

@ -0,0 +1,39 @@
{
"intent": "Add python_pkg/byox_ladder: a generator that ranks the build-your-own-x projects by difficulty and produces two offline HTML ladders (per-guide and per-category), committed as runnable pages.",
"scope": [
"python_pkg/byox_ladder/ (new package: parse_guides.py, build_pages.py, templates, built HTML, tests)",
"docs/superpowers/contracts + evidence artifacts for this change",
"No changes to existing packages, tooling, or the unrelated working-tree edits (i3 config, feedback-appimage)"
],
"changes": [
"parse_guides.py parses the upstream README into guides.json; a guide inherits its category tier, language only nudges within-tier order",
"build_pages.py inlines guides.json into the guide template and wraps both pages in a standalone HTML skeleton",
"Makefile drives fetch -> parse -> build; byox-readme.md and guides.json are gitignored derived artifacts",
"tests/ cover both modules to 100% branch coverage"
],
"verification": [
{
"command": "python3 -m pytest python_pkg/byox_ladder/tests --cov=python_pkg.byox_ladder --cov-branch",
"result": "pass",
"evidence": "21 passed; coverage 100.00% (parse_guides.py 69 stmts/16 branches, build_pages.py 32 stmts/4 branches, __init__ 0) with 0 missing"
},
{
"command": "pre-commit run --files python_pkg/byox_ladder/*.py python_pkg/byox_ladder/tests/*.py",
"result": "pass",
"evidence": "ruff, ruff-format, mypy, pylint, bandit, codespell, check-python-location, and pytest-coverage all reported Passed"
},
{
"command": "make -C python_pkg/byox_ladder build",
"result": "pass",
"evidence": "wrote guides.json (359 guides); wrote guide-ladder.html and category-ladder.html, both starting with <!doctype html>"
}
],
"risks": [
"Upstream README format drift could break the regex parser; parse_guides.py exits non-zero and the committed HTML stays valid until regenerated",
"Difficulty tiers are a subjective judgment; within-tier order is approximate by design"
],
"rollback": [
"git rm -r python_pkg/byox_ladder and delete the two docs/superpowers artifacts, then commit",
"After rollback, run pre-commit run --all-files to confirm the tree is still green"
]
}

4
python_pkg/byox_ladder/.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
# Fetched upstream README (third-party content — not vendored) and the derived
# dataset. The built *-ladder.html pages ARE committed as the runnable output.
byox-readme.md
guides.json

View File

@ -0,0 +1,20 @@
# Regenerate the Build-Your-Own-X difficulty ladders.
#
# make build # fetch README -> parse -> build both standalone pages
# make clean # remove the fetched README copy and derived data
README_URL = https://raw.githubusercontent.com/codecrafters-io/build-your-own-x/master/README.md
.PHONY: build clean
build: guides.json
python3 build_pages.py
guides.json: byox-readme.md parse_guides.py
python3 parse_guides.py
byox-readme.md:
curl -fsSL "$(README_URL)" -o byox-readme.md
clean:
rm -f byox-readme.md guides.json

View File

@ -0,0 +1,48 @@
# Build-Your-Own-X Difficulty Ladder
Two self-contained, offline HTML pages that rank the projects from
[codecrafters-io/build-your-own-x](https://github.com/codecrafters-io/build-your-own-x)
from easiest to hardest:
- **`guide-ladder.html`** — every individual tutorial (~360 guides), grouped
into difficulty tiers and filterable by tier, language, and category.
- **`category-ladder.html`** — the 30 top-level categories, grouped into
Beginner / Intermediate / Advanced / Expert tiers.
Both are single files with all CSS, JS, and data inlined — no server, fonts, or
network needed. Open either directly in a browser (`file://`) and it works
offline, in your OS light/dark theme.
## Difficulty model
Difficulty is a judgment call, scored on three axes: prerequisite domain
knowledge, minimal-version scope, and how much step-by-step guidance exists.
- A guide **inherits its category's tier** — the dominant signal. A Regex-Engine
guide is easier than an Operating-System guide whatever the language.
- **Language only nudges the order _within_ a tier**: lower-level languages
(C, Rust, Assembly) sort a little harder than Python or JavaScript. It never
moves a guide across a tier boundary.
- The **Unsorted** group is the repo's own "Uncategorized" bucket. Its guides
span every level, so they are deliberately left un-ranked rather than forced
into a tier.
The exact head-to-head order inside a tier is therefore approximate by design;
the tier boundaries are the trustworthy part.
## Regenerating
```sh
make build # curl the README -> parse_guides.py -> build_pages.py
make clean # remove the fetched README copy and guides.json
```
The pipeline:
1. `parse_guides.py` — parses a local copy of the upstream README
(`byox-readme.md`) into `guides.json`, applying the difficulty model above.
2. `build_pages.py` — injects `guides.json` into `templates/guide.template.html`
and wraps `templates/category.html`, writing the two standalone pages.
`byox-readme.md` and `guides.json` are derived artifacts and are gitignored; the
built `*-ladder.html` pages are committed as the runnable output.

View File

@ -0,0 +1,4 @@
"""Generate the Build-Your-Own-X difficulty ladder pages.
See ``README.md`` for the difficulty model and regeneration steps.
"""

View File

@ -0,0 +1,80 @@
#!/usr/bin/env python3
"""Build the standalone HTML ladder pages from templates and parsed guide data.
Produces two self-contained, dependency-free pages next to this script:
* ``guide-ladder.html`` — every individual guide, built by injecting
``guides.json`` (written by ``parse_guides.py``) into the guide template.
* ``category-ladder.html`` — the 30-category ladder, a static template.
Both are wrapped in a minimal HTML document skeleton so they open directly via
``file://`` with no server, fonts, or network access. Run ``make build`` to
regenerate the whole pipeline.
"""
from __future__ import annotations
import json
import logging
from pathlib import Path
logger = logging.getLogger(__name__)
HERE = Path(__file__).resolve().parent
TEMPLATES = HERE / "templates"
GUIDES_JSON = HERE / "guides.json"
# Placeholder in the guide template that the inlined dataset replaces.
MARKER = "/*__GUIDES__*/"
# Minimal, valid document skeleton added around the published page content
# (claude.ai injects an equivalent wrapper; local files need their own).
SKELETON_HEAD = (
"<!doctype html>\n"
'<html lang="en">\n'
"<head>\n"
'<meta charset="utf-8">\n'
'<meta name="viewport" content="width=device-width, initial-scale=1">\n'
)
SKELETON_TAIL = "\n</html>\n"
def standalone(content: str) -> str:
"""Wrap page content in a minimal HTML document skeleton."""
return SKELETON_HEAD + content + SKELETON_TAIL
def build_guide_page() -> str:
"""Inject ``guides.json`` into the guide template; return standalone HTML."""
guides = json.loads(GUIDES_JSON.read_text(encoding="utf-8"))
# Compact payload; guard any "</" so inline data can't close the script tag.
payload = json.dumps(guides, separators=(",", ":")).replace("</", "<\\/")
template = (TEMPLATES / "guide.template.html").read_text(encoding="utf-8")
if MARKER not in template:
message = f"marker {MARKER!r} not found in guide template"
raise ValueError(message)
return standalone(template.replace(MARKER, f"const GUIDES = {payload};"))
def build_category_page() -> str:
"""Return the standalone category ladder (static content, no data)."""
content = (TEMPLATES / "category.html").read_text(encoding="utf-8")
return standalone(content)
def main() -> int:
"""Write both standalone ladder pages next to this script; return an exit code."""
if not GUIDES_JSON.exists():
logger.error(
"missing %s — run parse_guides.py first (or make build)", GUIDES_JSON
)
return 1
(HERE / "guide-ladder.html").write_text(build_guide_page(), encoding="utf-8")
(HERE / "category-ladder.html").write_text(build_category_page(), encoding="utf-8")
logger.info("wrote guide-ladder.html and category-ladder.html")
return 0
if __name__ == "__main__":
logging.basicConfig(level=logging.INFO, format="%(message)s")
raise SystemExit(main())

View File

@ -0,0 +1,363 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Build Your Own X — Difficulty Ladder</title>
<style>
/* ---- Tokens: light is the base, dark + explicit theme override below ---- */
:root{
--bg:#f4f6f9; --panel:#ffffff; --panel-2:#eef1f6; --border:#dde3ec;
--text:#1a2230; --muted:#5b6676; --faint:#8a94a3;
--accent:#0e7490; --accent-ink:#ffffff; --accent-soft:#0e749018;
--t1:#1a7f37; --t2:#1f6feb; --t3:#9a6700; --t4:#cf222e;
--t1-soft:#1a7f3718; --t2-soft:#1f6feb18; --t3-soft:#9a670018; --t4-soft:#cf222e18;
--shadow:0 1px 2px rgba(16,23,38,.06), 0 8px 24px rgba(16,23,38,.06);
--font-display:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
--font-body:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
@media (prefers-color-scheme:dark){
:root{
--bg:#0d1117; --panel:#161b22; --panel-2:#1c232d; --border:#2b333d;
--text:#c9d4e0; --muted:#8b96a5; --faint:#6b7684;
--accent:#38bdf8; --accent-ink:#05202b; --accent-soft:#38bdf815;
--t1:#3fb950; --t2:#58a6ff; --t3:#d29922; --t4:#f85149;
--t1-soft:#3fb95018; --t2-soft:#58a6ff18; --t3-soft:#d2992218; --t4-soft:#f8514918;
--shadow:0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.35);
}
}
/* Viewer toggle wins over the media query, in both directions */
:root[data-theme="light"]{
--bg:#f4f6f9; --panel:#ffffff; --panel-2:#eef1f6; --border:#dde3ec;
--text:#1a2230; --muted:#5b6676; --faint:#8a94a3;
--accent:#0e7490; --accent-ink:#ffffff; --accent-soft:#0e749018;
--t1:#1a7f37; --t2:#1f6feb; --t3:#9a6700; --t4:#cf222e;
--t1-soft:#1a7f3718; --t2-soft:#1f6feb18; --t3-soft:#9a670018; --t4-soft:#cf222e18;
--shadow:0 1px 2px rgba(16,23,38,.06), 0 8px 24px rgba(16,23,38,.06);
}
:root[data-theme="dark"]{
--bg:#0d1117; --panel:#161b22; --panel-2:#1c232d; --border:#2b333d;
--text:#c9d4e0; --muted:#8b96a5; --faint:#6b7684;
--accent:#38bdf8; --accent-ink:#05202b; --accent-soft:#38bdf815;
--t1:#3fb950; --t2:#58a6ff; --t3:#d29922; --t4:#f85149;
--t1-soft:#3fb95018; --t2-soft:#58a6ff18; --t3-soft:#d2992218; --t4-soft:#f8514918;
--shadow:0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.35);
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0; background:var(--bg); color:var(--text);
font-family:var(--font-body); line-height:1.55;
font-size:16px; letter-spacing:.01em;
}
a{color:inherit}
.wrap{max-width:1200px; margin:0 auto; padding:clamp(20px,4vw,44px) clamp(16px,4vw,40px) 64px}
/* ---- Header ---- */
header{margin-bottom:28px}
.eyebrow{
font-family:var(--font-display); font-size:.72rem; letter-spacing:.22em;
text-transform:uppercase; color:var(--accent); margin:0 0 10px;
display:flex; align-items:center; gap:10px;
}
.eyebrow::before{content:"//"; opacity:.55}
h1{
font-family:var(--font-display); font-weight:650;
font-size:clamp(1.9rem,5.4vw,3.1rem); line-height:1.02;
letter-spacing:-.01em; margin:0 0 14px; text-wrap:balance;
}
h1 .x{color:var(--accent)}
.lede{
max-width:60ch; color:var(--muted); font-size:1.02rem; margin:0 0 4px;
}
.lede a{color:var(--accent); text-underline-offset:3px}
/* ---- Scoring axes note ---- */
.axes{
display:flex; flex-wrap:wrap; gap:10px; margin-top:20px;
}
.axis{
background:var(--panel); border:1px solid var(--border); border-radius:10px;
padding:10px 13px; flex:1 1 200px; box-shadow:var(--shadow);
}
.axis b{display:block; font-size:.82rem; font-family:var(--font-display); letter-spacing:.02em}
.axis span{font-size:.8rem; color:var(--muted)}
/* ---- Layout ---- */
.layout{display:grid; grid-template-columns:1fr; gap:26px; margin-top:30px}
@media (min-width:900px){ .layout{grid-template-columns:230px 1fr; gap:34px} }
aside{align-self:start}
@media (min-width:900px){ aside{position:sticky; top:22px} }
.ctl-group{margin-bottom:22px}
.ctl-label{
font-family:var(--font-display); font-size:.7rem; letter-spacing:.16em;
text-transform:uppercase; color:var(--faint); margin:0 0 10px;
}
select{
width:100%; font-family:var(--font-body); font-size:.9rem; color:var(--text);
background:var(--panel); border:1px solid var(--border); border-radius:9px;
padding:9px 11px; cursor:pointer;
}
.chips{display:flex; flex-direction:column; gap:8px}
.chip{
display:flex; align-items:center; gap:9px; width:100%; cursor:pointer;
font-family:var(--font-body); font-size:.88rem; text-align:left;
background:var(--panel); color:var(--text);
border:1px solid var(--border); border-radius:9px; padding:8px 11px;
transition:background .15s, border-color .15s, opacity .15s;
}
.chip .dot{width:10px; height:10px; border-radius:3px; flex:none; background:var(--tc)}
.chip .n{margin-left:auto; font-family:var(--font-display); font-size:.78rem; color:var(--muted); font-variant-numeric:tabular-nums}
.chip[aria-pressed="false"]{opacity:.4; background:transparent}
.chip:hover{border-color:var(--tc)}
.chip:focus-visible, select:focus-visible, .reset:focus-visible{outline:2px solid var(--accent); outline-offset:2px}
.reset{
background:none; border:none; color:var(--accent); cursor:pointer;
font-family:var(--font-display); font-size:.76rem; letter-spacing:.04em;
padding:6px 0; margin-top:4px;
}
.count{
font-family:var(--font-display); font-size:.76rem; color:var(--faint);
letter-spacing:.04em; margin-top:16px;
}
.count b{color:var(--text)}
/* ---- Cards ---- */
.grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(255px,1fr)); gap:16px}
.card{
position:relative; background:var(--panel); border:1px solid var(--border);
border-left:4px solid var(--tc); border-radius:12px;
padding:16px 16px 15px; box-shadow:var(--shadow);
display:flex; flex-direction:column; gap:9px; min-width:0;
transition:transform .16s ease, border-color .16s ease;
}
.card:hover{transform:translateY(-3px); border-color:var(--tc)}
.card-top{display:flex; align-items:baseline; gap:11px}
.rank{
font-family:var(--font-display); font-weight:650; font-size:1.15rem;
color:var(--tc); font-variant-numeric:tabular-nums; line-height:1;
}
.rank::before{content:"#"; opacity:.5; font-size:.85em}
.name{font-weight:650; font-size:1.04rem; letter-spacing:-.01em; line-height:1.2}
.badge{
margin-left:auto; align-self:center; font-family:var(--font-display);
font-size:.63rem; letter-spacing:.08em; text-transform:uppercase;
color:var(--tc); background:var(--tc-soft); border:1px solid var(--tc);
border-radius:999px; padding:3px 8px; white-space:nowrap;
}
.why{font-size:.88rem; color:var(--muted); margin:0; flex:1}
.tags{display:flex; flex-wrap:wrap; gap:6px}
.tag{
font-family:var(--font-display); font-size:.68rem; color:var(--faint);
background:var(--panel-2); border-radius:6px; padding:3px 7px; white-space:nowrap;
}
.repo-link{
font-family:var(--font-display); font-size:.74rem; letter-spacing:.03em;
color:var(--accent); text-decoration:none; margin-top:2px;
display:inline-flex; align-items:center; gap:5px; align-self:flex-start;
}
.repo-link:hover{text-decoration:underline; text-underline-offset:3px}
.repo-link::after{content:"↗"; font-size:.85em}
@media (prefers-reduced-motion:no-preference){
.card{animation:pop .28s ease both}
@keyframes pop{from{opacity:0; transform:translateY(6px)} to{opacity:1; transform:none}}
}
footer{margin-top:34px; padding-top:18px; border-top:1px solid var(--border);
font-size:.82rem; color:var(--faint)}
footer a{color:var(--accent)}
</style>
<div class="wrap">
<header>
<p class="eyebrow">codecrafters-io / build-your-own-x</p>
<h1>The Build-Your-Own-<span class="x">X</span> Difficulty Ladder</h1>
<p class="lede">All 30 project categories from the
<a href="https://github.com/codecrafters-io/build-your-own-x#table-of-contents" target="_blank" rel="noopener">build-your-own-x</a>
collection, ranked easiest&nbsp;→&nbsp;hardest and grouped into four tiers.
Sort or filter, then follow a card to its tutorials.</p>
<div class="axes" aria-label="How difficulty is scored">
<div class="axis"><b>Prerequisites</b><span>Domain knowledge a beginner must bring — crypto, linear algebra, OS internals.</span></div>
<div class="axis"><b>Scope</b><span>How much code a working toy version actually needs.</span></div>
<div class="axis"><b>Guidance</b><span>Whether a canonical step-by-step tutorial exists to lean on.</span></div>
</div>
</header>
<div class="layout">
<aside>
<div class="ctl-group">
<p class="ctl-label">Sort</p>
<select id="sort" aria-label="Sort order">
<option value="rank">Difficulty (easy → hard)</option>
<option value="rankdesc">Difficulty (hard → easy)</option>
<option value="az">Name (A → Z)</option>
</select>
</div>
<div class="ctl-group">
<p class="ctl-label">Tiers</p>
<div class="chips" id="chips"></div>
<button class="reset" id="reset" type="button">reset filters</button>
</div>
<p class="count" id="count"></p>
</aside>
<main>
<div class="grid" id="grid"></div>
</main>
</div>
<footer>
Difficulty is a judgment call, scored on prerequisites × scope × guidance for the
<em>typical</em> tutorial in each section — not the hardest possible reading. Data mirrors the
<a href="https://github.com/codecrafters-io/build-your-own-x" target="_blank" rel="noopener">build-your-own-x</a> README.
</footer>
</div>
<script>
// Tier metadata: order carries difficulty, color encodes it (green -> red).
const TIERS = {
beginner: {label:"Beginner", color:"var(--t1)", soft:"var(--t1-soft)"},
intermediate: {label:"Intermediate", color:"var(--t2)", soft:"var(--t2-soft)"},
advanced: {label:"Advanced", color:"var(--t3)", soft:"var(--t3-soft)"},
expert: {label:"Expert", color:"var(--t4)", soft:"var(--t4-soft)"},
};
// GitHub slug rule: lowercase, strip backticks/punctuation, spaces -> hyphens.
const anchor = s => "https://github.com/codecrafters-io/build-your-own-x#build-your-own-" + s;
const DATA = [
{r:1, name:"Command-Line Tool", tier:"beginner", slug:"command-line-tool",
why:"Parse arguments, do one job. The least scaffolding of anything here.", tags:["Any language","argv","stdin/stdout"]},
{r:2, name:"Bot", tier:"beginner", slug:"bot",
why:"Mostly glue: call an API, handle events, respond. Little novel machinery.", tags:["HTTP APIs","Webhooks","Event loop"]},
{r:3, name:"Template Engine", tier:"beginner", slug:"template-engine",
why:"Scan a string, substitute placeholders, handle loops. Small and self-contained.", tags:["String parsing","Tokenizing"]},
{r:4, name:"Text Editor", tier:"beginner", slug:"text-editor",
why:"Raw terminal mode and a screen buffer — the classic ~1k-line kilo build is heavily guided.", tags:["termios","Terminal I/O","C"]},
{r:5, name:"Git", tier:"beginner", slug:"git",
why:"A content-addressable object store: blobs, trees, commits, SHA-1. Simple model, big payoff.", tags:["Hashing","zlib","File formats"]},
{r:6, name:"Shell", tier:"intermediate", slug:"shell",
why:"fork/exec, pipes and redirection, basic job control — real OS-process work, but bounded.", tags:["fork/exec","Pipes","POSIX"]},
{r:7, name:"Web Server", tier:"intermediate", slug:"web-server",
why:"Open a socket, parse HTTP, route requests. Concurrency is where it gets spicy.", tags:["Sockets","HTTP","Concurrency"]},
{r:8, name:"Search Engine", tier:"intermediate", slug:"search-engine",
why:"Build an inverted index and rank with tf-idf. Data-structure heavy, math-light.", tags:["Inverted index","tf-idf","Tokenizing"]},
{r:9, name:"BitTorrent Client", tier:"intermediate", slug:"bittorrent-client",
why:"Bencode parsing plus the tracker and peer wire protocols — a real network protocol to implement.", tags:["Bencode","TCP","Protocol"]},
{r:10, name:"Front-end Framework / Library", tier:"intermediate", slug:"front-end-framework--library",
why:"A virtual DOM, diffing, and a reactivity system. Conceptually tidy, JS-detail heavy.", tags:["Virtual DOM","Diffing","Reactivity"]},
{r:11, name:"Regex Engine", tier:"intermediate", slug:"regex-engine",
why:"Compile patterns to an NFA/DFA (Thompson construction) and simulate it. Clean but algorithmic.", tags:["NFA/DFA","Automata","Parsing"]},
{r:12, name:"Game", tier:"intermediate", slug:"game",
why:"Guided clones (snake, tetris, a minecraft-lite) are approachable; scope swings wide by target.", tags:["Game loop","Rendering","Input"]},
{r:13, name:"Programming Language", tier:"advanced", slug:"programming-language",
why:"Lexer → parser → tree-walk interpreter. Big, but Crafting Interpreters holds your hand well.", tags:["Lexing","Parsing","Interpreter"]},
{r:14, name:"Emulator / Virtual Machine", tier:"advanced", slug:"emulator--virtual-machine",
why:"CHIP-8 is a weekend; an NES/GB emulator is months. Ranked at the typical middle.", tags:["Opcodes","CPU cycles","Bit-ops"]},
{r:15, name:"Neural Network", tier:"advanced", slug:"neural-network",
why:"Forward pass, backprop by hand, gradient descent on MNIST. The calculus is the wall.", tags:["Backprop","Linear algebra","Calculus"]},
{r:16, name:"Memory Allocator", tier:"advanced", slug:"memory-allocator",
why:"Implement malloc/free with free lists over sbrk/mmap. Small code, unforgiving pointer work.", tags:["Pointers","sbrk/mmap","C"]},
{r:17, name:"Blockchain / Cryptocurrency", tier:"advanced", slug:"blockchain--cryptocurrency",
why:"Hashing and signatures, a consensus rule, and P2P gossip all have to click together.", tags:["Crypto","Consensus","P2P"]},
{r:18, name:"Database", tier:"advanced", slug:"database",
why:"A storage engine (B-trees), a write-ahead log, and a query layer. Broad and deep.", tags:["B-trees","WAL","Query parsing"]},
{r:19, name:"3D Renderer", tier:"advanced", slug:"3d-renderer",
why:"Rasterization or ray tracing built on solid linear algebra and a graphics pipeline.", tags:["Linear algebra","Rasterizing","Shading"]},
{r:20, name:"Physics Engine", tier:"advanced", slug:"physics-engine",
why:"Collision detection plus stable numerical integration — subtle math bugs everywhere.", tags:["Collision","Integration","Vectors"]},
{r:21, name:"Docker", tier:"advanced", slug:"docker",
why:"Little code, deep Linux: namespaces, cgroups and an overlay filesystem to isolate a process.", tags:["Namespaces","cgroups","Linux"]},
{r:22, name:"Voxel Engine", tier:"advanced", slug:"voxel-engine",
why:"3D graphics plus chunk streaming and greedy meshing to keep a blocky world fast.", tags:["Meshing","Chunking","GPU"]},
{r:23, name:"Network Stack", tier:"expert", slug:"network-stack",
why:"A working TCP/IP: the full connection state machine, retransmission and congestion control.", tags:["TCP/IP","State machine","Kernel"]},
{r:24, name:"AI Model", tier:"expert", slug:"ai-model",
why:"Train a transformer/LLM from scratch. The architecture is only half — the training loop is the rest.", tags:["Transformers","Training","GPUs"]},
{r:25, name:"Visual Recognition System", tier:"expert", slug:"visual-recognition-system",
why:"A full computer-vision + ML pipeline from pixels to predictions.", tags:["CV","CNNs","Feature extraction"]},
{r:26, name:"Augmented Reality", tier:"expert", slug:"augmented-reality",
why:"Fuse real-time computer vision with 3D graphics — tracking, calibration, and rendering at once.", tags:["CV","3D math","Real-time"]},
{r:27, name:"Web Browser", tier:"expert", slug:"web-browser",
why:"Parse HTML/CSS, run layout, paint, and drive a JS engine. Enormous surface area.", tags:["Parsing","Layout","JS engine"]},
{r:28, name:"Processor", tier:"expert", slug:"processor",
why:"Design a CPU in an HDL from logic gates up — this one crosses into hardware.", tags:["Digital logic","HDL","Architecture"]},
{r:29, name:"Operating System", tier:"expert", slug:"operating-system",
why:"Bootloader, kernel, memory management, a scheduler. The canonical hardest build.", tags:["Kernel","Bootloader","Scheduling"]},
{r:30, name:"Distributed Systems", tier:"expert", slug:"distributed-systems",
why:"Consensus (Raft/Paxos), replication and fault tolerance — the hardest to reason about correctly.", tags:["Consensus","Replication","Fault tolerance"]},
];
const grid = document.getElementById("grid");
const chipsEl = document.getElementById("chips");
const countEl = document.getElementById("count");
const sortEl = document.getElementById("sort");
// active tiers as a Set; all on by default
const active = new Set(Object.keys(TIERS));
// Build filter chips with per-tier counts
Object.entries(TIERS).forEach(([key, t]) => {
const n = DATA.filter(d => d.tier === key).length;
const b = document.createElement("button");
b.type = "button"; b.className = "chip"; b.dataset.tier = key;
b.setAttribute("aria-pressed", "true");
b.style.setProperty("--tc", t.color);
b.innerHTML = `<span class="dot"></span>${t.label}<span class="n">${n}</span>`;
b.addEventListener("click", () => {
active.has(key) ? active.delete(key) : active.add(key);
b.setAttribute("aria-pressed", active.has(key));
render();
});
chipsEl.appendChild(b);
});
document.getElementById("reset").addEventListener("click", () => {
active.clear(); Object.keys(TIERS).forEach(k => active.add(k));
chipsEl.querySelectorAll(".chip").forEach(c => c.setAttribute("aria-pressed","true"));
sortEl.value = "rank"; render();
});
sortEl.addEventListener("change", render);
function render(){
const mode = sortEl.value;
let rows = DATA.filter(d => active.has(d.tier));
if (mode === "az") rows = rows.slice().sort((a,b)=> a.name.localeCompare(b.name));
else if (mode === "rankdesc") rows = rows.slice().sort((a,b)=> b.r - a.r);
else rows = rows.slice().sort((a,b)=> a.r - b.r);
grid.innerHTML = "";
rows.forEach(d => {
const t = TIERS[d.tier];
const el = document.createElement("article");
el.className = "card";
el.style.setProperty("--tc", t.color);
el.style.setProperty("--tc-soft", t.soft);
el.innerHTML = `
<div class="card-top">
<span class="rank">${d.r}</span>
<span class="name">${d.name}</span>
<span class="badge">${t.label}</span>
</div>
<p class="why">${d.why}</p>
<div class="tags">${d.tags.map(x=>`<span class="tag">${x}</span>`).join("")}</div>
<a class="repo-link" href="${anchor(d.slug)}" target="_blank" rel="noopener">tutorials</a>`;
grid.appendChild(el);
});
countEl.innerHTML = `Showing <b>${rows.length}</b> of ${DATA.length} categories`;
}
render();
</script>
</html>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,197 @@
#!/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())

View File

@ -0,0 +1,356 @@
<title>Build Your Own X — Difficulty Ladder</title>
<style>
/* ---- Tokens: light is the base, dark + explicit theme override below ---- */
:root{
--bg:#f4f6f9; --panel:#ffffff; --panel-2:#eef1f6; --border:#dde3ec;
--text:#1a2230; --muted:#5b6676; --faint:#8a94a3;
--accent:#0e7490; --accent-ink:#ffffff; --accent-soft:#0e749018;
--t1:#1a7f37; --t2:#1f6feb; --t3:#9a6700; --t4:#cf222e;
--t1-soft:#1a7f3718; --t2-soft:#1f6feb18; --t3-soft:#9a670018; --t4-soft:#cf222e18;
--shadow:0 1px 2px rgba(16,23,38,.06), 0 8px 24px rgba(16,23,38,.06);
--font-display:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
--font-body:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
@media (prefers-color-scheme:dark){
:root{
--bg:#0d1117; --panel:#161b22; --panel-2:#1c232d; --border:#2b333d;
--text:#c9d4e0; --muted:#8b96a5; --faint:#6b7684;
--accent:#38bdf8; --accent-ink:#05202b; --accent-soft:#38bdf815;
--t1:#3fb950; --t2:#58a6ff; --t3:#d29922; --t4:#f85149;
--t1-soft:#3fb95018; --t2-soft:#58a6ff18; --t3-soft:#d2992218; --t4-soft:#f8514918;
--shadow:0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.35);
}
}
/* Viewer toggle wins over the media query, in both directions */
:root[data-theme="light"]{
--bg:#f4f6f9; --panel:#ffffff; --panel-2:#eef1f6; --border:#dde3ec;
--text:#1a2230; --muted:#5b6676; --faint:#8a94a3;
--accent:#0e7490; --accent-ink:#ffffff; --accent-soft:#0e749018;
--t1:#1a7f37; --t2:#1f6feb; --t3:#9a6700; --t4:#cf222e;
--t1-soft:#1a7f3718; --t2-soft:#1f6feb18; --t3-soft:#9a670018; --t4-soft:#cf222e18;
--shadow:0 1px 2px rgba(16,23,38,.06), 0 8px 24px rgba(16,23,38,.06);
}
:root[data-theme="dark"]{
--bg:#0d1117; --panel:#161b22; --panel-2:#1c232d; --border:#2b333d;
--text:#c9d4e0; --muted:#8b96a5; --faint:#6b7684;
--accent:#38bdf8; --accent-ink:#05202b; --accent-soft:#38bdf815;
--t1:#3fb950; --t2:#58a6ff; --t3:#d29922; --t4:#f85149;
--t1-soft:#3fb95018; --t2-soft:#58a6ff18; --t3-soft:#d2992218; --t4-soft:#f8514918;
--shadow:0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.35);
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{
margin:0; background:var(--bg); color:var(--text);
font-family:var(--font-body); line-height:1.55;
font-size:16px; letter-spacing:.01em;
}
a{color:inherit}
.wrap{max-width:1200px; margin:0 auto; padding:clamp(20px,4vw,44px) clamp(16px,4vw,40px) 64px}
/* ---- Header ---- */
header{margin-bottom:28px}
.eyebrow{
font-family:var(--font-display); font-size:.72rem; letter-spacing:.22em;
text-transform:uppercase; color:var(--accent); margin:0 0 10px;
display:flex; align-items:center; gap:10px;
}
.eyebrow::before{content:"//"; opacity:.55}
h1{
font-family:var(--font-display); font-weight:650;
font-size:clamp(1.9rem,5.4vw,3.1rem); line-height:1.02;
letter-spacing:-.01em; margin:0 0 14px; text-wrap:balance;
}
h1 .x{color:var(--accent)}
.lede{
max-width:60ch; color:var(--muted); font-size:1.02rem; margin:0 0 4px;
}
.lede a{color:var(--accent); text-underline-offset:3px}
/* ---- Scoring axes note ---- */
.axes{
display:flex; flex-wrap:wrap; gap:10px; margin-top:20px;
}
.axis{
background:var(--panel); border:1px solid var(--border); border-radius:10px;
padding:10px 13px; flex:1 1 200px; box-shadow:var(--shadow);
}
.axis b{display:block; font-size:.82rem; font-family:var(--font-display); letter-spacing:.02em}
.axis span{font-size:.8rem; color:var(--muted)}
/* ---- Layout ---- */
.layout{display:grid; grid-template-columns:1fr; gap:26px; margin-top:30px}
@media (min-width:900px){ .layout{grid-template-columns:230px 1fr; gap:34px} }
aside{align-self:start}
@media (min-width:900px){ aside{position:sticky; top:22px} }
.ctl-group{margin-bottom:22px}
.ctl-label{
font-family:var(--font-display); font-size:.7rem; letter-spacing:.16em;
text-transform:uppercase; color:var(--faint); margin:0 0 10px;
}
select{
width:100%; font-family:var(--font-body); font-size:.9rem; color:var(--text);
background:var(--panel); border:1px solid var(--border); border-radius:9px;
padding:9px 11px; cursor:pointer;
}
.chips{display:flex; flex-direction:column; gap:8px}
.chip{
display:flex; align-items:center; gap:9px; width:100%; cursor:pointer;
font-family:var(--font-body); font-size:.88rem; text-align:left;
background:var(--panel); color:var(--text);
border:1px solid var(--border); border-radius:9px; padding:8px 11px;
transition:background .15s, border-color .15s, opacity .15s;
}
.chip .dot{width:10px; height:10px; border-radius:3px; flex:none; background:var(--tc)}
.chip .n{margin-left:auto; font-family:var(--font-display); font-size:.78rem; color:var(--muted); font-variant-numeric:tabular-nums}
.chip[aria-pressed="false"]{opacity:.4; background:transparent}
.chip:hover{border-color:var(--tc)}
.chip:focus-visible, select:focus-visible, .reset:focus-visible{outline:2px solid var(--accent); outline-offset:2px}
.reset{
background:none; border:none; color:var(--accent); cursor:pointer;
font-family:var(--font-display); font-size:.76rem; letter-spacing:.04em;
padding:6px 0; margin-top:4px;
}
.count{
font-family:var(--font-display); font-size:.76rem; color:var(--faint);
letter-spacing:.04em; margin-top:16px;
}
.count b{color:var(--text)}
/* ---- Cards ---- */
.grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(255px,1fr)); gap:16px}
.card{
position:relative; background:var(--panel); border:1px solid var(--border);
border-left:4px solid var(--tc); border-radius:12px;
padding:16px 16px 15px; box-shadow:var(--shadow);
display:flex; flex-direction:column; gap:9px; min-width:0;
transition:transform .16s ease, border-color .16s ease;
}
.card:hover{transform:translateY(-3px); border-color:var(--tc)}
.card-top{display:flex; align-items:baseline; gap:11px}
.rank{
font-family:var(--font-display); font-weight:650; font-size:1.15rem;
color:var(--tc); font-variant-numeric:tabular-nums; line-height:1;
}
.rank::before{content:"#"; opacity:.5; font-size:.85em}
.name{font-weight:650; font-size:1.04rem; letter-spacing:-.01em; line-height:1.2}
.badge{
margin-left:auto; align-self:center; font-family:var(--font-display);
font-size:.63rem; letter-spacing:.08em; text-transform:uppercase;
color:var(--tc); background:var(--tc-soft); border:1px solid var(--tc);
border-radius:999px; padding:3px 8px; white-space:nowrap;
}
.why{font-size:.88rem; color:var(--muted); margin:0; flex:1}
.tags{display:flex; flex-wrap:wrap; gap:6px}
.tag{
font-family:var(--font-display); font-size:.68rem; color:var(--faint);
background:var(--panel-2); border-radius:6px; padding:3px 7px; white-space:nowrap;
}
.repo-link{
font-family:var(--font-display); font-size:.74rem; letter-spacing:.03em;
color:var(--accent); text-decoration:none; margin-top:2px;
display:inline-flex; align-items:center; gap:5px; align-self:flex-start;
}
.repo-link:hover{text-decoration:underline; text-underline-offset:3px}
.repo-link::after{content:"↗"; font-size:.85em}
@media (prefers-reduced-motion:no-preference){
.card{animation:pop .28s ease both}
@keyframes pop{from{opacity:0; transform:translateY(6px)} to{opacity:1; transform:none}}
}
footer{margin-top:34px; padding-top:18px; border-top:1px solid var(--border);
font-size:.82rem; color:var(--faint)}
footer a{color:var(--accent)}
</style>
<div class="wrap">
<header>
<p class="eyebrow">codecrafters-io / build-your-own-x</p>
<h1>The Build-Your-Own-<span class="x">X</span> Difficulty Ladder</h1>
<p class="lede">All 30 project categories from the
<a href="https://github.com/codecrafters-io/build-your-own-x#table-of-contents" target="_blank" rel="noopener">build-your-own-x</a>
collection, ranked easiest&nbsp;→&nbsp;hardest and grouped into four tiers.
Sort or filter, then follow a card to its tutorials.</p>
<div class="axes" aria-label="How difficulty is scored">
<div class="axis"><b>Prerequisites</b><span>Domain knowledge a beginner must bring — crypto, linear algebra, OS internals.</span></div>
<div class="axis"><b>Scope</b><span>How much code a working toy version actually needs.</span></div>
<div class="axis"><b>Guidance</b><span>Whether a canonical step-by-step tutorial exists to lean on.</span></div>
</div>
</header>
<div class="layout">
<aside>
<div class="ctl-group">
<p class="ctl-label">Sort</p>
<select id="sort" aria-label="Sort order">
<option value="rank">Difficulty (easy → hard)</option>
<option value="rankdesc">Difficulty (hard → easy)</option>
<option value="az">Name (A → Z)</option>
</select>
</div>
<div class="ctl-group">
<p class="ctl-label">Tiers</p>
<div class="chips" id="chips"></div>
<button class="reset" id="reset" type="button">reset filters</button>
</div>
<p class="count" id="count"></p>
</aside>
<main>
<div class="grid" id="grid"></div>
</main>
</div>
<footer>
Difficulty is a judgment call, scored on prerequisites × scope × guidance for the
<em>typical</em> tutorial in each section — not the hardest possible reading. Data mirrors the
<a href="https://github.com/codecrafters-io/build-your-own-x" target="_blank" rel="noopener">build-your-own-x</a> README.
</footer>
</div>
<script>
// Tier metadata: order carries difficulty, color encodes it (green -> red).
const TIERS = {
beginner: {label:"Beginner", color:"var(--t1)", soft:"var(--t1-soft)"},
intermediate: {label:"Intermediate", color:"var(--t2)", soft:"var(--t2-soft)"},
advanced: {label:"Advanced", color:"var(--t3)", soft:"var(--t3-soft)"},
expert: {label:"Expert", color:"var(--t4)", soft:"var(--t4-soft)"},
};
// GitHub slug rule: lowercase, strip backticks/punctuation, spaces -> hyphens.
const anchor = s => "https://github.com/codecrafters-io/build-your-own-x#build-your-own-" + s;
const DATA = [
{r:1, name:"Command-Line Tool", tier:"beginner", slug:"command-line-tool",
why:"Parse arguments, do one job. The least scaffolding of anything here.", tags:["Any language","argv","stdin/stdout"]},
{r:2, name:"Bot", tier:"beginner", slug:"bot",
why:"Mostly glue: call an API, handle events, respond. Little novel machinery.", tags:["HTTP APIs","Webhooks","Event loop"]},
{r:3, name:"Template Engine", tier:"beginner", slug:"template-engine",
why:"Scan a string, substitute placeholders, handle loops. Small and self-contained.", tags:["String parsing","Tokenizing"]},
{r:4, name:"Text Editor", tier:"beginner", slug:"text-editor",
why:"Raw terminal mode and a screen buffer — the classic ~1k-line kilo build is heavily guided.", tags:["termios","Terminal I/O","C"]},
{r:5, name:"Git", tier:"beginner", slug:"git",
why:"A content-addressable object store: blobs, trees, commits, SHA-1. Simple model, big payoff.", tags:["Hashing","zlib","File formats"]},
{r:6, name:"Shell", tier:"intermediate", slug:"shell",
why:"fork/exec, pipes and redirection, basic job control — real OS-process work, but bounded.", tags:["fork/exec","Pipes","POSIX"]},
{r:7, name:"Web Server", tier:"intermediate", slug:"web-server",
why:"Open a socket, parse HTTP, route requests. Concurrency is where it gets spicy.", tags:["Sockets","HTTP","Concurrency"]},
{r:8, name:"Search Engine", tier:"intermediate", slug:"search-engine",
why:"Build an inverted index and rank with tf-idf. Data-structure heavy, math-light.", tags:["Inverted index","tf-idf","Tokenizing"]},
{r:9, name:"BitTorrent Client", tier:"intermediate", slug:"bittorrent-client",
why:"Bencode parsing plus the tracker and peer wire protocols — a real network protocol to implement.", tags:["Bencode","TCP","Protocol"]},
{r:10, name:"Front-end Framework / Library", tier:"intermediate", slug:"front-end-framework--library",
why:"A virtual DOM, diffing, and a reactivity system. Conceptually tidy, JS-detail heavy.", tags:["Virtual DOM","Diffing","Reactivity"]},
{r:11, name:"Regex Engine", tier:"intermediate", slug:"regex-engine",
why:"Compile patterns to an NFA/DFA (Thompson construction) and simulate it. Clean but algorithmic.", tags:["NFA/DFA","Automata","Parsing"]},
{r:12, name:"Game", tier:"intermediate", slug:"game",
why:"Guided clones (snake, tetris, a minecraft-lite) are approachable; scope swings wide by target.", tags:["Game loop","Rendering","Input"]},
{r:13, name:"Programming Language", tier:"advanced", slug:"programming-language",
why:"Lexer → parser → tree-walk interpreter. Big, but Crafting Interpreters holds your hand well.", tags:["Lexing","Parsing","Interpreter"]},
{r:14, name:"Emulator / Virtual Machine", tier:"advanced", slug:"emulator--virtual-machine",
why:"CHIP-8 is a weekend; an NES/GB emulator is months. Ranked at the typical middle.", tags:["Opcodes","CPU cycles","Bit-ops"]},
{r:15, name:"Neural Network", tier:"advanced", slug:"neural-network",
why:"Forward pass, backprop by hand, gradient descent on MNIST. The calculus is the wall.", tags:["Backprop","Linear algebra","Calculus"]},
{r:16, name:"Memory Allocator", tier:"advanced", slug:"memory-allocator",
why:"Implement malloc/free with free lists over sbrk/mmap. Small code, unforgiving pointer work.", tags:["Pointers","sbrk/mmap","C"]},
{r:17, name:"Blockchain / Cryptocurrency", tier:"advanced", slug:"blockchain--cryptocurrency",
why:"Hashing and signatures, a consensus rule, and P2P gossip all have to click together.", tags:["Crypto","Consensus","P2P"]},
{r:18, name:"Database", tier:"advanced", slug:"database",
why:"A storage engine (B-trees), a write-ahead log, and a query layer. Broad and deep.", tags:["B-trees","WAL","Query parsing"]},
{r:19, name:"3D Renderer", tier:"advanced", slug:"3d-renderer",
why:"Rasterization or ray tracing built on solid linear algebra and a graphics pipeline.", tags:["Linear algebra","Rasterizing","Shading"]},
{r:20, name:"Physics Engine", tier:"advanced", slug:"physics-engine",
why:"Collision detection plus stable numerical integration — subtle math bugs everywhere.", tags:["Collision","Integration","Vectors"]},
{r:21, name:"Docker", tier:"advanced", slug:"docker",
why:"Little code, deep Linux: namespaces, cgroups and an overlay filesystem to isolate a process.", tags:["Namespaces","cgroups","Linux"]},
{r:22, name:"Voxel Engine", tier:"advanced", slug:"voxel-engine",
why:"3D graphics plus chunk streaming and greedy meshing to keep a blocky world fast.", tags:["Meshing","Chunking","GPU"]},
{r:23, name:"Network Stack", tier:"expert", slug:"network-stack",
why:"A working TCP/IP: the full connection state machine, retransmission and congestion control.", tags:["TCP/IP","State machine","Kernel"]},
{r:24, name:"AI Model", tier:"expert", slug:"ai-model",
why:"Train a transformer/LLM from scratch. The architecture is only half — the training loop is the rest.", tags:["Transformers","Training","GPUs"]},
{r:25, name:"Visual Recognition System", tier:"expert", slug:"visual-recognition-system",
why:"A full computer-vision + ML pipeline from pixels to predictions.", tags:["CV","CNNs","Feature extraction"]},
{r:26, name:"Augmented Reality", tier:"expert", slug:"augmented-reality",
why:"Fuse real-time computer vision with 3D graphics — tracking, calibration, and rendering at once.", tags:["CV","3D math","Real-time"]},
{r:27, name:"Web Browser", tier:"expert", slug:"web-browser",
why:"Parse HTML/CSS, run layout, paint, and drive a JS engine. Enormous surface area.", tags:["Parsing","Layout","JS engine"]},
{r:28, name:"Processor", tier:"expert", slug:"processor",
why:"Design a CPU in an HDL from logic gates up — this one crosses into hardware.", tags:["Digital logic","HDL","Architecture"]},
{r:29, name:"Operating System", tier:"expert", slug:"operating-system",
why:"Bootloader, kernel, memory management, a scheduler. The canonical hardest build.", tags:["Kernel","Bootloader","Scheduling"]},
{r:30, name:"Distributed Systems", tier:"expert", slug:"distributed-systems",
why:"Consensus (Raft/Paxos), replication and fault tolerance — the hardest to reason about correctly.", tags:["Consensus","Replication","Fault tolerance"]},
];
const grid = document.getElementById("grid");
const chipsEl = document.getElementById("chips");
const countEl = document.getElementById("count");
const sortEl = document.getElementById("sort");
// active tiers as a Set; all on by default
const active = new Set(Object.keys(TIERS));
// Build filter chips with per-tier counts
Object.entries(TIERS).forEach(([key, t]) => {
const n = DATA.filter(d => d.tier === key).length;
const b = document.createElement("button");
b.type = "button"; b.className = "chip"; b.dataset.tier = key;
b.setAttribute("aria-pressed", "true");
b.style.setProperty("--tc", t.color);
b.innerHTML = `<span class="dot"></span>${t.label}<span class="n">${n}</span>`;
b.addEventListener("click", () => {
active.has(key) ? active.delete(key) : active.add(key);
b.setAttribute("aria-pressed", active.has(key));
render();
});
chipsEl.appendChild(b);
});
document.getElementById("reset").addEventListener("click", () => {
active.clear(); Object.keys(TIERS).forEach(k => active.add(k));
chipsEl.querySelectorAll(".chip").forEach(c => c.setAttribute("aria-pressed","true"));
sortEl.value = "rank"; render();
});
sortEl.addEventListener("change", render);
function render(){
const mode = sortEl.value;
let rows = DATA.filter(d => active.has(d.tier));
if (mode === "az") rows = rows.slice().sort((a,b)=> a.name.localeCompare(b.name));
else if (mode === "rankdesc") rows = rows.slice().sort((a,b)=> b.r - a.r);
else rows = rows.slice().sort((a,b)=> a.r - b.r);
grid.innerHTML = "";
rows.forEach(d => {
const t = TIERS[d.tier];
const el = document.createElement("article");
el.className = "card";
el.style.setProperty("--tc", t.color);
el.style.setProperty("--tc-soft", t.soft);
el.innerHTML = `
<div class="card-top">
<span class="rank">${d.r}</span>
<span class="name">${d.name}</span>
<span class="badge">${t.label}</span>
</div>
<p class="why">${d.why}</p>
<div class="tags">${d.tags.map(x=>`<span class="tag">${x}</span>`).join("")}</div>
<a class="repo-link" href="${anchor(d.slug)}" target="_blank" rel="noopener">tutorials</a>`;
grid.appendChild(el);
});
countEl.innerHTML = `Showing <b>${rows.length}</b> of ${DATA.length} categories`;
}
render();
</script>

View File

@ -0,0 +1,313 @@
<title>Build Your Own X — Guide Ladder</title>
<style>
:root{
--bg:#f4f6f9; --panel:#ffffff; --panel-2:#eef1f6; --border:#dde3ec;
--text:#1a2230; --muted:#5b6676; --faint:#8a94a3;
--accent:#0e7490; --accent-ink:#ffffff; --accent-soft:#0e749014;
--t1:#1a7f37; --t2:#1f6feb; --t3:#9a6700; --t4:#cf222e; --t5:#7a8698;
--t1-soft:#1a7f3714; --t2-soft:#1f6feb14; --t3-soft:#9a670014; --t4-soft:#cf222e14; --t5-soft:#7a869814;
--shadow:0 1px 2px rgba(16,23,38,.05), 0 8px 24px rgba(16,23,38,.05);
--font-display:ui-monospace,"SF Mono","JetBrains Mono",Menlo,Consolas,monospace;
--font-body:system-ui,-apple-system,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
}
@media (prefers-color-scheme:dark){
:root{
--bg:#0d1117; --panel:#161b22; --panel-2:#1c232d; --border:#2b333d;
--text:#c9d4e0; --muted:#8b96a5; --faint:#6b7684;
--accent:#38bdf8; --accent-ink:#05202b; --accent-soft:#38bdf814;
--t1:#3fb950; --t2:#58a6ff; --t3:#d29922; --t4:#f85149; --t5:#8b96a5;
--t1-soft:#3fb95016; --t2-soft:#58a6ff16; --t3-soft:#d2992216; --t4-soft:#f8514916; --t5-soft:#8b96a516;
--shadow:0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.3);
}
}
:root[data-theme="light"]{
--bg:#f4f6f9; --panel:#ffffff; --panel-2:#eef1f6; --border:#dde3ec;
--text:#1a2230; --muted:#5b6676; --faint:#8a94a3;
--accent:#0e7490; --accent-ink:#ffffff; --accent-soft:#0e749014;
--t1:#1a7f37; --t2:#1f6feb; --t3:#9a6700; --t4:#cf222e; --t5:#7a8698;
--t1-soft:#1a7f3714; --t2-soft:#1f6feb14; --t3-soft:#9a670014; --t4-soft:#cf222e14; --t5-soft:#7a869814;
--shadow:0 1px 2px rgba(16,23,38,.05), 0 8px 24px rgba(16,23,38,.05);
}
:root[data-theme="dark"]{
--bg:#0d1117; --panel:#161b22; --panel-2:#1c232d; --border:#2b333d;
--text:#c9d4e0; --muted:#8b96a5; --faint:#6b7684;
--accent:#38bdf8; --accent-ink:#05202b; --accent-soft:#38bdf814;
--t1:#3fb950; --t2:#58a6ff; --t3:#d29922; --t4:#f85149; --t5:#8b96a5;
--t1-soft:#3fb95016; --t2-soft:#58a6ff16; --t3-soft:#d2992216; --t4-soft:#f8514916; --t5-soft:#8b96a516;
--shadow:0 1px 2px rgba(0,0,0,.4), 0 10px 30px rgba(0,0,0,.3);
}
*{box-sizing:border-box}
html{-webkit-text-size-adjust:100%}
body{margin:0; background:var(--bg); color:var(--text); font-family:var(--font-body);
line-height:1.5; font-size:15.5px; letter-spacing:.01em}
a{color:inherit; text-decoration:none}
.wrap{max-width:1180px; margin:0 auto; padding:clamp(20px,4vw,40px) clamp(14px,4vw,36px) 60px}
/* header */
.eyebrow{font-family:var(--font-display); font-size:.7rem; letter-spacing:.2em;
text-transform:uppercase; color:var(--accent); margin:0 0 9px; display:flex; align-items:center; gap:9px}
.eyebrow::before{content:"//"; opacity:.55}
h1{font-family:var(--font-display); font-weight:650; font-size:clamp(1.7rem,4.6vw,2.7rem);
line-height:1.04; letter-spacing:-.01em; margin:0 0 12px; text-wrap:balance}
h1 .x{color:var(--accent)}
.lede{max-width:64ch; color:var(--muted); font-size:1rem; margin:0}
.lede a{color:var(--accent); text-underline-offset:3px; text-decoration:underline}
.note{margin-top:14px; font-size:.86rem; color:var(--faint); max-width:70ch}
.note b{color:var(--muted); font-weight:600}
/* layout */
.layout{display:grid; grid-template-columns:1fr; gap:22px; margin-top:26px}
@media (min-width:860px){ .layout{grid-template-columns:250px 1fr; gap:30px} }
aside{align-self:start}
@media (min-width:860px){ aside{position:sticky; top:18px; max-height:calc(100vh - 34px); overflow-y:auto} }
.ctl{margin-bottom:18px}
.ctl-label{font-family:var(--font-display); font-size:.68rem; letter-spacing:.15em;
text-transform:uppercase; color:var(--faint); margin:0 0 8px}
.field{width:100%; font-family:var(--font-body); font-size:.88rem; color:var(--text);
background:var(--panel); border:1px solid var(--border); border-radius:9px; padding:9px 11px}
select.field{cursor:pointer}
input.field::placeholder{color:var(--faint)}
.chips{display:flex; flex-direction:column; gap:7px}
.chip{display:flex; align-items:center; gap:9px; width:100%; cursor:pointer;
font-family:var(--font-body); font-size:.85rem; text-align:left; color:var(--text);
background:var(--panel); border:1px solid var(--border); border-radius:9px; padding:7px 10px;
transition:background .15s, border-color .15s, opacity .15s}
.chip .dot{width:9px; height:9px; border-radius:3px; flex:none; background:var(--tc)}
.chip .n{margin-left:auto; font-family:var(--font-display); font-size:.74rem; color:var(--muted);
font-variant-numeric:tabular-nums}
.chip[aria-pressed="false"]{opacity:.4; background:transparent}
.chip:hover{border-color:var(--tc)}
.chip:focus-visible,.field:focus-visible,.reset:focus-visible,.row:focus-visible{
outline:2px solid var(--accent); outline-offset:2px}
.reset{background:none; border:none; color:var(--accent); cursor:pointer;
font-family:var(--font-display); font-size:.74rem; letter-spacing:.04em; padding:5px 0}
.count{font-family:var(--font-display); font-size:.75rem; color:var(--faint);
letter-spacing:.03em; margin-top:6px}
.count b{color:var(--text)}
/* list */
main{min-width:0}
.tierhead{display:flex; align-items:center; gap:11px; margin:22px 0 10px; padding-top:4px}
.tierhead:first-child{margin-top:0}
.tierhead .bar{width:26px; height:4px; border-radius:2px; background:var(--tc)}
.tierhead h2{font-family:var(--font-display); font-size:.9rem; letter-spacing:.06em;
text-transform:uppercase; margin:0; color:var(--tc)}
.tierhead .sub{font-size:.78rem; color:var(--faint); margin-left:auto; font-family:var(--font-display)}
.list{display:flex; flex-direction:column; gap:7px}
.row{display:grid; grid-template-columns:1fr auto; align-items:center; gap:10px 14px;
background:var(--panel); border:1px solid var(--border); border-left:4px solid var(--tc);
border-radius:10px; padding:11px 14px; box-shadow:var(--shadow);
transition:transform .12s ease, border-color .12s ease}
.row:hover{transform:translateX(2px); border-color:var(--tc)}
.row .title{font-weight:600; font-size:.96rem; line-height:1.3}
.row:hover .title{color:var(--accent)}
.rowmeta{display:flex; flex-wrap:wrap; align-items:center; gap:6px; margin-top:5px}
.cat{font-family:var(--font-display); font-size:.7rem; color:var(--muted)}
.cat::after{content:"·"; margin-left:6px; color:var(--faint)}
.lang{font-family:var(--font-display); font-size:.68rem; color:var(--faint);
background:var(--panel-2); border-radius:5px; padding:2px 6px; white-space:nowrap}
.vid{font-family:var(--font-display); font-size:.64rem; letter-spacing:.05em; text-transform:uppercase;
color:var(--accent); border:1px solid var(--accent); border-radius:5px; padding:1px 5px}
.badge{font-family:var(--font-display); font-size:.62rem; letter-spacing:.07em; text-transform:uppercase;
color:var(--tc); background:var(--tc-soft); border:1px solid var(--tc); border-radius:999px;
padding:3px 8px; white-space:nowrap; align-self:center}
.empty{color:var(--muted); font-size:.92rem; padding:26px 4px}
footer{margin-top:34px; padding-top:16px; border-top:1px solid var(--border);
font-size:.8rem; color:var(--faint); max-width:74ch}
footer a{color:var(--accent)}
@media (prefers-reduced-motion:reduce){ .row{transition:none} }
</style>
<div class="wrap">
<header>
<p class="eyebrow">codecrafters-io / build-your-own-x</p>
<h1>The Build-Your-Own-<span class="x">X</span> Guide Ladder</h1>
<p class="lede">Every individual tutorial from the
<a href="https://github.com/codecrafters-io/build-your-own-x" target="_blank" rel="noopener">build-your-own-x</a>
collection — <span id="total">0</span> guides across 30 categories — placed on a difficulty
ladder. Search, filter by tier / language / category, and click any guide to open it.</p>
<p class="note"><b>How difficulty is assigned:</b> each guide inherits its category's tier
(a Regex-Engine guide is easier than an OS guide, whatever the language). Language only nudges
the order <i>within</i> a tier — lower-level languages (C, Rust, Asm) sit a little higher than
Python or JS. The <b>Unsorted</b> group is the repo's own uncategorized bucket; its guides span
all levels, so they aren't ranked.</p>
</header>
<div class="layout">
<aside>
<div class="ctl">
<p class="ctl-label">Search</p>
<input id="q" class="field" type="search" placeholder="title, category, language…" autocomplete="off">
</div>
<div class="ctl">
<p class="ctl-label">Sort</p>
<select id="sort" class="field" aria-label="Sort order">
<option value="easy">Difficulty (easy → hard)</option>
<option value="hard">Difficulty (hard → easy)</option>
<option value="az">Title (A → Z)</option>
</select>
</div>
<div class="ctl">
<p class="ctl-label">Tiers</p>
<div class="chips" id="chips"></div>
</div>
<div class="ctl">
<p class="ctl-label">Language</p>
<select id="lang" class="field" aria-label="Filter by language"></select>
</div>
<div class="ctl">
<p class="ctl-label">Category</p>
<select id="cat" class="field" aria-label="Filter by category"></select>
</div>
<button class="reset" id="reset" type="button">reset all filters</button>
<p class="count" id="count"></p>
</aside>
<main id="main"></main>
</div>
<footer>
A per-guide expansion of the category difficulty ladder. Difficulty is a judgment call scored on
category × language; the exact head-to-head order within a tier is approximate by design. Data
mirrors the <a href="https://github.com/codecrafters-io/build-your-own-x" target="_blank" rel="noopener">build-your-own-x</a> README.
</footer>
</div>
<script>
/*__GUIDES__*/
const TIERS = {
beginner: {label:"Beginner", color:"var(--t1)", soft:"var(--t1-soft)", blurb:"app-level logic, few prerequisites"},
intermediate: {label:"Intermediate", color:"var(--t2)", soft:"var(--t2-soft)", blurb:"data structures, networking, a real protocol"},
advanced: {label:"Advanced", color:"var(--t3)", soft:"var(--t3-soft)", blurb:"deep domain knowledge or large scope"},
expert: {label:"Expert", color:"var(--t4)", soft:"var(--t4-soft)", blurb:"broad + deep, little hand-holding"},
unsorted: {label:"Unsorted", color:"var(--t5)", soft:"var(--t5-soft)", blurb:"repo's uncategorized bucket — spans all levels"},
};
const TIER_ORDER = ["beginner","intermediate","advanced","expert","unsorted"];
const $ = id => document.getElementById(id);
const esc = s => s.replace(/[&<>"]/g, c => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;"}[c]));
document.getElementById("total").textContent = GUIDES.length;
const state = {
q:"", sort:"easy", lang:"all", cat:"all",
tiers:new Set(TIER_ORDER),
};
// ---- build tier chips with counts ----
const chipsEl = $("chips");
TIER_ORDER.forEach(key=>{
const t = TIERS[key];
const n = GUIDES.filter(g=>g.tier===key).length;
const b = document.createElement("button");
b.type="button"; b.className="chip"; b.dataset.tier=key;
b.setAttribute("aria-pressed","true");
b.style.setProperty("--tc", t.color);
b.innerHTML = `<span class="dot"></span>${t.label}<span class="n">${n}</span>`;
b.addEventListener("click", ()=>{
state.tiers.has(key) ? state.tiers.delete(key) : state.tiers.add(key);
b.setAttribute("aria-pressed", state.tiers.has(key));
render();
});
chipsEl.appendChild(b);
});
// ---- language select (alpha, with counts) ----
const langCount = {};
GUIDES.forEach(g=>g.langs.forEach(l=>langCount[l]=(langCount[l]||0)+1));
const langEl = $("lang");
langEl.appendChild(new Option(`All languages (${GUIDES.length})`, "all"));
Object.keys(langCount).sort((a,b)=>a.localeCompare(b)).forEach(l=>{
langEl.appendChild(new Option(`${l} (${langCount[l]})`, l));
});
// ---- category select (in difficulty order) ----
const catMeta = {};
GUIDES.forEach(g=>{ if(!catMeta[g.category]) catMeta[g.category]={rank:g.cat_rank,n:0}; catMeta[g.category].n++; });
const catEl = $("cat");
catEl.appendChild(new Option(`All categories (30)`, "all"));
Object.keys(catMeta).sort((a,b)=>catMeta[a].rank-catMeta[b].rank).forEach(c=>{
catEl.appendChild(new Option(`${c} (${catMeta[c].n})`, c));
});
// ---- wiring ----
let t=null;
$("q").addEventListener("input", e=>{ state.q=e.target.value.toLowerCase().trim();
clearTimeout(t); t=setTimeout(render,110); });
$("sort").addEventListener("change", e=>{ state.sort=e.target.value; render(); });
$("lang").addEventListener("change", e=>{ state.lang=e.target.value; render(); });
$("cat").addEventListener("change", e=>{ state.cat=e.target.value; render(); });
$("reset").addEventListener("click", ()=>{
state.q=""; state.sort="easy"; state.lang="all"; state.cat="all";
state.tiers=new Set(TIER_ORDER);
$("q").value=""; $("sort").value="easy"; $("lang").value="all"; $("cat").value="all";
chipsEl.querySelectorAll(".chip").forEach(c=>c.setAttribute("aria-pressed","true"));
render();
});
function matches(g){
if(!state.tiers.has(g.tier)) return false;
if(state.lang!=="all" && !g.langs.includes(state.lang)) return false;
if(state.cat!=="all" && g.category!==state.cat) return false;
if(state.q){
const hay = (g.title+" "+g.category+" "+g.langs.join(" ")).toLowerCase();
if(!hay.includes(state.q)) return false;
}
return true;
}
function rowHtml(g){
const t = TIERS[g.tier];
const langs = g.langs.map(l=>`<span class="lang">${esc(l)}</span>`).join("");
const vid = g.video ? `<span class="vid">video</span>` : "";
return `<a class="row" href="${esc(g.url)}" target="_blank" rel="noopener"
style="--tc:${t.color};--tc-soft:${t.soft}">
<div>
<div class="title">${esc(g.title)}</div>
<div class="rowmeta"><span class="cat">${esc(g.category)}</span>${langs}${vid}</div>
</div>
<span class="badge">${t.label}</span>
</a>`;
}
function render(){
let rows = GUIDES.filter(matches);
if(state.sort==="az") rows.sort((a,b)=>a.title.toLowerCase().localeCompare(b.title.toLowerCase()));
else if(state.sort==="hard") rows.sort((a,b)=>b.score-a.score || a.title.localeCompare(b.title));
else rows.sort((a,b)=>a.score-b.score || a.title.localeCompare(b.title));
const main = $("main");
if(!rows.length){ main.innerHTML = `<p class="empty">No guides match these filters. Try widening them or hit <em>reset</em>.</p>`;
$("count").innerHTML = `Showing <b>0</b> of ${GUIDES.length} guides`; return; }
const grouped = state.sort!=="az";
let html = "";
if(grouped){
// score sort keeps tiers contiguous, so walk runs of the same tier
for(let i=0; i<rows.length; ){
const tier = rows[i].tier, t = TIERS[tier];
const items = [];
while(i<rows.length && rows[i].tier===tier){ items.push(rows[i]); i++; }
html += `<div class="tierhead" style="--tc:${t.color}">
<span class="bar"></span><h2>${t.label}</h2>
<span class="sub">${items.length} guide${items.length!==1?"s":""} · ${t.blurb}</span></div>`;
html += `<div class="list">${items.map(rowHtml).join("")}</div>`;
}
} else {
html = `<div class="list">${rows.map(rowHtml).join("")}</div>`;
}
main.innerHTML = html;
$("count").innerHTML = `Showing <b>${rows.length}</b> of ${GUIDES.length} guides`;
}
render();
</script>

View File

@ -0,0 +1 @@
"""Tests for the byox_ladder package."""

View File

@ -0,0 +1,99 @@
"""Unit tests for the standalone page builder."""
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from python_pkg.byox_ladder.build_pages import (
build_category_page,
build_guide_page,
main,
standalone,
)
if TYPE_CHECKING:
from pathlib import Path
def _write(dirpath: Path, name: str, body: str) -> None:
(dirpath / name).write_text(body, encoding="utf-8")
def test_standalone_wraps_content() -> None:
out = standalone("<p>hi</p>")
assert out.startswith("<!doctype html>")
assert "<p>hi</p>" in out
assert out.rstrip().endswith("</html>")
def test_build_guide_page_injects_data(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
templates = tmp_path / "templates"
templates.mkdir()
_write(
templates,
"guide.template.html",
"<title>x</title><script>/*__GUIDES__*/</script>",
)
guides_json = tmp_path / "guides.json"
guides_json.write_text('[{"title": "A"}]', encoding="utf-8")
monkeypatch.setattr("python_pkg.byox_ladder.build_pages.TEMPLATES", templates)
monkeypatch.setattr("python_pkg.byox_ladder.build_pages.GUIDES_JSON", guides_json)
html = build_guide_page()
assert "const GUIDES = [" in html
assert html.startswith("<!doctype html>")
def test_build_guide_page_missing_marker_raises(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
templates = tmp_path / "templates"
templates.mkdir()
_write(templates, "guide.template.html", "<title>no marker</title>")
guides_json = tmp_path / "guides.json"
guides_json.write_text("[]", encoding="utf-8")
monkeypatch.setattr("python_pkg.byox_ladder.build_pages.TEMPLATES", templates)
monkeypatch.setattr("python_pkg.byox_ladder.build_pages.GUIDES_JSON", guides_json)
with pytest.raises(ValueError, match="marker"):
build_guide_page()
def test_build_category_page_wraps_template(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
templates = tmp_path / "templates"
templates.mkdir()
_write(templates, "category.html", "<title>cat</title>")
monkeypatch.setattr("python_pkg.byox_ladder.build_pages.TEMPLATES", templates)
html = build_category_page()
assert "<title>cat</title>" in html
assert html.startswith("<!doctype html>")
def test_main_missing_guides_returns_error(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(
"python_pkg.byox_ladder.build_pages.GUIDES_JSON", tmp_path / "nope.json"
)
assert main() == 1
def test_main_writes_both_pages(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
templates = tmp_path / "templates"
templates.mkdir()
_write(templates, "guide.template.html", "<script>/*__GUIDES__*/</script>")
_write(templates, "category.html", "<title>cat</title>")
guides_json = tmp_path / "guides.json"
guides_json.write_text("[]", encoding="utf-8")
monkeypatch.setattr("python_pkg.byox_ladder.build_pages.TEMPLATES", templates)
monkeypatch.setattr("python_pkg.byox_ladder.build_pages.GUIDES_JSON", guides_json)
monkeypatch.setattr("python_pkg.byox_ladder.build_pages.HERE", tmp_path)
assert main() == 0
assert (tmp_path / "guide-ladder.html").exists()
assert (tmp_path / "category-ladder.html").exists()

View File

@ -0,0 +1,129 @@
"""Unit tests for the guide parser and its difficulty model."""
from __future__ import annotations
from typing import TYPE_CHECKING
from python_pkg.byox_ladder.parse_guides import (
DEFAULT_EFFORT,
clean_title,
lang_effort,
main,
parse_guide_line,
parse_readme,
split_langs,
)
if TYPE_CHECKING:
from pathlib import Path
import pytest
# A tiny README covering a real category, the Uncategorized bucket, a
# before-any-header line, and a non-guide line under a category.
README_SAMPLE = """intro line before any header
#### Build your own `Git`
* [**Go**: _Git in Go_](https://example.com/git)
not a guide line under a category
#### Uncategorized
* [**C++**: _Linux Debugger_](https://example.com/dbg)
"""
def test_split_langs_multiple_separators() -> None:
assert split_langs("C# / TypeScript & JavaScript") == [
"C#",
"TypeScript",
"JavaScript",
]
def test_split_langs_or_separator() -> None:
assert split_langs("Python or Go") == ["Python", "Go"]
def test_split_langs_empty_falls_back_to_various() -> None:
assert split_langs("**") == ["Various"]
def test_lang_effort_prefers_easiest_language() -> None:
assert lang_effort(["C", "Python"]) == 0.3
def test_lang_effort_unknown_language_uses_default() -> None:
assert lang_effort(["Cobol"]) == DEFAULT_EFFORT
def test_lang_effort_empty_uses_default() -> None:
assert lang_effort([]) == DEFAULT_EFFORT
def test_clean_title_strips_emphasis() -> None:
assert clean_title("_Hello World_") == "Hello World"
def test_parse_guide_line_full_record() -> None:
line = "* [**Go**: _Build X_](https://example.com/x) [video]"
guide = parse_guide_line(line, "Git")
assert guide is not None
assert guide["title"] == "Build X"
assert guide["langs"] == ["Go"]
assert guide["category"] == "Git"
assert guide["tier"] == "beginner"
assert guide["video"] is True
def test_parse_guide_line_without_language_prefix() -> None:
guide = parse_guide_line("* [Plain title](https://example.com)", "Git")
assert guide is not None
assert guide["langs"] == ["Various"]
assert guide["title"] == "Plain title"
assert guide["video"] is False
def test_parse_guide_line_unknown_category_is_unsorted() -> None:
guide = parse_guide_line("* [**C**: _Thing_](https://example.com)", "Uncategorized")
assert guide is not None
assert guide["tier"] == "unsorted"
assert guide["cat_rank"] == 99
def test_parse_guide_line_non_guide_returns_none() -> None:
assert parse_guide_line("not a bullet", "Git") is None
def test_parse_readme_groups_and_sorts_by_difficulty() -> None:
guides = parse_readme(README_SAMPLE)
titles = [guide["title"] for guide in guides]
assert "Git in Go" in titles
assert "Linux Debugger" in titles
# Git (rank 5) must sort ahead of the Uncategorized bucket (rank 99).
assert titles.index("Git in Go") < titles.index("Linux Debugger")
def test_parse_readme_ignores_lines_before_any_header() -> None:
assert parse_readme("* [**Go**: _Orphan_](https://example.com)\n") == []
def test_main_missing_readme_returns_error(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setattr(
"python_pkg.byox_ladder.parse_guides.README", tmp_path / "nope.md"
)
monkeypatch.setattr(
"python_pkg.byox_ladder.parse_guides.OUTPUT", tmp_path / "out.json"
)
assert main() == 1
def test_main_writes_guides_json(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
readme = tmp_path / "byox-readme.md"
readme.write_text(README_SAMPLE, encoding="utf-8")
output = tmp_path / "guides.json"
monkeypatch.setattr("python_pkg.byox_ladder.parse_guides.README", readme)
monkeypatch.setattr("python_pkg.byox_ladder.parse_guides.OUTPUT", output)
assert main() == 0
assert output.exists()