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

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