use hyperchad::template::Containers;
pub type PageRenderer = fn(&DocPage) -> Containers;
#[derive(Clone, Copy)]
pub struct DocsSection {
pub id: &'static str,
pub title: &'static str,
}
impl DocsSection {
#[must_use]
pub const fn new(id: &'static str, title: &'static str) -> Self {
Self { id, title }
}
}
#[derive(Clone)]
pub enum PageKind {
Markdown {
contents: &'static str,
},
GeneratedMarkdown {
generate: fn() -> String,
},
Custom {
render: PageRenderer,
},
}
#[derive(Clone)]
pub struct DocPage {
pub source: Option<&'static str>,
pub route: &'static str,
pub title: Option<&'static str>,
pub section: Option<&'static str>,
pub nav_label: Option<&'static str>,
pub kind: PageKind,
}
impl DocPage {
#[must_use]
pub const fn markdown(
source: &'static str,
route: &'static str,
contents: &'static str,
) -> Self {
Self {
source: Some(source),
route,
title: None,
section: None,
nav_label: None,
kind: PageKind::Markdown { contents },
}
}
#[must_use]
pub const fn generated_markdown(route: &'static str, generate: fn() -> String) -> Self {
Self {
source: None,
route,
title: None,
section: None,
nav_label: None,
kind: PageKind::GeneratedMarkdown { generate },
}
}
#[must_use]
pub const fn generated(route: &'static str, generate: fn() -> String) -> Self {
Self::generated_markdown(route, generate)
}
#[must_use]
pub const fn custom(route: &'static str, render: PageRenderer) -> Self {
Self {
source: None,
route,
title: None,
section: None,
nav_label: None,
kind: PageKind::Custom { render },
}
}
#[must_use]
pub const fn title(mut self, title: &'static str) -> Self {
self.title = Some(title);
self
}
#[must_use]
pub const fn section(mut self, section: &'static str) -> Self {
self.section = Some(section);
self
}
#[must_use]
pub const fn nav_label(mut self, nav_label: &'static str) -> Self {
self.nav_label = Some(nav_label);
self
}
#[must_use]
pub const fn nav(mut self, section: &'static str, nav_label: &'static str) -> Self {
self.section = Some(section);
self.nav_label = Some(nav_label);
self
}
}
#[derive(Clone)]
pub struct NavSection {
pub title: &'static str,
pub items: Vec<NavItem>,
}
#[derive(Clone)]
pub struct NavItem {
pub label: &'static str,
pub href: &'static str,
}
#[must_use]
pub fn nav_sections(sections: &[DocsSection], pages: &[DocPage]) -> Vec<NavSection> {
sections
.iter()
.filter_map(|section| {
let items: Vec<NavItem> = pages
.iter()
.filter(|page| page.section == Some(section.id))
.filter_map(|page| {
page.nav_label.map(|label| NavItem {
label,
href: page.route,
})
})
.collect();
if items.is_empty() {
None
} else {
Some(NavSection {
title: section.title,
items,
})
}
})
.collect()
}
#[macro_export]
macro_rules! docs_markdown_page {
(
source: $source:literal,
route: $route:literal,
title: None,
section: $section:expr,
nav_label: $nav_label:expr,
contents: $contents:expr $(,)?
) => {
$crate::DocPage::markdown($source, $route, $contents).nav($section, $nav_label)
};
(
source: $source:literal,
route: $route:literal,
title: None,
section: $section:expr,
nav_label: $nav_label:expr $(,)?
) => {
$crate::DocPage::markdown(
$source,
$route,
include_str!(concat!("../../../../", $source)),
)
.nav($section, $nav_label)
};
(
source: $source:literal,
route: $route:literal,
title: $title:expr,
section: $section:expr,
nav_label: $nav_label:expr,
contents: $contents:expr $(,)?
) => {
$crate::DocPage::markdown($source, $route, $contents)
.title($title)
.nav($section, $nav_label)
};
(
source: $source:literal,
route: $route:literal,
title: $title:expr,
section: $section:expr,
nav_label: $nav_label:expr $(,)?
) => {
$crate::DocPage::markdown(
$source,
$route,
include_str!(concat!("../../../../", $source)),
)
.title($title)
.nav($section, $nav_label)
};
}
#[macro_export]
macro_rules! docs_generated_page {
(
route: $route:literal,
title: None,
section: $section:expr,
nav_label: $nav_label:expr,
generate: $generate:expr $(,)?
) => {
$crate::DocPage::generated_markdown($route, $generate).nav($section, $nav_label)
};
(
route: $route:literal,
title: $title:expr,
section: $section:expr,
nav_label: $nav_label:expr,
generate: $generate:expr $(,)?
) => {
$crate::DocPage::generated_markdown($route, $generate)
.title($title)
.nav($section, $nav_label)
};
}
#[cfg(test)]
mod tests {
use super::*;
fn generate() -> String {
"# Generated".to_string()
}
#[test]
fn nav_sets_section_and_label() {
let page = DocPage::generated("/docs/generated", generate).nav("reference", "Generated");
assert_eq!(page.section, Some("reference"));
assert_eq!(page.nav_label, Some("Generated"));
}
#[test]
fn nav_sections_preserve_declared_section_and_page_order() {
let sections = [
DocsSection::new("getting-started", "Getting Started"),
DocsSection::new("reference", "Reference"),
];
let pages = [
DocPage::generated("/docs/cli", generate).nav("reference", "CLI"),
DocPage::generated("/docs", generate).nav("getting-started", "Overview"),
DocPage::generated("/docs/config", generate).nav("reference", "Config"),
DocPage::generated("/hidden", generate),
];
let nav = nav_sections(§ions, &pages);
assert_eq!(nav.len(), 2);
assert_eq!(nav[0].title, "Getting Started");
assert_eq!(nav[0].items[0].label, "Overview");
assert_eq!(nav[1].title, "Reference");
assert_eq!(nav[1].items[0].label, "CLI");
assert_eq!(nav[1].items[1].label, "Config");
}
}