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.

commands.rs 13KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. use std::sync::mpsc::channel;
  2. use std::time::Duration;
  3. use std::{env, fs, path};
  4. use fs_extra::dir;
  5. use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
  6. use toml::Value;
  7. use config::Config;
  8. use post::{parse_post, read_posts_dir};
  9. use write::{write_post, write_post_listing};
  10. pub fn build(include_drafts: bool) {
  11. let cwd = env::current_dir().expect("Couldn't read current directory");
  12. let config = match fs::read_to_string(cwd.join("casaubon.toml")) {
  13. Ok(contents) => match contents.parse::<Value>() {
  14. Ok(config) => Config {
  15. site_name: String::from(config["site_name"].as_str().unwrap()),
  16. },
  17. Err(_) => panic!("Invalid casaubon.toml"),
  18. },
  19. Err(_) => {
  20. panic!("Can't find casaubon.toml");
  21. }
  22. };
  23. match fs::read_dir(cwd.join("public")) {
  24. Ok(_) => {
  25. fs::remove_dir_all(cwd.join("public")).unwrap();
  26. }
  27. Err(_) => {}
  28. }
  29. fs::create_dir(cwd.join("public")).expect("Couldn't create public directory");
  30. let layout_template = fs::read_to_string(&cwd.join("templates").join("layout.html"))
  31. .expect("Couldn't find layout template");
  32. let post_template = fs::read_to_string(cwd.join("templates").join("post.html"))
  33. .expect("Couldn't find post template");
  34. let post_listing_template = fs::read_to_string(cwd.join("templates").join("post_listing.html"))
  35. .expect("Couldn't find post listing item template");
  36. let post_item_template =
  37. fs::read_to_string(cwd.join("templates").join("post_listing_item.html"))
  38. .expect("Couldn't find post listing item template");
  39. let post_paths = match include_drafts {
  40. true => {
  41. let mut posts = read_posts_dir(&cwd.join("posts"));
  42. posts.append(&mut read_posts_dir(&cwd.join("drafts")));
  43. posts
  44. }
  45. false => read_posts_dir(&cwd.join("posts")),
  46. };
  47. let posts = post_paths
  48. .into_iter()
  49. .map(|entry| {
  50. let post = parse_post(entry.path());
  51. write_post(&cwd, &layout_template, &post_template, &post, &config);
  52. post
  53. })
  54. .collect();
  55. write_post_listing(
  56. &cwd,
  57. &layout_template,
  58. &post_listing_template,
  59. &post_item_template,
  60. &posts,
  61. &config,
  62. );
  63. fs_extra::copy_items(
  64. &vec![cwd.join("css"), cwd.join("js")],
  65. cwd.join("public"),
  66. &dir::CopyOptions::new(),
  67. )
  68. .expect("Couldn't copy css/js directories");
  69. }
  70. pub fn new(name: &str) {
  71. let cwd = env::current_dir().expect("Couldn't read current directory");
  72. let project_path = cwd.join(name);
  73. fs::create_dir(&project_path).expect(&format!("Couldn't create directory '{}'", &name));
  74. fs::write(
  75. project_path.join("casaubon.toml"),
  76. format!("site_name = \"{}\"", &name),
  77. )
  78. .expect("Could not create casaubon.toml");
  79. for dir in &["drafts", "posts", "public", "templates", "css", "js"] {
  80. fs::create_dir(&project_path.join(&dir))
  81. .expect(&format!("Couldn't create {} directory", &dir));
  82. }
  83. fs::write(project_path.join("css").join("style.css"), "")
  84. .expect("Couldn't create css/style.css");
  85. fs::write(project_path.join("js").join("index.js"), "").expect("Couldn't create js/index.js");
  86. for file in &["layout", "post_listing", "post", "post_listing_item"] {
  87. fs::write(
  88. &project_path
  89. .join("templates")
  90. .join(format!("{}.html", file)),
  91. "",
  92. )
  93. .expect(&format!("Couldn't write templates/{}.html", file));
  94. }
  95. }
  96. fn should_rebuild(cwd: &path::PathBuf, path: &path::PathBuf) -> bool {
  97. let path_string = path.to_str().unwrap().to_string();
  98. let change_is_from_public = path_string.contains(cwd.join("public").to_str().unwrap());
  99. let change_is_from_git = path_string.contains(cwd.join(".git").to_str().unwrap());
  100. !change_is_from_public && !change_is_from_git
  101. }
  102. pub fn watch(include_drafts: bool) -> notify::Result<()> {
  103. let cwd = env::current_dir().expect("Couldn't read current directory");
  104. let (tx, rx) = channel();
  105. let mut watcher: RecommendedWatcher = try!(Watcher::new(tx, Duration::from_secs(2)));
  106. try!(watcher.watch(&cwd, RecursiveMode::Recursive));
  107. println!("Watching {}", cwd.to_str().unwrap());
  108. let handle_event = |path: &path::PathBuf| {
  109. if should_rebuild(&cwd, &path) {
  110. println!("Rebuilding");
  111. build(include_drafts);
  112. }
  113. };
  114. loop {
  115. match rx.recv() {
  116. Ok(e) => match e {
  117. DebouncedEvent::Create(path) => {
  118. handle_event(&path);
  119. }
  120. DebouncedEvent::Write(path) => {
  121. handle_event(&path);
  122. }
  123. _ => {}
  124. },
  125. Err(e) => println!("watch error: {:?}", e),
  126. }
  127. }
  128. }
  129. #[cfg(test)]
  130. mod tests {
  131. #[allow(unused_imports)]
  132. use super::*;
  133. #[allow(unused_imports)]
  134. use uuid::Uuid;
  135. #[test]
  136. fn test_new() {
  137. let temp_dir = env::temp_dir();
  138. env::set_current_dir(&temp_dir).unwrap();
  139. let uuid = Uuid::new_v4().to_string();
  140. let project_dir = temp_dir.join(&uuid);
  141. new(&uuid);
  142. for dir in &["public", "posts", "templates"] {
  143. fs::read_dir(&project_dir.join(dir)).unwrap();
  144. }
  145. assert_eq!(
  146. "",
  147. fs::read_to_string(&project_dir.join("templates").join("post_listing.html")).unwrap()
  148. );
  149. assert_eq!(
  150. "",
  151. fs::read_to_string(&project_dir.join("templates").join("layout.html")).unwrap()
  152. );
  153. assert_eq!(
  154. "",
  155. fs::read_to_string(&project_dir.join("templates").join("post_listing_item.html"))
  156. .unwrap()
  157. );
  158. assert_eq!(
  159. "",
  160. fs::read_to_string(&project_dir.join("templates").join("post.html")).unwrap()
  161. );
  162. assert_eq!(
  163. "",
  164. fs::read_to_string(&project_dir.join("css").join("style.css")).unwrap()
  165. );
  166. assert_eq!(
  167. "",
  168. fs::read_to_string(&project_dir.join("js").join("index.js")).unwrap()
  169. );
  170. assert_eq!(
  171. format!("site_name = \"{}\"", &uuid),
  172. fs::read_to_string(&project_dir.join("casaubon.toml")).unwrap()
  173. );
  174. fs::remove_dir_all(project_dir).unwrap();
  175. }
  176. #[test]
  177. fn test_build() {
  178. let temp_dir = env::temp_dir();
  179. let uuid = Uuid::new_v4().to_string();
  180. let project_dir = temp_dir.join(&uuid);
  181. fs::create_dir(&project_dir).unwrap();
  182. env::set_current_dir(&project_dir).unwrap();
  183. fs::create_dir(project_dir.join("posts")).unwrap();
  184. fs::create_dir(project_dir.join("public")).unwrap();
  185. fs::create_dir(project_dir.join("templates")).unwrap();
  186. fs::create_dir(project_dir.join("css")).unwrap();
  187. fs::create_dir(project_dir.join("js")).unwrap();
  188. fs::write(
  189. project_dir.join("css").join("style.css"),
  190. "body { background: blue; }",
  191. )
  192. .unwrap();
  193. fs::write(
  194. project_dir.join("js").join("index.js"),
  195. "window.onload = function () { alert() }",
  196. )
  197. .unwrap();
  198. fs::write(
  199. project_dir.join("templates").join("layout.html"),
  200. "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>",
  201. )
  202. .unwrap();
  203. fs::write(
  204. project_dir.join("templates").join("post.html"),
  205. "<article><h1>{{ title }}</h1><div>{{ body }}</div></article>",
  206. )
  207. .unwrap();
  208. fs::write(
  209. project_dir.join("templates").join("post_listing.html"),
  210. "<ul>{{ post_listing }}</ul>",
  211. )
  212. .unwrap();
  213. fs::write(
  214. project_dir.join("templates").join("post_listing_item.html"),
  215. "<li><a href=\"/{{ slug }}\">{{ title }}</a></li>",
  216. )
  217. .unwrap();
  218. fs::write(
  219. project_dir.join("posts").join("first-post.md"),
  220. "# First post\n\nThis is the first post\n\nIt has multiple paragraphs",
  221. )
  222. .unwrap();
  223. fs::write(project_dir.join("casaubon.toml"), "site_name = \"Test Site\"").unwrap();
  224. build(false);
  225. assert_eq!(
  226. "<html><head><title>Test Site</title></head><body><ul><li><a href=\"/fir\
  227. st-post\">First post</a></li></ul></body></html>",
  228. fs::read_to_string(project_dir.join("public").join("index.html")).unwrap(),
  229. );
  230. assert_eq!(
  231. "<html><head><title>First post | Test Site</title></head><body><article><h1>First pos\
  232. t</h1><div><p>This is the first post</p><p>It has multiple paragra\
  233. phs</p></div></article></body></html>",
  234. fs::read_to_string(
  235. project_dir
  236. .join("public")
  237. .join("first-post")
  238. .join("index.html")
  239. )
  240. .unwrap()
  241. .replace("\n", ""),
  242. );
  243. assert_eq!(
  244. "body { background: blue; }",
  245. fs::read_to_string(project_dir.join("public").join("css").join("style.css")).unwrap()
  246. );
  247. assert_eq!(
  248. "window.onload = function () { alert() }",
  249. fs::read_to_string(project_dir.join("public").join("js").join("index.js")).unwrap()
  250. );
  251. fs::remove_dir_all(project_dir).unwrap();
  252. }
  253. #[test]
  254. fn test_build_drafts() {
  255. let temp_dir = env::temp_dir();
  256. let uuid = Uuid::new_v4().to_string();
  257. let project_dir = temp_dir.join(&uuid);
  258. fs::create_dir(&project_dir).unwrap();
  259. env::set_current_dir(&project_dir).unwrap();
  260. fs::create_dir(project_dir.join("drafts")).unwrap();
  261. fs::create_dir(project_dir.join("posts")).unwrap();
  262. fs::create_dir(project_dir.join("public")).unwrap();
  263. fs::create_dir(project_dir.join("templates")).unwrap();
  264. fs::create_dir(project_dir.join("css")).unwrap();
  265. fs::create_dir(project_dir.join("js")).unwrap();
  266. fs::write(
  267. project_dir.join("css").join("style.css"),
  268. "body { background: blue; }",
  269. )
  270. .unwrap();
  271. fs::write(
  272. project_dir.join("js").join("index.js"),
  273. "window.onload = function () { alert() }",
  274. )
  275. .unwrap();
  276. fs::write(
  277. project_dir.join("templates").join("layout.html"),
  278. "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>",
  279. )
  280. .unwrap();
  281. fs::write(
  282. project_dir.join("templates").join("post.html"),
  283. "<article><h1>{{ title }}</h1><div>{{ body }}</div></article>",
  284. )
  285. .unwrap();
  286. fs::write(
  287. project_dir.join("templates").join("post_listing.html"),
  288. "<ul>{{ post_listing }}</ul>",
  289. )
  290. .unwrap();
  291. fs::write(
  292. project_dir.join("templates").join("post_listing_item.html"),
  293. "<li><a href=\"/{{ slug }}\">{{ title }}</a></li>",
  294. )
  295. .unwrap();
  296. fs::write(
  297. project_dir.join("posts").join("first-post.md"),
  298. "# First post\n\nThis is the first post",
  299. )
  300. .unwrap();
  301. fs::write(
  302. project_dir.join("drafts").join("first-draft.md"),
  303. "# First draft\n\nThis is the first draft",
  304. )
  305. .unwrap();
  306. fs::write(project_dir.join("casaubon.toml"), "site_name = \"Test Site\"").unwrap();
  307. build(true);
  308. assert_eq!(
  309. "<html><head><title>Test Site</title></head><body><ul><li><a href=\"/first-post\">First post</a></li><li><a href=\"/first-draft\">First draft</a></li></ul></body></html>",
  310. fs::read_to_string(project_dir.join("public").join("index.html")).unwrap().replace("\n", ""),
  311. );
  312. assert_eq!(
  313. "<html><head><title>First post | Test Site</title></head><body><article><h1>First post</h1><div><p>This is the first post</p></div></article></body></html>",
  314. fs::read_to_string(
  315. project_dir
  316. .join("public")
  317. .join("first-post")
  318. .join("index.html")
  319. ).unwrap()
  320. .replace("\n", ""),
  321. );
  322. assert_eq!(
  323. "<html><head><title>First draft | Test Site</title></head><body><article><h1>First draft</h1><div><p>This is the first draft</p></div></article></body></html>",
  324. fs::read_to_string(
  325. project_dir
  326. .join("public")
  327. .join("first-draft")
  328. .join("index.html")
  329. ).unwrap()
  330. .replace("\n", ""),
  331. );
  332. fs::remove_dir_all(project_dir).unwrap();
  333. }
  334. #[test]
  335. fn test_should_rebuild() {
  336. let cwd = env::current_dir().unwrap();
  337. assert_eq!(
  338. false,
  339. should_rebuild(&cwd, &cwd.join("public").join("index.html"))
  340. );
  341. assert_eq!(
  342. false,
  343. should_rebuild(&cwd, &cwd.join(".git").join("index.html"))
  344. );
  345. assert_eq!(
  346. true,
  347. should_rebuild(&cwd, &cwd.join("posts").join("test.md"))
  348. );
  349. assert_eq!(
  350. true,
  351. should_rebuild(&cwd, &cwd.join("drafts").join("test.md"))
  352. );
  353. assert_eq!(
  354. true,
  355. should_rebuild(&cwd, &cwd.join("css").join("style.css"))
  356. );
  357. assert_eq!(true, should_rebuild(&cwd, &cwd.join("js").join("index.js")));
  358. }
  359. }