Skip to content

Instantly share code, notes, and snippets.

@timm
Last active June 27, 2026 01:37
Show Gist options
  • Select an option

  • Save timm/5c0f4a9847f08612d43056444807585b to your computer and use it in GitHub Desktop.

Select an option

Save timm/5c0f4a9847f08612d43056444807585b to your computer and use it in GitHub Desktop.
gistsite: render konfig-style gists into a static html catalog. http://tiny.cc/gistsite

AuthorLanguageDepsLicensePurpose

gistsite: one short file that turns a GitHub user's gists into a static catalog. It keeps only the curated ones -- gists whose files include a ,<name>.md README hook (the konfig convention) -- fetches each README, pipes it through pandoc (gfm -> html), and writes <name>.html plus an index.html roster. Pure stdlib + pandoc; every page links timm.fyi's site.css, so the catalog matches the rest of the site with no extra config.

# install and test
git clone http://tiny.cc/gistsite && cd gistsite
python3 -B gistsite.py --checks      # self-tests (no network)
python3 -B gistsite.py -o docs       # render catalog -> docs/

qr

Sections: NAME | SYNOPSIS | OPTIONS | DETECT | OUTPUT | TESTS | SEE ALSO | LICENSE | AUTHOR

Files: gistsite.py | Makefile | pyproject.toml

NAME

gistsite - render konfig-style gists into a static html catalog

SYNOPSIS

python3 -B gistsite.py [-u USER] [-o DIR] [-c CSS] [--checks] [-h]

OPTIONS

-u --user  github user                       user=timm
-o --out   output dir                         out=docs
-c --css   stylesheet href for every page     css=https://timm.fyi/site.css
   --checks run self-tests (no network)
-h --help  show the docstring

DETECT

