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

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
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`.

View file

@ -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");

View file

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

View file

@ -27,6 +27,7 @@ mod front_matter;
mod site;
mod markdown;
mod section;
mod pagination;
/// Additional filters for Tera
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
/// 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]

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 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(&section.file_path);
section.path = section.components.join("/");
section.permalink = config.make_permalink(&section.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()
}

View file

@ -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(&section));
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(&section.permalink, &self.tera)?)?;
}
}
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"
description = ""
title = "Index"
+++

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
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);
}