Pagination

This commit is contained in:
Vincent Prouillet 2017-05-03 17:52:49 +09:00
parent 27287a50c3
commit a3318d4b56
14 changed files with 566 additions and 62 deletions

22
Cargo.lock generated
View file

@ -2,8 +2,8 @@
name = "gutenberg" name = "gutenberg"
version = "0.0.4" version = "0.0.4"
dependencies = [ dependencies = [
"base64 0.5.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.0 (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)", "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)", "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)", "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
@ -74,7 +74,7 @@ dependencies = [
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.5.0" version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "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]] [[package]]
name = "bytes" name = "bytes"
version = "0.4.2" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
@ -146,7 +146,7 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.3.0" version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", "num 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)",
@ -823,7 +823,7 @@ name = "tera"
version = "0.10.1" version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ 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)", "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)", "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)", "humansize 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)",
@ -895,7 +895,7 @@ dependencies = [
[[package]] [[package]]
name = "toml" name = "toml"
version = "0.4.0" version = "0.4.0"
source = "git+https://github.com/alexcrichton/toml-rs#95b3545938f67ca98d313be5c9c8930ee2407a30" source = "git+https://github.com/alexcrichton/toml-rs#58f51ef03b88e06745c4113e13ea2738e1af247d"
dependencies = [ dependencies = [
"serde 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", "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" source = "registry+https://github.com/rust-lang/crates.io-index"
dependencies = [ dependencies = [
"byteorder 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "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)", "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)", "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)", "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 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 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 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 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.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" "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 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 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.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 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.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 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 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" "checksum conduit-mime-types 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)" = "95ca30253581af809925ef68c2641cc140d6183f43e12e0af4992d53768bd7b8"

View file

@ -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 Sections will also automatically pick up their subsections, allowing you to make some complex pages layout and
table of contents. 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` 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`. Pages that can be sorted will currently be silently dropped: the final page will be rendered but it will not appear in 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. 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 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. 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 themes
Code highlighting can be turned on by setting `highlight_code = true` in `config.toml`. Code highlighting can be turned on by setting `highlight_code = true` in `config.toml`.

View file

@ -75,11 +75,11 @@ pub fn serve(interface: &str, port: &str, config_file: &str) -> Result<()> {
let (tx, rx) = channel(); let (tx, rx) = channel();
let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap(); let mut watcher = watcher(tx, Duration::from_secs(2)).unwrap();
watcher.watch("content/", RecursiveMode::Recursive) 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) 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) 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"); let ws_address = format!("{}:{}", interface, "1112");

View file

@ -44,7 +44,7 @@ pub struct FrontMatter {
pub draft: Option<bool>, pub draft: Option<bool>,
/// Only one category allowed /// Only one category allowed
pub category: Option<String>, 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)] #[serde(skip_serializing)]
pub sort_by: Option<SortBy>, pub sort_by: Option<SortBy>,
/// Integer to use to order content. Lowest is at the bottom, highest first /// 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 /// Optional template, if we want to specify which template to render for that page
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub template: Option<String>, 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 /// Any extra parameter present in the front matter
pub extra: Option<HashMap<String, Value>>, pub extra: Option<HashMap<String, Value>>,
} }
@ -62,7 +68,7 @@ impl FrontMatter {
bail!("Front matter of file is missing"); 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, Ok(d) => d,
Err(e) => bail!(e), 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) Ok(f)
} }
@ -106,6 +118,14 @@ impl FrontMatter {
None => SortBy::Date, 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 { impl Default for FrontMatter {
@ -122,6 +142,8 @@ impl Default for FrontMatter {
sort_by: None, sort_by: None,
order: None, order: None,
template: None, template: None,
paginate_by: None,
paginate_path: None,
extra: None, extra: None,
} }
} }

View file

@ -27,6 +27,7 @@ mod front_matter;
mod site; mod site;
mod markdown; mod markdown;
mod section; mod section;
mod pagination;
/// Additional filters for Tera /// Additional filters for Tera
mod filters; mod filters;

View file

@ -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 /// 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. /// when the sorting method is order will be ignored.
pub fn sort_pages(pages: Vec<Page>, section: Option<&Section>) -> (Vec<Page>, Vec<Page>) { pub fn sort_pages(pages: Vec<Page>, section: Option<&Section>) -> (Vec<Page>, Vec<Page>) {
let sort_by = if let Some(ref sec) = section { let sort_by = if let Some(s) = section {
sec.meta.sort_by() s.meta.sort_by()
} else { } else {
SortBy::Date SortBy::None
}; };
match sort_by { match sort_by {
@ -390,9 +390,9 @@ mod tests {
]; ];
let (pages, _) = sort_pages(input, None); let (pages, _) = sort_pages(input, None);
// Should be sorted by date // Should be sorted by date
assert_eq!(pages[0].clone().meta.date.unwrap(), "2019-01-01"); assert_eq!(pages[0].clone().meta.date.unwrap(), "2018-01-01");
assert_eq!(pages[1].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(), "2017-01-01"); assert_eq!(pages[2].clone().meta.date.unwrap(), "2019-01-01");
} }
#[test] #[test]

244
src/pagination.rs Normal file
View file

@ -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(&current_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(), &section);
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(), &section);
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(), &section);
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());
}
}

