| 420 | } |
| 421 | |
| 422 | function detectLanguage( |
| 423 | filePath: string, |
| 424 | firstLine: string | null, |
| 425 | ): string | null { |
| 426 | const base = basename(filePath) |
| 427 | const ext = extname(filePath).slice(1) |
| 428 | |
| 429 | // Filename-based lookup (handles Dockerfile, Makefile, CMakeLists.txt, etc.) |
| 430 | const stem = base.split('.')[0] ?? '' |
| 431 | const byName = FILENAME_LANGS[base] ?? FILENAME_LANGS[stem] |
| 432 | if (byName && hljs().getLanguage(byName)) return byName |
| 433 | if (ext) { |
| 434 | const lang = hljs().getLanguage(ext) |
| 435 | if (lang) return ext |
| 436 | } |
| 437 | // Shebang / first-line detection (strip UTF-8 BOM) |
| 438 | if (firstLine) { |
| 439 | const line = firstLine.startsWith('\ufeff') ? firstLine.slice(1) : firstLine |
| 440 | if (line.startsWith('#!')) { |
| 441 | if (line.includes('bash') || line.includes('/sh')) return 'bash' |
| 442 | if (line.includes('python')) return 'python' |
| 443 | if (line.includes('node')) return 'javascript' |
| 444 | if (line.includes('ruby')) return 'ruby' |
| 445 | if (line.includes('perl')) return 'perl' |
| 446 | } |
| 447 | if (line.startsWith('<?php')) return 'php' |
| 448 | if (line.startsWith('<?xml')) return 'xml' |
| 449 | } |
| 450 | return null |
| 451 | } |
| 452 | |
| 453 | function scopeColor( |
| 454 | scope: string | undefined, |