Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
File.php
}
+ public function highlight( FileRenderer $renderer, string $content ): string {
+ $lang = match( $this->mediaType ) {
+ 'text/x-php', 'application/x-php', 'application/x-httpd-php' => 'php',
+ 'text/html' => 'html',
+ 'text/css' => 'css',
+ 'application/javascript', 'text/javascript', 'text/x-javascript' => 'javascript',
+ 'application/json', 'text/json', 'application/x-json' => 'json',
+ 'application/xml', 'text/xml', 'image/svg+xml' => 'xml',
+ 'text/x-shellscript', 'application/x-sh' => 'bash',
+ 'text/x-c', 'text/x-csrc' => 'c',
+ 'text/x-c++src', 'text/x-c++', 'text/x-cpp' => 'cpp',
+ 'text/x-java', 'text/x-java-source', 'application/java-archive' => 'java',
+ 'text/x-python', 'application/x-python-code' => 'python',
+ 'text/x-ruby', 'application/x-ruby' => 'ruby',
+ 'text/x-go', 'text/go' => 'go',
+ 'text/rust', 'text/x-rust' => 'rust',
+ 'text/x-lua', 'text/lua' => 'lua',
+ 'text/markdown', 'text/x-markdown' => 'markdown',
+ 'text/x-r', 'text/x-r-source', 'application/R' => 'r',
+ 'application/sql', 'text/sql', 'text/x-sql' => 'sql',
+ 'text/yaml', 'text/x-yaml', 'application/yaml' => 'yaml',
+ 'application/typescript', 'text/typescript' => 'typescript',
+ default => null
+ };
+
+ if ( $lang === null ) {
+ $ext = strtolower( pathinfo( $this->name, PATHINFO_EXTENSION ) );
+
+ $lang = match( $ext ) {
+ 'php', 'phtml', 'php8', 'php7' => 'php',
+ 'c', 'h' => 'c',
+ 'cpp', 'hpp', 'cc', 'cxx' => 'cpp',
+ 'java' => 'java',
+ 'js', 'jsx', 'mjs' => 'javascript',
+ 'ts', 'tsx' => 'typescript',
+ 'go' => 'go',
+ 'rs' => 'rust',
+ 'py', 'pyw' => 'python',
+ 'rb', 'erb' => 'ruby',
+ 'lua' => 'lua',
+ 'sh', 'bash', 'zsh' => 'bash',
+ 'md', 'markdown' => 'markdown',
+ 'rmd' => 'rmd',
+ 'r' => 'r',
+ 'xml', 'svg' => 'xml',
+ 'html', 'htm' => 'html',
+ 'css' => 'css',
+ 'json', 'lock' => 'json',
+ 'sql' => 'sql',
+ 'yaml', 'yml' => 'yaml',
+ default => 'text'
+ };
+ }
+
+ return $renderer->highlight( $content, $lang );
+ }
+
public function isImage(): bool {
return $this->category === self::CAT_IMAGE;
private function detectMediaType( string $buffer ): string {
+ if ( $buffer === '' ) return 'application/x-empty';
+
$finfo = new finfo( FILEINFO_MIME_TYPE );
$mediaType = $finfo->buffer( $buffer );
pages/FilePage.php
echo '<div class="blob-content"><pre class="blob-code">' .
- htmlspecialchars( $content ) . '</pre></div>';
+ $file->highlight( $renderer, $content ) . '</pre></div>';
}
} else {
render/FileRenderer.php
public function renderSize( int $bytes ): void;
-}
-
-class HtmlFileRenderer implements FileRenderer {
- private string $repoSafeName;
- private string $currentPath;
-
- public function __construct( string $repoSafeName, string $currentPath = '' ) {
- $this->repoSafeName = $repoSafeName;
- $this->currentPath = trim( $currentPath, '/' );
- }
-
- public function renderListEntry(
- string $name,
- string $sha,
- string $mode,
- string $iconClass,
- int $timestamp,
- int $size
- ): void {
- $fullPath = ($this->currentPath === '' ? '' : $this->currentPath . '/') .
- $name;
-
- $url = '?repo=' . urlencode( $this->repoSafeName ) .
- '&hash=' . $sha .
- '&name=' . urlencode( $fullPath );
-
- echo '<a href="' . $url . '" class="file-item">';
- echo '<span class="file-mode">' . $mode . '</span>';
- echo '<span class="file-name">';
- echo '<i class="fas ' . $iconClass . ' file-icon-container"></i>';
- echo htmlspecialchars( $name );
- echo '</span>';
-
- if( $size > 0 ) {
- echo '<span class="file-size">' . $this->formatSize( $size ) . '</span>';
- }
-
- if( $timestamp > 0 ) {
- echo '<span class="file-date">';
- $this->renderTime( $timestamp );
- echo '</span>';
- }
-
- echo '</a>';
- }
-
- public function renderMedia(
- File $file,
- string $url,
- string $mediaType
- ): bool {
- $rendered = false;
-
- if( $file->isImage() ) {
- echo '<div class="blob-content blob-content-image">' .
- '<img src="' . $url . '"></div>';
- $rendered = true;
- } elseif( $file->isVideo() ) {
- echo '<div class="blob-content blob-content-video">' .
- '<video controls><source src="' . $url . '" type="' .
- $mediaType . '"></video></div>';
- $rendered = true;
- } elseif( $file->isAudio() ) {
- echo '<div class="blob-content blob-content-audio">' .
- '<audio controls><source src="' . $url . '" type="' .
- $mediaType . '"></audio></div>';
- $rendered = true;
- }
-
- return $rendered;
- }
-
- public function renderSize( int $bytes ): void {
- echo $this->formatSize( $bytes );
- }
-
- private function renderTime( int $timestamp ): void {
- $tokens = [
- 31536000 => 'year',
- 2592000 => 'month',
- 604800 => 'week',
- 86400 => 'day',
- 3600 => 'hour',
- 60 => 'minute',
- 1 => 'second'
- ];
-
- $diff = $timestamp ? time() - $timestamp : null;
- $result = 'never';
-
- if( $diff && $diff >= 5 ) {
- foreach( $tokens as $unit => $text ) {
- if( $diff < $unit ) {
- continue;
- }
-
- $num = floor( $diff / $unit );
- $result = $num . ' ' . $text . ($num > 1 ? 's' : '') . ' ago';
- break;
- }
- } elseif( $diff ) {
- $result = 'just now';
- }
-
- echo $result;
- }
-
- private function formatSize( int $bytes ): string {
- $units = [ 'B', 'KB', 'MB', 'GB', 'TB' ];
- $i = 0;
-
- while( $bytes >= 1024 && $i < count( $units ) - 1 ) {
- $bytes /= 1024;
- $i++;
- }
- return ($bytes === 0 ? 0 : round( $bytes )) . ' ' . $units[$i];
- }
+ public function highlight( string $content, string $language ): string;
}
render/Highlighter.php
+<?php
+require_once __DIR__ . '/LanguageDefinitions.php';
+
+class Highlighter {
+ private string $content;
+ private string $lang;
+ private array $rules;
+
+ public function __construct(string $content, string $lang) {
+ $this->content = $content;
+ $this->lang = strtolower($lang);
+ $this->rules = LanguageDefinitions::get($this->lang);
+ }
+
+ public function render(): string {
+ if (empty($this->rules)) {
+ return htmlspecialchars($this->content);
+ }
+
+ $patterns = [];
+ foreach ($this->rules as $name => $pattern) {
+ $delim = $pattern[0];
+ $inner = substr($pattern, 1, strrpos($pattern, $delim) - 1);
+ $patterns[] = "(?P<$name>$inner)";
+ }
+
+ $combined = '/' . implode('|', $patterns) . '/msu';
+
+ return preg_replace_callback($combined, function ($matches) {
+ foreach ($matches as $key => $value) {
+ if (!is_numeric($key) && $value !== '') {
+ if ($key === 'string_interp') {
+ return $this->renderInterpolatedString($value);
+ }
+
+ return '<span class="hl-' . $key . '">' . htmlspecialchars($value) . '</span>';
+ }
+ }
+
+ return htmlspecialchars($matches[0]);
+ }, $this->content);
+ }
+
+ private function renderInterpolatedString(string $content): string {
+ $pattern = '/(\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*|\$\{[^}]+\})/';
+ $parts = preg_split($pattern, $content, -1, PREG_SPLIT_DELIM_CAPTURE);
+ $output = '<span class="hl-string">';
+
+ foreach ($parts as $part) {
+ if (str_starts_with($part, '$')) {
+ $output .= '<span class="hl-variable">' . htmlspecialchars($part) . '</span>';
+ } else {
+ $output .= htmlspecialchars($part);
+ }
+ }
+
+ $output .= '</span>';
+
+ return $output;
+ }
+}
render/HtmlFileRenderer.php
+<?php
+require_once __DIR__ . '/Highlighter.php';
+require_once __DIR__ . '/FileRenderer.php';
+
+class HtmlFileRenderer implements FileRenderer {
+ private string $repoSafeName;
+ private string $currentPath;
+
+ public function __construct( string $repoSafeName, string $currentPath = '' ) {
+ $this->repoSafeName = $repoSafeName;
+ $this->currentPath = trim( $currentPath, '/' );
+ }
+
+ public function renderListEntry(
+ string $name,
+ string $sha,
+ string $mode,
+ string $iconClass,
+ int $timestamp,
+ int $size
+ ): void {
+ $fullPath = ($this->currentPath === '' ? '' : $this->currentPath . '/') .
+ $name;
+
+ $url = '?repo=' . urlencode( $this->repoSafeName ) .
+ '&hash=' . $sha .
+ '&name=' . urlencode( $fullPath );
+
+ echo '<a href="' . $url . '" class="file-item">';
+ echo '<span class="file-mode">' . $mode . '</span>';
+ echo '<span class="file-name">';
+ echo '<i class="fas ' . $iconClass . ' file-icon-container"></i>';
+ echo htmlspecialchars( $name );
+ echo '</span>';
+
+ if( $size > 0 ) {
+ echo '<span class="file-size">' . $this->formatSize( $size ) . '</span>';
+ }
+
+ if( $timestamp > 0 ) {
+ echo '<span class="file-date">';
+ $this->renderTime( $timestamp );
+ echo '</span>';
+ }
+
+ echo '</a>';
+ }
+
+ public function renderMedia(
+ File $file,
+ string $url,
+ string $mediaType
+ ): bool {
+ $rendered = false;
+
+ if( $file->isImage() ) {
+ echo '<div class="blob-content blob-content-image">' .
+ '<img src="' . $url . '"></div>';
+ $rendered = true;
+ } elseif( $file->isVideo() ) {
+ echo '<div class="blob-content blob-content-video">' .
+ '<video controls><source src="' . $url . '" type="' .
+ $mediaType . '"></video></div>';
+ $rendered = true;
+ } elseif( $file->isAudio() ) {
+ echo '<div class="blob-content blob-content-audio">' .
+ '<audio controls><source src="' . $url . '" type="' .
+ $mediaType . '"></audio></div>';
+ $rendered = true;
+ }
+
+ return $rendered;
+ }
+
+ public function renderSize( int $bytes ): void {
+ echo $this->formatSize( $bytes );
+ }
+
+ public function highlight( string $content, string $language ): string {
+ return (new Highlighter($content, $language))->render();
+ }
+
+ private function renderTime( int $timestamp ): void {
+ $tokens = [
+ 31536000 => 'year',
+ 2592000 => 'month',
+ 604800 => 'week',
+ 86400 => 'day',
+ 3600 => 'hour',
+ 60 => 'minute',
+ 1 => 'second'
+ ];
+
+ $diff = $timestamp ? time() - $timestamp : null;
+ $result = 'never';
+
+ if( $diff && $diff >= 5 ) {
+ foreach( $tokens as $unit => $text ) {
+ if( $diff < $unit ) {
+ continue;
+ }
+
+ $num = floor( $diff / $unit );
+ $result = $num . ' ' . $text . ($num > 1 ? 's' : '') . ' ago';
+ break;
+ }
+ } elseif( $diff ) {
+ $result = 'just now';
+ }
+
+ echo $result;
+ }
+
+ private function formatSize( int $bytes ): string {
+ $units = [ 'B', 'KB', 'MB', 'GB', 'TB' ];
+ $i = 0;
+
+ while( $bytes >= 1024 && $i < count( $units ) - 1 ) {
+ $bytes /= 1024;
+ $i++;
+ }
+
+ return ($bytes === 0 ? 0 : round( $bytes )) . ' ' . $units[$i];
+ }
+}
render/LanguageDefinitions.php
+<?php
+class LanguageDefinitions {
+ public static function get(string $lang): array {
+ $int = '(-?\b\d+(\.\d+)?\b)';
+ $str = '(".*?"|\'.*?\')';
+ $float = '(-?\d+(\.\d+)?([eE][+-]?\d+)?)';
+
+ $rules = [
+ 'php' => [
+ 'comment' => '/(\/\/.*$|#.*$|\/\*.*?\*\/)/m',
+ 'string_interp' => '/(".*?")/',
+ 'string' => '/(\'.*?\')/',
+ 'keyword' => '/\b(abstract|and|array|as|break|callable|case|catch|class|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|eval|exit|extends|final|finally|fn|for|foreach|function|global|goto|if|implements|include|include_once|instanceof|insteadof|interface|isset|list|match|namespace|new|or|print|private|protected|public|require|require_once|return|static|switch|throw|trait|try|unset|use|var|while|xor|yield)\b/',
+ 'variable' => '/(\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'bash' => [
+ 'comment' => '/(#.*$)/m',
+ 'string_interp' => '/(".*?")/',
+ 'string' => '/(\'.*?\')/',
+ 'keyword' => '/\b(if|then|else|elif|fi|case|esac|for|while|until|do|done|function|local|return|exit|break|continue|export|source|alias|eval|exec)\b/',
+ 'variable' => '/(\$[a-zA-Z_][a-zA-Z0-9_]*|\$\{[^}]+\})/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'c' => [
+ 'comment' => '/(\/\/.*$|\/\*.*?\*\/)/m',
+ 'string' => '/' . $str . '/',
+ 'keyword' => '/\b(auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|volatile|while)\b/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'cpp' => [
+ 'comment' => '/(\/\/.*$|\/\*.*?\*\/)/m',
+ 'string' => '/' . $str . '/',
+ 'keyword' => '/\b(alignas|alignof|and|and_eq|asm|auto|bitand|bitor|bool|break|case|catch|char|char16_t|char32_t|class|compl|const|constexpr|const_cast|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|false|float|for|friend|goto|if|inline|int|long|mutable|namespace|new|noexcept|not|not_eq|nullptr|operator|or|or_eq|private|protected|public|register|reinterpret_cast|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|true|try|typedef|typeid|typename|union|unsigned|using|virtual|void|volatile|wchar_t|while|xor|xor_eq)\b/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'java' => [
+ 'comment' => '/(\/\/.*$|\/\*.*?\*\/)/m',
+ 'string' => '/' . $str . '/',
+ 'keyword' => '/\b(abstract|assert|boolean|break|byte|case|catch|char|class|const|continue|default|do|double|else|enum|extends|final|finally|float|for|goto|if|implements|import|instanceof|int|interface|long|native|new|package|private|protected|public|return|short|static|strictfp|super|switch|synchronized|this|throw|throws|transient|try|void|volatile|while)\b/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'go' => [
+ 'comment' => '/(\/\/.*$|\/\*.*?\*\/)/m',
+ 'string' => '/(".*?"|`.*?`)/s',
+ 'keyword' => '/\b(break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go|goto|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'rust' => [
+ 'comment' => '/(\/\/.*$|\/\*.*?\*\/)/m',
+ 'string' => '/(".*?"|\'.*?\')/',
+ 'keyword' => '/\b(as|break|const|continue|crate|else|enum|extern|false|fn|for|if|impl|in|let|loop|match|mod|move|mut|pub|ref|return|self|Self|static|struct|super|trait|true|type|unsafe|use|where|while|async|await|dyn)\b/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'python' => [
+ 'comment' => '/(#.*$)/m',
+ 'string' => '/(\'\'\'.*?\'\'\'|""".*?"""|".*?"|\'.*?\')/s',
+ 'keyword' => '/\b(False|None|True|and|as|assert|async|await|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield)\b/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'ruby' => [
+ 'comment' => '/(#.*$)/m',
+ 'string_interp' => '/(".*?")/',
+ 'string' => '/(\'.*?\')/',
+ 'keyword' => '/\b(alias|and|begin|break|case|class|def|defined|do|else|elsif|end|ensure|false|for|if|in|module|next|nil|not|or|redo|rescue|retry|return|self|super|then|true|undef|unless|until|when|while|yield)\b/',
+ 'variable' => '/(@[a-zA-Z_]\w*|\$[a-zA-Z_]\w*)/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'lua' => [
+ 'comment' => '/(--.*$)/m',
+ 'string' => '/' . $str . '/',
+ 'keyword' => '/\b(and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'javascript' => [
+ 'comment' => '/(\/\/.*$|\/\*.*?\*\/)/m',
+ 'string' => '/(".*?"|\'.*?\'|`.*?`)/s',
+ 'keyword' => '/\b(async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|export|extends|false|finally|for|function|if|import|in|instanceof|new|null|return|super|switch|this|throw|true|try|typeof|var|void|while|with|yield|let|static|enum)\b/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'typescript' => [
+ 'comment' => '/(\/\/.*$|\/\*.*?\*\/)/m',
+ 'string' => '/(".*?"|\'.*?\'|`.*?`)/s',
+ 'keyword' => '/\b(any|as|boolean|break|case|catch|class|const|continue|debugger|declare|default|delete|do|else|enum|export|extends|false|finally|for|from|function|if|implements|import|in|instanceof|interface|let|module|namespace|new|null|number|of|package|private|protected|public|require|return|static|string|super|switch|this|throw|true|try|type|typeof|var|void|while|with|yield)\b/',
+ 'number' => '/' . $int . '/',
+ ],
+ 'xml' => [
+ 'comment' => '/()/s',
+ 'string' => '/' . $str . '/',
+ 'keyword' => '/(<\/?[a-zA-Z0-9:-]+)/',
+ 'variable' => '/([a-zA-Z0-9:-]+=)/',
+ ],
+ 'html' => [
+ 'comment' => '/()/s',
+ 'string' => '/' . $str . '/',
+ 'keyword' => '/(<\/?[a-zA-Z0-9:-]+)/',
+ 'variable' => '/([a-zA-Z0-9:-]+=)/',
+ ],
+ 'css' => [
+ 'comment' => '/(\/\*.*?\*\/)/s',
+ 'keyword' => '/(@[a-zA-Z-]+)/',
+ 'string' => '/' . $str . '/',
+ 'variable' => '/(\.|#)[a-zA-Z0-9_-]+/',
+ 'number' => '/(-?(\d*\.)?\d+(px|em|rem|%|vh|vw|s|ms|deg))/',
+ ],
+ 'json' => [
+ 'keyword' => '/(".*?")(?=\s*:)/',
+ 'string' => '/(".*?")/',
+ 'number' => '/\b(-?\d+(\.\d+)?([eE][+-]?\d+)?|true|false|null)\b/',
+ ],
+ 'sql' => [
+ 'comment' => '/(--.*$|\/\*.*?\*\/)/m',
+ 'string' => '/(\'.*?\')/',
+ 'keyword' => '/\b(SELECT|FROM|WHERE|INSERT|UPDATE|DELETE|JOIN|LEFT|RIGHT|INNER|OUTER|ON|GROUP|BY|ORDER|HAVING|LIMIT|OFFSET|CREATE|TABLE|DROP|ALTER|INDEX|KEY|PRIMARY|FOREIGN|CONSTRAINT|DEFAULT|NULL|NOT|AND|OR|IN|VALUES|SET|AS|DISTINCT|UNION|ALL|CASE|WHEN|THEN|ELSE|END)\b/i',
+ 'number' => '/' . $int . '/',
+ ],
+ 'yaml' => [
+ 'comment' => '/(#.*$)/m',
+ 'keyword' => '/^(\s*[a-zA-Z0-9_-]+:)/m',
+ 'string' => '/' . $str . '/',
+ 'number' => '/' . $float . '/',
+ ],
+ 'markdown' => [
+ 'comment' => '/()/s',
+ 'keyword' => '/^(#{1,6}\s+.*)$/m',
+ 'string' => '/(\*\*.*?\*\*|__.*?__|\*.*?\*|_.*?_)/',
+ 'variable' => '/(\[.*?\]\(.*?\))/',
+ 'number' => '/^(\s*[-*+]\s|\s*\d+\.\s)/m',
+ ],
+ 'rmd' => [
+ 'comment' => '/()/s',
+ 'keyword' => '/^(#{1,6}\s+.*)$/m',
+ 'variable' => '/(`{3}\{r.*?`{3})/s',
+ ],
+ 'r' => [
+ 'comment' => '/(#.*$)/m',
+ 'string' => '/' . $str . '/',
+ 'keyword' => '/\b(if|else|repeat|while|function|for|in|next|break|TRUE|FALSE|NULL|Inf|NaN|NA)\b/',
+ 'number' => '/' . $float . '/',
+ ]
+ ];
+
+ return $rules[strtolower($lang)] ?? [];
+ }
+}

Adds syntax highlighting

Author Dave Jarvis <email>
Date 2026-02-14 18:41:50 GMT-0800
Commit c67e733dce1606a04810d9f5a04393c1a9722e4d
Parent d23f15b
Delta 392 lines added, 118 lines removed, 274-line increase