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.

487 lines
17KB

  1. /// A page, can be a blog post or a basic page
  2. use std::cmp::Ordering;
  3. use std::collections::HashMap;
  4. use std::fs::{read_dir};
  5. use std::path::{Path, PathBuf};
  6. use std::result::Result as StdResult;
  7. use tera::{Tera, Context};
  8. use serde::ser::{SerializeStruct, self};
  9. use slug::slugify;
  10. use errors::{Result, ResultExt};
  11. use config::Config;
  12. use front_matter::{FrontMatter, SortBy, split_content};
  13. use section::Section;
  14. use markdown::markdown_to_html;
  15. use utils::{read_file, find_content_components};
  16. /// Looks into the current folder for the path and see if there's anything that is not a .md
  17. /// file. Those will be copied next to the rendered .html file
  18. fn find_related_assets(path: &Path) -> Vec<PathBuf> {
  19. let mut assets = vec![];
  20. for entry in read_dir(path).unwrap().filter_map(|e| e.ok()) {
  21. let entry_path = entry.path();
  22. if entry_path.is_file() {
  23. match entry_path.extension() {
  24. Some(e) => match e.to_str() {
  25. Some("md") => continue,
  26. _ => assets.push(entry_path.to_path_buf()),
  27. },
  28. None => continue,
  29. }
  30. }
  31. }
  32. assets
  33. }
  34. #[derive(Clone, Debug, PartialEq)]
  35. pub struct Page {
  36. /// The .md path
  37. pub file_path: PathBuf,
  38. /// The .md path, starting from the content directory, with / slashes
  39. pub relative_path: String,
  40. /// The parent directory of the file. Is actually the grand parent directory
  41. /// if it's an asset folder
  42. pub parent_path: PathBuf,
  43. /// The name of the .md file
  44. pub file_name: String,
  45. /// The directories above our .md file
  46. /// for example a file at content/kb/solutions/blabla.md will have 2 components:
  47. /// `kb` and `solutions`
  48. pub components: Vec<String>,
  49. /// The actual content of the page, in markdown
  50. pub raw_content: String,
  51. /// All the non-md files we found next to the .md file
  52. pub assets: Vec<PathBuf>,
  53. /// The HTML rendered of the page
  54. pub content: String,
  55. /// The front matter meta-data
  56. pub meta: FrontMatter,
  57. /// The slug of that page.
  58. /// First tries to find the slug in the meta and defaults to filename otherwise
  59. pub slug: String,
  60. /// The URL path of the page
  61. pub path: String,
  62. /// The full URL for that page
  63. pub permalink: String,
  64. /// The summary for the article, defaults to None
  65. /// When <!-- more --> is found in the text, will take the content up to that part
  66. /// as summary
  67. pub summary: Option<String>,
  68. /// The previous page, by whatever sorting is used for the index/section
  69. pub previous: Option<Box<Page>>,
  70. /// The next page, by whatever sorting is used for the index/section
  71. pub next: Option<Box<Page>>,
  72. }
  73. impl Page {
  74. pub fn new(meta: FrontMatter) -> Page {
  75. Page {
  76. file_path: PathBuf::new(),
  77. relative_path: String::new(),
  78. parent_path: PathBuf::new(),
  79. file_name: "".to_string(),
  80. components: vec![],
  81. raw_content: "".to_string(),
  82. assets: vec![],
  83. content: "".to_string(),
  84. slug: "".to_string(),
  85. path: "".to_string(),
  86. permalink: "".to_string(),
  87. summary: None,
  88. meta: meta,
  89. previous: None,
  90. next: None,
  91. }
  92. }
  93. pub fn has_date(&self) -> bool {
  94. self.meta.date.is_some()
  95. }
  96. /// Get word count and estimated reading time
  97. pub fn get_reading_analytics(&self) -> (usize, usize) {
  98. // Only works for latin language but good enough for a start
  99. let word_count: usize = self.raw_content.split_whitespace().count();
  100. // https://help.medium.com/hc/en-us/articles/214991667-Read-time
  101. // 275 seems a bit too high though
  102. (word_count, (word_count / 200))
  103. }
  104. /// Parse a page given the content of the .md file
  105. /// Files without front matter or with invalid front matter are considered
  106. /// erroneous
  107. pub fn parse(file_path: &Path, content: &str, config: &Config) -> Result<Page> {
  108. // 1. separate front matter from content
  109. let (meta, content) = split_content(file_path, content)?;
  110. let mut page = Page::new(meta);
  111. page.file_path = file_path.to_path_buf();
  112. page.parent_path = page.file_path.parent().unwrap().to_path_buf();
  113. page.raw_content = content;
  114. let path = Path::new(file_path);
  115. page.file_name = path.file_stem().unwrap().to_string_lossy().to_string();
  116. page.slug = {
  117. if let Some(ref slug) = page.meta.slug {
  118. slug.trim().to_string()
  119. } else {
  120. slugify(page.file_name.clone())
  121. }
  122. };
  123. page.components = find_content_components(&page.file_path);
  124. page.relative_path = format!("{}/{}.md", page.components.join("/"), page.file_name);
  125. // 4. Find sections
  126. // Pages with custom urls exists outside of sections
  127. if let Some(ref u) = page.meta.url {
  128. page.path = u.trim().to_string();
  129. } else if !page.components.is_empty() {
  130. // If we have a folder with an asset, don't consider it as a component
  131. if page.file_name == "index" {
  132. page.components.pop();
  133. // also set parent_path to grandparent instead
  134. page.parent_path = page.parent_path.parent().unwrap().to_path_buf();
  135. }
  136. // Don't add a trailing slash to sections
  137. page.path = format!("{}/{}", page.components.join("/"), page.slug);
  138. } else {
  139. page.path = page.slug.clone();
  140. }
  141. page.permalink = config.make_permalink(&page.path);
  142. Ok(page)
  143. }
  144. /// Read and parse a .md file into a Page struct
  145. pub fn from_file<P: AsRef<Path>>(path: P, config: &Config) -> Result<Page> {
  146. let path = path.as_ref();
  147. let content = read_file(path)?;
  148. let mut page = Page::parse(path, &content, config)?;
  149. page.assets = find_related_assets(path.parent().unwrap());
  150. if !page.assets.is_empty() && page.file_name != "index" {
  151. bail!("Page `{}` has assets ({:?}) but is not named index.md", path.display(), page.assets);
  152. }
  153. Ok(page)
  154. }
  155. /// We need access to all pages url to render links relative to content
  156. /// so that can't happen at the same time as parsing
  157. pub fn render_markdown(&mut self, permalinks: &HashMap<String, String>, tera: &Tera, config: &Config) -> Result<()> {
  158. self.content = markdown_to_html(&self.raw_content, permalinks, tera, config)?;
  159. if self.raw_content.contains("<!-- more -->") {
  160. self.summary = Some({
  161. let summary = self.raw_content.splitn(2, "<!-- more -->").collect::<Vec<&str>>()[0];
  162. markdown_to_html(summary, permalinks, tera, config)?
  163. })
  164. }
  165. Ok(())
  166. }
  167. /// Renders the page using the default layout, unless specified in front-matter
  168. pub fn render_html(&self, tera: &Tera, config: &Config) -> Result<String> {
  169. let tpl_name = match self.meta.template {
  170. Some(ref l) => l.to_string(),
  171. None => "page.html".to_string()
  172. };
  173. let mut context = Context::new();
  174. context.add("config", config);
  175. context.add("page", self);
  176. context.add("current_url", &self.permalink);
  177. context.add("current_path", &self.path);
  178. tera.render(&tpl_name, &context)
  179. .chain_err(|| format!("Failed to render page '{}'", self.file_path.display()))
  180. }
  181. }
  182. impl ser::Serialize for Page {
  183. fn serialize<S>(&self, serializer: S) -> StdResult<S::Ok, S::Error> where S: ser::Serializer {
  184. let mut state = serializer.serialize_struct("page", 16)?;
  185. state.serialize_field("content", &self.content)?;
  186. state.serialize_field("title", &self.meta.title)?;
  187. state.serialize_field("description", &self.meta.description)?;
  188. state.serialize_field("date", &self.meta.date)?;
  189. state.serialize_field("slug", &self.slug)?;
  190. state.serialize_field("path", &format!("/{}", self.path))?;
  191. state.serialize_field("permalink", &self.permalink)?;
  192. state.serialize_field("summary", &self.summary)?;
  193. state.serialize_field("tags", &self.meta.tags)?;
  194. state.serialize_field("draft", &self.meta.draft)?;
  195. state.serialize_field("category", &self.meta.category)?;
  196. state.serialize_field("extra", &self.meta.extra)?;
  197. let (word_count, reading_time) = self.get_reading_analytics();
  198. state.serialize_field("word_count", &word_count)?;
  199. state.serialize_field("reading_time", &reading_time)?;
  200. state.serialize_field("previous", &self.previous)?;
  201. state.serialize_field("next", &self.next)?;
  202. state.end()
  203. }
  204. }
  205. /// Sort pages using the method for the given section
  206. ///
  207. /// Any pages that doesn't have a date when the sorting method is date or order
  208. /// when the sorting method is order will be ignored.
  209. pub fn sort_pages(pages: Vec<Page>, section: Option<&Section>) -> (Vec<Page>, Vec<Page>) {
  210. let sort_by = if let Some(ref sec) = section {
  211. sec.meta.sort_by()
  212. } else {
  213. SortBy::Date
  214. };
  215. match sort_by {
  216. SortBy::Date => {
  217. let mut can_be_sorted = vec![];
  218. let mut cannot_be_sorted = vec![];
  219. for page in pages {
  220. if page.meta.date.is_some() {
  221. can_be_sorted.push(page);
  222. } else {
  223. cannot_be_sorted.push(page);
  224. }
  225. }
  226. can_be_sorted.sort_by(|a, b| b.meta.date().unwrap().cmp(&a.meta.date().unwrap()));
  227. (can_be_sorted, cannot_be_sorted)
  228. },
  229. SortBy::Order => {
  230. let mut can_be_sorted = vec![];
  231. let mut cannot_be_sorted = vec![];
  232. for page in pages {
  233. if page.meta.order.is_some() {
  234. can_be_sorted.push(page);
  235. } else {
  236. cannot_be_sorted.push(page);
  237. }
  238. }
  239. can_be_sorted.sort_by(|a, b| b.meta.order().cmp(&a.meta.order()));
  240. (can_be_sorted, cannot_be_sorted)
  241. },
  242. SortBy::None => {
  243. let mut p = vec![];
  244. for page in pages {
  245. p.push(page);
  246. }
  247. (p, vec![])
  248. },
  249. }
  250. }
  251. /// Used only by the RSS feed (I think)
  252. impl PartialOrd for Page {
  253. fn partial_cmp(&self, other: &Page) -> Option<Ordering> {
  254. if self.meta.date.is_none() {
  255. return Some(Ordering::Less);
  256. }
  257. if other.meta.date.is_none() {
  258. return Some(Ordering::Greater);
  259. }
  260. let this_date = self.meta.date().unwrap();
  261. let other_date = other.meta.date().unwrap();
  262. if this_date > other_date {
  263. return Some(Ordering::Less);
  264. }
  265. if this_date < other_date {
  266. return Some(Ordering::Greater);
  267. }
  268. Some(Ordering::Equal)
  269. }
  270. }
  271. /// Horribly inefficient way to set previous and next on each pages
  272. /// So many clones
  273. pub fn populate_previous_and_next_pages(input: &[Page]) -> Vec<Page> {
  274. let pages = input.to_vec();
  275. let mut res = Vec::new();
  276. // the input is already sorted
  277. // We might put prev/next randomly if a page is missing date/order, probably fine
  278. for (i, page) in input.iter().enumerate() {
  279. let mut new_page = page.clone();
  280. if i > 0 {
  281. let next = &pages[i - 1];
  282. new_page.next = Some(Box::new(next.clone()));
  283. }
  284. if i < input.len() - 1 {
  285. let previous = &pages[i + 1];
  286. new_page.previous = Some(Box::new(previous.clone()));
  287. }
  288. res.push(new_page);
  289. }
  290. res
  291. }
  292. #[cfg(test)]
  293. mod tests {
  294. use tempdir::TempDir;
  295. use std::fs::File;
  296. use std::path::Path;
  297. use front_matter::{FrontMatter, SortBy};
  298. use section::Section;
  299. use super::{Page, find_related_assets, sort_pages, populate_previous_and_next_pages};
  300. fn create_page_with_date(date: &str) -> Page {
  301. let mut front_matter = FrontMatter::default();
  302. front_matter.date = Some(date.to_string());
  303. Page::new(front_matter)
  304. }
  305. fn create_page_with_order(order: usize) -> Page {
  306. let mut front_matter = FrontMatter::default();
  307. front_matter.order = Some(order);
  308. Page::new(front_matter)
  309. }
  310. #[test]
  311. fn test_find_related_assets() {
  312. let tmp_dir = TempDir::new("example").expect("create temp dir");
  313. File::create(tmp_dir.path().join("index.md")).unwrap();
  314. File::create(tmp_dir.path().join("example.js")).unwrap();
  315. File::create(tmp_dir.path().join("graph.jpg")).unwrap();
  316. File::create(tmp_dir.path().join("fail.png")).unwrap();
  317. let assets = find_related_assets(tmp_dir.path());
  318. assert_eq!(assets.len(), 3);
  319. assert_eq!(assets.iter().filter(|p| p.extension().unwrap() != "md").count(), 3);
  320. assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "example.js").count(), 1);
  321. assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "graph.jpg").count(), 1);
  322. assert_eq!(assets.iter().filter(|p| p.file_name().unwrap() == "fail.png").count(), 1);
  323. }
  324. #[test]
  325. fn test_can_default_sort() {
  326. let input = vec![
  327. create_page_with_date("2018-01-01"),
  328. create_page_with_date("2017-01-01"),
  329. create_page_with_date("2019-01-01"),
  330. ];
  331. let (pages, _) = sort_pages(input, None);
  332. // Should be sorted by date
  333. assert_eq!(pages[0].clone().meta.date.unwrap(), "2019-01-01");
  334. assert_eq!(pages[1].clone().meta.date.unwrap(), "2018-01-01");
  335. assert_eq!(pages[2].clone().meta.date.unwrap(), "2017-01-01");
  336. }
  337. #[test]
  338. fn test_can_sort_dates() {
  339. let input = vec![
  340. create_page_with_date("2018-01-01"),
  341. create_page_with_date("2017-01-01"),
  342. create_page_with_date("2019-01-01"),
  343. ];
  344. let mut front_matter = FrontMatter::default();
  345. front_matter.sort_by = Some(SortBy::Date);
  346. let section = Section::new(Path::new("hey"), front_matter);
  347. let (pages, _) = sort_pages(input, Some(&section));
  348. // Should be sorted by date
  349. assert_eq!(pages[0].clone().meta.date.unwrap(), "2019-01-01");
  350. assert_eq!(pages[1].clone().meta.date.unwrap(), "2018-01-01");
  351. assert_eq!(pages[2].clone().meta.date.unwrap(), "2017-01-01");
  352. }
  353. #[test]
  354. fn test_can_sort_order() {
  355. let input = vec![
  356. create_page_with_order(2),
  357. create_page_with_order(3),
  358. create_page_with_order(1),
  359. ];
  360. let mut front_matter = FrontMatter::default();
  361. front_matter.sort_by = Some(SortBy::Order);
  362. let section = Section::new(Path::new("hey"), front_matter);
  363. let (pages, _) = sort_pages(input, Some(&section));
  364. // Should be sorted by date
  365. assert_eq!(pages[0].clone().meta.order.unwrap(), 3);
  366. assert_eq!(pages[1].clone().meta.order.unwrap(), 2);
  367. assert_eq!(pages[2].clone().meta.order.unwrap(), 1);
  368. }
  369. #[test]
  370. fn test_can_sort_none() {
  371. let input = vec![
  372. create_page_with_order(2),
  373. create_page_with_order(3),
  374. create_page_with_order(1),
  375. ];
  376. let mut front_matter = FrontMatter::default();
  377. front_matter.sort_by = Some(SortBy::None);
  378. let section = Section::new(Path::new("hey"), front_matter);
  379. let (pages, _) = sort_pages(input, Some(&section));
  380. // Should be sorted by date
  381. assert_eq!(pages[0].clone().meta.order.unwrap(), 2);
  382. assert_eq!(pages[1].clone().meta.order.unwrap(), 3);
  383. assert_eq!(pages[2].clone().meta.order.unwrap(), 1);
  384. }
  385. #[test]
  386. fn test_ignore_page_with_missing_field() {
  387. let input = vec![
  388. create_page_with_order(2),
  389. create_page_with_order(3),
  390. create_page_with_date("2019-01-01"),
  391. ];
  392. let mut front_matter = FrontMatter::default();
  393. front_matter.sort_by = Some(SortBy::Order);
  394. let section = Section::new(Path::new("hey"), front_matter);
  395. let (pages, unsorted) = sort_pages(input, Some(&section));
  396. assert_eq!(pages.len(), 2);
  397. assert_eq!(unsorted.len(), 1);
  398. }
  399. #[test]
  400. fn test_populate_previous_and_next_pages() {
  401. let input = vec![
  402. create_page_with_order(3),
  403. create_page_with_order(2),
  404. create_page_with_order(1),
  405. ];
  406. let pages = populate_previous_and_next_pages(input.as_slice());
  407. assert!(pages[0].clone().next.is_none());
  408. assert!(pages[0].clone().previous.is_some());
  409. assert_eq!(pages[0].clone().previous.unwrap().meta.order.unwrap(), 2);
  410. assert!(pages[1].clone().next.is_some());
  411. assert!(pages[1].clone().previous.is_some());
  412. assert_eq!(pages[1].clone().next.unwrap().meta.order.unwrap(), 3);
  413. assert_eq!(pages[1].clone().previous.unwrap().meta.order.unwrap(), 1);
  414. assert!(pages[2].clone().next.is_some());
  415. assert!(pages[2].clone().previous.is_none());
  416. assert_eq!(pages[2].clone().next.unwrap().meta.order.unwrap(), 2);
  417. }
  418. }