'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) => '' . $m[1] . '', $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 = ''; } }; $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[2], $rawbase) . ""; } elseif (preg_match('/^\s*[-*+]\s+(.*)/', $l, $m)) { $flush(); if ($list !== 'ul') { $close_list(); $out .= '