Merge pull request #222 from Keats/rebuild-tests

Move test_site and turn rebuild.rs into a component
This commit is contained in:
Vincent Prouillet 2018-01-29 21:31:20 +01:00 committed by GitHub
commit 094dfb4f2f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
71 changed files with 591 additions and 293 deletions

6
.gitmodules vendored
View file

@ -13,9 +13,6 @@
[submodule "sublime_syntaxes/Julia-sublime"] [submodule "sublime_syntaxes/Julia-sublime"]
path = sublime_syntaxes/Julia-sublime path = sublime_syntaxes/Julia-sublime
url = https://github.com/JuliaEditorSupport/Julia-sublime.git url = https://github.com/JuliaEditorSupport/Julia-sublime.git
[submodule "sublime_syntaxes/Elm.tmLanguage"]
path = sublime_syntaxes/Elm.tmLanguage
url = https://github.com/elm-community/Elm.tmLanguage.git
[submodule "sublime_syntaxes/sublime_toml_highlighting"] [submodule "sublime_syntaxes/sublime_toml_highlighting"]
path = sublime_syntaxes/sublime_toml_highlighting path = sublime_syntaxes/sublime_toml_highlighting
url = https://github.com/Jayflux/sublime_toml_highlighting.git url = https://github.com/Jayflux/sublime_toml_highlighting.git
@ -31,3 +28,6 @@
[submodule "sublime_syntaxes/TypeScript-TmLanguage"] [submodule "sublime_syntaxes/TypeScript-TmLanguage"]
path = sublime_syntaxes/TypeScript-TmLanguage path = sublime_syntaxes/TypeScript-TmLanguage
url = https://github.com/Microsoft/TypeScript-TmLanguage url = https://github.com/Microsoft/TypeScript-TmLanguage
[submodule "sublime_syntaxes/SublimeElmLanguageSupport"]
path = sublime_syntaxes/SublimeElmLanguageSupport
url = https://github.com/elm-community/SublimeElmLanguageSupport

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

@ -45,7 +45,7 @@ You can also add a submodule to the repository of the wanted syntax:
```bash ```bash
$ cd sublime_syntaxes $ cd sublime_syntaxes
$ git submodule add https://github.com/elm-community/Elm.tmLanguage.git $ git submodule add https://github.com/elm-community/SublimeElmLanguageSupport
``` ```
Note that you can also only copy manually the updated syntax definition file but this means Note that you can also only copy manually the updated syntax definition file but this means

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 +1 @@
Subproject commit df5a27523dd37ebe67ba4c7d36ea162dae95b2c3 Subproject commit 987eb72681357b7872a46e8409dfb6f43f2fa673

@ -1 +1 @@
Subproject commit c29d12d8aceb1a68af4cb6e466199846f41dd2ed Subproject commit 0247d1444a66e683bb4005df38218a0fd9576d03

Binary file not shown.

Binary file not shown.

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 %}