build_wiki.py (8731B)
1 #!/usr/bin/env python3 2 """ 3 Gera HTML estático em site/public/wiki/ a partir dos .txt em site/wiki/. 4 Executar localmente antes de site/genlanding.py. Não copia para o servidor 5 por si — só o conteúdo de site/public/ é implantado. 6 7 Apenas biblioteca padrão Python 3. 8 """ 9 10 from __future__ import annotations 11 12 import html 13 import re 14 import sys 15 from pathlib import Path 16 17 SCRIPT_DIR = Path(__file__).resolve().parent 18 SITE_DIR = SCRIPT_DIR.parent 19 ADMIN_DIR = SITE_DIR.parent / "scripts" / "admin" 20 if str(ADMIN_DIR) not in sys.path: 21 sys.path.insert(0, str(ADMIN_DIR)) 22 23 from admin_guard import ensure_admin_cli 24 25 OUT_DIR = SITE_DIR / "public" / "wiki" 26 SITEMAP_PATH = SITE_DIR / "public" / "sitemap.xml" 27 28 TXT_GLOB = "[0-9][0-9]_*.txt" 29 SLUG_RE = re.compile(r"^(\d+)_(.+)\.txt$") 30 31 32 def eprint(*args: object) -> None: 33 print(*args, file=sys.stderr) 34 35 36 def is_heading_line(s: str) -> bool: 37 s = s.strip() 38 if not s or len(s) > 120: 39 return False 40 letters = [c for c in s if c.isalpha()] 41 if not letters: 42 return False 43 return all(c.isupper() for c in letters) 44 45 46 def paragraph_blocks(text: str) -> list[list[str]]: 47 lines = text.strip().splitlines() 48 blocks: list[list[str]] = [] 49 cur: list[str] = [] 50 for line in lines: 51 if not line.strip(): 52 if cur: 53 blocks.append(cur) 54 cur = [] 55 else: 56 cur.append(line.rstrip()) 57 if cur: 58 blocks.append(cur) 59 return blocks 60 61 62 def block_to_html(block: list[str], *, is_first: bool) -> str: 63 if len(block) == 1: 64 line = block[0].strip() 65 if is_first: 66 return f'<h1 class="hero-title subpage-title wiki-page-title">{html.escape(line)}</h1>' 67 if is_heading_line(line): 68 return f"<h2>{html.escape(line)}</h2>" 69 return f"<p>{html.escape(line)}</p>" 70 71 stripped = [l.strip() for l in block if l.strip()] 72 if stripped and all( 73 s.startswith("- ") or s.startswith("– ") or s.startswith("— ") for s in stripped 74 ): 75 items = [] 76 for s in stripped: 77 for prefix in ("- ", "– ", "— "): 78 if s.startswith(prefix): 79 items.append(s[len(prefix) :]) 80 break 81 lis = "".join(f"<li>{html.escape(i)}</li>" for i in items) 82 return f"<ul>{lis}</ul>" 83 84 inner = "<br>\n".join(html.escape(l) for l in block) 85 return f"<p>{inner}</p>" 86 87 88 def txt_to_article_body(raw: str) -> str: 89 blocks = paragraph_blocks(raw) 90 parts: list[str] = [] 91 for i, b in enumerate(blocks): 92 parts.append(block_to_html(b, is_first=(i == 0))) 93 return "\n\n".join(parts) 94 95 96 def page_shell( 97 *, 98 title: str, 99 description: str, 100 body_main: str, 101 nav_pages: list[tuple[str, str]], 102 current_slug: str | None, 103 ) -> str: 104 nav_items = [] 105 for slug, label in nav_pages: 106 if current_slug is not None and slug == current_slug: 107 nav_items.append( 108 f'<span class="hero-nav-current" aria-current="page">{html.escape(label)}</span>' 109 ) 110 else: 111 href = "/wiki/" if slug == "index" else f"/wiki/{slug}.html" 112 nav_items.append(f'<a href="{html.escape(href, quote=True)}">{html.escape(label)}</a>') 113 nav_inner = '\n <span class="hero-nav-sep" aria-hidden="true">·</span>\n '.join( 114 nav_items 115 ) 116 return f"""<!DOCTYPE html> 117 <html lang="pt-BR"> 118 <head> 119 <meta charset="utf-8"> 120 <meta name="viewport" content="width=device-width, initial-scale=1"> 121 <title>{html.escape(title)}</title> 122 <meta name="description" content="{html.escape(description)}"> 123 <link rel="preconnect" href="https://fonts.googleapis.com"> 124 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 125 <link href="https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible:ital,wght@0,400;0,700;1,400&family=IBM+Plex+Mono:wght@400;600&family=Syne:wght@600;700;800&display=swap" rel="stylesheet"> 126 <link rel="icon" href="/favicon.svg" type="image/svg+xml"> 127 <link rel="stylesheet" href="../assets/style.css"> 128 </head> 129 <body> 130 <div class="wrap"> 131 <nav class="top-nav"><a href="/">← runv.club</a></nav> 132 133 <header> 134 <p class="eyebrow">runv.club</p> 135 <nav class="hero-nav wiki-hero-nav" aria-label="Páginas da wiki"> 136 <a href="/news/">Notícias</a> 137 <span class="hero-nav-sep" aria-hidden="true">·</span> 138 {nav_inner} 139 <span class="hero-nav-sep" aria-hidden="true">·</span> 140 <a href="/junte-se/">Junte-se</a> 141 </nav> 142 </header> 143 144 <main class="section prose-block subpage-main wiki-main"> 145 {body_main} 146 </main> 147 148 <footer class="site-footer"> 149 <p>Administração: <a href="mailto:admin@runv.club">admin@runv.club</a><span class="footer-sep" aria-hidden="true"> · </span><a href="/faq/" class="footer-link-discrete">FAQ</a></p> 150 </footer> 151 </div> 152 </body> 153 </html> 154 """ 155 156 157 LABELS: dict[str, str] = { 158 "index": "Índice", 159 "visao-geral": "Visão geral", 160 "contas-e-acesso": "Contas e acesso", 161 "regras-da-comunidade": "Regras", 162 "punicoes-e-moderacao": "Punições", 163 "privacidade-e-seguranca": "Privacidade", 164 "faq": "FAQ wiki", 165 } 166 167 168 def slug_and_label(path: Path) -> tuple[str, str] | None: 169 m = SLUG_RE.match(path.name) 170 if not m: 171 return None 172 slug = m.group(2) 173 label = LABELS.get(slug, slug.replace("-", " ").title()) 174 return slug, label 175 176 177 def first_line_title(raw: str) -> str: 178 for line in raw.strip().splitlines(): 179 t = line.strip() 180 if t: 181 return t[:70] + ("…" if len(t) > 70 else "") 182 return "Wiki" 183 184 185 def build_nav_order(paths: list[Path]) -> list[tuple[str, str]]: 186 ordered: list[tuple[str, str]] = [] 187 for p in sorted(paths): 188 sl = slug_and_label(p) 189 if sl: 190 ordered.append(sl) 191 # Índice primeiro na nav 192 idx = next((i for i, (s, _) in enumerate(ordered) if s == "index"), None) 193 if idx is not None and idx > 0: 194 ordered.insert(0, ordered.pop(idx)) 195 return ordered 196 197 198 def patch_sitemap(wiki_urls: list[str]) -> None: 199 if not SITEMAP_PATH.is_file(): 200 return 201 text = SITEMAP_PATH.read_text(encoding="utf-8") 202 marker_start = " <!-- wiki:gerado -->" 203 marker_end = " <!-- /wiki:gerado -->" 204 block_lines = [marker_start] 205 for url in wiki_urls: 206 block_lines.append(" <url>") 207 block_lines.append(f" <loc>{html.escape(url)}</loc>") 208 block_lines.append(" </url>") 209 block_lines.append(marker_end) 210 new_block = "\n".join(block_lines) + "\n" 211 212 if marker_start in text and marker_end in text: 213 before, rest = text.split(marker_start, 1) 214 _, after = rest.split(marker_end, 1) 215 text = before + new_block + after.lstrip("\n") 216 else: 217 text = text.replace( 218 "</urlset>", 219 new_block + "</urlset>", 220 1, 221 ) 222 SITEMAP_PATH.write_text(text, encoding="utf-8") 223 224 225 def main() -> int: 226 ensure_admin_cli(script_name=Path(__file__).name) 227 txt_files = sorted(SCRIPT_DIR.glob(TXT_GLOB)) 228 if not txt_files: 229 eprint("Nenhum ficheiro", TXT_GLOB, "em", SCRIPT_DIR) 230 return 1 231 232 OUT_DIR.mkdir(parents=True, exist_ok=True) 233 nav_pages = build_nav_order(txt_files) 234 235 base_url = "https://runv.club" 236 wiki_urls: list[str] = [f"{base_url}/wiki/"] 237 238 for path in txt_files: 239 sl = slug_and_label(path) 240 if not sl: 241 continue 242 slug, _label = sl 243 raw = path.read_text(encoding="utf-8") 244 title_line = first_line_title(raw) 245 article = txt_to_article_body(raw) 246 247 if slug == "index": 248 # Navegação já está em hero-nav; o corpo vem só de 01_index.txt. 249 body_main = article 250 out_name = "index.html" 251 current = "index" 252 desc = "Início da wiki runv.club: regras, contas, privacidade e FAQ." 253 else: 254 body_main = article 255 out_name = f"{slug}.html" 256 current = slug 257 desc = f"{title_line} — wiki runv.club." 258 wiki_urls.append(f"{base_url}/wiki/{slug}.html") 259 260 full_title = f"{title_line} — Wiki runv.club" if slug != "index" else "Wiki — runv.club" 261 html_out = page_shell( 262 title=full_title, 263 description=desc, 264 body_main=body_main, 265 nav_pages=nav_pages, 266 current_slug=current, 267 ) 268 (OUT_DIR / out_name).write_text(html_out, encoding="utf-8") 269 print("Wrote", OUT_DIR / out_name) 270 271 wiki_urls = sorted(set(wiki_urls)) 272 patch_sitemap(wiki_urls) 273 print("Updated", SITEMAP_PATH) 274 return 0 275 276 277 if __name__ == "__main__": 278 raise SystemExit(main())