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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  1. use std::sync::mpsc::channel;
  2. use std::time::Duration;
  3. use std::{env, fs};
  4. use fs_extra::dir;
  5. use notify::{RecommendedWatcher, RecursiveMode, Watcher};
  6. use toml::Value;
  7. use config::Config;
  8. use post::{parse_post, read_posts_dir};
  9. use write::{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("tlon.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 tlon.toml"),
  18. },
  19. Err(_) => {
  20. panic!("Can't find tlon.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. 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 post_paths = match include_drafts {
  40. true => {
  41. let mut posts = read_posts_dir(&cwd.join("posts"));
  42. posts.append(&mut read_posts_dir(&cwd.join("drafts")));
  43. posts
  44. }
  45. false => read_posts_dir(&cwd.join("posts")),
  46. };
  47. let posts = post_paths
  48. .into_iter()
  49. .map(|entry| {
  50. let post = parse_post(entry.path());
  51. write_post(&cwd, &layout_template, &post_template, &post, &config);
  52. post
  53. }).collect();
  54. write_post_listing(
  55. &cwd,
  56. &layout_template,
  57. &post_listing_template,
  58. &post_item_template,
  59. &posts,
  60. &config,
  61. );
  62. fs_extra::copy_items(
  63. &vec![cwd.join("css"), cwd.join("js")],
  64. cwd.join("public"),
  65. &dir::CopyOptions::new(),
  66. ).expect("Couldn't copy css/js directories");
  67. }
  68. pub fn new(name: &str) {
  69. let cwd = env::current_dir().expect("Couldn't read current directory");
  70. let project_path = cwd.join(name);
  71. fs::create_dir(&project_path).expect(&format!("Couldn't create directory '{}'", &name));
  72. fs::write(
  73. project_path.join("tlon.toml"),
  74. format!("site_name = \"{}\"", &name),
  75. ).expect("Could not create tlon.toml");
  76. for dir in &["drafts", "posts", "public", "templates", "css", "js"] {
  77. fs::create_dir(&project_path.join(&dir))
  78. .expect(&format!("Couldn't create {} directory", &dir));
  79. }
  80. fs::write(project_path.join("css").join("style.css"), "")
  81. .expect("Couldn't create css/style.css");
  82. fs::write(project_path.join("js").join("index.js"), "").expect("Couldn't create js/index.js");
  83. for file in &["layout", "post_listing", "post", "post_listing_item"] {
  84. fs::write(
  85. &project_path
  86. .join("templates")
  87. .join(format!("{}.html", file)),
  88. "",
  89. ).expect(&format!("Couldn't write templates/{}.html", file));
  90. }
  91. }
  92. pub fn watch(include_drafts: bool) -> notify::Result<()> {
  93. let cwd = env::current_dir().expect("Couldn't read current directory");
  94. let (tx, rx) = channel();
  95. let mut watcher: RecommendedWatcher = try!(Watcher::new(tx, Duration::from_secs(2)));
  96. try!(watcher.watch(&cwd.join("posts"), RecursiveMode::Recursive));
  97. println!("Watching {}/posts", cwd.to_str().unwrap());
  98. loop {
  99. match rx.recv() {
  100. Ok(_) => {
  101. build(include_drafts);
  102. }
  103. Err(e) => println!("watch error: {:?}", e),
  104. }
  105. }
  106. }
  107. #[cfg(test)]
  108. mod tests {
  109. #[allow(unused_imports)]
  110. use super::*;
  111. #[allow(unused_imports)]
  112. use uuid::Uuid;
  113. #[test]
  114. fn test_new() {
  115. let temp_dir = env::temp_dir();
  116. env::set_current_dir(&temp_dir).unwrap();
  117. let uuid = Uuid::new_v4().to_string();
  118. let project_dir = temp_dir.join(&uuid);
  119. new(&uuid);
  120. for dir in &["public", "posts", "templates"] {
  121. fs::read_dir(&project_dir.join(dir)).unwrap();
  122. }
  123. assert_eq!(
  124. "",
  125. fs::read_to_string(&project_dir.join("templates").join("post_listing.html")).unwrap()
  126. );
  127. assert_eq!(
  128. "",
  129. fs::read_to_string(&project_dir.join("templates").join("layout.html")).unwrap()
  130. );
  131. assert_eq!(
  132. "",
  133. fs::read_to_string(&project_dir.join("templates").join("post_listing_item.html"))
  134. .unwrap()
  135. );
  136. assert_eq!(
  137. "",
  138. fs::read_to_string(&project_dir.join("templates").join("post.html")).unwrap()
  139. );
  140. assert_eq!(
  141. "",
  142. fs::read_to_string(&project_dir.join("css").join("style.css")).unwrap()
  143. );
  144. assert_eq!(
  145. "",
  146. fs::read_to_string(&project_dir.join("js").join("index.js")).unwrap()
  147. );
  148. assert_eq!(
  149. format!("site_name = \"{}\"", &uuid),
  150. fs::read_to_string(&project_dir.join("tlon.toml")).unwrap()
  151. );
  152. fs::remove_dir_all(project_dir).unwrap();
  153. }
  154. #[test]
  155. fn test_build() {
  156. let temp_dir = env::temp_dir();
  157. let uuid = Uuid::new_v4().to_string();
  158. let project_dir = temp_dir.join(&uuid);
  159. fs::create_dir(&project_dir).unwrap();
  160. env::set_current_dir(&project_dir).unwrap();
  161. fs::create_dir(project_dir.join("posts")).unwrap();
  162. fs::create_dir(project_dir.join("public")).unwrap();
  163. fs::create_dir(project_dir.join("templates")).unwrap();
  164. fs::create_dir(project_dir.join("css")).unwrap();
  165. fs::create_dir(project_dir.join("js")).unwrap();
  166. fs::write(
  167. project_dir.join("css").join("style.css"),
  168. "body { background: blue; }",
  169. ).unwrap();
  170. fs::write(
  171. project_dir.join("js").join("index.js"),
  172. "window.onload = function () { alert() }",
  173. ).unwrap();
  174. fs::write(
  175. project_dir.join("templates").join("layout.html"),
  176. "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>",
  177. ).unwrap();
  178. fs::write(
  179. project_dir.join("templates").join("post.html"),
  180. "<article><h1>{{ title }}</h1><div>{{ body }}</div></article>",
  181. ).unwrap();
  182. fs::write(
  183. project_dir.join("templates").join("post_listing.html"),
  184. "<ul>{{ post_listing }}</ul>",
  185. ).unwrap();
  186. fs::write(
  187. project_dir.join("templates").join("post_listing_item.html"),
  188. "<li><a href=\"/{{ slug }}\">{{ title }}</a></li>",
  189. ).unwrap();
  190. fs::write(
  191. project_dir.join("posts").join("first-post.md"),
  192. "# First post\n\nThis is the first post\n\nIt has multiple paragraphs",
  193. ).unwrap();
  194. fs::write(project_dir.join("tlon.toml"), "site_name = \"Test Site\"").unwrap();
  195. build(false);
  196. assert_eq!(
  197. "<html><head><title>Test Site</title></head><body><ul><li><a href=\"/fir\
  198. st-post\">First post</a></li></ul></body></html>",
  199. fs::read_to_string(project_dir.join("public").join("index.html")).unwrap(),
  200. );
  201. assert_eq!(
  202. "<html><head><title>First post | Test Site</title></head><body><article><h1>First pos\
  203. t</h1><div><p>This is the first post</p><p>It has multiple paragra\
  204. phs</p></div></article></body></html>",
  205. fs::read_to_string(
  206. project_dir
  207. .join("public")
  208. .join("first-post")
  209. .join("index.html")
  210. ).unwrap()
  211. .replace("\n", ""),
  212. );
  213. assert_eq!(
  214. "body { background: blue; }",
  215. fs::read_to_string(project_dir.join("public").join("css").join("style.css")).unwrap()
  216. );
  217. assert_eq!(
  218. "window.onload = function () { alert() }",
  219. fs::read_to_string(project_dir.join("public").join("js").join("index.js")).unwrap()
  220. );
  221. fs::remove_dir_all(project_dir).unwrap();
  222. }
  223. #[test]
  224. fn test_build_drafts() {
  225. let temp_dir = env::temp_dir();
  226. let uuid = Uuid::new_v4().to_string();
  227. let project_dir = temp_dir.join(&uuid);
  228. fs::create_dir(&project_dir).unwrap();
  229. env::set_current_dir(&project_dir).unwrap();
  230. fs::create_dir(project_dir.join("drafts")).unwrap();
  231. fs::create_dir(project_dir.join("posts")).unwrap();
  232. fs::create_dir(project_dir.join("public")).unwrap();
  233. fs::create_dir(project_dir.join("templates")).unwrap();
  234. fs::create_dir(project_dir.join("css")).unwrap();
  235. fs::create_dir(project_dir.join("js")).unwrap();
  236. fs::write(
  237. project_dir.join("css").join("style.css"),
  238. "body { background: blue; }",
  239. ).unwrap();
  240. fs::write(
  241. project_dir.join("js").join("index.js"),
  242. "window.onload = function () { alert() }",
  243. ).unwrap();
  244. fs::write(
  245. project_dir.join("templates").join("layout.html"),
  246. "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>",
  247. ).unwrap();
  248. fs::write(
  249. project_dir.join("templates").join("post.html"),
  250. "<article><h1>{{ title }}</h1><div>{{ body }}</div></article>",
  251. ).unwrap();
  252. fs::write(
  253. project_dir.join("templates").join("post_listing.html"),
  254. "<ul>{{ post_listing }}</ul>",
  255. ).unwrap();
  256. fs::write(
  257. project_dir.join("templates").join("post_listing_item.html"),
  258. "<li><a href=\"/{{ slug }}\">{{ title }}</a></li>",
  259. ).unwrap();
  260. fs::write(
  261. project_dir.join("posts").join("first-post.md"),
  262. "# First post\n\nThis is the first post",
  263. ).unwrap();
  264. fs::write(
  265. project_dir.join("drafts").join("first-draft.md"),
  266. "# First draft\n\nThis is the first draft",
  267. ).unwrap();
  268. fs::write(project_dir.join("tlon.toml"), "site_name = \"Test Site\"").unwrap();
  269. build(true);
  270. assert_eq!(
  271. "<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>",
  272. fs::read_to_string(project_dir.join("public").join("index.html")).unwrap().replace("\n", ""),
  273. );
  274. assert_eq!(
  275. "<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>",
  276. fs::read_to_string(
  277. project_dir
  278. .join("public")
  279. .join("first-post")
  280. .join("index.html")
  281. ).unwrap()
  282. .replace("\n", ""),
  283. );
  284. assert_eq!(
  285. "<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>",
  286. fs::read_to_string(
  287. project_dir
  288. .join("public")
  289. .join("first-draft")
  290. .join("index.html")
  291. ).unwrap()
  292. .replace("\n", ""),
  293. );
  294. fs::remove_dir_all(project_dir).unwrap();
  295. }
  296. }