sueta / deploy / gen-browse.py
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
#!/usr/bin/env python3
"""Generates a static code browser (/browse/) for the public git site.

Reads HEAD of the repo it's run in; writes dir indexes + escaped file pages.
Pure stdlib — runs anywhere publish-repo.sh runs.
"""
import html
import subprocess
import sys
from pathlib import Path

# Syntax highlighting is a build-time nicety: with Pygments installed the
# pages ship pre-colored static HTML (no JS, no CDN); without it they fall
# back to plain escaped <pre>.
try:
    from pygments import highlight
    from pygments.formatters import HtmlFormatter
    from pygments.lexers import TextLexer, guess_lexer_for_filename
    from pygments.util import ClassNotFound

    FORMATTER = HtmlFormatter(style="github-dark", linenos="table", classprefix="pyg-")
    PYG_CSS = "<style>" + FORMATTER.get_style_defs(".highlight") + """
.highlight{background:var(--panel)!important;border:1px solid var(--border);border-radius:10px;padding:10px 6px;overflow-x:auto}
.highlight pre{background:none!important;border:none;padding:2px 8px}
.highlight table{border-spacing:0} .highlight .linenos pre{color:#4d5666;user-select:none;text-align:right}
</style>"""
except ImportError:  # pragma: no cover
    highlight = None
    PYG_CSS = ""

OUT = Path(sys.argv[1]) / "browse"
STYLE = """<style>
:root{--bg:#0d1117;--panel:#161b22;--border:#262c36;--text:#e6edf3;--muted:#8b949e;--accent:#3fb68b}
*{margin:0;padding:0;box-sizing:border-box}
body{background:var(--bg);color:var(--text);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;padding:28px 16px}
main{max-width:920px;margin:0 auto}
h1{font-size:19px;margin-bottom:14px;color:var(--accent)}
h1 a{color:var(--muted);text-decoration:none} h1 a:hover{color:var(--accent)}
ul{list-style:none;background:var(--panel);border:1px solid var(--border);border-radius:10px;overflow:hidden}
li{border-bottom:1px solid var(--border)} li:last-child{border-bottom:none}
li a{display:flex;justify-content:space-between;padding:9px 14px;color:var(--text);text-decoration:none}
li a:hover{background:var(--border)} li .sz{color:var(--muted);font-size:12.5px}
pre{background:var(--panel);border:1px solid var(--border);border-radius:10px;padding:14px 16px;overflow-x:auto;font:12.8px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace;tab-size:2}
.crumb{margin-bottom:12px;font-size:13.5px}.crumb a{color:var(--accent);text-decoration:none}
.muted{color:var(--muted);font-size:13px;margin:10px 0}
</style>"""


def sh(*args: str) -> bytes:
    return subprocess.check_output(args)


def crumbs(path: str, is_file: bool) -> str:
    parts = path.split("/") if path else []
    dirparts = parts[:-1] if is_file else parts
    depth = len(dirparts)  # directories between browse/ and this page's directory
    out = [f'<a href="{"../" * depth or "./"}index.html">sueta</a>']
    for i, p in enumerate(dirparts):
        out.append(f'<a href="{"../" * (depth - 1 - i)}index.html">{html.escape(p)}</a>')
    if is_file and parts:
        out.append(html.escape(parts[-1]))
    return " / ".join(out)


entries = [
    line.split("\t", 1)
    for line in sh("git", "ls-tree", "-r", "--long", "HEAD").decode().splitlines()
]
files = []  # (path, size)
for meta, path in entries:
    size = meta.split()[3]
    files.append((path, int(size) if size.isdigit() else 0))

dirs: dict[str, list] = {"": []}
for path, size in files:
    parts = path.split("/")
    for i in range(len(parts) - 1):
        d = "/".join(parts[: i + 1])
        parent = "/".join(parts[:i])
        dirs.setdefault(d, [])
        if ("d", parts[i]) not in [(t, n) for t, n, *_ in dirs[parent]]:
            dirs[parent].append(("d", parts[i]))
    dirs["/".join(parts[:-1])].append(("f", parts[-1], size, path))

for d, items in dirs.items():
    out = OUT / d / "index.html"
    out.parent.mkdir(parents=True, exist_ok=True)
    rows = []
    if d:
        rows.append('<li><a href="../index.html">../</a></li>')
    for it in sorted(items, key=lambda x: (x[0] != "d", x[1])):
        if it[0] == "d":
            rows.append(f'<li><a href="{html.escape(it[1])}/index.html">{html.escape(it[1])}/</a></li>')
        else:
            _, name, size, _p = it
            rows.append(
                f'<li><a href="{html.escape(name)}.html">{html.escape(name)}<span class="sz">{size:,} B</span></a></li>'
            )
    title = html.escape(d or "sueta")
    out.write_text(
        f'<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
        f"<title>{title}</title>{STYLE}<main><div class=crumb>{crumbs(d, False)}</div>"
        f'<h1>{title or "sueta"}/</h1><ul>{"".join(rows)}</ul>'
        f'<p class=muted>static mirror of HEAD · <a style="color:var(--accent)" href="{"../" * (d.count("/") + (1 if d else 0))}../index.html">about</a> · clone: <code>git clone https://git-chat.ardegazu.ro/sueta.git</code></p></main>'
    )

for path, size in files:
    out = OUT / (path + ".html")
    out.parent.mkdir(parents=True, exist_ok=True)
    blob = sh("git", "show", f"HEAD:{path}")
    extra = ""
    if b"\0" in blob[:8000]:
        body = f'<p class="muted">binary file · {size:,} bytes</p>'
    else:
        text = blob.decode("utf-8", "replace")
        body = f"<pre>{html.escape(text)}</pre>"
        if highlight is not None:
            try:
                lexer = guess_lexer_for_filename(path, text)
            except ClassNotFound:
                lexer = TextLexer()
            body = highlight(text, lexer, FORMATTER)
            extra = PYG_CSS
    name = html.escape(path.rsplit("/", 1)[-1])
    out.write_text(
        f'<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">'
        f"<title>{name}</title>{STYLE}{extra}<main><div class=crumb>{crumbs(path, True)}</div>{body}</main>"
    )

print(f"browse: {len(files)} files, {len(dirs)} dirs")