Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/treetrek.git
A UrlBuilder.php
1
<?php
2
class UrlBuilder {
3
  private $repo;
4
  private $action;
5
  private $hash;
6
  private $name;
7
  private $switcher;
8
9
  public function withRepo( $repo ) {
10
    $this->repo = $repo;
11
    return $this;
12
  }
13
14
  public function withAction( $action ) {
15
    $this->action = $action;
16
    return $this;
17
  }
18
19
  public function withHash( $hash ) {
20
    $this->hash = $hash;
21
    return $this;
22
  }
23
24
  public function withName( $name ) {
25
    $this->name = $name;
26
    return $this;
27
  }
28
29
  public function withSwitcher( $jsValue ) {
30
    $this->switcher = $jsValue;
31
    return $this;
32
  }
33
34
  public function build() {
35
    if( $this->switcher ) {
36
      $url = "window.location.href='?repo=' + encodeURIComponent(" .
37
             $this->switcher . ")";
38
    } else {
39
      $params = [];
40
41
      if( $this->repo ) {
42
        $params['repo'] = $this->repo;
43
      }
44
45
      if( $this->action ) {
46
        $params['action'] = $this->action;
47
      }
48
49
      if( $this->hash ) {
50
        $params['hash'] = $this->hash;
51
      }
52
53
      if( $this->name ) {
54
        $params['name'] = $this->name;
55
      }
56
57
      $url = empty( $params ) ? '?' : '?' . http_build_query( $params );
58
    }
59
60
    return $url;
61
  }
62
}
1 63
M pages/BasePage.php
1 1
<?php
2
require_once __DIR__ . '/Page.php';
3 2
require_once __DIR__ . '/../File.php';
3
require_once __DIR__ . '/../UrlBuilder.php';
4
require_once __DIR__ . '/Page.php';
4 5
5 6
abstract class BasePage implements Page {
...
31 32
        <h1><?php echo Config::SITE_TITLE; ?></h1>
32 33
        <nav class="nav">
33
          <a href="?">Home</a>
34
          <a href="<?php echo (new UrlBuilder())->build(); ?>">Home</a>
34 35
          <?php if( $currentRepo ):
35
            $safeName = urlencode( $currentRepo['safe_name'] ); ?>
36
            <a href="?repo=<?php echo $safeName; ?>">Files</a>
37
            <a href="?action=commits&repo=<?php echo $safeName; ?>">Commits</a>
38
            <a href="?action=refs&repo=<?php echo $safeName; ?>">Branches</a>
39
            <a href="?action=tags&repo=<?php echo $safeName; ?>">Tags</a>
36
            $safeName = $currentRepo['safe_name'];
37
            $repoUrl = (new UrlBuilder())->withRepo( $safeName );
38
            ?>
39
            <a href="<?php echo $repoUrl->build(); ?>">Files</a>
40
            <a href="<?php echo (new UrlBuilder())->withRepo( $safeName )->withAction( 'commits' )->build(); ?>">Commits</a>
41
            <a href="<?php echo (new UrlBuilder())->withRepo( $safeName )->withAction( 'refs' )->build(); ?>">Branches</a>
42
            <a href="<?php echo (new UrlBuilder())->withRepo( $safeName )->withAction( 'tags' )->build(); ?>">Tags</a>
40 43
          <?php endif; ?>
41 44
42 45
          <?php if( $currentRepo ): ?>
43 46
          <div class="repo-selector">
44 47
            <label>Repository:</label>
45
            <select onchange="window.location.href='?repo=' + encodeURIComponent(this.value)">
48
            <select onchange="<?php echo (new UrlBuilder())->withSwitcher( 'this.value' )->build(); ?>">
46 49
              <?php foreach( $this->repositories as $r ): ?>
47 50
              <option value="<?php echo htmlspecialchars( $r['safe_name'] ); ?>"
...
72 75
    </html>
73 76
    <?php
77
  }
78
79
  protected function renderBreadcrumbs( $repo, $trail = [] ) {
80
    $home = (new UrlBuilder())->build();
81
    $repoUrl = (new UrlBuilder())->withRepo( $repo['safe_name'] )->build();
82
83
    $parts = array_merge(
84
      [
85
        '<a href="' . $home . '">Repositories</a>',
86
        '<a href="' . $repoUrl . '">' . htmlspecialchars( $repo['name'] ) . '</a>'
87
      ],
88
      $trail
89
    );
90
91
    echo '<div class="breadcrumb">' . implode( ' / ', $parts ) . '</div>';
74 92
  }
75 93
}
M pages/CommitsPage.php
1 1
<?php
2 2
require_once __DIR__ . '/BasePage.php';
3
require_once __DIR__ . '/../UrlBuilder.php';
3 4
4 5
class CommitsPage extends BasePage {
...
30 31
      }
31 32
32
      $this->renderBreadcrumbs();
33
      $this->renderBreadcrumbs( $this->currentRepo, ['Commits'] );
33 34
34 35
      echo '<h2>Commit History <span class="branch-badge">' .
35 36
           htmlspecialchars( $main['name'] ) . '</span></h2>';
36 37
      echo '<div class="commit-list">';
37 38
38 39
      $start = $this->hash ?: $main['hash'];
39
      $repoParam = '&repo=' . urlencode( $this->currentRepo['safe_name'] );
40 40
41
      $this->git->history( $start, 100, function( $commit ) use ( $repoParam ) {
41
      $this->git->history( $start, 100, function( $commit ) {
42 42
        $msg = htmlspecialchars( explode( "\n", $commit->message )[0] );
43
44
        $url = (new UrlBuilder())
45
          ->withRepo( $this->currentRepo['safe_name'] )
46
          ->withAction( 'commit' )
47
          ->withHash( $commit->sha )
48
          ->build();
43 49
44 50
        echo '<div class="commit-row">';
45
        echo '<a href="?action=commit&hash=' . $commit->sha . $repoParam .
46
             '" class="sha">' . substr( $commit->sha, 0, 7 ) . '</a>';
51
        echo '<a href="' . $url . '" class="sha">' . substr( $commit->sha, 0, 7 ) . '</a>';
47 52
        echo '<span class="message">' . $msg . '</span>';
48 53
        echo '<span class="meta">' . htmlspecialchars( $commit->author ) .
49 54
             ' &bull; ' . date( 'Y-m-d', $commit->date ) . '</span>';
50 55
        echo '</div>';
51 56
      } );
52 57
53 58
      echo '</div>';
54 59
    }, $this->currentRepo );
55
  }
56
57
  private function renderBreadcrumbs() {
58
    $repoUrl = '?repo=' . urlencode( $this->currentRepo['safe_name'] );
59
    $crumbs = [
60
      '<a href="?">Repositories</a>',
61
      '<a href="' . $repoUrl . '">' .
62
        htmlspecialchars( $this->currentRepo['name'] ) . '</a>',
63
      'Commits'
64
    ];
65
66
    echo '<div class="breadcrumb">' . implode( ' / ', $crumbs ) . '</div>';
67 60
  }
68 61
}
M pages/DiffPage.php
1 1
<?php
2 2
require_once __DIR__ . '/BasePage.php';
3
require_once __DIR__ . '/../UrlBuilder.php';
3 4
require_once __DIR__ . '/../git/GitDiff.php';
4 5
...
47 48
      $changes = $diffEngine->compare( $this->hash );
48 49
49
      $this->renderBreadcrumbs();
50
      $commitsUrl = (new UrlBuilder())
51
        ->withRepo( $this->currentRepo['safe_name'] )
52
        ->withAction( 'commits' )
53
        ->build();
54
55
      $this->renderBreadcrumbs( $this->currentRepo, [
56
        '<a href="' . $commitsUrl . '">Commits</a>',
57
        substr( $this->hash, 0, 7 )
58
      ]);
50 59
51 60
      $author = $headers['author'] ?? 'Unknown';
...
62 71
63 72
      if( isset( $headers['parent'] ) ) {
64
        $repoUrl = '&repo=' . urlencode( $this->currentRepo['safe_name'] );
73
        $url = (new UrlBuilder())
74
          ->withRepo( $this->currentRepo['safe_name'] )
75
          ->withAction( 'commit' )
76
          ->withHash( $headers['parent'] )
77
          ->build();
65 78
66 79
        echo '<div class="commit-info-row"><span class="commit-info-label">Parent</span>' .
67 80
             '<span class="commit-info-value">';
68
        echo '<a href="?action=commit&hash=' . $headers['parent'] . $repoUrl .
69
             '" class="parent-link">' . substr( $headers['parent'], 0, 7 ) . '</a>';
81
        echo '<a href="' . $url . '" class="parent-link">' .
82
             substr( $headers['parent'], 0, 7 ) . '</a>';
70 83
        echo '</span></div>';
71 84
      }
