yaml_front_matter/
lib.rs

1//! # YAML Front Matter (YFM) Parser
2//!
3//! **yaml-front-matter** parses a valid YAML string into a `struct` which
4//! implements the `DeserializeOwned` trait from serde.
5//!
6//! Consider the following YAML content on the top of your markdown file:
7//!
8//! ```ignore
9//! ---
10//! title: 'Parsing a Markdown file metadata into a struct'
11//! description: 'This tutorial walks you through the practice of parsing markdown files for metadata'
12//! tags: ['markdown', 'rust', 'files', 'parsing', 'metadata']
13//! similar_posts:
14//!   - 'Rendering markdown'
15//!   - 'Using Rust to render markdown'
16//! date: '2021-09-13T03:48:00'
17//! favorite_numbers:
18//!     - 3.14
19//!     - 1970
20//!     - 12345
21//! ---
22//! ```
23//!
24//! This crate takes care of extracting this header from your markdown file and
25//! parse extracted data using `serde` and `serde_yaml`.
26//!
27//! ## Example
28//!
29//! ```rust
30//! use serde::Deserialize;
31//! use yaml_front_matter::{Document, YamlFrontMatter};
32//!
33//! const SIMPLE_MARKDOWN_YFM: &str = r#"
34//! ---
35//! title: 'Parsing a Markdown file metadata into a struct'
36//! description: 'This tutorial walks you through the practice of parsing markdown files for metadata'
37//! tags: ['markdown', 'rust', 'files', 'parsing', 'metadata']
38//! similar_posts:
39//!   - 'Rendering markdown'
40//!   - 'Using Rust to render markdown'
41//! date: '2021-09-13T03:48:00'
42//! favorite_numbers:
43//!     - 3.14
44//!     - 1970
45//!     - 12345
46//! ---
47//!
48//!
49//! # Parsing a **Markdown** file metadata into a `struct`
50//!
51//! > This tutorial walks you through the practice of parsing markdown files for metadata
52//! "#;
53//!
54//! #[derive(Deserialize)]
55//! struct Metadata {
56//!     title: String,
57//!     description: String,
58//!     tags: Vec<String>,
59//!     similar_posts: Vec<String>,
60//!     date: String,
61//!     favorite_numbers: Vec<f64>,
62//! }
63//!
64//! let document: Document<Metadata> = YamlFrontMatter::parse::<Metadata>(&SIMPLE_MARKDOWN_YFM).unwrap();
65//!
66//! let Metadata {
67//!     title,
68//!     description,
69//!     tags,
70//!     similar_posts,
71//!     date,
72//!     favorite_numbers,
73//! } = document.metadata;
74//!
75//! assert_eq!(title, "Parsing a Markdown file metadata into a struct");
76//! assert_eq!(
77//!     description,
78//!     "This tutorial walks you through the practice of parsing markdown files for metadata"
79//! );
80//! assert_eq!(
81//!     tags,
82//!     vec!["markdown", "rust", "files", "parsing", "metadata"]
83//! );
84//! assert_eq!(
85//!     similar_posts,
86//!     vec!["Rendering markdown", "Using Rust to render markdown"]
87//! );
88//! assert_eq!(date, "2021-09-13T03:48:00");
89//! assert_eq!(favorite_numbers, vec![3.14, 1970., 12345.]);
90//! ```
91//!
92use serde::de::DeserializeOwned;
93
94/// A `Document` represents the Markdown file provided as input to
95/// `YamlFrontMatter::parse` associated function.
96///
97/// The document holds two relevant fields:
98///
99/// - `metadata`: A generic type with the structure of the Markdown's
100/// front matter header.
101///
102/// - `content`: The body of the Markdown without the front matter header
103pub struct Document<T: DeserializeOwned> {
104    /// A generic type with the structure of the Markdown's
105    /// front matter header.
106    pub metadata: T,
107    /// The body of the Markdown without the front matter header
108    pub content: String,
109}
110
111/// YAML Front Matter (YFM) is an optional section of valid YAML that is
112/// placed at the top of a page and is used for maintaining metadata for the
113/// page and its contents.
114pub struct YamlFrontMatter;
115
116impl YamlFrontMatter {
117    pub fn parse<T: DeserializeOwned>(
118        markdown: &str,
119    ) -> Result<Document<T>, Box<dyn std::error::Error>> {
120        let yaml = YamlFrontMatter::extract(markdown)?;
121        let metadata = serde_yaml::from_str::<T>(yaml.0.as_str())?;
122
123        Ok(Document {
124            metadata,
125            content: yaml.1,
126        })
127    }
128
129    fn extract(markdown: &str) -> Result<(String, String), Box<dyn std::error::Error>> {
130        let mut front_matter = String::default();
131        let mut sentinel = false;
132        let mut front_matter_lines = 0;
133        let lines = markdown.lines();
134
135        for line in lines.clone() {
136            front_matter_lines += 1;
137
138            if line.trim() == "---" {
139                if sentinel {
140                    break;
141                }
142
143                sentinel = true;
144                continue;
145            }
146
147            if sentinel {
148                front_matter.push_str(line);
149                front_matter.push('\n');
150            }
151        }
152
153        Ok((
154            front_matter,
155            lines
156                .skip(front_matter_lines)
157                .collect::<Vec<&str>>()
158                .join("\n"),
159        ))
160    }
161}
162
163#[cfg(test)]
164mod test {
165    use serde::{Deserialize, __private::doc};
166
167    const MARKDOWN: &'static str = r#"
168---
169title: "Installing The Rust Programming Language on Windows"
170description: "A tutorial on installing the Rust Programming Language on Windows."
171categories: [rust, tutorial, windows, install]
172date: 2021-09-13T03:48:00
173---
174
175# Installing The Rust Programming Language on Windows
176
177## Motivation
178
179In the past days I´ve been using Unix based systems to do my software
180development work, macOS and Ubuntu are both my main operative systems nowadays.
181
182But Windows is getting closer as well, as I get more involved into systems
183programming, I'm also getting into writing Rust crates which must be supported in
184different platforms, such as macOS, Linux and Windows.
185
186Currently I'm working on a crate called [local-ip-address](https://github.com/EstebanBorai/local-ip-address).
187
188The main goal of this crate is to list system's network interfaces along
189with related data such as interface name, interface family (AFINET or AFINET6 for instance),
190IP address, subnet mask and any other relevant properties.
191
192Given that every system has a particular way to gather network interfaces
193details, I decided to install Windows in my PC as a dual-boot option along with Ubuntu.
194
195This will give me first-class access to the popular Win32 API, which I'm using through [windows-rs](https://github.com/microsoft/windows-rs) crate.
196
197After having Windows up and running, I'm also installing Rust on Windows and I'm documenting
198it for future references.
199"#;
200
201    const FRONT_MATTER: &'static str = r#"title: "Installing The Rust Programming Language on Windows"
202description: "A tutorial on installing the Rust Programming Language on Windows."
203categories: [rust, tutorial, windows, install]
204date: 2021-09-13T03:48:00
205"#;
206
207    const CONTENT: &'static str = r#"
208# Installing The Rust Programming Language on Windows
209
210## Motivation
211
212In the past days I´ve been using Unix based systems to do my software
213development work, macOS and Ubuntu are both my main operative systems nowadays.
214
215But Windows is getting closer as well, as I get more involved into systems
216programming, I'm also getting into writing Rust crates which must be supported in
217different platforms, such as macOS, Linux and Windows.
218
219Currently I'm working on a crate called [local-ip-address](https://github.com/EstebanBorai/local-ip-address).
220
221The main goal of this crate is to list system's network interfaces along
222with related data such as interface name, interface family (AFINET or AFINET6 for instance),
223IP address, subnet mask and any other relevant properties.
224
225Given that every system has a particular way to gather network interfaces
226details, I decided to install Windows in my PC as a dual-boot option along with Ubuntu.
227
228This will give me first-class access to the popular Win32 API, which I'm using through [windows-rs](https://github.com/microsoft/windows-rs) crate.
229
230After having Windows up and running, I'm also installing Rust on Windows and I'm documenting
231it for future references."#;
232
233    #[derive(Deserialize)]
234    struct Metadata {
235        title: String,
236        description: String,
237        categories: Vec<String>,
238        date: String,
239    }
240
241    #[test]
242    fn retrieve_markdown_front_matter() {
243        let (front_matter, _) = super::YamlFrontMatter::extract(MARKDOWN).unwrap();
244
245        assert_eq!(front_matter, FRONT_MATTER);
246    }
247
248    #[test]
249    fn retrieve_markdown_content() {
250        let (_, content) = super::YamlFrontMatter::extract(MARKDOWN).unwrap();
251
252        assert_eq!(content, CONTENT);
253    }
254
255    #[test]
256    fn parses_markdown_into_document() {
257        let document = super::YamlFrontMatter::parse::<Metadata>(MARKDOWN).unwrap();
258        let metadata = document.metadata;
259
260        assert_eq!(
261            metadata.title,
262            "Installing The Rust Programming Language on Windows"
263        );
264        assert_eq!(
265            metadata.description,
266            "A tutorial on installing the Rust Programming Language on Windows."
267        );
268        assert_eq!(
269            metadata.categories,
270            vec!["rust", "tutorial", "windows", "install"]
271        );
272        assert_eq!(metadata.date, "2021-09-13T03:48:00");
273    }
274}