A static site generator written in Rust
Ви не можете вибрати більше 25 тем Теми мають розпочинатися з літери або цифри, можуть містити дефіси (-) і не повинні перевищувати 35 символів.

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263
  1. extern crate chrono;
  2. extern crate comrak;
  3. extern crate fs_extra;
  4. #[macro_use]
  5. extern crate lazy_static;
  6. extern crate notify;
  7. extern crate regex;
  8. extern crate toml;
  9. extern crate uuid;
  10. use std::sync::mpsc::channel;
  11. use std::time::Duration;
  12. use std::{fs, path};
  13. use fs_extra::dir;
  14. use notify::{DebouncedEvent, RecommendedWatcher, RecursiveMode, Watcher};
  15. use toml::Value;
  16. use config::Config;
  17. use entry::{parse_entry, read_entry_dir, write_entry, write_entry_listing, Entry};
  18. mod config;
  19. mod entry;
  20. mod page;
  21. mod post;
  22. pub fn build(include_drafts: bool, cwd: &path::Path) {
  23. let config = match fs::read_to_string(cwd.join("casaubon.toml")) {
  24. Ok(contents) => match contents.parse::<Value>() {
  25. Ok(config) => Config {
  26. site_name: String::from(config["site_name"].as_str().unwrap()),
  27. },
  28. Err(_) => panic!("Invalid casaubon.toml"),
  29. },
  30. Err(_) => {
  31. panic!("Can't find casaubon.toml");
  32. }
  33. };
  34. match fs::read_dir(cwd.join("public")) {
  35. Ok(_) => {
  36. fs::remove_dir_all(cwd.join("public")).unwrap();
  37. }
  38. Err(_) => {}
  39. }
  40. fs::create_dir(cwd.join("public")).expect("Couldn't create public directory");
  41. fs::create_dir(cwd.join("public").join("posts")).expect("Couldn't create posts directory");
  42. let layout_template = fs::read_to_string(&cwd.join("templates").join("layout.html"))
  43. .expect("Couldn't find layout template");
  44. let post_template = fs::read_to_string(cwd.join("templates").join("post.html"))
  45. .expect("Couldn't find post template");
  46. let post_listing_template = fs::read_to_string(cwd.join("templates").join("post_listing.html"))
  47. .expect("Couldn't find post listing item template");
  48. let post_item_template =
  49. fs::read_to_string(cwd.join("templates").join("post_listing_item.html"))
  50. .expect("Couldn't find post listing item template");
  51. let page_template = fs::read_to_string(cwd.join("templates").join("page.html"))
  52. .expect("Couldn't find page template");
  53. let post_paths = match include_drafts {
  54. true => {
  55. let mut posts = read_entry_dir(&cwd.join("posts"));
  56. posts.append(&mut read_entry_dir(&cwd.join("drafts")));
  57. posts
  58. }
  59. false => read_entry_dir(&cwd.join("posts")),
  60. };
  61. let page_paths = read_entry_dir(&cwd.join("pages"));
  62. let mut posts: Vec<Entry> = post_paths
  63. .into_iter()
  64. .map(|entry| {
  65. let path = entry.path();
  66. let contents = fs::read_to_string(&path).expect("Couldn't read post file");
  67. parse_entry(&contents, &path)
  68. })
  69. .collect::<Vec<Entry>>();
  70. posts.sort_by(|a, b| b.date.cmp(&a.date));
  71. for post in &posts {
  72. write_entry(&cwd, &layout_template, &post_template, &post, &config);
  73. }
  74. for entry in page_paths.into_iter() {
  75. let path = entry.path();
  76. let contents = fs::read_to_string(&path).expect("Couldn't read page file");
  77. let page = parse_entry(&contents, &path);
  78. write_entry(&cwd, &layout_template, &page_template, &page, &config);
  79. }
  80. write_entry_listing(
  81. &cwd,
  82. &layout_template,
  83. &post_listing_template,
  84. &post_item_template,
  85. &posts,
  86. &config,
  87. );
  88. fs_extra::copy_items(
  89. &vec![cwd.join("css"), cwd.join("js")],
  90. cwd.join("public"),
  91. &dir::CopyOptions::new(),
  92. )
  93. .expect("Couldn't copy css/js directories");
  94. }
  95. pub fn new(name: &str, cwd: &path::Path) {
  96. let project_path = cwd.join(name);
  97. fs::create_dir(&project_path).expect(&format!("Couldn't create directory '{}'", &name));
  98. fs::write(
  99. project_path.join("casaubon.toml"),
  100. format!("site_name = \"{}\"", &name),
  101. )
  102. .expect("Could not create casaubon.toml");
  103. for dir in &[
  104. "drafts",
  105. "posts",
  106. "pages",
  107. "public",
  108. "templates",
  109. "css",
  110. "js",
  111. ] {
  112. fs::create_dir(&project_path.join(&dir))
  113. .expect(&format!("Couldn't create {} directory", &dir));
  114. }
  115. fs::write(project_path.join("css").join("style.css"), "")
  116. .expect("Couldn't create css/style.css");
  117. fs::write(project_path.join("js").join("index.js"), "").expect("Couldn't create js/index.js");
  118. let default_layout_template = format!(
  119. "<html>
  120. <head>
  121. <title>{}</title>
  122. </head>
  123. <body>
  124. <h1>{}</h1>
  125. {{{{ contents }}}}
  126. </body>
  127. </html>\n",
  128. name, name
  129. );
  130. let default_post_listing_template = format!(
  131. "<div>
  132. <h3>Posts</h3>
  133. <ul>{{{{ post_listing }}}}</ul>
  134. </div>\n"
  135. );
  136. let default_post_template = format!(
  137. "<article>
  138. <h1>{{{{ title }}}}</h1>
  139. <div>{{{{ body }}}}</div>
  140. </article>\n"
  141. );
  142. let default_page_template = format!(
  143. "<article>
  144. <h1>{{{{ title }}}}</h1>
  145. <div>{{{{ body }}}}</div>
  146. </article>\n"
  147. );
  148. let default_post_listing_item_template = format!(
  149. "<li>
  150. {{ date }} <a href=\"/posts/{{{{ slug }}}}/\">{{{{ title }}}}</a>
  151. </li>\n"
  152. );
  153. for (filename, contents) in &[
  154. ("layout", &default_layout_template),
  155. ("post_listing", &default_post_listing_template),
  156. ("post", &default_post_template),
  157. ("page", &default_page_template),
  158. ("post_listing_item", &default_post_listing_item_template),
  159. ] {
  160. fs::write(
  161. &project_path
  162. .join("templates")
  163. .join(format!("{}.html", filename)),
  164. &contents,
  165. )
  166. .expect(&format!("Couldn't write templates/{}.html", filename));
  167. }
  168. }
  169. fn should_rebuild(cwd: &path::Path, path: &path::PathBuf) -> bool {
  170. let path_string = path.to_str().unwrap().to_string();
  171. let change_is_from_public = path_string.contains(cwd.join("public").to_str().unwrap());
  172. let change_is_from_git = path_string.contains(cwd.join(".git").to_str().unwrap());
  173. !change_is_from_public && !change_is_from_git
  174. }
  175. pub fn watch(include_drafts: bool, cwd: &path::Path) -> notify::Result<()> {
  176. let (tx, rx) = channel();
  177. let mut watcher: RecommendedWatcher = try!(Watcher::new(tx, Duration::from_secs(2)));
  178. try!(watcher.watch(&cwd, RecursiveMode::Recursive));
  179. println!("Watching {}", cwd.to_str().unwrap());
  180. let handle_event = |path: &path::PathBuf| {
  181. if should_rebuild(&cwd, &path) {
  182. println!("Rebuilding");
  183. build(include_drafts, &cwd);
  184. }
  185. };
  186. loop {
  187. match rx.recv() {
  188. Ok(e) => match e {
  189. DebouncedEvent::Create(path) => {
  190. handle_event(&path);
  191. }
  192. DebouncedEvent::Write(path) => {
  193. handle_event(&path);
  194. }
  195. _ => {}
  196. },
  197. Err(e) => println!("watch error: {:?}", e),
  198. }
  199. }
  200. }
  201. #[cfg(test)]
  202. mod tests {
  203. #[allow(unused_imports)]
  204. use super::*;
  205. #[allow(unused_imports)]
  206. use uuid::Uuid;
  207. use std::env;
  208. #[test]
  209. fn test_should_rebuild() {
  210. let cwd = env::current_dir().unwrap();
  211. assert_eq!(
  212. false,
  213. should_rebuild(&cwd, &cwd.join("public").join("index.html"))
  214. );
  215. assert_eq!(
  216. false,
  217. should_rebuild(&cwd, &cwd.join(".git").join("index.html"))
  218. );
  219. assert_eq!(
  220. true,
  221. should_rebuild(&cwd, &cwd.join("posts").join("test.md"))
  222. );
  223. assert_eq!(
  224. true,
  225. should_rebuild(&cwd, &cwd.join("drafts").join("test.md"))
  226. );
  227. assert_eq!(
  228. true,
  229. should_rebuild(&cwd, &cwd.join("css").join("style.css"))
  230. );
  231. assert_eq!(true, should_rebuild(&cwd, &cwd.join("js").join("index.js")));
  232. }
  233. }