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.6KB

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