Close #446 Close #260 Close #478 Close #284 Close #480index-subcmd
| @@ -6,6 +6,8 @@ | |||
| - Gutenberg has changed name to REPLACE_ME! | |||
| - The `pagers` variable of Paginator objects has been removed | |||
| - `section.subsections` is now an array of paths to be used with the `get_section` | |||
| Tera function | |||
| ### Others | |||
| - Update dependencies, fixing a few bugs with templates | |||
| @@ -25,6 +27,8 @@ | |||
| - RSS feed now takes all available articles by default instead of limiting to 10000 | |||
| - `templates` directory is now optional | |||
| - Add Reason and F# syntax highlighting | |||
| - Add `parent_section` to pages and section pointing to the relative path of the parent | |||
| section if there is one to be used with the `get_section` Tera function | |||
| ## 0.4.2 (2018-09-03) | |||
| @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; | |||
| use tera::{Tera, Context as TeraContext, Value, Map}; | |||
| use slug::slugify; | |||
| use slotmap::{Key, DenseSlotMap}; | |||
| use slotmap::{Key}; | |||
| use errors::{Result, ResultExt}; | |||
| use config::Config; | |||
| @@ -23,6 +23,7 @@ pub struct SerializingPage<'a> { | |||
| content: &'a str, | |||
| permalink: &'a str, | |||
| slug: &'a str, | |||
| parent_section: Option<String>, | |||
| title: &'a Option<String>, | |||
| description: &'a Option<String>, | |||
| date: &'a Option<String>, | |||
| @@ -47,7 +48,7 @@ pub struct SerializingPage<'a> { | |||
| impl<'a> SerializingPage<'a> { | |||
| /// Grabs all the data from a page, including sibling pages | |||
| pub fn from_page(page: &'a Page, pages: &'a DenseSlotMap<Page>) -> Self { | |||
| pub fn from_page(page: &'a Page, library: &'a Library) -> Self { | |||
| let mut year = None; | |||
| let mut month = None; | |||
| let mut day = None; | |||
| @@ -56,12 +57,15 @@ impl<'a> SerializingPage<'a> { | |||
| month = Some(d.1); | |||
| day = Some(d.2); | |||
| } | |||
| let pages = library.pages(); | |||
| let lighter = page.lighter.map(|k| Box::new(SerializingPage::from_page_basic(pages.get(k).unwrap()))); | |||
| let heavier = page.heavier.map(|k| Box::new(SerializingPage::from_page_basic(pages.get(k).unwrap()))); | |||
| let earlier = page.earlier.map(|k| Box::new(SerializingPage::from_page_basic(pages.get(k).unwrap()))); | |||
| let later = page.later.map(|k| Box::new(SerializingPage::from_page_basic(pages.get(k).unwrap()))); | |||
| let parent_section = page.parent_section.map(|k| library.get_section_by_key(k).file.relative.clone()); | |||
| SerializingPage { | |||
| parent_section, | |||
| content: &page.content, | |||
| permalink: &page.permalink, | |||
| slug: &page.slug, | |||
| @@ -100,6 +104,7 @@ impl<'a> SerializingPage<'a> { | |||
| } | |||
| SerializingPage { | |||
| parent_section: None, | |||
| content: &page.content, | |||
| permalink: &page.permalink, | |||
| slug: &page.slug, | |||
| @@ -133,6 +138,8 @@ pub struct Page { | |||
| pub file: FileInfo, | |||
| /// The front matter meta-data | |||
| pub meta: PageFrontMatter, | |||
| /// The parent section if there is one | |||
| pub parent_section: Option<Key>, | |||
| /// The actual content of the page, in markdown | |||
| pub raw_content: String, | |||
| /// All the non-md files we found next to the .md file | |||
| @@ -177,6 +184,7 @@ impl Page { | |||
| Page { | |||
| file: FileInfo::new_page(file_path), | |||
| meta, | |||
| parent_section: None, | |||
| raw_content: "".to_string(), | |||
| assets: vec![], | |||
| content: "".to_string(), | |||
| @@ -320,7 +328,7 @@ impl Page { | |||
| context.insert("config", config); | |||
| context.insert("current_url", &self.permalink); | |||
| context.insert("current_path", &self.path); | |||
| context.insert("page", &self.to_serialized(library.pages())); | |||
| context.insert("page", &self.to_serialized(library)); | |||
| render_template(&tpl_name, tera, &context, &config.theme) | |||
| .chain_err(|| format!("Failed to render page '{}'", self.file.path.display())) | |||
| @@ -335,8 +343,8 @@ impl Page { | |||
| .collect() | |||
| } | |||
| pub fn to_serialized<'a>(&'a self, pages: &'a DenseSlotMap<Page>) -> SerializingPage<'a> { | |||
| SerializingPage::from_page(self, pages) | |||
| pub fn to_serialized<'a>(&'a self, library: &'a Library) -> SerializingPage<'a> { | |||
| SerializingPage::from_page(self, library) | |||
| } | |||
| pub fn to_serialized_basic(&self) -> SerializingPage { | |||
| @@ -349,6 +357,7 @@ impl Default for Page { | |||
| Page { | |||
| file: FileInfo::default(), | |||
| meta: PageFrontMatter::default(), | |||
| parent_section: None, | |||
| raw_content: "".to_string(), | |||
| assets: vec![], | |||
| content: "".to_string(), | |||
| @@ -21,6 +21,7 @@ use library::Library; | |||
| pub struct SerializingSection<'a> { | |||
| content: &'a str, | |||
| permalink: &'a str, | |||
| parent_section: Option<String>, | |||
| title: &'a Option<String>, | |||
| description: &'a Option<String>, | |||
| extra: &'a HashMap<String, Value>, | |||
| @@ -31,7 +32,7 @@ pub struct SerializingSection<'a> { | |||
| toc: &'a [Header], | |||
| assets: Vec<String>, | |||
| pages: Vec<SerializingPage<'a>>, | |||
| subsections: Vec<SerializingSection<'a>>, | |||
| subsections: Vec<&'a str>, | |||
| } | |||
| impl<'a> SerializingSection<'a> { | |||
| @@ -40,14 +41,17 @@ impl<'a> SerializingSection<'a> { | |||
| let mut subsections = Vec::with_capacity(section.subsections.len()); | |||
| for k in §ion.pages { | |||
| pages.push(library.get_page_by_key(*k).to_serialized(library.pages())); | |||
| pages.push(library.get_page_by_key(*k).to_serialized(library)); | |||
| } | |||
| for k in §ion.subsections { | |||
| subsections.push(library.get_section_by_key(*k).to_serialized(library)); | |||
| subsections.push(library.get_section_path_by_key(*k)); | |||
| } | |||
| let parent_section = section.parent_section.map(|k| library.get_section_by_key(k).file.relative.clone()); | |||
| SerializingSection { | |||
| parent_section, | |||
| content: §ion.content, | |||
| permalink: §ion.permalink, | |||
| title: §ion.meta.title, | |||
| @@ -67,6 +71,7 @@ impl<'a> SerializingSection<'a> { | |||
| /// Same as from_section but doesn't fetch pages and sections | |||
| pub fn from_section_basic(section: &'a Section) -> Self { | |||
| SerializingSection { | |||
| parent_section: None, | |||
| content: §ion.content, | |||
| permalink: §ion.permalink, | |||
| title: §ion.meta.title, | |||
| @@ -106,6 +111,8 @@ pub struct Section { | |||
| pub pages: Vec<Key>, | |||
| /// All pages that cannot be sorted in this section | |||
| pub ignored_pages: Vec<Key>, | |||
| /// The relative path of the parent section if there is one | |||
| pub parent_section: Option<Key>, | |||
| /// All direct subsections | |||
| pub subsections: Vec<Key>, | |||
| /// Toc made from the headers of the markdown file | |||
| @@ -124,6 +131,7 @@ impl Section { | |||
| Section { | |||
| file: FileInfo::new_section(file_path), | |||
| meta, | |||
| parent_section: None, | |||
| path: "".to_string(), | |||
| components: vec![], | |||
| permalink: "".to_string(), | |||
| @@ -262,6 +270,7 @@ impl Default for Section { | |||
| Section { | |||
| file: FileInfo::default(), | |||
| meta: SectionFrontMatter::default(), | |||
| parent_section: None, | |||
| path: "".to_string(), | |||
| components: vec![], | |||
| permalink: "".to_string(), | |||
| @@ -81,7 +81,7 @@ impl Library { | |||
| /// Find out the direct subsections of each subsection if there are some | |||
| /// as well as the pages for each section | |||
| pub fn populate_sections(&mut self) { | |||
| let mut grandparent_paths: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new(); | |||
| let mut grandparent_paths: HashMap<PathBuf, Vec<_>> = HashMap::new(); | |||
| for section in self.sections.values_mut() { | |||
| if let Some(ref grand_parent) = section.file.grand_parent { | |||
| @@ -99,6 +99,7 @@ impl Library { | |||
| let parent_section_path = page.file.parent.join("_index.md"); | |||
| if let Some(section_key) = self.paths_to_sections.get(&parent_section_path) { | |||
| self.sections.get_mut(*section_key).unwrap().pages.push(key); | |||
| page.parent_section = Some(*section_key); | |||
| } | |||
| } | |||
| @@ -109,14 +110,22 @@ impl Library { | |||
| for (key, section) in &self.sections { | |||
| sections_weight.insert(key, section.meta.weight); | |||
| } | |||
| for section in self.sections.values_mut() { | |||
| if let Some(paths) = grandparent_paths.get(§ion.file.parent) { | |||
| section.subsections = paths | |||
| .iter() | |||
| .map(|p| sections[p]) | |||
| .collect::<Vec<_>>(); | |||
| section.subsections | |||
| .sort_by(|a, b| sections_weight[a].cmp(§ions_weight[b])); | |||
| for (grandparent, children) in &grandparent_paths { | |||
| let mut subsections = vec![]; | |||
| let grandparent_path = grandparent.join("_index.md"); | |||
| if let Some(ref mut section) = self.get_section_mut(&grandparent_path) { | |||
| subsections = children.iter().map(|p| sections[p]).collect(); | |||
| subsections.sort_by(|a, b| sections_weight[a].cmp(§ions_weight[b])); | |||
| section.subsections = subsections.clone(); | |||
| } | |||
| // Only there for subsections so we must have a parent section | |||
| for key in &subsections { | |||
| if let Some(ref mut subsection) = self.sections.get_mut(*key) { | |||
| subsection.parent_section = Some(sections[&grandparent_path]); | |||
| } | |||
| } | |||
| } | |||
| } | |||
| @@ -219,6 +228,11 @@ impl Library { | |||
| None | |||
| } | |||
| /// Only used in tests | |||
| pub fn get_section_key(&self, path: &PathBuf) -> Option<&Key> { | |||
| self.paths_to_sections.get(path) | |||
| } | |||
| pub fn get_section(&self, path: &PathBuf) -> Option<&Section> { | |||
| self.sections.get(self.paths_to_sections.get(path).cloned().unwrap_or_default()) | |||
| } | |||
| @@ -231,6 +245,14 @@ impl Library { | |||
| self.sections.get(key).unwrap() | |||
| } | |||
| pub fn get_section_mut_by_key(&mut self, key: Key) -> &mut Section { | |||
| self.sections.get_mut(key).unwrap() | |||
| } | |||
| pub fn get_section_path_by_key(&self, key: Key) -> &str { | |||
| &self.get_section_by_key(key).file.relative | |||
| } | |||
| pub fn get_page(&self, path: &PathBuf) -> Option<&Page> { | |||
| self.pages.get(self.paths_to_pages.get(path).cloned().unwrap_or_default()) | |||
| } | |||
| @@ -241,7 +263,6 @@ impl Library { | |||
| pub fn remove_section(&mut self, path: &PathBuf) -> Option<Section> { | |||
| if let Some(k) = self.paths_to_sections.remove(path) { | |||
| // TODO: delete section from parent subsection if there is one | |||
| self.sections.remove(k) | |||
| } else { | |||
| None | |||
| @@ -250,7 +271,6 @@ impl Library { | |||
| pub fn remove_page(&mut self, path: &PathBuf) -> Option<Page> { | |||
| if let Some(k) = self.paths_to_pages.remove(path) { | |||
| // TODO: delete page from all parent sections | |||
| self.pages.remove(k) | |||
| } else { | |||
| None | |||
| @@ -240,10 +240,8 @@ impl Site { | |||
| } | |||
| self.register_early_global_fns(); | |||
| self.render_markdown()?; | |||
| self.populate_sections(); | |||
| // self.library.cache_all_pages(); | |||
| // self.library.cache_all_sections(); | |||
| self.render_markdown()?; | |||
| self.populate_taxonomies()?; | |||
| self.register_tera_global_fns(); | |||
| @@ -22,10 +22,6 @@ fn can_parse_site() { | |||
| assert_eq!(site.library.pages().len(), 15); | |||
| let posts_path = path.join("content").join("posts"); | |||
| // Make sure we remove all the pwd + content from the sections | |||
| let basic = site.library.get_page(&posts_path.join("simple.md")).unwrap(); | |||
| assert_eq!(basic.file.components, vec!["posts".to_string()]); | |||
| // Make sure the page with a url doesn't have any sections | |||
| let url_post = site.library.get_page(&posts_path.join("fixed-url.md")).unwrap(); | |||
| assert_eq!(url_post.path, "a-fixed-url/"); | |||
| @@ -41,10 +37,17 @@ fn can_parse_site() { | |||
| let index_section = site.library.get_section(&path.join("content").join("_index.md")).unwrap(); | |||
| assert_eq!(index_section.subsections.len(), 3); | |||
| assert_eq!(index_section.pages.len(), 1); | |||
| assert!(index_section.parent_section.is_none()); | |||
| let posts_section = site.library.get_section(&posts_path.join("_index.md")).unwrap(); | |||
| assert_eq!(posts_section.subsections.len(), 1); | |||
| assert_eq!(posts_section.pages.len(), 7); | |||
| assert_eq!(posts_section.parent_section, Some(*site.library.get_section_key(&index_section.file.path).unwrap())); | |||
| // Make sure we remove all the pwd + content from the sections | |||
| let basic = site.library.get_page(&posts_path.join("simple.md")).unwrap(); | |||
| assert_eq!(basic.file.components, vec!["posts".to_string()]); | |||
| assert_eq!(basic.parent_section, Some(*site.library.get_section_key(&posts_section.file.path).unwrap())); | |||
| let tutorials_section = site.library.get_section(&posts_path.join("tutorials").join("_index.md")).unwrap(); | |||
| assert_eq!(tutorials_section.subsections.len(), 2); | |||
| @@ -57,6 +60,7 @@ fn can_parse_site() { | |||
| let devops_section = site.library.get_section(&posts_path.join("tutorials").join("devops").join("_index.md")).unwrap(); | |||
| assert_eq!(devops_section.subsections.len(), 0); | |||
| assert_eq!(devops_section.pages.len(), 2); | |||
| assert_eq!(devops_section.parent_section, Some(*site.library.get_section_key(&tutorials_section.file.path).unwrap())); | |||
| let prog_section = site.library.get_section(&posts_path.join("tutorials").join("programming").join("_index.md")).unwrap(); | |||
| assert_eq!(prog_section.subsections.len(), 0); | |||
| @@ -56,7 +56,7 @@ pub fn make_get_page(library: &Library) -> GlobalFn { | |||
| for page in library.pages_values() { | |||
| pages.insert( | |||
| page.file.relative.clone(), | |||
| to_value(library.get_page(&page.file.path).unwrap().to_serialized(library.pages())).unwrap(), | |||
| to_value(library.get_page(&page.file.path).unwrap().to_serialized(library)).unwrap(), | |||
| ); | |||
| } | |||
| @@ -45,6 +45,8 @@ month: Number?; | |||
| day: Number?; | |||
| // Paths of colocated assets, relative to the content directory | |||
| assets: Array<String>; | |||
| // The relative path of the parent section if existing, for use with the `get_section` Tera function | |||
| parent_section: String?; | |||
| ``` | |||
| ## Section variables | |||
| @@ -70,7 +72,9 @@ extra: HashMap<String, Any>; | |||
| // Pages directly in this section, sorted if asked | |||
| pages: Array<Pages>; | |||
| // Direct subsections to this section, sorted by subsections weight | |||
| subsections: Array<Section>; | |||
| // This only contains the path to use in the `get_section` Tera function to get | |||
| // the actual section object if you need it | |||
| subsections: Array<String>; | |||
| // Unicode word count | |||
| word_count: Number; | |||
| // Based on https://help.medium.com/hc/en-us/articles/214991667-Read-time | |||
| @@ -79,6 +83,8 @@ reading_time: Number; | |||
| toc: Array<Header>; | |||
| // Paths of colocated assets, relative to the content directory | |||
| assets: Array<String>; | |||
| // The relative path of the parent section if existing, for use with the `get_section` Tera function | |||
| parent_section: String?; | |||
| ``` | |||
| ## Table of contents | |||
| @@ -8,7 +8,8 @@ | |||
| <div class="documentation"> | |||
| <aside class="documentation__sidebar"> | |||
| <ul> | |||
| {% for subsection in section.subsections %} | |||
| {% for p in section.subsections %} | |||
| {% set subsection = get_section(path=p) %} | |||
| <li> | |||
| <span class="documentation__sidebar__title">{{ subsection.title }}</span> | |||
| <ul> | |||
| @@ -4,7 +4,8 @@ | |||
| {% for page in section.pages %} | |||
| {{page.title}} | |||
| {% endfor %} | |||
| {% for subsection in section.subsections %} | |||
| {% for sub in section.subsections %} | |||
| {% set subsection = get_section(path=sub) %} | |||
| {{subsection.title}} | |||
| Sub-pages: {{subsection.pages | length}} | |||
| {% endfor %} | |||