From a147e68f7876265ff556e1742f25b7337768ed8b Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Tue, 13 Dec 2016 15:22:24 +0900 Subject: [PATCH] Start building sites --- src/cmd/build.rs | 38 ++++++++++++++++----- src/cmd/new.rs | 10 ++---- src/config.rs | 7 +--- src/main.rs | 10 ++++-- src/page.rs | 86 ++++++++++++++++++++++++++++++++++++++++-------- src/utils.rs | 12 +++++++ 6 files changed, 126 insertions(+), 37 deletions(-) create mode 100644 src/utils.rs diff --git a/src/cmd/build.rs b/src/cmd/build.rs index 0f7bdeb..1f93417 100644 --- a/src/cmd/build.rs +++ b/src/cmd/build.rs @@ -1,28 +1,50 @@ +use std::fs::{create_dir, remove_dir_all}; +use std::path::Path; + use glob::glob; use tera::Tera; use config:: Config; use errors::{Result, ResultExt}; use page::Page; +use utils::create_file; pub fn build(config: Config) -> Result<()> { + if Path::new("public").exists() { + // Delete current `public` directory so we can start fresh + remove_dir_all("public").chain_err(|| "Couldn't delete `public` directory")?; + } + let tera = Tera::new("layouts/**/*").chain_err(|| "Error parsing templates")?; - let mut pages: Vec = vec![]; + // let mut pages: Vec = vec![]; + + // ok we got all the pages HTML, time to write them down to disk + create_dir("public")?; + let public = Path::new("public"); // hardcoded pattern so can't error for entry in glob("content/**/*.md").unwrap().filter_map(|e| e.ok()) { let path = entry.as_path(); - // Remove the content string from name - let filepath = path.to_string_lossy().replace("content/", ""); - pages.push(Page::from_file(&filepath)?); - } + let mut page = Page::from_file(&path)?; - for page in pages { - let html = page.render_html(&tera, &config) - .chain_err(|| format!("Failed to render '{}'", page.filepath))?; + let mut current_path = public.clone().to_path_buf(); + for section in &page.sections { + current_path.push(section); + //current_path = current_path.join(section).as_path(); + if !current_path.exists() { + println!("Creating {:?} folder", current_path); + create_dir(¤t_path)?; + // TODO: create section index.html + // create_file(current_path.join("index.html"), ""); + } + } + current_path.push(&page.filename); + create_dir(¤t_path)?; + create_file(current_path.join("index.html"), &page.render_html(&tera, &config)?)?; } + Ok(()) } diff --git a/src/cmd/new.rs b/src/cmd/new.rs index 8f7edde..87fe628 100644 --- a/src/cmd/new.rs +++ b/src/cmd/new.rs @@ -1,9 +1,9 @@ -use std::io::prelude::*; -use std::fs::{create_dir, File}; +use std::fs::{create_dir}; use std::path::Path; use errors::Result; +use utils::create_file; const CONFIG: &'static str = r#" @@ -33,9 +33,3 @@ pub fn create_new_project>(name: P) -> Result<()> { Ok(()) } - -fn create_file>(path: P, content: &str) -> Result<()> { - let mut file = File::create(&path)?; - file.write_all(content.as_bytes())?; - Ok(()) -} diff --git a/src/config.rs b/src/config.rs index 8eb12d5..5d8fcea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -12,7 +12,6 @@ use errors::{Result, ErrorKind, ResultExt}; pub struct Config { pub title: String, pub base_url: String, - pub theme: String, pub favicon: Option, } @@ -22,7 +21,6 @@ impl Default for Config { Config { title: "".to_string(), base_url: "".to_string(), - theme: "".to_string(), favicon: None, } @@ -48,11 +46,8 @@ impl Config { return Ok(config); } else { - // TODO: handle error in parsing TOML - println!("parse errors: {:?}", parser.errors); + bail!("Errors parsing front matter: {:?}", parser.errors); } - - unreachable!() } pub fn from_file>(path: P) -> Result { diff --git a/src/main.rs b/src/main.rs index fc6342c..18d46fe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ extern crate tera; extern crate glob; +mod utils; mod config; mod errors; mod cmd; @@ -56,6 +57,7 @@ fn main() { match cmd::create_new_project(matches.value_of("name").unwrap()) { Ok(()) => { println!("Project created"); + println!("You will now need to set a theme in `config.toml`"); }, Err(e) => { println!("Error: {}", e); @@ -66,10 +68,14 @@ fn main() { ("build", Some(_)) => { match cmd::build(get_config()) { Ok(()) => { - println!("Project built"); + println!("Project built."); }, Err(e) => { - println!("Error: {}", e.iter().nth(1).unwrap().description()); + println!("Failed to build the site"); + println!("Error: {}", e); + for e in e.iter().skip(1) { + println!("Reason: {}", e) + } ::std::process::exit(1); }, }; diff --git a/src/page.rs b/src/page.rs index 557aacb..99ec127 100644 --- a/src/page.rs +++ b/src/page.rs @@ -3,8 +3,10 @@ use std::collections::HashMap; use std::default::Default; use std::fs::File; use std::io::prelude::*; +use std::path::Path; -// use pulldown_cmark as cmark; + +use pulldown_cmark as cmark; use regex::Regex; use tera::{Tera, Value, Context}; @@ -22,13 +24,22 @@ lazy_static! { pub struct Page { // .md filepath, excluding the content/ bit pub filepath: String, + // the name of the .md file + pub filename: String, + // the directories above our .md file are called sections + // for example a file at content/kb/solutions/blabla.md will have 2 sections: + // `kb` and `solutions` + pub sections: Vec, // of the page pub title: String, // The page slug pub slug: String, // the actual content of the page + pub raw_content: String, + // the HTML rendered of the page pub content: String, + // tags, not to be confused with categories pub tags: Vec<String>, // whether this page should be public or not @@ -43,7 +54,7 @@ pub struct Page { pub category: Option<String>, // optional date if we want to order pages (ie blog post) pub date: Option<String>, - // optional layout, if we want to specify which html to render for that page + // optional layout, if we want to specify which tpl to render for that page pub layout: Option<String>, // description that appears when linked, e.g. on twitter pub description: Option<String>, @@ -54,9 +65,12 @@ impl Default for Page { fn default() -> Page { Page { filepath: "".to_string(), + filename: "".to_string(), + sections: vec![], title: "".to_string(), slug: "".to_string(), + raw_content: "".to_string(), content: "".to_string(), tags: vec![], is_draft: false, @@ -90,37 +104,56 @@ impl Page { // 2. create our page, parse front matter and assign all of that let mut page = Page::default(); page.filepath = filepath.to_string(); - page.content = content.to_string(); + let path = Path::new(filepath); + page.filename = path.file_stem().expect("Couldn't get file stem").to_string_lossy().to_string(); + + // find out if we have sections + for section in path.parent().unwrap().components() { + page.sections.push(section.as_ref().to_string_lossy().to_string()); + } + + page.raw_content = content.to_string(); parse_front_matter(front_matter, &mut page) .chain_err(|| format!("Error when parsing front matter of file `{}`", filepath))?; + page.content = { + let mut html = String::new(); + let parser = cmark::Parser::new(&page.raw_content); + cmark::html::push_html(&mut html, parser); + html + }; + Ok(page) } - pub fn from_file(path: &str) -> Result<Page> { + pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Page> { + let path = path.as_ref(); + let mut content = String::new(); File::open(path) - .chain_err(|| format!("Failed to open '{:?}'", path))? + .chain_err(|| format!("Failed to open '{:?}'", path.display()))? .read_to_string(&mut content)?; - Page::from_str(path, &content) + // Remove the content string from name + // Maybe get a path as an arg instead and use strip_prefix? + Page::from_str(&path.strip_prefix("content").unwrap().to_string_lossy(), &content) } fn get_layout_name(&self) -> String { - // TODO: handle themes match self.layout { Some(ref l) => l.to_string(), - None => "_default/single.html".to_string() + None => "single.html".to_string() } } - pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> { + pub fn render_html(&mut self, tera: &Tera, config: &Config) -> Result<String> { let tpl = self.get_layout_name(); let mut context = Context::new(); context.add("site", config); context.add("page", self); - // println!("{:?}", tera); - tera.render(&tpl, context).chain_err(|| "") + + tera.render(&tpl, context) + .chain_err(|| "Error while rendering template") } } @@ -137,13 +170,40 @@ title = "Hello" slug = "hello-world" +++ Hello world"#; - let res = Page::from_str("", content); + let res = Page::from_str("post.md", content); assert!(res.is_ok()); let page = res.unwrap(); assert_eq!(page.title, "Hello".to_string()); assert_eq!(page.slug, "hello-world".to_string()); - assert_eq!(page.content, "Hello world".to_string()); + assert_eq!(page.raw_content, "Hello world".to_string()); + assert_eq!(page.content, "<p>Hello world</p>\n".to_string()); + } + + #[test] + fn test_can_find_one_parent_directory() { + let content = r#" +title = "Hello" +slug = "hello-world" ++++ +Hello world"#; + let res = Page::from_str("posts/intro.md", content); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.sections, vec!["posts".to_string()]); + } + + #[test] + fn test_can_find_multiplie_parent_directories() { + let content = r#" +title = "Hello" +slug = "hello-world" ++++ +Hello world"#; + let res = Page::from_str("posts/intro/start.md", content); + assert!(res.is_ok()); + let page = res.unwrap(); + assert_eq!(page.sections, vec!["posts".to_string(), "intro".to_string()]); } } diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..3e0a09f --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,12 @@ +use std::io::prelude::*; +use std::fs::{File}; +use std::path::Path; + +use errors::Result; + + +pub fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> { + let mut file = File::create(&path)?; + file.write_all(content.as_bytes())?; + Ok(()) +}