//! A repository's navigation, described: the identity line, the bar and the //! path trail. //! //! `templates/partials/git_nav.html`'s contents, said in the vocabulary rather //! than written as markup. Ten git templates include that partial, so this is //! the gate on the source browser: none of them can become a described screen //! while the bar they all carry is hand-written HTML. //! //! [`super::carousel`]'s shape, and for its reason: [`region`] is the //! description and [`html`] is the Askama half, so a page that converts later //! embeds the same node instead of re-saying it. Nothing here waits on the //! pages. //! //! # What the bar is, in the vocabulary //! //! Two members, and the split is the one the markup already made. //! //! The tab strip is a column-less [`Node::Table`], which is what a list is: //! rows of the same kind of thing, each //! going somewhere, one of them [`Row::current`]. That is the whole of what the //! six `` elements said, `is-selected` included, and `current` is the //! member for it -- the app's own pointer at where the reader is, which is //! exactly what `aria-current` exists to announce. Drawn as a row rather than a //! column by the stylesheet, which is where "how a set is laid out" has lived //! throughout. //! //! The ref chooser is a [`Field`] of [`layout::FieldKind::Select`]. It is one //! question with many answers, and a list of two hundred branches drawn as //! links would be the page. Its answer goes somewhere, so it //! [`writes`](Field::writes) a navigating read -- the same shape the site //! header's search box has. //! //! ## Why that write needs a route of its own //! //! A described write submits its value as a parameter, and the canonical //! address of a tree is a path segment (`/git/{owner}/{repo}/tree/{ref}`). Those //! are not the same address, and the path one is what every link on the site, //! every bookmark and every crawler already holds, so it is not the one to //! move. //! //! [`crate::routes::git::browsing::ref_chosen`] is the join: it takes the //! parameter and answers a redirect to the canonical path. One address for the //! content, one address for the control, and the reader lands on the first. use quasi_declare::declare; use quasi_router::Action; use quasi_router::screen::Choice; use crate::git::{Breadcrumb, RefInfo}; /// The bar's own region, and what a page names to replace it. pub const REGION: &str = "git-nav"; /// The address the ref chooser's answer goes to. See the module header. pub const CHOOSE_PATH: &str = "/git/{owner}/{repo}/ref"; /// The parameter the chooser submits under. pub const CHOSEN: &str = "ref"; /// What a page passes when it is about the repository rather than about a ref. /// /// See [`region`] for why those pages carry no chooser. pub const NO_REFS: &[RefInfo] = &[]; /// Everything the bar draws, gathered rather than passed as eight arguments. pub struct Nav<'a> { pub owner: &'a str, pub repo: &'a str, /// The branch or tag being browsed, which is the chooser's answer. pub current_ref: &'a str, /// Which tab is current, by the key the strip uses. pub active_tab: &'a str, pub open_issue_count: i64, /// Settings is the owner's, so a visitor is not offered it. pub is_owner: bool, pub refs: &'a [RefInfo], } /// One place this repository has, as the strip draws it. /// /// Named members rather than a tuple, for `policy`'s reason: a description /// names what it draws, and `.1` is not a name. Whether the reader is here is /// decided in [`Nav::tabs`] rather than at the row, because `commit` is one of /// the commits Commits lists and the reader is in the same place either way -- /// a fact about the set of tabs, not about one of them. struct Tab { /// What the tab is called, count included where there is one. label: String, /// Where it goes. route: String, /// Whether the reader is there. current: bool, } impl Nav<'_> { /// The repository's own prefix, which every address here is under. fn base(&self) -> String { format!("/git/{}/{}", self.owner, self.repo) } /// Where the chooser's answer goes. See the module header. fn ref_path(&self) -> String { format!("{}/ref", self.base()) } /// Whether there is anything to choose between. fn has_refs(&self) -> bool { !self.refs.is_empty() } /// The six places this repository has, in the order the strip draws them. fn tabs(&self) -> Vec { let base = self.base(); let current = |key: &str| self.active_tab == key || (key == "commits" && self.active_tab == "commit"); let mut tabs = vec![ Tab { label: "Files".to_owned(), route: format!("{base}/tree/{}", self.current_ref), current: current("files"), }, Tab { label: "Commits".to_owned(), route: format!("{base}/commits/{}", self.current_ref), current: current("commits"), }, Tab { label: "Tags".to_owned(), route: format!("{base}/tags"), current: current("tags"), }, Tab { label: "Notes".to_owned(), route: format!("{base}/notes"), current: current("notes"), }, Tab { // The count is part of the name a reader reads, and there is no // second thing to say: a tab reading `Issues` when there are // none and `Issues (3)` when there are three is one label. label: if self.open_issue_count > 0 { format!("Issues ({})", self.open_issue_count) } else { "Issues".to_owned() }, route: format!("{base}/issues"), current: current("issues"), }, ]; if self.is_owner { tabs.push(Tab { label: "Settings".to_owned(), route: format!("{base}/settings"), current: current("settings"), }); } tabs } } /// What a ref is called in the chooser. /// /// A tag reads as `tag: v1.2.0` and a branch as its bare name, which is the /// distinction the shipped `` elements said, /// `is-selected` included, and `current` is the member for it -- the app's /// own pointer at where the reader is, which is exactly what `aria-current` /// exists to announce. shape strip(nav: &Nav<'_>) -> Node; list { for tab in nav.tabs() { row tab.label { current tab.current; activate to get tab.route navigating; } } } } declare! { /// The line that says which repository this is. /// /// Two links and a separator in the shipped markup, and only the links are /// described: a `/` between an owner and a repository is punctuation the /// reader never presses, which is presentation and stays in the stylesheet. /// Named rather than headed, because a heading carries text and this /// carries two addresses. #[must_use] pub shape heading(owner: &str, repo: &str) -> Node; region "git-repo-name" as Group { named "{owner} / {repo}"; across Wrap { beside Essential link owner to get "/git/{owner}" navigating; beside Essential link repo to get "/git/{owner}/{repo}" navigating; } } } declare! { /// The path from the repository root down to what is being shown. /// /// The last crumb is the thing itself and goes nowhere, which is what /// `Breadcrumb::is_link` already says. The separators are the stylesheet's, /// for [`heading`]'s reason. #[must_use] pub shape breadcrumb( owner: &str, repo: &str, current_ref: &str, crumbs: &[Breadcrumb], ) -> Node; let tree = "/git/{owner}/{repo}/tree/{current_ref}"; region "git-breadcrumb" as Group { named "Path"; across Wrap { beside Essential link repo to get tree.clone() navigating; for crumb in crumbs.iter() { beside Essential given crumb.is_link { true -> link crumb.name.clone() to get "{tree}/{crumb.path}" navigating; otherwise -> text crumb.name.clone(); } } } } } #[cfg(test)] mod tests { use super::*; fn refs() -> Vec { vec![ RefInfo { name: "main".into(), is_branch: true, }, RefInfo { name: "v1.0.0".into(), is_branch: false, }, ] } fn nav<'a>(refs: &'a [RefInfo], active: &'a str, is_owner: bool, issues: i64) -> Nav<'a> { Nav { owner: "ada", repo: "engine", current_ref: "main", active_tab: active, open_issue_count: issues, is_owner, refs, } } /// The strip marks where the reader is, which is what `is-selected` said. #[test] fn the_current_tab_is_the_current_row() { let refs = refs(); let html = html(&nav(&refs, "tags", false, 0)); let tags = html .split("tag: v1.0.0"), "{html}" ); assert!( html.contains(""), "{html}" ); } /// The chooser's answer goes to the join route, which redirects to the /// canonical tree address. See the module header. #[test] fn choosing_a_ref_asks_the_join_route() { let refs = refs(); let html = html(&nav(&refs, "files", false, 0)); assert!(html.contains("/git/ada/engine/ref"), "{html}"); assert!(html.contains("name=\"ref\""), "{html}"); } /// A page that is about the repository rather than about a ref carries the /// strip and no chooser: there is nothing there for choosing one to change. #[test] fn a_page_with_no_refs_still_carries_the_strip() { let html = html(&nav(NO_REFS, "issues", true, 1)); assert!(!html.contains("alert(1)".into(), is_branch: true, }]; let html = html(&nav(&refs, "files", false, 0)); assert!(!html.contains("