diff --git a/src/config.rs b/src/config.rs index f43b908..045f4fd 100644 --- a/src/config.rs +++ b/src/config.rs @@ -66,6 +66,15 @@ impl Config { Config::parse(&content) } + + /// Makes a url, taking into account that the base url might have a trailing slash + pub fn make_permalink(&self, path: &str) -> String { + if self.base_url.ends_with('/') { + format!("{}{}", self.base_url, path) + } else { + format!("{}/{}", self.base_url, path) + } + } } impl Default for Config { diff --git a/src/page.rs b/src/page.rs index 6ff9376..be7d6ed 100644 --- a/src/page.rs +++ b/src/page.rs @@ -173,11 +173,7 @@ impl Page { } } - page.permalink = if config.base_url.ends_with('/') { - format!("{}{}", config.base_url, page.url) - } else { - format!("{}/{}", config.base_url, page.url) - }; + page.permalink = config.make_permalink(&page.url); Ok(page) } diff --git a/src/section.rs b/src/section.rs index 2c6cc68..3013a44 100644 --- a/src/section.rs +++ b/src/section.rs @@ -52,11 +52,7 @@ impl Section { section.url = section.components.join("/"); section.permalink = section.components.join("/"); - section.permalink = if config.base_url.ends_with('/') { - format!("{}{}", config.base_url, section.url) - } else { - format!("{}/{}", config.base_url, section.url) - }; + section.permalink = config.make_permalink(§ion.url); Ok(section) } diff --git a/src/site.rs b/src/site.rs index 67bf540..12709ab 100644 --- a/src/site.rs +++ b/src/site.rs @@ -60,6 +60,8 @@ pub struct Site { pub templates: Tera, live_reload: bool, output_path: PathBuf, + pub tags: HashMap>, + pub categories: HashMap>, } impl Site { @@ -80,8 +82,10 @@ impl Site { templates: tera, live_reload: false, output_path: PathBuf::from("public"), + tags: HashMap::new(), + categories: HashMap::new(), }; - site.parse_site()?; + site.parse()?; Ok(site) } @@ -99,7 +103,7 @@ impl Site { /// Reads all .md files in the `content` directory and create pages /// out of them - fn parse_site(&mut self) -> Result<()> { + pub fn parse(&mut self) -> Result<()> { let path = self.base_path.to_string_lossy().replace("\\", "/"); let content_glob = format!("{}/{}", path, "content/**/*.md"); @@ -122,7 +126,6 @@ impl Site { self.pages.insert(page.file_path.clone(), page); } } - // Find out the direct subsections of each subsection if there are some let mut grandparent_paths = HashMap::new(); for section in sections.values() { @@ -140,10 +143,32 @@ impl Site { } self.sections = sections; + self.parse_tags_and_categories(); Ok(()) } + /// Separated from `parse` for easier testing + pub fn parse_tags_and_categories(&mut self) { + for page in self.pages.values() { + if let Some(ref category) = page.meta.category { + self.categories + .entry(category.to_string()) + .or_insert_with(|| vec![]) + .push(page.file_path.clone()); + } + + if let Some(ref tags) = page.meta.tags { + for tag in tags { + self.tags + .entry(tag.to_string()) + .or_insert_with(|| vec![]) + .push(page.file_path.clone()); + } + } + } + } + /// Inject live reload script tag if in live reload mode fn inject_livereload(&self, html: String) -> String { if self.live_reload { @@ -197,7 +222,7 @@ impl Site { } pub fn rebuild_after_content_change(&mut self) -> Result<()> { - self.parse_site()?; + self.parse()?; self.build() } @@ -213,8 +238,6 @@ impl Site { } let mut pages = vec![]; - let mut category_pages: HashMap> = HashMap::new(); - let mut tag_pages: HashMap> = HashMap::new(); // First we render the pages themselves for page in self.pages.values() { @@ -243,21 +266,11 @@ impl Site { } pages.push(page); - - if let Some(ref category) = page.meta.category { - category_pages.entry(category.to_string()).or_insert_with(|| vec![]).push(page); - } - - if let Some(ref tags) = page.meta.tags { - for tag in tags { - tag_pages.entry(tag.to_string()).or_insert_with(|| vec![]).push(page); - } - } } // Outputting categories and pages - self.render_categories_and_tags(RenderList::Categories, &category_pages)?; - self.render_categories_and_tags(RenderList::Tags, &tag_pages)?; + self.render_categories_and_tags(RenderList::Categories)?; + self.render_categories_and_tags(RenderList::Tags)?; // And finally the index page let mut context = Context::new(); @@ -275,48 +288,63 @@ impl Site { self.clean()?; self.build_pages()?; self.render_sitemap()?; + if self.config.generate_rss.unwrap() { self.render_rss_feed()?; } + self.render_sections()?; self.copy_static_directory() } /// Render the /{categories, list} pages and each individual category/tag page - fn render_categories_and_tags(&self, kind: RenderList, container: &HashMap>) -> Result<()> { - if container.is_empty() { + /// They are the same thing fundamentally, a list of pages with something in common + fn render_categories_and_tags(&self, kind: RenderList) -> Result<()> { + let items = match kind { + RenderList::Categories => &self.categories, + RenderList::Tags => &self.tags, + }; + + if items.is_empty() { return Ok(()); } - let (name, list_tpl_name, single_tpl_name, var_name) = if kind == RenderList::Categories { - ("categories", "categories.html", "category.html", "category") + let (list_tpl_name, single_tpl_name, name, var_name) = if kind == RenderList::Categories { + ("categories.html", "category.html", "categories", "category") } else { - ("tags", "tags.html", "tag.html", "tag") + ("tags.html", "tag.html", "tags", "tag") }; + // Create the categories/tags directory first let public = self.output_path.clone(); let mut output_path = public.to_path_buf(); output_path.push(name); create_directory(&output_path)?; - // First we render the list of categories/tags page - let mut sorted_container = vec![]; - for (item, count) in Vec::from_iter(container).into_iter().map(|(a, b)| (a, b.len())) { - sorted_container.push(ListItem::new(item, count)); + // Then render the index page for that kind. + // We sort by number of page in that category/tag + let mut sorted_items = vec![]; + for (item, count) in Vec::from_iter(items).into_iter().map(|(a, b)| (a, b.len())) { + sorted_items.push(ListItem::new(&item, count)); } - sorted_container.sort_by(|a, b| b.count.cmp(&a.count)); - + sorted_items.sort_by(|a, b| b.count.cmp(&a.count)); let mut context = Context::new(); - context.add(name, &sorted_container); + context.add(name, &sorted_items); context.add("config", &self.config); - + // And render it immediately let list_output = self.templates.render(list_tpl_name, &context)?; create_file(output_path.join("index.html"), &self.inject_livereload(list_output))?; - // and then each individual item - for (item_name, mut pages) in container.clone() { - let mut context = Context::new(); + // Now, each individual item + for (item_name, pages_paths) in items.iter() { + let mut pages: Vec<&Page> = self.pages + .iter() + .filter(|&(path, _)| pages_paths.contains(&path)) + .map(|(_, page)| page) + .collect(); pages.sort_by(|a, b| a.partial_cmp(b).unwrap()); + + let mut context = Context::new(); let slug = slugify(&item_name); context.add(var_name, &item_name); context.add(&format!("{}_slug", var_name), &slug); @@ -338,7 +366,29 @@ impl Site { let mut context = Context::new(); context.add("pages", &self.pages.values().collect::>()); context.add("sections", &self.sections.values().collect::>()); - // TODO: add categories and tags pages + + let mut categories = vec![]; + if !self.categories.is_empty() { + categories.push(self.config.make_permalink("categories")); + for category in self.categories.keys() { + categories.push( + self.config.make_permalink(&format!("categories/{}", slugify(category))) + ); + } + } + context.add("categories", &categories); + + let mut tags = vec![]; + if !self.tags.is_empty() { + tags.push(self.config.make_permalink("tags")); + for tag in self.tags.keys() { + tags.push( + self.config.make_permalink(&format!("tags/{}", slugify(tag))) + ); + } + } + context.add("tags", &tags); + let sitemap = self.templates.render("sitemap.xml", &context)?; create_file(self.output_path.join("sitemap.xml"), &sitemap)?; diff --git a/src/templates/sitemap.xml b/src/templates/sitemap.xml index 24b7ccf..d867752 100644 --- a/src/templates/sitemap.xml +++ b/src/templates/sitemap.xml @@ -12,4 +12,14 @@ {{ section.permalink | safe }} {% endfor %} + {% for category in categories %} + + {{ category | safe }} + + {% endfor %} + {% for tag in tags %} + + {{ tag | safe }} + + {% endfor %} diff --git a/tests/site.rs b/tests/site.rs index 04f2675..3df6926 100644 --- a/tests/site.rs +++ b/tests/site.rs @@ -164,22 +164,21 @@ fn test_can_build_site_with_categories() { path.push("test_site"); let mut site = Site::new(&path).unwrap(); - let mut i = 0; - for (_, page) in &mut site.pages { + for (i, page) in site.pages.values_mut().enumerate() { page.meta.category = if i % 2 == 0 { Some("A".to_string()) } else { Some("B".to_string()) }; - i += 1; } - + site.parse_tags_and_categories(); let tmp_dir = TempDir::new("example").expect("create temp dir"); let public = &tmp_dir.path().join("public"); site.set_output_path(&public); site.build().unwrap(); assert!(Path::new(&public).exists()); + assert_eq!(site.categories.len(), 2); assert!(file_exists!(public, "index.html")); assert!(file_exists!(public, "sitemap.xml")); @@ -202,6 +201,10 @@ fn test_can_build_site_with_categories() { assert!(file_exists!(public, "categories/b/index.html")); // Tags aren't assert_eq!(file_exists!(public, "tags/index.html"), false); + + // Categories are in the sitemap + assert!(file_contains!(public, "sitemap.xml", "https://replace-this-with-your-url.com/categories")); + assert!(file_contains!(public, "sitemap.xml", "https://replace-this-with-your-url.com/categories/a")); } #[test] @@ -210,15 +213,14 @@ fn test_can_build_site_with_tags() { path.push("test_site"); let mut site = Site::new(&path).unwrap(); - let mut i = 0; - for (_, page) in &mut site.pages { + for (i, page) in site.pages.values_mut().enumerate() { page.meta.tags = if i % 2 == 0 { Some(vec!["tag1".to_string(), "tag2".to_string()]) } else { Some(vec!["tag with space".to_string()]) }; - i += 1; } + site.parse_tags_and_categories(); let tmp_dir = TempDir::new("example").expect("create temp dir"); let public = &tmp_dir.path().join("public"); @@ -226,6 +228,7 @@ fn test_can_build_site_with_tags() { site.build().unwrap(); assert!(Path::new(&public).exists()); + assert_eq!(site.tags.len(), 3); assert!(file_exists!(public, "index.html")); assert!(file_exists!(public, "sitemap.xml")); @@ -249,4 +252,7 @@ fn test_can_build_site_with_tags() { assert!(file_exists!(public, "tags/tag-with-space/index.html")); // Categories aren't assert_eq!(file_exists!(public, "categories/index.html"), false); + // Tags are in the sitemap + assert!(file_contains!(public, "sitemap.xml", "https://replace-this-with-your-url.com/tags")); + assert!(file_contains!(public, "sitemap.xml", "https://replace-this-with-your-url.com/tags/tag-with-space")); }