A static site generator written in Rust
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

lib.rs 7.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268
  1. extern crate chrono;
  2. extern crate fs_extra;
  3. extern crate htmlescape;
  4. #[macro_use]
  5. extern crate lazy_static;
  6. extern crate notify;
  7. extern crate pulldown_cmark;
  8. extern crate regex;
  9. extern crate toml;
  10. extern crate uuid;
  11. use std::sync::mpsc::channel;
  12. use std::time::Duration;
  13. use std::{fs, path};
  14. use fs_extra::dir;
  15. use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
  16. use toml::Value;
  17. use config::Config;
  18. use entry::{parse_entry, read_entry_dir, write_entry, write_entry_listing, Entry, EntryKind};
  19. use rss::generate_rss;
  20. mod config;
  21. mod entry;
  22. mod rss;
  23. pub fn build(include_drafts: bool, cwd: &path::Path) {
  24. let config = match fs::read_to_string(cwd.join("casaubon.toml")) {
  25. Ok(contents) => match contents.parse::<Value>() {
  26. Ok(config) => Config {
  27. site_name: String::from(config["site_name"].as_str().unwrap()),
  28. url: String::from(config["url"].as_str().unwrap()),
  29. description: String::from(config["description"].as_str().unwrap()),
  30. },
  31. Err(_) => panic!("Invalid casaubon.toml"),
  32. },
  33. Err(_) => {
  34. panic!("Can't find casaubon.toml");
  35. }
  36. };
  37. match fs::read_dir(cwd.join("public")) {
  38. Ok(_) => {
  39. fs::remove_dir_all(cwd.join("public")).unwrap();
  40. }
  41. Err(_) => {}
  42. }
  43. fs::create_dir(cwd.join("public")).expect("Couldn't create public directory");
  44. fs::create_dir(cwd.join("public").join("posts")).expect("Couldn't create posts directory");
  45. let layout_template = fs::read_to_string(&cwd.join("templates").join("layout.html"))
  46. .expect("Couldn't find layout template");
  47. let post_template = fs::read_to_string(cwd.join("templates").join("post.html"))
  48. .expect("Couldn't find post template");
  49. let post_listing_template = fs::read_to_string(cwd.join("templates").join("post_listing.html"))
  50. .expect("Couldn't find post listing item template");
  51. let post_item_template =
  52. fs::read_to_string(cwd.join("templates").join("post_listing_item.html"))
  53. .expect("Couldn't find post listing item template");
  54. let page_template = fs::read_to_string(cwd.join("templates").join("page.html"))
  55. .expect("Couldn't find page template");
  56. let post_paths = match include_drafts {
  57. true => {
  58. let mut posts = read_entry_dir(&cwd.join("posts"));
  59. posts.append(&mut read_entry_dir(&cwd.join("drafts")));
  60. posts
  61. }
  62. false => read_entry_dir(&cwd.join("posts")),
  63. };
  64. let page_paths = read_entry_dir(&cwd.join("pages"));
  65. let mut posts: Vec<Entry> = post_paths
  66. .into_iter()
  67. .map(|entry| {
  68. let path = entry.path();
  69. let contents = fs::read_to_string(&path).expect("Couldn't read post file");
  70. parse_entry(EntryKind::Post, &contents, &path)
  71. })
  72. .collect::<Vec<Entry>>();
  73. posts.sort_by(|a, b| b.date.cmp(&a.date));
  74. for post in &posts {
  75. write_entry(&cwd, &layout_template, &post_template, &post, &config);
  76. }
  77. for entry in page_paths.into_iter() {
  78. let path = entry.path();
  79. let contents = fs::read_to_string(&path).expect("Couldn't read page file");
  80. let page = parse_entry(EntryKind::Page, &contents, &path);
  81. write_entry(&cwd, &layout_template, &page_template, &page, &config);
  82. }
  83. write_entry_listing(
  84. &cwd,
  85. &layout_template,
  86. &post_listing_template,
  87. &post_item_template,
  88. &posts,
  89. &config,
  90. );
  91. fs_extra::copy_items(
  92. &vec![cwd.join("assets")],
  93. cwd.join("public"),
  94. &dir::CopyOptions::new(),
  95. )
  96. .expect("Couldn't copy assets directory");
  97. let rss = generate_rss(&config, &posts);
  98. fs::write(cwd.join("public").join("rss.xml"), rss).expect("Couldn't write rss file");
  99. }
  100. pub fn new(name: &str, cwd: &path::Path) {
  101. let project_path = cwd.join(name);
  102. fs::create_dir(&project_path).expect(&format!("Couldn't create directory '{}'", &name));
  103. fs::write(
  104. project_path.join("casaubon.toml"),
  105. format!(
  106. "site_name = \"{}\"\nurl = \"{}.com\"\ndescription = \"\"",
  107. &name, &name
  108. ),
  109. )
  110. .expect("Could not create casaubon.toml");
  111. for dir in &[
  112. "drafts",
  113. "posts",
  114. "pages",
  115. "public",
  116. "templates",
  117. "assets",
  118. "js",
  119. ] {
  120. fs::create_dir(&project_path.join(&dir))
  121. .expect(&format!("Couldn't create {} directory", &dir));
  122. }
  123. let default_layout_template = format!(
  124. "<html>
  125. <head>
  126. <title>{}</title>
  127. </head>
  128. <body>
  129. <h1>{}</h1>
  130. {{{{ contents }}}}
  131. </body>
  132. </html>\n",
  133. name, name
  134. );
  135. let default_post_listing_template = format!(
  136. "<div>
  137. <h3>Posts</h3>
  138. <ul>{{{{ post_listing }}}}</ul>
  139. </div>\n"
  140. );
  141. let default_post_template = format!(
  142. "<article>
  143. <h1>{{{{ title }}}}</h1>
  144. <div>{{{{ body }}}}</div>
  145. </article>\n"
  146. );
  147. let default_page_template = format!(
  148. "<article>
  149. <h1>{{{{ title }}}}</h1>
  150. <div>{{{{ body }}}}</div>
  151. </article>\n"
  152. );
  153. let default_post_listing_item_template = format!(
  154. "<li>
  155. {{ date }} <a href=\"/posts/{{{{ slug }}}}/\">{{{{ title }}}}</a>
  156. </li>\n"
  157. );
  158. for (filename, contents) in &[
  159. ("layout", &default_layout_template),
  160. ("post_listing", &default_post_listing_template),
  161. ("post", &default_post_template),
  162. ("page", &default_page_template),
  163. ("post_listing_item", &default_post_listing_item_template),
  164. ] {
  165. fs::write(
  166. &project_path
  167. .join("templates")
  168. .join(format!("{}.html", filename)),
  169. &contents,
  170. )
  171. .expect(&format!("Couldn't write templates/{}.html", filename));
  172. }
  173. }
  174. fn should_rebuild(cwd: &path::Path, path: &path::PathBuf) -> bool {
  175. let path_string = path.to_str().unwrap().to_string();
  176. let change_is_from_public = path_string.contains(cwd.join("public").to_str().unwrap());
  177. let change_is_from_git = path_string.contains(cwd.join(".git").to_str().unwrap());
  178. !change_is_from_public && !change_is_from_git
  179. }
  180. pub fn watch(include_drafts: bool, cwd: &path::Path) -> notify::Result<()> {
  181. let (tx, rx) = channel();
  182. let mut watcher: RecommendedWatcher = try!(Watcher::new(tx, Duration::from_secs(2)));
  183. try!(watcher.watch(&cwd, RecursiveMode::Recursive));
  184. println!("Watching {}", cwd.to_str().unwrap());
  185. let handle_event = |path: &path::PathBuf| {
  186. if should_rebuild(&cwd, &path) {
  187. println!("Rebuilding");
  188. build(include_drafts, &cwd);
  189. }
  190. };
  191. loop {
  192. match rx.recv() {
  193. Ok(e) => match e {
  194. DebouncedEvent::Create(path) => {
  195. handle_event(&path);
  196. }
  197. DebouncedEvent::Write(path) => {
  198. handle_event(&path);
  199. }
  200. _ => {}
  201. },
  202. Err(e) => println!("watch error: {:?}", e),
  203. }
  204. }
  205. }
  206. #[cfg(test)]
  207. mod tests {
  208. #[allow(unused_imports)]
  209. use super::*;
  210. #[allow(unused_imports)]
  211. use uuid::Uuid;
  212. use std::env;
  213. #[test]
  214. fn test_should_rebuild() {
  215. let cwd = env::current_dir().unwrap();
  216. assert_eq!(
  217. false,
  218. should_rebuild(&cwd, &cwd.join("public").join("index.html"))
  219. );
  220. assert_eq!(
  221. false,
  222. should_rebuild(&cwd, &cwd.join(".git").join("index.html"))
  223. );
  224. assert_eq!(
  225. true,
  226. should_rebuild(&cwd, &cwd.join("posts").join("test.md"))
  227. );
  228. assert_eq!(
  229. true,
  230. should_rebuild(&cwd, &cwd.join("drafts").join("test.md"))
  231. );
  232. assert_eq!(
  233. true,
  234. should_rebuild(&cwd, &cwd.join("css").join("style.css"))
  235. );
  236. assert_eq!(true, should_rebuild(&cwd, &cwd.join("js").join("index.js")));
  237. }
  238. }