2020-07-25 08:30:55 +00:00
|
|
|
pub mod feed;
|
2020-07-24 21:44:00 +00:00
|
|
|
pub mod link_checking;
|
2020-07-24 21:00:00 +00:00
|
|
|
pub mod sass;
|
2019-11-26 19:36:52 +00:00
|
|
|
pub mod sitemap;
|
2020-07-24 21:44:00 +00:00
|
|
|
pub mod tpls;
|
2019-03-19 19:42:16 +00:00
|
|
|
|
2019-06-02 18:21:06 +00:00
|
|
|
use std::collections::HashMap;
|
2020-07-24 21:00:00 +00:00
|
|
|
use std::fs::{copy, remove_dir_all};
|
2017-03-14 12:25:45 +00:00
|
|
|
use std::path::{Path, PathBuf};
|
2019-01-27 17:57:07 +00:00
|
|
|
use std::sync::{Arc, Mutex, RwLock};
|
2017-03-03 08:12:40 +00:00
|
|
|
|
|
|
|
use glob::glob;
|
2018-10-02 14:42:34 +00:00
|
|
|
use rayon::prelude::*;
|
2018-10-31 07:18:57 +00:00
|
|
|
use tera::{Context, Tera};
|
2017-07-06 13:19:15 +00:00
|
|
|
|
2020-07-25 08:30:55 +00:00
|
|
|
use config::{get_config, Config};
|
2020-07-24 21:16:21 +00:00
|
|
|
use errors::{bail, Error, Result};
|
2018-10-31 07:18:57 +00:00
|
|
|
use front_matter::InsertAnchor;
|
2020-07-25 08:30:55 +00:00
|
|
|
use library::{find_taxonomies, Library, Page, Paginator, Section, Taxonomy};
|
2020-07-24 21:44:00 +00:00
|
|
|
use templates::render_redirect_template;
|
2018-10-31 07:18:57 +00:00
|
|
|
use utils::fs::{copy_directory, create_directory, create_file, ensure_directory_exists};
|
2018-05-11 11:54:16 +00:00
|
|
|
use utils::net::get_available_port;
|
2020-07-24 21:44:00 +00:00
|
|
|
use utils::templates::render_template;
|
2017-07-04 23:27:27 +00:00
|
|
|
|
2017-03-03 08:12:40 +00:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub struct Site {
|
2018-10-18 20:50:06 +00:00
|
|
|
/// The base path of the zola site
|
2017-03-14 12:25:45 +00:00
|
|
|
pub base_path: PathBuf,
|
2017-05-16 04:37:00 +00:00
|
|
|
/// The parsed config for the site
|
2017-03-14 12:25:45 +00:00
|
|
|
pub config: Config,
|
2017-03-27 14:17:33 +00:00
|
|
|
pub tera: Tera,
|
2018-02-02 20:35:04 +00:00
|
|
|
imageproc: Arc<Mutex<imageproc::Processor>>,
|
2018-05-11 11:54:16 +00:00
|
|
|
// the live reload port to be used if there is one
|
|
|
|
pub live_reload: Option<u16>,
|
2018-03-14 21:03:06 +00:00
|
|
|
pub output_path: PathBuf,
|
2018-02-02 20:35:04 +00:00
|
|
|
content_path: PathBuf,
|
2017-08-23 10:17:24 +00:00
|
|
|
pub static_path: PathBuf,
|
2018-07-16 08:54:05 +00:00
|
|
|
pub taxonomies: Vec<Taxonomy>,
|
2017-05-13 13:37:01 +00:00
|
|
|
/// 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
|
2017-04-21 07:21:44 +00:00
|
|
|
pub permalinks: HashMap<String, String>,
|
2018-10-02 14:42:34 +00:00
|
|
|
/// Contains all pages and sections of the site
|
2019-01-27 17:57:07 +00:00
|
|
|
pub library: Arc<RwLock<Library>>,
|
2019-08-24 20:23:08 +00:00
|
|
|
/// Whether to load draft pages
|
|
|
|
include_drafts: bool,
|
2017-03-03 08:12:40 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl Site {
|
2017-03-14 12:25:45 +00:00
|
|
|
/// Parse a site at the given path. Defaults to the current dir
|
2020-01-21 19:52:24 +00:00
|
|
|
/// Passing in a path is used in tests and when --root argument is passed
|
2020-05-23 09:55:45 +00:00
|
|
|
pub fn new<P: AsRef<Path>, P2: AsRef<Path>>(path: P, config_file: P2) -> Result<Site> {
|
2017-03-14 12:25:45 +00:00
|
|
|
let path = path.as_ref();
|
2020-05-23 09:55:45 +00:00
|
|
|
let config_file = config_file.as_ref();
|
|
|
|
let mut config = get_config(config_file);
|
2018-10-09 12:33:43 +00:00
|
|
|
config.load_extra_syntaxes(path)?;
|
2017-03-14 12:25:45 +00:00
|
|
|
|
2017-08-24 23:38:03 +00:00
|
|
|
if let Some(theme) = config.theme.clone() {
|
|
|
|
// Grab data from the extra section of the theme
|
|
|
|
config.merge_with_theme(&path.join("themes").join(&theme).join("theme.toml"))?;
|
2017-08-23 10:17:24 +00:00
|
|
|
}
|
|
|
|
|
2020-07-25 08:49:07 +00:00
|
|
|
let tera = tpls::load_tera(path, &config)?;
|
2018-09-30 17:05:56 +00:00
|
|
|
|
2018-02-02 20:35:04 +00:00
|
|
|
let content_path = path.join("content");
|
|
|
|
let static_path = path.join("static");
|
2018-10-31 07:18:57 +00:00
|
|
|
let imageproc =
|
|
|
|
imageproc::Processor::new(content_path.clone(), &static_path, &config.base_url);
|
2018-02-02 20:35:04 +00:00
|
|
|
|
2017-03-20 10:00:00 +00:00
|
|
|
let site = Site {
|
2017-03-14 12:25:45 +00:00
|
|
|
base_path: path.to_path_buf(),
|
2018-03-28 15:01:14 +00:00
|
|
|
config,
|
|
|
|
tera,
|
2018-02-02 20:35:04 +00:00
|
|
|
imageproc: Arc::new(Mutex::new(imageproc)),
|
2018-05-11 11:54:16 +00:00
|
|
|
live_reload: None,
|
2017-04-18 05:07:02 +00:00
|
|
|
output_path: path.join("public"),
|
2018-02-02 20:35:04 +00:00
|
|
|
content_path,
|
|
|
|
static_path,
|
2018-07-16 08:54:05 +00:00
|
|
|
taxonomies: Vec::new(),
|
2017-04-21 07:21:44 +00:00
|
|
|
permalinks: HashMap::new(),
|
2019-08-24 20:23:08 +00:00
|
|
|
include_drafts: false,
|
2018-10-02 14:42:34 +00:00
|
|
|
// We will allocate it properly later on
|
2019-01-27 17:57:07 +00:00
|
|
|
library: Arc::new(RwLock::new(Library::new(0, 0, false))),
|
2017-03-03 08:12:40 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
Ok(site)
|
|
|
|
}
|
|
|
|
|
2019-08-24 20:23:08 +00:00
|
|
|
/// Set the site to load the drafts.
|
|
|
|
/// Needs to be called before loading it
|
|
|
|
pub fn include_drafts(&mut self) {
|
|
|
|
self.include_drafts = true;
|
|
|
|
}
|
|
|
|
|
2018-12-28 11:15:17 +00:00
|
|
|
/// The index sections are ALWAYS at those paths
|
2020-07-24 21:44:00 +00:00
|
|
|
/// There are one index section for the default language + 1 per language
|
2018-12-28 11:15:17 +00:00
|
|
|
fn index_section_paths(&self) -> Vec<(PathBuf, Option<String>)> {
|
|
|
|
let mut res = vec![(self.content_path.join("_index.md"), None)];
|
|
|
|
for language in &self.config.languages {
|
2018-12-29 10:17:43 +00:00
|
|
|
res.push((
|
|
|
|
self.content_path.join(format!("_index.{}.md", language.code)),
|
|
|
|
Some(language.code.clone()),
|
|
|
|
));
|
2018-12-28 11:15:17 +00:00
|
|
|
}
|
|
|
|
res
|
2018-03-14 21:03:06 +00:00
|
|
|
}
|
|
|
|
|
2018-10-19 14:33:11 +00:00
|
|
|
/// We avoid the port the server is going to use as it's not bound yet
|
|
|
|
/// when calling this function and we could end up having tried to bind
|
|
|
|
/// both http and websocket server to the same port
|
|
|
|
pub fn enable_live_reload(&mut self, port_to_avoid: u16) {
|
|
|
|
self.live_reload = get_available_port(port_to_avoid);
|
2017-03-14 12:25:45 +00:00
|
|
|
}
|
|
|
|
|
2018-02-02 20:35:04 +00:00
|
|
|
pub fn set_base_url(&mut self, base_url: String) {
|
2019-01-04 19:31:31 +00:00
|
|
|
let mut imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (set_base_url)");
|
2018-02-02 20:35:04 +00:00
|
|
|
imageproc.set_base_url(&base_url);
|
|
|
|
self.config.base_url = base_url;
|
|
|
|
}
|
|
|
|
|
2017-03-14 12:25:45 +00:00
|
|
|
pub fn set_output_path<P: AsRef<Path>>(&mut self, path: P) {
|
|
|
|
self.output_path = path.as_ref().to_path_buf();
|
|
|
|
}
|
|
|
|
|
2017-03-21 07:57:00 +00:00
|
|
|
/// Reads all .md files in the `content` directory and create pages/sections
|
2017-03-03 08:12:40 +00:00
|
|
|
/// out of them
|
2017-03-21 07:57:00 +00:00
|
|
|
pub fn load(&mut self) -> Result<()> {
|
2017-04-22 04:58:22 +00:00
|
|
|
let base_path = self.base_path.to_string_lossy().replace("\\", "/");
|
|
|
|
let content_glob = format!("{}/{}", base_path, "content/**/*.md");
|
2017-03-14 12:25:45 +00:00
|
|
|
|
2017-06-22 03:01:45 +00:00
|
|
|
let (section_entries, page_entries): (Vec<_>, Vec<_>) = glob(&content_glob)
|
2019-01-04 19:31:31 +00:00
|
|
|
.expect("Invalid glob")
|
2017-06-22 03:01:45 +00:00
|
|
|
.filter_map(|e| e.ok())
|
2018-09-30 19:15:09 +00:00
|
|
|
.filter(|e| !e.as_path().file_name().unwrap().to_str().unwrap().starts_with('.'))
|
2018-12-29 10:17:43 +00:00
|
|
|
.partition(|entry| {
|
|
|
|
entry.as_path().file_name().unwrap().to_str().unwrap().starts_with("_index.")
|
|
|
|
});
|
2017-06-22 03:01:45 +00:00
|
|
|
|
2019-02-09 18:54:46 +00:00
|
|
|
self.library = Arc::new(RwLock::new(Library::new(
|
|
|
|
page_entries.len(),
|
|
|
|
section_entries.len(),
|
|
|
|
self.config.is_multilingual(),
|
|
|
|
)));
|
2018-10-02 14:42:34 +00:00
|
|
|
|
2017-06-22 03:01:45 +00:00
|
|
|
let sections = {
|
|
|
|
let config = &self.config;
|
|
|
|
|
|
|
|
section_entries
|
|
|
|
.into_par_iter()
|
|
|
|
.map(|entry| {
|
|
|
|
let path = entry.as_path();
|
2019-03-08 22:26:57 +00:00
|
|
|
Section::from_file(path, config, &self.base_path)
|
2017-09-27 14:37:17 +00:00
|
|
|
})
|
|
|
|
.collect::<Vec<_>>()
|
2017-06-22 03:01:45 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
let pages = {
|
|
|
|
let config = &self.config;
|
|
|
|
|
|
|
|
page_entries
|
|
|
|
.into_par_iter()
|
2019-08-10 16:53:16 +00:00
|
|
|
.filter(|entry| match &config.ignored_content_globset {
|
|
|
|
Some(gs) => !gs.is_match(entry.as_path()),
|
|
|
|
None => true,
|
2019-08-01 08:18:42 +00:00
|
|
|
})
|
2017-06-22 03:01:45 +00:00
|
|
|
.map(|entry| {
|
|
|
|
let path = entry.as_path();
|
2019-03-08 22:26:57 +00:00
|
|
|
Page::from_file(path, config, &self.base_path)
|
2017-09-27 14:37:17 +00:00
|
|
|
})
|
|
|
|
.collect::<Vec<_>>()
|
2017-06-22 03:01:45 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Kinda duplicated code for add_section/add_page but necessary to do it that
|
|
|
|
// way because of the borrow checker
|
|
|
|
for section in sections {
|
|
|
|
let s = section?;
|
|
|
|
self.add_section(s, false)?;
|
|
|
|
}
|
|
|
|
|
2019-02-22 20:48:30 +00:00
|
|
|
self.create_default_index_sections()?;
|
|
|
|
|
|
|
|
let mut pages_insert_anchors = HashMap::new();
|
|
|
|
for page in pages {
|
|
|
|
let p = page?;
|
2019-08-24 20:23:08 +00:00
|
|
|
// Should draft pages be ignored?
|
|
|
|
if p.meta.draft && !self.include_drafts {
|
2019-07-12 20:45:08 +00:00
|
|
|
continue;
|
|
|
|
}
|
2019-02-22 20:48:30 +00:00
|
|
|
pages_insert_anchors.insert(
|
|
|
|
p.file.path.clone(),
|
|
|
|
self.find_parent_section_insert_anchor(&p.file.parent.clone(), &p.lang),
|
|
|
|
);
|
|
|
|
self.add_page(p, false)?;
|
|
|
|
}
|
|
|
|
|
2019-12-01 17:03:24 +00:00
|
|
|
{
|
|
|
|
let library = self.library.read().unwrap();
|
|
|
|
let collisions = library.check_for_path_collisions();
|
|
|
|
if !collisions.is_empty() {
|
|
|
|
return Err(Error::from_collisions(collisions));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-03-16 09:01:11 +00:00
|
|
|
// taxonomy Tera fns are loaded in `register_early_global_fns`
|
|
|
|
// so we do need to populate it first.
|
|
|
|
self.populate_taxonomies()?;
|
2019-02-22 20:48:30 +00:00
|
|
|
self.register_early_global_fns();
|
|
|
|
self.populate_sections();
|
|
|
|
self.render_markdown()?;
|
|
|
|
self.register_tera_global_fns();
|
|
|
|
|
2019-06-06 17:49:31 +00:00
|
|
|
// Needs to be done after rendering markdown as we only get the anchors at that point
|
2020-07-25 08:49:07 +00:00
|
|
|
link_checking::check_internal_links_with_anchors(&self)?;
|
2019-06-06 17:49:31 +00:00
|
|
|
|
2019-07-12 21:09:05 +00:00
|
|
|
if self.config.is_in_check_mode() {
|
2020-07-25 08:49:07 +00:00
|
|
|
link_checking::check_external_links(&self)?;
|
2019-05-27 12:05:07 +00:00
|
|
|
}
|
|
|
|
|
2019-02-22 20:48:30 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Insert a default index section for each language if necessary so we don't need to create
|
|
|
|
/// a _index.md to render the index page at the root of the site
|
|
|
|
pub fn create_default_index_sections(&mut self) -> Result<()> {
|
2018-12-28 11:15:17 +00:00
|
|
|
for (index_path, lang) in self.index_section_paths() {
|
2019-01-27 17:57:07 +00:00
|
|
|
if let Some(ref index_section) = self.library.read().unwrap().get_section(&index_path) {
|
2018-12-28 11:15:17 +00:00
|
|
|
if self.config.build_search_index && !index_section.meta.in_search_index {
|
|
|
|
bail!(
|
2018-03-14 21:03:06 +00:00
|
|
|
"You have enabled search in the config but disabled it in the index section: \
|
|
|
|
either turn off the search in the config or remote `in_search_index = true` from the \
|
|
|
|
section front-matter."
|
2018-12-28 11:15:17 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2019-01-27 17:57:07 +00:00
|
|
|
let mut library = self.library.write().expect("Get lock for load");
|
2018-12-28 11:15:17 +00:00
|
|
|
// Not in else because of borrow checker
|
2019-01-27 17:57:07 +00:00
|
|
|
if !library.contains_section(&index_path) {
|
2018-12-28 11:15:17 +00:00
|
|
|
let mut index_section = Section::default();
|
|
|
|
index_section.file.parent = self.content_path.clone();
|
2018-12-29 10:17:43 +00:00
|
|
|
index_section.file.filename =
|
|
|
|
index_path.file_name().unwrap().to_string_lossy().to_string();
|
2018-12-28 11:15:17 +00:00
|
|
|
if let Some(ref l) = lang {
|
2019-01-04 20:57:27 +00:00
|
|
|
index_section.file.name = format!("_index.{}", l);
|
2019-12-01 17:03:24 +00:00
|
|
|
index_section.path = format!("{}/", l);
|
2018-12-28 11:15:17 +00:00
|
|
|
index_section.permalink = self.config.make_permalink(l);
|
|
|
|
let filename = format!("_index.{}.md", l);
|
|
|
|
index_section.file.path = self.content_path.join(&filename);
|
|
|
|
index_section.file.relative = filename;
|
|
|
|
} else {
|
2019-01-04 20:57:27 +00:00
|
|
|
index_section.file.name = "_index".to_string();
|
2018-12-28 11:15:17 +00:00
|
|
|
index_section.permalink = self.config.make_permalink("");
|
|
|
|
index_section.file.path = self.content_path.join("_index.md");
|
|
|
|
index_section.file.relative = "_index.md".to_string();
|
2020-05-23 09:41:50 +00:00
|
|
|
index_section.path = "/".to_string();
|
2018-12-28 11:15:17 +00:00
|
|
|
}
|
2020-04-12 17:17:29 +00:00
|
|
|
index_section.lang = index_section.file.find_language(&self.config)?;
|
2019-01-27 17:57:07 +00:00
|
|
|
library.insert_section(index_section);
|
2018-03-14 21:03:06 +00:00
|
|
|
}
|
2017-05-12 07:30:01 +00:00
|
|
|
}
|
2017-03-21 07:57:00 +00:00
|
|
|
|
2018-01-09 20:57:29 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Render the markdown of all pages/sections
|
|
|
|
/// Used in a build and in `serve` if a shortcode has changed
|
|
|
|
pub fn render_markdown(&mut self) -> Result<()> {
|
|
|
|
// Another silly thing needed to not borrow &self in parallel and
|
|
|
|
// make the borrow checker happy
|
|
|
|
let permalinks = &self.permalinks;
|
|
|
|
let tera = &self.tera;
|
|
|
|
let config = &self.config;
|
|
|
|
|
|
|
|
// This is needed in the first place because of silly borrow checker
|
|
|
|
let mut pages_insert_anchors = HashMap::new();
|
2019-01-27 17:57:07 +00:00
|
|
|
for (_, p) in self.library.read().unwrap().pages() {
|
2018-10-31 07:18:57 +00:00
|
|
|
pages_insert_anchors.insert(
|
|
|
|
p.file.path.clone(),
|
2018-12-28 11:15:17 +00:00
|
|
|
self.find_parent_section_insert_anchor(&p.file.parent.clone(), &p.lang),
|
2018-10-31 07:18:57 +00:00
|
|
|
);
|
2017-05-12 09:05:00 +00:00
|
|
|
}
|
|
|
|
|
2019-01-27 17:57:07 +00:00
|
|
|
let mut library = self.library.write().expect("Get lock for render_markdown");
|
|
|
|
library
|
2018-10-02 14:42:34 +00:00
|
|
|
.pages_mut()
|
|
|
|
.values_mut()
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
.par_iter_mut()
|
|
|
|
.map(|page| {
|
2018-01-09 20:57:29 +00:00
|
|
|
let insert_anchor = pages_insert_anchors[&page.file.path];
|
2018-10-09 12:33:43 +00:00
|
|
|
page.render_markdown(permalinks, tera, config, insert_anchor)
|
2018-01-09 20:57:29 +00:00
|
|
|
})
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()?;
|
2017-03-21 07:57:00 +00:00
|
|
|
|
2019-01-27 17:57:07 +00:00
|
|
|
library
|
2018-10-02 14:42:34 +00:00
|
|
|
.sections_mut()
|
|
|
|
.values_mut()
|
|
|
|
.collect::<Vec<_>>()
|
|
|
|
.par_iter_mut()
|
2018-10-09 12:33:43 +00:00
|
|
|
.map(|section| section.render_markdown(permalinks, tera, config))
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()?;
|
2017-05-03 14:16:09 +00:00
|
|
|
|
2017-03-21 07:57:00 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-07-24 21:44:00 +00:00
|
|
|
// TODO: remove me in favour of the direct call to the fn once rebuild has changed
|
2018-02-02 20:35:04 +00:00
|
|
|
pub fn register_early_global_fns(&mut self) {
|
2020-07-25 08:49:07 +00:00
|
|
|
tpls::register_early_global_fns(self);
|
2018-02-02 20:35:04 +00:00
|
|
|
}
|
|
|
|
|
2020-07-24 21:44:00 +00:00
|
|
|
// TODO: remove me in favour of the direct call to the fn once rebuild has changed
|
2017-09-26 08:25:55 +00:00
|
|
|
pub fn register_tera_global_fns(&mut self) {
|
2020-07-25 08:49:07 +00:00
|
|
|
tpls::register_tera_global_fns(self);
|
2017-05-17 10:04:26 +00:00
|
|
|
}
|
|
|
|
|
2017-05-13 13:37:01 +00:00
|
|
|
/// Add a page to the site
|
|
|
|
/// The `render` parameter is used in the serve command, when rebuilding a page.
|
|
|
|
/// If `true`, it will also render the markdown for that page
|
2018-01-29 17:40:12 +00:00
|
|
|
/// Returns the previous page struct if there was one at the same path
|
2018-10-02 14:42:34 +00:00
|
|
|
pub fn add_page(&mut self, mut page: Page, render: bool) -> Result<Option<Page>> {
|
2017-05-15 10:53:39 +00:00
|
|
|
self.permalinks.insert(page.file.relative.clone(), page.permalink.clone());
|
2017-05-13 13:37:01 +00:00
|
|
|
if render {
|
2018-12-29 10:17:43 +00:00
|
|
|
let insert_anchor =
|
|
|
|
self.find_parent_section_insert_anchor(&page.file.parent, &page.lang);
|
2018-10-09 12:33:43 +00:00
|
|
|
page.render_markdown(&self.permalinks, &self.tera, &self.config, insert_anchor)?;
|
2017-05-13 13:37:01 +00:00
|
|
|
}
|
2019-01-27 17:57:07 +00:00
|
|
|
let mut library = self.library.write().expect("Get lock for add_page");
|
|
|
|
let prev = library.remove_page(&page.file.path);
|
|
|
|
library.insert_page(page);
|
2017-05-12 11:24:44 +00:00
|
|
|
|
2017-05-13 13:37:01 +00:00
|
|
|
Ok(prev)
|
2017-03-21 07:57:00 +00:00
|
|
|
}
|
|
|
|
|
2017-05-13 13:37:01 +00:00
|
|
|
/// Add a section to the site
|
|
|
|
/// The `render` parameter is used in the serve command, when rebuilding a page.
|
|
|
|
/// If `true`, it will also render the markdown for that page
|
2018-01-29 17:40:12 +00:00
|
|
|
/// Returns the previous section struct if there was one at the same path
|
2018-10-02 14:42:34 +00:00
|
|
|
pub fn add_section(&mut self, mut section: Section, render: bool) -> Result<Option<Section>> {
|
2017-05-15 10:53:39 +00:00
|
|
|
self.permalinks.insert(section.file.relative.clone(), section.permalink.clone());
|
2017-05-13 13:37:01 +00:00
|
|
|
if render {
|
2018-10-09 12:33:43 +00:00
|
|
|
section.render_markdown(&self.permalinks, &self.tera, &self.config)?;
|
2017-05-12 09:05:00 +00:00
|
|
|
}
|
2019-01-27 17:57:07 +00:00
|
|
|
let mut library = self.library.write().expect("Get lock for add_section");
|
|
|
|
let prev = library.remove_section(§ion.file.path);
|
|
|
|
library.insert_section(section);
|
2017-05-13 13:37:01 +00:00
|
|
|
|
|
|
|
Ok(prev)
|
2017-04-21 07:21:44 +00:00
|
|
|
}
|
|
|
|
|
2017-05-22 11:28:43 +00:00
|
|
|
/// Finds the insert_anchor for the parent section of the directory at `path`.
|
|
|
|
/// Defaults to `AnchorInsert::None` if no parent section found
|
2018-12-29 10:17:43 +00:00
|
|
|
pub fn find_parent_section_insert_anchor(
|
|
|
|
&self,
|
|
|
|
parent_path: &PathBuf,
|
2019-01-29 18:20:03 +00:00
|
|
|
lang: &str,
|
2018-12-29 10:17:43 +00:00
|
|
|
) -> InsertAnchor {
|
2019-01-29 18:20:03 +00:00
|
|
|
let parent = if lang != self.config.default_language {
|
|
|
|
parent_path.join(format!("_index.{}.md", lang))
|
2018-12-28 11:15:17 +00:00
|
|
|
} else {
|
|
|
|
parent_path.join("_index.md")
|
|
|
|
};
|
2019-01-27 17:57:07 +00:00
|
|
|
match self.library.read().unwrap().get_section(&parent) {
|
2018-03-14 17:22:24 +00:00
|
|
|
Some(s) => s.meta.insert_anchor_links,
|
2018-10-31 07:18:57 +00:00
|
|
|
None => InsertAnchor::None,
|
2017-05-22 11:28:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-03-21 07:57:00 +00:00
|
|
|
/// Find out the direct subsections of each subsection if there are some
|
|
|
|
/// as well as the pages for each section
|
2017-05-12 11:24:44 +00:00
|
|
|
pub fn populate_sections(&mut self) {
|
2019-01-27 17:57:07 +00:00
|
|
|
let mut library = self.library.write().expect("Get lock for populate_sections");
|
2019-01-29 18:20:03 +00:00
|
|
|
library.populate_sections(&self.config);
|
2017-03-03 08:12:40 +00:00
|
|
|
}
|
|
|
|
|
2017-05-16 04:37:00 +00:00
|
|
|
/// Find all the tags and categories if it's asked in the config
|
2018-07-16 16:14:48 +00:00
|
|
|
pub fn populate_taxonomies(&mut self) -> Result<()> {
|
2018-07-16 08:54:05 +00:00
|
|
|
if self.config.taxonomies.is_empty() {
|
2018-07-16 16:14:48 +00:00
|
|
|
return Ok(());
|
2017-05-16 04:37:00 +00:00
|
|
|
}
|
2017-03-20 03:42:43 +00:00
|
|
|
|
2019-01-27 17:57:07 +00:00
|
|
|
self.taxonomies = find_taxonomies(&self.config, &self.library.read().unwrap())?;
|
2018-07-16 16:14:48 +00:00
|
|
|
|
|
|
|
Ok(())
|
2017-03-20 03:42:43 +00:00
|
|
|
}
|
|
|
|
|
2017-03-14 12:25:45 +00:00
|
|
|
/// Inject live reload script tag if in live reload mode
|
2020-03-29 10:45:51 +00:00
|
|
|
fn inject_livereload(&self, mut html: String) -> String {
|
2018-05-11 11:54:16 +00:00
|
|
|
if let Some(port) = self.live_reload {
|
2020-04-22 08:07:17 +00:00
|
|
|
let script =
|
|
|
|
format!(r#"<script src="/livereload.js?port={}&mindelay=10"></script>"#, port,);
|
2020-03-29 10:45:51 +00:00
|
|
|
if let Some(index) = html.rfind("</body>") {
|
|
|
|
html.insert_str(index, &script);
|
|
|
|
} else {
|
|
|
|
html.push_str(&script);
|
|
|
|
}
|
2017-03-06 10:35:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
html
|
|
|
|
}
|
|
|
|
|
2017-08-23 10:17:24 +00:00
|
|
|
/// Copy the main `static` folder and the theme `static` folder if a theme is used
|
|
|
|
pub fn copy_static_directories(&self) -> Result<()> {
|
|
|
|
// The user files will overwrite the theme files
|
|
|
|
if let Some(ref theme) = self.config.theme {
|
2018-03-14 21:03:06 +00:00
|
|
|
copy_directory(
|
|
|
|
&self.base_path.join("themes").join(theme).join("static"),
|
2018-07-31 13:17:31 +00:00
|
|
|
&self.output_path,
|
2019-07-19 09:10:28 +00:00
|
|
|
false,
|
2017-08-23 10:17:24 +00:00
|
|
|
)?;
|
|
|
|
}
|
2017-10-25 12:49:54 +00:00
|
|
|
// We're fine with missing static folders
|
|
|
|
if self.static_path.exists() {
|
2019-07-10 21:37:19 +00:00
|
|
|
copy_directory(&self.static_path, &self.output_path, self.config.hard_link_static)?;
|
2017-10-25 12:49:54 +00:00
|
|
|
}
|
2017-08-23 10:17:24 +00:00
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2018-02-02 20:35:04 +00:00
|
|
|
pub fn num_img_ops(&self) -> usize {
|
2019-01-04 19:31:31 +00:00
|
|
|
let imageproc = self.imageproc.lock().expect("Couldn't lock imageproc (num_img_ops)");
|
2018-02-02 20:35:04 +00:00
|
|
|
imageproc.num_img_ops()
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn process_images(&self) -> Result<()> {
|
2019-01-04 19:34:20 +00:00
|
|
|
let mut imageproc =
|
|
|
|
self.imageproc.lock().expect("Couldn't lock imageproc (process_images)");
|
2018-02-02 20:35:04 +00:00
|
|
|
imageproc.prune()?;
|
|
|
|
imageproc.do_process()
|
|
|
|
}
|
|
|
|
|
2017-03-10 11:39:58 +00:00
|
|
|
/// Deletes the `public` directory if it exists
|
|
|
|
pub fn clean(&self) -> Result<()> {
|
2017-04-18 05:07:02 +00:00
|
|
|
if self.output_path.exists() {
|
2017-03-10 11:39:58 +00:00
|
|
|
// Delete current `public` directory so we can start fresh
|
2019-02-09 18:54:46 +00:00
|
|
|
remove_dir_all(&self.output_path)
|
|
|
|
.map_err(|e| Error::chain("Couldn't delete output directory", e))?;
|
2017-03-10 11:39:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2017-05-03 08:52:49 +00:00
|
|
|
/// Renders a single content page
|
2017-06-29 12:14:08 +00:00
|
|
|
pub fn render_page(&self, page: &Page) -> Result<()> {
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2017-03-03 08:12:40 +00:00
|
|
|
|
2017-05-01 08:10:22 +00:00
|
|
|
// Copy the nesting of the content directory if we have sections for that page
|
2017-05-08 10:29:37 +00:00
|
|
|
let mut current_path = self.output_path.to_path_buf();
|
2017-03-03 08:12:40 +00:00
|
|
|
|
2017-05-01 08:10:22 +00:00
|
|
|
for component in page.path.split('/') {
|
|
|
|
current_path.push(component);
|
2017-03-03 08:12:40 +00:00
|
|
|
|
2017-05-01 08:10:22 +00:00
|
|
|
if !current_path.exists() {
|
|
|
|
create_directory(¤t_path)?;
|
2017-03-03 08:12:40 +00:00
|
|
|
}
|
2017-05-01 08:10:22 +00:00
|
|
|
}
|
2017-03-03 08:12:40 +00:00
|
|
|
|
2017-05-01 08:10:22 +00:00
|
|
|
// Make sure the folder exists
|
|
|
|
create_directory(¤t_path)?;
|
2017-03-14 12:25:45 +00:00
|
|
|
|
2017-05-01 08:10:22 +00:00
|
|
|
// Finally, create a index.html file there with the page rendered
|
2019-01-27 17:57:07 +00:00
|
|
|
let output = page.render_html(&self.tera, &self.config, &self.library.read().unwrap())?;
|
2017-05-16 05:54:50 +00:00
|
|
|
create_file(¤t_path.join("index.html"), &self.inject_livereload(output))?;
|
2017-03-14 12:25:45 +00:00
|
|
|
|
2017-05-01 08:10:22 +00:00
|
|
|
// Copy any asset we found previously into the same directory as the index.html
|
|
|
|
for asset in &page.assets {
|
|
|
|
let asset_path = asset.as_path();
|
2019-01-04 19:34:20 +00:00
|
|
|
copy(
|
|
|
|
&asset_path,
|
|
|
|
¤t_path
|
|
|
|
.join(asset_path.file_name().expect("Couldn't get filename from page asset")),
|
|
|
|
)?;
|
2017-05-01 08:10:22 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
2017-03-14 12:25:45 +00:00
|
|
|
|
2017-05-16 04:37:00 +00:00
|
|
|
/// Deletes the `public` directory and builds the site
|
2017-03-10 11:39:58 +00:00
|
|
|
pub fn build(&self) -> Result<()> {
|
|
|
|
self.clean()?;
|
2019-03-11 19:25:28 +00:00
|
|
|
|
|
|
|
// Generate/move all assets before rendering any content
|
|
|
|
if let Some(ref theme) = self.config.theme {
|
|
|
|
let theme_path = self.base_path.join("themes").join(theme);
|
|
|
|
if theme_path.join("sass").exists() {
|
2020-07-24 21:00:00 +00:00
|
|
|
sass::compile_sass(&theme_path, &self.output_path)?;
|
2019-03-11 19:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if self.config.compile_sass {
|
2020-07-24 21:00:00 +00:00
|
|
|
sass::compile_sass(&self.base_path, &self.output_path)?;
|
2019-03-11 19:25:28 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if self.config.build_search_index {
|
|
|
|
self.build_search_index()?;
|
|
|
|
}
|
|
|
|
|
2017-06-16 14:09:01 +00:00
|
|
|
// Render aliases first to allow overwriting
|
|
|
|
self.render_aliases()?;
|
2017-05-08 10:29:37 +00:00
|
|
|
self.render_sections()?;
|
|
|
|
self.render_orphan_pages()?;
|
2017-03-10 11:39:58 +00:00
|
|
|
self.render_sitemap()?;
|
2019-01-02 21:11:34 +00:00
|
|
|
|
2019-01-27 17:57:07 +00:00
|
|
|
let library = self.library.read().unwrap();
|
Support and default to generating Atom feeds
This includes several breaking changes, but they’re easy to adjust for.
Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and
legal, though information from the last decade is hard to find.
http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared
has some info which is probably still mostly correct.
How do RSS and Atom compare in terms of implementation support? The
impression I get is that proper Atom support in normal content websites
has been universal for over twelve years, but that support in podcasts
was not quite so good, but getting there, over twelve years ago. I have
no more recent facts or figures; no one talks about this stuff these
days. I remember investigating this stuff back in 2011–2013 and coming
to the same conclusion. At that time, I went with Atom on websites and
RSS in podcasts. Now I’d just go full Atom and hang any podcast tools
that don’t support Atom, because Atom’s semantics truly are much better.
In light of all this, I make the bold recommendation to default to Atom.
Nonetheless, for compatibility for existing users, and for those that
have Opinions, I’ve retained the RSS template, so that you can escape
the breaking change easily.
I personally prefer to give feeds a basename that doesn’t mention “Atom”
or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using
my own template with more Atom features anyway, like author information,
taxonomies and making the title field HTML.
Some notes about the Atom feed template:
- I went with atom.xml rather than something like feed.atom (the .atom
file format being registered for this purpose by RFC4287) due to lack
of confidence that it’ll be served with the right MIME type. .xml is a
safer default.
- It might be nice to get Zola’s version number into the <generator>
tag. Not for any particularly good reason, y’know. Just picture it:
<generator uri="https://www.getzola.org/" version="0.10.0">
Zola
</generator>
- I’d like to get taxonomies into the feed, but this requires exposing a
little more info than is currently exposed. I think it’d require
`TaxonomyConfig` to preferably have a new member `permalink` added
(which should be equivalent to something like `config.base_url ~ "/" ~
taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies
passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`).
Then, the template could be like this, inside the entry:
{% for taxonomy, terms in page.taxonomies %}
{% for term in terms %}
<category scheme="{{ taxonomies[taxonomy].permalink }}"
term="{{ term.slug }}" label="{{ term.name }}" />
{% endfor %}
{% endfor %}
Other remarks:
- I have added a date field `extra.updated` to my posts and include that
in the feed; I’ve observed others with a similar field. I believe this
should be included as an official field. I’m inclined to add author to
at least config.toml, too, for feeds.
- We need to have a link from the docs to the source of the built-in
templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
|
|
|
if self.config.generate_feed {
|
2020-04-03 07:36:30 +00:00
|
|
|
let is_multilingual = self.config.is_multilingual();
|
|
|
|
let pages = if is_multilingual {
|
2019-01-27 17:57:07 +00:00
|
|
|
library
|
2019-01-02 21:11:34 +00:00
|
|
|
.pages_values()
|
|
|
|
.iter()
|
2019-01-29 18:20:03 +00:00
|
|
|
.filter(|p| p.lang == self.config.default_language)
|
Fix clippy warnings (#744)
Clippy is returning some warnings. Let's fix or explicitly ignore
them. In particular:
- In `components/imageproc/src/lib.rs`, we implement `Hash` explicitly
but derive `PartialEq`. We need to maintain the property that two
keys being equal implies the hashes of those two keys are equal.
Our `Hash` implementations preserve this, so we'll explicitly ignore
the warnings.
- In `components/site/src/lib.rs`, we were calling `.into()` on some
values that are already of the correct type.
- In `components/site/src/lib.rs`, we were using `.map(|x| *x)` in
iterator chains to remove a level of indirection; we can instead say
`.copied()` (introduced in Rust v1.36) or `.cloned()`. Using
`.copied` here is better from a type-checking point of view, but
we'll use `.cloned` for now as Rust v1.36 was only recently
released.
- In `components/templates/src/filters.rs` and
`components/utils/src/site.rs`, we were taking `HashMap`s as
function arguments but not generically accepting alternate `Hasher`
implementations.
- In `src/cmd/check.rs`, we use `env::current_dir()` as a default
value, but our use of `unwrap_or` meant that we would always
retrieve the current directory even when not needed.
- In `components/errors/src/lib.rs`, we can use `if let` rather than
`match`.
- In `components/library/src/content/page.rs`, we can collapse a
nested conditional into `else if let ...`.
- In `components/library/src/sorting.rs`, a function takes `&&Page`
arguments. Clippy warns about this for efficiency reasons, but
we're doing it here to match a particular sorting API, so we'll
explicitly ignore the warning.
2019-07-12 08:29:44 +00:00
|
|
|
.cloned()
|
2019-01-02 21:11:34 +00:00
|
|
|
.collect()
|
|
|
|
} else {
|
2019-01-27 17:57:07 +00:00
|
|
|
library.pages_values()
|
2019-01-02 21:11:34 +00:00
|
|
|
};
|
2020-07-25 08:30:55 +00:00
|
|
|
self.render_feed(pages, None, &self.config.default_language, |c| c)?;
|
2019-01-02 21:11:34 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for lang in &self.config.languages {
|
Support and default to generating Atom feeds
This includes several breaking changes, but they’re easy to adjust for.
Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and
legal, though information from the last decade is hard to find.
http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared
has some info which is probably still mostly correct.
How do RSS and Atom compare in terms of implementation support? The
impression I get is that proper Atom support in normal content websites
has been universal for over twelve years, but that support in podcasts
was not quite so good, but getting there, over twelve years ago. I have
no more recent facts or figures; no one talks about this stuff these
days. I remember investigating this stuff back in 2011–2013 and coming
to the same conclusion. At that time, I went with Atom on websites and
RSS in podcasts. Now I’d just go full Atom and hang any podcast tools
that don’t support Atom, because Atom’s semantics truly are much better.
In light of all this, I make the bold recommendation to default to Atom.
Nonetheless, for compatibility for existing users, and for those that
have Opinions, I’ve retained the RSS template, so that you can escape
the breaking change easily.
I personally prefer to give feeds a basename that doesn’t mention “Atom”
or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using
my own template with more Atom features anyway, like author information,
taxonomies and making the title field HTML.
Some notes about the Atom feed template:
- I went with atom.xml rather than something like feed.atom (the .atom
file format being registered for this purpose by RFC4287) due to lack
of confidence that it’ll be served with the right MIME type. .xml is a
safer default.
- It might be nice to get Zola’s version number into the <generator>
tag. Not for any particularly good reason, y’know. Just picture it:
<generator uri="https://www.getzola.org/" version="0.10.0">
Zola
</generator>
- I’d like to get taxonomies into the feed, but this requires exposing a
little more info than is currently exposed. I think it’d require
`TaxonomyConfig` to preferably have a new member `permalink` added
(which should be equivalent to something like `config.base_url ~ "/" ~
taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies
passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`).
Then, the template could be like this, inside the entry:
{% for taxonomy, terms in page.taxonomies %}
{% for term in terms %}
<category scheme="{{ taxonomies[taxonomy].permalink }}"
term="{{ term.slug }}" label="{{ term.name }}" />
{% endfor %}
{% endfor %}
Other remarks:
- I have added a date field `extra.updated` to my posts and include that
in the feed; I’ve observed others with a similar field. I believe this
should be included as an official field. I’m inclined to add author to
at least config.toml, too, for feeds.
- We need to have a link from the docs to the source of the built-in
templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
|
|
|
if !lang.feed {
|
2019-01-02 21:11:34 +00:00
|
|
|
continue;
|
|
|
|
}
|
2019-02-09 18:54:46 +00:00
|
|
|
let pages =
|
Fix clippy warnings (#744)
Clippy is returning some warnings. Let's fix or explicitly ignore
them. In particular:
- In `components/imageproc/src/lib.rs`, we implement `Hash` explicitly
but derive `PartialEq`. We need to maintain the property that two
keys being equal implies the hashes of those two keys are equal.
Our `Hash` implementations preserve this, so we'll explicitly ignore
the warnings.
- In `components/site/src/lib.rs`, we were calling `.into()` on some
values that are already of the correct type.
- In `components/site/src/lib.rs`, we were using `.map(|x| *x)` in
iterator chains to remove a level of indirection; we can instead say
`.copied()` (introduced in Rust v1.36) or `.cloned()`. Using
`.copied` here is better from a type-checking point of view, but
we'll use `.cloned` for now as Rust v1.36 was only recently
released.
- In `components/templates/src/filters.rs` and
`components/utils/src/site.rs`, we were taking `HashMap`s as
function arguments but not generically accepting alternate `Hasher`
implementations.
- In `src/cmd/check.rs`, we use `env::current_dir()` as a default
value, but our use of `unwrap_or` meant that we would always
retrieve the current directory even when not needed.
- In `components/errors/src/lib.rs`, we can use `if let` rather than
`match`.
- In `components/library/src/content/page.rs`, we can collapse a
nested conditional into `else if let ...`.
- In `components/library/src/sorting.rs`, a function takes `&&Page`
arguments. Clippy warns about this for efficiency reasons, but
we're doing it here to match a particular sorting API, so we'll
explicitly ignore the warning.
2019-07-12 08:29:44 +00:00
|
|
|
library.pages_values().iter().filter(|p| p.lang == lang.code).cloned().collect();
|
2020-07-25 08:30:55 +00:00
|
|
|
self.render_feed(pages, Some(&PathBuf::from(lang.code.clone())), &lang.code, |c| c)?;
|
2017-03-12 03:59:28 +00:00
|
|
|
}
|
2019-01-02 21:11:34 +00:00
|
|
|
|
2018-06-26 06:24:57 +00:00
|
|
|
self.render_404()?;
|
2017-03-20 12:40:03 +00:00
|
|
|
self.render_robots()?;
|
2018-07-16 08:54:05 +00:00
|
|
|
self.render_taxonomies()?;
|
2019-05-19 14:03:17 +00:00
|
|
|
// We process images at the end as we might have picked up images to process from markdown
|
|
|
|
// or from templates
|
|
|
|
self.process_images()?;
|
2019-06-03 09:29:44 +00:00
|
|
|
// Processed images will be in static so the last step is to copy it
|
|
|
|
self.copy_static_directories()?;
|
2017-03-20 12:40:03 +00:00
|
|
|
|
2018-03-15 17:58:32 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn build_search_index(&self) -> Result<()> {
|
2019-07-20 14:09:16 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2018-03-15 17:58:32 +00:00
|
|
|
// index first
|
|
|
|
create_file(
|
2018-03-20 20:27:33 +00:00
|
|
|
&self.output_path.join(&format!("search_index.{}.js", self.config.default_language)),
|
2018-03-15 17:58:32 +00:00
|
|
|
&format!(
|
|
|
|
"window.searchIndex = {};",
|
2020-06-29 18:02:05 +00:00
|
|
|
search::build_index(
|
|
|
|
&self.config.default_language,
|
|
|
|
&self.library.read().unwrap(),
|
|
|
|
&self.config
|
|
|
|
)?
|
2018-03-15 17:58:32 +00:00
|
|
|
),
|
|
|
|
)?;
|
|
|
|
|
2019-09-03 14:50:23 +00:00
|
|
|
for language in &self.config.languages {
|
|
|
|
if language.code != self.config.default_language && language.search {
|
|
|
|
create_file(
|
|
|
|
&self.output_path.join(&format!("search_index.{}.js", &language.code)),
|
|
|
|
&format!(
|
|
|
|
"window.searchIndex = {};",
|
2020-06-29 18:02:05 +00:00
|
|
|
search::build_index(
|
|
|
|
&language.code,
|
|
|
|
&self.library.read().unwrap(),
|
|
|
|
&self.config
|
|
|
|
)?
|
2019-09-03 14:50:23 +00:00
|
|
|
),
|
|
|
|
)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-03-15 17:58:32 +00:00
|
|
|
// then elasticlunr.min.js
|
2018-10-31 07:18:57 +00:00
|
|
|
create_file(&self.output_path.join("elasticlunr.min.js"), search::ELASTICLUNR_JS)?;
|
2018-03-15 17:58:32 +00:00
|
|
|
|
|
|
|
Ok(())
|
2017-03-10 11:39:58 +00:00
|
|
|
}
|
2017-06-16 14:09:01 +00:00
|
|
|
|
2019-06-02 18:21:06 +00:00
|
|
|
fn render_alias(&self, alias: &str, permalink: &str) -> Result<()> {
|
|
|
|
let mut output_path = self.output_path.to_path_buf();
|
|
|
|
let mut split = alias.split('/').collect::<Vec<_>>();
|
|
|
|
|
|
|
|
// If the alias ends with an html file name, use that instead of mapping
|
|
|
|
// as a path containing an `index.html`
|
|
|
|
let page_name = match split.pop() {
|
|
|
|
Some(part) if part.ends_with(".html") => part,
|
|
|
|
Some(part) => {
|
|
|
|
split.push(part);
|
|
|
|
"index.html"
|
|
|
|
}
|
|
|
|
None => "index.html",
|
|
|
|
};
|
|
|
|
|
|
|
|
for component in split {
|
|
|
|
output_path.push(&component);
|
|
|
|
|
|
|
|
if !output_path.exists() {
|
|
|
|
create_directory(&output_path)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
create_file(
|
|
|
|
&output_path.join(page_name),
|
|
|
|
&render_redirect_template(&permalink, &self.tera)?,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2020-07-24 21:44:00 +00:00
|
|
|
/// Renders all the aliases for each page/section: a magic HTML template that redirects to
|
|
|
|
/// the canonical one
|
2017-06-16 14:09:01 +00:00
|
|
|
pub fn render_aliases(&self) -> Result<()> {
|
2018-10-20 15:19:13 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2019-06-02 18:21:06 +00:00
|
|
|
let library = self.library.read().unwrap();
|
|
|
|
for (_, page) in library.pages() {
|
2018-03-21 15:18:24 +00:00
|
|
|
for alias in &page.meta.aliases {
|
2019-06-02 18:21:06 +00:00
|
|
|
self.render_alias(&alias, &page.permalink)?;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (_, section) in library.sections() {
|
|
|
|
for alias in §ion.meta.aliases {
|
|
|
|
self.render_alias(&alias, §ion.permalink)?;
|
2017-06-16 14:09:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
}
|
2017-03-10 11:39:58 +00:00
|
|
|
|
2018-06-26 06:24:57 +00:00
|
|
|
/// Renders 404.html
|
|
|
|
pub fn render_404(&self) -> Result<()> {
|
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2018-07-05 15:43:30 +00:00
|
|
|
let mut context = Context::new();
|
|
|
|
context.insert("config", &self.config);
|
2019-01-23 18:20:02 +00:00
|
|
|
let output = render_template("404.html", &self.tera, context, &self.config.theme)?;
|
2018-12-10 17:21:08 +00:00
|
|
|
create_file(&self.output_path.join("404.html"), &self.inject_livereload(output))
|
2018-06-26 06:24:57 +00:00
|
|
|
}
|
|
|
|
|
2017-05-03 08:52:49 +00:00
|
|
|
/// Renders robots.txt
|
2017-05-13 13:37:01 +00:00
|
|
|
pub fn render_robots(&self) -> Result<()> {
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2018-10-02 06:15:26 +00:00
|
|
|
let mut context = Context::new();
|
|
|
|
context.insert("config", &self.config);
|
2017-03-20 12:40:03 +00:00
|
|
|
create_file(
|
2017-05-16 05:54:50 +00:00
|
|
|
&self.output_path.join("robots.txt"),
|
2019-01-23 18:20:02 +00:00
|
|
|
&render_template("robots.txt", &self.tera, context, &self.config.theme)?,
|
2017-03-20 12:40:03 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-07-19 09:10:28 +00:00
|
|
|
/// Renders all taxonomies
|
2018-07-16 08:54:05 +00:00
|
|
|
pub fn render_taxonomies(&self) -> Result<()> {
|
|
|
|
for taxonomy in &self.taxonomies {
|
|
|
|
self.render_taxonomy(taxonomy)?;
|
2017-05-13 13:37:01 +00:00
|
|
|
}
|
2017-03-20 03:42:43 +00:00
|
|
|
|
2017-05-16 04:37:00 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2017-03-07 06:01:20 +00:00
|
|
|
|
2017-05-16 04:37:00 +00:00
|
|
|
fn render_taxonomy(&self, taxonomy: &Taxonomy) -> Result<()> {
|
2017-05-30 10:23:07 +00:00
|
|
|
if taxonomy.items.is_empty() {
|
2018-07-31 13:17:31 +00:00
|
|
|
return Ok(());
|
2017-05-30 10:23:07 +00:00
|
|
|
}
|
|
|
|
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2019-01-29 18:20:03 +00:00
|
|
|
let output_path = if taxonomy.kind.lang != self.config.default_language {
|
|
|
|
let mid_path = self.output_path.join(&taxonomy.kind.lang);
|
2019-01-07 20:03:34 +00:00
|
|
|
create_directory(&mid_path)?;
|
|
|
|
mid_path.join(&taxonomy.kind.name)
|
|
|
|
} else {
|
|
|
|
self.output_path.join(&taxonomy.kind.name)
|
|
|
|
};
|
2019-02-09 18:54:46 +00:00
|
|
|
let list_output =
|
|
|
|
taxonomy.render_all_terms(&self.tera, &self.config, &self.library.read().unwrap())?;
|
2017-03-10 11:39:58 +00:00
|
|
|
create_directory(&output_path)?;
|
2017-05-16 05:54:50 +00:00
|
|
|
create_file(&output_path.join("index.html"), &self.inject_livereload(list_output))?;
|
2019-01-27 17:57:07 +00:00
|
|
|
let library = self.library.read().unwrap();
|
2017-07-05 10:34:41 +00:00
|
|
|
taxonomy
|
|
|
|
.items
|
|
|
|
.par_iter()
|
|
|
|
.map(|item| {
|
2018-11-29 19:24:45 +00:00
|
|
|
let path = output_path.join(&item.slug);
|
2018-07-16 08:54:05 +00:00
|
|
|
if taxonomy.kind.is_paginated() {
|
2018-10-31 07:18:57 +00:00
|
|
|
self.render_paginated(
|
2018-11-29 19:24:45 +00:00
|
|
|
&path,
|
2019-01-27 17:57:07 +00:00
|
|
|
&Paginator::from_taxonomy(&taxonomy, item, &library),
|
2018-11-29 19:24:45 +00:00
|
|
|
)?;
|
2018-07-16 08:54:05 +00:00
|
|
|
} else {
|
2018-10-31 07:18:57 +00:00
|
|
|
let single_output =
|
2019-01-27 17:57:07 +00:00
|
|
|
taxonomy.render_term(item, &self.tera, &self.config, &library)?;
|
2018-07-16 08:54:05 +00:00
|
|
|
create_directory(&path)?;
|
2018-11-29 19:24:45 +00:00
|
|
|
create_file(&path.join("index.html"), &self.inject_livereload(single_output))?;
|
|
|
|
}
|
|
|
|
|
Support and default to generating Atom feeds
This includes several breaking changes, but they’re easy to adjust for.
Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and
legal, though information from the last decade is hard to find.
http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared
has some info which is probably still mostly correct.
How do RSS and Atom compare in terms of implementation support? The
impression I get is that proper Atom support in normal content websites
has been universal for over twelve years, but that support in podcasts
was not quite so good, but getting there, over twelve years ago. I have
no more recent facts or figures; no one talks about this stuff these
days. I remember investigating this stuff back in 2011–2013 and coming
to the same conclusion. At that time, I went with Atom on websites and
RSS in podcasts. Now I’d just go full Atom and hang any podcast tools
that don’t support Atom, because Atom’s semantics truly are much better.
In light of all this, I make the bold recommendation to default to Atom.
Nonetheless, for compatibility for existing users, and for those that
have Opinions, I’ve retained the RSS template, so that you can escape
the breaking change easily.
I personally prefer to give feeds a basename that doesn’t mention “Atom”
or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using
my own template with more Atom features anyway, like author information,
taxonomies and making the title field HTML.
Some notes about the Atom feed template:
- I went with atom.xml rather than something like feed.atom (the .atom
file format being registered for this purpose by RFC4287) due to lack
of confidence that it’ll be served with the right MIME type. .xml is a
safer default.
- It might be nice to get Zola’s version number into the <generator>
tag. Not for any particularly good reason, y’know. Just picture it:
<generator uri="https://www.getzola.org/" version="0.10.0">
Zola
</generator>
- I’d like to get taxonomies into the feed, but this requires exposing a
little more info than is currently exposed. I think it’d require
`TaxonomyConfig` to preferably have a new member `permalink` added
(which should be equivalent to something like `config.base_url ~ "/" ~
taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies
passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`).
Then, the template could be like this, inside the entry:
{% for taxonomy, terms in page.taxonomies %}
{% for term in terms %}
<category scheme="{{ taxonomies[taxonomy].permalink }}"
term="{{ term.slug }}" label="{{ term.name }}" />
{% endfor %}
{% endfor %}
Other remarks:
- I have added a date field `extra.updated` to my posts and include that
in the feed; I’ve observed others with a similar field. I believe this
should be included as an official field. I’m inclined to add author to
at least config.toml, too, for feeds.
- We need to have a link from the docs to the source of the built-in
templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
|
|
|
if taxonomy.kind.feed {
|
|
|
|
self.render_feed(
|
2019-01-27 17:57:07 +00:00
|
|
|
item.pages.iter().map(|p| library.get_page_by_key(*p)).collect(),
|
2018-11-29 19:24:45 +00:00
|
|
|
Some(&PathBuf::from(format!("{}/{}", taxonomy.kind.name, item.slug))),
|
2020-04-03 07:36:30 +00:00
|
|
|
if self.config.is_multilingual() && !taxonomy.kind.lang.is_empty() {
|
|
|
|
&taxonomy.kind.lang
|
|
|
|
} else {
|
|
|
|
&self.config.default_language
|
|
|
|
},
|
2020-07-25 08:30:55 +00:00
|
|
|
|mut context: Context| {
|
|
|
|
context.insert("taxonomy", &taxonomy.kind);
|
|
|
|
context
|
|
|
|
.insert("term", &feed::SerializedFeedTaxonomyItem::from_item(item));
|
|
|
|
context
|
|
|
|
},
|
2018-11-29 19:24:45 +00:00
|
|
|
)
|
|
|
|
} else {
|
|
|
|
Ok(())
|
2018-07-16 08:54:05 +00:00
|
|
|
}
|
2017-07-05 10:34:41 +00:00
|
|
|
})
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()
|
2017-03-06 14:45:57 +00:00
|
|
|
}
|
|
|
|
|
2017-05-13 13:37:01 +00:00
|
|
|
/// What it says on the tin
|
|
|
|
pub fn render_sitemap(&self) -> Result<()> {
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
|
|
|
|
2019-03-19 19:42:16 +00:00
|
|
|
let library = self.library.read().unwrap();
|
2020-07-25 08:49:07 +00:00
|
|
|
let all_sitemap_entries =
|
|
|
|
{ sitemap::find_entries(&library, &self.taxonomies[..], &self.config) };
|
2019-03-14 19:57:22 +00:00
|
|
|
let sitemap_limit = 30000;
|
|
|
|
|
2019-03-19 19:42:16 +00:00
|
|
|
if all_sitemap_entries.len() < sitemap_limit {
|
2019-03-14 19:57:22 +00:00
|
|
|
// Create single sitemap
|
|
|
|
let mut context = Context::new();
|
2019-03-14 20:15:01 +00:00
|
|
|
context.insert("entries", &all_sitemap_entries);
|
2019-03-14 19:57:22 +00:00
|
|
|
let sitemap = &render_template("sitemap.xml", &self.tera, context, &self.config.theme)?;
|
|
|
|
create_file(&self.output_path.join("sitemap.xml"), sitemap)?;
|
2019-03-16 09:01:11 +00:00
|
|
|
return Ok(());
|
2019-03-14 19:57:22 +00:00
|
|
|
}
|
2019-03-14 20:15:01 +00:00
|
|
|
|
2019-03-14 19:57:22 +00:00
|
|
|
// Create multiple sitemaps (max 30000 urls each)
|
|
|
|
let mut sitemap_index = Vec::new();
|
2019-03-16 09:01:11 +00:00
|
|
|
for (i, chunk) in
|
|
|
|
all_sitemap_entries.iter().collect::<Vec<_>>().chunks(sitemap_limit).enumerate()
|
|
|
|
{
|
2019-03-14 19:57:22 +00:00
|
|
|
let mut context = Context::new();
|
2019-03-14 20:15:01 +00:00
|
|
|
context.insert("entries", &chunk);
|
2019-03-14 19:57:22 +00:00
|
|
|
let sitemap = &render_template("sitemap.xml", &self.tera, context, &self.config.theme)?;
|
2019-03-16 09:01:11 +00:00
|
|
|
let file_name = format!("sitemap{}.xml", i + 1);
|
2019-03-14 19:57:22 +00:00
|
|
|
create_file(&self.output_path.join(&file_name), sitemap)?;
|
2019-03-16 09:01:11 +00:00
|
|
|
let mut sitemap_url: String = self.config.make_permalink(&file_name);
|
2019-03-14 19:57:22 +00:00
|
|
|
sitemap_url.pop(); // Remove trailing slash
|
|
|
|
sitemap_index.push(sitemap_url);
|
|
|
|
}
|
2020-07-24 21:44:00 +00:00
|
|
|
|
2019-03-14 19:57:22 +00:00
|
|
|
// Create main sitemap that reference numbered sitemaps
|
|
|
|
let mut main_context = Context::new();
|
|
|
|
main_context.insert("sitemaps", &sitemap_index);
|
2019-03-16 09:01:11 +00:00
|
|
|
let sitemap = &render_template(
|
|
|
|
"split_sitemap_index.xml",
|
|
|
|
&self.tera,
|
|
|
|
main_context,
|
|
|
|
&self.config.theme,
|
|
|
|
)?;
|
2019-03-14 20:15:01 +00:00
|
|
|
create_file(&self.output_path.join("sitemap.xml"), sitemap)?;
|
|
|
|
|
2017-03-03 08:12:40 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
2017-03-07 07:43:27 +00:00
|
|
|
|
Support and default to generating Atom feeds
This includes several breaking changes, but they’re easy to adjust for.
Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and
legal, though information from the last decade is hard to find.
http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared
has some info which is probably still mostly correct.
How do RSS and Atom compare in terms of implementation support? The
impression I get is that proper Atom support in normal content websites
has been universal for over twelve years, but that support in podcasts
was not quite so good, but getting there, over twelve years ago. I have
no more recent facts or figures; no one talks about this stuff these
days. I remember investigating this stuff back in 2011–2013 and coming
to the same conclusion. At that time, I went with Atom on websites and
RSS in podcasts. Now I’d just go full Atom and hang any podcast tools
that don’t support Atom, because Atom’s semantics truly are much better.
In light of all this, I make the bold recommendation to default to Atom.
Nonetheless, for compatibility for existing users, and for those that
have Opinions, I’ve retained the RSS template, so that you can escape
the breaking change easily.
I personally prefer to give feeds a basename that doesn’t mention “Atom”
or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using
my own template with more Atom features anyway, like author information,
taxonomies and making the title field HTML.
Some notes about the Atom feed template:
- I went with atom.xml rather than something like feed.atom (the .atom
file format being registered for this purpose by RFC4287) due to lack
of confidence that it’ll be served with the right MIME type. .xml is a
safer default.
- It might be nice to get Zola’s version number into the <generator>
tag. Not for any particularly good reason, y’know. Just picture it:
<generator uri="https://www.getzola.org/" version="0.10.0">
Zola
</generator>
- I’d like to get taxonomies into the feed, but this requires exposing a
little more info than is currently exposed. I think it’d require
`TaxonomyConfig` to preferably have a new member `permalink` added
(which should be equivalent to something like `config.base_url ~ "/" ~
taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies
passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`).
Then, the template could be like this, inside the entry:
{% for taxonomy, terms in page.taxonomies %}
{% for term in terms %}
<category scheme="{{ taxonomies[taxonomy].permalink }}"
term="{{ term.slug }}" label="{{ term.name }}" />
{% endfor %}
{% endfor %}
Other remarks:
- I have added a date field `extra.updated` to my posts and include that
in the feed; I’ve observed others with a similar field. I believe this
should be included as an official field. I’m inclined to add author to
at least config.toml, too, for feeds.
- We need to have a link from the docs to the source of the built-in
templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
|
|
|
/// Renders a feed for the given path and at the given path
|
|
|
|
/// If both arguments are `None`, it will render only the feed for the whole
|
2018-07-16 08:54:05 +00:00
|
|
|
/// site at the root folder.
|
Support and default to generating Atom feeds
This includes several breaking changes, but they’re easy to adjust for.
Atom 1.0 is superior to RSS 2.0 in a number of ways, both technical and
legal, though information from the last decade is hard to find.
http://www.intertwingly.net/wiki/pie/Rss20AndAtom10Compared
has some info which is probably still mostly correct.
How do RSS and Atom compare in terms of implementation support? The
impression I get is that proper Atom support in normal content websites
has been universal for over twelve years, but that support in podcasts
was not quite so good, but getting there, over twelve years ago. I have
no more recent facts or figures; no one talks about this stuff these
days. I remember investigating this stuff back in 2011–2013 and coming
to the same conclusion. At that time, I went with Atom on websites and
RSS in podcasts. Now I’d just go full Atom and hang any podcast tools
that don’t support Atom, because Atom’s semantics truly are much better.
In light of all this, I make the bold recommendation to default to Atom.
Nonetheless, for compatibility for existing users, and for those that
have Opinions, I’ve retained the RSS template, so that you can escape
the breaking change easily.
I personally prefer to give feeds a basename that doesn’t mention “Atom”
or “RSS”, e.g. “feed.xml”. I’ll be doing that myself, as I’ll be using
my own template with more Atom features anyway, like author information,
taxonomies and making the title field HTML.
Some notes about the Atom feed template:
- I went with atom.xml rather than something like feed.atom (the .atom
file format being registered for this purpose by RFC4287) due to lack
of confidence that it’ll be served with the right MIME type. .xml is a
safer default.
- It might be nice to get Zola’s version number into the <generator>
tag. Not for any particularly good reason, y’know. Just picture it:
<generator uri="https://www.getzola.org/" version="0.10.0">
Zola
</generator>
- I’d like to get taxonomies into the feed, but this requires exposing a
little more info than is currently exposed. I think it’d require
`TaxonomyConfig` to preferably have a new member `permalink` added
(which should be equivalent to something like `config.base_url ~ "/" ~
taxonomy.slug ~ "/"`), and for the feed to get all the taxonomies
passed into it (`taxonomies: HashMap<String, TaxonomyTerm>`).
Then, the template could be like this, inside the entry:
{% for taxonomy, terms in page.taxonomies %}
{% for term in terms %}
<category scheme="{{ taxonomies[taxonomy].permalink }}"
term="{{ term.slug }}" label="{{ term.name }}" />
{% endfor %}
{% endfor %}
Other remarks:
- I have added a date field `extra.updated` to my posts and include that
in the feed; I’ve observed others with a similar field. I believe this
should be included as an official field. I’m inclined to add author to
at least config.toml, too, for feeds.
- We need to have a link from the docs to the source of the built-in
templates, to help people that wish to alter it.
2019-08-11 10:25:24 +00:00
|
|
|
pub fn render_feed(
|
2018-10-31 07:18:57 +00:00
|
|
|
&self,
|
|
|
|
all_pages: Vec<&Page>,
|
|
|
|
base_path: Option<&PathBuf>,
|
2020-04-03 07:36:30 +00:00
|
|
|
lang: &str,
|
2020-07-25 08:30:55 +00:00
|
|
|
additional_context_fn: impl Fn(Context) -> Context,
|
2018-10-31 07:18:57 +00:00
|
|
|
) -> Result<()> {
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2017-05-08 10:29:37 +00:00
|
|
|
|
2020-07-25 08:49:07 +00:00
|
|
|
let feed = match feed::render_feed(self, all_pages, lang, base_path, additional_context_fn)?
|
|
|
|
{
|
2020-07-25 08:30:55 +00:00
|
|
|
Some(v) => v,
|
|
|
|
None => return Ok(()),
|
2017-03-10 11:39:58 +00:00
|
|
|
};
|
2020-07-25 08:30:55 +00:00
|
|
|
let feed_filename = &self.config.feed_filename;
|
2017-03-07 07:43:27 +00:00
|
|
|
|
2018-07-16 08:54:05 +00:00
|
|
|
if let Some(ref base) = base_path {
|
2018-11-19 14:04:22 +00:00
|
|
|
let mut output_path = self.output_path.clone();
|
2018-07-16 08:54:05 +00:00
|
|
|
for component in base.components() {
|
|
|
|
output_path.push(component);
|
|
|
|
if !output_path.exists() {
|
|
|
|
create_directory(&output_path)?;
|
|
|
|
}
|
|
|
|
}
|
2020-07-25 08:30:55 +00:00
|
|
|
create_file(&output_path.join(feed_filename), &feed)?;
|
2018-07-16 08:54:05 +00:00
|
|
|
} else {
|
2020-07-25 08:30:55 +00:00
|
|
|
create_file(&self.output_path.join(feed_filename), &feed)?;
|
2018-07-16 08:54:05 +00:00
|
|
|
}
|
2017-03-14 12:25:45 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2017-05-12 13:32:35 +00:00
|
|
|
/// Renders a single section
|
2017-05-13 13:37:01 +00:00
|
|
|
pub fn render_section(&self, section: &Section, render_pages: bool) -> Result<()> {
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2018-11-19 14:04:22 +00:00
|
|
|
let mut output_path = self.output_path.clone();
|
2018-12-28 16:30:47 +00:00
|
|
|
|
2019-01-29 18:20:03 +00:00
|
|
|
if section.lang != self.config.default_language {
|
|
|
|
output_path.push(§ion.lang);
|
2019-01-04 20:57:27 +00:00
|
|
|
if !output_path.exists() {
|
|
|
|
create_directory(&output_path)?;
|
|
|
|
}
|
2018-12-28 16:30:47 +00:00
|
|
|
}
|
|
|
|
|
2017-05-15 10:53:39 +00:00
|
|
|
for component in §ion.file.components {
|
2017-05-12 13:32:35 +00:00
|
|
|
output_path.push(component);
|
2017-03-14 12:25:45 +00:00
|
|
|
|
2017-05-12 13:32:35 +00:00
|
|
|
if !output_path.exists() {
|
|
|
|
create_directory(&output_path)?;
|
2017-05-11 05:33:23 +00:00
|
|
|
}
|
2017-05-12 13:32:35 +00:00
|
|
|
}
|
2017-05-11 05:33:23 +00:00
|
|
|
|
2018-08-07 10:14:59 +00:00
|
|
|
// Copy any asset we found previously into the same directory as the index.html
|
|
|
|
for asset in §ion.assets {
|
|
|
|
let asset_path = asset.as_path();
|
2019-01-04 19:34:20 +00:00
|
|
|
copy(
|
|
|
|
&asset_path,
|
|
|
|
&output_path.join(
|
|
|
|
asset_path.file_name().expect("Failed to get asset filename for section"),
|
|
|
|
),
|
|
|
|
)?;
|
2018-08-07 10:14:59 +00:00
|
|
|
}
|
|
|
|
|
2017-05-13 13:37:01 +00:00
|
|
|
if render_pages {
|
2017-06-22 12:37:03 +00:00
|
|
|
section
|
|
|
|
.pages
|
|
|
|
.par_iter()
|
2019-01-27 17:57:07 +00:00
|
|
|
.map(|k| self.render_page(self.library.read().unwrap().get_page_by_key(*k)))
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()?;
|
2017-05-12 13:32:35 +00:00
|
|
|
}
|
2017-05-11 05:33:23 +00:00
|
|
|
|
2018-03-14 17:22:24 +00:00
|
|
|
if !section.meta.render {
|
2017-05-12 13:32:35 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2017-07-25 07:56:13 +00:00
|
|
|
if let Some(ref redirect_to) = section.meta.redirect_to {
|
|
|
|
let permalink = self.config.make_permalink(redirect_to);
|
2018-10-31 07:18:57 +00:00
|
|
|
create_file(
|
|
|
|
&output_path.join("index.html"),
|
|
|
|
&render_redirect_template(&permalink, &self.tera)?,
|
|
|
|
)?;
|
2017-07-25 07:56:13 +00:00
|
|
|
return Ok(());
|
|
|
|
}
|
|
|
|
|
2017-05-12 13:32:35 +00:00
|
|
|
if section.meta.is_paginated() {
|
2019-02-09 18:54:46 +00:00
|
|
|
self.render_paginated(
|
|
|
|
&output_path,
|
|
|
|
&Paginator::from_section(§ion, &self.library.read().unwrap()),
|
|
|
|
)?;
|
2017-05-12 13:32:35 +00:00
|
|
|
} else {
|
2019-02-09 18:54:46 +00:00
|
|
|
let output =
|
|
|
|
section.render_html(&self.tera, &self.config, &self.library.read().unwrap())?;
|
2017-05-16 05:54:50 +00:00
|
|
|
create_file(&output_path.join("index.html"), &self.inject_livereload(output))?;
|
2017-05-08 10:29:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2020-07-24 21:44:00 +00:00
|
|
|
// TODO: remove me when reload has changed
|
2017-10-24 18:11:39 +00:00
|
|
|
/// Used only on reload
|
2017-05-13 13:37:01 +00:00
|
|
|
pub fn render_index(&self) -> Result<()> {
|
2017-10-24 18:11:39 +00:00
|
|
|
self.render_section(
|
2019-01-04 19:34:20 +00:00
|
|
|
&self
|
|
|
|
.library
|
2019-02-09 18:54:46 +00:00
|
|
|
.read()
|
|
|
|
.unwrap()
|
2019-01-04 19:34:20 +00:00
|
|
|
.get_section(&self.content_path.join("_index.md"))
|
|
|
|
.expect("Failed to get index section"),
|
2018-07-31 13:17:31 +00:00
|
|
|
false,
|
2017-10-24 18:11:39 +00:00
|
|
|
)
|
2017-05-13 13:37:01 +00:00
|
|
|
}
|
|
|
|
|
2017-05-12 13:32:35 +00:00
|
|
|
/// Renders all sections
|
2017-05-13 13:37:01 +00:00
|
|
|
pub fn render_sections(&self) -> Result<()> {
|
2018-10-02 14:42:34 +00:00
|
|
|
self.library
|
2019-02-09 18:54:46 +00:00
|
|
|
.read()
|
|
|
|
.unwrap()
|
2018-10-02 14:42:34 +00:00
|
|
|
.sections_values()
|
2017-06-22 12:37:03 +00:00
|
|
|
.into_par_iter()
|
|
|
|
.map(|s| self.render_section(s, true))
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()
|
2017-05-12 13:32:35 +00:00
|
|
|
}
|
|
|
|
|
2017-05-08 10:29:37 +00:00
|
|
|
/// Renders all pages that do not belong to any sections
|
2017-05-13 13:37:01 +00:00
|
|
|
pub fn render_orphan_pages(&self) -> Result<()> {
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2019-01-27 17:57:07 +00:00
|
|
|
let library = self.library.read().unwrap();
|
|
|
|
for page in library.get_all_orphan_pages() {
|
2017-06-29 12:14:08 +00:00
|
|
|
self.render_page(page)?;
|
2017-05-03 08:52:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Renders a list of pages when the section/index is wanting pagination.
|
2018-07-16 08:54:05 +00:00
|
|
|
pub fn render_paginated(&self, output_path: &Path, paginator: &Paginator) -> Result<()> {
|
2017-05-16 04:37:00 +00:00
|
|
|
ensure_directory_exists(&self.output_path)?;
|
2017-05-08 10:29:37 +00:00
|
|
|
|
2018-07-16 08:54:05 +00:00
|
|
|
let folder_path = output_path.join(&paginator.paginate_path);
|
2017-06-22 12:37:03 +00:00
|
|
|
create_directory(&folder_path)?;
|
2017-07-06 09:51:36 +00:00
|
|
|
|
2017-06-22 12:37:03 +00:00
|
|
|
paginator
|
|
|
|
.pagers
|
|
|
|
.par_iter()
|
2018-09-18 17:18:50 +00:00
|
|
|
.map(|pager| {
|
|
|
|
let page_path = folder_path.join(&format!("{}", pager.index));
|
2017-06-22 12:37:03 +00:00
|
|
|
create_directory(&page_path)?;
|
2019-02-09 18:54:46 +00:00
|
|
|
let output = paginator.render_pager(
|
|
|
|
pager,
|
|
|
|
&self.config,
|
|
|
|
&self.tera,
|
|
|
|
&self.library.read().unwrap(),
|
|
|
|
)?;
|
2018-09-18 17:18:50 +00:00
|
|
|
if pager.index > 1 {
|
2017-06-22 12:37:03 +00:00
|
|
|
create_file(&page_path.join("index.html"), &self.inject_livereload(output))?;
|
|
|
|
} else {
|
|
|
|
create_file(&output_path.join("index.html"), &self.inject_livereload(output))?;
|
2018-10-31 07:18:57 +00:00
|
|
|
create_file(
|
|
|
|
&page_path.join("index.html"),
|
|
|
|
&render_redirect_template(&paginator.permalink, &self.tera)?,
|
|
|
|
)?;
|
2017-06-22 12:37:03 +00:00
|
|
|
}
|
|
|
|
Ok(())
|
|
|
|
})
|
2018-09-12 19:23:23 +00:00
|
|
|
.collect::<Result<()>>()
|
2017-03-07 07:43:27 +00:00
|
|
|
}
|
2017-03-03 08:12:40 +00:00
|
|
|
}
|