1pub use graphviz_pure_core::{Graph, GraphvizError, Result};
4pub use graphviz_pure_layout::{
5 LayoutEngine, LayoutOptions, layout_graph, layout_graph_with_options,
6};
7pub use graphviz_pure_parser::{GlobalAttrs, parse_dot, parse_dot_all};
8pub use graphviz_pure_render::{OutputFormat, render_graph, render_graph_named};
9
10pub fn render_dot(dot_source: &str, engine: &str, format: &str) -> Result<String> {
13 let output_format = OutputFormat::parse(format)?;
14 graphviz_pure_render::reset_render_state();
16 let graph = parse_dot(dot_source)?;
17 render_parsed(
18 graph,
19 engine,
20 output_format,
21 format,
22 LayoutOptions::default(),
23 )
24}
25
26pub fn render_dot_all(
28 dot_source: &str,
29 engine: &str,
30 format: &str,
31 globals: &GlobalAttrs,
32) -> Result<Vec<String>> {
33 render_dot_all_with_options(
34 dot_source,
35 engine,
36 format,
37 globals,
38 LayoutOptions::default(),
39 )
40}
41
42pub fn render_dot_all_with_options(
44 dot_source: &str,
45 engine: &str,
46 format: &str,
47 globals: &GlobalAttrs,
48 options: LayoutOptions,
49) -> Result<Vec<String>> {
50 let output_format = OutputFormat::parse(format)?;
51 graphviz_pure_render::reset_render_state();
52 parse_dot_all(dot_source, globals)?
53 .into_iter()
54 .map(|graph| render_parsed(graph, engine, output_format, format, options))
55 .collect()
56}
57
58pub fn render_dot_all_partial(
61 dot_source: &str,
62 engine: &str,
63 format: &str,
64 globals: &GlobalAttrs,
65 options: LayoutOptions,
66) -> (Vec<String>, Option<graphviz_pure_core::GraphvizError>) {
67 let output_format = match OutputFormat::parse(format) {
68 Ok(f) => f,
69 Err(e) => return (Vec::new(), Some(e)),
70 };
71 graphviz_pure_render::reset_render_state();
72 let (graphs, parse_err) = graphviz_pure_parser::parse_dot_all_partial(dot_source, globals);
73 let mut out = Vec::with_capacity(graphs.len());
74 for graph in graphs {
75 match render_parsed(graph, engine, output_format, format, options) {
76 Ok(s) => out.push(s),
77 Err(e) => return (out, Some(e)),
78 }
79 }
80 (out, parse_err)
81}
82
83pub fn render_svg_all_partial(
88 dot_source: &str,
89 engine: &str,
90 globals: &GlobalAttrs,
91 options: LayoutOptions,
92) -> (
93 Vec<(String, bool)>,
94 Option<graphviz_pure_core::GraphvizError>,
95) {
96 graphviz_pure_render::reset_render_state();
97 let (graphs, parse_err) = graphviz_pure_parser::parse_dot_all_partial(dot_source, globals);
98 let mut out = Vec::with_capacity(graphs.len());
99 for graph in graphs {
100 let explicit_dpi = ["dpi", "resolution"].iter().any(|k| {
101 graph
102 .graph_attrs
103 .get(*k)
104 .and_then(|v| v.trim().parse::<f64>().ok())
105 .is_some_and(|d| d > 0.0)
106 });
107 match render_parsed(graph, engine, OutputFormat::Svg, "svg", options) {
108 Ok(s) => out.push((s, explicit_dpi)),
109 Err(e) => return (out, Some(e)),
110 }
111 }
112 (out, parse_err)
113}
114
115fn render_parsed(
116 mut graph: Graph,
117 engine: &str,
118 output_format: OutputFormat,
119 format_name: &str,
120 options: LayoutOptions,
121) -> Result<String> {
122 let layout_engine = match graph.graph_attrs.get("layout") {
124 Some(name) if !name.trim().is_empty() => LayoutEngine::parse(name.trim())?,
125 _ => LayoutEngine::parse(engine)?,
126 };
127 let lname = format_name.to_ascii_lowercase();
128 if output_format != OutputFormat::Canon && lname != "dot_json" && lname != "xdot_json" {
129 layout_graph_with_options(&mut graph, layout_engine, options)?;
130 }
131 render_graph_named(&graph, output_format, format_name)
132}
133
134#[cfg(test)]
135mod tests {
136
137 #[test]
138 fn repeated_renders_are_identical() {
139 let src = r#"digraph { a [style=filled, fillcolor="red:blue"];
142 b [label=<<TABLE><TR><TD HREF="x">c</TD></TR></TABLE>>]; a -> b }"#;
143 let first = render_dot(src, "dot", "svg").unwrap();
144 assert_eq!(first, render_dot(src, "dot", "svg").unwrap());
145 assert!(first.contains("_l_0"));
146 }
147
148 use super::*;
149 use std::fs;
150 use std::path::PathBuf;
151
152 #[test]
153 fn smoke_render_svg() {
154 let svg = render_dot("digraph G { a -> b; }", "dot", "svg").unwrap();
155 assert!(svg.contains("<svg"));
156 }
157
158 #[test]
159 fn render_dot_all_renders_every_graph_with_globals() {
160 let mut globals = GlobalAttrs::default();
161 globals.node.insert("shape".into(), "box".into());
162 let outs = render_dot_all(
163 "digraph G { a -> b }\ndigraph H { c -> d }",
164 "dot",
165 "svg",
166 &globals,
167 )
168 .unwrap();
169 assert_eq!(outs.len(), 2);
170 assert!(outs[0].contains("<title>a</title>") && !outs[0].contains("<ellipse"));
171 assert!(outs[1].contains("<title>c</title>"));
172 }
173
174 #[test]
175 fn test_multigraph_and_duplicate_cluster_scopes() {
176 let multi = "digraph G { a -> b; }\ndigraph H { c -> d; }\n";
178 let svg = render_dot(multi, "dot", "svg").unwrap();
179 assert!(svg.contains("<svg"));
180 assert!(svg.contains("<title>a</title>"));
181
182 let b7 = r#"
184 digraph G {
185 subgraph cluster_0 {
186 subgraph cluster_1 {}
187 a -> c;
188 }
189 subgraph cluster_1 {
190 y -> z;
191 y -> q;
192 }
193 y -> b;
194 }
195 "#;
196 let g = parse_dot(b7).unwrap();
197 assert_eq!(g.subgraphs.len(), 3);
198 let j0 = render_dot(b7, "dot", "json0").unwrap();
199 assert!(j0.contains("\"_subgraph_cnt\": 3"));
200 }
201
202 #[test]
203 fn test_wild_corpus_renders_without_error() {
204 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
205 let wild_dir = manifest_dir.join("../../tests/wild");
206 let mut entries: Vec<PathBuf> = fs::read_dir(&wild_dir)
207 .expect("tests/wild directory must exist")
208 .filter_map(|e| e.ok().map(|e| e.path()))
209 .filter(|p| p.extension().is_some_and(|ext| ext == "gv"))
210 .collect();
211 entries.sort();
212 assert!(
213 entries.len() >= 30,
214 "expected at least 30 wild corpus fixtures"
215 );
216
217 for path in entries {
218 let src = graphviz_pure_core::charset::decode(fs::read(&path).unwrap());
219 let svg = render_dot(&src, "dot", "svg")
220 .unwrap_or_else(|e| panic!("Failed rendering SVG for {}: {e}", path.display()));
221 assert!(svg.contains("<svg"), "Missing <svg in {}", path.display());
222 assert!(
223 !svg.contains("NaN") && !svg.contains("Infinity"),
224 "Non-finite coordinate in {}",
225 path.display()
226 );
227
228 let j0 = render_dot(&src, "dot", "json0")
229 .unwrap_or_else(|e| panic!("Failed rendering json0 for {}: {e}", path.display()));
230 assert!(j0.contains("\"bb\":"), "Missing bb in {}", path.display());
231 }
232 }
233}