commit
c9c6b4c808
22
Cargo.lock
generated
22
Cargo.lock
generated
|
@ -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;
|
||||
|
||||
|
|
12
src/page.rs
12
src/page.rs
|
@ -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
244
src/pagination.rs
Normal 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(¤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()
|
||||
}
|
||||
|
|
112
src/site.rs
112
src/site.rs
|
@ -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(())
|
||||
|
|
8
src/templates/internal/alias.html
Normal file
8
src/templates/internal/alias.html
Normal 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>
|
|
@ -1,4 +1,3 @@
|
|||
+++
|
||||
title = "Home"
|
||||
description = ""
|
||||
title = "Index"
|
||||
+++
|
||||
|
|
33
test_site/templates/index_paginated.html
Normal file
33
test_site/templates/index_paginated.html
Normal 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>
|
17
test_site/templates/section_paginated.html
Normal file
17
test_site/templates/section_paginated.html
Normal 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 %}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue