Skip to main content

blazegraph_io_core/graphs/
graph.rs

1use super::analytics::GraphAnalytics;
2use crate::types::*;
3use anyhow::Result;
4use std::collections::HashMap;
5
6impl Default for DocumentGraph {
7    fn default() -> Self {
8        Self::new()
9    }
10}
11
12impl DocumentGraph {
13    /// Create a new graph with a deterministic root ID.
14    pub fn new_with_root(root_id: NodeId) -> Self {
15        use crate::types::{DocumentInfo, DocumentMetadata};
16
17        let document_info = DocumentInfo {
18            root_id,
19            document_metadata: DocumentMetadata::default(),
20            bookmark_data: None,
21        };
22
23        Self {
24            nodes: HashMap::new(),
25            document_info,
26            structural_profile: StructuralProfile::default(),
27        }
28    }
29
30    /// Create a new graph with a random UUIDv4 root ID (legacy).
31    pub fn new() -> Self {
32        use crate::types::{DocumentInfo, DocumentMetadata};
33        use uuid::Uuid;
34
35        let document_info = DocumentInfo {
36            root_id: Uuid::new_v4(),
37            document_metadata: DocumentMetadata::default(),
38            bookmark_data: None,
39        };
40
41        Self {
42            nodes: HashMap::new(),
43            document_info,
44            structural_profile: StructuralProfile::default(),
45        }
46    }
47
48    pub fn max_depth(&self) -> u32 {
49        self.nodes
50            .values()
51            .map(|n| n.location.semantic.depth)
52            .max()
53            .unwrap_or(0)
54    }
55
56    pub fn save_to_json(&self, path: &str) -> Result<()> {
57        let sorted_graph = self.to_sorted_graph();
58        let json = serde_json::to_string_pretty(&sorted_graph)?;
59        std::fs::write(path, json)?;
60        Ok(())
61    }
62
63    pub fn to_sorted_graph(&self) -> SortedDocumentGraph {
64        // Collect all nodes and sort by text_order, with root node first
65        let mut nodes: Vec<&DocumentNode> = self.nodes.values().collect();
66        nodes.sort_by(|a, b| {
67            // Document root (with text_order = None) should come first
68            match (a.text_order, b.text_order) {
69                (None, None) => std::cmp::Ordering::Equal,
70                (None, Some(_)) => std::cmp::Ordering::Less,
71                (Some(_), None) => std::cmp::Ordering::Greater,
72                (Some(a_order), Some(b_order)) => a_order.cmp(&b_order),
73            }
74        });
75
76        SortedDocumentGraph {
77            schema_version: SCHEMA_VERSION.to_string(),
78            nodes: nodes.into_iter().cloned().collect(),
79            document_info: self.document_info.clone(),
80            structural_profile: self.structural_profile.clone(),
81        }
82    }
83
84    /// Compute breadcrumbs for all nodes by walking the tree top-down.
85    /// Sections contribute their text to the trail. Non-section nodes inherit
86    /// their parent's breadcrumbs without adding to them.
87    /// If document metadata has a title, it becomes the first breadcrumb.
88    pub fn compute_breadcrumbs(&mut self) {
89        let root_id = self.document_info.root_id;
90
91        // Start with document title as first crumb if available
92        let root_breadcrumbs: Vec<String> = self
93            .document_info
94            .document_metadata
95            .title
96            .as_ref()
97            .filter(|t| !t.is_empty())
98            .map(|t| vec![t.clone()])
99            .unwrap_or_default();
100
101        // Set breadcrumbs on the Document node itself
102        if let Some(doc_node) = self.nodes.get_mut(&root_id) {
103            doc_node.location.semantic.breadcrumbs = root_breadcrumbs.clone();
104        }
105
106        // Collect children to avoid borrow conflict
107        let root_children: Vec<NodeId> = self
108            .nodes
109            .get(&root_id)
110            .map(|n| n.children.clone())
111            .unwrap_or_default();
112
113        for child_id in root_children {
114            self.propagate_breadcrumbs(child_id, &root_breadcrumbs);
115        }
116    }
117
118    /// Recursively propagate breadcrumbs down the tree
119    fn propagate_breadcrumbs(&mut self, node_id: NodeId, parent_breadcrumbs: &[String]) {
120        // Determine this node's breadcrumbs
121        let (node_breadcrumbs, children) = {
122            let node = match self.nodes.get(&node_id) {
123                Some(n) => n,
124                None => return,
125            };
126
127            let breadcrumbs = if node.node_type == "Section" {
128                // Sections contribute their text to the trail
129                let mut crumbs = parent_breadcrumbs.to_vec();
130                crumbs.push(node.content.text.clone());
131                crumbs
132            } else {
133                // Non-sections inherit parent breadcrumbs
134                parent_breadcrumbs.to_vec()
135            };
136
137            (breadcrumbs, node.children.clone())
138        };
139
140        // Set breadcrumbs on this node
141        if let Some(node) = self.nodes.get_mut(&node_id) {
142            node.location.semantic.breadcrumbs = node_breadcrumbs.clone();
143        }
144
145        // Recurse into children
146        for child_id in children {
147            self.propagate_breadcrumbs(child_id, &node_breadcrumbs);
148        }
149    }
150
151    /// Analyze any subtree starting from given node
152    pub fn _analyze_subtree(&self, root_node_id: NodeId) -> Option<GraphAnalyticsResult> {
153        let subtree_nodes = self._collect_subtree_nodes(root_node_id);
154        if subtree_nodes.is_empty() {
155            return None;
156        }
157        Some(GraphAnalytics::compute_analytics(&subtree_nodes))
158    }
159
160    /// Collect all nodes in a subtree starting from given root
161    fn _collect_subtree_nodes(&self, root_node_id: NodeId) -> Vec<&DocumentNode> {
162        let mut subtree_nodes = Vec::new();
163
164        if let Some(root_node) = self.nodes.get(&root_node_id) {
165            self._collect_subtree_recursive(root_node, &mut subtree_nodes);
166        }
167
168        subtree_nodes
169    }
170
171    /// Recursively collect all nodes in subtree
172    fn _collect_subtree_recursive<'a>(
173        &'a self,
174        node: &'a DocumentNode,
175        collected: &mut Vec<&'a DocumentNode>,
176    ) {
177        collected.push(node);
178
179        for child_id in &node.children {
180            if let Some(child_node) = self.nodes.get(child_id) {
181                self._collect_subtree_recursive(child_node, collected);
182            }
183        }
184    }
185}