...
151 164
152 165
    echo '</div>';
153
  }
154
155
  private function renderBreadcrumbs() {
156
    $safeName = urlencode( $this->currentRepo['safe_name'] );
157
    $crumbs = [
158
      '<a href="?">Repositories</a>',
159
      '<a href="?repo=' . $safeName . '">' .
160
        htmlspecialchars( $this->currentRepo['name'] ) . '</a>',
161
      '<a href="?action=commits&repo=' . $safeName . '">Commits</a>',
162
      substr( $this->hash, 0, 7 )
163
    ];
164
165
    echo '<div class="breadcrumb">' . implode( ' / ', $crumbs ) . '</div>';
166 166
  }
167 167
}
M pages/FilePage.php
1 1
<?php
2 2
require_once __DIR__ . '/BasePage.php';
3
require_once __DIR__ . '/../UrlBuilder.php';
3 4
require_once __DIR__ . '/../render/HtmlFileRenderer.php';
4 5
...
50 51
    $path = $_GET['name'] ?? '';
51 52
52
    $this->renderBreadcrumbs( $targetHash, 'Tree' );
53
    $this->emitBreadcrumbs( $targetHash, 'Tree', $path );
53 54
54 55
    echo '<h2>' . htmlspecialchars( $this->currentRepo['name'] ) .
55 56
         ' <span class="branch-badge">' .
56 57
         htmlspecialchars( $main['name'] ) . '</span></h2>';
57 58
58 59
    usort( $entries, function( $a, $b ) {
59 60
      return $a->compare( $b );
60 61
    } );
61 62
62
    echo '<div class="file-list">';
63
    $renderer = new HtmlFileRenderer( $this->currentRepo['safe_name'], $path );
63
    echo '<table class="file-list-table">';
64
    echo '<thead>';
65
    echo '<tr>';
66
    echo '<th></th>';
67
    echo '<th>Name</th>';
68
    echo '<th class="file-mode-cell">Mode</th>';
69
    echo '<th class="file-size-cell">Size</th>';
70
    echo '</tr>';
71
    echo '</thead>';
72
    echo '<tbody>';
73
74
    $currentPath = $this->hash ? $path : '';
75
    $renderer = new HtmlFileRenderer(
76
      $this->currentRepo['safe_name'], $currentPath
77
    );
64 78
65 79
    foreach( $entries as $file ) {
66 80
      $file->renderListEntry( $renderer );
67 81
    }
68 82
69
    echo '</div>';
83
    echo '</tbody>';
84
    echo '</table>';
70 85
  }
71 86
72 87
  private function renderBlob( $targetHash ) {
73
    $repoParam = '&repo=' . urlencode( $this->currentRepo['safe_name'] );
74 88
    $filename = $_GET['name'] ?? '';
75 89
    $file = $this->git->readFile( $targetHash, $filename );
76 90
    $size = $this->git->getObjectSize( $targetHash );
77 91
    $renderer = new HtmlFileRenderer( $this->currentRepo['safe_name'] );
78 92
79
    $this->renderBreadcrumbs( $targetHash, 'File' );
93
    $this->emitBreadcrumbs( $targetHash, 'File', $filename );
80 94
81 95
    if( $size === 0 ) {
82 96
      $this->renderDownloadState( $targetHash, "This file is empty." );
83 97
    } else {
84
      $rawUrl = '?action=raw&hash=' . $targetHash . $repoParam .
85
                '&name=' . urlencode( $filename );
98
      $rawUrl = (new UrlBuilder())
99
        ->withRepo( $this->currentRepo['safe_name'] )
100
        ->withAction( 'raw' )
101
        ->withHash( $targetHash )
102
        ->withName( $filename )
103
        ->build();
86 104
87 105
      if( !$file->renderMedia( $renderer, $rawUrl ) ) {
...
120 138
  private function renderDownloadState( $hash, $reason ) {
121 139
    $filename = $_GET['name'] ?? '';
122
    $url = '?action=raw&hash=' . $hash . '&repo=' .
123
           urlencode( $this->currentRepo['safe_name'] ) .
124
           '&name=' . urlencode( $filename );
140
    $url = (new UrlBuilder())
141
        ->withRepo( $this->currentRepo['safe_name'] )
142
        ->withAction( 'raw' )
143
        ->withHash( $hash )
144
        ->withName( $filename )
145
        ->build();
125 146
126 147
    echo '<div class="empty-state download-state">';
127 148
    echo '<p>' . htmlspecialchars( $reason ) . '</p>';
128 149
    echo '<a href="' . $url . '" class="btn-download">Download</a>';
129 150
    echo '</div>';
130 151
  }
131 152
132
  private function renderBreadcrumbs( $hash, $type ) {
133
    $repoUrl = '?repo=' . urlencode( $this->currentRepo['safe_name'] );
134
    $path = $_GET['name'] ?? '';
135
    $crumbs = [
136
      '<a href="?">Repositories</a>',
137
      '<a href="' . $repoUrl . '">' .
138
        htmlspecialchars( $this->currentRepo['name'] ) . '</a>'
139
    ];
153
  private function emitBreadcrumbs( $hash, $type, $path ) {
154
    $trail = [];
140 155
141 156
    if( $path ) {
142 157
      $parts = explode( '/', trim( $path, '/' ) );
143 158
      $acc = '';
144 159
145 160
      foreach( $parts as $idx => $part ) {
146 161
        $acc .= ($idx === 0 ? '' : '/') . $part;
147 162
148 163
        if( $idx === count( $parts ) - 1 ) {
149
          $crumbs[] = htmlspecialchars( $part );
164
          $trail[] = htmlspecialchars( $part );
150 165
        } else {
151
          $crumbs[] = '<a href="' . $repoUrl . '&name=' . urlencode( $acc ) . '">' .
166
          $url = (new UrlBuilder())
167
            ->withRepo( $this->currentRepo['safe_name'] )
168
            ->withName( $acc )
169
            ->build();
170
171
          $trail[] = '<a href="' . $url . '">' .
152 172
                      htmlspecialchars( $part ) . '</a>';
153 173
        }
154 174
      }
155 175
    } elseif( $this->hash ) {
156
      $crumbs[] = $type . ' ' . substr( $hash, 0, 7 );
176
      $trail[] = $type . ' ' . substr( $hash, 0, 7 );
157 177
    }
158 178
159
    echo '<div class="breadcrumb">' . implode( ' / ', $crumbs ) . '</div>';
179
    $this->renderBreadcrumbs( $this->currentRepo, $trail );
160 180
  }
161 181
}
M pages/HomePage.php
1 1
<?php
2 2
require_once __DIR__ . '/BasePage.php';
3
require_once __DIR__ . '/../UrlBuilder.php';
3 4
4 5
class HomePage extends BasePage {
...
46 47
    } );
47 48
48
    echo '<a href="?repo=' . urlencode( $repo['safe_name'] ) . '" class="repo-card">';
49
    $url = (new UrlBuilder())->withRepo( $repo['safe_name'] )->build();
50
    echo '<a href="' . $url . '" class="repo-card">';
49 51
    echo '<h3>' . htmlspecialchars( $repo['name'] ) . '</h3>';
50 52
    echo '<p class="repo-meta">';
M pages/TagsPage.php
20 20
  public function render() {
21 21
    $this->renderLayout( function() {
22
      $this->renderBreadcrumbs();
22
      $this->renderBreadcrumbs( $this->currentRepo, ['Tags'] );
23 23
24 24
      echo '<h2>Tags</h2>';
...
59 59
      echo '</table>';
60 60
    }, $this->currentRepo );
61
  }
62
63
  private function renderBreadcrumbs() {
64
    $repoUrl = '?repo=' . urlencode( $this->currentRepo['safe_name'] );
65
    $crumbs = [
66
      '<a href="?">Repositories</a>',
67
      '<a href="' . $repoUrl . '">' .
68
        htmlspecialchars( $this->currentRepo['name'] ) . '</a>',
69
      'Tags'
70
    ];
71
72
    echo '<div class="breadcrumb">' . implode( ' / ', $crumbs ) . '</div>';
73 61
  }
74 62
}
M render/Highlighter.php
139 139
    $language = match( $basename ) {
140 140
      'Containerfile',
141
      'Dockerfile'    => 'containerfile',
142
      'Makefile'      => 'makefile',
143
      'Jenkinsfile'   => 'groovy',
144
      default         => null
141
      'Dockerfile'     => 'containerfile',
142
      'Makefile'       => 'makefile',
143
      'Jenkinsfile'    => 'groovy',
144
      default          => null
145 145
    };
146 146
...
182 182
        'ini', 'cfg', 'conf'           => 'ini',
183 183
        'toml'                         => 'toml',
184
        'dockerfile'                   => 'dockerfile',
185 184
        'mk', 'mak'                    => 'makefile',
186 185
        'diff', 'patch'                => 'diff',
M render/HtmlFileRenderer.php
2 2
require_once __DIR__ . '/FileRenderer.php';
3 3
require_once __DIR__ . '/Highlighter.php';
4
require_once __DIR__ . '/../UrlBuilder.php';
4 5
5 6
class HtmlFileRenderer implements FileRenderer {
...
23 24
      $name;
24 25
25
    $url = '?repo=' . urlencode( $this->repoSafeName ) .
26
           '&hash=' . $sha .
27
           '&name=' . urlencode( $fullPath );
26
    // 2. Refactor: Use UrlBuilder instead of manual string concatenation
27
    $url = (new UrlBuilder())
28
      ->withRepo( $this->repoSafeName )
29
      ->withHash( $sha )
30
      ->withName( $fullPath )
31
      ->build();
28 32
29
    echo '<a href="' . $url . '" class="file-item">';
30
    echo '<span class="file-mode">' . $mode . '</span>';
31
    echo '<span class="file-name">';
32
    echo '<i class="fas ' . $iconClass . ' file-icon-container"></i>';
33
    echo htmlspecialchars( $name );
34
    echo '</span>';
33
    echo '<tr>';
34
    echo '<td class="file-icon-cell">';
35
    echo '<i class="fas ' . $iconClass . '"></i>';
36
    echo '</td>';
37
    echo '<td class="file-name-cell">';
38
    echo '<a href="' . $url . '">' . htmlspecialchars( $name ) . '</a>';
39
    echo '</td>';
40
    echo '<td class="file-mode-cell">';
41
    echo $this->formatMode( $mode );
42
    echo '</td>';
43
    echo '<td class="file-size-cell">';
35 44
36 45
    if( $size > 0 ) {
37
      echo '<span class="file-size">' . $this->formatSize( $size ) . '</span>';
38
    }
39
40
    if( $timestamp > 0 ) {
41
      echo '<span class="file-date">';
42
      $this->renderTime( $timestamp );
43
      echo '</span>';
46
      echo $this->formatSize( $size );
44 47
    }
45 48
46
    echo '</a>';
49
    echo '</td>';
50
    echo '</tr>';
47 51
  }
48 52
...
126 130
127 131
    return ($bytes === 0 ? 0 : round( $bytes )) . ' ' . $units[$i];
132
  }
