@@ -2,8 +2,8 @@ | |||
name = "gutenberg" | |||
version = "0.0.4" | |||
dependencies = [ | |||
"base64 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"base64 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"clap 2.23.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -74,7 +74,7 @@ dependencies = [ | |||
[[package]] | |||
name = "base64" | |||
version = "0.5.0" | |||
version = "0.5.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -123,7 +123,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
[[package]] | |||
name = "bytes" | |||
version = "0.4.2" | |||
version = "0.4.3" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -146,7 +146,7 @@ dependencies = [ | |||
[[package]] | |||
name = "chrono" | |||
version = "0.3.0" | |||
version = "0.3.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -823,7 +823,7 @@ name = "tera" | |||
version = "0.10.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"error-chain 0.10.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"humansize 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -895,7 +895,7 @@ dependencies = [ | |||
[[package]] | |||
name = "toml" | |||
version = "0.4.0" | |||
source = "git+https://github.com/alexcrichton/toml-rs#95b3545938f67ca98d313be5c9c8930ee2407a30" | |||
source = "git+https://github.com/alexcrichton/toml-rs#58f51ef03b88e06745c4113e13ea2738e1af247d" | |||
dependencies = [ | |||
"serde 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
] | |||
@@ -1034,7 +1034,7 @@ version = "0.7.1" | |||
source = "registry+https://github.com/rust-lang/crates.io-index" | |||
dependencies = [ | |||
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"bytes 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"bytes 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"httparse 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"log 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
"mio 0.6.7 (registry+https://github.com/rust-lang/crates.io-index)", | |||
@@ -1072,7 +1072,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
"checksum atty 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d912da0db7fa85514874458ca3651fe2cddace8d0b0505571dbdcd41ab490159" | |||
"checksum backtrace 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f551bc2ddd53aea015d453ef0b635af89444afa5ed2405dd0b2062ad5d600d80" | |||
"checksum backtrace-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "d192fd129132fbc97497c1f2ec2c2c5174e376b95f535199ef4fe0a293d33842" | |||
"checksum base64 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6c902f607515b17ee069f2757c58a6d4b2afa7411b8995f96c4a3c19247b5fcf" | |||
"checksum base64 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "124e5332dfc4e387b4ca058909aa175c0c3eccf03846b7c1a969b9ad067b8df2" | |||
"checksum bincode 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "55eb0b7fd108527b0c77860f75eca70214e11a8b4c6ef05148c54c05a25d48ad" | |||
"checksum bitflags 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "8dead7461c1127cf637931a1e50934eb6eee8bff2f74433ac7909e9afcee04a3" | |||
"checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" | |||
@@ -1080,10 +1080,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" | |||
"checksum byteorder 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" | |||
"checksum byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c40977b0ee6b9885c9013cd41d9feffdd22deb3bb4dc3a71d901cc7a77de18c8" | |||
"checksum bytes 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c129aff112dcc562970abb69e2508b40850dd24c274761bb50fb8a0067ba6c27" | |||
"checksum bytes 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3941933da81d8717b427c2ddc2d73567cd15adb6c57514a2726d9ee598a5439a" | |||
"checksum bytes 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f9edb851115d67d1f18680f9326901768a91d37875b87015518357c6ce22b553" | |||
"checksum cfg-if 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "de1e760d7b6535af4241fca8bd8adf68e2e7edacc6b29f5d399050c5e48cf88c" | |||
"checksum chrono 0.2.25 (registry+https://github.com/rust-lang/crates.io-index)" = "9213f7cd7c27e95c2b57c49f0e69b1ea65b27138da84a170133fd21b07659c00" | |||
"checksum chrono 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "158b0bd7d75cbb6bf9c25967a48a2e9f77da95876b858eadfabaa99cd069de6e" | |||
"checksum chrono 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d9123be86fd2a8f627836c235ecdf331fdd067ecf7ac05aa1a68fbcf2429f056" | |||
"checksum clap 2.23.3 (registry+https://github.com/rust-lang/crates.io-index)" = "f57e9b63057a545ad2ecd773ea61e49422ed1b1d63d74d5da5ecaee55b3396cd" | |||
"checksum cmake 0.1.22 (registry+https://github.com/rust-lang/crates.io-index)" = "d18d68987ed4c516dcc3e7913659bfa4076f5182eea4a7e0038bb060953e76ac" | |||
"checksum conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "95ca30253581af809925ef68c2641cc140d6183f43e12e0af4992d53768bd7b8" | |||
@@ -124,13 +124,17 @@ You can also set the `template` variable to change which template will be used t | |||
Sections will also automatically pick up their subsections, allowing you to make some complex pages layout and | |||
table of contents. | |||
You can define how a section pages are sorted using the `sort_by` key in the front-matter. The choices are `date` (default), `order` | |||
and `none`. Pages that can be sorted will currently be silently dropped: the final page will be rendered but it will not appear in | |||
You can define how a section pages are sorted using the `sort_by` key in the front-matter. The choices are `date`, `order` | |||
and `none` (default). Pages that can't be sorted will currently be silently dropped: the final page will be rendered but it will not appear in | |||
the `pages` variable in the section template. | |||
A special case is the `_index.md` at the root of the `content` directory which represents the homepage. It is only there | |||
to control pagination and sorting of the homepage. | |||
You can also paginate section, including the index by setting the `paginate_by` field in the front matter to an integer. | |||
This represents the number of pages for each pager of the paginator. | |||
You will need to access pages through the `paginator` object. (TODO: document that). | |||
### Code highlighting themes | |||
Code highlighting can be turned on by setting `highlight_code = true` in `config.toml`. | |||
@@ -75,11 +75,11 @@ pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> { | |||
let (tx, rx) = channel(); | |||
let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap(); | |||
watcher.watch("content/", RecursiveMode::Recursive) | |||
.chain_err(|| format!("Can't watch the `content` folder. Does it exist?"))?; | |||
.chain_err(|| "Can't watch the `content` folder. Does it exist?")?; | |||
watcher.watch("static/", RecursiveMode::Recursive) | |||
.chain_err(|| format!("Can't watch the `static` folder. Does it exist?"))?; | |||
.chain_err(|| "Can't watch the `static` folder. Does it exist?")?; | |||
watcher.watch("templates/", RecursiveMode::Recursive) | |||
.chain_err(|| format!("Can't watch the `templates` folder. Does it exist?"))?; | |||
.chain_err(|| "Can't watch the `templates` folder. Does it exist?")?; | |||
let ws_address = format!("{}:{}", interface, "1112"); | |||
@@ -44,7 +44,7 @@ pub struct FrontMatter { | |||
pub draft: Option<bool>, | |||
/// Only one category allowed | |||
pub category: Option<String>, | |||
/// Whether to sort by "date", "order" or "none" | |||
/// Whether to sort by "date", "order" or "none". Defaults to `none`. | |||
#[serde(skip_serializing)] | |||
pub sort_by: Option<SortBy>, | |||
/// Integer to use to order content. Lowest is at the bottom, highest first | |||
@@ -52,6 +52,12 @@ pub struct FrontMatter { | |||
/// Optional template, if we want to specify which template to render for that page | |||
#[serde(skip_serializing)] | |||
pub template: Option<String>, | |||
/// How many pages to be displayed per paginated page. No pagination will happen if this isn't set | |||
#[serde(skip_serializing)] | |||
pub paginate_by: Option<usize>, | |||
/// Path to be used by pagination: the page number will be appended after it. Defaults to `page`. | |||
#[serde(skip_serializing)] | |||
pub paginate_path: Option<String>, | |||
/// Any extra parameter present in the front matter | |||
pub extra: Option<HashMap<String, Value>>, | |||
} | |||
@@ -62,7 +68,7 @@ impl FrontMatter { | |||
bail!("Front matter of file is missing"); | |||
} | |||
let f: FrontMatter = match toml::from_str(toml) { | |||
let mut f: FrontMatter = match toml::from_str(toml) { | |||
Ok(d) => d, | |||
Err(e) => bail!(e), | |||
}; | |||
@@ -79,6 +85,12 @@ impl FrontMatter { | |||
} | |||
} | |||
if f.paginate_path.is_none() { | |||
f.paginate_path = Some("page".to_string()); | |||
} | |||
Ok(f) | |||
} | |||
@@ -106,6 +118,14 @@ impl FrontMatter { | |||
None => SortBy::Date, | |||
} | |||
} | |||
/// Only applies to section, whether it is paginated or not. | |||
pub fn is_paginated(&self) -> bool { | |||
match self.paginate_by { | |||
Some(v) => v > 0, | |||
None => false | |||
} | |||
} | |||
} | |||
impl Default for FrontMatter { | |||
@@ -122,6 +142,8 @@ impl Default for FrontMatter { | |||
sort_by: None, | |||
order: None, | |||
template: None, | |||
paginate_by: None, | |||
paginate_path: None, | |||
extra: None, | |||
} | |||
} | |||
@@ -27,6 +27,7 @@ mod front_matter; | |||
mod site; | |||
mod markdown; | |||
mod section; | |||
mod pagination; | |||
/// Additional filters for Tera | |||
mod filters; | |||
@@ -244,10 +244,10 @@ impl ser::Serialize for Page { | |||
/// Any pages that doesn't have a date when the sorting method is date or order | |||
/// when the sorting method is order will be ignored. | |||
pub fn sort_pages(pages: Vec<Page>, section: Option<&Section>) -> (Vec<Page>, Vec<Page>) { | |||
let sort_by = if let Some(ref sec) = section { | |||
sec.meta.sort_by() | |||
let sort_by = if let Some(s) = section { | |||
s.meta.sort_by() | |||
} else { | |||
SortBy::Date | |||
SortBy::None | |||
}; | |||
match sort_by { | |||
@@ -390,9 +390,9 @@ mod tests { | |||
]; | |||
let (pages, _) = sort_pages(input, None); | |||
// Should be sorted by date | |||
assert_eq!(pages[0].clone().meta.date.unwrap(), "2019-01-01"); | |||
assert_eq!(pages[1].clone().meta.date.unwrap(), "2018-01-01"); | |||
assert_eq!(pages[2].clone().meta.date.unwrap(), "2017-01-01"); | |||
assert_eq!(pages[0].clone().meta.date.unwrap(), "2018-01-01"); | |||
assert_eq!(pages[1].clone().meta.date.unwrap(), "2017-01-01"); | |||
assert_eq!(pages[2].clone().meta.date.unwrap(), "2019-01-01"); | |||
} | |||
#[test] | |||
@@ -0,0 +1,244 @@ | |||
use std::collections::HashMap; | |||
use tera::{Context, to_value, Value}; | |||
use errors::{Result, ResultExt}; | |||
use page::Page; | |||
use section::Section; | |||
use site::Site; | |||
/// A list of all the pages in the paginator with their index and links | |||
#[derive(Clone, Debug, PartialEq, Serialize)] | |||
pub struct Pager<'a> { | |||
/// The page number in the paginator (1-indexed) | |||
index: usize, | |||
/// Permalink to that page | |||
permalink: String, | |||
/// Path to that page | |||
path: String, | |||
/// All pages for the pager | |||
pages: Vec<&'a Page> | |||
} | |||
impl<'a> Pager<'a> { | |||
fn new(index: usize, pages: Vec<&'a Page>, permalink: String, path: String) -> Pager<'a> { | |||
Pager { | |||
index: index, | |||
permalink: permalink, | |||
path: path, | |||
pages: pages, | |||
} | |||
} | |||
} | |||
#[derive(Clone, Debug, PartialEq)] | |||
pub struct Paginator<'a> { | |||
/// All pages in the section | |||
all_pages: &'a [Page], | |||
/// Pages split in chunks of `paginate_by` | |||
pub pagers: Vec<Pager<'a>>, | |||
/// How many content pages on a paginated page at max | |||
paginate_by: usize, | |||
/// The section struct we're building the paginator for | |||
section: &'a Section, | |||
} | |||
impl<'a> Paginator<'a> { | |||
pub fn new(all_pages: &'a [Page], section: &'a Section) -> Paginator<'a> { | |||
let paginate_by = section.meta.paginate_by.unwrap(); | |||
let paginate_path = match section.meta.paginate_path { | |||
Some(ref p) => p, | |||
None => unreachable!(), | |||
}; | |||
let mut pages = vec![]; | |||
let mut current_page = vec![]; | |||
for page in all_pages { | |||
current_page.push(page); | |||
if current_page.len() == paginate_by { | |||
pages.push(current_page); | |||
current_page = vec![]; | |||
} | |||
} | |||
if !current_page.is_empty() { | |||
pages.push(current_page); | |||
} | |||
let mut pagers = vec![]; | |||
for index in 0..pages.len() { | |||
// First page has no pagination path | |||
if index == 0 { | |||
pagers.push(Pager::new(1, pages[index].clone(), section.permalink.clone(), section.path.clone())); | |||
continue; | |||
} | |||
let page_path = format!("{}/{}", paginate_path, index + 1); | |||
let permalink = if section.permalink.ends_with('/') { | |||
format!("{}{}", section.permalink, page_path) | |||
} else { | |||
format!("{}/{}", section.permalink, page_path) | |||
}; | |||
pagers.push(Pager::new( | |||
index + 1, | |||
pages[index].clone(), | |||
permalink, | |||
if section.is_index() { format!("{}", page_path) } else { format!("{}/{}", section.path, page_path) } | |||
)); | |||
} | |||
//println!("{:?}", pagers); | |||
Paginator { | |||
all_pages: all_pages, | |||
pagers: pagers, | |||
paginate_by: paginate_by, | |||
section: section, | |||
} | |||
} | |||
pub fn build_paginator_context(&self, current_pager: &Pager) -> HashMap<&str, Value> { | |||
let mut paginator = HashMap::new(); | |||
// the pager index is 1-indexed so we want a 0-indexed one for indexing there | |||
let pager_index = current_pager.index - 1; | |||
// Global variables | |||
paginator.insert("paginate_by", to_value(self.paginate_by).unwrap()); | |||
paginator.insert("first", to_value(&self.section.permalink).unwrap()); | |||
let last_pager = &self.pagers[self.pagers.len() - 1]; | |||
paginator.insert("last", to_value(&last_pager.permalink).unwrap()); | |||
paginator.insert("pagers", to_value(&self.pagers).unwrap()); | |||
// Variables for this specific page | |||
if pager_index > 0 { | |||
let prev_pager = &self.pagers[pager_index - 1]; | |||
paginator.insert("previous", to_value(&prev_pager.permalink).unwrap()); | |||
} else { | |||
paginator.insert("previous", to_value::<Option<()>>(None).unwrap()); | |||
} | |||
if pager_index < self.pagers.len() - 1 { | |||
let next_pager = &self.pagers[pager_index + 1]; | |||
paginator.insert("next", to_value(&next_pager.permalink).unwrap()); | |||
} else { | |||
paginator.insert("next", to_value::<Option<()>>(None).unwrap()); | |||
} | |||
paginator.insert("pages", to_value(¤t_pager.pages).unwrap()); | |||
paginator.insert("current_index", to_value(current_pager.index).unwrap()); | |||
paginator | |||
} | |||
pub fn render_pager(&self, pager: &Pager, site: &Site) -> Result<String> { | |||
let mut context = Context::new(); | |||
context.add("config", &site.config); | |||
context.add("section", self.section); | |||
context.add("current_url", &pager.permalink); | |||
context.add("current_path", &pager.path); | |||
context.add("paginator", &self.build_paginator_context(pager)); | |||
if self.section.is_index() { | |||
context.add("section", &site.sections); | |||
} | |||
site.tera.render(&self.section.get_template_name(), &context) | |||
.chain_err(|| format!("Failed to render pager {} of section '{}'", pager.index, self.section.file_path.display())) | |||
} | |||
} | |||
#[cfg(test)] | |||
mod tests { | |||
use tera::{to_value}; | |||
use front_matter::FrontMatter; | |||
use page::Page; | |||
use section::Section; | |||
use super::{Paginator}; | |||
fn create_section(is_index: bool) -> Section { | |||
let mut f = FrontMatter::default(); | |||
f.paginate_by = Some(2); | |||
f.paginate_path = Some("page".to_string()); | |||
let mut s = Section::new("content/_index.md", f); | |||
if !is_index { | |||
s.path = "posts".to_string(); | |||
s.permalink = "https://vincent.is/posts".to_string(); | |||
s.components = vec!["posts".to_string()]; | |||
} else { | |||
s.permalink = "https://vincent.is".to_string(); | |||
} | |||
s | |||
} | |||
#[test] | |||
fn test_can_create_paginator() { | |||
let pages = vec![ | |||
Page::new(FrontMatter::default()), | |||
Page::new(FrontMatter::default()), | |||
Page::new(FrontMatter::default()), | |||
]; | |||
let section = create_section(false); | |||
let paginator = Paginator::new(pages.as_slice(), §ion); | |||
assert_eq!(paginator.pagers.len(), 2); | |||
assert_eq!(paginator.pagers[0].index, 1); | |||
assert_eq!(paginator.pagers[0].pages.len(), 2); | |||
assert_eq!(paginator.pagers[0].permalink, "https://vincent.is/posts"); | |||
assert_eq!(paginator.pagers[0].path, "posts"); | |||
assert_eq!(paginator.pagers[1].index, 2); | |||
assert_eq!(paginator.pagers[1].pages.len(), 1); | |||
assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/posts/page/2"); | |||
assert_eq!(paginator.pagers[1].path, "posts/page/2"); | |||
} | |||
#[test] | |||
fn test_can_create_paginator_for_index() { | |||
let pages = vec![ | |||
Page::new(FrontMatter::default()), | |||
Page::new(FrontMatter::default()), | |||
Page::new(FrontMatter::default()), | |||
]; | |||
let section = create_section(true); | |||
let paginator = Paginator::new(pages.as_slice(), §ion); | |||
assert_eq!(paginator.pagers.len(), 2); | |||
assert_eq!(paginator.pagers[0].index, 1); | |||
assert_eq!(paginator.pagers[0].pages.len(), 2); | |||
assert_eq!(paginator.pagers[0].permalink, "https://vincent.is"); | |||
assert_eq!(paginator.pagers[0].path, ""); | |||
assert_eq!(paginator.pagers[1].index, 2); | |||
assert_eq!(paginator.pagers[1].pages.len(), 1); | |||
assert_eq!(paginator.pagers[1].permalink, "https://vincent.is/page/2"); | |||
assert_eq!(paginator.pagers[1].path, "page/2"); | |||
} | |||
#[test] | |||
fn test_can_build_paginator_context() { | |||
let pages = vec![ | |||
Page::new(FrontMatter::default()), | |||
Page::new(FrontMatter::default()), | |||
Page::new(FrontMatter::default()), | |||
]; | |||
let section = create_section(false); | |||
let paginator = Paginator::new(pages.as_slice(), §ion); | |||
assert_eq!(paginator.pagers.len(), 2); | |||
let context = paginator.build_paginator_context(&paginator.pagers[0]); | |||
assert_eq!(context["paginate_by"], to_value(2).unwrap()); | |||
assert_eq!(context["first"], to_value("https://vincent.is/posts").unwrap()); | |||
assert_eq!(context["last"], to_value("https://vincent.is/posts/page/2").unwrap()); | |||
assert_eq!(context["previous"], to_value::<Option<()>>(None).unwrap()); | |||
assert_eq!(context["next"], to_value("https://vincent.is/posts/page/2").unwrap()); | |||
assert_eq!(context["current_index"], to_value(1).unwrap()); | |||
let context = paginator.build_paginator_context(&paginator.pagers[1]); | |||
assert_eq!(context["paginate_by"], to_value(2).unwrap()); | |||
assert_eq!(context["first"], to_value("https://vincent.is/posts").unwrap()); | |||
assert_eq!(context["last"], to_value("https://vincent.is/posts/page/2").unwrap()); | |||
assert_eq!(context["next"], to_value::<Option<()>>(None).unwrap()); | |||
assert_eq!(context["previous"], to_value("https://vincent.is/posts").unwrap()); | |||
assert_eq!(context["current_index"], to_value(2).unwrap()); | |||
} | |||
} |
@@ -8,7 +8,7 @@ use config::Config; | |||
use front_matter::{FrontMatter, split_content}; | |||
use errors::{Result, ResultExt}; | |||
use utils::{read_file, find_content_components}; | |||
use page::Page; | |||
use page::{Page, sort_pages}; | |||
#[derive(Clone, Debug, PartialEq)] | |||
@@ -34,7 +34,9 @@ pub struct Section { | |||
} | |||
impl Section { | |||
pub fn new(file_path: &Path, meta: FrontMatter) -> Section { | |||
pub fn new<P: AsRef<Path>>(file_path: P, meta: FrontMatter) -> Section { | |||
let file_path = file_path.as_ref(); | |||
Section { | |||
file_path: file_path.to_path_buf(), | |||
relative_path: "".to_string(), | |||
@@ -54,8 +56,11 @@ impl Section { | |||
section.components = find_content_components(§ion.file_path); | |||
section.path = section.components.join("/"); | |||
section.permalink = config.make_permalink(§ion.path); | |||
section.relative_path = format!("{}/_index.md", section.components.join("/")); | |||
if section.components.len() == 0 { | |||
section.relative_path = "_index.md".to_string(); | |||
} else { | |||
section.relative_path = format!("{}/_index.md", section.components.join("/")); | |||
} | |||
Ok(section) | |||
} | |||
@@ -68,15 +73,22 @@ impl Section { | |||
Section::parse(path, &content, config) | |||
} | |||
pub fn get_template_name(&self) -> String { | |||
match self.meta.template { | |||
Some(ref l) => l.to_string(), | |||
None => { | |||
if self.is_index() { | |||
return "index.html".to_string(); | |||
} | |||
"section.html".to_string() | |||
}, | |||
} | |||
} | |||
/// Renders the page using the default layout, unless specified in front-matter | |||
pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> { | |||
let tpl_name = match self.meta.template { | |||
Some(ref l) => l.to_string(), | |||
None => "section.html".to_string() | |||
}; | |||
let tpl_name = self.get_template_name(); | |||
// TODO: create a helper to create context to ensure all contexts | |||
// have the same names | |||
let mut context = Context::new(); | |||
context.add("config", config); | |||
context.add("section", self); | |||
@@ -86,6 +98,10 @@ impl Section { | |||
tera.render(&tpl_name, &context) | |||
.chain_err(|| format!("Failed to render section '{}'", self.file_path.display())) | |||
} | |||
pub fn is_index(&self) -> bool { | |||
self.components.len() == 0 | |||
} | |||
} | |||
impl ser::Serialize for Section { | |||
@@ -95,7 +111,8 @@ impl ser::Serialize for Section { | |||
state.serialize_field("description", &self.meta.description)?; | |||
state.serialize_field("path", &format!("/{}", self.path))?; | |||
state.serialize_field("permalink", &self.permalink)?; | |||
state.serialize_field("pages", &self.pages)?; | |||
let (sorted_pages, _) = sort_pages(self.pages.clone(), Some(self)); | |||
state.serialize_field("pages", &sorted_pages)?; | |||
state.serialize_field("subsections", &self.subsections)?; | |||
state.end() | |||
} | |||
@@ -11,6 +11,7 @@ use walkdir::WalkDir; | |||
use errors::{Result, ResultExt}; | |||
use config::{Config, get_config}; | |||
use page::{Page, populate_previous_and_next_pages, sort_pages}; | |||
use pagination::Paginator; | |||
use utils::{create_file, create_directory}; | |||
use section::{Section}; | |||
use filters; | |||
@@ -28,11 +29,23 @@ lazy_static! { | |||
("shortcodes/youtube.html", include_str!("templates/shortcodes/youtube.html")), | |||
("shortcodes/vimeo.html", include_str!("templates/shortcodes/vimeo.html")), | |||
("shortcodes/gist.html", include_str!("templates/shortcodes/gist.html")), | |||
("internal/alias.html", include_str!("templates/internal/alias.html")), | |||
]).unwrap(); | |||
tera | |||
}; | |||
} | |||
/// Renders the `internal/alias.html` template that will redirect | |||
/// via refresh to the url given | |||
fn render_alias(url: &str, tera: &Tera) -> Result<String> { | |||
let mut context = Context::new(); | |||
context.add("url", &url); | |||
tera.render("internal/alias.html", &context) | |||
.chain_err(|| format!("Failed to render alias for '{}'", url)) | |||
} | |||
#[derive(Debug, PartialEq)] | |||
enum RenderList { | |||
@@ -201,7 +214,7 @@ impl Site { | |||
for (parent_path, section) in &mut self.sections { | |||
// TODO: avoid this clone | |||
let (sorted_pages, _) = sort_pages(section.pages.clone(), Some(§ion)); | |||
let (sorted_pages, _) = sort_pages(section.pages.clone(), Some(section)); | |||
section.pages = sorted_pages; | |||
match grandparent_paths.get(parent_path) { | |||
@@ -303,22 +316,21 @@ impl Site { | |||
// probably just an update so just re-parse that page | |||
self.add_page_and_render(path)?; | |||
} | |||
} else { | |||
} else if is_section { | |||
// File doesn't exist -> a deletion so we remove it from everything | |||
if is_section { | |||
if !is_index_section { | |||
let relative_path = self.sections[path].relative_path.clone(); | |||
self.sections.remove(path); | |||
self.permalinks.remove(&relative_path); | |||
} else { | |||
self.index = None; | |||
} | |||
} else { | |||
let relative_path = self.pages[path].relative_path.clone(); | |||
self.pages.remove(path); | |||
if !is_index_section { | |||
let relative_path = self.sections[path].relative_path.clone(); | |||
self.sections.remove(path); | |||
self.permalinks.remove(&relative_path); | |||
} else { | |||
self.index = None; | |||
} | |||
} else { | |||
let relative_path = self.pages[path].relative_path.clone(); | |||
self.pages.remove(path); | |||
self.permalinks.remove(&relative_path); | |||
} | |||
self.populate_sections(); | |||
self.populate_tags_and_categories(); | |||
self.build() | |||
@@ -333,6 +345,7 @@ impl Site { | |||
} | |||
} | |||
/// Renders a single content page | |||
pub fn render_page(&self, page: &Page) -> Result<()> { | |||
let public = self.output_path.clone(); | |||
if !public.exists() { | |||
@@ -366,6 +379,7 @@ impl Site { | |||
Ok(()) | |||
} | |||
/// Renders all content, categories, tags and index pages | |||
pub fn build_pages(&self) -> Result<()> { | |||
let public = self.output_path.clone(); | |||
if !public.exists() { | |||
@@ -374,7 +388,7 @@ impl Site { | |||
// Sort the pages first | |||
// TODO: avoid the clone() | |||
let (mut sorted_pages, cannot_sort_pages) = sort_pages(self.pages.values().map(|p| p.clone()).collect(), self.index.as_ref()); | |||
let (mut sorted_pages, cannot_sort_pages) = sort_pages(self.pages.values().cloned().collect(), self.index.as_ref()); | |||
sorted_pages = populate_previous_and_next_pages(&sorted_pages); | |||
for page in &sorted_pages { | |||
@@ -393,15 +407,26 @@ impl Site { | |||
} | |||
// And finally the index page | |||
let mut context = Context::new(); | |||
let mut rendered_index = false; | |||
// Try to render the index as a paginated page first if needed | |||
if let Some(ref i) = self.index { | |||
if i.meta.is_paginated() { | |||
self.render_paginated(&self.output_path, i)?; | |||
rendered_index = true; | |||
} | |||
} | |||
context.add("pages", &sorted_pages); | |||
context.add("sections", &self.sections.values().collect::<Vec<&Section>>()); | |||
context.add("config", &self.config); | |||
context.add("current_url", &self.config.base_url); | |||
context.add("current_path", &""); | |||
let index = self.tera.render("index.html", &context)?; | |||
create_file(public.join("index.html"), &self.inject_livereload(index))?; | |||
// Otherwise render the default index page | |||
if !rendered_index { | |||
let mut context = Context::new(); | |||
context.add("pages", &sorted_pages); | |||
context.add("sections", &self.sections.values().collect::<Vec<&Section>>()); | |||
context.add("config", &self.config); | |||
context.add("current_url", &self.config.base_url); | |||
context.add("current_path", &""); | |||
let index = self.tera.render("index.html", &context)?; | |||
create_file(public.join("index.html"), &self.inject_livereload(index))?; | |||
} | |||
Ok(()) | |||
} | |||
@@ -422,6 +447,7 @@ impl Site { | |||
self.copy_static_directory() | |||
} | |||
/// Renders robots.txt | |||
fn render_robots(&self) -> Result<()> { | |||
create_file( | |||
self.output_path.join("robots.txt"), | |||
@@ -580,8 +606,46 @@ impl Site { | |||
} | |||
} | |||
let output = section.render_html(&self.tera, &self.config)?; | |||
create_file(output_path.join("index.html"), &self.inject_livereload(output))?; | |||
if section.meta.is_paginated() { | |||
self.render_paginated(&output_path, section)?; | |||
} else { | |||
let output = section.render_html(&self.tera, &self.config)?; | |||
create_file(output_path.join("index.html"), &self.inject_livereload(output))?; | |||
} | |||
} | |||
Ok(()) | |||
} | |||
/// Renders a list of pages when the section/index is wanting pagination. | |||
fn render_paginated(&self, output_path: &Path, section: &Section) -> Result<()> { | |||
let paginate_path = match section.meta.paginate_path { | |||
Some(ref s) => s.clone(), | |||
None => unreachable!() | |||
}; | |||
// this will sort too many times! | |||
// TODO: make sorting happen once for everything so we don't need to sort all the time | |||
let sorted_pages = if section.is_index() { | |||
sort_pages(self.pages.values().cloned().collect(), self.index.as_ref()).0 | |||
} else { | |||
sort_pages(section.pages.clone(), Some(section)).0 | |||
}; | |||
let paginator = Paginator::new(&sorted_pages, section); | |||
for (i, pager) in paginator.pagers.iter().enumerate() { | |||
let folder_path = output_path.join(&paginate_path); | |||
let page_path = folder_path.join(&format!("{}", i + 1)); | |||
create_directory(&folder_path)?; | |||
create_directory(&page_path)?; | |||
let output = paginator.render_pager(pager, self)?; | |||
if i > 0 { | |||
create_file(page_path.join("index.html"), &self.inject_livereload(output))?; | |||
} else { | |||
create_file(output_path.join("index.html"), &self.inject_livereload(output))?; | |||
create_file(page_path.join("index.html"), &render_alias(§ion.permalink, &self.tera)?)?; | |||
} | |||
} | |||
Ok(()) | |||
@@ -0,0 +1,8 @@ | |||
<!DOCTYPE html> | |||
<html> | |||
<head> | |||
<link rel="canonical" href="{{ url | safe }}" /> | |||
<meta http-equiv="content-type" content="text/html; charset=utf-8" /> | |||
<meta http-equiv="refresh" content="0;url={{ url | safe }}" /> | |||
</head> | |||
</html> |
@@ -1,4 +1,3 @@ | |||
+++ | |||
title = "Home" | |||
description = "" | |||
title = "Index" | |||
+++ |
@@ -0,0 +1,33 @@ | |||
<!DOCTYPE html> | |||
<html lang="{{ config.language_code }}"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<meta name="apple-mobile-web-app-capable" content="yes"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1"> | |||
<meta name="description" content="{{ config.description }}"> | |||
<meta name="author" content="{{ config.extra.author.name }}"> | |||
<link href="https://fonts.googleapis.com/css?family=Fira+Mono|Fira+Sans|Merriweather" rel="stylesheet"> | |||
<link href="{{ config.base_url }}/site.css" rel="stylesheet"> | |||
<title>{{ config.title }}</title> | |||
</head> | |||
<body> | |||
<div class="content"> | |||
{% block content %} | |||
<div class="list-posts"> | |||
{% for page in paginator.pages %} | |||
<article> | |||
<h3 class="post__title"><a href="{{ page.permalink }}">{{ page.title }}</a></h3> | |||
</article> | |||
{% endfor %} | |||
{% if paginator.previous %}has_prev{% endif %} | |||
{% if paginator.next %}has_next{% endif %} | |||
Num pages: {{ paginator.pagers | length }} | |||
Current index: {{ paginator.current_index }} | |||
First: {{ paginator.first | safe }} | |||
Last: {{ paginator.last | safe }} | |||
</div> | |||
{% endblock content %} | |||
</div> | |||
</body> | |||
</html> |
@@ -0,0 +1,17 @@ | |||
{% extends "index.html" %} | |||
{% block content %} | |||
{% for page in paginator.pages %} | |||
{{page.title}} | |||
{% endfor %} | |||
{% for pager in paginator.pagers %} | |||
{{pager.index}}: {{pager.path | safe }} | |||
{% endfor %} | |||
Num pages: {{ paginator.pages | length }} | |||
Page size: {{ paginator.paginate_by }} | |||
Current index: {{ paginator.current_index }} | |||
First: {{ paginator.first | safe }} | |||
Last: {{ paginator.last | safe }} | |||
{% if paginator.previous %}has_prev{% endif%} | |||
{% if paginator.next %}has_next{% endif%} | |||
{% endblock content %} |
@@ -43,7 +43,6 @@ fn test_can_parse_site() { | |||
// And that the sections are correct | |||
let posts_section = &site.sections[&posts_path]; | |||
assert_eq!(posts_section.subsections.len(), 1); | |||
//println!("{:#?}", posts_section.pages); | |||
assert_eq!(posts_section.pages.len(), 4); | |||
let tutorials_section = &site.sections[&posts_path.join("tutorials")]; | |||
@@ -82,6 +81,7 @@ macro_rules! file_contains { | |||
let mut file = File::open(&path).unwrap(); | |||
let mut s = String::new(); | |||
file.read_to_string(&mut s).unwrap(); | |||
println!("{}", s); | |||
s.contains($text) | |||
} | |||
} | |||
@@ -287,3 +287,98 @@ fn test_can_build_site_and_insert_anchor_links() { | |||
// anchor link inserted | |||
assert!(file_contains!(public, "posts/something-else/index.html", "<h1 id=\"title\"><a class=\"anchor\" href=\"#title\"")); | |||
} | |||
#[test] | |||
fn test_can_build_site_with_pagination_for_section() { | |||
let mut path = env::current_dir().unwrap().to_path_buf(); | |||
path.push("test_site"); | |||
let mut site = Site::new(&path, "config.toml").unwrap(); | |||
site.load().unwrap(); | |||
for section in site.sections.values_mut(){ | |||
section.meta.paginate_by = Some(2); | |||
section.meta.template = Some("section_paginated.html".to_string()); | |||
} | |||
let tmp_dir = TempDir::new("example").expect("create temp dir"); | |||
let public = &tmp_dir.path().join("public"); | |||
site.set_output_path(&public); | |||
site.build().unwrap(); | |||
assert!(Path::new(&public).exists()); | |||
assert!(file_exists!(public, "index.html")); | |||
assert!(file_exists!(public, "sitemap.xml")); | |||
assert!(file_exists!(public, "robots.txt")); | |||
assert!(file_exists!(public, "a-fixed-url/index.html")); | |||
assert!(file_exists!(public, "posts/python/index.html")); | |||
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html")); | |||
assert!(file_exists!(public, "posts/with-assets/index.html")); | |||
// Sections | |||
assert!(file_exists!(public, "posts/index.html")); | |||
// And pagination! | |||
assert!(file_exists!(public, "posts/page/1/index.html")); | |||
// should redirect to posts/ | |||
assert!(file_contains!( | |||
public, | |||
"posts/page/1/index.html", | |||
"http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/posts\"" | |||
)); | |||
assert!(file_contains!(public, "posts/index.html", "Num pages: 2")); | |||
assert!(file_contains!(public, "posts/index.html", "Page size: 2")); | |||
assert!(file_contains!(public, "posts/index.html", "Current index: 1")); | |||
assert!(file_contains!(public, "posts/index.html", "has_next")); | |||
assert!(file_contains!(public, "posts/index.html", "First: https://replace-this-with-your-url.com/posts")); | |||
assert!(file_contains!(public, "posts/index.html", "Last: https://replace-this-with-your-url.com/posts/page/2")); | |||
assert_eq!(file_contains!(public, "posts/index.html", "has_prev"), false); | |||
assert!(file_exists!(public, "posts/page/2/index.html")); | |||
assert!(file_contains!(public, "posts/page/2/index.html", "Num pages: 2")); | |||
assert!(file_contains!(public, "posts/page/2/index.html", "Page size: 2")); | |||
assert!(file_contains!(public, "posts/page/2/index.html", "Current index: 2")); | |||
assert!(file_contains!(public, "posts/page/2/index.html", "has_prev")); | |||
assert_eq!(file_contains!(public, "posts/page/2/index.html", "has_next"), false); | |||
assert!(file_contains!(public, "posts/page/2/index.html", "First: https://replace-this-with-your-url.com/posts")); | |||
assert!(file_contains!(public, "posts/page/2/index.html", "Last: https://replace-this-with-your-url.com/posts/page/2")); | |||
} | |||
#[test] | |||
fn test_can_build_site_with_pagination_for_index() { | |||
let mut path = env::current_dir().unwrap().to_path_buf(); | |||
path.push("test_site"); | |||
let mut site = Site::new(&path, "config.toml").unwrap(); | |||
site.load().unwrap(); | |||
let mut index = site.index.unwrap(); | |||
index.meta.paginate_by = Some(2); | |||
index.meta.template = Some("index_paginated.html".to_string()); | |||
site.index = Some(index); | |||
let tmp_dir = TempDir::new("example").expect("create temp dir"); | |||
let public = &tmp_dir.path().join("public"); | |||
site.set_output_path(&public); | |||
site.build().unwrap(); | |||
assert!(Path::new(&public).exists()); | |||
assert!(file_exists!(public, "index.html")); | |||
assert!(file_exists!(public, "sitemap.xml")); | |||
assert!(file_exists!(public, "robots.txt")); | |||
assert!(file_exists!(public, "a-fixed-url/index.html")); | |||
assert!(file_exists!(public, "posts/python/index.html")); | |||
assert!(file_exists!(public, "posts/tutorials/devops/nix/index.html")); | |||
assert!(file_exists!(public, "posts/with-assets/index.html")); | |||
// And pagination! | |||
assert!(file_exists!(public, "page/1/index.html")); | |||
// should redirect to index | |||
assert!(file_contains!( | |||
public, | |||
"page/1/index.html", | |||
"http-equiv=\"refresh\" content=\"0;url=https://replace-this-with-your-url.com/\"" | |||
)); | |||
assert!(file_contains!(public, "index.html", "Num pages: 2")); | |||
assert!(file_contains!(public, "index.html", "Current index: 1")); | |||
assert!(file_contains!(public, "index.html", "has_next")); | |||
assert!(file_contains!(public, "index.html", "First: https://replace-this-with-your-url.com/")); | |||
assert!(file_contains!(public, "index.html", "Last: https://replace-this-with-your-url.com/page/2")); | |||
assert_eq!(file_contains!(public, "index.html", "has_prev"), false); | |||
} |