Move test_site and turn rebuild.rs into a component
This commit is contained in:
parent
7316b98c9b
commit
16f658c70c
20
Cargo.lock
generated
20
Cargo.lock
generated
|
@ -282,6 +282,11 @@ dependencies = [
|
|||
"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]]
|
||||
name = "fsevent"
|
||||
version = "0.2.17"
|
||||
|
@ -342,6 +347,7 @@ dependencies = [
|
|||
"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)",
|
||||
"notify 4.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rebuild 0.1.0",
|
||||
"site 0.1.0",
|
||||
"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)",
|
||||
|
@ -864,6 +870,19 @@ dependencies = [
|
|||
"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]]
|
||||
name = "redox_syscall"
|
||||
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 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 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-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"
|
||||
|
|
|
@ -36,6 +36,7 @@ errors = { path = "components/errors" }
|
|||
content = { path = "components/content" }
|
||||
front_matter = { path = "components/front_matter" }
|
||||
utils = { path = "components/utils" }
|
||||
rebuild = { path = "components/rebuild" }
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
|
@ -45,6 +46,7 @@ members = [
|
|||
"components/front_matter",
|
||||
"components/highlighting",
|
||||
"components/pagination",
|
||||
"components/rebuild",
|
||||
"components/rendering",
|
||||
"components/site",
|
||||
"components/taxonomies",
|
||||
|
|
|
@ -59,7 +59,7 @@ impl Page {
|
|||
|
||||
Page {
|
||||
file: FileInfo::new_page(file_path),
|
||||
meta: meta,
|
||||
meta,
|
||||
raw_content: "".to_string(),
|
||||
assets: vec![],
|
||||
content: "".to_string(),
|
||||
|
|
15
components/rebuild/Cargo.toml
Normal file
15
components/rebuild/Cargo.toml
Normal 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"
|
371
components/rebuild/src/lib.rs
Normal file
371
components/rebuild/src/lib.rs
Normal 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(§ion_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(¤t, &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(¤t, &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]);
|
||||
}
|
||||
}
|
126
components/rebuild/tests/rebuild.rs
Normal file
126
components/rebuild/tests/rebuild.rs
Normal 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>"));
|
||||
}
|
|
@ -21,6 +21,5 @@ pagination = { path = "../pagination" }
|
|||
taxonomies = { path = "../taxonomies" }
|
||||
content = { path = "../content" }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
tempdir = "0.3"
|
||||
|
|
|
@ -275,7 +275,7 @@ impl Site {
|
|||
/// 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
|
||||
/// 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>> {
|
||||
let path = page.file.path.clone();
|
||||
self.permalinks.insert(page.file.relative.clone(), page.permalink.clone());
|
||||
|
@ -293,7 +293,7 @@ impl Site {
|
|||
/// 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
|
||||
/// 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>> {
|
||||
let path = section.file.path.clone();
|
||||
self.permalinks.insert(section.file.relative.clone(), section.permalink.clone());
|
||||
|
@ -333,11 +333,11 @@ impl Site {
|
|||
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() {
|
||||
let parent_section_path = page.file.parent.join("_index.md");
|
||||
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());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,13 +12,13 @@ use site::Site;
|
|||
|
||||
#[test]
|
||||
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");
|
||||
let mut site = Site::new(&path, "config.toml").unwrap();
|
||||
site.load().unwrap();
|
||||
|
||||
// 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");
|
||||
|
||||
// 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()]);
|
||||
|
||||
// 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
|
||||
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);
|
||||
|
||||
let posts_section = &site.sections[&posts_path.join("_index.md")];
|
||||
|
@ -91,7 +91,7 @@ macro_rules! file_contains {
|
|||
|
||||
#[test]
|
||||
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");
|
||||
let mut site = Site::new(&path, "config.toml").unwrap();
|
||||
site.load().unwrap();
|
||||
|
@ -152,7 +152,7 @@ fn can_build_site_without_live_reload() {
|
|||
|
||||
#[test]
|
||||
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");
|
||||
let mut site = Site::new(&path, "config.toml").unwrap();
|
||||
site.load().unwrap();
|
||||
|
@ -190,7 +190,7 @@ fn can_build_site_with_live_reload() {
|
|||
|
||||
#[test]
|
||||
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");
|
||||
let mut site = Site::new(&path, "config.toml").unwrap();
|
||||
site.config.generate_categories_pages = Some(true);
|
||||
|
@ -244,7 +244,7 @@ fn can_build_site_with_categories() {
|
|||
|
||||
#[test]
|
||||
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");
|
||||
let mut site = Site::new(&path, "config.toml").unwrap();
|
||||
site.config.generate_tags_pages = Some(true);
|
||||
|
@ -296,7 +296,7 @@ fn can_build_site_with_tags() {
|
|||
|
||||
#[test]
|
||||
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");
|
||||
let mut site = Site::new(&path, "config.toml").unwrap();
|
||||
site.load().unwrap();
|
||||
|
@ -313,7 +313,7 @@ fn can_build_site_and_insert_anchor_links() {
|
|||
|
||||
#[test]
|
||||
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");
|
||||
let mut site = Site::new(&path, "config.toml").unwrap();
|
||||
site.load().unwrap();
|
||||
|
@ -372,7 +372,7 @@ fn can_build_site_with_pagination_for_section() {
|
|||
|
||||
#[test]
|
||||
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");
|
||||
let mut site = Site::new(&path, "config.toml").unwrap();
|
||||
site.load().unwrap();
|
||||
|
@ -417,7 +417,7 @@ fn can_build_site_with_pagination_for_index() {
|
|||
|
||||
#[test]
|
||||
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");
|
||||
let mut site = Site::new(&path, "config.toml").unwrap();
|
||||
site.load().unwrap();
|
||||
|
|
|
@ -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 {
|
||||
let mut sections = HashMap::new();
|
||||
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());
|
||||
}
|
||||
|
||||
Box::new(move |args| -> Result<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) {
|
||||
Some(p) => Ok(to_value(p).unwrap()),
|
||||
None => Err(format!("Section `{}` not found.", path).into())
|
||||
|
|
|
@ -52,7 +52,7 @@ paginate_path = "page"
|
|||
# Options are "left", "right" and "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
|
||||
# to be used directly
|
||||
render = true
|
||||
|
|
|
@ -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
|
||||
|
||||
```jinja2
|
||||
{% set section = get_page(path="blog/_index.md") %}
|
||||
{% set section = get_section(path="blog/_index.md") %}
|
||||
```
|
||||
|
||||
### ` get_url`
|
||||
|
|
|
@ -62,7 +62,6 @@ fn livereload_handler(_: &mut Request) -> IronResult<Response> {
|
|||
Ok(Response::with((status::Ok, LIVE_RELOAD.to_string())))
|
||||
}
|
||||
|
||||
|
||||
fn rebuild_done_handling(broadcaster: &Sender, res: Result<()>, reload_path: &str) {
|
||||
match res {
|
||||
Ok(_) => {
|
||||
|
|
|
@ -16,12 +16,12 @@ extern crate errors;
|
|||
extern crate content;
|
||||
extern crate front_matter;
|
||||
extern crate utils;
|
||||
extern crate rebuild;
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
mod cmd;
|
||||
mod console;
|
||||
mod rebuild;
|
||||
mod cli;
|
||||
mod prompt;
|
||||
|
||||
|
|
265
src/rebuild.rs
265
src/rebuild.rs
|
@ -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(¤t_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(¤t);
|
||||
}
|
||||
|
||||
// Front matter changed
|
||||
for changes in find_page_front_matter_changes(¤t.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(§ion_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
1
test_site/README.md
Normal file
|
@ -0,0 +1 @@
|
|||
Test site used by some components (`site`, `rebuild`) for integration tests.
|
5
test_site/content/rebuild/_index.md
Normal file
5
test_site/content/rebuild/_index.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
+++
|
||||
paginate_by = 1
|
||||
sort_by = "order"
|
||||
template = "rebuild.html"
|
||||
+++
|
7
test_site/content/rebuild/first.md
Normal file
7
test_site/content/rebuild/first.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
+++
|
||||
title = "first"
|
||||
order = 10
|
||||
date = 2017-01-01
|
||||
+++
|
||||
|
||||
# A title
|
7
test_site/content/rebuild/second.md
Normal file
7
test_site/content/rebuild/second.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
+++
|
||||
title = "second"
|
||||
order = 100
|
||||
date = 2016-01-01
|
||||
+++
|
||||
|
||||
# A title
|
7
test_site/templates/rebuild.html
Normal file
7
test_site/templates/rebuild.html
Normal 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 %}
|
Loading…
Reference in a new issue