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.

114 lines
3.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"^\n?\+\+\+\n((?s).*(?-s))\+\+\+\n?((?s).*(?-s))$").unwrap();
  10. }
  11. /// The front matter of every page
  12. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
  13. pub struct FrontMatter {
  14. // Mandatory fields
  15. /// <title> of the page
  16. pub title: String,
  17. /// Description that appears when linked, e.g. on twitter
  18. pub description: String,
  19. // Optional stuff
  20. /// Date if we want to order pages (ie blog post)
  21. pub date: Option<String>,
  22. /// The page slug. Will be used instead of the filename if present
  23. /// Can't be an empty string if present
  24. pub slug: Option<String>,
  25. /// The url the page appears at, overrides the slug if set in the front-matter
  26. /// otherwise is set after parsing front matter and sections
  27. /// Can't be an empty string if present
  28. pub url: Option<String>,
  29. /// Tags, not to be confused with categories
  30. pub tags: Option<Vec<String>>,
  31. /// Whether this page is a draft and should be published or not
  32. pub draft: Option<bool>,
  33. /// Only one category allowed
  34. pub category: Option<String>,
  35. /// Optional template, if we want to specify which template to render for that page
  36. #[serde(skip_serializing)]
  37. pub template: Option<String>,
  38. /// Any extra parameter present in the front matter
  39. pub extra: Option<HashMap<String, Value>>,
  40. }
  41. impl FrontMatter {
  42. pub fn parse(toml: &str) -> Result<FrontMatter> {
  43. if toml.trim() == "" {
  44. bail!("Front matter of file is missing");
  45. }
  46. let f: FrontMatter = match toml::from_str(toml) {
  47. Ok(d) => d,
  48. Err(e) => bail!(e),
  49. };
  50. if let Some(ref slug) = f.slug {
  51. if slug == "" {
  52. bail!("`slug` can't be empty if present")
  53. }
  54. }
  55. if let Some(ref url) = f.url {
  56. if url == "" {
  57. bail!("`url` can't be empty if present")
  58. }
  59. }
  60. Ok(f)
  61. }
  62. /// Converts the date in the front matter, which can be in 2 formats, into a NaiveDateTime
  63. pub fn parse_date(&self) -> Option<NaiveDateTime> {
  64. match self.date {
  65. Some(ref d) => {
  66. if d.contains('T') {
  67. DateTime::parse_from_rfc3339(d).ok().and_then(|s| Some(s.naive_local()))
  68. } else {
  69. NaiveDate::parse_from_str(d, "%Y-%m-%d").ok().and_then(|s| Some(s.and_hms(0,0,0)))
  70. }
  71. },
  72. None => None,
  73. }
  74. }
  75. }
  76. /// Split a file between the front matter and its content
  77. /// It will parse the front matter as well and returns any error encountered
  78. /// TODO: add tests
  79. pub fn split_content(file_path: &Path, content: &str) -> Result<(FrontMatter, String)> {
  80. if !PAGE_RE.is_match(content) {
  81. bail!("Couldn't find front matter in `{}`. Did you forget to add `+++`?", file_path.to_string_lossy());
  82. }
  83. // 2. extract the front matter and the content
  84. let caps = PAGE_RE.captures(content).unwrap();
  85. // caps[0] is the full match
  86. let front_matter = &caps[1];
  87. let content = &caps[2];
  88. // 3. create our page, parse front matter and assign all of that
  89. let meta = FrontMatter::parse(front_matter)
  90. .chain_err(|| format!("Error when parsing front matter of file `{}`", file_path.to_string_lossy()))?;
  91. Ok((meta, content.to_string()))
  92. }