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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  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 page::{parse_page, read_pages_dir};
  9. use post::{parse_post, read_posts_dir, Post};
  10. use write::{write_page, write_post, write_post_listing};
  11. pub fn build(include_drafts: bool) {
  12. let cwd = env::current_dir().expect("Couldn't read current directory");
  13. let config = match fs::read_to_string(cwd.join("casaubon.toml")) {
  14. Ok(contents) => match contents.parse::<Value>() {
  15. Ok(config) => Config {
  16. site_name: String::from(config["site_name"].as_str().unwrap()),
  17. },
  18. Err(_) => panic!("Invalid casaubon.toml"),
  19. },
  20. Err(_) => {
  21. panic!("Can't find casaubon.toml");
  22. }
  23. };
  24. match fs::read_dir(cwd.join("public")) {
  25. Ok(_) => {
  26. fs::remove_dir_all(cwd.join("public")).unwrap();
  27. }
  28. Err(_) => {}
  29. }
  30. fs::create_dir(cwd.join("public")).expect("Couldn't create public directory");
  31. fs::create_dir(cwd.join("public").join("posts")).expect("Couldn't create posts directory");
  32. let layout_template = fs::read_to_string(&cwd.join("templates").join("layout.html"))
  33. .expect("Couldn't find layout template");
  34. let post_template = fs::read_to_string(cwd.join("templates").join("post.html"))
  35. .expect("Couldn't find post template");
  36. let post_listing_template = fs::read_to_string(cwd.join("templates").join("post_listing.html"))
  37. .expect("Couldn't find post listing item template");
  38. let post_item_template =
  39. fs::read_to_string(cwd.join("templates").join("post_listing_item.html"))
  40. .expect("Couldn't find post listing item template");
  41. let page_template = fs::read_to_string(cwd.join("templates").join("page.html"))
  42. .expect("Couldn't find page template");
  43. let post_paths = match include_drafts {
  44. true => {
  45. let mut posts = read_posts_dir(&cwd.join("posts"));
  46. posts.append(&mut read_posts_dir(&cwd.join("drafts")));
  47. posts
  48. }
  49. false => read_posts_dir(&cwd.join("posts")),
  50. };
  51. let page_paths = read_pages_dir(&cwd.join("pages"));
  52. let mut posts: Vec<Post> = post_paths
  53. .into_iter()
  54. .map(|entry| parse_post(entry.path()))
  55. .collect::<Vec<Post>>();
  56. posts.sort_by(|a, b| b.date.cmp(&a.date));
  57. for post in &posts {
  58. write_post(&cwd, &layout_template, &post_template, &post, &config);
  59. }
  60. for entry in page_paths.into_iter() {
  61. let page = parse_page(entry.path());
  62. write_page(&cwd, &layout_template, &page_template, &page, &config);
  63. }
  64. write_post_listing(
  65. &cwd,
  66. &layout_template,
  67. &post_listing_template,
  68. &post_item_template,
  69. &posts,
  70. &config,
  71. );
  72. fs_extra::copy_items(
  73. &vec![cwd.join("css"), cwd.join("js")],
  74. cwd.join("public"),
  75. &dir::CopyOptions::new(),
  76. )
  77. .expect("Couldn't copy css/js directories");
  78. }
  79. pub fn new(name: &str) {
  80. let cwd = env::current_dir().expect("Couldn't read current directory");
  81. let project_path = cwd.join(name);
  82. fs::create_dir(&project_path).expect(&format!("Couldn't create directory '{}'", &name));
  83. fs::write(
  84. project_path.join("casaubon.toml"),
  85. format!("site_name = \"{}\"", &name),
  86. )
  87. .expect("Could not create casaubon.toml");
  88. for dir in &[
  89. "drafts",
  90. "posts",
  91. "pages",
  92. "public",
  93. "templates",
  94. "css",
  95. "js",
  96. ] {
  97. fs::create_dir(&project_path.join(&dir))
  98. .expect(&format!("Couldn't create {} directory", &dir));
  99. }
  100. fs::write(project_path.join("css").join("style.css"), "")
  101. .expect("Couldn't create css/style.css");
  102. fs::write(project_path.join("js").join("index.js"), "").expect("Couldn't create js/index.js");
  103. let default_layout_template = format!(
  104. "<html>
  105. <head>
  106. <title>{}</title>
  107. </head>
  108. <body>
  109. <h1>{}</h1>
  110. {{{{ contents }}}}
  111. </body>
  112. </html>\n",
  113. name, name
  114. );
  115. let default_post_listing_template = format!(
  116. "<div>
  117. <h3>Posts</h3>
  118. <ul>{{{{ post_listing }}}}</ul>
  119. </div>\n"
  120. );
  121. let default_post_template = format!(
  122. "<article>
  123. <h1>{{{{ title }}}}</h1>
  124. <div>{{{{ body }}}}</div>
  125. </article>\n"
  126. );
  127. let default_page_template = format!(
  128. "<article>
  129. <h1>{{{{ title }}}}</h1>
  130. <div>{{{{ body }}}}</div>
  131. </article>\n"
  132. );
  133. let default_post_listing_item_template = format!(
  134. "<li>
  135. <a href=\"/posts/{{{{ slug }}}}/\">{{{{ title }}}}</a>
  136. </li>\n"
  137. );
  138. for (filename, contents) in &[
  139. ("layout", &default_layout_template),
  140. ("post_listing", &default_post_listing_template),
  141. ("post", &default_post_template),
  142. ("page", &default_page_template),
  143. ("post_listing_item", &default_post_listing_item_template),
  144. ] {
  145. fs::write(
  146. &project_path
  147. .join("templates")
  148. .join(format!("{}.html", filename)),
  149. &contents,
  150. )
  151. .expect(&format!("Couldn't write templates/{}.html", filename));
  152. }
  153. }
  154. fn should_rebuild(cwd: &path::PathBuf, path: &path::PathBuf) -> bool {
  155. let path_string = path.to_str().unwrap().to_string();
  156. let change_is_from_public = path_string.contains(cwd.join("public").to_str().unwrap());
  157. let change_is_from_git = path_string.contains(cwd.join(".git").to_str().unwrap());
  158. !change_is_from_public && !change_is_from_git
  159. }
  160. pub fn watch(include_drafts: bool) -> notify::Result<()> {
  161. let cwd = env::current_dir().expect("Couldn't read current directory");
  162. let (tx, rx) = channel();
  163. let mut watcher: RecommendedWatcher = try!(Watcher::new(tx, Duration::from_secs(2)));
  164. try!(watcher.watch(&cwd, RecursiveMode::Recursive));
  165. println!("Watching {}", cwd.to_str().unwrap());
  166. let handle_event = |path: &path::PathBuf| {
  167. if should_rebuild(&cwd, &path) {
  168. println!("Rebuilding");
  169. build(include_drafts);
  170. }
  171. };
  172. loop {
  173. match rx.recv() {
  174. Ok(e) => match e {
  175. DebouncedEvent::Create(path) => {
  176. handle_event(&path);
  177. }
  178. DebouncedEvent::Write(path) => {
  179. handle_event(&path);
  180. }
  181. _ => {}
  182. },
  183. Err(e) => println!("watch error: {:?}", e),
  184. }
  185. }
  186. }
  187. #[cfg(test)]
  188. mod tests {
  189. #[allow(unused_imports)]
  190. use super::*;
  191. #[allow(unused_imports)]
  192. use uuid::Uuid;
  193. #[test]
  194. fn test_new() {
  195. let temp_dir = env::temp_dir();
  196. env::set_current_dir(&temp_dir).unwrap();
  197. let uuid = Uuid::new_v4().to_string();
  198. let project_dir = temp_dir.join(&uuid);
  199. new(&uuid);
  200. for dir in &["public", "pages", "posts", "templates"] {
  201. fs::read_dir(&project_dir.join(dir)).unwrap();
  202. }
  203. assert_eq!(
  204. format!(
  205. "<html><head><title>{}</title></head><body><h1>{}</h1>{{{{ contents }}}}</body></html>",
  206. uuid, uuid
  207. ),
  208. fs::read_to_string(&project_dir.join("templates").join("layout.html"))
  209. .unwrap()
  210. .replace("\n", "")
  211. .replace(" ", "")
  212. );
  213. assert_eq!(
  214. format!("<div><h3>Posts</h3><ul>{{{{ post_listing }}}}</ul></div>"),
  215. fs::read_to_string(&project_dir.join("templates").join("post_listing.html"))
  216. .unwrap()
  217. .replace("\n", "")
  218. .replace(" ", "")
  219. );
  220. assert_eq!(
  221. format!("<li><a href=\"/posts/{{{{ slug }}}}/\">{{{{ title }}}}</a></li>"),
  222. fs::read_to_string(&project_dir.join("templates").join("post_listing_item.html"))
  223. .unwrap()
  224. .replace("\n", "")
  225. .replace(" ", "")
  226. );
  227. assert_eq!(
  228. format!("<article><h1>{{{{ title }}}}</h1><div>{{{{ body }}}}</div></article>"),
  229. fs::read_to_string(&project_dir.join("templates").join("post.html"))
  230. .unwrap()
  231. .replace("\n", "")
  232. .replace(" ", "")
  233. );
  234. assert_eq!(
  235. format!("<article><h1>{{{{ title }}}}</h1><div>{{{{ body }}}}</div></article>"),
  236. fs::read_to_string(&project_dir.join("templates").join("page.html"))
  237. .unwrap()
  238. .replace("\n", "")
  239. .replace(" ", "")
  240. );
  241. assert_eq!(
  242. "",
  243. fs::read_to_string(&project_dir.join("css").join("style.css")).unwrap()
  244. );
  245. assert_eq!(
  246. "",
  247. fs::read_to_string(&project_dir.join("js").join("index.js")).unwrap()
  248. );
  249. assert_eq!(
  250. format!("site_name = \"{}\"", &uuid),
  251. fs::read_to_string(&project_dir.join("casaubon.toml")).unwrap()
  252. );
  253. fs::remove_dir_all(project_dir).unwrap();
  254. }
  255. #[test]
  256. fn test_build() {
  257. let temp_dir = env::temp_dir();
  258. let uuid = Uuid::new_v4().to_string();
  259. let project_dir = temp_dir.join(&uuid);
  260. fs::create_dir(&project_dir).unwrap();
  261. env::set_current_dir(&project_dir).unwrap();
  262. fs::create_dir(project_dir.join("posts")).unwrap();
  263. fs::create_dir(project_dir.join("pages")).unwrap();
  264. fs::create_dir(project_dir.join("public")).unwrap();
  265. fs::create_dir(project_dir.join("public").join("posts")).unwrap();
  266. fs::create_dir(project_dir.join("templates")).unwrap();
  267. fs::create_dir(project_dir.join("css")).unwrap();
  268. fs::create_dir(project_dir.join("js")).unwrap();
  269. fs::write(
  270. project_dir.join("css").join("style.css"),
  271. "body { background: blue; }",
  272. )
  273. .unwrap();
  274. fs::write(
  275. project_dir.join("js").join("index.js"),
  276. "window.onload = function () { alert() }",
  277. )
  278. .unwrap();
  279. fs::write(
  280. project_dir.join("templates").join("layout.html"),
  281. "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>",
  282. )
  283. .unwrap();
  284. fs::write(
  285. project_dir.join("templates").join("post.html"),
  286. "<article><h1>{{ title }}</h1><div>{{ body }}</div></article>",
  287. )
  288. .unwrap();
  289. fs::write(
  290. project_dir.join("templates").join("page.html"),
  291. "<article class=\"page\"><h1>{{ title }}</h1><div>{{ body }}</div></article>",
  292. )
  293. .unwrap();
  294. fs::write(
  295. project_dir.join("templates").join("post_listing.html"),
  296. "<ul>{{ post_listing }}</ul>",
  297. )
  298. .unwrap();
  299. fs::write(
  300. project_dir.join("templates").join("post_listing_item.html"),
  301. "<li><a href=\"/{{ slug }}\">{{ title }}</a></li>",
  302. )
  303. .unwrap();
  304. fs::write(
  305. project_dir.join("posts").join("first-post.md"),
  306. "# First post | 2019-01-01\n\nThis is the first post\n\nIt has multiple paragraphs",
  307. )
  308. .unwrap();
  309. fs::write(
  310. project_dir.join("pages").join("first-page.md"),
  311. "# First page\n\nThis is the first page\n\nIt has multiple paragraphs",
  312. )
  313. .unwrap();
  314. fs::write(
  315. project_dir.join("casaubon.toml"),
  316. "site_name = \"Test Site\"",
  317. )
  318. .unwrap();
  319. build(false);
  320. assert_eq!(
  321. "<html><head><title>Test Site</title></head><body><ul><li><a href=\"/fir\
  322. st-post\">First post</a></li></ul></body></html>",
  323. fs::read_to_string(project_dir.join("public").join("index.html")).unwrap(),
  324. );
  325. assert_eq!(
  326. "<html><head><title>First post | Test Site</title></head><body><article><h1>First pos\
  327. t</h1><div><p>This is the first post</p><p>It has multiple paragra\
  328. phs</p></div></article></body></html>",
  329. fs::read_to_string(
  330. project_dir
  331. .join("public")
  332. .join("posts")
  333. .join("first-post")
  334. .join("index.html")
  335. )
  336. .unwrap()
  337. .replace("\n", ""),
  338. );
  339. assert_eq!(
  340. "<html><head><title>First page | Test Site</title></head><body><article class=\"page\"><h1>First pag\
  341. e</h1><div><p>This is the first page</p><p>It has multiple paragra\
  342. phs</p></div></article></body></html>",
  343. fs::read_to_string(
  344. project_dir
  345. .join("public")
  346. .join("first-page")
  347. .join("index.html")
  348. )
  349. .unwrap()
  350. .replace("\n", ""),
  351. );
  352. assert_eq!(
  353. "body { background: blue; }",
  354. fs::read_to_string(project_dir.join("public").join("css").join("style.css")).unwrap()
  355. );
  356. assert_eq!(
  357. "window.onload = function () { alert() }",
  358. fs::read_to_string(project_dir.join("public").join("js").join("index.js")).unwrap()
  359. );
  360. fs::remove_dir_all(project_dir).unwrap();
  361. }
  362. #[test]
  363. fn test_build_drafts() {
  364. let temp_dir = env::temp_dir();
  365. let uuid = Uuid::new_v4().to_string();
  366. let project_dir = temp_dir.join(&uuid);
  367. fs::create_dir(&project_dir).unwrap();
  368. env::set_current_dir(&project_dir).unwrap();
  369. fs::create_dir(project_dir.join("drafts")).unwrap();
  370. fs::create_dir(project_dir.join("posts")).unwrap();
  371. fs::create_dir(project_dir.join("pages")).unwrap();
  372. fs::create_dir(project_dir.join("public")).unwrap();
  373. fs::create_dir(project_dir.join("public").join("posts")).unwrap();
  374. fs::create_dir(project_dir.join("templates")).unwrap();
  375. fs::create_dir(project_dir.join("css")).unwrap();
  376. fs::create_dir(project_dir.join("js")).unwrap();
  377. fs::write(
  378. project_dir.join("css").join("style.css"),
  379. "body { background: blue; }",
  380. )
  381. .unwrap();
  382. fs::write(
  383. project_dir.join("js").join("index.js"),
  384. "window.onload = function () { alert() }",
  385. )
  386. .unwrap();
  387. fs::write(
  388. project_dir.join("templates").join("layout.html"),
  389. "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>",
  390. )
  391. .unwrap();
  392. fs::write(
  393. project_dir.join("templates").join("post.html"),
  394. "<article><h1>{{ title }}</h1><div>{{ body }}</div></article>",
  395. )
  396. .unwrap();
  397. fs::write(
  398. project_dir.join("templates").join("page.html"),
  399. "<article class=\"page\"><h1>{{ title }}</h1><div>{{ body }}</div></article>",
  400. )
  401. .unwrap();
  402. fs::write(
  403. project_dir.join("templates").join("post_listing.html"),
  404. "<ul>{{ post_listing }}</ul>",
  405. )
  406. .unwrap();
  407. fs::write(
  408. project_dir.join("templates").join("post_listing_item.html"),
  409. "<li><a href=\"/{{ slug }}\">{{ title }}</a></li>",
  410. )
  411. .unwrap();
  412. fs::write(
  413. project_dir.join("posts").join("first-post.md"),
  414. "# First post | 2019-01-01\n\nThis is the first post",
  415. )
  416. .unwrap();
  417. fs::write(
  418. project_dir.join("pages").join("first-page.md"),
  419. "# First page\n\nThis is the first page",
  420. )
  421. .unwrap();
  422. fs::write(
  423. project_dir.join("drafts").join("first-draft.md"),
  424. "# First draft | 2019-01-01\n\nThis is the first draft",
  425. )
  426. .unwrap();
  427. fs::write(
  428. project_dir.join("casaubon.toml"),
  429. "site_name = \"Test Site\"",
  430. )
  431. .unwrap();
  432. build(true);
  433. assert_eq!(
  434. "<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>",
  435. fs::read_to_string(project_dir.join("public").join("index.html")).unwrap().replace("\n", ""),
  436. );
  437. assert_eq!(
  438. "<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>",
  439. fs::read_to_string(
  440. project_dir
  441. .join("public")
  442. .join("posts")
  443. .join("first-post")
  444. .join("index.html")
  445. ).unwrap()
  446. .replace("\n", ""),
  447. );
  448. assert_eq!(
  449. "<html><head><title>First page | Test Site</title></head><body><article class=\"page\"><h1>First page</h1><div><p>This is the first page</p></div></article></body></html>",
  450. fs::read_to_string(
  451. project_dir
  452. .join("public")
  453. .join("first-page")
  454. .join("index.html")
  455. ).unwrap()
  456. .replace("\n", ""),
  457. );
  458. assert_eq!(
  459. "<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>",
  460. fs::read_to_string(
  461. project_dir
  462. .join("public")
  463. .join("posts")
  464. .join("first-draft")
  465. .join("index.html")
  466. ).unwrap()
  467. .replace("\n", ""),
  468. );
  469. fs::remove_dir_all(project_dir).unwrap();
  470. }
  471. #[test]
  472. fn test_should_rebuild() {
  473. let cwd = env::current_dir().unwrap();
  474. assert_eq!(
  475. false,
  476. should_rebuild(&cwd, &cwd.join("public").join("index.html"))
  477. );
  478. assert_eq!(
  479. false,
  480. should_rebuild(&cwd, &cwd.join(".git").join("index.html"))
  481. );
  482. assert_eq!(
  483. true,
  484. should_rebuild(&cwd, &cwd.join("posts").join("test.md"))
  485. );
  486. assert_eq!(
  487. true,
  488. should_rebuild(&cwd, &cwd.join("drafts").join("test.md"))
  489. );
  490. assert_eq!(
  491. true,
  492. should_rebuild(&cwd, &cwd.join("css").join("style.css"))
  493. );
  494. assert_eq!(true, should_rebuild(&cwd, &cwd.join("js").join("index.js")));
  495. }
  496. }