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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251 | #!/usr/bin/env python3
"""Generates the static in-browser source viewer for the published repo.
gen-browse.py --repo <path> --app <name> --out <dir> [--clone-url <url>]
Reads HEAD of --repo (tracked content only — the same bytes the published
mirror serves) and writes <out>/index.html + one page per file:
* code pages are syntax-highlighted with Pygments (pre-colored static
HTML — no JS); a plain escaped <pre> when Pygments is missing
* markdown files are rendered to HTML when python-markdown is installed
(a directory's README.md is also rendered inline under its listing);
plain <pre> otherwise
* binary, oversized (>200 KB) and lockfile pages are stubs pointing at
the git clone
Fully self-contained: the stylesheet is written into <out>/browse.css and
referenced relatively per page depth, so the viewer works at the domain root
AND under /ipfs/<cid>/ paths. No external hosts, no JS.
"""
import argparse
import html
import posixpath
import re
import subprocess
from pathlib import Path
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(linenos="table")
except ImportError: # pragma: no cover
highlight = None
FORMATTER = None
try:
import markdown
def md_html(text: str) -> str:
return markdown.markdown(
text,
extensions=["fenced_code", "tables", "codehilite"],
extension_configs={"codehilite": {"css_class": "highlight",
"guess_lang": False}},
)
except ImportError: # pragma: no cover
md_html = None
LINK_RE = re.compile(r'(<a\s[^>]*href=")([^"#?]+)([^"]*")')
def relink_md(rendered: str, base_dir: str, tracked: set) -> str:
"""Point relative links at browse pages: a markdown link to a tracked
file (README.md, src/foo.ts) 404s as-is — only <path>.html exists."""
def fix(m):
url = m.group(2)
if "://" in url or url.startswith(("/", "mailto:", "#")):
return m.group(0)
target = posixpath.normpath(posixpath.join(base_dir, url))
if target in tracked:
return m.group(1) + url + ".html" + m.group(3)
if (target + "/README.md" in tracked) or any(
t.startswith(target + "/") for t in tracked
): # directory link -> its listing
return m.group(1) + url.rstrip("/") + "/index.html" + m.group(3)
return m.group(0)
return LINK_RE.sub(fix, rendered)
GENERATED = {"package-lock.json", "yarn.lock", "pnpm-lock.yaml", "go.sum"}
MAX_RENDER = 200_000 # bytes; bigger files get a stub page
BASE_CSS = """\
:root { --bg:#131118; --panel:#1b1823; --panel-2:#221e2c; --border:#2e2938;
--text:#ece8f4; --muted:#9b93ab; --accent:#ff8a5c; --accent-2:#8f7bff; }
* { margin:0; padding:0; box-sizing:border-box; }
body { background:var(--bg); color:var(--text);
font:15px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
padding:28px 16px 60px; }
main { max-width:960px; margin:0 auto; }
a { color:var(--accent); text-decoration:none; }
a:hover { text-decoration:underline; }
h1 { font-size:22px; margin:10px 0 16px; }
.crumb { color:var(--muted); font-size:13px; margin-bottom:6px; }
.crumb a { color:var(--accent-2); }
.muted { color:var(--muted); font-size:13px; margin-top:22px; }
code { font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:13px;
background:var(--panel-2); border-radius:5px; padding:1px 6px; }
ul.tree { list-style:none; margin:8px 0; }
ul.tree li { border-bottom:1px solid var(--border); }
ul.tree a { display:flex; justify-content:space-between; padding:7px 8px;
font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:13.5px; }
ul.tree a:hover { background:var(--panel); text-decoration:none; }
.sz { color:var(--muted); font-size:12px; }
pre, .highlight, .highlighttable { background:var(--panel);
border:1px solid var(--border); border-radius:10px; overflow-x:auto; }
pre { padding:12px 14px;
font:12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
.highlight pre, .highlighttable pre { border:0; border-radius:0; background:none; }
.highlighttable { display:block; width:100%; padding:10px 4px; }
.highlighttable td.linenos { color:var(--muted); padding-right:10px;
text-align:right; user-select:none; }
.md { margin-top:12px; }
.md h1, .md h2, .md h3 { margin:18px 0 8px; }
.md p, .md li { margin:7px 0; color:#d5cfe1; }
.md ul, .md ol { padding-left:22px; }
.md pre { margin:10px 0; }
.md table { border-collapse:collapse; margin:10px 0; display:block; overflow-x:auto; }
.md th, .md td { border:1px solid var(--border); padding:5px 10px; font-size:13.5px; }
"""
ap = argparse.ArgumentParser()
ap.add_argument("--repo", required=True)
ap.add_argument("--app", required=True)
ap.add_argument("--out", required=True)
ap.add_argument("--clone-url", default=None)
args = ap.parse_args()
APP = args.app
OUT = Path(args.out)
CLONE = args.clone_url or f"https://cod-sursa.cautpartener.ro/{APP}.git"
def head_tag(depth: int, title: str) -> str:
css = "../" * depth + "browse.css"
return (
f'<!doctype html><meta charset="utf-8">'
f'<meta name="viewport" content="width=device-width,initial-scale=1">'
f'<link rel="stylesheet" href="{css}"><title>{html.escape(title)}</title>'
)
def sh(*a: str) -> bytes:
return subprocess.check_output(a, cwd=args.repo)
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)
out = [f'<a href="{"../" * depth or "./"}index.html">{html.escape(APP)}</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)
def footer(depth: int) -> str:
up = "../" * depth
return (
f'<p class="muted">static mirror of HEAD · <a href="{up}../index.html">about</a>'
f' · clone: <code>git clone {html.escape(CLONE)}</code></p>'
)
OUT.mkdir(parents=True, exist_ok=True)
css = BASE_CSS
if FORMATTER is not None:
css += "\n" + FORMATTER.get_style_defs(".highlight")
(OUT / "browse.css").write_text(css)
files = [] # (path, size)
for line in sh("git", "ls-tree", "-r", "--long", "HEAD").decode().splitlines():
meta, path = line.split("\t", 1)
size = meta.split()[3]
files.append((path, int(size) if size.isdigit() else 0))
tracked = {p for p, _ in files}
dirs: dict = {"": []}
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)
depth = d.count("/") + (1 if d else 0)
rows = []
if d:
rows.append('<li><a href="../index.html">../</a></li>')
readme = None
for it in sorted(items, key=lambda x: (x[0] != "d", x[1].lower())):
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)}'
f'<span class="sz">{size:,} B</span></a></li>'
)
if name.lower() == "readme.md":
readme = p
inline_md = ""
if readme and md_html is not None:
text = sh("git", "show", f"HEAD:{readme}").decode("utf-8", "replace")
inline_md = ('<div class="md" style="margin-top:16px">'
+ relink_md(md_html(text), d, tracked) + '</div>')
title = d or APP
out.write_text(
f"{head_tag(depth, title)}<main>"
f'<div class="crumb">{crumbs(d, False)}</div>'
f'<h1>{html.escape(title)}/</h1><ul class="tree">{"".join(rows)}</ul>'
f"{inline_md}{footer(depth)}</main>"
)
for path, size in files:
out = OUT / (path + ".html")
out.parent.mkdir(parents=True, exist_ok=True)
depth = path.count("/")
blob = sh("git", "show", f"HEAD:{path}")
name = path.rsplit("/", 1)[-1]
if name in GENERATED:
body = f'<p class="muted">generated file · {size:,} bytes · not rendered — see the git clone</p>'
elif b"\0" in blob[:8000]:
body = f'<p class="muted">binary file · {size:,} bytes</p>'
elif size > MAX_RENDER:
body = f'<p class="muted">file too large to render ({size:,} bytes) — see the git clone</p>'
else:
text = blob.decode("utf-8", "replace")
if name.lower().endswith((".md", ".markdown")) and md_html is not None:
base = path.rsplit('/', 1)[0] if '/' in path else ''
body = '<div class="md">' + relink_md(md_html(text), base, tracked) + '</div>'
elif highlight is not None:
try:
lexer = guess_lexer_for_filename(path, text)
except ClassNotFound:
lexer = TextLexer()
body = highlight(text, lexer, FORMATTER)
else:
body = f'<pre class="plain">{html.escape(text)}</pre>'
out.write_text(
f"{head_tag(depth, name)}<main>"
f'<div class="crumb">{crumbs(path, True)}</div>{body}{footer(depth)}</main>'
)
hl = "pygments" if highlight is not None else "plain <pre> (Pygments not installed)"
md = "python-markdown" if md_html is not None else "plain (python-markdown not installed)"
print(f"browse[{APP}]: {len(files)} files, {len(dirs)} dirs -> {OUT} (code: {hl}; md: {md})")
|