From a3318d4b56f54ddbaf5ba464a8af00b9868b90a0 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Wed, 3 May 2017 17:52:49 +0900 Subject: [PATCH] Pagination --- Cargo.lock | 22 +- README.md | 8 +- src/cmd/serve.rs | 6 +- src/front_matter.rs | 26 ++- src/lib.rs | 1 + src/page.rs | 12 +- src/pagination.rs | 244 +++++++++++++++++++++ src/section.rs | 39 +++- src/site.rs | 112 ++++++++-- src/templates/internal/alias.html | 8 + test_site/content/_index.md | 3 +- test_site/templates/index_paginated.html | 33 +++ test_site/templates/section_paginated.html | 17 ++ tests/site.rs | 97 +++++++- 14 files changed, 566 insertions(+), 62 deletions(-) create mode 100644 src/pagination.rs create mode 100644 src/templates/internal/alias.html create mode 100644 test_site/templates/index_paginated.html create mode 100644 test_site/templates/section_paginated.html diff --git a/Cargo.lock b/Cargo.lock index 8d178e9b..83554080 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/README.md b/README.md index 0df38ae3..58f0bb1b 100644 --- a/README.md +++ b/README.md @@ -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`. diff --git a/src/cmd/serve.rs b/src/cmd/serve.rs index 9bf2a8af..fb16ee2f 100644 --- a/src/cmd/serve.rs +++ b/src/cmd/serve.rs @@ -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"); diff --git a/src/front_matter.rs b/src/front_matter.rs index d8e45264..1e437fb1 100644 --- a/src/front_matter.rs +++ b/src/front_matter.rs @@ -44,7 +44,7 @@ pub struct FrontMatter { pub draft: Option, /// Only one category allowed pub category: Option, - /// 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, /// 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, + /// 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, + /// Path to be used by pagination: the page number will be appended after it. Defaults to `page`. + #[serde(skip_serializing)] + pub paginate_path: Option, /// Any extra parameter present in the front matter pub extra: Option>, } @@ -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, } } diff --git a/src/lib.rs b/src/lib.rs index 5c73dc1d..5965712e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,6 +27,7 @@ mod front_matter; mod site; mod markdown; mod section; +mod pagination; /// Additional filters for Tera mod filters; diff --git a/src/page.rs b/src/page.rs index 80264d37..6fdad491 100644 --- a/src/page.rs +++ b/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, section: Option<&Section>) -> (Vec, Vec) { - 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] diff --git a/src/pagination.rs b/src/pagination.rs new file mode 100644 index 00000000..1d8bc8f4 --- /dev/null +++ b/src/pagination.rs @@ -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>, + /// 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::>(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::>(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 { + 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::>(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::>(None).unwrap()); + assert_eq!(context["previous"], to_value("https://vincent.is/posts").unwrap()); + assert_eq!(context["current_index"], to_value(2).unwrap()); + } +} diff --git a/src/section.rs b/src/section.rs index adc36372..12e5eb63 100644 --- a/src/section.rs +++ b/src/section.rs @@ -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>(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 { - 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() } diff --git a/src/site.rs b/src/site.rs index e9069390..fccdf353 100644 --- a/src/site.rs +++ b/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 { + 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::>()); - 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::>()); + 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(()) diff --git a/src/templates/internal/alias.html b/src/templates/internal/alias.html new file mode 100644 index 00000000..36fb8d9e --- /dev/null +++ b/src/templates/internal/alias.html @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/test_site/content/_index.md b/test_site/content/_index.md index a51bfdc4..e2be969a 100644 --- a/test_site/content/_index.md +++ b/test_site/content/_index.md @@ -1,4 +1,3 @@ +++ -title = "Home" -description = "" +title = "Index" +++ diff --git a/test_site/templates/index_paginated.html b/test_site/templates/index_paginated.html new file mode 100644 index 00000000..02aadcd8 --- /dev/null +++ b/test_site/templates/index_paginated.html @@ -0,0 +1,33 @@ + + + + + + + + + + + {{ config.title }} + + + +
+ {% block content %} +
+ {% for page in paginator.pages %} + + {% 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 }} +
+ {% endblock content %} +
+ + diff --git a/test_site/templates/section_paginated.html b/test_site/templates/section_paginated.html new file mode 100644 index 00000000..cf745143 --- /dev/null +++ b/test_site/templates/section_paginated.html @@ -0,0 +1,17 @@ +{% extends "index.html" %} + +{% block content %} + {% for page in paginator.pages %} + {{page.title}} + {% endfor %} + {% for pager in paginator.pagers %} + {{pager.index}}: {{pager.path | safe }} + {% endfor %} + Num pages: {{ paginator.pages | length }} + Page size: {{ paginator.paginate_by }} + Current index: {{ paginator.current_index }} + First: {{ paginator.first | safe }} + Last: {{ paginator.last | safe }} + {% if paginator.previous %}has_prev{% endif%} + {% if paginator.next %}has_next{% endif%} +{% endblock content %} diff --git a/tests/site.rs b/tests/site.rs index 1dd2e04b..03ed8665 100644 --- a/tests/site.rs +++ b/tests/site.rs @@ -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", "