diff --git a/.gitignore b/.gitignore index fc80547..75e8162 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ Cargo.lock # .dot file for tests **/*.dot + +# Editor related configuratigurations +.vscode \ No newline at end of file diff --git a/src/arrow.rs b/src/arrow.rs index 9663fa1..14e9a85 100644 --- a/src/arrow.rs +++ b/src/arrow.rs @@ -1,4 +1,3 @@ - /// This structure holds all information that can describe an arrow connected to /// either start or end of an edge. #[derive(Clone, Hash, PartialEq, Eq)] @@ -16,9 +15,7 @@ impl Arrow { /// Arrow constructor which returns a default arrow pub fn default() -> Arrow { - Arrow { - arrows: vec![], - } + Arrow { arrows: vec![] } } /// Arrow constructor which returns an empty arrow @@ -31,7 +28,7 @@ impl Arrow { /// Arrow constructor which returns a regular triangle arrow, without modifiers pub fn normal() -> Arrow { Arrow { - arrows: vec![ArrowShape::normal()] + arrows: vec![ArrowShape::normal()], } } @@ -47,12 +44,11 @@ impl Arrow { let mut cow = String::new(); for arrow in &self.arrows { cow.push_str(&arrow.to_dot_string()); - }; + } cow } } - impl Into for [ArrowShape; 2] { fn into(self) -> Arrow { Arrow { @@ -103,14 +99,13 @@ pub enum Side { impl Side { pub fn as_slice(self) -> &'static str { match self { - Side::Left => "l", + Side::Left => "l", Side::Right => "r", - Side::Both => "", + Side::Both => "", } } } - /// This enumeration represents all possible arrow edge /// as defined in [grapviz documentation](http://www.graphviz.org/content/arrow-shapes). #[derive(Clone, Copy, Hash, PartialEq, Eq)] @@ -199,37 +194,37 @@ impl ArrowShape { pub fn to_dot_string(&self) -> String { let mut res = String::new(); match *self { - Box(fill, side) | ICurve(fill, side)| Diamond(fill, side) | - Inv(fill, side) | Normal(fill, side)=> { + Box(fill, side) + | ICurve(fill, side) + | Diamond(fill, side) + | Inv(fill, side) + | Normal(fill, side) => { res.push_str(fill.as_slice()); match side { Side::Left | Side::Right => res.push_str(side.as_slice()), - Side::Both => {}, + Side::Both => {} }; - }, - Dot(fill) => res.push_str(fill.as_slice()), - Crow(side) | Curve(side) | Tee(side) - | Vee(side) => { - match side { - Side::Left | Side::Right => res.push_str(side.as_slice()), - Side::Both => {}, - } } - NoArrow => {}, + Dot(fill) => res.push_str(fill.as_slice()), + Crow(side) | Curve(side) | Tee(side) | Vee(side) => match side { + Side::Left | Side::Right => res.push_str(side.as_slice()), + Side::Both => {} + }, + NoArrow => {} }; match *self { - NoArrow => res.push_str("none"), - Normal(_, _) => res.push_str("normal"), - Box(_, _) => res.push_str("box"), - Crow(_) => res.push_str("crow"), - Curve(_) => res.push_str("curve"), - ICurve(_, _) => res.push_str("icurve"), - Diamond(_, _) => res.push_str("diamond"), - Dot(_) => res.push_str("dot"), - Inv(_, _) => res.push_str("inv"), - Tee(_) => res.push_str("tee"), - Vee(_) => res.push_str("vee"), + NoArrow => res.push_str("none"), + Normal(_, _) => res.push_str("normal"), + Box(_, _) => res.push_str("box"), + Crow(_) => res.push_str("crow"), + Curve(_) => res.push_str("curve"), + ICurve(_, _) => res.push_str("icurve"), + Diamond(_, _) => res.push_str("diamond"), + Dot(_) => res.push_str("dot"), + Inv(_, _) => res.push_str("inv"), + Tee(_) => res.push_str("tee"), + Vee(_) => res.push_str("vee"), }; res } -} \ No newline at end of file +} diff --git a/src/edge.rs b/src/edge.rs index 814fa3d..2faa389 100644 --- a/src/edge.rs +++ b/src/edge.rs @@ -1,8 +1,4 @@ -use crate::{ - arrow::{Arrow}, - style::{Style}, - utils::{quote_string}, -}; +use crate::{arrow::Arrow, style::Style, utils::quote_string}; /// `Graph`'s edge. #[derive(Clone)] @@ -10,6 +6,8 @@ pub struct Edge { from: String, to: String, label: String, + label_url: String, + url: String, style: Style, start_arrow: Arrow, end_arrow: Arrow, @@ -18,40 +16,55 @@ pub struct Edge { impl Edge { pub fn new(from: &str, to: &str, label: &str) -> Self { - Edge { from: String::from(from), to: String::from(to), label: String::from(label), style: Style::None, start_arrow: Arrow::default(), end_arrow: Arrow::default(), color: None } + Edge { + from: String::from(from), + to: String::from(to), + label: String::from(label), + label_url: Default::default(), + color: None, + style: Style::None, + start_arrow: Arrow::default(), + end_arrow: Arrow::default(), + url: Default::default(), + } } - pub fn label(&mut self, label: &str) -> Self { - let mut edge = self.clone(); - edge.label = String::from(label); - edge + pub fn label(mut self, label: &str) -> Self { + self.label = String::from(label); + self } - pub fn style(&mut self, style: Style) -> Self { - let mut edge = self.clone(); - edge.style = style; - edge + pub fn style(mut self, style: Style) -> Self { + self.style = style; + self } - pub fn color(&mut self, color: Option<&str>) -> Self { - let mut edge = self.clone(); - edge.color = match color { + pub fn color(mut self, color: Option<&str>) -> Self { + self.color = match color { Some(c) => Some(String::from(c)), - None => None + None => None, }; - edge + self + } + + pub fn start_arrow(mut self, arrow: Arrow) -> Self { + self.start_arrow = arrow; + self } - pub fn start_arrow(&mut self, arrow: Arrow) -> Self { - let mut edge = self.clone(); - edge.start_arrow = arrow; - edge + pub fn end_arrow(mut self, arrow: Arrow) -> Self { + self.end_arrow = arrow; + self } - pub fn end_arrow(&mut self, arrow: Arrow) -> Self { - let mut edge = self.clone(); - edge.end_arrow = arrow; - edge + pub fn label_url(mut self, url: String) -> Self { + self.label_url = url; + self + } + + pub fn url(mut self, url: String) -> Self { + self.url = url; + self } pub fn to_dot_string(&self, edge_symbol: &str) -> String { @@ -59,15 +72,36 @@ impl Edge { let escaped_label: &String = "e_string(self.label.clone()); let start_arrow_s: String = self.start_arrow.to_dot_string(); let end_arrow_s: String = self.end_arrow.to_dot_string(); + let escaped_label_url: &String = "e_string(self.label_url.clone()); + let escaped_url: &String = "e_string(self.url.clone()); - let mut text = vec!["\"", self.from.as_str(), "\" ", - edge_symbol, " ", - "\"", self.to.as_str(), "\"",]; + let mut text = vec![ + "\"", + self.from.as_str(), + "\" ", + edge_symbol, + " ", + "\"", + self.to.as_str(), + "\"", + ]; text.push("[label="); text.push(escaped_label.as_str()); text.push("]"); + if !self.label_url.is_empty() { + text.push("[labelURL="); + text.push(escaped_label_url.as_str()); + text.push("]"); + } + + if !self.url.is_empty() { + text.push("[URL="); + text.push(escaped_url.as_str()); + text.push("]"); + } + if self.style != Style::None { text.push("[style=\""); text.push(self.style.as_slice()); @@ -75,9 +109,7 @@ impl Edge { } let color: Option = match &self.color { - Some(l) => { - Some((*l).clone()) - }, + Some(l) => Some((*l).clone()), None => None, }; if let Some(c) = color { @@ -91,10 +123,18 @@ impl Edge { let mut arrow_str: String = String::new(); if !self.start_arrow.is_default() || !self.end_arrow.is_default() { if !self.end_arrow.is_default() { - arrow_text.push(vec!["arrowhead=\"", &end_arrow_s, "\""].into_iter().collect()); + arrow_text.push( + vec!["arrowhead=\"", &end_arrow_s, "\""] + .into_iter() + .collect(), + ); } if !self.start_arrow.is_default() { - arrow_text.push(vec!["arrowtail=\"", &start_arrow_s, "\""].into_iter().collect()); + arrow_text.push( + vec!["arrowtail=\"", &start_arrow_s, "\""] + .into_iter() + .collect(), + ); } if !self.start_arrow.is_default() && !self.end_arrow.is_default() { arrow_text.push(String::from("dir=\"both\"")); @@ -110,4 +150,4 @@ impl Edge { text.push(";"); return text.into_iter().collect(); } -} \ No newline at end of file +} diff --git a/src/graph.rs b/src/graph.rs index feaee1f..8a57259 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -1,22 +1,28 @@ -use crate::{ - node::{Node}, - edge::{Edge}, subgraph::Subgraph, -}; -use std::io::prelude::*; +use crate::{edge::Edge, node::Node, subgraph::Subgraph, utils::quote_string}; use std::io; +use std::io::prelude::*; /// Entry point of this library, use `to_dot_string` to get the string output. +#[derive(Clone)] pub struct Graph { name: String, kind: Kind, + url: String, nodes: Vec, edges: Vec, - subgraph: Vec + subgraph: Vec, } impl Graph { pub fn new(name: &str, kind: Kind) -> Graph { - Graph { name: String::from(name), kind: kind, nodes: vec![], edges: vec![], subgraph: vec![] } + Graph { + name: String::from(name), + kind: kind, + nodes: vec![], + edges: vec![], + subgraph: vec![], + url: Default::default(), + } } pub fn add_node(&mut self, node: Node) -> () { @@ -31,6 +37,12 @@ impl Graph { self.subgraph.push(subgraph.edgeop(self.kind.edgeop())) } + pub fn url(&mut self, url: String) -> Self { + let mut graph = self.clone(); + graph.url = url; + graph + } + pub fn to_dot_string(&self) -> io::Result { let mut writer = Vec::new(); self.render_opts(&mut writer).unwrap(); @@ -41,11 +53,7 @@ impl Graph { /// Renders graph `g` into the writer `w` in DOT syntax. /// (Main entry point for the library.) - fn render_opts<'a, - W: Write> - (&self, - w: &mut W) - -> io::Result<()> { + fn render_opts<'a, W: Write>(&self, w: &mut W) -> io::Result<()> { fn writeln(w: &mut W, arg: &[&str]) -> io::Result<()> { for &s in arg { w.write_all(s.as_bytes())?; @@ -58,6 +66,12 @@ impl Graph { } writeln(w, &[self.kind.keyword(), " ", self.name.as_str(), " {"])?; + + if !self.url.is_empty() { + indent(w)?; + writeln(w, &["URL=", quote_string(self.url.clone()).as_str()])?; + } + for n in self.subgraph.iter() { indent(w)?; let mut text: Vec<&str> = vec![]; @@ -101,7 +115,7 @@ impl Kind { pub fn keyword(&self) -> &'static str { match *self { Kind::Digraph => "digraph", - Kind::Graph => "graph" + Kind::Graph => "graph", } } @@ -112,4 +126,4 @@ impl Kind { Kind::Graph => "--", } } -} \ No newline at end of file +} diff --git a/src/lib.rs b/src/lib.rs index 0124db8..a8aabdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,18 +5,18 @@ // except according to those terms. //! A library for generating Graphviz DOT language files. -//! +//! //! The very basic three parts of a DOT file and in this library is `Graph`, //! `Node` and `Edge`. `Graph` is the entry point of the library. You could //! generate an empty graph .dot file by simply use: -//! +//! //! ```rust //! use dot_graph::{Graph, Kind}; -//! +//! //! let graph = Graph::new("empty_graph", Kind::Digraph); -//! +//! //! let dot_string = graph.to_dot_string().unwrap(); -//! +//! //! assert_eq!(dot_string, //! r#"digraph empty_graph { //! } @@ -24,18 +24,18 @@ //! ``` //! //! In order to add some basic nodes and edges: -//! +//! //! ```rust //! use dot_graph::{Graph, Kind, Node, Edge}; -//! +//! //! let mut graph = Graph::new("single_edge", Kind::Digraph); -//! +//! //! graph.add_node(Node::new("N0")); //! graph.add_node(Node::new("N1")); //! graph.add_edge(Edge::new("N0", "N1", "E")); -//! +//! //! let dot_string = graph.to_dot_string().unwrap(); -//! +//! //! assert_eq!(dot_string, //! r#"digraph single_edge { //! "N0"[label="N0"]; @@ -44,19 +44,19 @@ //! } //! "#); //! ``` -//! +//! //! If you want add some more attributes, like style, arrow, color, //! you could call these methods in a chain, like: -//! +//! //! ```rust //! use dot_graph::{Graph, Kind, Node, Edge, Style}; -//! +//! //! let mut graph = Graph::new("single_edge", Kind::Digraph); -//! +//! //! graph.add_node(Node::new("N0")); //! graph.add_node(Node::new("N1")); //! graph.add_edge(Edge::new("N0", "N1", "E").style(Style::Bold).color(Some("red"))); -//! +//! //! assert_eq!(graph.to_dot_string().unwrap(), //! r#"digraph single_edge { //! "N0"[label="N0"]; @@ -65,9 +65,9 @@ //! } //! "#); //! ``` -//! +//! //! After version 0.2.1, dot_graph support subgraph generation, for example: -//! +//! //! ```rust //! #[test] //! fn test_subgraph() { @@ -85,7 +85,7 @@ //! graph.add_edge(Edge::new("N1", "N3", "")); //! graph.add_edge(Edge::new("N2", "N3", "")); //! -//! +//! //! assert_eq!(graph.to_dot_string().unwrap(), //! r#"digraph di { //! subgraph cluster_0 { @@ -106,30 +106,29 @@ //! "#); //! } //! ``` -//! +//! //! For more examples, please check the tests. -//! +//! //! The library is under active development, we'll include more dot attributes //! in the future. -//! +//! //! # References //! //! * [Graphviz](http://graphviz.org/) //! //! * [DOT language](http://graphviz.org/doc/info/lang.html) -mod style; mod arrow; -mod node; mod edge; mod graph; -mod utils; +mod node; +mod style; mod subgraph; +mod utils; -pub use style::Style; -pub use arrow::{Arrow, ArrowShape, Side, Fill}; -pub use node::{Node}; -pub use edge::{Edge}; +pub use arrow::{Arrow, ArrowShape, Fill, Side}; +pub use edge::Edge; pub use graph::{Graph, Kind}; +pub use node::Node; +pub use style::Style; pub use subgraph::Subgraph; - diff --git a/src/node.rs b/src/node.rs index efbc6f6..ff4b838 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,10 +1,6 @@ /// each node is an index in a vector in the graph. // pub type Node = usize; - -use crate::{ - style::Style, - utils::{quote_string}, -}; +use crate::{style::Style, utils::quote_string}; /// `Graph`'s node #[derive(Clone)] @@ -13,56 +9,72 @@ pub struct Node { label: String, style: Style, color: Option, - shape: Option + shape: Option, + url: String, } impl Node { pub fn new(name: &str) -> Self { - Node { name: new_name(name), label: String::from(name), style: Style::None, color: None, shape: None } + Node { + name: new_name(name), + label: String::from(name), + style: Style::None, + color: None, + shape: None, + url: Default::default(), + } } - pub fn label(&self, label: &str) -> Self { - let mut node = self.clone(); - node.label = String::from(label); - node + pub fn label(mut self, label: &str) -> Self { + self.label = String::from(label); + self } - pub fn style(&self, style: Style) -> Self { - let mut node = self.clone(); - node.style = style; - node + pub fn style(mut self, style: Style) -> Self { + self.style = style; + self } - pub fn shape(&self, shape: Option<&str>) -> Self { - let mut node = self.clone(); + pub fn shape(mut self, shape: Option<&str>) -> Self { match shape { - Some(s) => node.shape = Some(String::from(s)), - None => node.shape = None + Some(s) => self.shape = Some(String::from(s)), + None => self.shape = None, } - node + self } - pub fn color(&self, color: Option<&str>) -> Self { - let mut node = self.clone(); - node.color = match color { + pub fn color(mut self, color: Option<&str>) -> Self { + self.color = match color { Some(c) => Some(String::from(c)), - None => None + None => None, }; - node + self + } + + pub fn url(mut self, url: String) -> Self { + self.url = url; + self } pub fn to_dot_string(&self) -> String { let colorstring: String; - let escaped: String = quote_string(self.label.clone()); + let escaped_label: String = quote_string(self.label.clone()); + let escaped_url: String = quote_string(self.url.clone()); let shape: String; let mut text = vec!["\"", self.name.as_str(), "\""]; text.push("[label="); - text.push(escaped.as_str()); + text.push(escaped_label.as_str()); text.push("]"); + if !self.url.is_empty() { + text.push("[URL="); + text.push(escaped_url.as_str()); + text.push("]"); + } + if self.style != Style::None { text.push("[style=\""); text.push(self.style.as_slice()); @@ -86,7 +98,6 @@ impl Node { text.push(";"); return text.into_iter().collect(); } - } /// Check if the node's name is illegal. @@ -112,7 +123,7 @@ fn new_name(name: &str) -> String { if !chars.all(is_constituent) { panic!("The name of the node should only contain letter/number/underscore/dot") } - return String::from(name); + return String::from(name); fn is_letter_or_underscore_or_dot(c: char) -> bool { in_range('a', c, 'z') || in_range('A', c, 'Z') || c == '_' || c == '.' @@ -123,4 +134,4 @@ fn new_name(name: &str) -> String { fn in_range(low: char, c: char, high: char) -> bool { low as usize <= c as usize && c as usize <= high as usize } -} \ No newline at end of file +} diff --git a/src/style.rs b/src/style.rs index eeb36a6..0694601 100644 --- a/src/style.rs +++ b/src/style.rs @@ -32,4 +32,4 @@ impl Style { Style::Wedged => "wedged", } } -} \ No newline at end of file +} diff --git a/src/subgraph.rs b/src/subgraph.rs index fdff9c6..6c4c9ec 100644 --- a/src/subgraph.rs +++ b/src/subgraph.rs @@ -1,9 +1,4 @@ -use crate::{ - node::Node, - style::Style, - utils::quote_string, Edge, - Kind -}; +use crate::{node::Node, style::Style, utils::quote_string, Edge, Kind}; /// `Graph`'s subgraph #[derive(Clone)] @@ -15,11 +10,21 @@ pub struct Subgraph { style: Style, color: Option, edgeop: String, + url: String, } impl Subgraph { pub fn new(name: &str) -> Self { - Subgraph { name: new_name(name), nodes: vec![], edges: vec![], label: String::new(), style: Style::None, color: None, edgeop: String::from(Kind::Digraph.edgeop())} + Subgraph { + name: new_name(name), + nodes: vec![], + edges: vec![], + label: String::new(), + style: Style::None, + color: None, + edgeop: String::from(Kind::Digraph.edgeop()), + url: Default::default(), + } } pub fn add_node(&mut self, node: Node) -> () { @@ -34,36 +39,45 @@ impl Subgraph { self.edges.push(edge); } - pub fn label(&self, label: &str) -> Self { - let mut subg = self.clone(); - subg.label = String::from(label); - subg + pub fn label(mut self, label: &str) -> Self { + self.label = String::from(label); + self } - pub fn style(&self, style: Style) -> Self { - let mut subg = self.clone(); - subg.style = style; - subg + pub fn style(mut self, style: Style) -> Self { + self.style = style; + self } - pub fn color(&self, color: Option<&str>) -> Self { - let mut subg = self.clone(); - subg.color = match color { + pub fn color(mut self, color: Option<&str>) -> Self { + self.color = match color { Some(c) => Some(String::from(c)), - None => None + None => None, }; - subg + self + } + + pub fn edgeop(mut self, edgeop: &str) -> Self { + self.edgeop = String::from(edgeop); + self } - pub fn edgeop(&self, edgeop: &str) -> Self { - let mut subg = self.clone(); - subg.edgeop = String::from(edgeop); - subg + pub fn url(mut self, url: String) -> Self { + self.url = url; + self } pub fn to_dot_string(&self) -> String { let mut text = vec!["subgraph ", self.name.as_str(), " {\n "]; + let escaped_url: String; + if !self.url.is_empty() { + escaped_url = quote_string(self.url.clone()); + text.push("URL="); + text.push(escaped_url.as_str()); + text.push(";\n "); + } + text.push("label=\""); text.push(self.label.as_str()); text.push("\";\n "); @@ -82,7 +96,8 @@ impl Subgraph { text.push(";\n "); } - let subgraph_node_names = self.nodes + let subgraph_node_names = self + .nodes .iter() .map(|n| n.to_dot_string()) .collect::>() @@ -93,10 +108,10 @@ impl Subgraph { text.push(&subgraph_node_names); text.push("\n "); } - let edge_symbol = &self.edgeop; - let subgraph_edge_strs = self.edges + let subgraph_edge_strs = self + .edges .iter() .map(|e| e.to_dot_string(&edge_symbol)) .collect::>() @@ -109,9 +124,7 @@ impl Subgraph { } text.push("}"); - - return text.into_iter().collect(); } } @@ -143,7 +156,7 @@ fn new_name(name: &str) -> String { if !name.starts_with("cluster_") { panic!("The name of the subgraph should start with \"cluster_\"") } - return String::from(name); + return String::from(name); fn is_letter_or_underscore(c: char) -> bool { in_range('a', c, 'z') || in_range('A', c, 'Z') || c == '_' @@ -154,4 +167,4 @@ fn new_name(name: &str) -> String { fn in_range(low: char, c: char, high: char) -> bool { low as usize <= c as usize && c as usize <= high as usize } -} \ No newline at end of file +} diff --git a/src/utils.rs b/src/utils.rs index b2c5056..6092f6a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,3 +1,3 @@ pub fn quote_string(s: String) -> String { format!("\"{}\"", s) -} \ No newline at end of file +} diff --git a/tests/integration.rs b/tests/integration.rs index 10632e4..90fcc04 100644 --- a/tests/integration.rs +++ b/tests/integration.rs @@ -28,6 +28,19 @@ r#"digraph single_node { "#); } + #[test] + fn single_node_with_url() { + let mut graph = Graph::new("single_node", Kind::Digraph).url("https://example.com/".into()); + let node = Node::new("N0").url("https://example.com/".into()); + graph.add_node(node); + assert_eq!(graph.to_dot_string().unwrap(), +r#"digraph single_node { + URL="https://example.com/" + "N0"[label="N0"][URL="https://example.com/"]; +} +"#); + } + #[test] fn dot_in_node_name() { let mut graph = Graph::new("single_node", Kind::Digraph); @@ -249,7 +262,7 @@ r#"graph g { let mut c1 = Subgraph::new("cluster_0").label(""); c1.add_node(Node::new("N0")); c1.add_node(Node::new("N1")); - let mut c2 = Subgraph::new("cluster_1").label(""); + let mut c2 = Subgraph::new("cluster_1").url("https://example.com/".into()).label(""); c2.add_node(Node::new("N2")); c2.add_node(Node::new("N3")); graph.add_subgraph(c1); @@ -268,6 +281,7 @@ r#"digraph di { "N1"[label="N1"]; } subgraph cluster_1 { + URL="https://example.com/"; label=""; "N2"[label="N2"]; "N3"[label="N3"]; @@ -420,4 +434,52 @@ r#"digraph G { let result = graph.to_dot_string(); result.unwrap(); } + + #[test] + fn single_edge_with_url() { + let mut graph = Graph::new("single_edge", Kind::Digraph); + graph.add_node(Node::new("N0")); + graph.add_node(Node::new("N1")); + graph.add_edge(Edge::new("N0", "N1", "E").url("https://example.com/".into())); + assert_eq!(graph.to_dot_string().unwrap(), +r#"digraph single_edge { + "N0"[label="N0"]; + "N1"[label="N1"]; + "N0" -> "N1"[label="E"][URL="https://example.com/"]; +} +"#); + } + + #[test] + fn single_edge_with_label_url() { + let mut graph = Graph::new("single_edge", Kind::Digraph); + graph.add_node(Node::new("N0")); + graph.add_node(Node::new("N1")); + graph.add_edge(Edge::new("N0", "N1", "E").label_url("https://example.com/".into())); + assert_eq!(graph.to_dot_string().unwrap(), +r#"digraph single_edge { + "N0"[label="N0"]; + "N1"[label="N1"]; + "N0" -> "N1"[label="E"][labelURL="https://example.com/"]; +} +"#); + } + + #[test] + fn single_edge_with_label_url_and_url() { + let mut graph = Graph::new("single_edge", Kind::Digraph); + graph.add_node(Node::new("N0")); + graph.add_node(Node::new("N1")); + graph.add_edge(Edge::new("N0", "N1", "E") + .url("https://example.com/".into()) + .label_url("https://example.com/".into()) + ); + assert_eq!(graph.to_dot_string().unwrap(), +r#"digraph single_edge { + "N0"[label="N0"]; + "N1"[label="N1"]; + "N0" -> "N1"[label="E"][labelURL="https://example.com/"][URL="https://example.com/"]; +} +"#); + } } \ No newline at end of file