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.

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