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.

entry.rs 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354
  1. use chrono::NaiveDate;
  2. use comrak::{markdown_to_html, ComrakOptions};
  3. use regex::Regex;
  4. use std::fs;
  5. use std::path;
  6. use config::Config;
  7. #[derive(Debug)]
  8. pub enum EntryKind {
  9. Page,
  10. Post,
  11. }
  12. #[derive(Debug)]
  13. pub struct Entry {
  14. pub kind: EntryKind,
  15. pub title: String,
  16. pub slug: String,
  17. pub body: String,
  18. pub date: Option<NaiveDate>,
  19. }
  20. impl Entry {
  21. fn render(&self, template: &str, config: &Config) -> String {
  22. template
  23. .replace(
  24. "{{ page_title }}",
  25. &format!("{} | {}", &self.title, &config.site_name),
  26. )
  27. .replace("{{ title }}", &self.title)
  28. .replace("{{ slug }}", &self.slug)
  29. .replace(
  30. "{{ body }}",
  31. &markdown_to_html(&self.body, &ComrakOptions::default()),
  32. )
  33. }
  34. }
  35. pub fn read_entry_dir(cwd: &path::PathBuf) -> Vec<fs::DirEntry> {
  36. match fs::read_dir(cwd) {
  37. Ok(entries) => entries.into_iter().map(|entry| entry.unwrap()).collect(),
  38. Err(err) => panic!(err),
  39. }
  40. }
  41. pub fn parse_entry(path: path::PathBuf) -> Entry {
  42. let contents = fs::read_to_string(&path).expect("Couldn't read post file");
  43. lazy_static! {
  44. static ref re_with_date: Regex =
  45. Regex::new(r"^# (?P<title>.*) \| (?P<date>\d{4}-\d{2}-\d{2})\n\n(?s)(?P<body>.*)")
  46. .unwrap();
  47. static ref re_without_date: Regex =
  48. Regex::new(r"^# (?P<title>.*)\n\n(?s)(?P<body>.*)").unwrap();
  49. static ref slug_re: Regex = Regex::new(r"(?P<slug>\S+).md").unwrap();
  50. }
  51. let filename = &path.file_name().unwrap().to_str().unwrap();
  52. let slug = &slug_re
  53. .captures(filename)
  54. .expect("Couldn't parse slug from filename")["slug"];
  55. if let Some(date_string) = &re_with_date.captures(&contents) {
  56. let title = &re_with_date
  57. .captures(&contents)
  58. .expect("Couldn't parse title")["title"];
  59. let body = &re_with_date
  60. .captures(&contents)
  61. .expect("Couldn't parse title")["body"];
  62. let date = Some(
  63. NaiveDate::parse_from_str(&date_string["date"], "%Y-%m-%d")
  64. .expect("Couldn't parse date"),
  65. );
  66. Entry {
  67. kind: EntryKind::Post,
  68. title: String::from(title),
  69. body: String::from(body),
  70. slug: String::from(slug),
  71. date: date,
  72. }
  73. } else {
  74. let title = &re_without_date
  75. .captures(&contents)
  76. .expect("Couldn't parse title")["title"];
  77. let body = &re_without_date
  78. .captures(&contents)
  79. .expect("Couldn't parse title")["body"];
  80. Entry {
  81. kind: EntryKind::Page,
  82. title: String::from(title),
  83. body: String::from(body),
  84. slug: String::from(slug),
  85. date: None,
  86. }
  87. }
  88. }
  89. pub fn write_entry(
  90. cwd: &path::PathBuf,
  91. layout: &str,
  92. post_template: &str,
  93. entry: &Entry,
  94. config: &Config,
  95. ) {
  96. let root_path = match entry.kind {
  97. EntryKind::Post => cwd.join("public").join("posts").join(&entry.slug),
  98. EntryKind::Page => cwd.join("public").join(&entry.slug),
  99. };
  100. match fs::create_dir(&root_path) {
  101. Ok(_) => {}
  102. Err(err) => match err.kind() {
  103. std::io::ErrorKind::AlreadyExists => {}
  104. _ => panic!(err),
  105. },
  106. }
  107. let template = layout.replace("{{ contents }}", &post_template);
  108. fs::write(
  109. &root_path.join("index.html"),
  110. entry.render(&template, &config),
  111. )
  112. .expect("Unable to write file");
  113. }
  114. pub fn write_entry_listing(
  115. cwd: &path::PathBuf,
  116. layout: &str,
  117. post_listing_template: &str,
  118. post_item_template: &str,
  119. posts: &Vec<Entry>,
  120. config: &Config,
  121. ) {
  122. fs::write(
  123. cwd.join("public").join("index.html"),
  124. render_post_listing(
  125. layout,
  126. post_listing_template,
  127. post_item_template,
  128. posts,
  129. config,
  130. ),
  131. )
  132. .expect("Unable to write file");
  133. }
  134. pub fn render_post_listing(
  135. layout: &str,
  136. post_listing_template: &str,
  137. post_item_template: &str,
  138. posts: &Vec<Entry>,
  139. config: &Config,
  140. ) -> String {
  141. layout
  142. .replace("{{ page_title }}", &format!("{}", config.site_name))
  143. .replace(
  144. "{{ contents }}",
  145. &post_listing_template.replace(
  146. "{{ post_listing }}",
  147. &posts
  148. .iter()
  149. .map(|post| post.render(&post_item_template, &config))
  150. .collect::<Vec<String>>()
  151. .join("\n"),
  152. ),
  153. )
  154. }
  155. #[cfg(test)]
  156. mod tests {
  157. use super::{render_post_listing, write_entry, write_entry_listing, Config, Entry, EntryKind};
  158. use chrono::NaiveDate;
  159. use std::{env, fs};
  160. use uuid::Uuid;
  161. #[test]
  162. fn test_render_post() {
  163. let config = Config {
  164. site_name: String::from("Test Site"),
  165. };
  166. let post = Entry {
  167. title: String::from("hello world"),
  168. body: String::from("lorem ipsum dolor sit amet"),
  169. slug: String::from("hello-world"),
  170. date: Some(NaiveDate::from_ymd(2019, 1, 1)),
  171. kind: EntryKind::Post,
  172. };
  173. let output = post.render(
  174. "<html><head><title>{{ page_title }}</title></head><body><article><h1>{{ title }}</h1><div>{{ body }}</div></article></body></html>", &config
  175. )
  176. .replace("\n", "");
  177. assert_eq!(
  178. "<html><head><title>hello world | Test Site</title></head><body><article><h1>hello world</h1><div><p>lorem ipsum dolor sit amet</p></div></article></body></html>",
  179. &output,
  180. );
  181. }
  182. #[test]
  183. fn test_render_post_listing() {
  184. let config = Config {
  185. site_name: String::from("Test Site"),
  186. };
  187. let posts = vec![
  188. Entry {
  189. title: String::from("First post"),
  190. body: String::from("lorem ipsum dolor sit amet"),
  191. slug: String::from("first-post"),
  192. date: Some(NaiveDate::from_ymd(2019, 1, 1)),
  193. kind: EntryKind::Post,
  194. },
  195. Entry {
  196. title: String::from("Second post"),
  197. body: String::from("lorem ipsum dolor sit amet"),
  198. slug: String::from("second-post"),
  199. date: Some(NaiveDate::from_ymd(2019, 1, 1)),
  200. kind: EntryKind::Post,
  201. },
  202. Entry {
  203. title: String::from("Third post"),
  204. body: String::from("lorem ipsum dolor sit amet"),
  205. slug: String::from("third-post"),
  206. date: Some(NaiveDate::from_ymd(2019, 1, 1)),
  207. kind: EntryKind::Post,
  208. },
  209. ];
  210. let output = render_post_listing(
  211. "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>",
  212. "<ul>{{ post_listing }}</ul>",
  213. "<li><a href=\"/{{ slug }}\">{{ title }}</a></li>",
  214. &posts,
  215. &config,
  216. )
  217. .replace("\n", "");
  218. assert_eq!(
  219. "<html><head><title>Test Site</title></head><body><ul><li><a href=\"/first-post\">First post</a></li><li><a href=\"/second-post\">Second post</a></li><li><a href=\"/third-post\">Third post</a></li></ul></body></html>",
  220. &output,
  221. );
  222. }
  223. #[test]
  224. fn test_write_entry() {
  225. let temp_dir = env::temp_dir();
  226. let working_dir = temp_dir.join(&Uuid::new_v4().to_string());
  227. fs::create_dir(&working_dir).unwrap();
  228. env::set_current_dir(&working_dir).unwrap();
  229. let cwd = env::current_dir().unwrap();
  230. fs::create_dir(cwd.join("public")).unwrap();
  231. fs::create_dir(cwd.join("public").join("posts")).unwrap();
  232. let layout =
  233. "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>";
  234. let post_template = "<article><h1>{{ title }}</h1><div>{{ body }}</div></article>";
  235. let post = Entry {
  236. title: String::from("Hello world"),
  237. body: String::from("Lorem ipsum dolor sit amet"),
  238. slug: String::from("hello-world"),
  239. date: Some(NaiveDate::from_ymd(2019, 1, 1)),
  240. kind: EntryKind::Post,
  241. };
  242. let config = Config {
  243. site_name: "Test Site".to_string(),
  244. };
  245. write_entry(&cwd, &layout, &post_template, &post, &config);
  246. let content = fs::read_to_string(
  247. cwd.join("public")
  248. .join("posts")
  249. .join("hello-world")
  250. .join("index.html"),
  251. )
  252. .unwrap();
  253. assert_eq!(
  254. "<html><head><title>Hello world | Test Site</title></head><body><article><h1>Hello world</h1><div><p>Lorem ipsum dolor sit amet</p></div></article></body></html>",
  255. content.replace("\n", "")
  256. );
  257. fs::remove_dir_all(temp_dir.join(&working_dir)).unwrap();
  258. }
  259. #[test]
  260. fn test_write_post_listing() {
  261. let temp_dir = env::temp_dir();
  262. let working_dir = temp_dir.join(&Uuid::new_v4().to_string());
  263. fs::create_dir(&working_dir).unwrap();
  264. env::set_current_dir(&working_dir).unwrap();
  265. let cwd = env::current_dir().unwrap();
  266. fs::create_dir(cwd.join("public")).unwrap();
  267. let layout =
  268. "<html><head><title>{{ page_title }}</title></head><body>{{ contents }}</body></html>";
  269. let post_listing_template = "<ul>{{ post_listing }}</ul>";
  270. let post_item_template = "<li><a href=\"/{{ slug }}\">{{ title }}</a></li>";
  271. let posts = vec![
  272. Entry {
  273. title: String::from("First post"),
  274. body: String::from("lorem ipsum dolor sit amet"),
  275. slug: String::from("first-post"),
  276. date: Some(NaiveDate::from_ymd(2019, 1, 1)),
  277. kind: EntryKind::Post,
  278. },
  279. Entry {
  280. title: String::from("Second post"),
  281. body: String::from("lorem ipsum dolor sit amet"),
  282. slug: String::from("second-post"),
  283. date: Some(NaiveDate::from_ymd(2019, 1, 1)),
  284. kind: EntryKind::Post,
  285. },
  286. Entry {
  287. title: String::from("Third post"),
  288. body: String::from("lorem ipsum dolor sit amet"),
  289. slug: String::from("third-post"),
  290. date: Some(NaiveDate::from_ymd(2019, 1, 1)),
  291. kind: EntryKind::Post,
  292. },
  293. ];
  294. let config = Config {
  295. site_name: "Test Site".to_string(),
  296. };
  297. write_entry_listing(
  298. &cwd,
  299. &layout,
  300. &post_listing_template,
  301. &post_item_template,
  302. &posts,
  303. &config,
  304. );
  305. assert_eq!(
  306. "<html><head><title>Test Site</title></head><body><ul><li><a href\
  307. =\"/first-post\">First post</a></li><li><a href=\"/second-post\">\
  308. Second post</a></li><li><a href=\"/third-post\">Third post</a></li\
  309. ></ul></body></html>",
  310. fs::read_to_string(&cwd.join("public").join("index.html"))
  311. .unwrap()
  312. .replace("\n", ""),
  313. );
  314. fs::remove_dir_all(temp_dir.join(&working_dir)).unwrap();
  315. }
  316. }