A static site generator written in Rust
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

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. }