119 lines · 3.9 KB
Raw Download
1
<?php
2
/**
3
 * Build and render a nested file/folder tree from the flat file + folder rows
4
 * stored for a repository. Shared by the repository view (inline expandable
5
 * folders) and the file view (left-hand sidebar).
6
 */
7
8
require_once __DIR__ . '/helpers.php';
9
10
/**
11
 * Turn the flat file/folder lists into a nested tree. Each node is:
12
 *   ['dirs'  => ['name' => <node>, ...],   // sub-folders, keyed by segment
13
 *    'files' => [['name'=>..,'path'=>..,'size'=>..], ...]]
14
 * $files are rows with a full-path 'filename'; $folders are rows with 'path'.
15
 */
16
function build_file_tree(array $files, array $folders): array
17
{
18
    $root = ['dirs' => [], 'files' => []];
19
20
    // Returns a *reference* to the node for $path, creating each level as needed.
21
    $ensure_dir = function & (string $path) use (&$root) {
22
        $node = &$root;
23
        foreach (explode('/', trim($path, '/')) as $seg) {
24
            if ($seg === '') {
25
                continue;
26
            }
27
            if (!isset($node['dirs'][$seg])) {
28
                $node['dirs'][$seg] = ['dirs' => [], 'files' => []];
29
            }
30
            $node = &$node['dirs'][$seg];
31
        }
32
        return $node;
33
    };
34
35
    // Folders first so empty folders still appear.
36
    foreach ($folders as $fo) {
37
        $ref = &$ensure_dir($fo['path']);
38
        unset($ref);
39
    }
40
41
    foreach ($files as $f) {
42
        $full = $f['filename'];
43
        $dir  = dirname($full);
44
        $dir  = ($dir === '.' || $dir === '') ? '' : $dir;
45
        $node = &$ensure_dir($dir);
46
        $node['files'][] = [
47
            'name' => basename($full),
48
            'path' => $full,
49
            'size' => (int) ($f['filesize'] ?? 0),
50
        ];
51
        unset($node);
52
    }
53
54
    return $root;
55
}
56
57
/**
58
 * Render a tree node as a nested <ul>. Folders are native <details> disclosures
59
 * whose summary links to the folder view; files link to the blob view.
60
 *
61
 * $opts: [
62
 *   'active' => string  // full path of the file to highlight (file view)
63
 *   'open'   => array   // set of dir paths (path => true) to render expanded
64
 * ]
65
 */
66
function render_tree_nodes(array $node, array $repo, string $prefix = '', array $opts = []): string
67
{
68
    $active = $opts['active'] ?? null;
69
    $open   = $opts['open'] ?? [];
70
    $slug   = $repo['slug'];
71
72
    ksort($node['dirs'], SORT_FLAG_CASE | SORT_NATURAL);
73
    usort($node['files'], fn($a, $b) => strnatcasecmp($a['name'], $b['name']));
74
75
    $out = '<ul class="tree">';
76
77
    foreach ($node['dirs'] as $name => $child) {
78
        $path     = $prefix === '' ? $name : $prefix . '/' . $name;
79
        $isOpen   = isset($open[$path]);
80
        $contents = render_tree_nodes($child, $repo, $path, $opts);
81
        $out .= '<li class="tree-dir">'
82
              . '<details' . ($isOpen ? ' open' : '') . '>'
83
              . '<summary><span class="tree-caret" aria-hidden="true"></span>'
84
              . '<span class="tree-icon">&#128193;</span> '
85
              . '<a href="' . e(tree_url($slug, $path)) . '">' . e($name) . '</a>'
86
              . '</summary>'
87
              . $contents
88
              . '</details></li>';
89
    }
90
91
    foreach ($node['files'] as $file) {
92
        $isActive = $active !== null && $file['path'] === $active;
93
        $out .= '<li class="tree-file' . ($isActive ? ' active' : '') . '">'
94
              . '<span class="tree-icon">&#128196;</span> '
95
              . '<a href="' . e(blob_url($slug, $file['path'])) . '">' . e($file['name']) . '</a>'
96
              . '</li>';
97
    }
98
99
    $out .= '</ul>';
100
    return $out;
101
}
102
103
/** Set of every ancestor directory path of a file, e.g. "a/b/c.txt" => a, a/b. */
104
function ancestor_dirs(string $path): array
105
{
106
    $open = [];
107
    $accum = [];
108
    $segments = explode('/', trim($path, '/'));
109
    array_pop($segments); // drop the filename itself
110
    foreach ($segments as $seg) {
111
        if ($seg === '') {
112
            continue;
113
        }
114
        $accum[] = $seg;
115
        $open[implode('/', $accum)] = true;
116
    }
117
    return $open;
118
}
119