use std::collections::HashSet;
use scraps_libs::markdown::query::WikiRef;
use scraps_libs::model::{key::ScrapKey, scrap::Scrap};
use super::schema::{EdgeKind, KeyJson, LinkEdge};
pub fn link_edges(scraps: &[Scrap]) -> Vec<LinkEdge> {
let keys: HashSet<ScrapKey> = scraps.iter().map(Scrap::self_key).collect();
let keys = &keys;
scraps
.iter()
.flat_map(|scrap| {
let from = KeyJson::from(&scrap.self_key());
let sources = scrap.source_links().into_iter().filter_map({
let from = from.clone();
move |link| {
let to = link.target?;
Some(LinkEdge {
from: from.clone(),
resolved: keys.contains(&to),
to: KeyJson::from(&to),
kind: EdgeKind::Source,
heading: link.heading,
line: line_of(scrap.md_text(), &link.resource),
})
}
});
let refs = scrap.refs().iter().filter_map(move |r| {
let (to, kind, heading, line) = match r {
WikiRef::Link(l) => {
(ScrapKey::from(l), EdgeKind::Link, l.heading.clone(), l.line)
}
WikiRef::Embed(e) => (
ScrapKey::from_path_str(&embed_path(&e.ctx_path, &e.title)),
EdgeKind::Embed,
e.heading.clone(),
e.line,
),
WikiRef::MdLink(m) => {
(m.resolve(scrap.ctx())?, EdgeKind::Link, m.heading(), m.line)
}
WikiRef::Tag(_) => return None,
};
Some(LinkEdge {
from: from.clone(),
resolved: keys.contains(&to),
to: KeyJson::from(&to),
kind,
heading,
line,
})
});
refs.chain(sources)
})
.collect()
}
fn line_of(text: &str, resource: &str) -> usize {
text.lines()
.position(|line| line.contains(resource))
.map_or(1, |index| index + 1)
}
fn embed_path(ctx_path: &[String], title: &str) -> String {
if ctx_path.is_empty() {
title.to_string()
} else {
format!("{}/{}", ctx_path.join("/"), title)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_resolves_links_and_embeds_and_skips_tags() {
let scraps = vec![
Scrap::new(
"a",
&None,
"line one\n[[b#Sec]] #[[tag]]\n![[Ctx/c]] [[missing]]",
),
Scrap::new("b", &None, "## Sec"),
Scrap::new("c", &Some("Ctx".into()), ""),
];
let edges = link_edges(&scraps);
assert_eq!(edges.len(), 3);
assert_eq!(edges[0].to.stem, "b");
assert_eq!(edges[0].kind, EdgeKind::Link);
assert_eq!(edges[0].heading.as_deref(), Some("Sec"));
assert_eq!(edges[0].line, 2);
assert!(edges[0].resolved);
assert_eq!(edges[1].kind, EdgeKind::Embed);
assert_eq!(edges[1].to.ctx.as_deref(), Some("Ctx"));
assert!(edges[1].resolved);
assert_eq!(edges[2].to.stem, "missing");
assert!(!edges[2].resolved);
assert!(edges.iter().all(|e| e.from.stem == "a"));
}
#[test]
fn it_adds_an_edge_for_each_source_naming_a_scrap() {
let scraps = vec![
Scrap::new(
"okf",
&None,
"---\nsources:\n - resource: https://example.com\n - resource: /Blog/Intro.md\n - resource: /Gone.md\n---\n\nbody\n",
),
Scrap::new("Intro", &Some("Blog".into()), ""),
];
let edges = link_edges(&scraps);
assert_eq!(edges.len(), 2);
assert!(edges.iter().all(|e| e.kind == EdgeKind::Source));
assert_eq!(edges[0].to.stem, "Intro");
assert_eq!(edges[0].line, 4);
assert!(edges[0].resolved);
assert_eq!(edges[1].to.stem, "Gone");
assert!(!edges[1].resolved);
}
#[test]
fn it_resolves_markdown_links_from_the_linking_scrap() {
let scraps = vec [r](../metrics/revenue.md#definition)\n[g](./gone.md) [out](../../out.md)",
),
Scrap::new("customers", &Some("tables".into()), ""),
Scrap::new("revenue", &Some("metrics".into()), "# Definition"),
];
let edges = link_edges(&scraps);
assert_eq!(edges.len(), 3);
assert!(edges.iter().all(|e| e.kind == EdgeKind::Link));
assert_eq!(edges[0].to.stem, "customers");
assert_eq!(edges[0].to.ctx.as_deref(), Some("tables"));
assert!(edges[0].resolved);
assert_eq!(edges[1].to.ctx.as_deref(), Some("metrics"));
assert_eq!(edges[1].heading.as_deref(), Some("definition"));
assert!(edges[1].resolved);
assert_eq!(edges[2].to.stem, "gone");
assert_eq!(edges[2].line, 2);
assert!(!edges[2].resolved);
}
#[test]
fn it_keeps_duplicate_occurrences() {
let scraps = vec![Scrap::new("a", &None, "[[a]] [[a]]")];
assert_eq!(link_edges(&scraps).len(), 2);
}
}