|
| 1 | +"""Construit le site dans _site/ : la page d'accueil (docs/) et les pages de documentation, |
| 2 | +rendues depuis le README et le guide de contribution du dépôt pour ne jamais s'en écarter. |
| 3 | +
|
| 4 | + uvx --with markdown --with pymdown-extensions python docs/build.py && python3 -m http.server -d _site |
| 5 | +""" |
| 6 | + |
| 7 | +import html |
| 8 | +import os |
| 9 | +import re |
| 10 | +import shutil |
| 11 | +from pathlib import Path |
| 12 | + |
| 13 | +import markdown |
| 14 | +from pymdownx.slugs import slugify |
| 15 | + |
| 16 | +ROOT = Path(__file__).resolve().parent.parent |
| 17 | +SITE = ROOT / "docs" |
| 18 | +OUT = ROOT / "_site" |
| 19 | +REPO = "https://github.com/XNinety9/Printr" |
| 20 | +BRANCH = "master" |
| 21 | + |
| 22 | +# (page produite, titre, source, libellé dans la navigation) |
| 23 | +PAGES = [ |
| 24 | + ("guide.html", "Guide", ROOT / "README.md", "Guide"), |
| 25 | + ("contribuer.html", "Contribuer", ROOT / "CONTRIBUTING.md", "Contribuer"), |
| 26 | +] |
| 27 | +# Fichiers du dépôt qui ont leur page sur le site. |
| 28 | +PAGE_FOR = {"README.md": "guide.html", "CONTRIBUTING.md": "contribuer.html"} |
| 29 | +# Ce qui, dans docs/, ne sert qu'à fabriquer le site ou ses illustrations. |
| 30 | +SKIP = shutil.ignore_patterns("build.py", "demo", "*.sh", "*.py", "__pycache__") |
| 31 | + |
| 32 | +TEMPLATE = """<!DOCTYPE html> |
| 33 | +<html lang="fr"> |
| 34 | +<head> |
| 35 | +<meta charset="utf-8"> |
| 36 | +<meta name="viewport" content="width=device-width, initial-scale=1"> |
| 37 | +<title>{title} · Printr</title> |
| 38 | +<meta name="description" content="Documentation de Printr : {title_lower}."> |
| 39 | +<link rel="icon" href="mark.png" type="image/png"> |
| 40 | +<link rel="preconnect" href="https://fonts.googleapis.com"> |
| 41 | +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
| 42 | +<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet"> |
| 43 | +<script> |
| 44 | + // Thème choisi sur la page d'accueil, avant le premier affichage. |
| 45 | + try {{ var t = localStorage.getItem("printr-theme"); if (t === "dark" || t === "light") document.documentElement.setAttribute("data-theme", t); }} catch (e) {{}} |
| 46 | +</script> |
| 47 | +<link rel="stylesheet" href="docs.css"> |
| 48 | +</head> |
| 49 | +<body> |
| 50 | +<nav class="nav"> |
| 51 | + <div class="wrap"> |
| 52 | + <a class="brand" href="./"><img src="mark.png" alt="">Printr</a> |
| 53 | + <div class="links">{nav}<a href="{repo}">GitHub</a></div> |
| 54 | + </div> |
| 55 | +</nav> |
| 56 | +<main class="wrap docs"> |
| 57 | + <aside class="toc"><div class="title">Sur cette page</div>{toc}</aside> |
| 58 | + <article class="doc"> |
| 59 | + <h1>{title}</h1> |
| 60 | + {content} |
| 61 | + <p class="caption">Page générée depuis <a href="{repo}/blob/{branch}/{source}">{source}</a>.</p> |
| 62 | + </article> |
| 63 | +</main> |
| 64 | +<script src="toc.js" defer></script> |
| 65 | +</body> |
| 66 | +</html> |
| 67 | +""" |
| 68 | + |
| 69 | + |
| 70 | +def readme_body(text: str) -> str: |
| 71 | + """Le README sans son logo, sa devise, ses badges et ses liens d'en-tête : le site les a.""" |
| 72 | + return re.sub(r"\A(?:\s*<p align=\"center\">.*?</p>)+\s*", "", text, flags=re.S) |
| 73 | + |
| 74 | + |
| 75 | +def rewrite_links(body: str, source: Path) -> str: |
| 76 | + """Liens vers les autres pages Markdown : leur page du site ; images de docs/ : le site ; |
| 77 | + autres fichiers du dépôt : GitHub.""" |
| 78 | + base = source.parent.relative_to(ROOT) |
| 79 | + |
| 80 | + def target(url: str) -> str: |
| 81 | + if re.match(r"^(https?:|mailto:|#|data:)", url): |
| 82 | + return url |
| 83 | + path, _, anchor = url.partition("#") |
| 84 | + repo_path = os.path.normpath((base / path).as_posix()).replace("\\", "/") |
| 85 | + suffix = f"#{anchor}" if anchor else "" |
| 86 | + if repo_path in PAGE_FOR: |
| 87 | + return PAGE_FOR[repo_path] + suffix |
| 88 | + if repo_path.startswith("docs/") and (ROOT / repo_path).is_file(): |
| 89 | + return repo_path.removeprefix("docs/") + suffix |
| 90 | + kind = "tree" if (ROOT / repo_path).is_dir() else "blob" |
| 91 | + return f"{REPO}/{kind}/{BRANCH}/{repo_path}{suffix}" |
| 92 | + |
| 93 | + def fix(match: re.Match) -> str: |
| 94 | + attr, url = match.group(1), html.unescape(match.group(2)) |
| 95 | + if attr == "srcset": |
| 96 | + url = ", ".join(" ".join([target(p.split()[0]), *p.split()[1:]]) for p in url.split(",")) |
| 97 | + else: |
| 98 | + url = target(url) |
| 99 | + return f'{attr}="{html.escape(url)}"' |
| 100 | + |
| 101 | + return re.sub(r'(href|src|srcset)="([^"]+)"', fix, body) |
| 102 | + |
| 103 | + |
| 104 | +def render(page: str, title: str, source: Path) -> str: |
| 105 | + text = source.read_text() |
| 106 | + text = readme_body(text) if source.name == "README.md" else re.sub(r"^# .*\n", "", text, count=1) |
| 107 | + md = markdown.Markdown( |
| 108 | + # superfences : blocs de code dans les listes ; md_in_html : Markdown dans le HTML du README. |
| 109 | + extensions=["pymdownx.superfences", "tables", "toc", "sane_lists", "attr_list", "md_in_html"], |
| 110 | + extension_configs={ |
| 111 | + # Ancres à la GitHub (accents gardés) : les liens internes du README restent valides. |
| 112 | + "toc": {"permalink": "#", "toc_depth": "2-3", "slugify": slugify(case="lower")}, |
| 113 | + }, |
| 114 | + ) |
| 115 | + body = rewrite_links(md.convert(text), source) |
| 116 | + nav = "".join( |
| 117 | + f'<a href="{p}"{" aria-current=\"page\"" if p == page else ""}>{label}</a>' for p, _, _, label in PAGES |
| 118 | + ) |
| 119 | + return TEMPLATE.format( |
| 120 | + title=title, |
| 121 | + title_lower=title.lower(), |
| 122 | + nav=nav, |
| 123 | + # Le sommaire de Python-Markdown a son propre <div class="toc"> : la page fournit le sien. |
| 124 | + toc=re.sub(r'\A\s*<div class="toc">\s*|\s*</div>\s*\Z', "", md.toc), |
| 125 | + content=body, |
| 126 | + repo=REPO, |
| 127 | + branch=BRANCH, |
| 128 | + source=source.relative_to(ROOT).as_posix(), |
| 129 | + ) |
| 130 | + |
| 131 | + |
| 132 | +def main() -> None: |
| 133 | + shutil.rmtree(OUT, ignore_errors=True) |
| 134 | + shutil.copytree(SITE, OUT, ignore=SKIP) |
| 135 | + for page, title, source, _ in PAGES: |
| 136 | + (OUT / page).write_text(render(page, title, source)) |
| 137 | + (OUT / ".nojekyll").touch() # fichiers servis tels quels |
| 138 | + pages = sorted(p.name for p in OUT.glob("*.html")) |
| 139 | + print(f"site construit dans {OUT.relative_to(ROOT)} : " + ", ".join(pages)) |
| 140 | + |
| 141 | + |
| 142 | +if __name__ == "__main__": |
| 143 | + main() |
0 commit comments