diff --git a/src/guides.rs b/src/guides.rs index cdcce05..06af8dd 100644 --- a/src/guides.rs +++ b/src/guides.rs @@ -3,20 +3,34 @@ use std::collections::HashMap; use cot_site_common::md_pages::{MdPage, MdPageLink}; use cot_site_macros::md_page; -use crate::GuideLinkCategory; +use crate::{GuideCategoryItem, GuideItem, GuideLinkCategory}; -pub fn parse_guides(categories: Vec<(&'static str, Vec)>) -> ParsedPagesForVersion { +pub fn parse_guides(categories: Vec<(&'static str, Vec)>) -> ParsedPagesForVersion { let categories_links = categories .iter() - .map(|(title, guides)| GuideLinkCategory { + .map(|(title, items)| GuideLinkCategory { title, - guides: guides.iter().map(MdPageLink::from).collect(), + guides: items + .iter() + .map(|item| match item { + GuideItem::Page(page) => GuideCategoryItem::Page(MdPageLink::from(page)), + GuideItem::SubCategory { title, pages } => GuideCategoryItem::SubCategory { + title, + pages: pages.iter().map(MdPageLink::from).collect(), + }, + }) + .collect(), }) .collect(); + let guide_map = categories .into_iter() - .flat_map(|(_title, guides)| guides) - .map(|guide| (guide.link.clone(), guide)) + .flat_map(|(_title, items)| items) + .flat_map(|item| match item { + GuideItem::Page(page) => vec![page], + GuideItem::SubCategory { pages, .. } => pages, + }) + .map(|page| (page.link.clone(), page)) .collect(); ParsedPagesForVersion { @@ -40,39 +54,46 @@ pub fn get_prev_next_link<'a>( guides: &'a [GuideLinkCategory], current_id: &str, ) -> (Option<&'a MdPageLink>, Option<&'a MdPageLink>) { + let all_links: Vec<&MdPageLink> = guides + .iter() + .flat_map(|category| category.guides.iter()) + .flat_map(|item| match item { + GuideCategoryItem::Page(link) => vec![link], + GuideCategoryItem::SubCategory { pages, .. } => pages.iter().collect(), + }) + .collect(); + let mut prev = None; let mut has_found = false; - for category in guides { - for guide in &category.guides { - if has_found { - return (prev, Some(guide)); - } else if guide.link == current_id { - has_found = true; - } else { - prev = Some(guide); - } + for link in all_links { + if has_found { + return (prev, Some(link)); + } else if link.link == current_id { + has_found = true; + } else { + prev = Some(link); } } (prev, None) } -pub(crate) fn get_categories(master_version: Vec<(&'static str, Vec)>) -> ParsedPages { +pub fn get_categories(master_version: Vec<(&'static str, Vec)>) -> ParsedPages { let version_map = HashMap::from([ ( "v0.1", vec![( "Getting started", vec![ - md_page!("v0.1", "introduction"), - md_page!("v0.1", "templates"), - md_page!("v0.1", "forms"), - md_page!("v0.1", "db-models"), - md_page!("v0.1", "admin-panel"), - md_page!("v0.1", "static-files"), - md_page!("v0.1", "error-pages"), - md_page!("v0.1", "testing"), + GuideItem::Page(md_page!("v0.1", "introduction")), + GuideItem::Page(md_page!("v0.1", "templates")), + GuideItem::Page(md_page!("v0.1", "forms")), + GuideItem::Page(md_page!("v0.1", "db-models")), + GuideItem::Page(md_page!("v0.1", "admin-panel")), + GuideItem::Page(md_page!("v0.1", "static-files")), + GuideItem::Page(md_page!("v0.1", "error-pages")), + GuideItem::Page(md_page!("v0.1", "testing")), ], )], ), @@ -81,14 +102,14 @@ pub(crate) fn get_categories(master_version: Vec<(&'static str, Vec)>) - vec![( "Getting started", vec![ - md_page!("v0.2", "introduction"), - md_page!("v0.2", "templates"), - md_page!("v0.2", "forms"), - md_page!("v0.2", "db-models"), - md_page!("v0.2", "admin-panel"), - md_page!("v0.2", "static-files"), - md_page!("v0.2", "error-pages"), - md_page!("v0.2", "testing"), + GuideItem::Page(md_page!("v0.2", "introduction")), + GuideItem::Page(md_page!("v0.2", "templates")), + GuideItem::Page(md_page!("v0.2", "forms")), + GuideItem::Page(md_page!("v0.2", "db-models")), + GuideItem::Page(md_page!("v0.2", "admin-panel")), + GuideItem::Page(md_page!("v0.2", "static-files")), + GuideItem::Page(md_page!("v0.2", "error-pages")), + GuideItem::Page(md_page!("v0.2", "testing")), ], )], ), @@ -97,15 +118,15 @@ pub(crate) fn get_categories(master_version: Vec<(&'static str, Vec)>) - vec![( "Getting started", vec![ - md_page!("v0.3", "introduction"), - md_page!("v0.3", "templates"), - md_page!("v0.3", "forms"), - md_page!("v0.3", "db-models"), - md_page!("v0.3", "admin-panel"), - md_page!("v0.3", "static-files"), - md_page!("v0.3", "error-pages"), - md_page!("v0.3", "openapi"), - md_page!("v0.3", "testing"), + GuideItem::Page(md_page!("v0.3", "introduction")), + GuideItem::Page(md_page!("v0.3", "templates")), + GuideItem::Page(md_page!("v0.3", "forms")), + GuideItem::Page(md_page!("v0.3", "db-models")), + GuideItem::Page(md_page!("v0.3", "admin-panel")), + GuideItem::Page(md_page!("v0.3", "static-files")), + GuideItem::Page(md_page!("v0.3", "error-pages")), + GuideItem::Page(md_page!("v0.3", "openapi")), + GuideItem::Page(md_page!("v0.3", "testing")), ], )], ), @@ -115,18 +136,21 @@ pub(crate) fn get_categories(master_version: Vec<(&'static str, Vec)>) - ( "Getting started", vec![ - md_page!("v0.4", "introduction"), - md_page!("v0.4", "templates"), - md_page!("v0.4", "forms"), - md_page!("v0.4", "db-models"), - md_page!("v0.4", "admin-panel"), - md_page!("v0.4", "static-files"), - md_page!("v0.4", "error-pages"), - md_page!("v0.4", "openapi"), - md_page!("v0.4", "testing"), + GuideItem::Page(md_page!("v0.4", "introduction")), + GuideItem::Page(md_page!("v0.4", "templates")), + GuideItem::Page(md_page!("v0.4", "forms")), + GuideItem::Page(md_page!("v0.4", "db-models")), + GuideItem::Page(md_page!("v0.4", "admin-panel")), + GuideItem::Page(md_page!("v0.4", "static-files")), + GuideItem::Page(md_page!("v0.4", "error-pages")), + GuideItem::Page(md_page!("v0.4", "openapi")), + GuideItem::Page(md_page!("v0.4", "testing")), ], ), - ("Upgrading", vec![md_page!("v0.4", "upgrade-guide")]), + ( + "Upgrading", + vec![GuideItem::Page(md_page!("v0.4", "upgrade-guide"))], + ), ], ), ( @@ -135,44 +159,27 @@ pub(crate) fn get_categories(master_version: Vec<(&'static str, Vec)>) - ( "Getting started", vec![ - md_page!("v0.5", "introduction"), - md_page!("v0.5", "templates"), - md_page!("v0.5", "forms"), - md_page!("v0.5", "db-models"), - md_page!("v0.5", "admin-panel"), - md_page!("v0.5", "static-files"), - md_page!("v0.5", "sending-emails"), - md_page!("v0.5", "caching"), - md_page!("v0.5", "error-pages"), - md_page!("v0.5", "openapi"), - md_page!("v0.5", "testing"), + GuideItem::Page(md_page!("v0.5", "introduction")), + GuideItem::Page(md_page!("v0.5", "templates")), + GuideItem::Page(md_page!("v0.5", "forms")), + GuideItem::Page(md_page!("v0.5", "db-models")), + GuideItem::Page(md_page!("v0.5", "admin-panel")), + GuideItem::Page(md_page!("v0.5", "static-files")), + GuideItem::Page(md_page!("v0.5", "sending-emails")), + GuideItem::Page(md_page!("v0.5", "caching")), + GuideItem::Page(md_page!("v0.5", "error-pages")), + GuideItem::Page(md_page!("v0.5", "openapi")), + GuideItem::Page(md_page!("v0.5", "testing")), ], ), - ("Upgrading", vec![md_page!("v0.5", "upgrade-guide")]), - ("About", vec![md_page!("v0.5", "framework-comparison")]), - ], - ), - ( - "v0.6", - vec![ ( - "Getting started", - vec![ - md_page!("v0.6", "introduction"), - md_page!("v0.6", "templates"), - md_page!("v0.6", "forms"), - md_page!("v0.6", "db-models"), - md_page!("v0.6", "admin-panel"), - md_page!("v0.6", "static-files"), - md_page!("v0.6", "sending-emails"), - md_page!("v0.6", "caching"), - md_page!("v0.6", "error-pages"), - md_page!("v0.6", "openapi"), - md_page!("v0.6", "testing"), - ], + "Upgrading", + vec![GuideItem::Page(md_page!("v0.5", "upgrade-guide"))], + ), + ( + "About", + vec![GuideItem::Page(md_page!("v0.5", "framework-comparison"))], ), - ("Upgrading", vec![md_page!("v0.6", "upgrade-guide")]), - ("About", vec![md_page!("v0.6", "framework-comparison")]), ], ), ("master", master_version), diff --git a/src/lib.rs b/src/lib.rs index ba4030d..eb88d1e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -64,6 +64,81 @@ async fn index(base_context: BaseContext) -> cot::Result { Ok(Html::new(rendered)) } +#[derive(Debug, Clone)] +struct GuideLinkCategory { + title: &'static str, + guides: Vec, +} + +#[derive(Debug, Clone)] +enum GuideCategoryItem { + Page(MdPageLink), + SubCategory { + title: &'static str, + pages: Vec, + }, +} + +impl GuideCategoryItem { + /// Takes a link and checks whether any of the pages in that subcategory + /// matches it. We call this inside the templates to decide whether a + /// subcategory accordion should start open or closed. + fn contains_active_page(&self, current_link: &str) -> bool { + match self { + GuideCategoryItem::SubCategory { pages, .. } => { + pages.iter().any(|p| p.link == current_link) + } + GuideCategoryItem::Page(_) => false, + } + } + /// Returns a unique ID for the category which bootstrap uses to + /// control the open/close behavior of the accordion + fn collapse_id(&self) -> String { + match self { + GuideCategoryItem::SubCategory { title, .. } => title.to_lowercase().replace(' ', "-"), + GuideCategoryItem::Page(_) => String::new(), + } + } +} + +/// Represents an item in a documentation guide. Each item can either be a +/// single markdown page or a subcategory containing a collection of related +/// pages. +pub enum GuideItem { + /// A single markdown page to be rendered as part of the guide. + /// + /// # Examples + /// ``` + /// use cot_site::GuideItem; + /// use cot_site_macros::md_page; + /// + /// let page = GuideItem::Page(md_page!("guide", "introduction")); + /// ``` + Page(MdPage), + /// A subcategory containing a collection of related pages. + /// + /// # Examples + /// ``` + /// use cot_site::GuideItem; + /// use cot_site_macros::md_page; + /// + /// let subcategory = GuideItem::SubCategory( + /// { + /// title: "Database", + /// pages: vec![ + /// md_page!("guide/databases/overview"), + /// md_page!("guide/databases/queries"), + /// md_page!("guide/databases/migrations"), + /// ] + /// } + /// ) + /// ``` + SubCategory { + title: &'static str, + pages: Vec, + }, +} + #[derive(Debug, Template)] #[template(path = "guide.html")] struct GuideTemplate<'a> { @@ -79,12 +154,6 @@ struct GuideTemplate<'a> { next: Option<&'a MdPageLink>, } -#[derive(Debug, Clone)] -struct GuideLinkCategory { - title: &'static str, - guides: Vec, -} - fn render_section(section: &Section) -> Safe { #[derive(Debug, Clone, Template)] #[template(path = "_md_page_toc_item.html")] @@ -247,7 +316,7 @@ impl CotSiteApp { /// The `master_pages` parameter should contain a list of sections, where /// each section is a tuple containing the name of the section and list /// of pages inside it. - pub fn new(master_pages: Vec<(&'static str, Vec)>) -> Self { + pub fn new(master_pages: Vec<(&'static str, Vec)>) -> Self { let pages = get_categories(master_pages); Self { @@ -309,6 +378,7 @@ impl App for CotSiteApp { fn static_files(&self) -> Vec { static_files!( "favicon.ico", + "static/css/guide_chapters.css", "static/css/main.css", "static/js/color-modes.js", "static/js/search.js", diff --git a/static/static/css/guide_chapters.css b/static/static/css/guide_chapters.css new file mode 100644 index 0000000..a6ed7c8 --- /dev/null +++ b/static/static/css/guide_chapters.css @@ -0,0 +1,56 @@ +/* Subcategory toggle button */ +.guide-subcategory-toggle { + background: none; + border: none; + padding: 0.2rem 0; + font-size: inherit; + font-weight: 600; + color: var(--bs-secondary-color); + cursor: pointer; + transition: color 0.15s ease; +} + +.guide-subcategory-toggle:hover, +.guide-subcategory-toggle.active { + color: var(--bs-emphasis-color); +} + +/* Chevron rotation when open */ +.guide-subcategory-chevron { + flex-shrink: 0; + transition: transform 0.2s ease; +} + +.guide-subcategory-toggle[aria-expanded="true"] .guide-subcategory-chevron { + transform: rotate(180deg); +} + +/* Indented pages inside the subcategory */ +.guide-subcategory-pages li { + padding: 0.1rem 0; +} + + +.guide-subcategory-pages { + list-style: none; + padding: 0.25rem 0 0.25rem 0.75rem !important; + margin: 0; + border-left: 2px solid var(--bs-border-color); + +} + +/* Shared link styles */ +.guide-link { + color: var(--bs-secondary-color); + text-decoration: none; + transition: color 0.15s ease; +} + +.guide-link:hover { + color: var(--bs-emphasis-color); +} + +.guide-link.active { + color: var(--bs-primary); + font-weight: 500; +} diff --git a/templates/_base.html b/templates/_base.html index cf245e6..50eeb20 100644 --- a/templates/_base.html +++ b/templates/_base.html @@ -22,6 +22,7 @@ + diff --git a/templates/_guide_chapters.html b/templates/_guide_chapters.html index 3b81f34..069f638 100644 --- a/templates/_guide_chapters.html +++ b/templates/_guide_chapters.html @@ -23,8 +23,49 @@
    - {%- for guide_link in category.guides -%} -
  • {{ guide_link.title }}
  • + {%- for item in category.guides -%} + {%- let is_active = item.contains_active_page(&guide.link) -%} + {%- match item -%} + + {%- when GuideCategoryItem::Page(link) -%} +
  • + + {{ link.title }} + +
  • + + {%- when GuideCategoryItem::SubCategory { title, pages } -%} + {%- let collapse_id = item.collapse_id() -%} +
  • + +
    + +
    +
  • + + {%- endmatch -%} {%- endfor -%}