133
134
  private function formatMode( string $mode ): string {
135
    switch( $mode ) {
136
      case '100644': return 'w';
137
      case '100755': return 'x';
138
      case '040000': return 'd';
139
      case '120000': return 'l';
140
      case '160000': return 'm';
141
      default: return '?';
142
    }
128 143
  }
129 144
}
145
130 146
M render/LanguageDefinitions.php
29 29
        'string'        => '/(\'(?:\\\\.|[^\'\\\\])*\')/',
30 30
        'comment'       => '/(\/\/[^\r\n]*|#[^\r\n]*|\/\*.*?\*\/)/ms',
31
        '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|enum|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|readonly|require|require_once|return|static|switch|throw|trait|try|unset|use|var|while|xor|yield)\b/',
31
        'type'          => '/\b(?:array|bool|callable|float|int|iterable|mixed|never|object|string|void)\b/',
32
        'keyword'       => '/\b(?:abstract|and|as|break|case|catch|class|clone|const|continue|declare|default|die|do|echo|else|elseif|empty|enddeclare|endfor|endforeach|endif|endswitch|endwhile|enum|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|readonly|require|require_once|return|static|switch|throw|trait|try|unset|use|var|while|xor|yield)\b/',
32 33
        'function'      => '/\b([a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)\s*(?=\()/',
33 34
        'variable'      => '/(\$[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*)/',
...
58 59
        'include'      => '/(^\s*#include[^\r\n]*)/m',
59 60
        'preprocessor' => '/(^\s*#(?!include\b)[^\r\n]*)/m',
60
        'keyword'      => '/\b(?:auto|break|case|const|continue|default|do|else|enum|extern|for|goto|if|noreturn|register|return|signed|sizeof|static|struct|switch|typedef|union|unsigned|void|volatile|while)\b/',
61
        'type'         => '/\b(?:char|double|float|int|long|short|void)\b/',
61
        'type'         => '/\b(?:char|double|float|int|long|short|void|signed|unsigned)\b/',
62
        'keyword'      => '/\b(?:auto|break|case|const|continue|default|do|else|enum|extern|for|goto|if|noreturn|register|return|sizeof|static|struct|switch|typedef|union|volatile|while)\b/',
62 63
        'function'     => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
63 64
        'number'       => '/' . $int . '/',
64 65
      ],
65 66
      'cpp' => [
66 67
        'string'       => '/' . $str . '/',
67 68
        'comment'      => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
68 69
        'include'      => '/(^\s*#include[^\r\n]*)/m',
69 70
        'preprocessor' => '/(^\s*#(?!include\b)[^\r\n]*)/m',
71
        'type'         => '/\b(?:bool|char|char8_t|char16_t|char32_t|double|float|int|long|short|signed|unsigned|void|wchar_t)\b/',
70 72
        'keyword'      => '/\b(?:alignas|alignof|and|and_eq|asm|auto|bitand|bitor|break|case|catch|class|co_await|co_return|co_yield|compl|concept|const|consteval|constexpr|constinit|const_cast|continue|decltype|default|delete|do|dynamic_cast|else|enum|explicit|export|extern|for|friend|goto|if|inline|mutable|namespace|new|noexcept|noreturn|not|not_eq|nullptr|operator|or|or_eq|private|protected|public|register|reinterpret_cast|requires|return|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|union|using|virtual|volatile|while|xor|xor_eq)\b/',
71
        'type'         => '/\b(?:bool|char|char16_t|char32_t|double|float|int|long|short|signed|unsigned|void|wchar_t)\b/',
72 73
        'boolean'      => '/\b(?:true|false)\b/',
73 74
        'function'     => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
74 75
        'number'       => '/' . $int . '/',
75 76
      ],
76 77
      'java' => [
77 78
        'class'    => '/(@[a-zA-Z_][a-zA-Z0-9_]*)/',
78 79
        'string'   => '/' . $str . '/',
79 80
        'comment'  => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
80
        'keyword'  => '/\b(?:abstract|assert|break|case|catch|class|const|continue|default|do|else|enum|extends|final|finally|for|goto|if|implements|import|instanceof|interface|native|new|non-sealed|package|permits|private|protected|public|record|return|sealed|static|strictfp|super|switch|synchronized|this|throw|throws|transient|try|var|void|volatile|while|yield)\b/',
81 81
        'type'     => '/\b(?:boolean|byte|char|double|float|int|long|short|void)\b/',
82
        'keyword'  => '/\b(?:abstract|assert|break|case|catch|class|const|continue|default|do|else|enum|extends|final|finally|for|goto|if|implements|import|instanceof|interface|native|new|non-sealed|package|permits|private|protected|public|record|return|sealed|static|strictfp|super|switch|synchronized|this|throw|throws|transient|try|var|volatile|while|yield)\b/',
82 83
        'boolean'  => '/\b(?:true|false|null)\b/',
83 84
        'function' => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
84 85
        'number'   => '/' . $int . '/',
85 86
      ],
