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.

post.rs 5.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. use chrono::NaiveDate;
  2. use regex::Regex;
  3. use std::fs;
  4. use std::path;
  5. #[derive(Debug)]
  6. pub struct Post {
  7. pub title: String,
  8. pub body: String,
  9. pub slug: String,
  10. pub date: NaiveDate,
  11. }
  12. pub fn read_posts_dir(cwd: &path::PathBuf) -> Vec<fs::DirEntry> {
  13. match fs::read_dir(cwd) {
  14. Ok(posts) => posts.into_iter().map(|post| post.unwrap()).collect(),
  15. Err(err) => panic!(err),
  16. }
  17. }
  18. pub fn parse_post(path: path::PathBuf) -> Post {
  19. let contents = fs::read_to_string(&path).expect("Couldn't read post file");
  20. lazy_static! {
  21. static ref re: Regex =
  22. Regex::new(r"^# (?P<title>.*) \| (?P<date>\d{4}-\d{2}-\d{2})\n\n(?s)(?P<body>.*)")
  23. .unwrap();
  24. static ref slug_re: Regex = Regex::new(r"(?P<slug>\S+).md").unwrap();
  25. }
  26. let title = &re.captures(&contents).expect("Couldn't parse title")["title"];
  27. let date = &re.captures(&contents).expect("Couldn't parse date")["date"];
  28. let body = &re.captures(&contents).expect("Couldn't parse body")["body"];
  29. let filename = &path.file_name().unwrap().to_str().unwrap();
  30. let slug = &slug_re.captures(filename).expect("Couldn't parse slug")["slug"];
  31. Post {
  32. title: String::from(title),
  33. body: String::from(body),
  34. slug: String::from(slug),
  35. date: NaiveDate::parse_from_str(&date, "%Y-%m-%d").expect("Couldn't parse date"),
  36. }
  37. }
  38. #[cfg(test)]
  39. mod tests {
  40. #[allow(unused_imports)]
  41. use super::*;
  42. #[allow(unused_imports)]
  43. #[allow(unused_imports)]
  44. use std::{env, fs, path};
  45. #[allow(unused_imports)]
  46. use uuid::Uuid;
  47. #[test]
  48. fn test_read_posts_dir() {
  49. let temp_dir = env::temp_dir();
  50. let working_dir = temp_dir.join(&Uuid::new_v4().to_string());
  51. fs::create_dir(&working_dir).unwrap();
  52. env::set_current_dir(&working_dir).unwrap();
  53. let cwd = env::current_dir().unwrap();
  54. fs::create_dir(cwd.join("posts")).unwrap();
  55. let post_body = "# This is a post\n\nHere is some content that goes in the post";
  56. let mut uuids: Vec<String> = vec![];
  57. for _ in 1..11 {
  58. let uuid = String::from(Uuid::new_v4().to_string());
  59. uuids.push(uuid.clone());
  60. fs::write(
  61. cwd.join("posts").join(format!("{}.md", &uuid)),
  62. &String::from(post_body),
  63. )
  64. .unwrap();
  65. }
  66. let mut expected_paths: Vec<String> = uuids
  67. .into_iter()
  68. .map(|uuid| {
  69. String::from(
  70. cwd.join("posts")
  71. .join(format!("{}.md", uuid))
  72. .to_str()
  73. .unwrap(),
  74. )
  75. })
  76. .collect();
  77. expected_paths.sort();
  78. let mut actual_paths: Vec<String> = read_posts_dir(&cwd.join("posts"))
  79. .into_iter()
  80. .map(|dir_entry| String::from(dir_entry.path().to_str().unwrap()))
  81. .collect();
  82. actual_paths.sort();
  83. assert_eq!(expected_paths, actual_paths);
  84. fs::remove_dir_all(temp_dir.join(&working_dir)).unwrap();
  85. }
  86. #[test]
  87. fn test_parse_post() {
  88. let temp_dir = env::temp_dir();
  89. let working_dir = temp_dir.join(&Uuid::new_v4().to_string());
  90. fs::create_dir(&working_dir).unwrap();
  91. env::set_current_dir(&working_dir).unwrap();
  92. let cwd = env::current_dir().unwrap();
  93. fs::create_dir(cwd.join("posts")).unwrap();
  94. let slug = Uuid::new_v4().to_string();
  95. let filename = format!("{}.md", slug);
  96. fs::write(
  97. cwd.join("posts").join(&filename),
  98. "# This is a post | 2019-01-01\n\nHere is some content that goes in the post",
  99. )
  100. .unwrap();
  101. let post = parse_post(cwd.join("posts").join(&filename));
  102. let date = NaiveDate::from_ymd(2019, 1, 1);
  103. assert_eq!("This is a post", post.title);
  104. assert_eq!("Here is some content that goes in the post", post.body);
  105. assert_eq!(slug, post.slug);
  106. assert_eq!(date, post.date);
  107. fs::remove_dir_all(temp_dir.join(&working_dir)).unwrap();
  108. }
  109. #[test]
  110. fn test_post_with_multiple_paragraphs() {
  111. let temp_dir = env::temp_dir();
  112. let working_dir = temp_dir.join(&Uuid::new_v4().to_string());
  113. fs::create_dir(&working_dir).unwrap();
  114. env::set_current_dir(&working_dir).unwrap();
  115. let cwd = env::current_dir().unwrap();
  116. fs::create_dir(cwd.join("posts")).unwrap();
  117. let slug = Uuid::new_v4().to_string();
  118. let filename = format!("{}.md", slug);
  119. fs::write(
  120. cwd.join("posts").join(&filename),
  121. "# This is a post | 2019-01-01\n\nHere is a line\n\nHere is another line\n\nAnd a third",
  122. )
  123. .unwrap();
  124. let post = parse_post(cwd.join("posts").join(&filename));
  125. let date = NaiveDate::from_ymd(2019, 1, 1);
  126. assert_eq!("This is a post", post.title);
  127. assert_eq!(
  128. "Here is a line\n\nHere is another line\n\nAnd a third",
  129. post.body
  130. );
  131. assert_eq!(slug, post.slug);
  132. assert_eq!(date, post.date);
  133. fs::remove_dir_all(temp_dir.join(&working_dir)).unwrap();
  134. }
  135. }