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.

178 lines
5.5KB

  1. use std::collections::HashMap;
  2. use std::path::Path;
  3. use toml;
  4. use tera::Value;
  5. use chrono::prelude::*;
  6. use regex::Regex;
  7. use errors::{Result, ResultExt};
  8. lazy_static! {
  9. static ref PAGE_RE: Regex = Regex::new(r"^\r?\n?\+\+\+\r?\n((?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap();
  10. }
  11. #[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
  12. #[serde(rename_all = "lowercase")]
  13. pub enum SortBy {
  14. Date,
  15. Order,
  16. None,
  17. }
  18. /// The front matter of every page
  19. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
  20. pub struct FrontMatter {
  21. /// <title> of the page
  22. pub title: Option<String>,
  23. /// Description in <meta> that appears when linked, e.g. on twitter
  24. pub description: Option<String>,
  25. /// Date if we want to order pages (ie blog post)
  26. pub date: Option<String>,
  27. /// The page slug. Will be used instead of the filename if present
  28. /// Can't be an empty string if present
  29. pub slug: Option<String>,
  30. /// The url the page appears at, overrides the slug if set in the front-matter
  31. /// otherwise is set after parsing front matter and sections
  32. /// Can't be an empty string if present
  33. pub url: Option<String>,
  34. /// Tags, not to be confused with categories
  35. pub tags: Option<Vec<String>>,
  36. /// Whether this page is a draft and should be published or not
  37. pub draft: Option<bool>,
  38. /// Only one category allowed
  39. pub category: Option<String>,
  40. /// Whether to sort by "date", "order" or "none". Defaults to `none`.
  41. #[serde(skip_serializing)]
  42. pub sort_by: Option<SortBy>,
  43. /// Integer to use to order content. Lowest is at the bottom, highest first
  44. pub order: Option<usize>,
  45. /// Optional template, if we want to specify which template to render for that page
  46. #[serde(skip_serializing)]
  47. pub template: Option<String>,
  48. /// How many pages to be displayed per paginated page. No pagination will happen if this isn't set
  49. #[serde(skip_serializing)]
  50. pub paginate_by: Option<usize>,
  51. /// Path to be used by pagination: the page number will be appended after it. Defaults to `page`.
  52. #[serde(skip_serializing)]
  53. pub paginate_path: Option<String>,
  54. /// Whether to render that page/section or not. Defaults to `true`.
  55. #[serde(skip_serializing)]
  56. pub render: Option<bool>,
  57. /// Any extra parameter present in the front matter
  58. pub extra: Option<HashMap<String, Value>>,
  59. }
  60. impl FrontMatter {
  61. pub fn parse(toml: &str) -> Result<FrontMatter> {
  62. let mut f: FrontMatter = match toml::from_str(toml) {
  63. Ok(d) => d,
  64. Err(e) => bail!(e),
  65. };
  66. if let Some(ref slug) = f.slug {
  67. if slug == "" {
  68. bail!("`slug` can't be empty if present")
  69. }
  70. }
  71. if let Some(ref url) = f.url {
  72. if url == "" {
  73. bail!("`url` can't be empty if present")
  74. }
  75. }
  76. if f.paginate_path.is_none() {
  77. f.paginate_path = Some("page".to_string());
  78. }
  79. if f.render.is_none() {
  80. f.render = Some(true);
  81. }
  82. Ok(f)
  83. }
  84. /// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime
  85. pub fn date(&self) -> Option<NaiveDateTime> {
  86. match self.date {
  87. Some(ref d) => {
  88. if d.contains('T') {
  89. DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local()))
  90. } else {
  91. NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0)))
  92. }
  93. },
  94. None => None,
  95. }
  96. }
  97. pub fn order(&self) -> usize {
  98. self.order.unwrap()
  99. }
  100. /// Returns the current sorting method, defaults to `None` (== no sorting)
  101. pub fn sort_by(&self) -> SortBy {
  102. match self.sort_by {
  103. Some(ref s) => *s,
  104. None => SortBy::None,
  105. }
  106. }
  107. /// Only applies to section, whether it is paginated or not.
  108. pub fn is_paginated(&self) -> bool {
  109. match self.paginate_by {
  110. Some(v) => v > 0,
  111. None => false
  112. }
  113. }
  114. pub fn should_render(&self) -> bool {
  115. self.render.unwrap()
  116. }
  117. }
  118. impl Default for FrontMatter {
  119. fn default() -> FrontMatter {
  120. FrontMatter {
  121. title: None,
  122. description: None,
  123. date: None,
  124. slug: None,
  125. url: None,
  126. tags: None,
  127. draft: None,
  128. category: None,
  129. sort_by: None,
  130. order: None,
  131. template: None,
  132. paginate_by: None,
  133. paginate_path: Some("page".to_string()),
  134. render: Some(true),
  135. extra: None,
  136. }
  137. }
  138. }
  139. /// Split a file between the front matter and its content
  140. /// It will parse the front matter as well and returns any error encountered
  141. pub fn split_content(file_path: &Path, content: &str) -> Result<(FrontMatter, String)> {
  142. if !PAGE_RE.is_match(content) {
  143. bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy());
  144. }
  145. // 2. extract the front matter and the content
  146. let caps = PAGE_RE.captures(content).unwrap();
  147. // caps[0] is the full match
  148. let front_matter = &caps[1];
  149. let content = &caps[2];
  150. // 3. create our page, parse front matter and assign all of that
  151. let meta = FrontMatter::parse(front_matter)
  152. .chain_err(|| format!("Error when parsing front matter of file `{}`", file_path.to_string_lossy()))?;
  153. Ok((meta, content.to_string()))
  154. }