86 87
      'go' => [
87 88
        'string'   => '/("(?:\\\\.|[^"\\\\])*"|`.*?`)/s',
88 89
        'comment'  => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
90
        'type'     => '/\b(?:bool|byte|complex64|complex128|error|float32|float64|int|int8|int16|int32|int64|rune|string|uint|uint8|uint16|uint32|uint64|uintptr)\b/',
89 91
        '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/',
90 92
        'boolean'  => '/\b(?:true|false|nil|iota)\b/',
91 93
        'function' => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
92 94
        'number'   => '/' . $int . '/',
93 95
      ],
94 96
      'rust' => [
95 97
        'string'   => '/' . $str . '/',
96 98
        'comment'  => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
99
        'type'     => '/\b(?:bool|char|f32|f64|i8|i16|i32|i64|i128|isize|str|u8|u16|u32|u64|u128|usize)\b/',
97 100
        'keyword'  => '/\b(?:as|async|await|break|const|continue|crate|dyn|else|enum|extern|fn|for|if|impl|in|let|loop|match|mod|move|mut|pub|ref|return|self|Self|static|struct|super|trait|type|union|unsafe|use|where|while)\b/',
98 101
        'boolean'  => '/\b(?:true|false)\b/',
99 102
        'function' => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
100 103
        'number'   => '/' . $int . '/',
101 104
      ],
102 105
      'python' => [
103 106
        'string'   => '/(\'\'\'.*?\'\'\'|""".*?"""|"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\')/s',
104 107
        'comment'  => '/(#[^\r\n]*)/m',
108
        'type'     => '/\b(?:bool|bytearray|bytes|complex|dict|float|frozenset|int|list|memoryview|object|range|set|str|tuple)\b/',
105 109
        'keyword'  => '/\b(?: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/',
106 110
        'boolean'  => '/\b(?:False|None|True)\b/',
...
137 141
        'string'   => '/("(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|`(?:\\\\.|[^`\\\\])*`)/s',
138 142
        'comment'  => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
139
        'keyword'  => '/\b(?:abstract|any|as|break|case|catch|class|const|continue|debugger|declare|default|delete|do|else|enum|export|extends|finally|for|from|function|if|implements|import|in|instanceof|interface|is|let|module|namespace|new|of|package|private|protected|public|readonly|require|return|static|super|switch|this|throw|try|type|typeof|var|void|while|with|yield)\b/',
140
        'type'     => '/\b(?:boolean|number|string|void|any)\b/',
143
        'type'     => '/\b(?:boolean|number|string|void|any|never|unknown|object|symbol|bigint)\b/',
144
        'keyword'  => '/\b(?:abstract|as|break|case|catch|class|const|continue|debugger|declare|default|delete|do|else|enum|export|extends|finally|for|from|function|if|implements|import|in|instanceof|interface|is|let|module|namespace|new|of|package|private|protected|public|readonly|require|return|static|super|switch|this|throw|try|type|typeof|var|while|with|yield)\b/',
141 145
        'boolean'  => '/\b(?:true|false|null|undefined)\b/',
142 146
        'function' => '/\b([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?=\()/',
...
171 175
        'string'   => '/(\'.*?\')/',
172 176
        'comment'  => '/(--[^\r\n]*|\/\*.*?\*\/)/ms',
177
        'type'     => '/(?i)\b(?:BIGINT|BIT|BOOLEAN|CHAR|DATE|DATETIME|DECIMAL|DOUBLE|FLOAT|INT|INTEGER|MONEY|NUMERIC|REAL|SMALLINT|TEXT|TIME|TIMESTAMP|TINYINT|VARCHAR)\b/',
173 178
        'keyword'  => '/(?i)\b(ADD|ALTER|AND|AS|ASC|BEGIN|BETWEEN|BY|CASE|CHECK|COLUMN|COMMIT|CONSTRAINT|CREATE|DATABASE|DEFAULT|DELETE|DESC|DISTINCT|DROP|ELSE|END|EXISTS|FOREIGN|FROM|FULL|FUNCTION|GRANT|GROUP|HAVING|IF|IN|INDEX|INNER|INSERT|INTO|IS|JOIN|KEY|LEFT|LIKE|LIMIT|NOT|NULL|OFFSET|ON|OR|ORDER|OUTER|PRIMARY|PROCEDURE|REFERENCES|REVOKE|RIGHT|ROLLBACK|SCHEMA|SELECT|SET|TABLE|THEN|TRANSACTION|TRIGGER|TRUNCATE|UNION|UNIQUE|UPDATE|VALUES|VIEW|WHEN|WHERE)\b/',
174 179
        'boolean'  => '/(?i)\b(NULL|TRUE|FALSE)\b/',
...
236 241
        'comment'      => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
237 242
        'preprocessor' => '/(^\s*#[^\r\n]*)/m',
238
        'keyword'      => '/\b(?:abstract|as|base|bool|break|byte|case|catch|char|checked|class|const|continue|decimal|default|delegate|do|double|else|enum|event|explicit|extern|false|finally|fixed|float|for|foreach|goto|if|implicit|in|int|interface|internal|is|lock|long|namespace|new|null|object|operator|out|override|params|private|protected|public|readonly|ref|return|sbyte|sealed|short|sizeof|stackalloc|static|string|struct|switch|this|throw|true|try|typeof|uint|ulong|unchecked|unsafe|ushort|using|virtual|void|volatile|while)\b/',
243
        'type'         => '/\b(?:bool|byte|char|decimal|double|float|int|long|object|sbyte|short|string|uint|ulong|ushort|void)\b/',
244
        'keyword'      => '/\b(?:abstract|as|base|break|case|catch|checked|class|const|continue|default|delegate|do|else|enum|event|explicit|extern|false|finally|fixed|for|foreach|goto|if|implicit|in|interface|internal|is|lock|namespace|new|null|operator|out|override|params|private|protected|public|readonly|ref|return|sealed|sizeof|stackalloc|static|struct|switch|this|throw|true|try|typeof|unchecked|unsafe|using|virtual|volatile|while)\b/',
239 245
        'function'     => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
240 246
        'number'       => '/' . $int . '/',
241 247
      ],
242 248
      'kotlin' => [
243 249
        'string'   => '/("""[\s\S]*?"""|' . $str . ')/',
244 250
        'comment'  => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
251
        'type'     => '/\b(?:Boolean|Byte|Char|Double|Float|Int|Long|Short|String|Void|Unit|Any|Nothing)\b/',
245 252
        'keyword'  => '/\b(?:as|break|class|continue|do|else|false|for|fun|if|in|interface|is|null|object|package|return|super|this|throw|true|try|typealias|typeof|val|var|when|while)\b/',
246 253
        'function' => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
247 254
        'number'   => '/' . $int . '/',
248 255
      ],
249 256
      'scala' => [
250 257
        'string'   => '/("""[\s\S]*?"""|' . $str . ')/',
251 258
        'comment'  => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
259
        'type'     => '/\b(?:Boolean|Byte|Char|Double|Float|Int|Long|Short|String|Unit|Any|AnyRef|AnyVal|Nothing|Null|void)\b/',
252 260
        'keyword'  => '/\b(?:abstract|case|catch|class|def|do|else|extends|false|final|finally|for|forSome|if|implicit|import|lazy|match|new|null|object|override|package|private|protected|return|sealed|super|this|throw|trait|try|true|type|val|var|while|with|yield)\b/',
253 261
        'function' => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
254 262
        'number'   => '/' . $int . '/',
255 263
      ],
256 264
      'groovy' => [
257 265
        'string'        => '/(\'\'\'[\s\S]*?\'\'\'|""".*?"""|"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\'|\/[^\/]+\/)/',
258 266
        'comment'       => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
259
        'keyword'       => '/\b(?:def|as|assert|break|case|catch|class|const|continue|default|do|else|enum|extends|false|finally|for|goto|if|implements|import|in|instanceof|interface|new|null|package|return|super|switch|this|throw|throws|trait|true|try|var|void|while)\b/',
267
        'type'          => '/\b(?:boolean|byte|char|double|float|int|long|short|void)\b/',
268
        'keyword'       => '/\b(?:def|as|assert|break|case|catch|class|const|continue|default|do|else|enum|extends|false|finally|for|goto|if|implements|import|in|instanceof|interface|new|null|package|return|super|switch|this|throw|throws|trait|true|try|var|while)\b/',
260 269
        'function'      => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
261 270
        'number'        => '/' . $int . '/',
262 271
      ],
