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.

171 lines
5.2KB

  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, 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. /// Any extra parameter present in the front matter
  55. pub extra: Option<HashMap<String, Value>>,
  56. }
  57. impl FrontMatter {
  58. pub fn parse(toml: &str) -> Result<FrontMatter> {
  59. if toml.trim() == "" {
  60. bail!("Front matter of file is missing");
  61. }
  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. Ok(f)
  80. }
  81. /// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime
  82. pub fn date(&self) -> Option<NaiveDateTime> {
  83. match self.date {
  84. Some(ref d) => {
  85. if d.contains('T') {
  86. DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local()))
  87. } else {
  88. NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0)))
  89. }
  90. },
  91. None => None,
  92. }
  93. }
  94. pub fn order(&self) -> usize {
  95. self.order.unwrap()
  96. }
  97. pub fn sort_by(&self) -> SortBy {
  98. match self.sort_by {
  99. Some(ref s) => s.clone(),
  100. None => SortBy::Date,
  101. }
  102. }
  103. /// Only applies to section, whether it is paginated or not.
  104. pub fn is_paginated(&self) -> bool {
  105. match self.paginate_by {
  106. Some(v) => v > 0,
  107. None => false
  108. }
  109. }
  110. }
  111. impl Default for FrontMatter {
  112. fn default() -> FrontMatter {
  113. FrontMatter {
  114. title: None,
  115. description: None,
  116. date: None,
  117. slug: None,
  118. url: None,
  119. tags: None,
  120. draft: None,
  121. category: None,
  122. sort_by: None,
  123. order: None,
  124. template: None,
  125. paginate_by: None,
  126. paginate_path: None,
  127. extra: None,
  128. }
  129. }
  130. }
  131. /// Split a file between the front matter and its content
  132. /// It will parse the front matter as well and returns any error encountered
  133. pub fn split_content(file_path: &Path, content: &str) -> Result<(FrontMatter, String)> {
  134. if !PAGE_RE.is_match(content) {
  135. bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy());
  136. }
  137. // 2. extract the front matter and the content
  138. let caps = PAGE_RE.captures(content).unwrap();
  139. // caps[0] is the full match
  140. let front_matter = &caps[1];
  141. let content = &caps[2];
  142. // 3. create our page, parse front matter and assign all of that
  143. let meta = FrontMatter::parse(front_matter)
  144. .chain_err(|| format!("Error when parsing front matter of file `{}`", file_path.to_string_lossy()))?;
  145. Ok((meta, content.to_string()))
  146. }