use actix_web::HttpResponse;
use maud::{Markup, Render, html};
use super::Page;
use super::components::{RepositoryChrome, RepositoryTab};
/// A directory entry as the tree browser shows it.
pub struct TreeEntry {
pub name: String,
/// Directories lead into themselves; files lead onto their own page.
pub is_directory: bool,
}
/// A repository's tree as the tree browser shows it.
pub struct TreeDetail {
pub slug: String,
pub visibility: &'static str,
/// The branch the repository opens on, for the browse tab.
pub default_branch: String,
/// The revision being browsed: a branch, a tag, or a commit hash.
pub reference: String,
/// The directory within it, empty at the root.
pub path: String,
/// Directories first, then files, each by name.
pub entries: Vec<TreeEntry>,
}
pub(super) struct TreeBody<'a> {
owner: &'a str,
detail: &'a TreeDetail,
viewer: Option<&'a str>,
}
impl Render for TreeBody<'_> {
fn render(&self) -> Markup {
let detail = self.detail;
// Every address on the page hangs off this: the root of the ref,
// from which each directory below it is one more segment.
let root = format!("/~{}/{}/tree/{}", self.owner, detail.slug, detail.reference);
let within = if detail.path.is_empty() {
String::new()
} else {
format!("/{}", detail.path)
};
html! {
(RepositoryChrome {
owner: self.owner,
slug: &detail.slug,
visibility: detail.visibility,
default_branch: &detail.default_branch,
current_tab: RepositoryTab::Browse,
is_owner: self.viewer == Some(self.owner),
})
(Breadcrumb {
root: &root,
reference: &detail.reference,
path: &detail.path,
})
@if detail.entries.is_empty() {
p class="empty" { "Nothing here yet." }
} @else {
ul class="tree" {
@for entry in &detail.entries {
// A directory leads into itself, a file onto its
// own page; the slash after a directory's name
// says which is which before anything is opened.
li {
a href=(format!("{root}{within}/{}", entry.name)) {
@if entry.is_directory {
(entry.name) "/"
} @else {
(entry.name)
}
}
}
}
}
}
}
}
}
/// The path to wherever the page stands: the ref first, then each step
/// down to it, the last of which is where the reader already is.
///
/// The tree page and a file's page both stand at a point within a ref and
/// say so the same way, so both carry this rather than a copy of it each.
pub(super) struct Breadcrumb<'a> {
/// The tree address of the ref itself.
pub(super) root: &'a str,
pub(super) reference: &'a str,
/// The path below the ref, empty at its root.
pub(super) path: &'a str,
}
impl Render for Breadcrumb<'_> {
fn render(&self) -> Markup {
// One link per step, so the breadcrumb leads back the way a reader
// came rather than only naming it.
let components: Vec<&str> = self
.path
.split('/')
.filter(|part| !part.is_empty())
.collect();
let mut walked = String::new();
let mut steps = Vec::with_capacity(components.len());
for component in &components {
if !walked.is_empty() {
walked.push('/');
}
walked.push_str(component);
steps.push(format!("{}/{}", self.root, walked));
}
html! {
p class="breadcrumb" {
a href=(self.root) { (self.reference) }
@for (index, component) in components.iter().enumerate() {
" / "
@if index + 1 == components.len() {
span class="current" { (component) }
} @else {
a href=(&steps[index]) { (component) }
}
}
}
}
}
}
/// The page browsing one directory of one ref of `owner`'s repository.
pub fn tree(viewer: Option<&str>, owner: &str, detail: &TreeDetail) -> HttpResponse {
Page {
title: format!("~{owner}/{}: {} - Frogue", detail.slug, detail.reference),
viewer: viewer.map(str::to_string),
extra_head: None,
body: TreeBody {
owner,
detail,
viewer,
},
}
.into_response()
}