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

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