use std::collections::HashMap;
use serde::Serialize;
use crate::config::{Config, Social};
use crate::content::page::{Page, PageKind};
use crate::content::slug::Slug;
use crate::content::{PageAny, PageFrontmatter, Site};
use crate::markdown::{HeadingEntry, Rendered};
#[derive(Debug, Clone, Serialize)]
pub struct NavEntry {
pub title: String,
pub url: String,
}
impl NavEntry {
pub fn from_pages(pages: &[Page<PageFrontmatter>]) -> Vec<Self> {
let mut entries: Vec<_> = pages
.iter()
.map(|p| {
let order = p.frontmatter.order.unwrap_or(i32::MAX);
(
order,
NavEntry {
title: p.title().to_string(),
url: PageKind::Page.url_path(&p.slug),
},
)
})
.collect();
entries.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| a.1.title.cmp(&b.1.title)));
entries.into_iter().map(|(_, entry)| entry).collect()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct TocEntry {
pub level: u8,
pub text: String,
pub id: Slug,
}
impl From<&HeadingEntry> for TocEntry {
fn from(h: &HeadingEntry) -> Self {
Self {
level: h.level,
text: h.text.clone(),
id: h.id.clone(),
}
}
}
#[derive(Debug, Clone, Serialize)]
pub struct BacklinkEntry {
pub title: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct TagRef {
pub name: String,
pub slug: Slug,
}
#[derive(Debug, Clone, Serialize)]
pub struct PostEntry {
pub title: String,
pub url: String,
pub created: Option<String>,
pub image: Option<String>,
pub description: Option<String>,
pub tags: Vec<TagRef>,
}
impl PostEntry {
pub fn from_page(any: &PageAny<'_>) -> Self {
Self {
title: any.title().into_owned(),
url: any.url_path(),
created: any.created(),
image: any.image().map(String::from),
description: any.description().map(String::from),
tags: any
.tags()
.iter()
.map(|t| TagRef {
name: t.clone(),
slug: t.as_str().into(),
})
.collect(),
}
}
}
#[derive(Debug, Serialize)]
pub struct BlogIndexContext {
#[serde(flatten)]
pub site: SiteContext,
pub posts: Vec<PostEntry>,
}
#[derive(Debug, Clone, Serialize)]
pub struct HomeContent {
pub content: String,
}
#[derive(Debug, Serialize)]
pub struct HomeContext {
#[serde(flatten)]
pub site: SiteContext,
pub posts: Vec<PostEntry>,
pub home: Option<HomeContent>,
}
#[derive(Debug, Clone, Serialize)]
pub struct WikiEntry {
pub title: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct WikiCategory {
pub name: Option<String>,
pub pages: Vec<WikiEntry>,
}
impl WikiCategory {
pub fn from_site(site: &Site) -> Vec<Self> {
let mut by_category: HashMap<Option<String>, Vec<WikiEntry>> = HashMap::new();
for p in &site.wiki {
by_category
.entry(p.frontmatter.category.clone())
.or_default()
.push(WikiEntry {
title: p.title().to_string(),
url: p.url_path(),
});
}
for entries in by_category.values_mut() {
entries.sort_by(|a, b| a.title.cmp(&b.title));
}
let mut categories: Vec<Self> = by_category
.into_iter()
.map(|(name, pages)| Self { name, pages })
.collect();
let order = &site.config.wiki_categories;
categories.sort_by_cached_key(|c| match &c.name {
Some(name) => match order.iter().position(|x| x == name) {
Some(idx) => (0u8, idx, String::new()),
None => (1u8, 0, name.clone()),
},
None => (2u8, 0, String::new()),
});
categories
}
}
#[derive(Debug, Serialize)]
pub struct WikiIndexContext {
#[serde(flatten)]
pub site: SiteContext,
pub categories: Vec<WikiCategory>,
}
#[derive(Debug, Serialize)]
pub struct TagPageContext {
#[serde(flatten)]
pub site: SiteContext,
pub tag: String,
pub tag_slug: Slug,
pub posts: Vec<PostEntry>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TagEntry {
pub name: String,
pub slug: Slug,
pub count: usize,
}
#[derive(Debug, Serialize)]
pub struct TagsIndexContext {
#[serde(flatten)]
pub site: SiteContext,
pub tags: Vec<TagEntry>,
}
#[derive(Debug, Serialize)]
pub struct NotFoundContext {
#[serde(flatten)]
pub site: SiteContext,
}
#[derive(Debug, Clone, Serialize)]
pub struct SiteContext {
pub site_title: String,
pub base_url: String,
pub version: String,
pub nav_pages: Vec<NavEntry>,
pub socials: Vec<Social>,
}
impl SiteContext {
pub fn from_config(config: &Config, pages: &[Page<PageFrontmatter>]) -> Self {
Self {
site_title: config.title.clone(),
base_url: config.base_url.clone(),
version: env!("CARGO_PKG_VERSION").to_string(),
nav_pages: NavEntry::from_pages(pages),
socials: config.socials.clone(),
}
}
}
#[derive(Debug, Serialize)]
pub struct PageContext {
#[serde(flatten)]
pub site: SiteContext,
pub title: String,
pub url: String,
pub kind: PageKind,
pub content: String,
pub toc: Vec<TocEntry>,
pub backlinks: Vec<BacklinkEntry>,
pub category: Option<String>,
pub wiki_categories: Vec<WikiCategory>,
pub author: Option<String>,
pub image: Option<String>,
pub description: Option<String>,
pub created: Option<String>,
pub updated: Option<String>,
pub tags: Vec<TagRef>,
}
impl PageContext {
pub fn from_page(
page: &PageAny<'_>,
rendered: &Rendered,
site: &Site,
site_ctx: &SiteContext,
) -> Self {
Self {
site: site_ctx.clone(),
title: page.title().into_owned(),
url: page.url_path(),
kind: page.kind(),
content: rendered.html.clone(),
toc: rendered.toc.iter().map(TocEntry::from).collect(),
backlinks: site
.backlinks_for(page.slug())
.iter()
.map(|p| BacklinkEntry {
title: p.title().into_owned(),
url: p.url_path(),
})
.collect(),
category: page.category().map(String::from),
wiki_categories: match page.kind() {
PageKind::Wiki => WikiCategory::from_site(site),
_ => Vec::new(),
},
author: page.author().map(String::from),
image: page.image().map(String::from),
description: page.description().map(String::from),
created: page.created(),
updated: page.updated(),
tags: page
.tags()
.iter()
.map(|t| TagRef {
name: t.clone(),
slug: t.as_str().into(),
})
.collect(),
}
}
pub(super) fn template_name(&self) -> &'static str {
self.kind.template_name()
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use super::*;
use crate::content::WikiFrontmatter;
use crate::content::page::Page;
fn wiki_page(slug: &str, category: Option<&str>) -> Page<WikiFrontmatter> {
Page {
slug: slug.into(),
body: String::new(),
path: PathBuf::from(format!("content/wiki/{slug}.md")),
frontmatter: WikiFrontmatter {
title: None,
category: category.map(Into::into),
created: None,
updated: None,
tags: vec![],
},
}
}
fn site_with_wiki(wiki_categories: &[&str], pages: Vec<Page<WikiFrontmatter>>) -> Site {
let mut config: Config = "title = \"T\"\nbase_url = \"http://x\"".parse().unwrap();
config.wiki_categories = wiki_categories.iter().map(|s| (*s).to_owned()).collect();
Site::from_parts(config, vec![], pages, vec![]).unwrap()
}
#[test]
fn wiki_categories_ordered_by_config_then_alphabetical_then_none() {
let site = site_with_wiki(
&["Getting Started", "Content"],
vec![
wiki_page("alpha", Some("Development")), wiki_page("beta", Some("Content")), wiki_page("gamma", None), wiki_page("delta", Some("Customization")), wiki_page("epsilon", Some("Getting Started")), ],
);
let cats = WikiCategory::from_site(&site);
let names: Vec<_> = cats.iter().map(|c| c.name.as_deref()).collect();
assert_eq!(
names,
vec![
Some("Getting Started"),
Some("Content"),
Some("Customization"),
Some("Development"),
None,
]
);
}
#[test]
fn wiki_categories_default_to_alphabetical_when_config_empty() {
let site = site_with_wiki(
&[],
vec![
wiki_page("a", Some("Zeta")),
wiki_page("b", Some("Alpha")),
wiki_page("c", None),
],
);
let cats = WikiCategory::from_site(&site);
let names: Vec<_> = cats.iter().map(|c| c.name.as_deref()).collect();
assert_eq!(names, vec![Some("Alpha"), Some("Zeta"), None]);
}
}