aboutsummaryrefslogtreecommitdiffstats
path: root/scripts/gen_bots.py
diff options
context:
space:
mode:
authorAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
committerAhmed <git@gumx.cc>2026-06-14 01:46:29 +0300
commit5a8d568931d9b23ce0df1265d05259a7012081c9 (patch)
tree4ea19b8bb1763a38246f342f172c285f6579e09b /scripts/gen_bots.py
init: mostly vibed
Diffstat (limited to 'scripts/gen_bots.py')
-rw-r--r--scripts/gen_bots.py219
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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
+
+
+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])