263 272
      'dart' => [
264 273
        'string'   => '/(r?\'\'\'[\s\S]*?\'\'\'|r?"""[\s\S]*?"""|"(?:\\\\.|[^"\\\\])*"|\'(?:\\\\.|[^\'\\\\])*\')/',
265 274
        'comment'  => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
266
        'keyword'  => '/\b(?:abstract|as|assert|async|await|break|case|catch|class|const|continue|default|do|else|enum|export|extends|extension|external|factory|false|final|finally|for|get|if|implements|import|in|interface|is|library|mixin|new|null|on|operator|part|rethrow|return|set|static|super|switch|sync|this|throw|true|try|typedef|var|void|while|with|yield)\b/',
275
        'type'     => '/\b(?:void|bool|int|double|num|dynamic)\b/',
276
        'keyword'  => '/\b(?:abstract|as|assert|async|await|break|case|catch|class|const|continue|default|do|else|enum|export|extends|extension|external|factory|false|final|finally|for|get|if|implements|import|in|interface|is|library|mixin|new|null|on|operator|part|rethrow|return|set|static|super|switch|sync|this|throw|true|try|typedef|var|while|with|yield)\b/',
267 277
        'function' => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
268 278
        'number'   => '/' . $int . '/',
269 279
      ],
270 280
      'swift' => [
271 281
        'string'   => '/("""[\s\S]*?"""|' . $str . ')/',
272 282
        'comment'  => '/(\/\/[^\r\n]*|\/\*.*?\*\/)/ms',
273
        'keyword'  => '/\b(?:associatedtype|class|deinit|enum|extension|fileprivate|func|import|init|inout|internal|let|open|operator|private|protocol|public|rethrows|static|struct|subscript|typealias|var|break|case|continue|default|defer|do|else|fallthrough|for|guard|if|in|repeat|return|switch|where|while|as|Any|catch|false|is|nil|super|self|Self|throw|throws|true|try)\b/',
283
        'type'     => '/\b(?:Int|Double|Float|Bool|String|Void|Character|Any|AnyObject)\b/',
284
        'keyword'  => '/\b(?:associatedtype|class|deinit|enum|extension|fileprivate|func|import|init|inout|internal|let|open|operator|private|protocol|public|rethrows|static|struct|subscript|typealias|var|break|case|continue|default|defer|do|else|fallthrough|for|guard|if|in|repeat|return|switch|where|while|as|catch|false|is|nil|super|self|Self|throw|throws|true|try)\b/',
274 285
        'function' => '/\b([a-zA-Z_][a-zA-Z0-9_]*)\s*(?=\()/',
275 286
        'number'   => '/' . $int . '/',
...
289 300
        'function' => '/\b([a-zA-Z_][a-zA-Z0-9_-]*)\s*(?=\()/',
290 301
        'number'   => '/' . $int . '/',
291
      ],
292
      'dockerfile' => [
293
        'comment' => '/(#[^\r\n]*)/',
294
        'string'  => '/' . $str . '/',
295
        'keyword' => '/(?i)^\s*(?:FROM|MAINTAINER|RUN|CMD|LABEL|EXPOSE|ENV|ADD|COPY|ENTRYPOINT|VOLUME|USER|WORKDIR|ARG|ONBUILD|STOPSIGNAL|HEALTHCHECK|SHELL)\b/m',
296 302
      ],
