use std::sync::mpsc::channel; use std::time::Duration; use std::{env, fs, path}; use fs_extra::dir; use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher}; use toml::Value; use config::Config; use entry::{parse_entry, read_entry_dir, write_entry, write_entry_listing, Entry}; pub fn build(include_drafts: bool) { let cwd = env::current_dir().expect("Couldn't read current directory"); let config = match fs::read_to_string(cwd.join("casaubon.toml")) { Ok(contents) => match contents.parse::() { Ok(config) => Config { site_name: String::from(config["site_name"].as_str().unwrap()), }, Err(_) => panic!("Invalid casaubon.toml"), }, Err(_) => { panic!("Can't find casaubon.toml"); } }; match fs::read_dir(cwd.join("public")) { Ok(_) => { fs::remove_dir_all(cwd.join("public")).unwrap(); } Err(_) => {} } fs::create_dir(cwd.join("public")).expect("Couldn't create public directory"); fs::create_dir(cwd.join("public").join("posts")).expect("Couldn't create posts directory"); let layout_template = fs::read_to_string(&cwd.join("templates").join("layout.html")) .expect("Couldn't find layout template"); let post_template = fs::read_to_string(cwd.join("templates").join("post.html")) .expect("Couldn't find post template"); let post_listing_template = fs::read_to_string(cwd.join("templates").join("post_listing.html")) .expect("Couldn't find post listing item template"); let post_item_template = fs::read_to_string(cwd.join("templates").join("post_listing_item.html")) .expect("Couldn't find post listing item template"); let page_template = fs::read_to_string(cwd.join("templates").join("page.html")) .expect("Couldn't find page template"); let post_paths = match include_drafts { true => { let mut posts = read_entry_dir(&cwd.join("posts")); posts.append(&mut read_entry_dir(&cwd.join("drafts"))); posts } false => read_entry_dir(&cwd.join("posts")), }; let page_paths = read_entry_dir(&cwd.join("pages")); let mut posts: Vec = post_paths .into_iter() .map(|entry| parse_entry(entry.path())) .collect::>(); posts.sort_by(|a, b| b.date.cmp(&a.date)); for post in &posts { write_entry(&cwd, &layout_template, &post_template, &post, &config); } for entry in page_paths.into_iter() { let page = parse_entry(entry.path()); write_entry(&cwd, &layout_template, &page_template, &page, &config); } write_entry_listing( &cwd, &layout_template, &post_listing_template, &post_item_template, &posts, &config, ); fs_extra::copy_items( &vec![cwd.join("css"), cwd.join("js")], cwd.join("public"), &dir::CopyOptions::new(), ) .expect("Couldn't copy css/js directories"); } pub fn new(name: &str) { let cwd = env::current_dir().expect("Couldn't read current directory"); let project_path = cwd.join(name); fs::create_dir(&project_path).expect(&format!("Couldn't create directory '{}'", &name)); fs::write( project_path.join("casaubon.toml"), format!("site_name = \"{}\"", &name), ) .expect("Could not create casaubon.toml"); for dir in &[ "drafts", "posts", "pages", "public", "templates", "css", "js", ] { fs::create_dir(&project_path.join(&dir)) .expect(&format!("Couldn't create {} directory", &dir)); } fs::write(project_path.join("css").join("style.css"), "") .expect("Couldn't create css/style.css"); fs::write(project_path.join("js").join("index.js"), "").expect("Couldn't create js/index.js"); let default_layout_template = format!( " {}

{}

{{{{ contents }}}} \n", name, name ); let default_post_listing_template = format!( "

Posts

    {{{{ post_listing }}}}
\n" ); let default_post_template = format!( "

{{{{ title }}}}

{{{{ body }}}}
\n" ); let default_page_template = format!( "

{{{{ title }}}}

{{{{ body }}}}
\n" ); let default_post_listing_item_template = format!( "
  • {{ date }} {{{{ title }}}}
  • \n" ); for (filename, contents) in &[ ("layout", &default_layout_template), ("post_listing", &default_post_listing_template), ("post", &default_post_template), ("page", &default_page_template), ("post_listing_item", &default_post_listing_item_template), ] { fs::write( &project_path .join("templates") .join(format!("{}.html", filename)), &contents, ) .expect(&format!("Couldn't write templates/{}.html", filename)); } } fn should_rebuild(cwd: &path::PathBuf, path: &path::PathBuf) -> bool { let path_string = path.to_str().unwrap().to_string(); let change_is_from_public = path_string.contains(cwd.join("public").to_str().unwrap()); let change_is_from_git = path_string.contains(cwd.join(".git").to_str().unwrap()); !change_is_from_public && !change_is_from_git } pub fn watch(include_drafts: bool) -> notify::Result<()> { let cwd = env::current_dir().expect("Couldn't read current directory"); let (tx, rx) = channel(); let mut watcher: RecommendedWatcher = try!(Watcher::new(tx, Duration::from_secs(2))); try!(watcher.watch(&cwd, RecursiveMode::Recursive)); println!("Watching {}", cwd.to_str().unwrap()); let handle_event = |path: &path::PathBuf| { if should_rebuild(&cwd, &path) { println!("Rebuilding"); build(include_drafts); } }; loop { match rx.recv() { Ok(e) => match e { DebouncedEvent::Create(path) => { handle_event(&path); } DebouncedEvent::Write(path) => { handle_event(&path); } _ => {} }, Err(e) => println!("watch error: {:?}", e), } } } #[cfg(test)] mod tests { #[allow(unused_imports)] use super::*; #[allow(unused_imports)] use uuid::Uuid; #[test] fn test_new() { let temp_dir = env::temp_dir(); env::set_current_dir(&temp_dir).unwrap(); let uuid = Uuid::new_v4().to_string(); let project_dir = temp_dir.join(&uuid); new(&uuid); for dir in &["public", "pages", "posts", "templates"] { fs::read_dir(&project_dir.join(dir)).unwrap(); } assert_eq!( format!( "{}

    {}

    {{{{ contents }}}}", uuid, uuid ), fs::read_to_string(&project_dir.join("templates").join("layout.html")) .unwrap() .replace("\n", "") .replace(" ", "") ); assert_eq!( format!("

    Posts

      {{{{ post_listing }}}}
    "), fs::read_to_string(&project_dir.join("templates").join("post_listing.html")) .unwrap() .replace("\n", "") .replace(" ", "") ); assert_eq!( format!("
  • {{ date }} {{{{ title }}}}
  • "), fs::read_to_string(&project_dir.join("templates").join("post_listing_item.html")) .unwrap() .replace("\n", "") .replace(" ", "") ); assert_eq!( format!("

    {{{{ title }}}}

    {{{{ body }}}}
    "), fs::read_to_string(&project_dir.join("templates").join("post.html")) .unwrap() .replace("\n", "") .replace(" ", "") ); assert_eq!( format!("

    {{{{ title }}}}

    {{{{ body }}}}
    "), fs::read_to_string(&project_dir.join("templates").join("page.html")) .unwrap() .replace("\n", "") .replace(" ", "") ); assert_eq!( "", fs::read_to_string(&project_dir.join("css").join("style.css")).unwrap() ); assert_eq!( "", fs::read_to_string(&project_dir.join("js").join("index.js")).unwrap() ); assert_eq!( format!("site_name = \"{}\"", &uuid), fs::read_to_string(&project_dir.join("casaubon.toml")).unwrap() ); fs::remove_dir_all(project_dir).unwrap(); } #[test] fn test_build() { let temp_dir = env::temp_dir(); let uuid = Uuid::new_v4().to_string(); let project_dir = temp_dir.join(&uuid); fs::create_dir(&project_dir).unwrap(); env::set_current_dir(&project_dir).unwrap(); fs::create_dir(project_dir.join("posts")).unwrap(); fs::create_dir(project_dir.join("pages")).unwrap(); fs::create_dir(project_dir.join("public")).unwrap(); fs::create_dir(project_dir.join("public").join("posts")).unwrap(); fs::create_dir(project_dir.join("templates")).unwrap(); fs::create_dir(project_dir.join("css")).unwrap(); fs::create_dir(project_dir.join("js")).unwrap(); fs::write( project_dir.join("css").join("style.css"), "body { background: blue; }", ) .unwrap(); fs::write( project_dir.join("js").join("index.js"), "window.onload = function () { alert() }", ) .unwrap(); fs::write( project_dir.join("templates").join("layout.html"), "{{ page_title }}{{ contents }}", ) .unwrap(); fs::write( project_dir.join("templates").join("post.html"), "

    {{ title }}

    {{ body }}
    ", ) .unwrap(); fs::write( project_dir.join("templates").join("page.html"), "

    {{ title }}

    {{ body }}
    ", ) .unwrap(); fs::write( project_dir.join("templates").join("post_listing.html"), "
      {{ post_listing }}
    ", ) .unwrap(); fs::write( project_dir.join("templates").join("post_listing_item.html"), "
  • {{ title }}
  • ", ) .unwrap(); fs::write( project_dir.join("posts").join("first-post.md"), "# First post | 2019-01-01\n\nThis is the first post\n\nIt has multiple paragraphs", ) .unwrap(); fs::write( project_dir.join("pages").join("first-page.md"), "# First page\n\nThis is the first page\n\nIt has multiple paragraphs", ) .unwrap(); fs::write( project_dir.join("casaubon.toml"), "site_name = \"Test Site\"", ) .unwrap(); build(false); assert_eq!( "Test Site", fs::read_to_string(project_dir.join("public").join("index.html")).unwrap(), ); assert_eq!( "First post | Test Site

    First pos\ t

    This is the first post

    It has multiple paragra\ phs

    ", fs::read_to_string( project_dir .join("public") .join("posts") .join("first-post") .join("index.html") ) .unwrap() .replace("\n", ""), ); assert_eq!( "First page | Test Site

    First pag\ e

    This is the first page

    It has multiple paragra\ phs

    ", fs::read_to_string( project_dir .join("public") .join("first-page") .join("index.html") ) .unwrap() .replace("\n", ""), ); assert_eq!( "body { background: blue; }", fs::read_to_string(project_dir.join("public").join("css").join("style.css")).unwrap() ); assert_eq!( "window.onload = function () { alert() }", fs::read_to_string(project_dir.join("public").join("js").join("index.js")).unwrap() ); fs::remove_dir_all(project_dir).unwrap(); } #[test] fn test_build_drafts() { let temp_dir = env::temp_dir(); let uuid = Uuid::new_v4().to_string(); let project_dir = temp_dir.join(&uuid); fs::create_dir(&project_dir).unwrap(); env::set_current_dir(&project_dir).unwrap(); fs::create_dir(project_dir.join("drafts")).unwrap(); fs::create_dir(project_dir.join("posts")).unwrap(); fs::create_dir(project_dir.join("pages")).unwrap(); fs::create_dir(project_dir.join("public")).unwrap(); fs::create_dir(project_dir.join("public").join("posts")).unwrap(); fs::create_dir(project_dir.join("templates")).unwrap(); fs::create_dir(project_dir.join("css")).unwrap(); fs::create_dir(project_dir.join("js")).unwrap(); fs::write( project_dir.join("css").join("style.css"), "body { background: blue; }", ) .unwrap(); fs::write( project_dir.join("js").join("index.js"), "window.onload = function () { alert() }", ) .unwrap(); fs::write( project_dir.join("templates").join("layout.html"), "{{ page_title }}{{ contents }}", ) .unwrap(); fs::write( project_dir.join("templates").join("post.html"), "

    {{ title }}

    {{ body }}
    ", ) .unwrap(); fs::write( project_dir.join("templates").join("page.html"), "

    {{ title }}

    {{ body }}
    ", ) .unwrap(); fs::write( project_dir.join("templates").join("post_listing.html"), "
      {{ post_listing }}
    ", ) .unwrap(); fs::write( project_dir.join("templates").join("post_listing_item.html"), "
  • {{ title }}
  • ", ) .unwrap(); fs::write( project_dir.join("posts").join("first-post.md"), "# First post | 2019-01-01\n\nThis is the first post", ) .unwrap(); fs::write( project_dir.join("pages").join("first-page.md"), "# First page\n\nThis is the first page", ) .unwrap(); fs::write( project_dir.join("drafts").join("first-draft.md"), "# First draft | 2019-01-01\n\nThis is the first draft", ) .unwrap(); fs::write( project_dir.join("casaubon.toml"), "site_name = \"Test Site\"", ) .unwrap(); build(true); assert_eq!( "Test Site", fs::read_to_string(project_dir.join("public").join("index.html")).unwrap().replace("\n", ""), ); assert_eq!( "First post | Test Site

    First post

    This is the first post

    ", fs::read_to_string( project_dir .join("public") .join("posts") .join("first-post") .join("index.html") ).unwrap() .replace("\n", ""), ); assert_eq!( "First page | Test Site

    First page

    This is the first page

    ", fs::read_to_string( project_dir .join("public") .join("first-page") .join("index.html") ).unwrap() .replace("\n", ""), ); assert_eq!( "First draft | Test Site

    First draft

    This is the first draft

    ", fs::read_to_string( project_dir .join("public") .join("posts") .join("first-draft") .join("index.html") ).unwrap() .replace("\n", ""), ); fs::remove_dir_all(project_dir).unwrap(); } #[test] fn test_should_rebuild() { let cwd = env::current_dir().unwrap(); assert_eq!( false, should_rebuild(&cwd, &cwd.join("public").join("index.html")) ); assert_eq!( false, should_rebuild(&cwd, &cwd.join(".git").join("index.html")) ); assert_eq!( true, should_rebuild(&cwd, &cwd.join("posts").join("test.md")) ); assert_eq!( true, should_rebuild(&cwd, &cwd.join("drafts").join("test.md")) ); assert_eq!( true, should_rebuild(&cwd, &cwd.join("css").join("style.css")) ); assert_eq!(true, should_rebuild(&cwd, &cwd.join("js").join("index.js"))); } }