use crate::{CodeGraph, EdgeType, NodeType, Result};
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct DotOptions {
pub node_colors: HashMap<NodeType, String>,
pub edge_colors: HashMap<EdgeType, String>,
pub node_shapes: HashMap<NodeType, String>,
pub rankdir: String,
pub show_properties: Vec<String>,
}
impl Default for DotOptions {
fn default() -> Self {
let mut node_colors = HashMap::new();
node_colors.insert(NodeType::CodeFile, "#E0E0E0".to_string());
node_colors.insert(NodeType::Function, "#90CAF9".to_string());
node_colors.insert(NodeType::Class, "#FFE082".to_string());
node_colors.insert(NodeType::Variable, "#CE93D8".to_string());
node_colors.insert(NodeType::Interface, "#FFAB91".to_string());
node_colors.insert(NodeType::Module, "#BCAAA4".to_string());
let mut node_shapes = HashMap::new();
node_shapes.insert(NodeType::CodeFile, "folder".to_string());
node_shapes.insert(NodeType::Function, "box".to_string());
node_shapes.insert(NodeType::Class, "component".to_string());
node_shapes.insert(NodeType::Variable, "ellipse".to_string());
node_shapes.insert(NodeType::Interface, "diamond".to_string());
node_shapes.insert(NodeType::Module, "folder".to_string());
DotOptions {
node_colors,
edge_colors: HashMap::new(),
node_shapes,
rankdir: "LR".to_string(),
show_properties: vec![],
}
}
}
pub fn export_dot(graph: &CodeGraph) -> Result<String> {
export_dot_styled(graph, DotOptions::default())
}
pub fn export_dot_styled(graph: &CodeGraph, options: DotOptions) -> Result<String> {
let mut output = String::new();
output.push_str("digraph code_graph {\n");
output.push_str(&format!(" rankdir={};\n", options.rankdir));
output.push_str(" node [style=filled];\n\n");
for node_id in 0..graph.node_count() as u64 {
if let Ok(node) = graph.get_node(node_id) {
let mut label = if let Some(name) = node.properties.get_string("name") {
escape_dot_label(name)
} else if let Some(path) = node.properties.get_string("path") {
escape_dot_label(path)
} else {
format!("n{node_id}")
};
for prop_name in &options.show_properties {
if let Some(value) = node.properties.get(prop_name) {
label.push_str(&format!(
"\\n{}:{}",
prop_name,
format_property_value(value)
));
}
}
let color = options
.node_colors
.get(&node.node_type)
.map(|s| s.as_str())
.unwrap_or("#FFFFFF");
let shape = options
.node_shapes
.get(&node.node_type)
.map(|s| s.as_str())
.unwrap_or("box");
output.push_str(&format!(
" n{node_id} [label=\"{label}\", shape={shape}, fillcolor=\"{color}\"];\n"
));
}
}
output.push('\n');
for edge_id in 0..graph.edge_count() as u64 {
if let Ok(edge) = graph.get_edge(edge_id) {
let edge_label = format!("{:?}", edge.edge_type);
let color = options
.edge_colors
.get(&edge.edge_type)
.map(|c| format!(", color=\"{c}\""))
.unwrap_or_default();
output.push_str(&format!(
" n{} -> n{} [label=\"{}\"{}];\n",
edge.source_id, edge.target_id, edge_label, color
));
}
}
output.push_str("}\n");
Ok(output)
}
fn escape_dot_label(s: &str) -> String {
s.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
fn format_property_value(value: &crate::PropertyValue) -> String {
match value {
crate::PropertyValue::String(s) => s.clone(),
crate::PropertyValue::Int(i) => i.to_string(),
crate::PropertyValue::Float(f) => f.to_string(),
crate::PropertyValue::Bool(b) => b.to_string(),
crate::PropertyValue::StringList(v) => v.join(","),
crate::PropertyValue::IntList(v) => v
.iter()
.map(|i| i.to_string())
.collect::<Vec<_>>()
.join(","),
crate::PropertyValue::Null => "null".to_string(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_escape_dot_label() {
assert_eq!(escape_dot_label("hello"), "hello");
assert_eq!(escape_dot_label("line\\nbreak"), "line\\\\nbreak");
assert_eq!(escape_dot_label("quote\"here"), "quote\\\"here");
}
}