diff options
| author | Ahmed <git@gumx.cc> | 2026-06-14 01:46:29 +0300 |
|---|---|---|
| committer | Ahmed <git@gumx.cc> | 2026-06-14 01:46:29 +0300 |
| commit | 5a8d568931d9b23ce0df1265d05259a7012081c9 (patch) | |
| tree | 4ea19b8bb1763a38246f342f172c285f6579e09b /scripts | |
init: mostly vibed
Diffstat (limited to 'scripts')
| -rw-r--r-- | scripts/gen_bots.py | 219 |
1 files changed, 219 insertions, 0 deletions
diff --git a/scripts/gen_bots.py b/scripts/gen_bots.py new file mode 100644 index 0000000..ddb2d54 --- /dev/null +++ b/scripts/gen_bots.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 +"""Generate bot documentation HTML pages from docs/*.md.""" + +import os +import re +import sys + +BOTS = [ + ("alfred", "Server management, bot control, file hosting, soju bouncer"), + ("osterman", "Channel moderation, auto-modes, idle tracking"), + ("daffy", "Duck hunting game"), + ("contessa", "Recipe suggestions"), + ("shireen", "News headlines and weather"), + ("daft", "YouTube playlist manager"), + ("herald", "Owner-follow bot and keyword responder"), + ("salvador", "Image-to-mIRC-art renderer"), +] + +STYLE = """\ +@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Regular.woff2); font-weight: normal; } +@font-face { font-family: "Kawkab Mono"; src: url(/fonts/KawkabMono-Bold.woff2); font-weight: bold; } +* { unicode-bidi: plaintext; box-sizing: border-box; } +html { color: black; background-color: white; } +body { font-family: "Kawkab Mono"; font-size: 16px; line-height: 1.4; margin: 0; padding: 4rem 0; min-height: 100%; overflow-wrap: break-word; } +main, header, footer { max-width: 800px; margin-inline: auto; padding: 0 2rem; } +h1, header, footer { text-align: center; } +main { text-align: left; } +p, h2, h3, h4 { margin: 1em 0 0 0; } +table { margin: auto; border-collapse: collapse; } +th, td { border: 1px solid; padding: 0.3em 0.8em; } +pre { background: rgba(128,128,128,0.08); padding: 1em; overflow-x: auto; margin: 1em 0; } +code { font-size: 85%; } +header { margin-bottom: 1em; } +footer { margin-top: 3em; } +a { color: inherit; } +@media (max-width: 600px) { body { font-size: 0.9em; } h1 { font-size: 1.8em; } } +@media (max-width: 400px) { body { font-size: 0.8em; } h1 { font-size: 1.6em; } } +@media (prefers-color-scheme: dark) { html { filter: invert(1); } img { filter: invert(1); } }""" + +FOOTER = """\ +<footer> +<hr> +<a href="https://gumx.cc">gumx.cc</a> / +<a href="https://git.gumx.cc">git</a> / +<a href="https://mail.gumx.cc">mail</a> / +<a href="https://irc.gumx.cc">irc</a> / +<a href="https://vpn.gumx.cc">vpn</a> / +<a href="https://pgp.gumx.cc">pgp</a> / +<a href="https://wk.fo">wk.fo</a> +</footer>""" + + +def page(title, header_html, content_html): + return f"""<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width,initial-scale=1"> +<title>{title}</title> +<style> +{STYLE} +</style> +</head> +<body> +<header> +{header_html} +</header> +<main> +{content_html} +</main> +{FOOTER} +</body> +</html> +""" + + +def md_to_html(text): + """Minimal markdown → HTML: headers, code blocks, inline code, bold, links, paragraphs.""" + lines = text.splitlines() + out = [] + i = 0 + while i < len(lines): + line = lines[i] + + # fenced code block + if line.strip().startswith("```"): + lang = line.strip()[3:].strip() + i += 1 + code_lines = [] + while i < len(lines) and not lines[i].strip().startswith("```"): + code_lines.append(_esc(lines[i])) + i += 1 + out.append(f"<pre><code>{chr(10).join(code_lines)}</code></pre>") + i += 1 + continue + + # ATX headers + m = re.match(r'^(#{1,4})\s+(.*)', line) + if m: + level = len(m.group(1)) + out.append(f"<h{level}>{_inline(m.group(2))}</h{level}>") + i += 1 + continue + + # table row + if '|' in line and line.strip().startswith('|'): + table_lines = [] + while i < len(lines) and '|' in lines[i] and lines[i].strip().startswith('|'): + table_lines.append(lines[i]) + i += 1 + out.append(_table(table_lines)) + continue + + # blank line + if not line.strip(): + i += 1 + continue + + # paragraph + para = [line] + i += 1 + while i < len(lines) and lines[i].strip() and not lines[i].startswith('#') and not lines[i].strip().startswith('```') and not ('|' in lines[i] and lines[i].strip().startswith('|')): + para.append(lines[i]) + i += 1 + out.append(f"<p>{_inline(' '.join(para))}</p>") + + return "\n".join(out) + + +def _table(rows): + html = ["<table>"] + for j, row in enumerate(rows): + cells = [c.strip() for c in row.strip().strip('|').split('|')] + if all(re.match(r'^[-: ]+$', c) for c in cells): + continue + tag = "th" if j == 0 else "td" + html.append("<tr>" + "".join(f"<{tag}>{_inline(c)}</{tag}>" for c in cells) + "</tr>") + html.append("</table>") + return "\n".join(html) + + +def _inline(text): + text = _esc(text) + # bold + text = re.sub(r'\*\*(.+?)\*\*', r'<strong>\1</strong>', text) + # inline code + text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text) + # links + text = re.sub(r'\[([^\]]+)\]\(([^)]+)\)', r'<a href="\2">\1</a>', text) + return text + + +def _esc(text): + return text.replace("&", "&").replace("<", "<").replace(">", ">") + + +def build(src_dir, out_dir): + os.makedirs(out_dir, exist_ok=True) + + # Copy fonts + fonts_src = os.path.join(src_dir, "fonts") + fonts_dst = os.path.join(out_dir, "fonts") + if os.path.isdir(fonts_src): + os.makedirs(fonts_dst, exist_ok=True) + for f in os.listdir(fonts_src): + src = os.path.join(fonts_src, f) + dst = os.path.join(fonts_dst, f) + with open(src, "rb") as fh: + data = fh.read() + with open(dst, "wb") as fh: + fh.write(data) + + # Per-bot pages + for name, desc in BOTS: + doc_path = os.path.join(src_dir, "docs", f"{name}.md") + if not os.path.exists(doc_path): + continue + with open(doc_path) as f: + md = f.read() + content = md_to_html(md) + html = page( + f"{name} — irc.gumx.cc", + f'<h1><a href="https://irc.gumx.cc">irc.gumx.cc</a> / bots / {name}</h1>', + content, + ) + bot_dir = os.path.join(out_dir, name) + os.makedirs(bot_dir, exist_ok=True) + with open(os.path.join(bot_dir, "index.html"), "w") as f: + f.write(html) + + # Index page + rows = "\n".join( + f'<tr><td><a href="{name}/">{name}</a></td><td>{desc}</td></tr>' + for name, desc in BOTS + if os.path.exists(os.path.join(src_dir, "docs", f"{name}.md")) + ) + index_content = f"""<h2>bots</h2> +<table> +<tr><th>bot</th><th>purpose</th></tr> +{rows} +</table> +<p>All bots run <a href="https://sopel.chat/">Sopel</a>. Source: <a href="https://git.gumx.cc/irc-bots">git.gumx.cc/irc-bots</a>.</p>""" + + index_html = page( + "irc bots — irc.gumx.cc", + '<h1><a href="https://irc.gumx.cc">irc.gumx.cc</a> / bots</h1>', + index_content, + ) + with open(os.path.join(out_dir, "index.html"), "w") as f: + f.write(index_html) + + print(f"Generated {len(BOTS)} bot pages + index in {out_dir}") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} <src_dir> <out_dir>", file=sys.stderr) + sys.exit(1) + build(sys.argv[1], sys.argv[2]) |
