Start building sites

This commit is contained in:
Vincent Prouillet 2016-12-13 15:22:24 +09:00
parent 3cd5da2128
commit a147e68f78
6 changed files with 126 additions and 37 deletions

View file

@ -1,28 +1,50 @@
use std::fs::{create_dir, remove_dir_all};
use std::path::Path;
use glob::glob; use glob::glob;
use tera::Tera; use tera::Tera;
use config:: Config; use config:: Config;
use errors::{Result, ResultExt}; use errors::{Result, ResultExt};
use page::Page; use page::Page;
use utils::create_file;
pub fn build(config: Config) -> Result<()> { 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 tera = Tera::new("layouts/**/*").chain_err(|| "Error parsing templates")?;
let mut pages: Vec<Page> = vec![]; // let mut pages: Vec<Page> = 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 // hardcoded pattern so can't error
for entry in glob("content/**/*.md").unwrap().filter_map(|e| e.ok()) { for entry in glob("content/**/*.md").unwrap().filter_map(|e| e.ok()) {
let path = entry.as_path(); let path = entry.as_path();
// Remove the content string from name let mut page = Page::from_file(&path)?;
let filepath = path.to_string_lossy().replace("content/", "");
pages.push(Page::from_file(&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(&current_path)?;
// TODO: create section index.html
// create_file(current_path.join("index.html"), "");
}
}
current_path.push(&page.filename);
create_dir(&current_path)?;
create_file(current_path.join("index.html"), &page.render_html(&tera, &config)?)?;
} }
for page in pages {
let html = page.render_html(&tera, &config)
.chain_err(|| format!("Failed to render '{}'", page.filepath))?;
}
Ok(()) Ok(())
} }

View file

@ -1,9 +1,9 @@
use std::io::prelude::*; use std::fs::{create_dir};
use std::fs::{create_dir, File};
use std::path::Path; use std::path::Path;
use errors::Result; use errors::Result;
use utils::create_file;
const CONFIG: &'static str = r#" const CONFIG: &'static str = r#"
@ -33,9 +33,3 @@ pub fn create_new_project<P: AsRef<Path>>(name: P) -> Result<()> {
Ok(()) Ok(())
} }
fn create_file<P: AsRef<Path>>(path: P, content: &str) -> Result<()> {
let mut file = File::create(&path)?;
file.write_all(content.as_bytes())?;
Ok(())
}

View file

@ -12,7 +12,6 @@ use errors::{Result, ErrorKind, ResultExt};
pub struct Config { pub struct Config {
pub title: String, pub title: String,
pub base_url: String, pub base_url: String,
pub theme: String,
pub favicon: Option<String>, pub favicon: Option<String>,
} }
@ -22,7 +21,6 @@ impl Default for Config {
Config { Config {
title: "".to_string(), title: "".to_string(),
base_url: "".to_string(), base_url: "".to_string(),
theme: "".to_string(),
favicon: None, favicon: None,
} }
@ -48,11 +46,8 @@ impl Config {
return Ok(config); return Ok(config);
} else { } else {
// TODO: handle error in parsing TOML bail!("Errors parsing front matter: {:?}", parser.errors);
println!("parse errors: {:?}", parser.errors);
} }
unreachable!()
} }
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Config> { pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Config> {

View file

@ -13,6 +13,7 @@ extern crate tera;
extern crate glob; extern crate glob;
mod utils;
mod config; mod config;
mod errors; mod errors;
mod cmd; mod cmd;
@ -56,6 +57,7 @@ fn main() {
match cmd::create_new_project(matches.value_of("name").unwrap()) { match cmd::create_new_project(matches.value_of("name").unwrap()) {
Ok(()) => { Ok(()) => {
println!("Project created"); println!("Project created");
println!("You will now need to set a theme in `config.toml`");
}, },
Err(e) => { Err(e) => {
println!("Error: {}", e); println!("Error: {}", e);
@ -66,10 +68,14 @@ fn main() {
("build", Some(_)) => { ("build", Some(_)) => {
match cmd::build(get_config()) { match cmd::build(get_config()) {
Ok(()) => { Ok(()) => {
println!("Project built"); println!("Project built.");
}, },
Err(e) => { 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); ::std::process::exit(1);
}, },
}; };

View file

@ -3,8 +3,10 @@ use std::collections::HashMap;
use std::default::Default; use std::default::Default;
use std::fs::File; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
use std::path::Path;
// use pulldown_cmark as cmark;
use pulldown_cmark as cmark;
use regex::Regex; use regex::Regex;
use tera::{Tera, Value, Context}; use tera::{Tera, Value, Context};
@ -22,13 +24,22 @@ lazy_static! {
pub struct Page { pub struct Page {
// .md filepath, excluding the content/ bit // .md filepath, excluding the content/ bit
pub filepath: String, 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<String>,
// <title> of the page // <title> of the page
pub title: String, pub title: String,
// The page slug // The page slug
pub slug: String, pub slug: String,
// the actual content of the page // the actual content of the page
pub raw_content: String,
// the HTML rendered of the page
pub content: String, pub content: String,
// tags, not to be confused with categories // tags, not to be confused with categories
pub tags: Vec<String>, pub tags: Vec<String>,
// whether this page should be public or not // whether this page should be public or not
@ -43,7 +54,7 @@ pub struct Page {
pub category: Option<String>, pub category: Option<String>,
// optional date if we want to order pages (ie blog post) // optional date if we want to order pages (ie blog post)
pub date: Option<String>, 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>, pub layout: Option<String>,
// description that appears when linked, e.g. on twitter // description that appears when linked, e.g. on twitter
pub description: Option<String>, pub description: Option<String>,
@ -54,9 +65,12 @@ impl Default for Page {
fn default() -> Page { fn default() -> Page {
Page { Page {
filepath: "".to_string(), filepath: "".to_string(),
filename: "".to_string(),
sections: vec![],
title: "".to_string(), title: "".to_string(),
slug: "".to_string(), slug: "".to_string(),
raw_content: "".to_string(),
content: "".to_string(), content: "".to_string(),
tags: vec![], tags: vec![],
is_draft: false, is_draft: false,
@ -90,37 +104,56 @@ impl Page {
// 2. create our page, parse front matter and assign all of that // 2. create our page, parse front matter and assign all of that
let mut page = Page::default(); let mut page = Page::default();
page.filepath = filepath.to_string(); 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) parse_front_matter(front_matter, &mut page)
.chain_err(|| format!("Error when parsing front matter of file `{}`", filepath))?; .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) 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(); let mut content = String::new();
File::open(path) File::open(path)
.chain_err(|| format!("Failed to open '{:?}'", path))? .chain_err(|| format!("Failed to open '{:?}'", path.display()))?
.read_to_string(&mut content)?; .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 { fn get_layout_name(&self) -> String {
// TODO: handle themes
match self.layout { match self.layout {
Some(ref l) => l.to_string(), 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 tpl = self.get_layout_name();
let mut context = Context::new(); let mut context = Context::new();
context.add("site", config); context.add("site", config);
context.add("page", self); 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" slug = "hello-world"
+++ +++
Hello world"#; Hello world"#;
let res = Page::from_str("", content); let res = Page::from_str("post.md", content);
assert!(res.is_ok()); assert!(res.is_ok());
let page = res.unwrap(); let page = res.unwrap();
assert_eq!(page.title, "Hello".to_string()); assert_eq!(page.title, "Hello".to_string());
assert_eq!(page.slug, "hello-world".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()]);
} }
} }

12
src/utils.rs Normal file
View file

@ -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(())
}