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.

149 lines
4.4KB

  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"
  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. /// Any extra parameter present in the front matter
  49. pub extra: Option<HashMap<String, Value>>,
  50. }
  51. impl FrontMatter {
  52. pub fn parse(toml: &str) -> Result<FrontMatter> {
  53. if toml.trim() == "" {
  54. bail!("Front matter of file is missing");
  55. }
  56. let f: FrontMatter = match toml::from_str(toml) {
  57. Ok(d) => d,
  58. Err(e) => bail!(e),
  59. };
  60. if let Some(ref slug) = f.slug {
  61. if slug == "" {
  62. bail!("`slug` can't be empty if present")
  63. }
  64. }
  65. if let Some(ref url) = f.url {
  66. if url == "" {
  67. bail!("`url` can't be empty if present")
  68. }
  69. }
  70. Ok(f)
  71. }
  72. /// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime
  73. pub fn date(&self) -> Option<NaiveDateTime> {
  74. match self.date {
  75. Some(ref d) => {
  76. if d.contains('T') {
  77. DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local()))
  78. } else {
  79. NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0)))
  80. }
  81. },
  82. None => None,
  83. }
  84. }
  85. pub fn order(&self) -> usize {
  86. self.order.unwrap()
  87. }
  88. pub fn sort_by(&self) -> SortBy {
  89. match self.sort_by {
  90. Some(ref s) => s.clone(),
  91. None => SortBy::Date,
  92. }
  93. }
  94. }
  95. impl Default for FrontMatter {
  96. fn default() -> FrontMatter {
  97. FrontMatter {
  98. title: None,
  99. description: None,
  100. date: None,
  101. slug: None,
  102. url: None,
  103. tags: None,
  104. draft: None,
  105. category: None,
  106. sort_by: None,
  107. order: None,
  108. template: None,
  109. extra: None,
  110. }
  111. }
  112. }
  113. /// Split a file between the front matter and its content
  114. /// It will parse the front matter as well and returns any error encountered
  115. pub fn split_content(file_path: &Path, content: &str) -> Result<(FrontMatter, String)> {
  116. if !PAGE_RE.is_match(content) {
  117. bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy());
  118. }
  119. // 2. extract the front matter and the content
  120. let caps = PAGE_RE.captures(content).unwrap();
  121. // caps[0] is the full match
  122. let front_matter = &caps[1];
  123. let content = &caps[2];
  124. // 3. create our page, parse front matter and assign all of that
  125. let meta = FrontMatter::parse(front_matter)
  126. .chain_err(|| format!("Error when parsing front matter of file `{}`", file_path.to_string_lossy()))?;
  127. Ok((meta, content.to_string()))
  128. }