|
|
@@ -1,47 +1,24 @@ |
|
|
|
use std::collections::{HashMap}; |
|
|
|
use std::iter::FromIterator; |
|
|
|
use std::collections::HashMap; |
|
|
|
use std::fs::{remove_dir_all, copy, create_dir_all}; |
|
|
|
use std::path::{Path, PathBuf}; |
|
|
|
|
|
|
|
use glob::glob; |
|
|
|
use tera::{Tera, Context}; |
|
|
|
use slug::slugify; |
|
|
|
use walkdir::WalkDir; |
|
|
|
|
|
|
|
use errors::{Result, ResultExt}; |
|
|
|
use config::{Config, get_config}; |
|
|
|
use utils::{create_file, create_directory}; |
|
|
|
use content::{Page, Section, Paginator, SortBy, populate_previous_and_next_pages, sort_pages}; |
|
|
|
use fs::{create_file, create_directory, ensure_directory_exists}; |
|
|
|
use content::{Page, Section, Paginator, SortBy, Taxonomy, populate_previous_and_next_pages, sort_pages}; |
|
|
|
use templates::{GUTENBERG_TERA, global_fns, render_redirect_template}; |
|
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, PartialEq)] |
|
|
|
enum RenderList { |
|
|
|
Tags, |
|
|
|
Categories, |
|
|
|
} |
|
|
|
|
|
|
|
/// A tag or category |
|
|
|
#[derive(Debug, Serialize, PartialEq)] |
|
|
|
struct ListItem { |
|
|
|
name: String, |
|
|
|
slug: String, |
|
|
|
count: usize, |
|
|
|
} |
|
|
|
|
|
|
|
impl ListItem { |
|
|
|
pub fn new(name: &str, count: usize) -> ListItem { |
|
|
|
ListItem { |
|
|
|
name: name.to_string(), |
|
|
|
slug: slugify(name), |
|
|
|
count: count, |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
#[derive(Debug)] |
|
|
|
pub struct Site { |
|
|
|
/// The base path of the gutenberg site |
|
|
|
pub base_path: PathBuf, |
|
|
|
/// The parsed config for the site |
|
|
|
pub config: Config, |
|
|
|
pub pages: HashMap<PathBuf, Page>, |
|
|
|
pub sections: HashMap<PathBuf, Section>, |
|
|
@@ -49,8 +26,8 @@ pub struct Site { |
|
|
|
live_reload: bool, |
|
|
|
output_path: PathBuf, |
|
|
|
static_path: PathBuf, |
|
|
|
pub tags: HashMap<String, Vec<PathBuf>>, |
|
|
|
pub categories: HashMap<String, Vec<PathBuf>>, |
|
|
|
pub tags: Option<Taxonomy>, |
|
|
|
pub categories: Option<Taxonomy>, |
|
|
|
/// A map of all .md files (section and pages) and their permalink |
|
|
|
/// We need that if there are relative links in the content that need to be resolved |
|
|
|
pub permalinks: HashMap<String, String>, |
|
|
@@ -75,8 +52,8 @@ impl Site { |
|
|
|
live_reload: false, |
|
|
|
output_path: path.join("public"), |
|
|
|
static_path: path.join("static"), |
|
|
|
tags: HashMap::new(), |
|
|
|
categories: HashMap::new(), |
|
|
|
tags: None, |
|
|
|
categories: None, |
|
|
|
permalinks: HashMap::new(), |
|
|
|
}; |
|
|
|
|
|
|
@@ -88,15 +65,6 @@ impl Site { |
|
|
|
self.live_reload = true; |
|
|
|
} |
|
|
|
|
|
|
|
/// Gets the path of all ignored pages in the site |
|
|
|
/// Used for reporting them in the CLI |
|
|
|
pub fn get_ignored_pages(&self) -> Vec<PathBuf> { |
|
|
|
self.sections |
|
|
|
.values() |
|
|
|
.flat_map(|s| s.ignored_pages.iter().map(|p| p.file.path.clone())) |
|
|
|
.collect() |
|
|
|
} |
|
|
|
|
|
|
|
/// Get all the orphan (== without section) pages in the site |
|
|
|
pub fn get_all_orphan_pages(&self) -> Vec<&Page> { |
|
|
|
let mut pages_in_sections = vec![]; |
|
|
@@ -115,17 +83,6 @@ impl Site { |
|
|
|
orphans |
|
|
|
} |
|
|
|
|
|
|
|
/// Finds the section that contains the page given if there is one |
|
|
|
pub fn find_parent_section(&self, page: &Page) -> Option<&Section> { |
|
|
|
for section in self.sections.values() { |
|
|
|
if section.is_child_page(page) { |
|
|
|
return Some(section) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
None |
|
|
|
} |
|
|
|
|
|
|
|
/// Used by tests to change the output path to a tmp dir |
|
|
|
#[doc(hidden)] |
|
|
|
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) { |
|
|
@@ -251,24 +208,23 @@ impl Site { |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// Separated from `parse` for easier testing |
|
|
|
/// Find all the tags and categories if it's asked in the config |
|
|
|
pub fn populate_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()); |
|
|
|
} |
|
|
|
let generate_tags_pages = self.config.generate_tags_pages.unwrap(); |
|
|
|
let generate_categories_pages = self.config.generate_categories_pages.unwrap(); |
|
|
|
if !generate_tags_pages && !generate_categories_pages { |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
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()); |
|
|
|
} |
|
|
|
} |
|
|
|
// TODO: can we pass a reference? |
|
|
|
let (tags, categories) = Taxonomy::find_tags_and_categories( |
|
|
|
self.pages.values().cloned().collect::<Vec<_>>() |
|
|
|
); |
|
|
|
if generate_tags_pages { |
|
|
|
self.tags = Some(tags); |
|
|
|
} |
|
|
|
if generate_categories_pages { |
|
|
|
self.categories = Some(categories); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
@@ -284,14 +240,6 @@ impl Site { |
|
|
|
html |
|
|
|
} |
|
|
|
|
|
|
|
fn ensure_public_directory_exists(&self) -> Result<()> { |
|
|
|
let public = self.output_path.clone(); |
|
|
|
if !public.exists() { |
|
|
|
create_directory(&public)?; |
|
|
|
} |
|
|
|
Ok(()) |
|
|
|
} |
|
|
|
|
|
|
|
/// Copy static file to public directory. |
|
|
|
pub fn copy_static_file<P: AsRef<Path>>(&self, path: P) -> Result<()> { |
|
|
|
let relative_path = path.as_ref().strip_prefix(&self.static_path).unwrap(); |
|
|
@@ -333,7 +281,7 @@ impl Site { |
|
|
|
|
|
|
|
/// Renders a single content page |
|
|
|
pub fn render_page(&self, page: &Page) -> Result<()> { |
|
|
|
self.ensure_public_directory_exists()?; |
|
|
|
ensure_directory_exists(&self.output_path)?; |
|
|
|
|
|
|
|
// Copy the nesting of the content directory if we have sections for that page |
|
|
|
let mut current_path = self.output_path.to_path_buf(); |
|
|
@@ -362,7 +310,7 @@ impl Site { |
|
|
|
Ok(()) |
|
|
|
} |
|
|
|
|
|
|
|
/// Builds the site to the `public` directory after deleting it |
|
|
|
/// Deletes the `public` directory and builds the site |
|
|
|
pub fn build(&self) -> Result<()> { |
|
|
|
self.clean()?; |
|
|
|
self.render_sections()?; |
|
|
@@ -382,98 +330,45 @@ impl Site { |
|
|
|
|
|
|
|
/// Renders robots.txt |
|
|
|
pub fn render_robots(&self) -> Result<()> { |
|
|
|
self.ensure_public_directory_exists()?; |
|
|
|
ensure_directory_exists(&self.output_path)?; |
|
|
|
create_file( |
|
|
|
self.output_path.join("robots.txt"), |
|
|
|
&self.tera.render("robots.txt", &Context::new())? |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
/// Renders all categories if the config allows it |
|
|
|
/// Renders all categories and the single category pages if there are some |
|
|
|
pub fn render_categories(&self) -> Result<()> { |
|
|
|
if self.config.generate_categories_pages.unwrap() { |
|
|
|
self.render_categories_and_tags(RenderList::Categories) |
|
|
|
} else { |
|
|
|
Ok(()) |
|
|
|
if let Some(ref categories) = self.categories { |
|
|
|
self.render_taxonomy(categories)?; |
|
|
|
} |
|
|
|
|
|
|
|
Ok(()) |
|
|
|
} |
|
|
|
|
|
|
|
/// Renders all tags if the config allows it |
|
|
|
/// Renders all tags and the single tag pages if there are some |
|
|
|
pub fn render_tags(&self) -> Result<()> { |
|
|
|
if self.config.generate_tags_pages.unwrap() { |
|
|
|
self.render_categories_and_tags(RenderList::Tags) |
|
|
|
} else { |
|
|
|
Ok(()) |
|
|
|
if let Some(ref tags) = self.tags { |
|
|
|
self.render_taxonomy(tags)?; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// Render the /{categories, list} pages and each individual category/tag page |
|
|
|
/// They are the same thing fundamentally, a list of pages with something in common |
|
|
|
/// TODO: revisit this function, lots of things have changed since then |
|
|
|
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(()); |
|
|
|
} |
|
|
|
Ok(()) |
|
|
|
} |
|
|
|
|
|
|
|
let (list_tpl_name, single_tpl_name, name, var_name) = if kind == RenderList::Categories { |
|
|
|
("categories.html", "category.html", "categories", "category") |
|
|
|
} else { |
|
|
|
("tags.html", "tag.html", "tags", "tag") |
|
|
|
}; |
|
|
|
self.ensure_public_directory_exists()?; |
|
|
|
fn render_taxonomy(&self, taxonomy: &Taxonomy) -> Result<()> { |
|
|
|
ensure_directory_exists(&self.output_path)?; |
|
|
|
|
|
|
|
// Create the categories/tags directory first |
|
|
|
let public = self.output_path.clone(); |
|
|
|
let mut output_path = public.to_path_buf(); |
|
|
|
output_path.push(name); |
|
|
|
let output_path = self.output_path.join(&taxonomy.get_list_name()); |
|
|
|
let list_output = taxonomy.render_list(&self.tera, &self.config)?; |
|
|
|
create_directory(&output_path)?; |
|
|
|
|
|
|
|
// 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_items.sort_by(|a, b| b.count.cmp(&a.count)); |
|
|
|
let mut context = Context::new(); |
|
|
|
context.add(name, &sorted_items); |
|
|
|
context.add("config", &self.config); |
|
|
|
context.add("current_url", &self.config.make_permalink(name)); |
|
|
|
context.add("current_path", &format!("/{}", name)); |
|
|
|
// And render it immediately |
|
|
|
let list_output = self.tera.render(list_tpl_name, &context)?; |
|
|
|
create_file(output_path.join("index.html"), &self.inject_livereload(list_output))?; |
|
|
|
|
|
|
|
// Now, each individual item |
|
|
|
for (item_name, pages_paths) in items.iter() { |
|
|
|
let pages: Vec<&Page> = self.pages |
|
|
|
.iter() |
|
|
|
.filter(|&(path, _)| pages_paths.contains(path)) |
|
|
|
.map(|(_, page)| page) |
|
|
|
.collect(); |
|
|
|
// TODO: how to sort categories and tag content? |
|
|
|
// Have a setting in config.toml or a _category.md and _tag.md |
|
|
|
// The latter is more in line with the rest of Gutenberg but order ordering |
|
|
|
// doesn't really work across sections. |
|
|
|
|
|
|
|
let mut context = Context::new(); |
|
|
|
let slug = slugify(&item_name); |
|
|
|
context.add(var_name, &item_name); |
|
|
|
context.add(&format!("{}_slug", var_name), &slug); |
|
|
|
context.add("pages", &pages); |
|
|
|
context.add("config", &self.config); |
|
|
|
context.add("current_url", &self.config.make_permalink(&format!("{}/{}", name, slug))); |
|
|
|
context.add("current_path", &format!("/{}/{}", name, slug)); |
|
|
|
let single_output = self.tera.render(single_tpl_name, &context)?; |
|
|
|
|
|
|
|
create_directory(&output_path.join(&slug))?; |
|
|
|
for item in &taxonomy.items { |
|
|
|
let single_output = taxonomy.render_single_item(item, &self.tera, &self.config)?; |
|
|
|
|
|
|
|
create_directory(&output_path.join(&item.slug))?; |
|
|
|
create_file( |
|
|
|
output_path.join(&slug).join("index.html"), |
|
|
|
output_path.join(&item.slug).join("index.html"), |
|
|
|
&self.inject_livereload(single_output) |
|
|
|
)?; |
|
|
|
} |
|
|
@@ -483,28 +378,31 @@ impl Site { |
|
|
|
|
|
|
|
/// What it says on the tin |
|
|
|
pub fn render_sitemap(&self) -> Result<()> { |
|
|
|
self.ensure_public_directory_exists()?; |
|
|
|
ensure_directory_exists(&self.output_path)?; |
|
|
|
|
|
|
|
let mut context = Context::new(); |
|
|
|
context.add("pages", &self.pages.values().collect::<Vec<&Page>>()); |
|
|
|
context.add("sections", &self.sections.values().collect::<Vec<&Section>>()); |
|
|
|
|
|
|
|
let mut categories = vec![]; |
|
|
|
if self.config.generate_categories_pages.unwrap() && !self.categories.is_empty() { |
|
|
|
categories.push(self.config.make_permalink("categories")); |
|
|
|
for category in self.categories.keys() { |
|
|
|
if let Some(ref c) = self.categories { |
|
|
|
let name = c.get_list_name(); |
|
|
|
categories.push(self.config.make_permalink(&name)); |
|
|
|
for item in &c.items { |
|
|
|
categories.push( |
|
|
|
self.config.make_permalink(&format!("categories/{}", slugify(category))) |
|
|
|
self.config.make_permalink(&format!("{}/{}", &name, item.slug)) |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
|
context.add("categories", &categories); |
|
|
|
|
|
|
|
let mut tags = vec![]; |
|
|
|
if self.config.generate_tags_pages.unwrap() && !self.tags.is_empty() { |
|
|
|
tags.push(self.config.make_permalink("tags")); |
|
|
|
for tag in self.tags.keys() { |
|
|
|
if let Some(ref t) = self.tags { |
|
|
|
let name = t.get_list_name(); |
|
|
|
tags.push(self.config.make_permalink(&name)); |
|
|
|
for item in &t.items { |
|
|
|
tags.push( |
|
|
|
self.config.make_permalink(&format!("tags/{}", slugify(tag))) |
|
|
|
self.config.make_permalink(&format!("{}/{}", &name, item.slug)) |
|
|
|
); |
|
|
|
} |
|
|
|
} |
|
|
@@ -518,7 +416,7 @@ impl Site { |
|
|
|
} |
|
|
|
|
|
|
|
pub fn render_rss_feed(&self) -> Result<()> { |
|
|
|
self.ensure_public_directory_exists()?; |
|
|
|
ensure_directory_exists(&self.output_path)?; |
|
|
|
|
|
|
|
let mut context = Context::new(); |
|
|
|
let pages = self.pages.values() |
|
|
@@ -561,7 +459,7 @@ impl Site { |
|
|
|
|
|
|
|
/// Renders a single section |
|
|
|
pub fn render_section(&self, section: &Section, render_pages: bool) -> Result<()> { |
|
|
|
self.ensure_public_directory_exists()?; |
|
|
|
ensure_directory_exists(&self.output_path)?; |
|
|
|
let public = self.output_path.clone(); |
|
|
|
|
|
|
|
let mut output_path = public.to_path_buf(); |
|
|
@@ -611,7 +509,7 @@ impl Site { |
|
|
|
|
|
|
|
/// Renders all pages that do not belong to any sections |
|
|
|
pub fn render_orphan_pages(&self) -> Result<()> { |
|
|
|
self.ensure_public_directory_exists()?; |
|
|
|
ensure_directory_exists(&self.output_path)?; |
|
|
|
|
|
|
|
for page in self.get_all_orphan_pages() { |
|
|
|
self.render_page(page)?; |
|
|
@@ -622,7 +520,7 @@ impl Site { |
|
|
|
|
|
|
|
/// Renders a list of pages when the section/index is wanting pagination. |
|
|
|
fn render_paginated(&self, output_path: &Path, section: &Section) -> Result<()> { |
|
|
|
self.ensure_public_directory_exists()?; |
|
|
|
ensure_directory_exists(&self.output_path)?; |
|
|
|
|
|
|
|
let paginate_path = match section.meta.paginate_path { |
|
|
|
Some(ref s) => s.clone(), |
|
|
|