Skip to content
177 changes: 92 additions & 85 deletions src/guides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MdPage>)>) -> ParsedPagesForVersion {
pub fn parse_guides(categories: Vec<(&'static str, Vec<GuideItem>)>) -> 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 {
Expand All @@ -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<MdPage>)>) -> ParsedPages {
pub fn get_categories(master_version: Vec<(&'static str, Vec<GuideItem>)>) -> 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")),
],
)],
),
Expand All @@ -81,14 +102,14 @@ pub(crate) fn get_categories(master_version: Vec<(&'static str, Vec<MdPage>)>) -
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")),
],
)],
),
Expand All @@ -97,15 +118,15 @@ pub(crate) fn get_categories(master_version: Vec<(&'static str, Vec<MdPage>)>) -
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")),
],
)],
),
Expand All @@ -115,18 +136,21 @@ pub(crate) fn get_categories(master_version: Vec<(&'static str, Vec<MdPage>)>) -
(
"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"))],
),
],
),
(
Expand All @@ -135,44 +159,27 @@ pub(crate) fn get_categories(master_version: Vec<(&'static str, Vec<MdPage>)>) -
(
"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),
Expand Down
80 changes: 73 additions & 7 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,77 @@ async fn index(base_context: BaseContext) -> cot::Result<Html> {
Ok(Html::new(rendered))
}

#[derive(Debug, Clone)]
struct GuideLinkCategory {
title: &'static str,
guides: Vec<GuideCategoryItem>,
}

#[derive(Debug, Clone)]
enum GuideCategoryItem {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if GuideCategoryItem could be merged with GuideItem as they seem almost the same. Why did you decide to split them? 🤔

Copy link
Copy Markdown
Contributor Author

@ElijahAhianyo ElijahAhianyo Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think they serve the same purpose, per se. The previous version of CotApp::new accepted MdPage, which was a public-facing representation of a page. When parsing the guide(cot_site::guides::parse_guides), we convert that to MdPageLink(almost similar signature as MdPage), which is an internal representation of the page that gets rendered in the template.

This change also follows the same concept/design; GuideItem is public-facing, whereas GuideCategoryItem is an internal representation of MdPageLink(s) that get rendered in the template

Page(MdPageLink),
SubCategory {
title: &'static str,
pages: Vec<MdPageLink>,
},
}

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
/// ```ignore
/// use cot_site::{GuideItem, md_page};
///
/// let page = GuideItem::Page(md_page!("guide", "introduction"));
/// ```
Page(MdPage),
/// A subcategory containing a collection of related pages.
///
/// # Examples
/// ```ignore
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ignore here because the md_page macro will try to read the guides which we dont want

/// use cot_site::{GuideItem, 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<MdPage>,
},
}

#[derive(Debug, Template)]
#[template(path = "guide.html")]
struct GuideTemplate<'a> {
Expand All @@ -79,12 +150,6 @@ struct GuideTemplate<'a> {
next: Option<&'a MdPageLink>,
}

#[derive(Debug, Clone)]
struct GuideLinkCategory {
title: &'static str,
guides: Vec<MdPageLink>,
}

fn render_section(section: &Section) -> Safe<String> {
#[derive(Debug, Clone, Template)]
#[template(path = "_md_page_toc_item.html")]
Expand Down Expand Up @@ -257,7 +322,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<MdPage>)>) -> Self {
pub fn new(master_pages: Vec<(&'static str, Vec<GuideItem>)>) -> Self {
let pages = get_categories(master_pages);

Self {
Expand Down Expand Up @@ -335,6 +400,7 @@ impl App for CotSiteApp {
fn static_files(&self) -> Vec<StaticFile> {
static_files!(
"favicon.ico",
"static/css/guide_chapters.css",
Comment thread
ElijahAhianyo marked this conversation as resolved.
Outdated
"static/css/main.css",
"static/js/color-modes.js",
"static/js/search.js",
Expand Down
Loading
Loading