'c','h'=>'c','cpp'=>'cpp','cc'=>'cpp','hpp'=>'cpp','py'=>'python','sh'=>'bash','bash'=>'bash',
'php'=>'php','js'=>'javascript','ts'=>'typescript','html'=>'xml','xml'=>'xml','css'=>'css','json'=>'json',
'yml'=>'yaml','yaml'=>'yaml','rs'=>'rust','go'=>'go','java'=>'java','vim'=>'vim','mk'=>'makefile',
'makefile'=>'makefile','ini'=>'ini','conf'=>'ini','toml'=>'ini','sql'=>'sql','lua'=>'lua','glsl'=>'glsl',
'vert'=>'glsl','frag'=>'glsl','s'=>'x86asm','asm'=>'x86asm','diff'=>'diff','patch'=>'diff'];
$MIME = ['png'=>'image/png','jpg'=>'image/jpeg','jpeg'=>'image/jpeg','gif'=>'image/gif','svg'=>'image/svg+xml',
'webp'=>'image/webp','pdf'=>'application/pdf','ico'=>'image/x-icon'];
$LCOLOR = ['C'=>'#555555','C++'=>'#f34b7d','Python'=>'#3572A5','Shell'=>'#89e051','JavaScript'=>'#f1e05a','TypeScript'=>'#3178c6',
'PHP'=>'#4F5D95','HTML'=>'#e34c26','CSS'=>'#663399','Rust'=>'#dea584','Go'=>'#00ADD8','Java'=>'#b07219',
'Vim Script'=>'#199f4b','Lua'=>'#000080','GLSL'=>'#5686a5','Assembly'=>'#6E4C13','Markdown'=>'#083fa1',
'Makefile'=>'#427819','Ruby'=>'#701516','Perl'=>'#0298c3','C#'=>'#178600','Kotlin'=>'#A97BFF','Swift'=>'#F05138','TeX'=>'#3D6117'];
function h(?string $s): string { return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
function valid(string $s): bool { return $s !== '.' && $s !== '..' && preg_match('/^[A-Za-z0-9._ +-]+$/', $s) === 1; }
function git(string $dir, string ...$args): string {
$cmd = 'git -c safe.directory=* -C ' . escapeshellarg($dir);
foreach ($args as $a) $cmd .= ' ' . escapeshellarg($a);
return (string)shell_exec($cmd . ' 2>/dev/null');
}
function safe_path(array $segs): ?string {
foreach ($segs as $s) if (!valid($s)) return null;
return implode('/', $segs);
}
function ago(int $t): string {
if ($t <= 0) return '';
$d = time() - $t;
foreach ([31536000=>'year', 2592000=>'month', 86400=>'day', 3600=>'hour', 60=>'minute'] as $s => $n)
if ($d >= $s) { $v = intdiv($d, $s); return "$v $n" . ($v > 1 ? 's' : '') . ' ago'; }
return 'just now';
}
function ext(string $name): string {
$b = strtolower(basename($name));
return $b === 'makefile' ? 'makefile' : strtolower(pathinfo($b, PATHINFO_EXTENSION));
}
function fmt_size(int $b): string {
if ($b < 1024) return "$b B";
if ($b < 1048576) return round($b / 1024, 1) . ' KB';
return round($b / 1048576, 1) . ' MB';
}
function lcolor(string $l): string { global $LCOLOR; return $LCOLOR[$l] ?? '#' . substr(md5($l), 0, 6); }
/* ---------- data ---------- */
function categories(): array {
global $ROOT; $cats = [];
$f = "$ROOT/.categories";
if (is_file($f)) foreach (file($f, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $l) {
[$slug, $title] = array_pad(explode('|', $l, 2), 2, '');
if (valid($slug) && is_dir("$ROOT/$slug")) $cats[$slug] = $title !== '' ? $title : $slug;
}
if (is_dir($ROOT)) foreach (scandir($ROOT) as $d)
if ($d[0] !== '.' && is_dir("$ROOT/$d") && !isset($cats[$d])) $cats[$d] = $d;
return $cats;
}
function load_repo(string $cat, string $name): ?array {
global $ROOT, $BASE, $PREFIX, $SCHEME, $HOST;
if (!valid($cat) || !valid($name)) return null;
if (is_dir("$ROOT/$cat/$name.git")) { $dir = "$ROOT/$cat/$name.git"; $isgit = true; $mf = "$dir/gitsync.meta"; }
elseif (is_dir("$ROOT/$cat/$name")) { $dir = "$ROOT/$cat/$name"; $isgit = false; $mf = "$dir/.gitsync.meta"; }
else return null;
$m = ['description'=>'', 'tags'=>'', 'origin'=>'', 'created'=>'0', 'modified'=>'0', 'languages'=>'', 'synced'=>'0'];
if (is_file($mf)) foreach (file($mf, FILE_IGNORE_NEW_LINES) as $l)
if (str_contains($l, '=')) { [$k, $v] = explode('=', $l, 2); $m[$k] = trim($v); }
$langs = []; $total = 0;
foreach (array_filter(explode(',', $m['languages'])) as $p) { [$l, $b] = array_pad(explode(':', $p), 2, '0'); $langs[$l] = (int)$b; $total += (int)$b; }
foreach ($langs as $l => $b) $langs[$l] = $total ? $b / $total * 100 : 0;
$url = $PREFIX . '/' . rawurlencode($cat) . '/' . rawurlencode($name);
return ['cat'=>$cat, 'name'=>$name, 'dir'=>$dir, 'git'=>$isgit, 'desc'=>$m['description'], 'url'=>$url,
'tags'=>array_values(array_filter(explode(',', $m['tags']))), 'origin'=>$m['origin'],
'created'=>(int)$m['created'], 'modified'=>(int)$m['modified'] ?: ($isgit ? 0 : (int)filemtime($dir)), 'synced'=>(int)$m['synced'],
'langs'=>$langs, 'private'=>$PREFIX !== '',
'clone'=>($PREFIX ? "$SCHEME://USER@$HOST" : $BASE) . "$url.git", 'archive'=>"$BASE$url/archive/" . rawurlencode($name) . '.tar.gz'];
}
function repos_in(string $cat): array {
global $ROOT; $out = [];
foreach (scandir("$ROOT/$cat") as $d) {
if ($d[0] === '.' || !is_dir("$ROOT/$cat/$d")) continue;
$r = load_repo($cat, preg_replace('/\.git$/', '', $d));
if ($r) $out[$r['name']] = $r;
}
ksort($out, SORT_NATURAL | SORT_FLAG_CASE);
return $out;
}
function parse_commit(string $l): ?array {
if ($l === '') return null;
[$H, $h, $an, $at, $s] = array_pad(explode("\x1f", $l), 5, '');
return ['H'=>$H, 'h'=>$h, 'author'=>$an, 'time'=>(int)$at, 'subject'=>$s];
}
const LOGFMT = '--format=%H%x1f%h%x1f%an%x1f%at%x1f%s';
function last_commit(array $r): ?array { return $r['git'] ? parse_commit(trim(git($r['dir'], 'log', '-1', LOGFMT))) : null; }
function branch(array $r): string { $b = trim(git($r['dir'], 'symbolic-ref', '--short', 'HEAD')); return $b !== '' ? $b : 'HEAD'; }
function obj_type(array $r, string $p): string {
if ($r['git']) return trim(git($r['dir'], 'cat-file', '-t', 'HEAD:' . $p));
$f = "{$r['dir']}/$p";
return is_dir($f) ? 'tree' : (is_file($f) ? 'blob' : '');
}
function tree(array $r, string $p): array {
$items = [];
if ($r['git']) {
$spec = $p === '' ? 'HEAD' : "HEAD:$p";
foreach (explode("\n", trim(git($r['dir'], 'ls-tree', '-l', $spec))) as $l) {
if ($l === '' || !preg_match('/^(\d+) (\w+) (\w+) +(-|\d+)\t(.+)$/', $l, $m)) continue;
$items[] = ['name'=>$m[5], 'dir'=>$m[2] === 'tree', 'size'=>$m[4] === '-' ? 0 : (int)$m[4]];
}
} else {
$base = rtrim("{$r['dir']}/$p", '/');
foreach (scandir($base) as $f) {
if ($f === '.' || $f === '..' || $f === '.gitsync.meta') continue;
$items[] = ['name'=>$f, 'dir'=>is_dir("$base/$f"), 'size'=>is_file("$base/$f") ? (int)filesize("$base/$f") : 0];
}
}
usort($items, fn($a, $b) => [$b['dir'], strtolower($a['name'])] <=> [$a['dir'], strtolower($b['name'])]);
return $items;
}
function blob(array $r, string $p): string {
return $r['git'] ? git($r['dir'], 'cat-file', 'blob', "HEAD:$p") : (string)file_get_contents("{$r['dir']}/$p");
}
function find_readme(array $r): ?string {
foreach (tree($r, '') as $i) if (!$i['dir'] && preg_match('/^readme(\.(md|markdown|txt))?$/i', $i['name'])) return $i['name'];
return null;
}
function activity_days(array $r): array { /* day => commits, last 53 weeks */
$days = [];
foreach (explode("\n", trim(git($r['dir'], 'log', '--all', '--since=53 weeks ago', '--format=%at'))) as $t)
if ($t !== '') { $k = date('Y-m-d', (int)$t); $days[$k] = ($days[$k] ?? 0) + 1; }
return $days;
}
/* ---------- markdown (server side, keeps lynx happy) ---------- */
function md_inline(string $s, string $rawbase): string {
$s = h($s);
$s = preg_replace_callback('/`([^`]+)`/', fn($m) => '' . $m[1] . '', $s);
$fix = fn($u) => preg_match('#^([a-z]+:|/|\#)#i', $u) ? $u : $rawbase . ltrim($u, './');
$s = preg_replace_callback('/!\[([^\]]*)\]\(([^)\s]+)[^)]*\)/', fn($m) => '', $s);
$s = preg_replace_callback('/\[([^\]]+)\]\(([^)\s]+)[^)]*\)/', fn($m) => '' . $m[1] . '', $s);
$s = preg_replace('#(?])\bhttps?://[^\s<]+#', '$0', $s);
$s = preg_replace('/(\*\*|__)(.+?)\1/', '$2', $s);
$s = preg_replace('/(?$2', $s);
$s = preg_replace('/~~(.+?)~~/', '
$1', $s);
return $s;
}
function md(string $src, string $rawbase): string {
$lines = explode("\n", str_replace("\r", '', $src)); $n = count($lines); $out = ''; $i = 0;
$list = ''; $para = [];
$close_list = function () use (&$list, &$out) { if ($list) { $out .= "$list>"; $list = ''; } };
$flush = function () use (&$para, &$out, $rawbase) { if ($para) { $out .= '
' . md_inline(implode("\n", $para), $rawbase) . '
'; $para = []; } }; while ($i < $n) { $l = $lines[$i]; if (preg_match('/^\s*(```|~~~)\s*(\w*)/', $l, $m)) { $flush(); $close_list(); $buf = []; $i++; while ($i < $n && !preg_match('/^\s*' . preg_quote($m[1]) . '/', $lines[$i])) $buf[] = $lines[$i++]; $i++; $out .= '' . h(implode("\n", $buf)) . ""; continue;
}
if (preg_match('/^(#{1,6})\s+(.*?)\s*#*$/', $l, $m)) { $flush(); $close_list(); $k = strlen($m[1]); $out .= "' . md_inline($m[1], $rawbase) . ''; } elseif (preg_match('/^\s*([-*_])(\s*\1){2,}\s*$/', $l)) { $flush(); $close_list(); $out .= '
' . h(implode("\n", $buf)) . ''; continue;
}
elseif (trim($l) === '') { $flush(); $close_list(); }
else $para[] = $l;
$i++;
}
$flush(); $close_list();
return $out;
}
/* ---------- html ---------- */
function page_start(string $title, array $crumbs = []): void {
global $HOST;
echo "";
echo 'Not found.
'; page_end(); exit; } function hue(string $t): int { return hexdec(substr(md5($t), 0, 2)) * 360 >> 8; } function tag_links(array $tags, string $cls = 'tag'): string { return implode(' ', array_map(fn($t) => '' . h($t) . '', $tags)); } function lang_bar(array $langs, bool $text): string { if (!$langs) return ''; $bar = ''; if ($text) { $parts = []; foreach ($langs as $l => $p) if ($p >= 1) $parts[] = '' . h($l) . ' ' . round($p, 1) . '%'; $bar .= '' . implode(' ', $parts) . ''; } return $bar; } function cmd_box(string $cmd, string $label): string { return '' . h($cmd) . '';
}
function activity_svg(array $days, string $label): string {
$total = array_sum($days);
$start = strtotime('monday this week') - 52 * 7 * 86400;
$max = max(1, ...array_values($days ?: [0]));
$svg = '