297 303
      'containerfile' => [
M repo.css
197 197
  border-radius: 6px;
198 198
  overflow: hidden;
199
}
200
201
.blob-header {
202
  background: #21262d;
203
  padding: 12px 16px;
204
  border-bottom: 1px solid #30363d;
205
  font-size: 0.875rem;
206
  color: #8b949e;
207
}
208
209
.blob-code {
210
  padding: 16px;
211
  overflow-x: auto;
212
  font-family: 'SFMono-Regular', Consolas, monospace;
213
  font-size: 0.875rem;
214
  line-height: 1.6;
215
  white-space: pre-wrap;
216
  overflow-wrap: break-word;
217
}
218
219
.refs-list {
220
  display: grid;
221
  gap: 10px;
222
}
223
224
.ref-item {
225
  background: #161b22;
226
  border: 1px solid #30363d;
227
  border-radius: 6px;
228
  padding: 12px 16px;
229
  display: flex;
230
  align-items: center;
231
  gap: 12px;
232
}
233
234
.ref-type {
235
  background: #238636;
236
  color: white;
237
  padding: 2px 8px;
238
  border-radius: 12px;
239
  font-size: 0.75rem;
240
  font-weight: 600;
241
  text-transform: uppercase;
242
}
243
244
.ref-type.tag {
245
  background: #8957e5;
246
}
247
248
.ref-name {
249
  font-weight: 600;
250
  color: #f0f6fc;
251
}
252
253
.empty-state {
254
  text-align: center;
255
  padding: 60px 20px;
256
  color: #8b949e;
257
}
258
259
.commit-details {
260
  background: #161b22;
261
  border: 1px solid #30363d;
262
  border-radius: 6px;
263
  padding: 20px;
264
  margin-bottom: 20px;
265
}
266
267
.commit-header {
268
  margin-bottom: 20px;
269
}
270
271
.commit-title {
272
  font-size: 1.25rem;
273
  color: #f0f6fc;
274
  margin-bottom: 10px;
275
}
276
277
.commit-info {
278
  display: grid;
279
  gap: 8px;
280
  font-size: 0.875rem;
281
}
282
283
.commit-info-row {
284
  display: flex;
285
  gap: 10px;
286
}
287
288
.commit-info-label {
289
  color: #8b949e;
290
  width: 80px;
291
  flex-shrink: 0;
292
}
293
294
.commit-info-value {
295
  color: #c9d1d9;
296
  font-family: monospace;
297
}
298
299
.parent-link {
300
  color: #58a6ff;
301
  text-decoration: none;
302
}
303
304
.parent-link:hover {
305
  text-decoration: underline;
306
}
307
308
.repo-grid {
309
  display: grid;
310
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
311
  gap: 16px;
312
  margin-top: 20px;
313
}
314
315
.repo-card {
316
  background: #161b22;
317
  border: 1px solid #30363d;
318
  border-radius: 8px;
319
  padding: 20px;
320
  text-decoration: none;
321
  color: inherit;
322
  transition: border-color 0.2s, transform 0.1s;
323
}
324
325
.repo-card:hover {
326
  border-color: #58a6ff;
327
  transform: translateY(-2px);
328
}
329
330
.repo-card h3 {
331
  color: #58a6ff;
332
  margin-bottom: 8px;
333
  font-size: 1.1rem;
334
}
335
336
.repo-card p {
337
  color: #8b949e;
338
  font-size: 0.875rem;
339
  margin: 0;
340
}
341
342
.current-repo {
343
  background: #21262d;
344
  border: 1px solid #58a6ff;
345
  padding: 8px 16px;
346
  border-radius: 6px;
347
  font-size: 0.875rem;
348
  color: #f0f6fc;
349
}
350
351
.current-repo strong {
352
  color: #58a6ff;
353
}
354
355
.branch-badge {
356
  background: #238636;
357
  color: white;
358
  padding: 2px 8px;
359
  border-radius: 12px;
360
  font-size: 0.75rem;
361
  font-weight: 600;
362
  margin-left: 10px;
363
}
364
365
.commit-row {
366
  display: flex;
367
  padding: 10px 0;
368
  border-bottom: 1px solid #30363d;
369
  gap: 15px;
370
  align-items: baseline;
371
}
372
373
.commit-row:last-child {
374
  border-bottom: none;
375
}
376
377
.commit-row .sha {
378
  font-family: monospace;
379
  color: #58a6ff;
380
  text-decoration: none;
381
}
382
383
.commit-row .message {
384
  flex: 1;
385
  font-weight: 500;
386
}
387
388
.commit-row .meta {
389
  font-size: 0.85em;
390
  color: #8b949e;
391
  white-space: nowrap;
392
}
393
394
.blob-content-image {
395
  text-align: center;
396
  padding: 20px;
397
  background: #0d1117;
398
}
399
400
.blob-content-image img {
401
  max-width: 100%;
402
  border: 1px solid #30363d;
403
}
404
405
.blob-content-video {
406
  text-align: center;
407
  padding: 20px;
408
  background: #000;
409
}
410
411
.blob-content-video video {
412
  max-width: 100%;
413
  max-height: 80vh;
414
}
415
416
.blob-content-audio {
417
  text-align: center;
418
  padding: 40px;
419
  background: #161b22;
420
}
421
422
.blob-content-audio audio {
423
  width: 100%;
424
  max-width: 600px;
425
}
426
427
.download-state {
428
  text-align: center;
429
  padding: 40px;
430
  border: 1px solid #30363d;
431
  border-radius: 6px;
432
  margin-top: 10px;
433
}
434
435
.download-state p {
436
  margin-bottom: 20px;
437
  color: #8b949e;
438
}
439
440
.btn-download {
441
  display: inline-block;
442
  padding: 6px 16px;
443
  background: #238636;
444
  color: white;
445
  text-decoration: none;
446
  border-radius: 6px;
447
  font-weight: 600;
448
}
449
450
.repo-info-banner {
451
  margin-top: 15px;
452
}
453
454
.file-icon-container {
455
  width: 20px;
456
  text-align: center;
457
  margin-right: 5px;
458
  color: #8b949e;
459
}
460
461
.file-size {
462
  color: #8b949e;
463
  font-size: 0.8em;
464
  margin-left: 10px;
465
}
466
467
.file-date {
468
  color: #8b949e;
469
  font-size: 0.8em;
470
  margin-left: auto;
471
}
472
473
.repo-card-time {
474
  margin-top: 8px;
475
  color: #58a6ff;
476
}
477
478
479
.diff-container {
480
  display: flex;
481
  flex-direction: column;
482
  gap: 20px;
483
}
484
485
.diff-file {
486
  background: #161b22;
487
  border: 1px solid #30363d;
488
  border-radius: 6px;
489
  overflow: hidden;
490
}
491
492
.diff-header {
493
  background: #21262d;
494
  padding: 10px 16px;
495
  border-bottom: 1px solid #30363d;
496
  display: flex;
497
  align-items: center;
498
  gap: 10px;
499
}
500
501
.diff-path {
502
  font-family: monospace;
503
  font-size: 0.9rem;
504
  color: #f0f6fc;
505
}
506
507
.diff-binary {
508
  padding: 20px;
509
  text-align: center;
510
  color: #8b949e;
511
  font-style: italic;
512
}
513
514
.diff-content {
515
  overflow-x: auto;
516
}
517
518
.diff-content table {
519
  width: 100%;
520
  border-collapse: collapse;
521
  font-family: 'SFMono-Regular', Consolas, monospace;
522
  font-size: 12px;
523
}
524
525
.diff-content td {
526
  padding: 2px 0;
527
  line-height: 20px;
528
}
529
530
.diff-num {
531
  width: 1%;
532
  min-width: 40px;
533
  text-align: right;
534
  padding-right: 10px;
535
  color: #6e7681;
536
  user-select: none;
537
  background: #0d1117;
538
  border-right: 1px solid #30363d;
539
}
540
541
.diff-num::before {
542
  content: attr(data-num);
543
}
544
545
.diff-code {
546
  padding-left: 10px;
547
  white-space: pre-wrap;
548
  word-break: break-all;
549
  color: #c9d1d9;
550
}
551
552
.diff-marker {
553
  display: inline-block;
554
  width: 15px;
555
  user-select: none;
556
  color: #8b949e;
557
}
558
559
/* Protanopia Safe Colors: Blue (Add) and Yellow (Del) */
560
.diff-add {
561
  background-color: rgba(2, 59, 149, 0.25);
562
}
563
.diff-add .diff-code {
564
  color: #79c0ff;
565
}
566
.diff-add .diff-marker {
567
  color: #79c0ff;
568
}
569
570
.diff-del {
571
  background-color: rgba(148, 99, 0, 0.25);
572
}
573
.diff-del .diff-code {
574
  color: #d29922;
575
}
576
.diff-del .diff-marker {
577
  color: #d29922;
578
}
579
580
.diff-gap {
581
  background: #0d1117;
582
  color: #484f58;
583
  text-align: center;
584
  font-size: 0.8em;
585
  height: 20px;
586
}
587
.diff-gap td {
588
  padding: 0;
589
  line-height: 20px;
590
  background: rgba(110, 118, 129, 0.1);
591
}
592
593
.status-add { color: #58a6ff; }
594
.status-del { color: #d29922; }
595
.status-mod { color: #a371f7; }
596
597
.tag-table {
598
  width: 100%;
599
  border-collapse: collapse;
600
  margin-top: 10px;
601
}
602
603
.tag-table th {
604
  text-align: left;
605
  padding: 10px 16px;
606
  border-bottom: 2px solid #30363d;
607
  color: #8b949e;
608
  font-size: 0.875rem;
609
  font-weight: 600;
610
  white-space: nowrap;
611
}
612
613
.tag-table td {
614
  padding: 12px 16px;
615
  border-bottom: 1px solid #21262d;
616
  vertical-align: top;
617
  color: #c9d1d9;
618
  font-size: 0.9rem;
619
}
620
621
.tag-table tr:hover td {
622
  background: #161b22;
623
}
624
625
.tag-table .tag-name {
626
  min-width: 140px;
627
  width: 20%;
628
}
629
630
.tag-table .tag-message {
631
  width: auto;
632
  white-space: normal;
633
  word-break: break-word;
634
  color: #c9d1d9;
635
  font-weight: 500;
636
}
637
638
.tag-table .tag-author,
639
.tag-table .tag-time,
640
.tag-table .tag-hash {
641
  width: 1%;
642
  white-space: nowrap;
643
}
644
645
.tag-table .tag-time {
646
  text-align: right;
647
  color: #8b949e;
648
}
649
650
.tag-table .tag-hash {
651
  text-align: right;
652
}
653
654
.tag-table .tag-name a {
655
  color: #58a6ff;
656
  text-decoration: none;
657
  font-family: 'SFMono-Regular', Consolas, monospace;
658
}
659
660
.tag-table .tag-author {
661
  color: #c9d1d9;
662
}
663
664
.tag-table .tag-age-header {
665
  text-align: right;
666
}
667
668
.tag-table .tag-commit-header {
669
  text-align: right;
670
}
671
672
.tag-table .commit-hash {
673
  font-family: 'SFMono-Regular', Consolas, monospace;
674
  color: #58a6ff;
675
  text-decoration: none;
676
}
677
678
.tag-table .commit-hash:hover {
679
  text-decoration: underline;
680
}
681
682
.blob-code {
683
  font-family: 'SFMono-Regular', Consolas, monospace;
684
  background-color: #161b22;
685
  color: #fcfcfa;
686
  font-size: 0.875rem;
687
  line-height: 1.6;
688
  tab-size: 2;
689
}
690
691
.hl-comment,
692
.hl-doc-comment {
693
  color: #727072;
694
  font-style: italic;
695
}
696
697
.hl-function,
698
.hl-method {
699
  color: #78dce8;
700
}
701
702
.hl-tag {
703
  color: #3e8bff;
704
}
705
706
.hl-class,
707
.hl-type,
708
.hl-interface,
709
.hl-struct {
710
  color: #a9dc76;
711
}
712
713
.hl-keyword,
714
.hl-storage,
715
.hl-modifier,
716
.hl-statement {
717
  color: #ff6188;
718
  font-weight: 600;
719
}
720
721
.hl-string,
722
.hl-string_interp {
723
  color: #ffd866;
724
}
725
726
.hl-number,
727
.hl-boolean,
728
.hl-constant,
729
.hl-preprocessor {
730
  color: #ab9df2;
731
}
732
733
.hl-variable {
734
  color: #fcfcfa;
735
}
736
737
.hl-attribute,
738
.hl-property {
739
  color: #fc9867;
740
}
741
742
.hl-operator,
743
.hl-punctuation,
744
.hl-escape {
745
  color: #939293;
746
}
747
748
.hl-interp-punct {
749
  color: #ff6188;
750
}
751
752
.hl-math {
753
  color: #ab9df2;
754
  font-style: italic;
755
}
756
757
.hl-code {
758
  display: inline-block;
759
  width: 100%;
760
  background-color: #0d1117;
761
  color: #c9d1d9;
762
  padding: 2px 4px;
763
  border-radius: 3px;
199
  max-width: 100%;
200
}
201
202
.blob-header {
203
  background: #21262d;
204
  padding: 12px 16px;
205
  border-bottom: 1px solid #30363d;
206
  font-size: 0.875rem;
207
  color: #8b949e;
208
}
209
210
.blob-code {
211
  padding: 16px;
212
  overflow-x: auto;
213
  font-family: 'SFMono-Regular', Consolas, monospace;
214
  font-size: 0.875rem;
215
  line-height: 1.6;
216
  white-space: pre-wrap;
217
  overflow-wrap: break-word;
218
}
219
220
.blob-code pre {
221
    overflow-x: auto;
222
}
223
224
.refs-list {
225
  display: grid;
226
  gap: 10px;
227
}
228
229
.ref-item {
230
  background: #161b22;
231
  border: 1px solid #30363d;
232
  border-radius: 6px;
233
  padding: 12px 16px;
234
  display: flex;
235
  align-items: center;
236
  gap: 12px;
237
}
238
239
.ref-type {
240
  background: #238636;
241
  color: white;
242
  padding: 2px 8px;
243
  border-radius: 12px;
244
  font-size: 0.75rem;
245
  font-weight: 600;
246
  text-transform: uppercase;
247
}
248
249
.ref-type.tag {
250
  background: #8957e5;
251
}
252
253
.ref-name {
254
  font-weight: 600;
255
  color: #f0f6fc;
256
}
257
258
.empty-state {
259
  text-align: center;
260
  padding: 60px 20px;
261
  color: #8b949e;
262
}
263
264
.commit-details {
265
  background: #161b22;
266
  border: 1px solid #30363d;
267
  border-radius: 6px;
268
  padding: 20px;
269
  margin-bottom: 20px;
270
}
271
272
.commit-header {
273
  margin-bottom: 20px;
274
}
275
276
.commit-title {
277
  font-size: 1.25rem;
278
  color: #f0f6fc;
279
  margin-bottom: 10px;
280
}
281
282
.commit-info {
283
  display: grid;
284
  gap: 8px;
285
  font-size: 0.875rem;
286
}
287
288
.commit-info-row {
289
  display: flex;
290
  gap: 10px;
291
}
292
293
.commit-info-label {
294
  color: #8b949e;
295
  width: 80px;
296
  flex-shrink: 0;
297
}
298
299
.commit-info-value {
300
  color: #c9d1d9;
301
  font-family: monospace;
302
}
303
304
.parent-link {
305
  color: #58a6ff;
306
  text-decoration: none;
307
}
308
309
.parent-link:hover {
310
  text-decoration: underline;
311
}
312
313
.repo-grid {
314
  display: grid;
315
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
316
  gap: 16px;
317
  margin-top: 20px;
318
}
319
320
.repo-card {
321
  background: #161b22;
322
  border: 1px solid #30363d;
323
  border-radius: 8px;
324
  padding: 20px;
325
  text-decoration: none;
326
  color: inherit;
327
  transition: border-color 0.2s, transform 0.1s;
328
}
329
330
.repo-card:hover {
331
  border-color: #58a6ff;
332
  transform: translateY(-2px);
333
}
334
335
.repo-card h3 {
336
  color: #58a6ff;
337
  margin-bottom: 8px;
338
  font-size: 1.1rem;
339
}
340
341
.repo-card p {
342
  color: #8b949e;
343
  font-size: 0.875rem;
344
  margin: 0;
345
}
346
347
.current-repo {
348
  background: #21262d;
349
  border: 1px solid #58a6ff;
350
  padding: 8px 16px;
351
  border-radius: 6px;
352
  font-size: 0.875rem;
353
  color: #f0f6fc;
354
}
355
356
.current-repo strong {
357
  color: #58a6ff;
358
}
359
360
.branch-badge {
361
  background: #238636;
362
  color: white;
363
  padding: 2px 8px;
364
  border-radius: 12px;
365
  font-size: 0.75rem;
366
  font-weight: 600;
367
  margin-left: 10px;
368
}
369
370
.commit-row {
371
  display: flex;
372
  padding: 10px 0;
373
  border-bottom: 1px solid #30363d;
374
  gap: 15px;
375
  align-items: baseline;
376
}
377
378
.commit-row:last-child {
379
  border-bottom: none;
380
}
381
382
.commit-row .sha {
383
  font-family: monospace;
384
  color: #58a6ff;
385
  text-decoration: none;
386
}
387
388
.commit-row .message {
389
  flex: 1;
390
  font-weight: 500;
391
}
392
393
.commit-row .meta {
394
  font-size: 0.85em;
395
  color: #8b949e;
396
  white-space: nowrap;
397
}
398
399
.blob-content-image {
400
  text-align: center;
401
  padding: 20px;
402
  background: #0d1117;
403
}
404
405
.blob-content-image img {
406
  max-width: 100%;
407
  border: 1px solid #30363d;
408
}
409
410
.blob-content-video {
411
  text-align: center;
412
  padding: 20px;
413
  background: #000;
414
}
415
416
.blob-content-video video {
417
  max-width: 100%;
418
  max-height: 80vh;
419
}
420
421
.blob-content-audio {
422
  text-align: center;
423
  padding: 40px;
424
  background: #161b22;
425
}
426
427
.blob-content-audio audio {
428
  width: 100%;
429
  max-width: 600px;
430
}
431
432
.download-state {
433
  text-align: center;
434
  padding: 40px;
435
  border: 1px solid #30363d;
436
  border-radius: 6px;
437
  margin-top: 10px;
438
}
439
440
.download-state p {
441
  margin-bottom: 20px;
442
  color: #8b949e;
443
}
444
445
.btn-download {
446
  display: inline-block;
447
  padding: 6px 16px;
448
  background: #238636;
449
  color: white;
450
  text-decoration: none;
451
  border-radius: 6px;
452
  font-weight: 600;
453
}
454
455
.repo-info-banner {
456
  margin-top: 15px;
457
}
458
459
.file-icon-container {
460
  width: 20px;
461
  text-align: center;
462
  margin-right: 5px;
463
  color: #8b949e;
464
}
465
466
.file-size {
467
  color: #8b949e;
468
  font-size: 0.8em;
469
  margin-left: 10px;
470
}
471
472
.file-date {
473
  color: #8b949e;
474
  font-size: 0.8em;
475
  margin-left: auto;
476
}
477
478
.repo-card-time {
479
  margin-top: 8px;
480
  color: #58a6ff;
481
}
482
483
484
.diff-container {
485
  display: flex;
486
  flex-direction: column;
487
  gap: 20px;
488
}
489
490
.diff-file {
491
  background: #161b22;
492
  border: 1px solid #30363d;
493
  border-radius: 6px;
494
  overflow: hidden;
495
}
496
497
.diff-header {
498
  background: #21262d;
499
  padding: 10px 16px;
500
  border-bottom: 1px solid #30363d;
501
  display: flex;
502
  align-items: center;
503
  gap: 10px;
504
}
505
506
.diff-path {
507
  font-family: monospace;
508
  font-size: 0.9rem;
509
  color: #f0f6fc;
510
}
511
512
.diff-binary {
513
  padding: 20px;
514
  text-align: center;
515
  color: #8b949e;
516
  font-style: italic;
517
}
518
519
.diff-content {
520
  overflow-x: auto;
521
}
522
523
.diff-content table {
524
  width: 100%;
525
  border-collapse: collapse;
526
  font-family: 'SFMono-Regular', Consolas, monospace;
527
  font-size: 12px;
528
}
529
530
.diff-content td {
531
  padding: 2px 0;
532
  line-height: 20px;
533
}
534
535
.diff-num {
536
  width: 1%;
537
  min-width: 40px;
538
  text-align: right;
539
  padding-right: 10px;
540
  color: #6e7681;
541
  user-select: none;
542
  background: #0d1117;
543
  border-right: 1px solid #30363d;
544
}
545
546
.diff-num::before {
547
  content: attr(data-num);
548
}
549
550
.diff-code {
551
  padding-left: 10px;
552
  white-space: pre-wrap;
553
  word-break: break-all;
554
  color: #c9d1d9;
555
}
556
557
.diff-marker {
558
  display: inline-block;
559
  width: 15px;
560
  user-select: none;
561
  color: #8b949e;
562
}
563
564
/* Protanopia Safe Colors: Blue (Add) and Yellow (Del) */
565
.diff-add {
566
  background-color: rgba(2, 59, 149, 0.25);
567
}
568
.diff-add .diff-code {
569
  color: #79c0ff;
570
}
571
.diff-add .diff-marker {
572
  color: #79c0ff;
573
}
574
575
.diff-del {
576
  background-color: rgba(148, 99, 0, 0.25);
577
}
578
.diff-del .diff-code {
579
  color: #d29922;
580
}
581
.diff-del .diff-marker {
582
  color: #d29922;
583
}
584
585
.diff-gap {
586
  background: #0d1117;
587
  color: #484f58;
588
  text-align: center;
589
  font-size: 0.8em;
590
  height: 20px;
591
}
592
.diff-gap td {
593
  padding: 0;
594
  line-height: 20px;
595
  background: rgba(110, 118, 129, 0.1);
596
}
597
598
.status-add { color: #58a6ff; }
599
.status-del { color: #d29922; }
600
.status-mod { color: #a371f7; }
601
602
.tag-table, .file-list-table {
603
  width: 100%;
604
  border-collapse: collapse;
605
  margin-top: 10px;
606
  background: #161b22;
607
  border: 1px solid #30363d;
608
  border-radius: 6px;
609
  overflow: hidden;
610
}
611
612
.tag-table th, .file-list-table th {
613
  text-align: left;
614
  padding: 10px 16px;
615
  border-bottom: 2px solid #30363d;
616
  color: #8b949e;
617
  font-size: 0.875rem;
618
  font-weight: 600;
619
  white-space: nowrap;
620
}
621
622
.tag-table td, .file-list-table td {
623
  padding: 12px 16px;
624
  border-bottom: 1px solid #21262d;
625
  vertical-align: middle;
626
  color: #c9d1d9;
627
  font-size: 0.9rem;
628
}
629
630
.tag-table tr:hover td, .file-list-table tr:hover td {
631
  background: #161b22;
632
}
633
634
.tag-table .tag-name {
635
  min-width: 140px;
636
  width: 20%;
637
}
638
639
.tag-table .tag-message {
640
  width: auto;
641
  white-space: normal;
642
  word-break: break-word;
643
  color: #c9d1d9;
644
  font-weight: 500;
645
}
646
647
.tag-table .tag-author,
648
.tag-table .tag-time,
649
.tag-table .tag-hash {
650
  width: 1%;
651
  white-space: nowrap;
652
}
653
654
.tag-table .tag-time {
655
  text-align: right;
656
  color: #8b949e;
657
}
658
659
.tag-table .tag-hash {
660
  text-align: right;
661
}
662
663
.tag-table .tag-name a {
664
  color: #58a6ff;
665
  text-decoration: none;
666
  font-family: 'SFMono-Regular', Consolas, monospace;
667
}
668
669
.tag-table .tag-author {
670
  color: #c9d1d9;
671
}
672
673
.tag-table .tag-age-header {
674
  text-align: right;
675
}
676
677
.tag-table .tag-commit-header {
678
  text-align: right;
679
}
680
681
.tag-table .commit-hash {
682
  font-family: 'SFMono-Regular', Consolas, monospace;
683
  color: #58a6ff;
684
  text-decoration: none;
685
}
686
687
.tag-table .commit-hash:hover {
688
  text-decoration: underline;
689
}
690
691
.file-list-table .file-icon-cell {
692
    width: 20px;
693
    text-align: center;
694
    color: #8b949e;
695
    padding-right: 0;
696
}
697
698
.file-list-table .file-name-cell a {
699
    color: #58a6ff;
700
    text-decoration: none;
701
    font-weight: 500;
702
}
703
704
.file-list-table .file-name-cell a:hover {
705
    text-decoration: underline;
706
}
707
708
.file-list-table .file-mode-cell {
709
    font-family: 'SFMono-Regular', Consolas, monospace;
710
    color: #8b949e;
711
    font-size: 0.8rem;
712
    width: 1%;
713
    white-space: nowrap;
714
    text-align: center;
715
}
716
717
.file-list-table .file-size-cell {
718
    color: #8b949e;
719
    text-align: right;
720
    width: 1%;
721
    white-space: nowrap;
722
    font-size: 0.85rem;
723
}
724
725
.file-list-table .file-date-cell {
726
    color: #8b949e;
727
    text-align: right;
728
    width: 150px;
729
    font-size: 0.85rem;
730
    white-space: nowrap;
731
}
732
733
734
.blob-code {
735
  font-family: 'SFMono-Regular', Consolas, monospace;
736
  background-color: #161b22;
737
  color: #fcfcfa;
738
  font-size: 0.875rem;
739
  line-height: 1.6;
740
  tab-size: 2;
741
}
742
743
.hl-comment,
744
.hl-doc-comment {
745
  color: #727072;
746
  font-style: italic;
747
}
748
749
.hl-function,
750
.hl-method {
751
  color: #78dce8;
752
}
753
754
.hl-tag {
755
  color: #3e8bff;
756
}
757
758
.hl-class,
759
.hl-interface,
760
.hl-struct {
761
  color: #a9dc76;
762
}
763
764
.hl-type {
765
  color: #a9dc76;
766
}
767
768
.hl-keyword,
769
.hl-storage,
770
.hl-modifier,
771
.hl-statement {
772
  color: #ff6188;
773
  font-weight: 600;
774
}
775
776
.hl-string,
777
.hl-string_interp {
778
  color: #ffd866;
779
}
780
781
.hl-number,
782
.hl-boolean,
783
.hl-constant,
784
.hl-preprocessor {
785
  color: #ab9df2;
786
}
787
788
.hl-variable {
789
  color: #fcfcfa;
790
}
791
792
.hl-attribute,
793
.hl-property {
794
  color: #fc9867;
795
}
796
797
.hl-operator,
798
.hl-punctuation,
799
.hl-escape {
800
  color: #939293;
801
}
802
803
.hl-interp-punct {
804
  color: #ff6188;
805
}
806
807
.hl-math {
808
  color: #ab9df2;
809
  font-style: italic;
810
}
811
812
.hl-code {
813
  display: inline-block;
814
  width: 100%;
815
  background-color: #0d1117;
816
  color: #c9d1d9;
817
  padding: 2px 4px;
818
  border-radius: 3px;
819
}
820
821
@media (max-width: 768px) {
822
  .container {
823
    padding: 10px;
824
  }
825
826
  h1 { font-size: 1.5rem; }
827
  h2 { font-size: 1.2rem; }
828
829
  .nav {
830
    flex-direction: column;
831
    align-items: flex-start;
832
    gap: 10px;
833
  }
834
835
  .repo-selector {
836
    margin-left: 0;
837
    width: 100%;
838
  }
839
840
  .repo-selector select {
841
    flex: 1;
842
  }
843
844
  .file-list-table th,
845
  .file-list-table td {
846
    padding: 8px 10px;
847
  }
848
849
  .file-list-table .file-mode-cell,
850
  .file-list-table .file-date-cell {
851
    display: none;
852
  }
853
854
  .commit-details {
855
    padding: 15px;
856
  }
857
858
  .commit-title {
859
    font-size: 1.1rem;
860
    word-break: break-word;
861
  }
862
863
  .commit-info-row {
864
    flex-direction: column;
865
    gap: 2px;
866
    margin-bottom: 10px;
867
  }
868
869
  .commit-info-label {
870
    width: 100%;
871
    font-size: 0.8rem;
872
    color: #8b949e;
873
  }
874
875
  .commit-info-value {
876
    word-break: break-all;
877
    font-family: 'SFMono-Regular', Consolas, monospace;
878
    font-size: 0.9rem;
879
    padding-left: 0;
880
  }
881
882
  .commit-row {
883
    flex-direction: column;
884
    gap: 5px;
885
  }
886
887
  .commit-row .message {
888
    width: 100%;
889
    white-space: normal;
890
  }
891
892
  .commit-row .meta {
893
    font-size: 0.8rem;
894
  }
895
896
  .tag-table .tag-author,
897
  .tag-table .tag-time,
898
  .tag-table .tag-hash {
899
    font-size: 0.8rem;
900
  }
901
902
  .blob-code, .diff-content {
903
    overflow-x: scroll;
904
    -webkit-overflow-scrolling: touch;
905
  }
906
}
907
908
@media screen and (orientation: landscape) and (max-height: 600px) {
909
  .container {
910
    max-width: 100%;
911
  }
912
913
  header {
914
    margin-bottom: 15px;
915
    padding-bottom: 10px;
916
  }
917
918
  .file-list-table .file-date-cell {
919
    display: table-cell;
920
  }
764 921
}
765 922