View file

@ -8,7 +8,7 @@ use config::Config;
use front_matter::{FrontMatter, split_content}; use front_matter::{FrontMatter, split_content};
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use utils::{read_file, find_content_components}; use utils::{read_file, find_content_components};
use page::Page; use page::{Page, sort_pages};
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
@ -34,7 +34,9 @@ pub struct Section {
} }
impl 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 { Section {
file_path: file_path.to_path_buf(), file_path: file_path.to_path_buf(),
relative_path: "".to_string(), relative_path: "".to_string(),
@ -54,8 +56,11 @@ impl Section {
section.components = find_content_components(&section.file_path); section.components = find_content_components(&section.file_path);
section.path = section.components.join("/"); section.path = section.components.join("/");
section.permalink = config.make_permalink(&section.path); section.permalink = config.make_permalink(&section.path);
if section.components.len() == 0 {
section.relative_path = "_index.md".to_string();
} else {
section.relative_path = format!("{}/_index.md", section.components.join("/")); section.relative_path = format!("{}/_index.md", section.components.join("/"));
}
Ok(section) Ok(section)
} }
@ -68,15 +73,22 @@ impl Section {
Section::parse(path, &content, config) 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 /// Renders the page using the default layout, unless specified in front-matter
pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> { pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> {
let tpl_name = match self.meta.template { let tpl_name = self.get_template_name();
Some(ref l) => l.to_string(),
None => "section.html".to_string()
};
// TODO: create a helper to create context to ensure all contexts
// have the same names
let mut context = Context::new(); let mut context = Context::new();
context.add("config", config); context.add("config", config);
context.add("section", self); context.add("section", self);
@ -86,6 +98,10 @@ impl Section {
tera.render(&tpl_name, &context) tera.render(&tpl_name, &context)
.chain_err(|| format!("Failed to render section '{}'", self.file_path.display())) .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 { impl ser::Serialize for Section {
@ -95,7 +111,8 @@ impl ser::Serialize for Section {
state.serialize_field("description", &self.meta.description)?; state.serialize_field("description", &self.meta.description)?;
state.serialize_field("path", &format!("/{}", self.path))?; state.serialize_field("path", &format!("/{}", self.path))?;
state.serialize_field("permalink", &self.permalink)?; 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.serialize_field("subsections", &self.subsections)?;
state.end() state.end()
} }

View file

@ -11,6 +11,7 @@ use walkdir::WalkDir;
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use config::{Config, get_config}; use config::{Config, get_config};
use page::{Page, populate_previous_and_next_pages, sort_pages}; use page::{Page, populate_previous_and_next_pages, sort_pages};
use pagination::Paginator;
use utils::{create_file, create_directory}; use utils::{create_file, create_directory};
use section::{Section}; use section::{Section};
use filters; use filters;
@ -28,11 +29,23 @@ lazy_static! {
("shortcodes/youtube.html", include_str!("templates/shortcodes/youtube.html")), ("shortcodes/youtube.html", include_str!("templates/shortcodes/youtube.html")),
("shortcodes/vimeo.html", include_str!("templates/shortcodes/vimeo.html")), ("shortcodes/vimeo.html", include_str!("templates/shortcodes/vimeo.html")),
("shortcodes/gist.html", include_str!("templates/shortcodes/gist.html")), ("shortcodes/gist.html", include_str!("templates/shortcodes/gist.html")),
("internal/alias.html", include_str!("templates/internal/alias.html")),
]).unwrap(); ]).unwrap();
tera 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)] #[derive(Debug, PartialEq)]
enum RenderList { enum RenderList {
@ -201,7 +214,7 @@ impl Site {
for (parent_path, section) in &mut self.sections { for (parent_path, section) in &mut self.sections {
// TODO: avoid this clone // TODO: avoid this clone
let (sorted_pages, _) = sort_pages(section.pages.clone(), Some(&section)); let (sorted_pages, _) = sort_pages(section.pages.clone(), Some(section));
section.pages = sorted_pages; section.pages = sorted_pages;
match grandparent_paths.get(parent_path) { match grandparent_paths.get(parent_path) {
@ -303,9 +316,8 @@ impl Site {
// probably just an update so just re-parse that page // probably just an update so just re-parse that page
self.add_page_and_render(path)?; self.add_page_and_render(path)?;
} }
} else { } else if is_section {
// File doesn't exist -> a deletion so we remove it from everything // File doesn't exist -> a deletion so we remove it from everything
if is_section {
if !is_index_section { if !is_index_section {
let relative_path = self.sections[path].relative_path.clone(); let relative_path = self.sections[path].relative_path.clone();
self.sections.remove(path); self.sections.remove(path);
@ -318,7 +330,7 @@ impl Site {
self.pages.remove(path); self.pages.remove(path);
self.permalinks.remove(&relative_path); self.permalinks.remove(&relative_path);
} }
}
self.populate_sections(); self.populate_sections();
self.populate_tags_and_categories(); self.populate_tags_and_categories();
self.build() self.build()
@ -333,6 +345,7 @@ impl Site {
} }
} }
/// Renders a single content page
pub fn render_page(&self, page: &Page) -> Result<()> { pub fn render_page(&self, page: &Page) -> Result<()> {
let public = self.output_path.clone(); let public = self.output_path.clone();
if !public.exists() { if !public.exists() {
@ -366,6 +379,7 @@ impl Site {
Ok(()) Ok(())
} }
/// Renders all content, categories, tags and index pages
pub fn build_pages(&self) -> Result<()> { pub fn build_pages(&self) -> Result<()> {
let public = self.output_path.clone(); let public = self.output_path.clone();
if !public.exists() { if !public.exists() {
@ -374,7 +388,7 @@ impl Site {
// Sort the pages first // Sort the pages first
// TODO: avoid the clone() // 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); sorted_pages = populate_previous_and_next_pages(&sorted_pages);
for page in &sorted_pages { for page in &sorted_pages {
@ -393,8 +407,18 @@ impl Site {
} }
// And finally the index page // 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;
}
}
// Otherwise render the default index page
if !rendered_index {
let mut context = Context::new();
context.add("pages", &sorted_pages); context.add("pages", &sorted_pages);
context.add("sections", &self.sections.values().collect::<Vec<&Section>>()); context.add("sections", &self.sections.values().collect::<Vec<&Section>>());
context.add("config", &self.config); context.add("config", &self.config);
@ -402,6 +426,7 @@ impl Site {
context.add("current_path", &""); context.add("current_path", &"");
let index = self.tera.render("index.html", &context)?; let index = self.tera.render("index.html", &context)?;
create_file(public.join("index.html"), &self.inject_livereload(index))?; create_file(public.join("index.html"), &self.inject_livereload(index))?;
}
Ok(()) Ok(())
} }
@ -422,6 +447,7 @@ impl Site {
self.copy_static_directory() self.copy_static_directory()
} }
/// Renders robots.txt
fn render_robots(&self) -> Result<()> { fn render_robots(&self) -> Result<()> {
create_file( create_file(
self.output_path.join("robots.txt"), self.output_path.join("robots.txt"),
@ -580,9 +606,47 @@ impl Site {
} }
} }
if section.meta.is_paginated() {
self.render_paginated(&output_path, section)?;
} else {
let output = section.render_html(&self.tera, &self.config)?; let output = section.render_html(&self.tera, &self.config)?;
create_file(output_path.join("index.html"), &self.inject_livereload(output))?; 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(&section.permalink, &self.tera)?)?;
}
}
Ok(()) Ok(())
} }

View file

@ -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>

View file

@ -1,4 +1,3 @@
+++ +++
title = "Home" title = "Index"
description = ""
+++ +++

View file

@ -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>

View file

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

View file

@ -43,7 +43,6 @@ fn test_can_parse_site() {
// And that the sections are correct // And that the sections are correct
let posts_section = &site.sections[&posts_path]; let posts_section = &site.sections[&posts_path];
assert_eq!(posts_section.subsections.len(), 1); assert_eq!(posts_section.subsections.len(), 1);
//println!("{:#?}", posts_section.pages);
assert_eq!(posts_section.pages.len(), 4); assert_eq!(posts_section.pages.len(), 4);
let tutorials_section = &site.sections[&posts_path.join("tutorials")]; 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 file = File::open(&path).unwrap();
let mut s = String::new(); let mut s = String::new();
file.read_to_string(&mut s).unwrap(); file.read_to_string(&mut s).unwrap();
println!("{}", s);
s.contains($text) s.contains($text)
} }
} }
@ -287,3 +287,98 @@ fn test_can_build_site_and_insert_anchor_links() {
// anchor link inserted // anchor link inserted
assert!(file_contains!(public, "posts/something-else/index.html", "<h1 id=\"title\"><a class=\"anchor\" href=\"#title\"")); 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);
}