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 14KB

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