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

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