Skip to main content

drft/builders/
frontmatter.rs

1//! The `frontmatter` builder: emits link edges from frontmatter link-target
2//! values, plus the parsed frontmatter block as metadata on the file it read.
3//! It is colocated — its metadata is about its own file — so its drift rides the
4//! file's `fs` hash; it contributes no hash of its own.
5
6use globset::GlobSet;
7
8use crate::builders::link_edge;
9use crate::model::{Graph, Node};
10use crate::parsers::Parser;
11use crate::parsers::frontmatter::FrontmatterParser;
12
13/// Build the `frontmatter` graph fragment from text files, labeled `label`.
14/// `filter` scopes which paths the builder reads (`None` reads all). The
15/// fragment carries edges plus a node per file whose frontmatter parses to an
16/// object.
17pub fn build(label: &str, texts: &[(String, String)], filter: Option<GlobSet>) -> Graph {
18    let parser = FrontmatterParser {
19        file_filter: filter,
20    };
21    let mut graph = Graph::labeled(label);
22
23    for (path, content) in texts {
24        if !parser.matches(path) {
25            continue;
26        }
27        let result = parser.parse(path, content);
28
29        if let Some(serde_json::Value::Object(block)) = result.metadata {
30            graph.set_node(path.clone(), Node::new(block));
31        }
32
33        for raw in result.links {
34            if let Some(edge) = link_edge(path, &raw) {
35                graph.add_edge(edge);
36            }
37        }
38    }
39
40    graph.sort_edges();
41    graph
42}
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47
48    fn texts(pairs: &[(&str, &str)]) -> Vec<(String, String)> {
49        pairs
50            .iter()
51            .map(|(p, c)| (p.to_string(), c.to_string()))
52            .collect()
53    }
54
55    #[test]
56    fn emits_metadata_and_edges() {
57        let t = texts(&[(
58            "analysis.md",
59            "---\ntitle: Analysis\nstatus: draft\nsources:\n  - ./data/notes.md\n---\n\n# Body\n",
60        )]);
61        let graph = build("frontmatter", &t, None);
62        assert_eq!(graph.label.as_deref(), Some("frontmatter"));
63
64        let meta = &graph.nodes["analysis.md"].metadata;
65        assert_eq!(meta["title"], serde_json::json!("Analysis"));
66        assert_eq!(meta["status"], serde_json::json!("draft"));
67
68        let targets: Vec<&str> = graph.edges.iter().map(|e| e.target.as_str()).collect();
69        assert_eq!(targets, vec!["data/notes.md"]);
70    }
71
72    #[test]
73    fn no_node_without_frontmatter() {
74        let t = texts(&[("plain.md", "# Just a heading\n")]);
75        let graph = build("frontmatter", &t, None);
76        assert!(graph.nodes.is_empty());
77        assert!(graph.edges.is_empty());
78    }
79}