Skip to main content

specfold_core/graph/
build.rs

1//! Builds the graph: indexes ids, paths and aliases, then resolves relations and links into edges.
2
3use super::types::{DOC_RELATIONS, Edge, Graph, MENTIONS, Resolution};
4use crate::corpus::Corpus;
5use crate::model::RefText;
6
7impl Graph {
8    pub fn build(c: &Corpus) -> Self {
9        let mut g = Graph {
10            anchors: vec![Default::default(); c.docs.len()],
11            outgoing: vec![vec![]; c.docs.len()],
12            incoming: vec![vec![]; c.docs.len()],
13            ..Default::default()
14        };
15        for (i, d) in c.docs.iter().enumerate() {
16            g.by_id.insert(d.id.to_string(), i);
17            g.by_path.insert(d.path.clone(), i);
18            if let Some(dir) = &d.dir {
19                g.by_path.insert(dir.clone(), i);
20            }
21            for a in d.list_field("aliases") {
22                g.by_alias.entry(a).or_default().push(i);
23            }
24            g.anchors[i] = d.anchors.iter().map(|a| a.item.clone()).collect();
25        }
26        for (old, news) in &c.aliases.map {
27            for n in news {
28                if let Some(&i) = g.by_id.get(n) {
29                    let list = g.by_alias.entry(old.clone()).or_default();
30                    if !list.contains(&i) {
31                        list.push(i);
32                    }
33                }
34            }
35        }
36        for (i, d) in c.docs.iter().enumerate() {
37            for (kind, targets) in d.rel() {
38                if !DOC_RELATIONS.contains(&kind.as_str()) {
39                    continue;
40                }
41                for t in targets {
42                    g.link(i, &t, &kind);
43                }
44            }
45            for l in &d.links {
46                g.link(i, &l.item, MENTIONS);
47            }
48        }
49        g
50    }
51
52    fn link(&mut self, from: usize, text: &str, kind: &str) {
53        if let Resolution::Doc { idx, anchor, .. } = self.resolve(text) {
54            if idx == from {
55                return;
56            }
57            let e = self.edges.len();
58            self.edges.push(Edge { from, to: idx, kind: kind.to_string(), anchor });
59            self.outgoing[from].push(e);
60            self.incoming[idx].push(e);
61        }
62    }
63
64    /// Resolves an id, an alias, or a path to a document, checking the anchor if there is one.
65    pub fn resolve(&self, text: &str) -> Resolution {
66        let r = RefText::parse(text);
67        // A path written relative to the citing file (`../../.spec/…`) names the same document;
68        // ids never start with a dot, so stripping these prefixes cannot change an id.
69        let mut target = r.target.trim_end_matches('/');
70        while let Some(rest) = target.strip_prefix("../").or_else(|| target.strip_prefix("./")) {
71            target = rest;
72        }
73        let (candidates, via_alias) = if let Some(&i) = self.by_id.get(target).or_else(|| self.by_path.get(target)) {
74            (vec![i], false)
75        } else if let Some(list) = self.by_alias.get(target) {
76            (list.clone(), true)
77        } else {
78            (vec![], false)
79        };
80        match candidates.as_slice() {
81            [] => Resolution::Missing,
82            [idx] => match r.anchor {
83                Some(a) if !self.anchors[*idx].contains(&a) => Resolution::MissingAnchor { idx: *idx, anchor: a },
84                anchor => Resolution::Doc { idx: *idx, anchor, via_alias },
85            },
86            many => Resolution::Ambiguous(many.to_vec()),
87        }
88    }
89
90    pub fn edges_from(&self, idx: usize) -> impl Iterator<Item = &Edge> {
91        self.outgoing[idx].iter().map(|&e| &self.edges[e])
92    }
93
94    pub fn edges_to(&self, idx: usize) -> impl Iterator<Item = &Edge> {
95        self.incoming[idx].iter().map(|&e| &self.edges[e])
96    }
97}