A gist is curated if one of its files is named ,<name>.md. The leading comma sorts that README to the top of the gist listing (see konfig's style_gist). gistsite reads the GitHub gists API, keeps only those gists, and takes <name> (comma + .md stripped) as the page slug. Everything else -- scratch gists, snippets -- is skipped.

,xomo.md     -> xomo.html      (curated)
,nuff.md     -> nuff.html      (curated)
README.md    -> skipped
snippet.py   -> skipped

OUTPUT

One <name>.html per curated gist (its rendered README) plus an index.html roster:

gist     what                                    src   updated
nuff     tiny stdlib python tricks library       src   2026-06-12
xomo     Monte-Carlo COCOMO-II + COQUALMO        src   2026-06-15

The src link points back to the gist on github; the gist name links to its rendered page. Unauthenticated, the API allows 60 requests/hour -- ample for a personal roster.

TESTS

--checks runs every test_* (no network needed):

test_curated   ,name.md is detected, slug strips comma + ext
test_skip      a gist with no ,name.md is ignored
test_pandoc    pandoc renders gfm to html
test_wrap      a page carries its title + css href
test_esc       html-special chars are escaped in the roster

SEE ALSO

konfig    http://tiny.cc/konfig   shared Makefile/boilerplate + styles
nuff      http://tiny.cc/nuff     tiny stdlib python tricks (a catalog entry)
xomo      http://tiny.cc/xomo     cocomo-ii + coqualmo    (a catalog entry)
live      https://timm.fyi/gists/ the rendered catalog

LICENSE

MIT. https://choosealicense.com/licenses/mit/

AUTHOR

Tim Menzies, timm@ieee.org

_ _ _ _
__ _(_)___| |_| (_) |_ ___
/ _` | / __| __| | | __/ _ \
| (_| | \__ \ |_| | | || __/
\__, |_|___/\__|_|_|\__\___|
|___/
catalog konfig-style gists. http://tiny.cc/gistsite
"GitHub gists -> a static man-page catalog."
#!/usr/bin/env python3 -B
"""
gistsite.py, render konfig-style gists into a static catalog (cli)
(c) 2026, Tim Menzies <timm@ieee.org>, MIT license
Hit the GitHub gists API for a user, keep only the curated ones (a
file named `,<name>.md` is the README hook -- see konfig style_gist),
fetch each README, run it through pandoc (gfm -> html), and write
<name>.html plus an index.html roster. Pure stdlib + pandoc; pages
inherit timm.fyi's site.css, so the catalog looks like the rest of
the site with zero extra config.
Note: GitHub gists have no folders, so the API returns EVERY gist with
a ,name.md hook -- including retired ones. A local old/ dir means
nothing to the API; exclude stale gists by slug with --skip.
Options:
-u --user github user user=timm
-o --out output dir out=docs
-c --css stylesheet href for every page css=https://timm.fyi/site.css
-x --skip comma list of slugs to exclude skip=
eg: python3 gistsite.py --checks # self-tests (no network)
python3 gistsite.py -o docs # render catalog -> docs/
python3 gistsite.py --skip ape,ruler # drop retired gists
"""
import os, re, sys, json, subprocess
from urllib.request import urlopen, Request
from types import SimpleNamespace as o
HDRS = {"User-Agent": "gistsite",
"Accept": "application/vnd.github+json"}
HOOK = re.compile(r"^,(.+)\.md$") # a curated gist's README filename
# ## github + render --------------------------------------------
def get(url, raw=False):
"Fetch a url with the API headers; json unless raw."
txt = urlopen(Request(url, headers=HDRS)).read().decode()
return txt if raw else json.loads(txt)
def gists(user):
"Every gist for a user, walking the paginated API."
out, page = [], 1
while batch := get(f"https://api.github.com/users/{user}"
f"/gists?per_page=100&page={page}"):
out += batch; page += 1
return out
def slug(s):
"Filesafe page name: lowercase, runs of non-alphanumerics -> single -."
return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")
def curated(gist):
"If a gist has a ,name.md README, return its catalog record, else None."
for fname, f in gist["files"].items():
if m := HOOK.match(fname):
return o(name=slug(m.group(1)), raw=f["raw_url"], url=gist["html_url"],
desc=gist["description"] or m.group(1),
at=(gist["updated_at"] or "")[:10])
def pandoc(md):
"gfm markdown -> html fragment."
p = subprocess.run(["pandoc", "-f", "gfm", "-t", "html"],
input=md, capture_output=True, text=True)
return p.stdout
def ghanchor(filename):
"GitHub gist's in-page anchor id for a filename (lowercase, .->-)."
return "file-" + re.sub(r"[^a-z0-9_]", "-", filename.lower())
def relink(html, gist):
"Point README #file-* links at the real file (raw url), else the gist."
raws = {ghanchor(fn): f["raw_url"] for fn, f in gist["files"].items()}
fix = lambda m: 'href="%s"' % raws.get(
m.group(1), gist["html_url"] + "#" + m.group(1))
return re.sub(r'href="#(file-[a-z0-9_-]+)"', fix, html)
# ## html templates ---------------------------------------------
def esc(s):
return (s or "").replace("&","&amp;").replace("<","&lt;").replace(">","&gt;")
STYLE = """<style>
body{max-width:860px}
pre{overflow-x:auto;white-space:pre;color:#e6edf3;
background:linear-gradient(145deg,#0d1117 0%,#161b22 60%,#1b2230 100%);
border:1px solid #30363d;border-left:3px solid #ffb86c;
border-radius:8px;padding:12px 14px;
box-shadow:inset 0 1px 0 #000, 0 0 20px rgba(255,184,108,.07)}
code{font-family:ui-monospace,Menlo,Consolas,monospace}
.sourceCode .co{color:#888;font-style:italic}
.sourceCode .kw,.sourceCode .cf{color:#ff7b72}
.sourceCode .fu,.sourceCode .at{color:#d2a8ff}
.sourceCode .st,.sourceCode .vs,.sourceCode .ss{color:#a5d6ff}
.sourceCode .dv,.sourceCode .bn,.sourceCode .fl{color:#79c0ff}
.sourceCode .va,.sourceCode .cn{color:#ffa657}
.sourceCode .op,.sourceCode .sc{color:#e0e0e0}
</style>"""
HEAD = """<!DOCTYPE html><html lang="en"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="{css}">
{style}
<title>{title}</title></head><body><main>
<nav><span><b><a href="index.html">tools</a></b> |
<a href="https://timm.fyi">timm.fyi</a></span>
<span><a href="https://github.com/{user}">github</a></span></nav>
<hr>
"""
FOOT = ('\n<hr>\n<p class="foot"><span>built by '
'<a href="http://tiny.cc/gistsite">gistsite</a></span></p>\n'
'</main></body></html>\n')
def wrap(title, body, the):
head = HEAD.format(css=the.css, title=esc(title), user=the.user, style=STYLE)
return head + body + FOOT
def roster(items, the):
"The index.html body: one table row per curated gist."
rows = "\n".join(
f'<tr><td><a href="{g.name}.html">{esc(g.name)}</a></td>'
f'<td>{esc(g.desc)}</td>'
f'<td><a href="{g.url}">src</a></td><td>{g.at}</td></tr>'
for g in items)
return (f"<h1>tools</h1>\n<p>{len(items)} curated gists by "
f'<a href="https://github.com/{the.user}">{the.user}</a>. each '
"carries a one-file README, runs in three commands.</p>\n"
"<table><tr><th>tool</th><th>what</th><th>src</th>"
f"<th>updated</th></tr>\n{rows}\n</table>")
# ## build ------------------------------------------------------
def build(the):
os.makedirs(the.out, exist_ok=True)
skip = {s for s in getattr(the, "skip", "").split(",") if s}
items = []
for gist in gists(the.user):
if (g := curated(gist)) and g.name not in skip:
open(f"{the.out}/{g.name}.html", "w").write(
wrap(g.name, relink(pandoc(get(g.raw, raw=True)), gist), the))
items.append(g); print("ok", g.name)
items.sort(key=lambda g: g.name)
open(f"{the.out}/index.html", "w").write(wrap("gists", roster(items, the), the))
print(f"# {len(items)} gists -> {the.out}")
# ## checks (run via --checks; no network) ----------------------
def test_curated():
"A ,name.md file marks a curated gist; its name drops comma + ext."
g = curated({"description": "d", "html_url": "u",
"updated_at": "2026-06-15T09:00:00Z",
"files": {",xomo.md": {"raw_url": "r"}, "xomo.py": {"raw_url": "r2"}}})
assert g.name == "xomo" and g.desc == "d" and g.at == "2026-06-15"
def test_skip():
"No ,name.md -> not curated."
assert curated({"description": None, "html_url": "u", "updated_at": "",
"files": {"README.md": {"raw_url": "r"}}}) is None
def test_pandoc():
"pandoc turns gfm into html."
assert "<h1" in pandoc("# hi")
def test_wrap():
"Page carries the title and the css href."
p = wrap("x", "body", o(css="c.css", user="timm"))
assert "<title>x</title>" in p and 'href="c.css"' in p
def test_esc():
assert esc("a&<b>") == "a&amp;&lt;b&gt;"
def test_slug():
"Slug is lowercase + filesafe; punctuation collapses to one hyphen."
assert slug("dot files") == "dot-files" and slug("Preci0us") == "preci0us"
assert slug("sand-box") == "sand-box"
def test_ghanchor():
"Filename -> GitHub gist anchor: lowercase, dot/punct -> -, _ kept."
assert ghanchor("auto93.csv") == "file-auto93-csv"
assert ghanchor("a_b.c-d.CSV") == "file-a_b-c-d-csv"
def test_relink():
"A #file-* link points at the matching file's raw url, else the gist."
g = {"html_url": "U", "files": {"auto93.csv": {"raw_url": "RAW"}}}
assert 'href="RAW"' in relink('<a href="#file-auto93-csv">x</a>', g)
assert 'href="U#file-gone-csv"' in relink('<a href="#file-gone-csv">x</a>', g)
# ## lib + cli --------------------------------------------------
def settings(s):
"Parse var=val pairs from a string into an o (val may be empty)."
return o(**dict(re.findall(r"(\w+)=(\S*)", s)))
def main(the, g):
argv = sys.argv[1:]
vals = {"-u": "user", "--user": "user", "-o": "out", "--out": "out",
"-c": "css", "--css": "css", "-x": "skip", "--skip": "skip"}
i = 0
while i < len(argv):
a = argv[i]
if "=" in a and a.lstrip("-").split("=")[0] in vals.values():
k, v = a.lstrip("-").split("=", 1); setattr(the, k, v)
elif a in vals and i+1 < len(argv):
setattr(the, vals[a], argv[i+1]); i += 1
i += 1
if "-h" in argv or "--help" in argv: return print((__doc__ or "").strip())
if "--checks" in argv:
tests = {k: v for k, v in g.items() if k.startswith("test_")}
ok = 0
for name, fn in tests.items():
try: fn(); ok += 1; print("PASS", name)
except Exception as e: print("FAIL", name, e)
return print(f"# {ok}/{len(tests)} ok")
build(the)
the = settings(__doc__)
if __name__ == "__main__":
main(the, globals())
# vim: ts=2 sw=2 sts=2 et :
# knobs only; shared targets live in $(KONFIG)/Makefile
KONFIG ?= ../konfig
APP := gistsite
MAIN := gistsite.py
EXT := py
LANG := python
SRC := *.py
LINT := ruff check gistsite.py
TOOLS := python3:run pandoc:render ruff:lint
PKG := python3 pandoc gawk ruff neovim tmux
$(KONFIG)/Makefile:
@test -f $@ || { echo "missing konfig: git clone http://tiny.cc/konfig $(KONFIG)"; exit 1; }
include $(KONFIG)/Makefile
OUT ?= docs # for: make demo OUT=dir
demo: ## render the catalog into $(OUT)/ (hits the github gists api)
@python3 -B gistsite.py -o $(OUT)
CHECKS: ## test: self-checks pass (no network)
@python3 -B gistsite.py --checks | grep -q "8/8 ok" && echo "ok checks"
test: ## run every UPPERCASE rule
@gawk -F: '/^[A-Z][A-Z_]*:[^=]/ {print $$1}' $(MAKEFILE_LIST) | \
sort -u | while read t; do \
printf "\n=== %s ===\n" "$$t"; $(MAKE) -s $$t; done
# gistsite's terse style on purpose: 2-space indent, one-line guards,
# semicolons, inline lambdas (see konfig/style_code.md ZIP + BAIL).
[tool.ruff]
line-length = 80
[tool.ruff.lint]
ignore = [
"E701", # one-line if/for/def colon bodies
"E702", # `a; b` multiple statements
"E731", # `f = lambda ...` assignments
"E741", # short names like l/o/i
"E401", # grouped `import a, b, c`
]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment