🐸 Frogue(Fed)

~ololduck/Frogue

Raw

use actix_web::HttpResponse;
use maud::{Markup, Render, html};

use super::{
    Page,
    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()
}