Skip to main content

drft/builders/
mod.rs

1//! Builders turn a source's `(path, bytes)` records into graph nodes and edges.
2//! v0.8 ships the `fs` node builder plus two text builders (parsers):
3//! `markdown` (edges) and `frontmatter` (edges + metadata).
4
5pub mod frontmatter;
6pub mod fs;
7pub mod markdown;
8
9use serde_json::Value;
10
11use crate::model::{Edge, Metadata};
12use crate::util::{is_uri, resolve_link};
13
14/// Turn a raw link string discovered by a text builder into an edge from
15/// `source` to its resolved target.
16///
17/// Returns `None` for links with no file target (empty or anchor-only). A
18/// fragment (`#heading`) is stripped from the target — which is the node
19/// identity — and preserved as the edge's `link` metadata. Non-URI targets are
20/// resolved relative to `source`; URIs pass through unchanged.
21pub fn link_edge(source: &str, raw: &str) -> Option<Edge> {
22    let trimmed = raw.trim();
23    if trimmed.is_empty() || trimmed.starts_with('#') {
24        return None;
25    }
26
27    let (base, fragment) = match trimmed.find('#') {
28        Some(i) => (&trimmed[..i], Some(&trimmed[i..])),
29        None => (trimmed, None),
30    };
31    if base.is_empty() {
32        return None;
33    }
34
35    let target = if is_uri(base) {
36        base.to_string()
37    } else {
38        resolve_link(source, base)
39    };
40
41    let mut metadata = Metadata::new();
42    if let Some(frag) = fragment {
43        metadata.insert("link".into(), Value::String(format!("{target}{frag}")));
44    }
45
46    Some(Edge::with_metadata(source, target, metadata))
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn resolves_relative_target() {
55        let edge = link_edge("docs/guide.md", "setup.md").unwrap();
56        assert_eq!(edge.source, "docs/guide.md");
57        assert_eq!(edge.target, "docs/setup.md");
58        assert!(edge.metadata.is_empty());
59    }
60
61    #[test]
62    fn strips_fragment_into_link_metadata() {
63        let edge = link_edge("a.md", "b.md#heading").unwrap();
64        assert_eq!(edge.target, "b.md");
65        assert_eq!(edge.metadata["link"], Value::String("b.md#heading".into()));
66    }
67
68    #[test]
69    fn passes_through_uris() {
70        let edge = link_edge("a.md", "https://example.com/x").unwrap();
71        assert_eq!(edge.target, "https://example.com/x");
72    }
73
74    #[test]
75    fn drops_anchor_only_and_empty() {
76        assert!(link_edge("a.md", "#section").is_none());
77        assert!(link_edge("a.md", "   ").is_none());
78    }
79}