| 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">📁</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">📄</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 |
|