zola/components/front_matter/src/lib.rs
Kevin Ottens 4754cb5792 Yaml frontmatter (#990)
* Accept dates presented as strings

Still, if we find a string we involve the TOML parser to make sure the
content of said string would be a correct TOML date. In a pure TOML
world this isn't exactly relevant, but this will come in handy when
using a YAML deserializer.

Signed-off-by: Kevin Ottens <ervin@ipsquad.net>

* Add serde_yaml and test-case dependencies

This will be necessary for the YAML frontmatter handling and
corresponding unit tests.

Signed-off-by: Kevin Ottens <ervin@ipsquad.net>

* Add YAML front matter handling

Signed-off-by: Kevin Ottens <ervin@ipsquad.net>

* Switch RawFrontMatter enum to wrap &str instead of String

Signed-off-by: Kevin Ottens <ervin@ipsquad.net>

* Update the documentation to mention YAML frontmatter

This is just a light update on purpose. There would be no point in
pushing YAML too much, this is mainly here to help people with a backlog
of posts to transition.

Signed-off-by: Kevin Ottens <ervin@ipsquad.net>
2020-12-14 20:43:31 +01:00

249 lines
6.2 KiB
Rust

use lazy_static::lazy_static;
use serde_derive::{Deserialize, Serialize};
use errors::{bail, Error, Result};
use regex::Regex;
use serde_yaml;
use std::path::Path;
use toml;
mod page;
mod section;
pub use page::PageFrontMatter;
pub use section::SectionFrontMatter;
lazy_static! {
static ref TOML_RE: Regex =
Regex::new(r"^[[:space:]]*\+\+\+(\r?\n(?s).*?(?-s))\+\+\+\r?\n?((?s).*(?-s))$").unwrap();
static ref YAML_RE: Regex =
Regex::new(r"^[[:space:]]*---(\r?\n(?s).*?(?-s))---\r?\n?((?s).*(?-s))$").unwrap();
}
pub enum RawFrontMatter<'a> {
Toml(&'a str),
Yaml(&'a str),
}
impl RawFrontMatter<'_> {
fn deserialize<T>(&self) -> Result<T>
where
T: serde::de::DeserializeOwned,
{
let f: T = match self {
RawFrontMatter::Toml(s) => toml::from_str(s)?,
RawFrontMatter::Yaml(s) => match serde_yaml::from_str(s) {
Ok(d) => d,
Err(e) => bail!(format!("YAML deserialize error: {:?}", e)),
},
};
Ok(f)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SortBy {
/// Most recent to oldest
Date,
/// Lower weight comes first
Weight,
/// No sorting
None,
}
#[derive(Debug, Copy, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum InsertAnchor {
Left,
Right,
None,
}
/// Split a file between the front matter and its content
/// Will return an error if the front matter wasn't found
fn split_content<'c>(file_path: &Path, content: &'c str) -> Result<(RawFrontMatter<'c>, &'c str)> {
let (re, is_toml) = if TOML_RE.is_match(content) {
(&TOML_RE as &Regex, true)
} else if YAML_RE.is_match(content) {
(&YAML_RE as &Regex, false)
} else {
bail!(
"Couldn't find front matter in `{}`. Did you forget to add `+++` or `---`?",
file_path.to_string_lossy()
);
};
// 2. extract the front matter and the content
let caps = re.captures(content).unwrap();
// caps[0] is the full match
// caps[1] => front matter
// caps[2] => content
let front_matter = caps.get(1).unwrap().as_str();
let content = caps.get(2).unwrap().as_str();
if is_toml {
Ok((RawFrontMatter::Toml(front_matter), content))
} else {
Ok((RawFrontMatter::Yaml(front_matter), content))
}
}
/// Split a file between the front matter and its content.
/// Returns a parsed `SectionFrontMatter` and the rest of the content
pub fn split_section_content<'c>(
file_path: &Path,
content: &'c str,
) -> Result<(SectionFrontMatter, &'c str)> {
let (front_matter, content) = split_content(file_path, content)?;
let meta = SectionFrontMatter::parse(&front_matter).map_err(|e| {
Error::chain(
format!("Error when parsing front matter of section `{}`", file_path.to_string_lossy()),
e,
)
})?;
Ok((meta, content))
}
/// Split a file between the front matter and its content
/// Returns a parsed `PageFrontMatter` and the rest of the content
pub fn split_page_content<'c>(
file_path: &Path,
content: &'c str,
) -> Result<(PageFrontMatter, &'c str)> {
let (front_matter, content) = split_content(file_path, content)?;
let meta = PageFrontMatter::parse(&front_matter).map_err(|e| {
Error::chain(
format!("Error when parsing front matter of page `{}`", file_path.to_string_lossy()),
e,
)
})?;
Ok((meta, content))
}
#[cfg(test)]
mod tests {
use std::path::Path;
use test_case::test_case;
use super::{split_page_content, split_section_content};
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-12
+++
Hello
"#; "toml")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-12
---
Hello
"#; "yaml")]
fn can_split_page_content_valid(content: &str) {
let (front_matter, content) = split_page_content(Path::new(""), content).unwrap();
assert_eq!(content, "Hello\n");
assert_eq!(front_matter.title.unwrap(), "Title");
}
#[test_case(r#"
+++
paginate_by = 10
+++
Hello
"#; "toml")]
#[test_case(r#"
---
paginate_by: 10
---
Hello
"#; "yaml")]
fn can_split_section_content_valid(content: &str) {
let (front_matter, content) = split_section_content(Path::new(""), content).unwrap();
assert_eq!(content, "Hello\n");
assert!(front_matter.is_paginated());
}
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-12
+++"#; "toml")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-12
---"#; "yaml")]
fn can_split_content_with_only_frontmatter_valid(content: &str) {
let (front_matter, content) = split_page_content(Path::new(""), content).unwrap();
assert_eq!(content, "");
assert_eq!(front_matter.title.unwrap(), "Title");
}
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-02T15:00:00Z
+++
+++"#, "+++"; "toml with pluses in content")]
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-02T15:00:00Z
+++
---"#, "---"; "toml with minuses in content")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-02T15:00:00Z
---
+++"#, "+++"; "yaml with pluses in content")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-02T15:00:00Z
---
---"#, "---"; "yaml with minuses in content")]
fn can_split_content_lazily(content: &str, expected: &str) {
let (front_matter, content) = split_page_content(Path::new(""), content).unwrap();
assert_eq!(content, expected);
assert_eq!(front_matter.title.unwrap(), "Title");
}
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-12"#; "toml")]
#[test_case(r#"
+++
title = "Title"
description = "hey there"
date = 2002-10-12
---"#; "toml unmatched")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-12"#; "yaml")]
#[test_case(r#"
---
title: Title
description: hey there
date: 2002-10-12
+++"#; "yaml unmatched")]
fn errors_if_cannot_locate_frontmatter(content: &str) {
let res = split_page_content(Path::new(""), content);
assert!(res.is_err());
}
}