Add a FileInfo struct to hold some common data about the files

This commit is contained in:
Vincent Prouillet 2017-05-15 19:53:39 +09:00
parent d9ed7df118
commit 056bf55881
11 changed files with 190 additions and 177 deletions

View file

@ -85,7 +85,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
// A section was deleted, many things can be impacted:
// - the pages of the section are becoming orphans
// - any page that was referencing the section (index, etc)
let relative_path = site.sections[path].relative_path.clone();
let relative_path = site.sections[path].file.relative.clone();
// Remove the link to it and the section itself from the Site
site.permalinks.remove(&relative_path);
site.sections.remove(path);
@ -94,7 +94,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
// A page was deleted, many things can be impacted:
// - the section the page is in
// - any page that was referencing the section (index, etc)
let relative_path = site.pages[path].relative_path.clone();
let relative_path = site.pages[path].file.relative.clone();
site.permalinks.remove(&relative_path);
if let Some(p) = site.pages.remove(path) {
if p.meta.has_tags() || p.meta.category.is_some() {
@ -172,7 +172,7 @@ pub fn after_content_change(site: &mut Site, path: &Path) -> Result<()> {
},
PageChangesNeeded::Sort => {
let section_path = match site.find_parent_section(&site.pages[path]) {
Some(s) => s.file_path.clone(),
Some(s) => s.file.path.clone(),
None => continue // Do nothing if it's an orphan page
};
site.populate_sections();

116
src/content/file_info.rs Normal file
View file

@ -0,0 +1,116 @@
use std::path::{Path, PathBuf};
/// Takes a full path to a file and returns only the components after the first `content` directory
/// Will not return the filename as last component
pub fn find_content_components<P: AsRef<Path>>(path: P) -> Vec<String> {
let path = path.as_ref();
let mut is_in_content = false;
let mut components = vec![];
for section in path.parent().unwrap().components() {
let component = section.as_ref().to_string_lossy();
if is_in_content {
components.push(component.to_string());
continue;
}
if component == "content" {
is_in_content = true;
}
}
components
}
/// Struct that contains all the information about the actual file
#[derive(Debug, Clone, PartialEq)]
pub struct FileInfo {
/// The full path to the .md file
pub path: PathBuf,
/// The name of the .md file without the extension, always `_index` for sections
pub name: String,
/// The .md path, starting from the content directory, with `/` slashes
pub relative: String,
/// Path of the directory containing the .md file
pub parent: PathBuf,
/// Path of the grand parent directory for that file. Only used in sections to find subsections.
pub grand_parent: Option<PathBuf>,
/// The folder names to this section file, starting from the `content` directory
/// For example a file at content/kb/solutions/blabla.md will have 2 components:
/// `kb` and `solutions`
pub components: Vec<String>,
}
impl FileInfo {
pub fn new_page(path: &Path) -> FileInfo {
let file_path = path.to_path_buf();
let mut parent = file_path.parent().unwrap().to_path_buf();
let name = path.file_stem().unwrap().to_string_lossy().to_string();
let mut components = find_content_components(&file_path);
let relative = format!("{}/{}.md", components.join("/"), name);
// If we have a folder with an asset, don't consider it as a component
if !components.is_empty() && name == "index" {
components.pop();
// also set parent_path to grandparent instead
parent = parent.parent().unwrap().to_path_buf();
}
FileInfo {
path: file_path,
// We don't care about grand parent for pages
grand_parent: None,
parent,
name,
components,
relative,
}
}
pub fn new_section(path: &Path) -> FileInfo {
let parent = path.parent().unwrap().to_path_buf();
let components = find_content_components(path);
let relative = if components.is_empty() {
// the index one
"_index.md".to_string()
} else {
format!("{}/_index.md", components.join("/"))
};
let grand_parent = parent.parent().map(|p| p.to_path_buf());
FileInfo {
path: path.to_path_buf(),
parent,
grand_parent,
name: "_index".to_string(),
components,
relative,
}
}
}
#[doc(hidden)]
impl Default for FileInfo {
fn default() -> FileInfo {
FileInfo {
path: PathBuf::new(),
parent: PathBuf::new(),
grand_parent: None,
name: String::new(),
components: vec![],
relative: String::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::find_content_components;
#[test]
fn can_find_content_components() {
let res = find_content_components("/home/vincent/code/site/content/posts/tutorials/python.md");
assert_eq!(res, ["posts".to_string(), "tutorials".to_string()]);
}
}

View file

@ -1,11 +1,9 @@
// TODO: move section/page and maybe pagination in this mod
// Not sure where pagination stands if I add a render mod
mod page;
mod pagination;
mod section;
mod sorting;
mod utils;
mod file_info;
pub use self::page::{Page};
pub use self::section::{Section};

View file

@ -13,34 +13,22 @@ use config::Config;
use front_matter::{PageFrontMatter, split_page_content};
use markdown::markdown_to_html;
use utils::{read_file};
use content::utils::{find_related_assets, find_content_components, get_reading_analytics};
use content::utils::{find_related_assets, get_reading_analytics};
use content::file_info::FileInfo;
#[derive(Clone, Debug, PartialEq)]
pub struct Page {
/// All info about the actual file
pub file: FileInfo,
/// The front matter meta-data
pub meta: PageFrontMatter,
/// The .md path
pub file_path: PathBuf,
/// The .md path, starting from the content directory, with / slashes
pub relative_path: String,
/// The parent directory of the file. Is actually the grand parent directory
/// if it's an asset folder
pub parent_path: PathBuf,
/// The name of the .md file
pub file_name: String,
/// The directories above our .md file
/// for example a file at content/kb/solutions/blabla.md will have 2 components:
/// `kb` and `solutions`
pub components: Vec<String>,
/// The actual content of the page, in markdown
pub raw_content: String,
/// All the non-md files we found next to the .md file
pub assets: Vec<PathBuf>,
/// The HTML rendered of the page
pub content: String,
/// The slug of that page.
/// First tries to find the slug in the meta and defaults to filename otherwise
pub slug: String,
@ -52,7 +40,6 @@ pub struct Page {
/// When <!-- more --> is found in the text, will take the content up to that part
/// as summary
pub summary: Option<String>,
/// The previous page, by whatever sorting is used for the index/section
pub previous: Option<Box<Page>>,
/// The next page, by whatever sorting is used for the index/section
@ -61,14 +48,12 @@ pub struct Page {
impl Page {
pub fn new(meta: PageFrontMatter) -> Page {
pub fn new<P: AsRef<Path>>(file_path: P, meta: PageFrontMatter) -> Page {
let file_path = file_path.as_ref();
Page {
file: FileInfo::new_page(file_path),
meta: meta,
file_path: PathBuf::new(),
relative_path: String::new(),
parent_path: PathBuf::new(),
file_name: "".to_string(),
components: vec![],
raw_content: "".to_string(),
assets: vec![],
content: "".to_string(),
@ -85,49 +70,26 @@ impl Page {
/// Files without front matter or with invalid front matter are considered
/// erroneous
pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Page> {
// 1. separate front matter from content
let (meta, content) = split_page_content(file_path, content)?;
let mut page = Page::new(meta);
page.file_path = file_path.to_path_buf();
page.parent_path = page.file_path.parent().unwrap().to_path_buf();
let mut page = Page::new(file_path, meta);
page.raw_content = content;
let path = Path::new(file_path);
page.file_name = path.file_stem().unwrap().to_string_lossy().to_string();
page.slug = {
if let Some(ref slug) = page.meta.slug {
slug.trim().to_string()
} else {
slugify(page.file_name.clone())
slugify(page.file.name.clone())
}
};
page.components = find_content_components(&page.file_path);
page.relative_path = format!("{}/{}.md", page.components.join("/"), page.file_name);
// 4. Find sections
// Pages with custom urls exists outside of sections
let mut path_set = false;
if let Some(ref u) = page.meta.url {
page.path = u.trim().to_string();
path_set = true;
} else {
page.path = if page.file.components.is_empty() {
page.slug.clone()
} else {
format!("{}/{}", page.file.components.join("/"), page.slug)
};
}
if !page.components.is_empty() {
// If we have a folder with an asset, don't consider it as a component
if page.file_name == "index" {
page.components.pop();
// also set parent_path to grandparent instead
page.parent_path = page.parent_path.parent().unwrap().to_path_buf();
}
if !path_set {
// Don't add a trailing slash to sections
page.path = format!("{}/{}", page.components.join("/"), page.slug);
}
} else if !path_set {
page.path = page.slug.clone();
}
page.permalink = config.make_permalink(&page.path);
Ok(page)
@ -140,7 +102,7 @@ impl Page {
let mut page = Page::parse(path, &content, config)?;
page.assets = find_related_assets(path.parent().unwrap());
if !page.assets.is_empty() && page.file_name != "index" {
if !page.assets.is_empty() && page.file.name != "index" {
bail!("Page `{}` has assets ({:?}) but is not named index.md", path.display(), page.assets);
}
@ -177,19 +139,15 @@ impl Page {
context.add("current_path", &self.path);
tera.render(&tpl_name, &context)
.chain_err(|| format!("Failed to render page '{}'", self.file_path.display()))
.chain_err(|| format!("Failed to render page '{}'", self.file.path.display()))
}
}
impl Default for Page {
fn default() -> Page {
Page {
file: FileInfo::default(),
meta: PageFrontMatter::default(),
file_path: PathBuf::new(),
relative_path: String::new(),
parent_path: PathBuf::new(),
file_name: "".to_string(),
components: vec![],
raw_content: "".to_string(),
assets: vec![],
content: "".to_string(),
@ -352,7 +310,7 @@ Hello world
);
assert!(res.is_ok());
let page = res.unwrap();
assert_eq!(page.parent_path, path.join("content").join("posts"));
assert_eq!(page.file.parent, path.join("content").join("posts"));
}
#[test]

View file

@ -145,7 +145,7 @@ impl<'a> Paginator<'a> {
}
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()))
.chain_err(|| format!("Failed to render pager {} of section '{}'", pager.index, self.section.file.path.display()))
}
}
@ -166,7 +166,7 @@ mod tests {
if !is_index {
s.path = "posts".to_string();
s.permalink = "https://vincent.is/posts".to_string();
s.components = vec!["posts".to_string()];
s.file.components = vec!["posts".to_string()];
} else {
s.permalink = "https://vincent.is".to_string();
}

View file

@ -11,21 +11,15 @@ use errors::{Result, ResultExt};
use utils::{read_file};
use markdown::markdown_to_html;
use content::Page;
use content::utils::find_content_components;
use content::file_info::FileInfo;
#[derive(Clone, Debug, PartialEq)]
pub struct Section {
/// All info about the actual file
pub file: FileInfo,
/// The front matter meta-data
pub meta: SectionFrontMatter,
/// The _index.md full path
pub file_path: PathBuf,
/// The .md path, starting from the content directory, with / slashes
pub relative_path: String,
/// Path of the directory containing the _index.md file
pub parent_path: PathBuf,
/// The folder names from `content` to this section file
pub components: Vec<String>,
/// The URL path of the page
pub path: String,
/// The full URL for that page
@ -47,11 +41,8 @@ impl Section {
let file_path = file_path.as_ref();
Section {
file: FileInfo::new_section(file_path),
meta: meta,
file_path: file_path.to_path_buf(),
relative_path: "".to_string(),
parent_path: file_path.parent().unwrap().to_path_buf(),
components: vec![],
path: "".to_string(),
permalink: "".to_string(),
raw_content: "".to_string(),
@ -66,16 +57,8 @@ impl Section {
let (meta, content) = split_section_content(file_path, content)?;
let mut section = Section::new(file_path, meta);
section.raw_content = content.clone();
section.components = find_content_components(&section.file_path);
section.path = section.components.join("/");
section.path = section.file.components.join("/");
section.permalink = config.make_permalink(&section.path);
if section.components.is_empty() {
// the index one
section.relative_path = "_index.md".to_string();
} else {
section.relative_path = format!("{}/_index.md", section.components.join("/"));
}
Ok(section)
}
@ -120,46 +103,36 @@ impl Section {
}
tera.render(&tpl_name, &context)
.chain_err(|| format!("Failed to render section '{}'", self.file_path.display()))
.chain_err(|| format!("Failed to render section '{}'", self.file.path.display()))
}
/// Is this the index section?
pub fn is_index(&self) -> bool {
self.components.is_empty()
self.file.components.is_empty()
}
/// Returns all the paths for the pages belonging to that section
/// Returns all the paths of the pages belonging to that section
pub fn all_pages_path(&self) -> Vec<PathBuf> {
let mut paths = vec![];
paths.extend(self.pages.iter().map(|p| p.file_path.clone()));
paths.extend(self.ignored_pages.iter().map(|p| p.file_path.clone()));
paths.extend(self.pages.iter().map(|p| p.file.path.clone()));
paths.extend(self.ignored_pages.iter().map(|p| p.file.path.clone()));
paths
}
/// Whether the page given belongs to that section
pub fn is_child_page(&self, page: &Page) -> bool {
for p in &self.pages {
if p.file_path == page.file_path {
return true;
}
}
for p in &self.ignored_pages {
if p.file_path == page.file_path {
return true;
}
}
false
self.all_pages_path().contains(&page.file.path)
}
}
impl ser::Serialize for Section {
fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer {
let mut state = serializer.serialize_struct("section", 7)?;
let mut state = serializer.serialize_struct("section", 9)?;
state.serialize_field("content", &self.content)?;
state.serialize_field("permalink", &self.permalink)?;
state.serialize_field("title", &self.meta.title)?;
state.serialize_field("description", &self.meta.description)?;
state.serialize_field("extra", &self.meta.extra)?;
state.serialize_field("path", &format!("/{}", self.path))?;
state.serialize_field("permalink", &self.permalink)?;
state.serialize_field("pages", &self.pages)?;
@ -168,15 +141,12 @@ impl ser::Serialize for Section {
}
}
impl Default for Section {
/// Used to create a default index section if there is no _index.md in the root content directory
impl Default for Section {
fn default() -> Section {
Section {
file: FileInfo::default(),
meta: SectionFrontMatter::default(),
file_path: PathBuf::new(),
relative_path: "".to_string(),
parent_path: PathBuf::new(),
components: vec![],
path: "".to_string(),
permalink: "".to_string(),
raw_content: "".to_string(),

View file

@ -81,13 +81,13 @@ mod tests {
fn create_page_with_date(date: &str) -> Page {
let mut front_matter = PageFrontMatter::default();
front_matter.date = Some(date.to_string());
Page::new(front_matter)
Page::new("content/hello.md", front_matter)
}
fn create_page_with_order(order: usize) -> Page {
let mut front_matter = PageFrontMatter::default();
front_matter.order = Some(order);
Page::new(front_matter)
Page::new("content/hello.md", front_matter)
}
#[test]

View file

@ -32,37 +32,13 @@ pub fn get_reading_analytics(content: &str) -> (usize, usize) {
(word_count, (word_count / 200))
}
/// Takes a full path to a .md and returns only the components after the first `content` directory
/// Will not return the filename as last component
pub fn find_content_components<P: AsRef<Path>>(path: P) -> Vec<String> {
let path = path.as_ref();
let mut is_in_content = false;
let mut components = vec![];
for section in path.parent().unwrap().components() {
let component = section.as_ref().to_string_lossy();
if is_in_content {
components.push(component.to_string());
continue;
}
if component == "content" {
is_in_content = true;
}
}
components
}
#[cfg(test)]
mod tests {
use std::fs::File;
use tempdir::TempDir;
use super::{find_related_assets, find_content_components, get_reading_analytics};
use super::{find_related_assets, get_reading_analytics};
#[test]
fn can_find_related_assets() {
@ -97,10 +73,4 @@ mod tests {
assert_eq!(word_count, 2000);
assert_eq!(reading_time, 10);
}
#[test]
fn can_find_content_components() {
let res = find_content_components("/home/vincent/code/site/content/posts/tutorials/python.md");
assert_eq!(res, ["posts".to_string(), "tutorials".to_string()]);
}
}

View file

@ -93,7 +93,7 @@ impl Site {
pub fn get_ignored_pages(&self) -> Vec<PathBuf> {
self.sections
.values()
.flat_map(|s| s.ignored_pages.iter().map(|p| p.file_path.clone()))
.flat_map(|s| s.ignored_pages.iter().map(|p| p.file.path.clone()))
.collect()
}
@ -107,7 +107,7 @@ impl Site {
}
for page in self.pages.values() {
if !pages_in_sections.contains(&page.file_path) {
if !pages_in_sections.contains(&page.file.path) {
orphans.push(page);
}
}
@ -146,8 +146,8 @@ impl Site {
self.add_page(path, false)?;
}
}
// Insert a default index section so we don't need to create a _index.md to render
// the index page
// Insert a default index section if necessary so we don't need to create
// a _index.md to render the index page
let index_path = self.base_path.join("content").join("_index.md");
if !self.sections.contains_key(&index_path) {
let mut index_section = Section::default();
@ -178,8 +178,8 @@ impl Site {
/// Returns the previous page struct if there was one
pub fn add_page(&mut self, path: &Path, render: bool) -> Result<Option<Page>> {
let page = Page::from_file(&path, &self.config)?;
self.permalinks.insert(page.relative_path.clone(), page.permalink.clone());
let prev = self.pages.insert(page.file_path.clone(), page);
self.permalinks.insert(page.file.relative.clone(), page.permalink.clone());
let prev = self.pages.insert(page.file.path.clone(), page);
if render {
let mut page = self.pages.get_mut(path).unwrap();
@ -192,11 +192,11 @@ impl Site {
/// Add a section to the site
/// The `render` parameter is used in the serve command, when rebuilding a page.
/// If `true`, it will also render the markdown for that page
/// Returns the previous page struct if there was one
/// Returns the previous section struct if there was one
pub fn add_section(&mut self, path: &Path, render: bool) -> Result<Option<Section>> {
let section = Section::from_file(path, &self.config)?;
self.permalinks.insert(section.relative_path.clone(), section.permalink.clone());
let prev = self.sections.insert(section.file_path.clone(), section);
self.permalinks.insert(section.file.relative.clone(), section.permalink.clone());
let prev = self.sections.insert(section.file.path.clone(), section);
if render {
let mut section = self.sections.get_mut(path).unwrap();
@ -211,7 +211,7 @@ impl Site {
pub fn populate_sections(&mut self) {
let mut grandparent_paths = HashMap::new();
for section in self.sections.values_mut() {
if let Some(grand_parent) = section.parent_path.parent() {
if let Some(ref grand_parent) = section.file.grand_parent {
grandparent_paths.entry(grand_parent.to_path_buf()).or_insert_with(|| vec![]).push(section.clone());
}
// Make sure the pages of a section are empty since we can call that many times on `serve`
@ -220,13 +220,14 @@ impl Site {
}
for page in self.pages.values() {
if self.sections.contains_key(&page.parent_path.join("_index.md")) {
self.sections.get_mut(&page.parent_path.join("_index.md")).unwrap().pages.push(page.clone());
let parent_section_path = page.file.parent.join("_index.md");
if self.sections.contains_key(&parent_section_path) {
self.sections.get_mut(&parent_section_path).unwrap().pages.push(page.clone());
}
}
for section in self.sections.values_mut() {
match grandparent_paths.get(&section.parent_path) {
match grandparent_paths.get(&section.file.parent) {
Some(paths) => section.subsections.extend(paths.clone()),
None => continue,
};
@ -257,7 +258,7 @@ impl Site {
self.categories
.entry(category.to_string())
.or_insert_with(|| vec![])
.push(page.file_path.clone());
.push(page.file.path.clone());
}
if let Some(ref tags) = page.meta.tags {
@ -265,7 +266,7 @@ impl Site {
self.tags
.entry(tag.to_string())
.or_insert_with(|| vec![])
.push(page.file_path.clone());
.push(page.file.path.clone());
}
}
}
@ -554,7 +555,7 @@ impl Site {
fn get_sections_map(&self) -> HashMap<String, Section> {
self.sections
.values()
.map(|s| (s.components.join("/"), s.clone()))
.map(|s| (s.file.components.join("/"), s.clone()))
.collect()
}
@ -564,7 +565,7 @@ impl Site {
let public = self.output_path.clone();
let mut output_path = public.to_path_buf();
for component in &section.components {
for component in &section.file.components {
output_path.push(component);
if !output_path.exists() {

View file

@ -9,7 +9,7 @@ use content::Page;
pub fn make_get_page(all_pages: &HashMap<PathBuf, Page>) -> GlobalFn {
let mut pages = HashMap::new();
for page in all_pages.values() {
pages.insert(page.relative_path.clone(), page.clone());
pages.insert(page.file.relative.clone(), page.clone());
}
Box::new(move |args| -> Result<Value> {

View file

@ -12,7 +12,7 @@ use gutenberg::{Site};
#[test]
fn test_can_parse_site() {
fn can_parse_site() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@ -24,7 +24,7 @@ fn test_can_parse_site() {
// Make sure we remove all the pwd + content from the sections
let basic = &site.pages[&posts_path.join("simple.md")];
assert_eq!(basic.components, vec!["posts".to_string()]);
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.pages[&posts_path.join("fixed-url.md")];
@ -32,7 +32,7 @@ fn test_can_parse_site() {
// Make sure the article in a folder with only asset doesn't get counted as a section
let asset_folder_post = &site.pages[&posts_path.join("with-assets").join("index.md")];
assert_eq!(asset_folder_post.components, vec!["posts".to_string()]);
assert_eq!(asset_folder_post.file.components, vec!["posts".to_string()]);
// That we have the right number of sections
assert_eq!(site.sections.len(), 6);
@ -89,7 +89,7 @@ macro_rules! file_contains {
}
#[test]
fn test_can_build_site_without_live_reload() {
fn can_build_site_without_live_reload() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@ -131,7 +131,7 @@ fn test_can_build_site_without_live_reload() {
}
#[test]
fn test_can_build_site_with_live_reload() {
fn can_build_site_with_live_reload() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@ -169,7 +169,7 @@ fn test_can_build_site_with_live_reload() {
}
#[test]
fn test_can_build_site_with_categories() {
fn can_build_site_with_categories() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@ -221,7 +221,7 @@ fn test_can_build_site_with_categories() {
}
#[test]
fn test_can_build_site_with_tags() {
fn can_build_site_with_tags() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@ -273,7 +273,7 @@ fn test_can_build_site_with_tags() {
}
#[test]
fn test_can_build_site_and_insert_anchor_links() {
fn can_build_site_and_insert_anchor_links() {
let mut path = env::current_dir().unwrap().to_path_buf();
path.push("test_site");
let mut site = Site::new(&path, "config.toml").unwrap();
@ -290,7 +290,7 @@ fn test_can_build_site_and_insert_anchor_links() {
}
#[test]
fn test_can_build_site_with_pagination_for_section() {
fn 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();
@ -349,7 +349,7 @@ fn test_can_build_site_with_pagination_for_section() {
}
#[test]
fn test_can_build_site_with_pagination_for_index() {
fn 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();