Skip to main content

spool/vault/
frontmatter.rs

1use anyhow::Context;
2use std::collections::BTreeMap;
3
4pub fn split_frontmatter(
5    raw: &str,
6) -> anyhow::Result<(BTreeMap<String, serde_json::Value>, String)> {
7    let mut lines = raw.lines();
8    if lines.next() != Some("---") {
9        return Ok((BTreeMap::new(), raw.to_string()));
10    }
11
12    let mut yaml = Vec::new();
13    let mut body = Vec::new();
14    let mut in_frontmatter = true;
15
16    for line in raw.lines().skip(1) {
17        if in_frontmatter && line == "---" {
18            in_frontmatter = false;
19            continue;
20        }
21
22        if in_frontmatter {
23            yaml.push(line);
24        } else {
25            body.push(line);
26        }
27    }
28
29    if in_frontmatter {
30        anyhow::bail!("frontmatter started with --- but was not closed with ---");
31    }
32
33    let yaml_str = yaml.join("\n");
34    if yaml_str.trim().is_empty() {
35        return Ok((BTreeMap::new(), body.join("\n")));
36    }
37
38    let yaml_value = serde_yaml::from_str::<serde_yaml::Value>(&yaml_str)
39        .with_context(|| "failed to parse frontmatter yaml")?;
40    let parsed = match yaml_value {
41        serde_yaml::Value::Null => BTreeMap::new(),
42        other => {
43            let json_value = serde_json::to_value(other)
44                .with_context(|| "failed to convert frontmatter yaml to json")?;
45            serde_json::from_value(json_value)
46                .with_context(|| "failed to deserialize frontmatter object")?
47        }
48    };
49
50    Ok((parsed, body.join("\n")))
51}