'stl', '3mf' => '3mf', 'obj' => 'obj', 'ply' => 'ply', 'gcode' => 'gcode', 'g' => 'gcode', 'gco' => 'gcode', ]; return $map[$ext] ?? ''; } /** * True if the model format is text-based (so a "Source" toggle can show it), * as opposed to a binary container we can only render. */ function is_text_model_format(string $format): bool { return in_array($format, ['obj', 'ply', 'gcode'], true); } /** Best-effort MIME type from a filename extension, for serving raw files. */ function mime_for_filename(string $filename): string { $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $map = [ 'png' => 'image/png', 'jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'gif' => 'image/gif', 'svg' => 'image/svg+xml', 'webp' => 'image/webp', 'bmp' => 'image/bmp', 'ico' => 'image/x-icon', 'avif' => 'image/avif', 'pdf' => 'application/pdf', 'json' => 'application/json', 'css' => 'text/css', 'html' => 'text/html', 'htm' => 'text/html', 'js' => 'text/javascript', 'txt' => 'text/plain', 'md' => 'text/plain', 'xml' => 'application/xml', 'stl' => 'model/stl', '3mf' => 'model/3mf', 'obj' => 'text/plain', 'ply' => 'application/octet-stream', 'gcode' => 'text/plain', 'g' => 'text/plain', 'gco' => 'text/plain', ]; return $map[$ext] ?? 'text/plain'; } /** Human-readable file size. */ function human_size(int $bytes): string { $units = ['B', 'KB', 'MB', 'GB']; $i = 0; $n = (float) $bytes; while ($n >= 1024 && $i < count($units) - 1) { $n /= 1024; $i++; } return ($i === 0 ? $n : round($n, 1)) . ' ' . $units[$i]; } /** Guess a highlight language from a filename extension. */ function language_from_filename(string $filename): string { $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $map = [ 'php' => 'php', 'js' => 'javascript', 'mjs' => 'javascript', 'jsx' => 'javascript', 'ts' => 'javascript', 'tsx' => 'javascript', 'py' => 'python', 'java' => 'java', 'css' => 'css', 'scss' => 'css', 'less' => 'css', 'html' => 'html', 'htm' => 'html', 'xml' => 'html', 'svg' => 'html', 'vue' => 'html', 'json' => 'json', 'c' => 'c', 'h' => 'c', 'cpp' => 'cpp', 'cc' => 'cpp', 'cxx' => 'cpp', 'hpp' => 'cpp', 'go' => 'go', 'rb' => 'ruby', 'sql' => 'sql', 'sh' => 'bash', 'bash' => 'bash', 'zsh' => 'bash', 'md' => 'markdown', 'markdown' => 'markdown', ]; return $map[$ext] ?? 'plain'; } /** * Display language for a single file when counting a repository's languages, * or '' when the file should not count toward detection. Documentation * (.md/.txt), LICENSE/COPYING files and images are ignored; only files whose * extension maps to a known programming/markup language are counted. */ function code_language_label(string $filename): string { $base = strtolower(basename($filename)); // Ignore licence/readme/notice style files regardless of extension. if (preg_match('/^(license|licence|copying|notice|readme|changelog|authors|contributors)(\.|$)/', $base)) { return ''; } if (is_image_file($filename)) { return ''; } $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); $map = [ 'php' => 'PHP', 'phtml' => 'PHP', 'js' => 'JavaScript', 'mjs' => 'JavaScript', 'cjs' => 'JavaScript', 'jsx' => 'JavaScript', 'ts' => 'TypeScript', 'tsx' => 'TypeScript', 'py' => 'Python', 'java' => 'Java', 'css' => 'CSS', 'scss' => 'SCSS', 'sass' => 'Sass', 'less' => 'Less', 'html' => 'HTML', 'htm' => 'HTML', 'vue' => 'Vue', 'svelte' => 'Svelte', 'json' => 'JSON', 'c' => 'C', 'h' => 'C', 'cpp' => 'C++', 'cc' => 'C++', 'cxx' => 'C++', 'hpp' => 'C++', 'hh' => 'C++', 'cs' => 'C#', 'go' => 'Go', 'rb' => 'Ruby', 'rs' => 'Rust', 'swift'=> 'Swift', 'kt' => 'Kotlin', 'kts' => 'Kotlin', 'sql' => 'SQL', 'sh' => 'Shell', 'bash' => 'Shell', 'zsh' => 'Shell', 'xml' => 'XML', 'yml' => 'YAML', 'yaml' => 'YAML', 'toml' => 'TOML', ]; return $map[$ext] ?? ''; } /** * Count code files per display language in a list of file rows (or filenames), * ordered most-common first. Non-code files are skipped (see code_language_label). */ function language_breakdown(array $files): array { $counts = []; foreach ($files as $f) { $name = is_array($f) ? (string) ($f['filename'] ?? '') : (string) $f; $lang = code_language_label($name); if ($lang === '') { continue; } $counts[$lang] = ($counts[$lang] ?? 0) + 1; } arsort($counts); return $counts; } /** The dominant language of a file list, or '' if no code files are present. */ function detect_language(array $files): string { $counts = language_breakdown($files); return $counts ? (string) array_key_first($counts) : ''; } /** * The language to show for a repository: its manual override when set, * otherwise the language auto-detected from its files. Returns '' if neither * is available. */ function repo_language_display(array $repo): string { $manual = trim((string) ($repo['language'] ?? '')); if ($manual !== '') { return $manual; } if (!function_exists('get_files_by_repo') || empty($repo['id'])) { return ''; } return detect_language(get_files_by_repo((int) $repo['id'])); }