A static site generator written in Rust
您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

commands.rs 18KB

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