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.

lib.rs 7.9KB

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