1#![forbid(unsafe_code)]
4
5mod agwrite;
6pub mod emit;
7pub mod json;
8mod svg_engine;
9mod xdot_engine;
10
11pub fn render_dot_positioned(graph: &Graph, xdot: bool) -> String {
13 render_graph(
14 graph,
15 if xdot {
16 OutputFormat::Xdot
17 } else {
18 OutputFormat::Dot
19 },
20 )
21 .unwrap_or_default()
22}
23
24pub fn render_plain(graph: &Graph) -> String {
26 render_graph(graph, OutputFormat::Plain).unwrap_or_default()
27}
28
29pub fn render_json(graph: &Graph, json0: bool) -> Result<String> {
31 render_graph(
32 graph,
33 if json0 {
34 OutputFormat::Json0
35 } else {
36 OutputFormat::Json
37 },
38 )
39}
40pub use emit::reset_render_state;
41
42pub fn render_svg(graph: &Graph) -> String {
44 let m = emit::model::Model::build(graph);
45 emit::run(&m, svg_engine::SvgEngine::default()).out
46}
47
48use graphviz_pure_core::{Graph, GraphvizError, Result};
49use graphviz_pure_parser::write_canon;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum OutputFormat {
53 Svg,
54 Json,
55 Json0,
56 Dot,
57 Xdot,
58 Canon,
59 Plain,
60}
61
62impl OutputFormat {
63 pub fn parse(name: &str) -> Result<Self> {
64 match name.trim().to_ascii_lowercase().as_str() {
65 "svg" => Ok(Self::Svg),
66 "json" => Ok(Self::Json),
67 "json0" => Ok(Self::Json0),
68 "dot" | "gv" => Ok(Self::Dot),
69 "xdot" | "xdot1.2" | "xdot1.4" => Ok(Self::Xdot),
70 "dot_json" | "xdot_json" => Ok(Self::Json),
71 "canon" => Ok(Self::Canon),
72 "plain" | "plain-ext" => Ok(Self::Plain),
73 other => Err(GraphvizError::UnsupportedFormat(other.to_string())),
74 }
75 }
76}
77
78pub fn render_graph(graph: &Graph, format: OutputFormat) -> Result<String> {
79 render_graph_named(graph, format, "")
80}
81
82fn input_edge_order(g: &Graph) -> Vec<usize> {
84 let mut out = vec![Vec::new(); g.nodes.len()];
85 for (i, e) in g.edges.iter().enumerate() {
86 out[e.tail.0 as usize].push(i);
87 }
88 for l in &mut out {
89 l.sort_by_key(|&e| (g.edges[e].head.0, e));
90 }
91 out.into_iter().flatten().collect()
92}
93
94fn clust_nodes(g: &Graph) -> Vec<bool> {
96 g.nodes
97 .iter()
98 .map(|n| n.attrs.contains_key(graphviz_pure_core::CLUST_NODE_KEY))
99 .collect()
100}
101
102fn edge_order(m: &emit::model::Model<'_>) -> Vec<usize> {
104 m.out_edges.iter().flatten().copied().collect()
105}
106
107fn attached(
109 m: &emit::model::Model<'_>,
110 xdot: bool,
111 fixed_xdot: Option<&str>,
112) -> (Graph, Vec<std::collections::BTreeSet<String>>) {
113 let (mut g, _, _, mut forced) = emit::output::attach_attrs(m);
114 if xdot {
115 let eng = xdot_engine::XdotEngine::new(m.root_attrs().get("xdotversion"), fixed_xdot);
116 let eng = emit::run(m, eng);
117 for (kind, name, value) in eng.sets {
118 let attrs = match kind {
119 emit::job::ObjKind::Root => &mut g.graph_attrs,
120 emit::job::ObjKind::Cluster(c) => &mut g.subgraphs[m.clusters[c].sub].graph_attrs,
121 emit::job::ObjKind::Node(n) => &mut g.nodes[n].attrs,
122 emit::job::ObjKind::Edge(e) => &mut g.edges[e].attrs,
123 };
124 if let emit::job::ObjKind::Cluster(c) = kind {
125 forced[m.clusters[c].sub].insert(name.to_string());
126 }
127 attrs.insert(name.to_string(), value);
128 }
129 }
130 (g, forced)
131}
132
133const NOP_KEY: &str = "\u{1}nop";
135
136const NO_LOW_CLUSTERS_KEY: &str = "\u{1}nolowclust";
139
140fn sync_dicts(g: &mut Graph, orig: &Graph) {
143 use graphviz_pure_core::AttrSym;
144 let set_graph = |d: &mut std::collections::BTreeMap<String, AttrSym>,
145 now: &graphviz_pure_core::AttrMap,
146 before: &graphviz_pure_core::AttrMap| {
147 for (k, v) in now {
148 if before.get(k) != Some(v) {
149 d.entry(k.clone()).or_default().defval = v.clone();
150 }
151 }
152 };
153 let (root_now, root_before) = (g.graph_attrs.clone(), &orig.graph_attrs);
154 for k in root_now.keys() {
156 if !root_before.contains_key(k) {
157 g.dicts[0].graph.entry(k.clone()).or_default();
158 }
159 }
160 set_graph(&mut g.dicts[0].graph, &root_now, root_before);
161 for i in 0..g.subgraphs.len() {
162 let now = g.subgraphs[i].graph_attrs.clone();
163 for k in now.keys() {
164 if !orig.subgraphs[i].graph_attrs.contains_key(k) {
165 g.dicts[0].graph.entry(k.clone()).or_default();
166 }
167 }
168 set_graph(
169 &mut g.dicts[i + 1].graph,
170 &now,
171 &orig.subgraphs[i].graph_attrs,
172 );
173 }
174 let mut node_keys = std::collections::BTreeSet::new();
175 for n in &g.nodes {
176 node_keys.extend(n.attrs.keys().cloned());
177 }
178 for k in node_keys {
179 if !g.dicts.iter().any(|d| d.node.contains_key(&k)) {
180 g.dicts[0].node.insert(k, AttrSym::default());
181 }
182 }
183 let mut edge_keys = std::collections::BTreeSet::new();
184 for e in &g.edges {
185 edge_keys.extend(e.attrs.keys().cloned());
186 }
187 for k in edge_keys {
188 if !g.dicts.iter().any(|d| d.edge.contains_key(&k)) {
189 g.dicts[0].edge.insert(k, AttrSym::default());
190 }
191 }
192}
193
194fn low_cluster_edges(graph: &Graph) -> Vec<std::collections::BTreeSet<usize>> {
197 let n = graph.subgraphs.len();
198 let mut parent: Vec<Option<usize>> = vec![None; n];
199 for (i, sg) in graph.subgraphs.iter().enumerate() {
200 for c in &sg.subgraphs {
201 parent[c.0 as usize] = Some(i);
202 }
203 }
204 let mut sets = vec![std::collections::BTreeSet::new(); n];
205 for (i, sg) in graph.subgraphs.iter().enumerate() {
206 if !sg.is_cluster {
207 continue;
208 }
209 let members: std::collections::BTreeSet<u32> = sg.nodes.iter().map(|x| x.0).collect();
210 for (ei, e) in graph.edges.iter().enumerate() {
211 if members.contains(&e.tail.0) && members.contains(&e.head.0) {
212 let mut cur = Some(i);
213 while let Some(c) = cur {
214 sets[c].insert(ei);
215 cur = parent[c];
216 }
217 }
218 }
219 }
220 sets
221}
222
223pub fn render_graph_named(graph: &Graph, format: OutputFormat, name: &str) -> Result<String> {
225 graphviz_pure_core::charset::begin_render(
226 &graph.name,
227 graph.graph_attrs.get("charset").map(String::as_str),
228 );
229 if format == OutputFormat::Canon {
230 if graph.dicts.len() == graph.subgraphs.len() + 1 {
231 return Ok(agwrite::agwrite(graph, &[]));
232 }
233 return Ok(write_canon(graph));
234 }
235 let lname = name.to_ascii_lowercase();
236 if lname == "dot_json" || lname == "xdot_json" {
237 return Ok(json::write_json(
239 graph,
240 lname == "xdot_json",
241 &input_edge_order(graph),
242 &clust_nodes(graph),
243 ));
244 }
245 let drawn = match (format, graph.render_view.as_deref()) {
247 (OutputFormat::Svg, Some(v)) => v,
248 _ => graph,
249 };
250 let m = emit::model::Model::build(drawn);
251 Ok(match format {
252 OutputFormat::Svg => emit::run(&m, svg_engine::SvgEngine::default()).out,
253 OutputFormat::Json | OutputFormat::Json0 => {
254 let (g, _) = attached(&m, format == OutputFormat::Json, None);
255 json::write_json(
256 &g,
257 format == OutputFormat::Json,
258 &edge_order(&m),
259 &clust_nodes(graph),
260 )
261 }
262 OutputFormat::Dot | OutputFormat::Xdot => {
263 let (mut g, forced) = attached(
264 &m,
265 format == OutputFormat::Xdot,
266 match lname.as_str() {
267 "xdot1.2" => Some("1.2"),
268 "xdot1.4" => Some("1.4"),
269 _ => None,
270 },
271 );
272 if g.dicts.len() == g.subgraphs.len() + 1 {
273 sync_dicts(&mut g, graph);
274 let extra = if graph.graph_attrs.contains_key(NOP_KEY)
275 || graph.graph_attrs.contains_key(NO_LOW_CLUSTERS_KEY)
276 {
277 Vec::new()
278 } else {
279 low_cluster_edges(graph)
280 };
281 agwrite::agwrite(&g, &extra)
282 } else {
283 emit::output::propagate_inherited(&mut g, graph, &forced);
284 graphviz_pure_parser::write_canon_forced(&g, &forced)
285 }
286 }
287 OutputFormat::Plain => {
288 let zoom = emit::job::Job::new(
289 &m,
290 emit::job::Features {
291 flags: 0,
292 default_pad: 0.0,
293 knowncolors: &[],
294 rgba: false,
295 },
296 0,
297 )
298 .zoom;
299 emit::output::write_plain(&m, zoom, name.eq_ignore_ascii_case("plain-ext"))
300 }
301 OutputFormat::Canon => unreachable!(),
302 })
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use graphviz_pure_core::{BezierSegment, BoundingBox, Point, Spline};
309 use graphviz_pure_parser::parse_dot;
310
311 #[test]
315 fn cluster_list_key_keeps_only_descendants() {
316 let mut g = parse_dot(
317 "digraph G { subgraph cluster_a { a subgraph cluster_c { c } } subgraph cluster_b { b } }",
318 )
319 .expect("parse");
320 let idx = |name: &str| g.subgraphs.iter().position(|s| s.name == name).unwrap();
321 let (a, c, b) = (idx("cluster_a"), idx("cluster_c"), idx("cluster_b"));
322 g.bb = BoundingBox::new(0.0, 0.0, 100.0, 100.0);
323 for s in &mut g.subgraphs {
324 s.bb = BoundingBox::new(10.0, 10.0, 50.0, 50.0);
325 }
326 g.subgraphs[a].graph_attrs.insert(
327 graphviz_pure_core::CLUSTER_LIST_KEY.to_string(),
328 format!("{a} {c} {c} {b} 99"),
329 );
330 let svg = render_graph(&g, OutputFormat::Svg).expect("render");
331 let titles: Vec<&str> = svg
332 .split("class=\"cluster\">\n<title>")
333 .skip(1)
334 .map(|s| &s[..s.find("</title>").unwrap()])
335 .collect();
336 assert_eq!(titles, ["cluster_a", "cluster_c", "cluster_b"]);
337 }
338
339 fn sample_laid_out_graph() -> Graph {
340 let mut g = parse_dot(
341 r##"
342 digraph G {
343 label="Sample Graph";
344 subgraph cluster_0 {
345 label="Cluster A";
346 style=filled;
347 fillcolor="#eeeeee";
348 a [label="Node A", shape=box, style="filled,rounded", fillcolor=lightblue];
349 b [label="{f0|f1|f2}", shape=record];
350 }
351 c [label=<<b>HTML</b><br/>Node>, shape=diamond];
352 a -> b [label="ab"];
353 b -> c [arrowhead=vee, style=dashed];
354 }
355 "##,
356 )
357 .expect("parse sample graph");
358
359 g.bb = BoundingBox::new(0.0, 0.0, 220.0, 180.0);
360
361 if let Some(sub) = g.subgraphs.first_mut() {
362 sub.bb = BoundingBox::new(16.0, 64.0, 204.0, 168.0);
363 }
364
365 if let Some(a) = g.nodes.get_mut(0) {
366 a.pos = Point::new(60.0, 110.0);
367 a.width = 1.0;
368 a.height = 0.5;
369 }
370 if let Some(b) = g.nodes.get_mut(1) {
371 b.pos = Point::new(150.0, 110.0);
372 b.width = 1.2;
373 b.height = 0.5;
374 }
375 if let Some(c) = g.nodes.get_mut(2) {
376 c.pos = Point::new(150.0, 36.0);
377 c.width = 1.1;
378 c.height = 0.6;
379 }
380
381 if let Some(e0) = g.edges.get_mut(0) {
382 e0.spline = Spline {
383 segments: vec![BezierSegment {
384 points: vec![
385 Point::new(96.0, 110.0),
386 Point::new(102.0, 110.0),
387 Point::new(105.0, 110.0),
388 Point::new(108.0, 110.0),
389 ],
390 start_arrow: None,
391 end_arrow: Some(Point::new(114.0, 110.0)),
392 }],
393 };
394 e0.label_pos = Some(Point::new(105.0, 122.0));
395 }
396
397 if let Some(e1) = g.edges.get_mut(1) {
398 e1.spline = Spline {
399 segments: vec![BezierSegment {
400 points: vec![
401 Point::new(150.0, 92.0),
402 Point::new(150.0, 80.0),
403 Point::new(150.0, 70.0),
404 Point::new(150.0, 62.0),
405 ],
406 start_arrow: None,
407 end_arrow: Some(Point::new(150.0, 58.0)),
408 }],
409 };
410 }
411
412 g
413 }
414
415 #[test]
416 fn svg_renderer_emits_graphviz_structure_shapes_and_edges() {
417 let g = sample_laid_out_graph();
418 let svg = render_graph(&g, OutputFormat::Svg).expect("render svg");
419
420 assert!(svg.starts_with("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>"));
421 assert!(svg.contains("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\""));
422 assert!(svg.contains("<svg width=\"228pt\" height=\"188pt\""));
423 assert!(svg.contains("viewBox=\"0.00 0.00 228.00 188.00\""));
424 assert!(svg.contains(
425 "<g id=\"graph0\" class=\"graph\" transform=\"scale(1 1) rotate(0) translate(4 184)\">"
426 ));
427 assert!(svg.contains("<g id=\"clust1\" class=\"cluster\">"));
428 assert!(svg.contains("<g id=\"node1\" class=\"node\">"));
429 assert!(svg.contains("<g id=\"node2\" class=\"node\">"));
430 assert!(svg.contains("<g id=\"node3\" class=\"node\">"));
431 assert!(svg.contains("<g id=\"edge1\" class=\"edge\">"));
432 assert!(svg.contains("<g id=\"edge2\" class=\"edge\">"));
433 assert!(svg.contains("<title>a->b</title>"));
434 assert!(svg.contains("<path fill=\"none\""));
435 assert!(svg.contains("font-weight=\"bold\""));
436 }
437
438 #[test]
439 fn json_and_json0_render_graphviz_schema() {
440 let g = sample_laid_out_graph();
441 let json0_str = render_graph(&g, OutputFormat::Json0).expect("render json0");
442 let v0: serde_json::Value = serde_json::from_str(&json0_str).expect("valid json0");
443 assert_eq!(v0["name"], "G");
444 assert_eq!(v0["directed"], true);
445 assert_eq!(v0["strict"], false);
446 assert_eq!(v0["bb"], "0,0,220,180");
447 assert_eq!(v0["_subgraph_cnt"], 1);
448 assert!(v0["objects"].is_array());
449 assert_eq!(v0["objects"].as_array().unwrap().len(), 4);
450 assert!(v0["objects"][0].get("_draw_").is_none());
451 assert_eq!(v0["edges"].as_array().unwrap().len(), 2);
452 assert!(
453 v0["edges"][0]["pos"]
454 .as_str()
455 .unwrap()
456 .starts_with("e,114,110 ")
457 );
458
459 let json_str = render_graph(&g, OutputFormat::Json).expect("render json");
460 let v: serde_json::Value = serde_json::from_str(&json_str).expect("valid json");
461 assert!(v["_draw_"].is_array());
462 assert!(v["objects"][0]["_draw_"].is_array());
463 assert!(v["objects"][1]["_draw_"].is_array());
464 assert!(v["edges"][0]["_draw_"].is_array());
465 assert!(v["edges"][0]["_hdraw_"].is_array());
466 }
467
468 #[test]
469 fn dot_xdot_and_plain_outputs_include_layout_positions() {
470 let g = sample_laid_out_graph();
471
472 let dot = render_graph(&g, OutputFormat::Dot).expect("render dot");
473 assert!(dot.contains("bb=\"0,0,220,180\""));
474 assert!(dot.contains("pos=\"60,110\""));
475 assert!(dot.contains("e,114,110 96,110 102,110 105,110 108,110"));
476 assert!(!dot.contains("_draw_"));
477
478 let xdot = render_graph(&g, OutputFormat::Xdot).expect("render xdot");
479 assert!(xdot.contains("xdotversion=1.7"));
480 assert!(xdot.contains("_draw_="));
481 assert!(xdot.contains("_ldraw_="));
482 assert!(xdot.contains("_hdraw_="));
483
484 let plain = render_graph(&g, OutputFormat::Plain).expect("render plain");
485 assert!(plain.starts_with("graph 1 "));
486 assert!(plain.contains("\nnode a "));
487 assert!(plain.contains("\nnode b "));
488 assert!(plain.contains("\nedge a b 4 "));
489 assert!(plain.ends_with("stop\n"));
490 }
491}