Move test_site and turn rebuild.rs into a component

This commit is contained in:
Vincent Prouillet 2018-01-29 18:40:12 +01:00
parent 7316b98c9b
commit 16f658c70c
64 changed files with 585 additions and 287 deletions

20
Cargo.lock generated
View File

@ -282,6 +282,11 @@ dependencies = [
"toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)", "toml 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "fs_extra"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
[[package]] [[package]]
name = "fsevent" name = "fsevent"
version = "0.2.17" version = "0.2.17"
@ -342,6 +347,7 @@ dependencies = [
"iron 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", "iron 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
"mount 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "mount 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
"notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)", "notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
"rebuild 0.1.0",
"site 0.1.0", "site 0.1.0",
"staticfile 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "staticfile 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)",
"term-painter 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "term-painter 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)",
@ -864,6 +870,19 @@ dependencies = [
"rand 0.3.20 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.3.20 (registry+https://github.com/rust-lang/crates.io-index)",
] ]
[[package]]
name = "rebuild"
version = "0.1.0"
dependencies = [
"content 0.1.0",
"errors 0.1.0",
"front_matter 0.1.0",
"fs_extra 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
"highlighting 0.1.0",
"site 0.1.0",
"tempdir 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]] [[package]]
name = "redox_syscall" name = "redox_syscall"
version = "0.1.37" version = "0.1.37"
@ -1446,6 +1465,7 @@ dependencies = [
"checksum filetime 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "714653f3e34871534de23771ac7b26e999651a0a228f47beb324dfdf1dd4b10f" "checksum filetime 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "714653f3e34871534de23771ac7b26e999651a0a228f47beb324dfdf1dd4b10f"
"checksum flate2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fac2277e84e5e858483756647a9d0aa8d9a2b7cba517fd84325a0aaa69a0909" "checksum flate2 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "9fac2277e84e5e858483756647a9d0aa8d9a2b7cba517fd84325a0aaa69a0909"
"checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3" "checksum fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2fad85553e09a6f881f739c29f0b00b0f01357c743266d478b68951ce23285f3"
"checksum fs_extra 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "5f2a4a2034423744d2cc7ca2068453168dcdb82c438419e639a26bd87839c674"
"checksum fsevent 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)" = "c4bbbf71584aeed076100b5665ac14e3d85eeb31fdbb45fbd41ef9a682b5ec05" "checksum fsevent 0.2.17 (registry+https://github.com/rust-lang/crates.io-index)" = "c4bbbf71584aeed076100b5665ac14e3d85eeb31fdbb45fbd41ef9a682b5ec05"
"checksum fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1a772d36c338d07a032d5375a36f15f9a7043bf0cb8ce7cee658e037c6032874" "checksum fsevent-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "1a772d36c338d07a032d5375a36f15f9a7043bf0cb8ce7cee658e037c6032874"
"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"

View File

@ -36,6 +36,7 @@ errors = { path = "components/errors" }
content = { path = "components/content" } content = { path = "components/content" }
front_matter = { path = "components/front_matter" } front_matter = { path = "components/front_matter" }
utils = { path = "components/utils" } utils = { path = "components/utils" }
rebuild = { path = "components/rebuild" }
[workspace] [workspace]
members = [ members = [
@ -45,6 +46,7 @@ members = [
"components/front_matter", "components/front_matter",
"components/highlighting", "components/highlighting",
"components/pagination", "components/pagination",
"components/rebuild",
"components/rendering", "components/rendering",
"components/site", "components/site",
"components/taxonomies", "components/taxonomies",

View File

@ -59,7 +59,7 @@ impl Page {
Page { Page {
file: FileInfo::new_page(file_path), file: FileInfo::new_page(file_path),
meta: meta, meta,
raw_content: "".to_string(), raw_content: "".to_string(),
assets: vec![], assets: vec![],
content: "".to_string(), content: "".to_string(),

View File

@ -0,0 +1,15 @@
[package]
name = "rebuild"
version = "0.1.0"
authors = ["Vincent Prouillet <vincent@wearewizards.io>"]
[dependencies]
errors = { path = "../errors" }
front_matter = { path = "../front_matter" }
highlighting = { path = "../highlighting" }
content = { path = "../content" }
site = { path = "../site" }
[dev-dependencies]
tempdir = "0.3"
fs_extra = "1.1.0"

View File

@ -0,0 +1,371 @@
extern crate site;
extern crate errors;
extern crate content;
extern crate front_matter;
use std::path::{Path, Component};
use errors::Result;
use site::Site;
use content::{Page, Section};
use front_matter::{PageFrontMatter, SectionFrontMatter};
/// Finds the section that contains the page given if there is one
pub fn find_parent_section<'a>(site: &'a Site, page: &Page) -> Option<&'a Section> {
for section in site.sections.values() {
if section.is_child_page(&page.file.path) {
return Some(section)
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PageChangesNeeded {
/// Editing `tags`
Tags,
/// Editing `categories`
Categories,
/// Editing `date`, `order` or `weight`
Sort,
/// Editing anything causes a re-render of the page
Render,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SectionChangesNeeded {
/// Editing `sort_by`
Sort,
/// Editing `title`, `description`, `extra`, `template` or setting `render` to true
Render,
/// Editing `paginate_by`, `paginate_path` or `insert_anchor_links`
RenderWithPages,
/// Setting `render` to false
Delete,
}
/// Evaluates all the params in the front matter that changed so we can do the smallest
/// delta in the serve command
/// Order matters as the actions will be done in insertion order
fn find_section_front_matter_changes(current: &SectionFrontMatter, new: &SectionFrontMatter) -> Vec<SectionChangesNeeded> {
let mut changes_needed = vec![];
if current.sort_by != new.sort_by {
changes_needed.push(SectionChangesNeeded::Sort);
}
// We want to hide the section
// TODO: what to do on redirect_path change?
if current.should_render() && !new.should_render() {
changes_needed.push(SectionChangesNeeded::Delete);
// Nothing else we can do
return changes_needed;
}
if current.paginate_by != new.paginate_by
|| current.paginate_path != new.paginate_path
|| current.insert_anchor_links != new.insert_anchor_links {
changes_needed.push(SectionChangesNeeded::RenderWithPages);
// Nothing else we can do
return changes_needed;
}
// Any new change will trigger a re-rendering of the section page only
changes_needed.push(SectionChangesNeeded::Render);
changes_needed
}
/// Evaluates all the params in the front matter that changed so we can do the smallest
/// delta in the serve command
/// Order matters as the actions will be done in insertion order
fn find_page_front_matter_changes(current: &PageFrontMatter, other: &PageFrontMatter) -> Vec<PageChangesNeeded> {
let mut changes_needed = vec![];
if current.tags != other.tags {
changes_needed.push(PageChangesNeeded::Tags);
}
if current.category != other.category {
changes_needed.push(PageChangesNeeded::Categories);
}
if current.date != other.date || current.order != other.order || current.weight != other.weight {
changes_needed.push(PageChangesNeeded::Sort);
}
changes_needed.push(PageChangesNeeded::Render);
changes_needed
}
/// Handles a path deletion: could be a page, a section, a folder
fn delete_element(site: &mut Site, path: &Path, is_section: bool) -> Result<()> {
// Ignore the event if this path was not known
if !site.sections.contains_key(path) && !site.pages.contains_key(path) {
return Ok(());
}
if is_section {
if let Some(s) = site.pages.remove(path) {
site.permalinks.remove(&s.file.relative);
site.populate_sections();
}
} else {
if let Some(p) = site.pages.remove(path) {
site.permalinks.remove(&p.file.relative);
if p.meta.has_tags() || p.meta.category.is_some() {
site.populate_tags_and_categories();
}
// if there is a parent section, we will need to re-render it
// most likely
if find_parent_section(site, &p).is_some() {
site.populate_sections();
}
};
}
// Ensure we have our fn updated so it doesn't contain the permalink(s)/section/page deleted
site.register_tera_global_fns();
// Deletion is something that doesn't happen all the time so we
// don't need to optimise it too much
return site.build();
}
/// Handles a `_index.md` (a section) being edited in some ways
fn handle_section_editing(site: &mut Site, path: &Path) -> Result<()> {
let section = Section::from_file(path, &site.config)?;
match site.add_section(section, true)? {
// Updating a section
Some(prev) => {
if site.sections[path].meta == prev.meta {
// Front matter didn't change, only content did
// so we render only the section page, not its pages
return site.render_section(&site.sections[path], false);
}
// Front matter changed
for changes in find_section_front_matter_changes(&site.sections[path].meta, &prev.meta) {
// Sort always comes first if present so the rendering will be fine
match changes {
SectionChangesNeeded::Sort => {
site.sort_sections_pages(Some(path));
site.register_tera_global_fns();
},
SectionChangesNeeded::Render => site.render_section(&site.sections[path], false)?,
SectionChangesNeeded::RenderWithPages => site.render_section(&site.sections[path], true)?,
// not a common enough operation to make it worth optimizing
SectionChangesNeeded::Delete => {
site.populate_sections();
site.build()?;
},
};
}
return Ok(());
},
// New section, only render that one
None => {
site.populate_sections();
site.register_tera_global_fns();
return site.render_section(&site.sections[path], true);
}
};
}
macro_rules! render_parent_section {
($site: expr, $path: expr) => {
match find_parent_section($site, &$site.pages[$path]) {
Some(s) => {
$site.render_section(s, false)?;
},
None => (),
};
}
}
/// Handles a page being edited in some ways
fn handle_page_editing(site: &mut Site, path: &Path) -> Result<()> {
let page = Page::from_file(path, &site.config)?;
match site.add_page(page, true)? {
// Updating a page
Some(prev) => {
// Front matter didn't change, only content did
if site.pages[path].meta == prev.meta {
// Other than the page itself, the summary might be seen
// on a paginated list for a blog for example
if site.pages[path].summary.is_some() {
render_parent_section!(site, path);
}
// TODO: register_tera_global_fns is expensive as it involves lots of cloning
// I can't think of a valid usecase where you would need the content
// of a page through a global fn so it's commented out for now
// site.register_tera_global_fns();
return site.render_page(& site.pages[path]);
}
// Front matter changed
let mut taxonomies_populated = false;
let mut sections_populated = false;
for changes in find_page_front_matter_changes(&site.pages[path].meta, &prev.meta) {
// Sort always comes first if present so the rendering will be fine
match changes {
PageChangesNeeded::Tags => {
if !taxonomies_populated {
site.populate_tags_and_categories();
taxonomies_populated = true;
}
site.register_tera_global_fns();
site.render_tags()?;
},
PageChangesNeeded::Categories => {
if !taxonomies_populated {
site.populate_tags_and_categories();
taxonomies_populated = true;
}
site.register_tera_global_fns();
site.render_categories()?;
},
PageChangesNeeded::Sort => {
let section_path = match find_parent_section(site, &site.pages[path]) {
Some(s) => s.file.path.clone(),
None => continue // Do nothing if it's an orphan page
};
if !sections_populated {
site.populate_sections();
sections_populated = true;
}
site.sort_sections_pages(Some(&section_path));
site.register_tera_global_fns();
site.render_index()?;
},
PageChangesNeeded::Render => {
if !sections_populated {
site.populate_sections();
sections_populated = true;
}
site.register_tera_global_fns();
render_parent_section!(site, path);
site.render_page(&site.pages[path])?;
},
};
}
Ok(())
},
// It's a new page!
None => {
site.populate_sections();
site.populate_tags_and_categories();
site.register_tera_global_fns();
// No need to optimise that yet, we can revisit if it becomes an issue
site.build()
}
}
}
// What happens when a section or a page is changed
pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
let is_section = path.file_name().unwrap() == "_index.md";
// A page or section got deleted
if !path.exists() {
delete_element(site, path, is_section)?;
}
if is_section {
handle_section_editing(site, path)
} else {
handle_page_editing(site, path)
}
}
/// What happens when a template is changed
pub fn after_template_change(site: &mut Site, path: &Path) -> Result<()> {
site.tera.full_reload()?;
let filename = path.file_name().unwrap().to_str().unwrap();
match filename {
"sitemap.xml" => site.render_sitemap(),
"rss.xml" => site.render_rss_feed(),
"robots.txt" => site.render_robots(),
"categories.html" | "category.html" => site.render_categories(),
"tags.html" | "tag.html" => site.render_tags(),
"page.html" => {
site.render_sections()?;
site.render_orphan_pages()
},
"section.html" => site.render_sections(),
// Either the index or some unknown template changed
// We can't really know what this change affects so rebuild all
// the things
_ => {
// If we are updating a shortcode, re-render the markdown of all pages/site
// because we have no clue which one needs rebuilding
// TODO: look if there the shortcode is used in the markdown instead of re-rendering
// everything
if path.components().collect::<Vec<_>>().contains(&Component::Normal("shortcodes".as_ref())) {
site.render_markdown()?;
}
site.populate_sections();
site.render_sections()?;
site.render_orphan_pages()?;
site.render_categories()?;
site.render_tags()
},
}
}
#[cfg(test)]
mod tests {
use front_matter::{PageFrontMatter, SectionFrontMatter, SortBy};
use super::{
find_page_front_matter_changes, find_section_front_matter_changes,
PageChangesNeeded, SectionChangesNeeded
};
#[test]
fn can_find_tag_changes_in_page_frontmatter() {
let new = PageFrontMatter { tags: Some(vec!["a tag".to_string()]), ..PageFrontMatter::default() };
let changes = find_page_front_matter_changes(&PageFrontMatter::default(), &new);
assert_eq!(changes, vec![PageChangesNeeded::Tags, PageChangesNeeded::Render]);
}
#[test]
fn can_find_category_changes_in_page_frontmatter() {
let current = PageFrontMatter { category: Some("a category".to_string()), ..PageFrontMatter::default() };
let changes = find_page_front_matter_changes(&current, &PageFrontMatter::default());
assert_eq!(changes, vec![PageChangesNeeded::Categories, PageChangesNeeded::Render]);
}
#[test]
fn can_find_multiple_changes_in_page_frontmatter() {
let current = PageFrontMatter { category: Some("a category".to_string()), order: Some(1), ..PageFrontMatter::default() };
let changes = find_page_front_matter_changes(&current, &PageFrontMatter::default());
assert_eq!(changes, vec![PageChangesNeeded::Categories, PageChangesNeeded::Sort, PageChangesNeeded::Render]);
}
#[test]
fn can_find_sort_changes_in_section_frontmatter() {
let new = SectionFrontMatter { sort_by: Some(SortBy::Date), ..SectionFrontMatter::default() };
let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new);
assert_eq!(changes, vec![SectionChangesNeeded::Sort, SectionChangesNeeded::Render]);
}
#[test]
fn can_find_render_changes_in_section_frontmatter() {
let new = SectionFrontMatter { render: Some(false), ..SectionFrontMatter::default() };
let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new);
assert_eq!(changes, vec![SectionChangesNeeded::Delete]);
}
#[test]
fn can_find_paginate_by_changes_in_section_frontmatter() {
let new = SectionFrontMatter { paginate_by: Some(10), ..SectionFrontMatter::default() };
let changes = find_section_front_matter_changes(&SectionFrontMatter::default(), &new);
assert_eq!(changes, vec![SectionChangesNeeded::RenderWithPages]);
}
}

View File

@ -0,0 +1,126 @@
extern crate rebuild;
extern crate site;
extern crate tempdir;
extern crate fs_extra;
use std::env;
use std::fs::{remove_dir_all, File};
use std::io::prelude::*;
use fs_extra::dir;
use tempdir::TempDir;
use site::Site;
use rebuild::after_content_change;
// Loads the test_site in a tempdir and build it there
// Returns (site_path_in_tempdir, site)
macro_rules! load_and_build_site {
($tmp_dir: expr) => {
{
let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site");
let mut options = dir::CopyOptions::new();
options.copy_inside = true;
dir::copy(&path, &$tmp_dir, &options).unwrap();
let site_path = $tmp_dir.path().join("test_site");
// delete useless sections for those tests
remove_dir_all(site_path.join("content").join("paginated")).unwrap();
remove_dir_all(site_path.join("content").join("posts")).unwrap();
let mut site = Site::new(&site_path, "config.toml").unwrap();
site.load().unwrap();
let public = &site_path.join("public");
site.set_output_path(&public);
site.build().unwrap();
(site_path, site)
}
}
}
/// Replace the file at the path (starting from root) by the given content
/// and return the file path that was modified
macro_rules! edit_file {
($site_path: expr, $path: expr, $content: expr) => {
{
let mut t = $site_path.clone();
for c in $path.split('/') {
t.push(c);
}
let mut file = File::create(&t).expect("Could not open/create file");
file.write_all($content).expect("Could not write to the file");
t
}
}
}
macro_rules! file_contains {
($site_path: expr, $path: expr, $text: expr) => {
{
let mut path = $site_path.clone();
for component in $path.split("/") {
path.push(component);
}
let mut file = File::open(&path).unwrap();
let mut s = String::new();
file.read_to_string(&mut s).unwrap();
println!("{:?} -> {}", path, s);
s.contains($text)
}
}
}
#[test]
fn can_rebuild_after_simple_change_to_page_content() {
let tmp_dir = TempDir::new("example").expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir);
let file_path = edit_file!(site_path, "content/rebuild/first.md", br#"
+++
title = "first"
order = 1
date = 2017-01-01
+++
Some content"#);
let res = after_content_change(&mut site, &file_path);
assert!(res.is_ok());
assert!(file_contains!(site_path, "public/rebuild/first/index.html", "<p>Some content</p>"));
}
#[test]
fn can_rebuild_after_title_change_page_global_func_usage() {
let tmp_dir = TempDir::new("example").expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir);
let file_path = edit_file!(site_path, "content/rebuild/first.md", br#"
+++
title = "Premier"
order = 10
date = 2017-01-01
+++
# A title"#);
let res = after_content_change(&mut site, &file_path);
assert!(res.is_ok());
assert!(file_contains!(site_path, "public/rebuild/index.html", "<h1>Premier</h1>"));
}
#[test]
fn can_rebuild_after_sort_change_in_section() {
let tmp_dir = TempDir::new("example").expect("create temp dir");
let (site_path, mut site) = load_and_build_site!(tmp_dir);
let file_path = edit_file!(site_path, "content/rebuild/_index.md", br#"
+++
paginate_by = 1
sort_by = "order"
template = "rebuild.html"
+++
"#);
let res = after_content_change(&mut site, &file_path);
assert!(res.is_ok());
assert!(file_contains!(site_path, "public/rebuild/index.html", "<h1>second</h1><h1>first</h1>"));
}

View File

@ -21,6 +21,5 @@ pagination = { path = "../pagination" }
taxonomies = { path = "../taxonomies" } taxonomies = { path = "../taxonomies" }
content = { path = "../content" } content = { path = "../content" }
[dev-dependencies] [dev-dependencies]
tempdir = "0.3" tempdir = "0.3"

View File

@ -275,7 +275,7 @@ impl Site {
/// Add a page to the site /// Add a page to the site
/// The `render` parameter is used in the serve command, when rebuilding a page. /// The `render` parameter is used in the serve command, when rebuilding a page.
/// If `true`, it will also render the markdown for that page /// If `true`, it will also render the markdown for that page
/// Returns the previous page struct if there was one /// Returns the previous page struct if there was one at the same path
pub fn add_page(&mut self, page: Page, render: bool) -> Result<Option<Page>> { pub fn add_page(&mut self, page: Page, render: bool) -> Result<Option<Page>> {
let path = page.file.path.clone(); let path = page.file.path.clone();
self.permalinks.insert(page.file.relative.clone(), page.permalink.clone()); self.permalinks.insert(page.file.relative.clone(), page.permalink.clone());
@ -293,7 +293,7 @@ impl Site {
/// Add a section to the site /// Add a section to the site
/// The `render` parameter is used in the serve command, when rebuilding a page. /// The `render` parameter is used in the serve command, when rebuilding a page.
/// If `true`, it will also render the markdown for that page /// If `true`, it will also render the markdown for that page
/// Returns the previous section struct if there was one /// Returns the previous section struct if there was one at the same path
pub fn add_section(&mut self, section: Section, render: bool) -> Result<Option<Section>> { pub fn add_section(&mut self, section: Section, render: bool) -> Result<Option<Section>> {
let path = section.file.path.clone(); let path = section.file.path.clone();
self.permalinks.insert(section.file.relative.clone(), section.permalink.clone()); self.permalinks.insert(section.file.relative.clone(), section.permalink.clone());
@ -333,11 +333,11 @@ impl Site {
section.ignored_pages = vec![]; section.ignored_pages = vec![];
} }
// TODO: use references instead of cloning to avoid having to call populate_section on
// content change
for page in self.pages.values() { for page in self.pages.values() {
let parent_section_path = page.file.parent.join("_index.md"); let parent_section_path = page.file.parent.join("_index.md");
if self.sections.contains_key(&parent_section_path) { if self.sections.contains_key(&parent_section_path) {
// TODO: use references instead of cloning to avoid having to call populate_section on
// content change
self.sections.get_mut(&parent_section_path).unwrap().pages.push(page.clone()); self.sections.get_mut(&parent_section_path).unwrap().pages.push(page.clone());
} }
} }

View File

@ -12,13 +12,13 @@ use site::Site;
#[test] #[test]
fn can_parse_site() { fn can_parse_site() {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site"); path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.load().unwrap(); site.load().unwrap();
// Correct number of pages (sections are pages too) // Correct number of pages (sections are pages too)
assert_eq!(site.pages.len(), 12); assert_eq!(site.pages.len(), 14);
let posts_path = path.join("content").join("posts"); let posts_path = path.join("content").join("posts");
// Make sure we remove all the pwd + content from the sections // Make sure we remove all the pwd + content from the sections
@ -34,11 +34,11 @@ fn can_parse_site() {
assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]); assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]);
// That we have the right number of sections // That we have the right number of sections
assert_eq!(site.sections.len(), 6); assert_eq!(site.sections.len(), 7);
// And that the sections are correct // And that the sections are correct
let index_section = &site.sections[&path.join("content").join("_index.md")]; let index_section = &site.sections[&path.join("content").join("_index.md")];
assert_eq!(index_section.subsections.len(), 2); assert_eq!(index_section.subsections.len(), 3);
assert_eq!(index_section.pages.len(), 1); assert_eq!(index_section.pages.len(), 1);
let posts_section = &site.sections[&posts_path.join("_index.md")]; let posts_section = &site.sections[&posts_path.join("_index.md")];
@ -91,7 +91,7 @@ macro_rules! file_contains {
#[test] #[test]
fn can_build_site_without_live_reload() { fn can_build_site_without_live_reload() {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site"); path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.load().unwrap(); site.load().unwrap();
@ -152,7 +152,7 @@ fn can_build_site_without_live_reload() {
#[test] #[test]
fn can_build_site_with_live_reload() { fn can_build_site_with_live_reload() {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site"); path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.load().unwrap(); site.load().unwrap();
@ -190,7 +190,7 @@ fn can_build_site_with_live_reload() {
#[test] #[test]
fn can_build_site_with_categories() { fn can_build_site_with_categories() {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site"); path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.config.generate_categories_pages = Some(true); site.config.generate_categories_pages = Some(true);
@ -244,7 +244,7 @@ fn can_build_site_with_categories() {
#[test] #[test]
fn can_build_site_with_tags() { fn can_build_site_with_tags() {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site"); path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.config.generate_tags_pages = Some(true); site.config.generate_tags_pages = Some(true);
@ -296,7 +296,7 @@ fn can_build_site_with_tags() {
#[test] #[test]
fn can_build_site_and_insert_anchor_links() { fn can_build_site_and_insert_anchor_links() {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site"); path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.load().unwrap(); site.load().unwrap();
@ -313,7 +313,7 @@ fn can_build_site_and_insert_anchor_links() {
#[test] #[test]
fn can_build_site_with_pagination_for_section() { fn can_build_site_with_pagination_for_section() {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site"); path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.load().unwrap(); site.load().unwrap();
@ -372,7 +372,7 @@ fn can_build_site_with_pagination_for_section() {
#[test] #[test]
fn can_build_site_with_pagination_for_index() { fn can_build_site_with_pagination_for_index() {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site"); path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.load().unwrap(); site.load().unwrap();
@ -417,7 +417,7 @@ fn can_build_site_with_pagination_for_index() {
#[test] #[test]
fn can_build_rss_feed() { fn can_build_rss_feed() {
let mut path = env::current_dir().unwrap().to_path_buf(); let mut path = env::current_dir().unwrap().parent().unwrap().parent().unwrap().to_path_buf();
path.push("test_site"); path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap(); let mut site = Site::new(&path, "config.toml").unwrap();
site.load().unwrap(); site.load().unwrap();

View File

@ -54,11 +54,15 @@ pub fn make_get_page(all_pages: &HashMap<PathBuf, Page>) -> GlobalFn {
pub fn make_get_section(all_sections: &HashMap<PathBuf, Section>) -> GlobalFn { pub fn make_get_section(all_sections: &HashMap<PathBuf, Section>) -> GlobalFn {
let mut sections = HashMap::new(); let mut sections = HashMap::new();
for section in all_sections.values() { for section in all_sections.values() {
if section.file.components == vec!["rebuild".to_string()] {
//println!("Setting sections:\n{:#?}", section.pages[0]);
}
sections.insert(section.file.relative.clone(), section.clone()); sections.insert(section.file.relative.clone(), section.clone());
} }
Box::new(move |args| -> Result<Value> { Box::new(move |args| -> Result<Value> {
let path = required_string_arg!(args.get("path"), "`get_section` requires a `path` argument with a string value"); let path = required_string_arg!(args.get("path"), "`get_section` requires a `path` argument with a string value");
//println!("Found {:#?}", sections.get(&path).unwrap().pages[0]);
match sections.get(&path) { match sections.get(&path) {
Some(p) => Ok(to_value(p).unwrap()), Some(p) => Ok(to_value(p).unwrap()),
None => Err(format!("Section `{}` not found.", path).into()) None => Err(format!("Section `{}` not found.", path).into())

View File

@ -52,7 +52,7 @@ paginate_path = "page"
# Options are "left", "right" and "none" # Options are "left", "right" and "none"
insert_anchor_links = "none" insert_anchor_links = "none"
# Whether to render that section or not. # Whether to render that section homepage or not.
# Useful when the section is only there to organize things but is not meant # Useful when the section is only there to organize things but is not meant
# to be used directly # to be used directly
render = true render = true

View File

@ -51,7 +51,7 @@ Takes a path to a `.md` file and returns the associated page
Takes a path to a `_index.md` file and returns the associated section Takes a path to a `_index.md` file and returns the associated section
```jinja2 ```jinja2
{% set section = get_page(path="blog/_index.md") %} {% set section = get_section(path="blog/_index.md") %}
``` ```
### ` get_url` ### ` get_url`

View File

@ -62,7 +62,6 @@ fn livereload_handler(_: &mut Request) -> IronResult<Response> {
Ok(Response::with((status::Ok, LIVE_RELOAD.to_string()))) Ok(Response::with((status::Ok, LIVE_RELOAD.to_string())))
} }
fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &str) { fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &str) {
match res { match res {
Ok(_) => { Ok(_) => {

View File

@ -16,12 +16,12 @@ extern crate errors;
extern crate content; extern crate content;
extern crate front_matter; extern crate front_matter;
extern crate utils; extern crate utils;
extern crate rebuild;
use std::time::Instant; use std::time::Instant;
mod cmd; mod cmd;
mod console; mod console;
mod rebuild;
mod cli; mod cli;
mod prompt; mod prompt;

View File

@ -1,265 +0,0 @@
use std::path::{Path, Component};
use errors::Result;
use site::Site;
use content::{Page, Section};
use front_matter::{PageFrontMatter, SectionFrontMatter};
/// Finds the section that contains the page given if there is one
pub fn find_parent_section<'a>(site: &'a Site, page: &Page) -> Option<&'a Section> {
for section in site.sections.values() {
if section.is_child_page(&page.file.path) {
return Some(section)
}
}
None
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum PageChangesNeeded {
/// Editing `tags`
Tags,
/// Editing `categories`
Categories,
/// Editing `date` or `order`
Sort,
/// Editing anything else
Render,
}
// TODO: seems like editing sort_by/render do weird stuff
#[derive(Debug, Clone, Copy, PartialEq)]
enum SectionChangesNeeded {
/// Editing `sort_by`
Sort,
/// Editing `title`, `description`, `extra`, `template` or setting `render` to true
Render,
/// Editing `paginate_by`, `paginate_path` or `insert_anchor_links`
RenderWithPages,
/// Setting `render` to false
Delete,
}
/// Evaluates all the params in the front matter that changed so we can do the smallest
/// delta in the serve command
fn find_section_front_matter_changes(current: &SectionFrontMatter, other: &SectionFrontMatter) -> Vec<SectionChangesNeeded> {
let mut changes_needed = vec![];
if current.sort_by != other.sort_by {
changes_needed.push(SectionChangesNeeded::Sort);
}
if !current.should_render() && other.should_render() {
changes_needed.push(SectionChangesNeeded::Delete);
// Nothing else we can do
return changes_needed;
}
if current.paginate_by != other.paginate_by
|| current.paginate_path != other.paginate_path
|| current.insert_anchor_links != other.insert_anchor_links {
changes_needed.push(SectionChangesNeeded::RenderWithPages);
// Nothing else we can do
return changes_needed;
}
// Any other change will trigger a re-rendering of the section page only
changes_needed.push(SectionChangesNeeded::Render);
changes_needed
}
/// Evaluates all the params in the front matter that changed so we can do the smallest
/// delta in the serve command
fn find_page_front_matter_changes(current: &PageFrontMatter, other: &PageFrontMatter) -> Vec<PageChangesNeeded> {
let mut changes_needed = vec![];
if current.tags != other.tags {
changes_needed.push(PageChangesNeeded::Tags);
}
if current.category != other.category {
changes_needed.push(PageChangesNeeded::Categories);
}
if current.date != other.date || current.order != other.order {
changes_needed.push(PageChangesNeeded::Sort);
}
changes_needed.push(PageChangesNeeded::Render);
changes_needed
}
// What happens when a section or a page is changed
pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
let is_section = path.file_name().unwrap() == "_index.md";
// A page or section got deleted
if !path.exists() {
// A folder got deleted, ignore this event
if !site.sections.contains_key(path) && !site.pages.contains_key(path) {
return Ok(());
}
if is_section {
// A section was deleted, many things can be impacted:
// - the pages of the section are becoming orphans
// - any page that was referencing the section (index, etc)
let relative_path = site.sections[path].file.relative.clone();
// Remove the link to it and the section itself from the Site
site.permalinks.remove(&relative_path);
site.sections.remove(path);
site.populate_sections();
} else {
// A page was deleted, many things can be impacted:
// - the section the page is in
// - any page that was referencing the section (index, etc)
let relative_path = site.pages[path].file.relative.clone();
site.permalinks.remove(&relative_path);
if let Some(p) = site.pages.remove(path) {
if p.meta.has_tags() || p.meta.category.is_some() {
site.populate_tags_and_categories();
}
if find_parent_section(site, &p).is_some() {
site.populate_sections();
}
};
}
// Ensure we have our fn updated so it doesn't contain the permalinks deleted
site.register_tera_global_fns();
// Deletion is something that doesn't happen all the time so we
// don't need to optimise it too much
return site.build();
}
// A section was edited
if is_section {
let section = Section::from_file(path, &site.config)?;
match site.add_section(section, true)? {
Some(prev) => {
// Updating a section
let current_meta = site.sections[path].meta.clone();
// Front matter didn't change, only content did
// so we render only the section page, not its pages
if current_meta == prev.meta {
return site.render_section(&site.sections[path], false);
}
// Front matter changed
for changes in find_section_front_matter_changes(&current_meta, &prev.meta) {
// Sort always comes first if present so the rendering will be fine
match changes {
SectionChangesNeeded::Sort => site.sort_sections_pages(Some(path)),
SectionChangesNeeded::Render => site.render_section(&site.sections[path], false)?,
SectionChangesNeeded::RenderWithPages => site.render_section(&site.sections[path], true)?,
// can't be arsed to make the Delete efficient, it's not a common enough operation
SectionChangesNeeded::Delete => {
site.populate_sections();
site.build()?;
},
};
}
return Ok(());
},
None => {
// New section, only render that one
site.populate_sections();
site.register_tera_global_fns();
return site.render_section(&site.sections[path], true);
}
};
}
// A page was edited
let page = Page::from_file(path, &site.config)?;
match site.add_page(page, true)? {
Some(prev) => {
// Updating a page
let current = site.pages[path].clone();
// Front matter didn't change, only content did
// so we render only the section page, not its content
if current.meta == prev.meta {
return site.render_page(&current);
}
// Front matter changed
for changes in find_page_front_matter_changes(&current.meta, &prev.meta) {
// Sort always comes first if present so the rendering will be fine
match changes {
PageChangesNeeded::Tags => {
site.populate_tags_and_categories();
site.render_tags()?;
},
PageChangesNeeded::Categories => {
site.populate_tags_and_categories();
site.render_categories()?;
},
PageChangesNeeded::Sort => {
let section_path = match find_parent_section(site, &site.pages[path]) {
Some(s) => s.file.path.clone(),
None => continue // Do nothing if it's an orphan page
};
site.populate_sections();
site.sort_sections_pages(Some(&section_path));
site.render_index()?;
},
PageChangesNeeded::Render => {
site.render_page(&site.pages[path])?;
},
};
}
site.register_tera_global_fns();
return Ok(());
},
None => {
// It's a new page!
site.populate_sections();
site.populate_tags_and_categories();
site.register_tera_global_fns();
// No need to optimise that yet, we can revisit if it becomes an issue
site.build()?;
}
}
Ok(())
}
/// What happens when a template is changed
pub fn after_template_change(site: &mut Site, path: &Path) -> Result<()> {
site.tera.full_reload()?;
let filename = path.file_name().unwrap().to_str().unwrap();
match filename {
"sitemap.xml" => site.render_sitemap(),
"rss.xml" => site.render_rss_feed(),
"robots.txt" => site.render_robots(),
"categories.html" | "category.html" => site.render_categories(),
"tags.html" | "tag.html" => site.render_tags(),
"page.html" => {
site.render_sections()?;
site.render_orphan_pages()
},
"section.html" => site.render_sections(),
// Either the index or some unknown template changed
// We can't really know what this change affects so rebuild all
// the things
_ => {
// If we are updating a shortcode, re-render the markdown of all pages/site
// because we have no clue which one needs rebuilding
// TODO: look if there the shortcode is used in the markdown instead of re-rendering
// everything
if path.components().collect::<Vec<_>>().contains(&Component::Normal("shortcodes".as_ref())) {
site.render_markdown()?;
}
site.populate_sections();
site.render_sections()?;
site.render_orphan_pages()?;
site.render_categories()?;
site.render_tags()
},
}
}

1
test_site/README.md Normal file
View File

@ -0,0 +1 @@
Test site used by some components (`site`, `rebuild`) for integration tests.

View File

@ -0,0 +1,5 @@
+++
paginate_by = 1
sort_by = "order"
template = "rebuild.html"
+++

View File

@ -0,0 +1,7 @@
+++
title = "first"
order = 10
date = 2017-01-01
+++
# A title

View File

@ -0,0 +1,7 @@
+++
title = "second"
order = 100
date = 2016-01-01
+++
# A title

View File

@ -0,0 +1,7 @@
{# Testing that global functions/section get reloaded properly #}
{% set section = get_section(path="rebuild/_index.md") %}
{% for page in section.pages -%}
<h1>{{ page.title }}</h1>
{%- endfor %}