extract_frontmatter/
lib.rs

1#![warn(clippy::cargo, clippy::nursery, clippy::pedantic)]
2
3//! A Rust library that allows a user to extract an arbitrary number of lines of "front-matter" from the start of any
4//! multiline string.
5//!
6//! Note that absolutely no parsing of extracted front-matter is performed; this is designed to output its results for
7//! another library to then parse.
8//!
9//! See [`Extractor`] for how to use this library.
10pub mod config;
11
12mod extraction;
13mod modification;
14
15use crate::config::{Modifier, Splitter};
16use crate::extraction::{
17    extract_by_delimiter_line, extract_by_enclosing_lines, extract_by_line_index, extract_by_line_prefix,
18};
19use crate::modification::{strip_first_line, strip_last_line, strip_prefix, trim_whitespace};
20use std::borrow::Cow;
21
22/// Holds configuration for how front-matter and data should be extracted from a string.
23///
24/// 1. Instantiate using [`Extractor::new()`] with the desired [`Splitter`]
25/// 2. (Optional) Call [`.with_modifier()`] with a desired [`Modifier`]; repeat as necessary
26/// 3. Call [`.extract()`] with a string to get back a tuple of front-matter and data
27///
28/// [`.with_modifier()`]: Self::with_modifier
29/// [`.extract()`]: Self::extract
30///
31/// # Usage
32///
33/// ## Example 1 (Markdown with TOML)
34///
35/// Input:
36///
37/// ```md
38/// [meta]
39/// field_one = 10
40/// field_two = [2, 4]
41/// +++
42///
43/// ## Markdown example
44///
45/// This is an example markdown document that contains the following TOML front-matter:
46///
47///     [meta]
48///     field_one = 10
49///     field_two = [2, 4]
50/// ```
51///
52/// Code:
53///
54/// ```rust
55/// # use extract_frontmatter::config::Splitter;
56/// # use extract_frontmatter::Extractor;
57/// # let input = include_str!("../resources/tests/example1/raw.md");
58/// let (front_matter, data) = Extractor::new(Splitter::DelimiterLine("+++")).extract(input);
59/// # assert_eq!(front_matter.trim(), include_str!("../resources/tests/example1/meta.toml").trim());
60/// # assert_eq!(data.trim(), include_str!("../resources/tests/example1/data.md").trim());
61/// ```
62///
63/// Front-matter output:
64///
65/// ```toml
66/// [meta]
67/// field_one = 10
68/// field_two = [2, 4]
69/// ```
70///
71/// Data output:
72///
73/// ```md
74/// ## Markdown example
75///
76/// This is an example markdown document that contains the following TOML front-matter:
77///
78///     [meta]
79///     field_one = 10
80///     field_two = [2, 4]
81/// ```
82///
83/// ## Example 2 (SQL with YAML)
84///
85/// Input:
86///
87/// ```sql
88/// -- meta:
89/// --   field_one: 10
90/// --   field_two:
91/// --     - 2
92/// --     - 4
93/// SELECT version();
94/// ```
95///
96/// Code:
97///
98/// ```rust
99/// # use extract_frontmatter::config::{Modifier, Splitter};
100/// # use extract_frontmatter::Extractor;
101/// # let input = include_str!("../resources/tests/example2/raw.sql");
102/// let (front_matter, data) = Extractor::new(Splitter::LinePrefix("-- "))
103///     .with_modifier(Modifier::StripPrefix("-- "))
104///     .extract(input);
105/// # assert_eq!(front_matter.trim(), include_str!("../resources/tests/example2/meta.yml").trim());
106/// # assert_eq!(data.trim(), include_str!("../resources/tests/example2/data.sql").trim());
107/// ```
108///
109/// Front-matter output:
110///
111/// ```yaml
112/// meta:
113///   field_one: 10
114///   field_two:
115///     - 2
116///     - 4
117/// ```
118///
119/// Data output:
120///
121/// ```sql
122/// SELECT version();
123/// ```
124#[derive(Debug)]
125pub struct Extractor<'opt> {
126    splitter: Splitter<'opt>,
127    modifiers: Vec<Modifier<'opt>>,
128}
129
130impl<'opt> Extractor<'opt> {
131    /// Instantiate a new [`Extractor`] config.
132    #[must_use]
133    pub const fn new(splitter: Splitter<'opt>) -> Self {
134        Self { splitter, modifiers: Vec::new() }
135    }
136
137    /// Add a modifier to the front-matter returned by [`.extract()`](Self::extract).
138    pub fn with_modifier(&mut self, modifier: Modifier<'opt>) -> &mut Self {
139        self.modifiers.push(modifier);
140
141        self
142    }
143
144    /// Split the given input string into a tuple of front-matter and data, applying any modifiers specified with
145    /// [`.with_modifier()`].
146    ///
147    /// [`.with_modifier()`]:Self::with_modifier
148    #[must_use]
149    pub fn extract<'input>(&self, input: &'input str) -> (Cow<'input, str>, &'input str) {
150        let (meta, data) = match self.splitter {
151            Splitter::LineIndex(index) => extract_by_line_index(input, index),
152            Splitter::DelimiterLine(delim) => extract_by_delimiter_line(input, delim),
153            Splitter::LinePrefix(prefix) => extract_by_line_prefix(input, prefix),
154            Splitter::EnclosingLines(delim) => extract_by_enclosing_lines(input, delim),
155        };
156
157        let mut meta = Cow::from(meta);
158
159        for modifier in &self.modifiers {
160            meta = Cow::from(match modifier {
161                Modifier::StripPrefix(prefix) => strip_prefix(&meta, prefix),
162                Modifier::TrimWhitespace => trim_whitespace(&meta),
163                Modifier::StripFirstLine => strip_first_line(&meta),
164                Modifier::StripLastLine => strip_last_line(&meta),
165            });
166        }
167
168        (meta, data)
169    }
170}