use super::resolver::Resolver;
use super::structure::DerivedIndex;
use super::{count_nodes, doc_path, emit_groups, EdgeGroups};
use crate::datatypes::values::{DataFrame, Value};
use crate::graph::mutation::maintain;
use crate::graph::DirGraph;
use crate::okf::model::{BuildOptions, BuildReport, ConceptDoc, Link, DEFAULT_LABEL, SOURCE_LABEL};
use std::collections::BTreeSet;
pub(super) fn build_edges(
graph: &mut DirGraph,
docs: &[ConceptDoc],
opts: &BuildOptions,
mut groups: EdgeGroups,
derived: &DerivedIndex,
report: &mut BuildReport,
) -> Result<(), String> {
let (resolver, alias_warnings) = Resolver::new(docs, &opts.profile);
report.warnings.extend(alias_warnings);
let mut dangling: BTreeSet<String> = BTreeSet::new();
for d in docs {
for link in &d.links {
let (target_label, target_id) = if link.is_external {
(SOURCE_LABEL.to_string(), link.target.clone())
} else {
let (id, label) = resolver.resolve(link, crate::okf::parent_dir(doc_path(d)));
if !resolver.id_to_label.contains_key(id.as_str()) {
dangling.insert(id.clone());
(label, id)
} else {
retarget(link, id, label, derived, d, report)
}
};
let (src_label, src_id, tgt_label, tgt_id) = if link.reverse {
(
target_label,
target_id,
d.label.clone(),
d.concept_id.clone(),
)
} else {
(
d.label.clone(),
d.concept_id.clone(),
target_label,
target_id,
)
};
groups
.entry((link.conn_type.clone(), src_label, tgt_label))
.or_default()
.push((src_id, tgt_id, link.props.clone()));
}
}
report.dangling = dangling.len();
count_nodes(report, DEFAULT_LABEL, dangling.len());
for id in &dangling {
report.warnings.push(format!("dangling link: `{id}`"));
}
if !dangling.is_empty() {
let rows: Vec<Vec<Value>> = dangling
.iter()
.map(|id| vec![Value::String(id.clone()), Value::Boolean(true)])
.collect();
let df = DataFrame::from_cypher_rows(
vec!["concept_id".to_string(), "_provisional".to_string()],
rows,
)?;
maintain::add_nodes(
graph,
df,
DEFAULT_LABEL.to_string(),
"concept_id".to_string(),
None,
Some("preserve".to_string()),
)?;
}
emit_groups(graph, groups, &opts.profile.edge_defaults, report)
}
fn anchor_of(link: &Link) -> Option<&str> {
link.props
.iter()
.find(|(k, _)| k == "anchor")
.and_then(|(_, v)| match v {
Value::String(anchor) if !anchor.is_empty() => Some(anchor.as_str()),
_ => None,
})
}
fn retarget(
link: &Link,
id: String,
label: String,
derived: &DerivedIndex,
d: &ConceptDoc,
report: &mut BuildReport,
) -> (String, String) {
let Some(anchor) = anchor_of(link) else {
return (label, id);
};
if let Some((derived_id, derived_label)) = derived.retarget(&id, anchor) {
return (derived_label, derived_id);
}
if derived.sections_declared() {
report.warnings.push(format!(
"{}: `#{anchor}` names no heading or block id in `{id}`; the link resolved \
to the note",
d.file_path
));
}
(label, id)
}
#[cfg(test)]
#[path = "edges_tests.rs"]
mod edges_tests;