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

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