1pub 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
14pub 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}