summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/attributes/arrow_type.rs50
-rw-r--r--src/attributes/cluster_mode.rs18
-rw-r--r--src/attributes/color.rs165
-rw-r--r--src/attributes/compass_point.rs41
-rw-r--r--src/attributes/direction.rs20
-rw-r--r--src/attributes/image.rs46
-rw-r--r--src/attributes/label.rs34
-rw-r--r--src/attributes/mod.rs1182
-rw-r--r--src/attributes/ordering.rs16
-rw-r--r--src/attributes/output_mode.rs29
-rw-r--r--src/attributes/pack_mode.rs30
-rw-r--r--src/attributes/page_direction.rs33
-rw-r--r--src/attributes/point.rs55
-rw-r--r--src/attributes/port_position.rs66
-rw-r--r--src/attributes/rankdir.rs22
-rw-r--r--src/attributes/ratio.rs22
-rw-r--r--src/attributes/rectangle.rs33
-rw-r--r--src/attributes/shape.rs132
-rw-r--r--src/attributes/spline_type.rs110
-rw-r--r--src/attributes/splines.rs31
-rw-r--r--src/attributes/style.rs92
-rw-r--r--src/dot.rs1498
-rw-r--r--src/lib.rs3852
-rw-r--r--tests/dot.rs307
24 files changed, 4039 insertions, 3845 deletions
diff --git a/src/attributes/arrow_type.rs b/src/attributes/arrow_type.rs
new file mode 100644
index 0000000..ac2d49f
--- /dev/null
+++ b/src/attributes/arrow_type.rs
@@ -0,0 +1,50 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub enum ArrowType {
+ Normal,
+ Dot,
+ Odot,
+ None,
+ Empty,
+ Diamond,
+ Ediamond,
+ Box,
+ Open,
+ Vee,
+ Inv,
+ Invdot,
+ Invodot,
+ Tee,
+ Invempty,
+ Odiamond,
+ Crow,
+ Obox,
+ Halfopen,
+}
+
+impl<'a> DotString<'a> for ArrowType {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ ArrowType::Normal => "normal".into(),
+ ArrowType::Dot => "dot".into(),
+ ArrowType::Odot => "odot".into(),
+ ArrowType::None => "none".into(),
+ ArrowType::Empty => "empty".into(),
+ ArrowType::Diamond => "diamond".into(),
+ ArrowType::Ediamond => "ediamond".into(),
+ ArrowType::Box => "box".into(),
+ ArrowType::Open => "open".into(),
+ ArrowType::Vee => "vee".into(),
+ ArrowType::Inv => "inv".into(),
+ ArrowType::Invdot => "invdot".into(),
+ ArrowType::Invodot => "invodot".into(),
+ ArrowType::Tee => "tee".into(),
+ ArrowType::Invempty => "invempty".into(),
+ ArrowType::Odiamond => "odiamond".into(),
+ ArrowType::Crow => "crow".into(),
+ ArrowType::Obox => "obox".into(),
+ ArrowType::Halfopen => "halfopen".into(),
+ }
+ }
+}
diff --git a/src/attributes/cluster_mode.rs b/src/attributes/cluster_mode.rs
new file mode 100644
index 0000000..03e2bf9
--- /dev/null
+++ b/src/attributes/cluster_mode.rs
@@ -0,0 +1,18 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub enum ClusterMode {
+ Local,
+ Global,
+ None,
+}
+
+impl<'a> DotString<'a> for ClusterMode {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ ClusterMode::Local => "local".into(),
+ ClusterMode::Global => "global".into(),
+ ClusterMode::None => "none".into(),
+ }
+ }
+}
diff --git a/src/attributes/color.rs b/src/attributes/color.rs
new file mode 100644
index 0000000..92a26eb
--- /dev/null
+++ b/src/attributes/color.rs
@@ -0,0 +1,165 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+#[derive(Copy, Clone, PartialEq, Debug)]
+pub enum Color<'a> {
+ RGB {
+ red: u8,
+ green: u8,
+ blue: u8,
+ },
+ RGBA {
+ red: u8,
+ green: u8,
+ blue: u8,
+ alpha: u8,
+ },
+ // TODO: constrain?
+ // Hue-Saturation-Value (HSV) 0.0 <= H,S,V <= 1.0
+ HSV {
+ hue: f32,
+ saturation: f32,
+ value: f32,
+ },
+ Named(&'a str),
+}
+
+impl<'a> DotString<'a> for Color<'a> {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ Color::RGB { red, green, blue } => {
+ format!("#{:02x?}{:02x?}{:02x?}", red, green, blue).into()
+ }
+ Color::RGBA {
+ red,
+ green,
+ blue,
+ alpha,
+ } => {
+ format!("#{:02x?}{:02x?}{:02x?}{:02x?}", red, green, blue, alpha).into()
+ }
+ Color::HSV {
+ hue,
+ saturation,
+ value,
+ } => format!("{} {} {}", hue, saturation, value).into(),
+ Color::Named(color) => (*color).into(),
+ }
+ }
+}
+
+// The sum of the optional weightings must sum to at most 1.
+pub struct WeightedColor<'a> {
+ pub color: Color<'a>,
+
+ // TODO: constrain
+ /// Must be in range 0 <= W <= 1.
+ pub weight: Option<f32>,
+}
+
+impl<'a> DotString<'a> for WeightedColor<'a> {
+ fn dot_string(&self) -> Cow<'a, str> {
+ let mut dot_string = self.color.dot_string().to_string();
+ if let Some(weight) = &self.weight {
+ dot_string.push_str(format!(";{}", weight).as_str());
+ }
+ dot_string.into()
+ }
+}
+
+pub struct ColorList<'a> {
+ pub colors: Vec<WeightedColor<'a>>,
+}
+
+impl<'a> DotString<'a> for ColorList<'a> {
+ /// A colon-separated list of weighted color values: WC(:WC)* where each WC has the form C(;F)?
+ /// Ex: fillcolor=yellow;0.3:blue
+ fn dot_string(&self) -> Cow<'a, str> {
+ let mut dot_string = String::new();
+ let mut iter = self.colors.iter();
+ let first = iter.next();
+ if first.is_none() {
+ return dot_string.into();
+ }
+ dot_string.push_str(&first.unwrap().dot_string());
+ for weighted_color in iter {
+ dot_string.push_str(":");
+ dot_string.push_str(&weighted_color.dot_string())
+ }
+
+ dot_string.into()
+ }
+}
+
+/// Convert an element like `(i, j)` into a WeightedColor
+pub trait IntoWeightedColor<'a> {
+ fn into_weighted_color(self) -> WeightedColor<'a>;
+}
+
+impl<'a> IntoWeightedColor<'a> for &(Color<'a>, Option<f32>) {
+ fn into_weighted_color(self) -> WeightedColor<'a> {
+ let (s, t) = *self;
+ WeightedColor {
+ color: s,
+ weight: t,
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::attributes::{Color, ColorList, WeightedColor};
+ use crate::DotString;
+
+ #[test]
+ fn colorlist_dot_string() {
+ let yellow = WeightedColor {
+ color: Color::Named("yellow"),
+ weight: Some(0.3),
+ };
+
+ let blue = WeightedColor {
+ color: Color::Named("blue"),
+ weight: None,
+ };
+
+ let color_list = ColorList {
+ colors: vec![yellow, blue],
+ };
+
+ let dot_string = color_list.dot_string();
+
+ assert_eq!("yellow;0.3:blue", dot_string);
+ }
+
+ #[test]
+ fn color_rbg_dot_string() {
+ let color = Color::RGB {
+ red: 160,
+ green: 82,
+ blue: 45,
+ };
+ assert_eq!("#a0522d", color.dot_string());
+ }
+
+ #[test]
+ fn color_rbga_dot_string() {
+ let color = Color::RGBA {
+ red: 160,
+ green: 82,
+ blue: 45,
+ alpha: 10,
+ };
+ assert_eq!("#a0522d0a", color.dot_string());
+ }
+
+ #[test]
+ fn color_hsv_dot_string() {
+ let color = Color::HSV {
+ hue: 0.051,
+ saturation: 0.718,
+ value: 0.627,
+ };
+ assert_eq!("0.051 0.718 0.627", color.dot_string());
+ }
+}
diff --git a/src/attributes/compass_point.rs b/src/attributes/compass_point.rs
new file mode 100644
index 0000000..3ebb250
--- /dev/null
+++ b/src/attributes/compass_point.rs
@@ -0,0 +1,41 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+// TODO: not sure we need this enum but should support setting nodeport either via
+// headport / tailport attributes e.g. a -> b [tailport=se]
+// or via edge declaration using the syntax node name:port_name e.g. a -> b:se
+// aka compass
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub enum CompassPoint {
+ N,
+ NE,
+ E,
+ SE,
+ S,
+ SW,
+ W,
+ NW,
+ C,
+ // TODO: none might not be a good name
+ // The compass point "_" specifies that an appropriate side of the port adjacent to the exterior
+ // of the node should be used, if such exists. Otherwise, the center is used.
+ // If no compass point is used with a portname, the default value is "_".
+ None,
+}
+
+impl<'a> DotString<'a> for CompassPoint {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ CompassPoint::N => "n".into(),
+ CompassPoint::NE => "ne".into(),
+ CompassPoint::E => "e".into(),
+ CompassPoint::SE => "se".into(),
+ CompassPoint::S => "s".into(),
+ CompassPoint::SW => "sw".into(),
+ CompassPoint::W => "w".into(),
+ CompassPoint::NW => "nw".into(),
+ CompassPoint::C => "c".into(),
+ CompassPoint::None => "_".into(),
+ }
+ }
+}
diff --git a/src/attributes/direction.rs b/src/attributes/direction.rs
new file mode 100644
index 0000000..bebc3c0
--- /dev/null
+++ b/src/attributes/direction.rs
@@ -0,0 +1,20 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub enum Direction {
+ Forward,
+ Back,
+ Both,
+ None,
+}
+
+impl<'a> DotString<'a> for Direction {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ Direction::Forward => "forward".into(),
+ Direction::Back => "back".into(),
+ Direction::Both => "both".into(),
+ Direction::None => "none".into(),
+ }
+ }
+}
diff --git a/src/attributes/image.rs b/src/attributes/image.rs
new file mode 100644
index 0000000..360191a
--- /dev/null
+++ b/src/attributes/image.rs
@@ -0,0 +1,46 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub enum ImagePosition {
+ TopLeft,
+ TopCentered,
+ TopRight,
+ MiddleLeft,
+ MiddleCentered,
+ MiddleRight,
+ BottomLeft,
+ BottomCentered,
+ BottomRight,
+}
+
+impl<'a> DotString<'a> for ImagePosition {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ ImagePosition::TopLeft => "tl".into(),
+ ImagePosition::TopCentered => "tc".into(),
+ ImagePosition::TopRight => "tr".into(),
+ ImagePosition::MiddleLeft => "ml".into(),
+ ImagePosition::MiddleCentered => "mc".into(),
+ ImagePosition::MiddleRight => "mr".into(),
+ ImagePosition::BottomLeft => "bl".into(),
+ ImagePosition::BottomCentered => "bc".into(),
+ ImagePosition::BottomRight => "br".into(),
+ }
+ }
+}
+
+pub enum ImageScale {
+ Width,
+ Height,
+ Both,
+}
+
+impl<'a> DotString<'a> for ImageScale {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ ImageScale::Width => "width".into(),
+ ImageScale::Height => "height".into(),
+ ImageScale::Both => "both".into(),
+ }
+ }
+}
diff --git a/src/attributes/label.rs b/src/attributes/label.rs
new file mode 100644
index 0000000..e62de2b
--- /dev/null
+++ b/src/attributes/label.rs
@@ -0,0 +1,34 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub enum LabelJustification {
+ Left,
+ Right,
+ Center,
+}
+
+impl<'a> DotString<'a> for LabelJustification {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ LabelJustification::Left => "l".into(),
+ LabelJustification::Right => "r".into(),
+ LabelJustification::Center => "c".into(),
+ }
+ }
+}
+
+pub enum LabelLocation {
+ Top,
+ Center,
+ Bottom,
+}
+
+impl<'a> DotString<'a> for LabelLocation {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ LabelLocation::Top => "t".into(),
+ LabelLocation::Center => "c".into(),
+ LabelLocation::Bottom => "b".into(),
+ }
+ }
+}
diff --git a/src/attributes/mod.rs b/src/attributes/mod.rs
new file mode 100644
index 0000000..d01d246
--- /dev/null
+++ b/src/attributes/mod.rs
@@ -0,0 +1,1182 @@
+mod arrow_type;
+mod cluster_mode;
+mod color;
+mod compass_point;
+mod direction;
+mod image;
+mod label;
+mod ordering;
+mod output_mode;
+mod pack_mode;
+mod page_direction;
+mod point;
+mod port_position;
+mod rankdir;
+mod ratio;
+mod rectangle;
+mod shape;
+mod spline_type;
+mod splines;
+mod style;
+
+pub use crate::attributes::arrow_type::ArrowType;
+pub use crate::attributes::cluster_mode::ClusterMode;
+pub use crate::attributes::color::{Color, ColorList, IntoWeightedColor, WeightedColor};
+pub use crate::attributes::compass_point::CompassPoint;
+pub use crate::attributes::direction::Direction;
+pub use crate::attributes::image::{ImagePosition, ImageScale};
+pub use crate::attributes::label::{LabelJustification, LabelLocation};
+pub use crate::attributes::ordering::Ordering;
+pub use crate::attributes::output_mode::OutputMode;
+pub use crate::attributes::pack_mode::PackMode;
+pub use crate::attributes::page_direction::PageDirection;
+pub use crate::attributes::point::Point;
+pub use crate::attributes::port_position::PortPosition;
+pub use crate::attributes::rankdir::RankDir;
+pub use crate::attributes::ratio::Ratio;
+pub use crate::attributes::rectangle::Rectangle;
+pub use crate::attributes::shape::Shape;
+pub use crate::attributes::spline_type::SplineType;
+pub use crate::attributes::splines::Splines;
+pub use crate::attributes::style::{EdgeStyle, GraphStyle, NodeStyle, Styles};
+pub use crate::attributes::AttributeText::{AttrStr, EscStr, HtmlStr, QuotedStr};
+use crate::dot::DotString;
+use indexmap::map::IndexMap;
+use std::borrow::Cow;
+use std::collections::HashMap;
+
+/// The text for a graphviz label on a node or edge.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub enum AttributeText<'a> {
+ /// Preserves the text directly as is.
+ AttrStr(Cow<'a, str>),
+
+ /// This kind of label uses the graphviz label escString type:
+ /// <http://www.graphviz.org/doc/info/attrs.html#k:escString>
+ ///
+ /// Occurrences of backslashes (`\`) are not escaped; instead they
+ /// are interpreted as initiating an escString escape sequence.
+ ///
+ /// Escape sequences of particular interest: in addition to `\n`
+ /// to break a line (centering the line preceding the `\n`), there
+ /// are also the escape sequences `\l` which left-justifies the
+ /// preceding line and `\r` which right-justifies it.
+ EscStr(Cow<'a, str>),
+
+ /// This uses a graphviz [HTML string label][html].
+ /// The string is printed exactly as given, but between `<` and `>`.
+ /// **No escaping is performed.**
+ ///
+ /// [html]: https://graphviz.org/doc/info/shapes.html#html
+ HtmlStr(Cow<'a, str>),
+
+ /// Preserves the text directly as is but wrapped in quotes.
+ ///
+ /// Occurrences of backslashes (`\`) are escaped, and thus appear
+ /// as backslashes in the rendered label.
+ QuotedStr(Cow<'a, str>),
+}
+
+impl<'a> AttributeText<'a> {
+ pub fn attr<S: Into<Cow<'a, str>>>(s: S) -> AttributeText<'a> {
+ AttrStr(s.into())
+ }
+
+ pub fn escaped<S: Into<Cow<'a, str>>>(s: S) -> AttributeText<'a> {
+ EscStr(s.into())
+ }
+
+ pub fn html<S: Into<Cow<'a, str>>>(s: S) -> AttributeText<'a> {
+ HtmlStr(s.into())
+ }
+
+ pub fn quoted<S: Into<Cow<'a, str>>>(s: S) -> AttributeText<'a> {
+ QuotedStr(s.into())
+ }
+
+ fn escape_char<F>(c: char, mut f: F)
+ where
+ F: FnMut(char),
+ {
+ match c {
+ // not escaping \\, since Graphviz escString needs to
+ // interpret backslashes; see EscStr above.
+ '\\' => f(c),
+ _ => {
+ for c in c.escape_default() {
+ f(c)
+ }
+ }
+ }
+ }
+
+ fn escape_str(s: &str) -> String {
+ let mut out = String::with_capacity(s.len());
+ for c in s.chars() {
+ AttributeText::escape_char(c, |c| out.push(c));
+ }
+ out
+ }
+
+ /// Renders text as string suitable for a attribute in a .dot file.
+ /// This includes quotes or suitable delimiters.
+ pub fn dot_string(&self) -> String {
+ match *self {
+ AttrStr(ref s) => format!("{}", s),
+ EscStr(ref s) => format!("\"{}\"", AttributeText::escape_str(&s)),
+ HtmlStr(ref s) => format!("<{}>", s),
+ QuotedStr(ref s) => format!("\"{}\"", s.escape_default()),
+ }
+ }
+}
+
+impl<'a> From<ArrowType> for AttributeText<'a> {
+ fn from(arrow_type: ArrowType) -> Self {
+ AttributeText::attr(arrow_type.dot_string())
+ }
+}
+
+impl<'a> From<bool> for AttributeText<'a> {
+ fn from(v: bool) -> Self {
+ AttributeText::attr(v.to_string())
+ }
+}
+
+impl<'a> From<ClusterMode> for AttributeText<'a> {
+ fn from(mode: ClusterMode) -> Self {
+ AttributeText::quoted(mode.dot_string())
+ }
+}
+
+impl<'a> From<Color<'a>> for AttributeText<'a> {
+ fn from(color: Color<'a>) -> Self {
+ AttributeText::quoted(color.dot_string())
+ }
+}
+
+impl<'a> From<ColorList<'a>> for AttributeText<'a> {
+ fn from(color_list: ColorList<'a>) -> Self {
+ AttributeText::quoted(color_list.dot_string())
+ }
+}
+
+impl<'a> From<CompassPoint> for AttributeText<'a> {
+ fn from(compass: CompassPoint) -> Self {
+ AttributeText::quoted(compass.dot_string())
+ }
+}
+
+impl<'a> From<Direction> for AttributeText<'a> {
+ fn from(direction: Direction) -> Self {
+ AttributeText::attr(direction.dot_string())
+ }
+}
+
+impl<'a> From<EdgeStyle> for AttributeText<'a> {
+ fn from(style: EdgeStyle) -> Self {
+ AttributeText::attr(style.dot_string())
+ }
+}
+
+impl<'a> From<f32> for AttributeText<'a> {
+ fn from(v: f32) -> Self {
+ AttributeText::attr(v.to_string())
+ }
+}
+
+impl<'a> From<GraphStyle> for AttributeText<'a> {
+ fn from(style: GraphStyle) -> Self {
+ AttributeText::attr(style.dot_string())
+ }
+}
+
+impl<'a> From<ImagePosition> for AttributeText<'a> {
+ fn from(pos: ImagePosition) -> Self {
+ AttributeText::quoted(pos.dot_string())
+ }
+}
+
+impl<'a> From<ImageScale> for AttributeText<'a> {
+ fn from(scale: ImageScale) -> Self {
+ AttributeText::quoted(scale.dot_string())
+ }
+}
+
+impl<'a> From<LabelJustification> for AttributeText<'a> {
+ fn from(label_justification: LabelJustification) -> Self {
+ AttributeText::attr(label_justification.dot_string())
+ }
+}
+
+impl<'a> From<LabelLocation> for AttributeText<'a> {
+ fn from(label_location: LabelLocation) -> Self {
+ AttributeText::attr(label_location.dot_string())
+ }
+}
+
+impl<'a> From<NodeStyle> for AttributeText<'a> {
+ fn from(style: NodeStyle) -> Self {
+ AttributeText::attr(style.dot_string())
+ }
+}
+
+impl<'a> From<Ordering> for AttributeText<'a> {
+ fn from(ordering: Ordering) -> Self {
+ AttributeText::quoted(ordering.dot_string())
+ }
+}
+
+impl<'a> From<OutputMode> for AttributeText<'a> {
+ fn from(mode: OutputMode) -> Self {
+ AttributeText::quoted(mode.dot_string())
+ }
+}
+
+impl<'a> From<PackMode> for AttributeText<'a> {
+ fn from(mode: PackMode) -> Self {
+ AttributeText::quoted(mode.dot_string())
+ }
+}
+
+impl<'a> From<PageDirection> for AttributeText<'a> {
+ fn from(page_direction: PageDirection) -> Self {
+ AttributeText::attr(page_direction.dot_string())
+ }
+}
+
+impl<'a> From<Point> for AttributeText<'a> {
+ fn from(point: Point) -> Self {
+ AttributeText::quoted(point.dot_string())
+ }
+}
+
+impl<'a> From<PortPosition> for AttributeText<'a> {
+ fn from(port_position: PortPosition) -> Self {
+ AttributeText::quoted(port_position.dot_string())
+ }
+}
+
+impl<'a> From<RankDir> for AttributeText<'a> {
+ fn from(rank_dir: RankDir) -> Self {
+ AttributeText::attr(rank_dir.dot_string())
+ }
+}
+
+impl<'a> From<Ratio> for AttributeText<'a> {
+ fn from(ratio: Ratio) -> Self {
+ match ratio {
+ Ratio::Aspect(_aspect) => AttributeText::attr(ratio.dot_string()),
+ _ => AttributeText::quoted(ratio.dot_string()),
+ }
+ }
+}
+
+impl<'a> From<Rectangle> for AttributeText<'a> {
+ fn from(rectangle: Rectangle) -> Self {
+ AttributeText::quoted(rectangle.dot_string())
+ }
+}
+
+impl<'a> From<Shape> for AttributeText<'a> {
+ fn from(shape: Shape) -> Self {
+ AttributeText::attr(shape.dot_string())
+ }
+}
+
+impl<'a> From<Splines> for AttributeText<'a> {
+ fn from(splines: Splines) -> Self {
+ AttributeText::quoted(splines.dot_string())
+ }
+}
+
+impl<'a> From<SplineType> for AttributeText<'a> {
+ fn from(spline_type: SplineType) -> Self {
+ AttributeText::quoted(spline_type.dot_string())
+ }
+}
+
+impl<'a> From<Styles> for AttributeText<'a> {
+ fn from(styles: Styles) -> Self {
+ match styles {
+ Styles::Edge(s) => AttributeText::from(s),
+ Styles::Node(s) => AttributeText::from(s),
+ Styles::Graph(s) => AttributeText::from(s),
+ }
+ }
+}
+
+impl<'a> From<u32> for AttributeText<'a> {
+ fn from(v: u32) -> Self {
+ AttributeText::attr(v.to_string())
+ }
+}
+
+#[derive(Hash, Eq, PartialEq, PartialOrd, Ord, Debug, Clone)]
+pub enum AttributeType {
+ Graph,
+ Node,
+ Edge,
+}
+
+pub trait AttributeStatement<'a> {
+ fn get_attribute_statement_type(&self) -> &'static str;
+
+ fn get_attributes(&self) -> &IndexMap<String, AttributeText<'a>>;
+
+ fn dot_string(&self) -> String {
+ if self.get_attributes().is_empty() {
+ return String::from("");
+ }
+ let mut dot_string = format!("{} [", self.get_attribute_statement_type());
+ let attributes = &self.get_attributes();
+ let mut iter = attributes.iter();
+ let first = iter.next().unwrap();
+ dot_string.push_str(format!("{}={}", first.0, first.1.dot_string()).as_str());
+ for (key, value) in iter {
+ dot_string.push_str(", ");
+ dot_string.push_str(format!("{}={}", key, value.dot_string()).as_str());
+ }
+ dot_string.push_str("];");
+ dot_string.to_string()
+ }
+}
+
+pub trait GraphAttributes<'a> {
+ fn background(&mut self, background: String) -> &mut Self {
+ self.add_attribute("_background", AttributeText::attr(background))
+ }
+
+ /// The color used as the background for entire canvas.
+ fn background_color(&mut self, background_color: Color<'a>) -> &mut Self {
+ self.add_attribute("bgcolor", AttributeText::from(background_color))
+ }
+
+ // TODO: constrain
+ /// The color used as the background for entire canvas with a gradient fill.
+ /// A colon-separated list of weighted color values: WC(:WC)* where each WC has the form C(;F)?
+ /// with C a color value and the optional F a floating-point number, 0 ≤ F ≤ 1.
+ /// The sum of the floating-point numbers in a colorList must sum to at most 1.
+ fn background_colorlist(&mut self, background_colors: ColorList<'a>) -> &mut Self {
+ self.add_attribute("bgcolor", AttributeText::from(background_colors))
+ }
+
+ /// Type: rect which is "%f,%f,%f,%f"
+ /// The rectangle llx,lly,urx,ury gives the coordinates, in points, of the lower-left corner (llx,lly)
+ /// and the upper-right corner (urx,ury).
+ fn bounding_box(&mut self, bounding_box: String) -> &mut Self {
+ self.add_attribute("bb", AttributeText::quoted(bounding_box))
+ }
+
+ /// If true, the drawing is centered in the output canvas.
+ fn center(&mut self, center: bool) -> &mut Self {
+ self.add_attribute("center", AttributeText::from(center))
+ }
+
+ /// Specifies the character encoding used when interpreting string input as a text label.
+ fn charset(&mut self, charset: String) -> &mut Self {
+ self.add_attribute("charset", AttributeText::quoted(charset))
+ }
+
+ /// Classnames to attach to the node, edge, graph, or cluster’s SVG element.
+ /// Combine with stylesheet for styling SVG output using CSS classnames.
+ /// Multiple space-separated classes are supported.
+ fn class(&mut self, class: String) -> &mut Self {
+ Attributes::class(self.get_attributes_mut(), class);
+ self
+ }
+
+ /// Mode used for handling clusters.
+ /// If clusterrank=local, a subgraph whose name begins with cluster is given special treatment.
+ /// The subgraph is laid out separately, and then integrated as a unit into its parent graph,
+ /// with a bounding rectangle drawn about it.
+ /// If the cluster has a label parameter, this label is displayed within the rectangle.
+ /// Note also that there can be clusters within clusters.
+ /// The modes clusterrank=global and clusterrank=none appear to be identical, both turning off the special cluster processing.
+ fn cluster_rank(&mut self, cluster_rank: ClusterMode) -> &mut Self {
+ self.add_attribute("clusterrank", AttributeText::from(cluster_rank))
+ }
+
+ /// This attribute specifies a color scheme namespace: the context for interpreting color names.
+ /// In particular, if a color value has form "xxx" or "//xxx", then the color xxx will be evaluated
+ /// according to the current color scheme. If no color scheme is set, the standard X11 naming is used.
+ /// For example, if colorscheme=bugn9, then color=7 is interpreted as color="/bugn9/7".
+ fn color_scheme(&mut self, color_scheme: String) -> &mut Self {
+ Attributes::color_scheme(self.get_attributes_mut(), color_scheme);
+ self
+ }
+
+ /// Comments are inserted into output. Device-dependent
+ fn comment(&mut self, comment: String) -> &mut Self {
+ Attributes::comment(self.get_attributes_mut(), comment);
+ self
+ }
+
+ fn compound(&mut self, compound: String) -> &mut Self {
+ self.add_attribute("compound", AttributeText::quoted(compound))
+ }
+
+ fn concentrate(&mut self, concentrate: String) -> &mut Self {
+ self.add_attribute("concentrate", AttributeText::quoted(concentrate))
+ }
+
+ /// Specifies the expected number of pixels per inch on a display device.
+ /// Also known as resolution
+ fn dpi(&mut self, dpi: f32) -> &mut Self {
+ self.add_attribute("dpi", AttributeText::from(dpi))
+ }
+
+ /// Color used to fill the background of a node or cluster assuming style=filled, or a filled arrowhead.
+ fn fill_color(&mut self, fill_color: Color<'a>) -> &mut Self {
+ Attributes::fill_color(self.get_attributes_mut(), fill_color);
+ self
+ }
+
+ /// Color used to fill the background, with a gradient, of a node or cluster assuming
+ /// style=filled, or a filled arrowhead.
+ fn fill_color_with_colorlist(&mut self, fill_colors: ColorList<'a>) -> &mut Self {
+ Attributes::fill_color_with_colorlist(self.get_attributes_mut(), fill_colors);
+ self
+ }
+
+ /// Color used to fill the background, with a gradient, of a node or cluster assuming
+ /// style=filled, or a filled arrowhead.
+ /// TODO: example
+ /// [crate::GraphAttributes::dpi]
+ fn fill_color_with_iter<I>(&mut self, fill_colors: I) -> &mut Self
+ where
+ I: IntoIterator,
+ I::Item: IntoWeightedColor<'a>,
+ {
+ Attributes::fill_color_with_iter(self.get_attributes_mut(), fill_colors);
+ self
+ }
+
+ /// Color used for text.
+ fn font_color(&mut self, font_color: Color<'a>) -> &mut Self {
+ Attributes::font_color(self.get_attributes_mut(), font_color);
+ self
+ }
+
+ /// Font used for text.
+ fn font_name(&mut self, font_name: String) -> &mut Self {
+ Attributes::font_name(self.get_attributes_mut(), font_name);
+ self
+ }
+
+ fn font_names(&mut self, font_names: String) -> &mut Self {
+ self.add_attribute("fontnames", AttributeText::quoted(font_names))
+ }
+
+ fn font_path(&mut self, font_path: String) -> &mut Self {
+ self.add_attribute("fontpath", AttributeText::quoted(font_path))
+ }
+
+ // TODO: constrain
+ /// Font size, in points, used for text.
+ /// default: 14.0, minimum: 1.0
+ fn font_size(&mut self, font_size: f32) -> &mut Self {
+ Attributes::font_size(self.get_attributes_mut(), font_size);
+ self
+ }
+
+ fn force_label(&mut self, force_label: bool) -> &mut Self {
+ self.add_attribute("forcelabel", AttributeText::from(force_label))
+ }
+
+ /// If a gradient fill is being used, this determines the angle of the fill.
+ fn gradient_angle(&mut self, gradient_angle: u32) -> &mut Self {
+ Attributes::gradient_angle(self.get_attributes_mut(), gradient_angle);
+ self
+ }
+
+ fn image_path(&mut self, image_path: String) -> &mut Self {
+ self.add_attribute("imagepath", AttributeText::escaped(image_path))
+ }
+
+ /// An escString or an HTML label.
+ fn label(&mut self, label: String) -> &mut Self {
+ Attributes::label(self.get_attributes_mut(), label);
+ self
+ }
+
+ /// If labeljust=r, the label is right-justified within bounding rectangle
+ /// If labeljust=l, left-justified
+ /// Else the label is centered.
+ fn label_justification(
+ &mut self,
+ label_justification: LabelJustification,
+ ) -> &mut Self {
+ self.add_attribute("labeljust", AttributeText::from(label_justification))
+ }
+
+ // Vertical placement of labels for nodes, root graphs and clusters.
+ // For graphs and clusters, only labelloc=t and labelloc=b are allowed, corresponding to
+ // placement at the top and bottom, respectively.
+ // By default, root graph labels go on the bottom and cluster labels go on the top.
+ // Note that a subgraph inherits attributes from its parent. Thus, if the root graph sets
+ // labelloc=b, the subgraph inherits this value.
+ // For nodes, this attribute is used only when the height of the node is larger than the height
+ // of its label.
+ // If labelloc=t, labelloc=c, labelloc=b, the label is aligned with the top, centered, or
+ // aligned with the bottom of the node, respectively.
+ // By default, the label is vertically centered.
+ fn label_location(&mut self, label_location: LabelLocation) -> &mut Self {
+ Attributes::label_location(self.get_attributes_mut(), label_location);
+ self
+ }
+
+ fn landscape(&mut self, landscape: bool) -> &mut Self {
+ self.add_attribute("landscape", AttributeText::from(landscape))
+ }
+
+ /// Specifies the separator characters used to split an attribute of type layerRange into a
+ /// list of ranges.
+ fn layer_list_sep(&mut self, layer_list_sep: String) -> &mut Self {
+ self.add_attribute("layerlistsep", AttributeText::attr(layer_list_sep))
+ }
+
+ /// Specifies a linearly ordered list of layer names attached to the graph
+ /// The graph is then output in separate layers.
+ /// Only those components belonging to the current output layer appear.
+ fn layers(&mut self, layers: String) -> &mut Self {
+ Attributes::layer(self.get_attributes_mut(), layers);
+ self
+ }
+
+ /// Selects a list of layers to be emitted.
+ fn layer_select(&mut self, layer_select: String) -> &mut Self {
+ self.add_attribute("layerselect", AttributeText::attr(layer_select))
+ }
+
+ /// Specifies the separator characters used to split the layers attribute into a list of layer names.
+ /// default: ":\t "
+ fn layer_sep(&mut self, layer_sep: String) -> &mut Self {
+ self.add_attribute("layersep", AttributeText::attr(layer_sep))
+ }
+
+ /// Height of graph or cluster label, in inches.
+ fn lheight(&mut self, lheight: f32) -> &mut Self {
+ self.add_attribute("lheight", AttributeText::from(lheight))
+ }
+
+ /// Label position
+ /// The position indicates the center of the label.
+ fn label_position(&mut self, lp: Point) -> &mut Self {
+ Attributes::label_position(self.get_attributes_mut(), lp);
+ self
+ }
+
+ /// Width of graph or cluster label, in inches.
+ fn lwidth(&mut self, lwidth: f32) -> &mut Self {
+ self.add_attribute("lwidth", AttributeText::from(lwidth))
+ }
+
+ /// Sets x and y margins of canvas, in inches.
+ /// Both margins are set equal to the given value.
+ /// See [`crate::GraphAttributes::margin_point`]
+ fn margin(&mut self, margin: f32) -> &mut Self {
+ self.margin_point(Point::new_2d(margin, margin))
+ }
+
+ /// Sets x and y margins of canvas, in inches.
+ /// Note that the margin is not part of the drawing but just empty space left around the drawing.
+ /// The margin basically corresponds to a translation of drawing, as would be necessary to
+ /// center a drawing on a page. Nothing is actually drawn in the margin.
+ /// To actually extend the background of a drawing, see the pad attribute.
+ /// Whilst it is possible to create a Point value with either a third co-ordinate
+ /// or a forced position, these are ignored for printing.
+ /// By default, the value is 0.11,0.055.
+ fn margin_point(&mut self, margin: Point) -> &mut Self {
+ Attributes::margin(self.get_attributes_mut(), margin);
+ self
+ }
+
+ /// Multiplicative scale factor used to alter the MinQuit (default = 8) and
+ /// MaxIter (default = 24) parameters used during crossing minimization.
+ /// These correspond to the number of tries without improvement before quitting and the
+ /// maximum number of iterations in each pass.
+ fn mclimit(&mut self, mclimit: f32) -> &mut Self {
+ self.add_attribute("mclimit", AttributeText::from(mclimit))
+ }
+
+ /// Specifies the minimum separation between all nodes.
+ fn mindist(&mut self, mindist: u32) -> &mut Self {
+ self.add_attribute("mindist", AttributeText::from(mindist))
+ }
+
+ /// Whether to use a single global ranking, ignoring clusters.
+ /// The original ranking algorithm in dot is recursive on clusters.
+ /// This can produce fewer ranks and a more compact layout, but sometimes at the cost of a
+ /// head node being place on a higher rank than the tail node.
+ /// It also assumes that a node is not constrained in separate, incompatible subgraphs.
+ /// For example, a node cannot be in a cluster and also be constrained by rank=same with
+ /// a node not in the cluster.
+ /// This allows nodes to be subject to multiple constraints.
+ /// Rank constraints will usually take precedence over edge constraints.
+ fn newrank(&mut self, newrank: bool) -> &mut Self {
+ self.add_attribute("newrank", AttributeText::from(newrank))
+ }
+
+ // TODO: add constraint
+ /// specifies the minimum space between two adjacent nodes in the same rank, in inches.
+ /// default: 0.25, minimum: 0.02
+ fn nodesep(&mut self, nodesep: f32) -> &mut Self {
+ self.add_attribute("nodesep", AttributeText::from(nodesep))
+ }
+
+ /// By default, the justification of multi-line labels is done within the largest context that makes sense.
+ /// Thus, in the label of a polygonal node, a left-justified line will align with the left side
+ /// of the node (shifted by the prescribed margin).
+ /// In record nodes, left-justified line will line up with the left side of the enclosing column
+ /// of fields.
+ /// If nojustify=true, multi-line labels will be justified in the context of itself.
+ /// For example, if nojustify is set, the first label line is long, and the second is shorter
+ /// and left-justified,
+ /// the second will align with the left-most character in the first line, regardless of how
+ /// large the node might be.
+ fn no_justify(&mut self, no_justify: bool) -> &mut Self {
+ Attributes::no_justify(self.get_attributes_mut(), no_justify);
+ self
+ }
+
+ /// Sets number of iterations in network simplex applications.
+ /// nslimit is used in computing node x coordinates.
+ /// If defined, # iterations = nslimit * # nodes; otherwise, # iterations = MAXINT.
+ fn nslimit(&mut self, nslimit: f32) -> &mut Self {
+ self.add_attribute("nslimit", AttributeText::from(nslimit))
+ }
+
+ /// If ordering="out", then the outedges of a node, that is, edges with the node as its tail
+ /// node, must appear left-to-right in the same order in which they are defined in the input.
+ ///
+ /// If ordering="in", then the inedges of a node must appear left-to-right in the same order in
+ /// which they are defined in the input.
+ ///
+ /// If defined as a graph or subgraph attribute, the value is applied to all nodes in the graph
+ /// or subgraph.
+ ///
+ /// Note that the graph attribute takes precedence over the node attribute.
+ fn ordering(&mut self, ordering: Ordering) -> &mut Self {
+ Attributes::ordering(self.get_attributes_mut(), ordering);
+ self
+ }
+
+ // TODO: constrain to 0 - 360. Docs say min is 360 which should be max right?
+ /// When used on nodes: Angle, in degrees, to rotate polygon node shapes.
+ /// For any number of polygon sides, 0 degrees rotation results in a flat base.
+ /// When used on graphs: If "[lL]*", sets graph orientation to landscape.
+ /// Used only if rotate is not defined.
+ /// Default: 0.0 and minimum: 360.0
+ fn orientation(&mut self, orientation: f32) -> &mut Self {
+ Attributes::orientation(self.get_attributes_mut(), orientation);
+ self
+ }
+
+ /// Specify order in which nodes and edges are drawn.
+ /// default: breadthfirst
+ fn output_order(&mut self, output_order: OutputMode) -> &mut Self {
+ self.add_attribute("outputorder", AttributeText::from(output_order))
+ }
+
+ /// Whether each connected component of the graph should be laid out separately, and then the
+ /// graphs packed together.
+ /// If false, the entire graph is laid out together.
+ /// The granularity and method of packing is influenced by the packmode attribute.
+ fn pack(&mut self, pack: bool) -> &mut Self {
+ self.add_attribute("pack", AttributeText::from(pack))
+ }
+
+ // TODO: constrain to non-negative integer.
+ /// Whether each connected component of the graph should be laid out separately, and then
+ /// the graphs packed together.
+ /// This is used as the size, in points,of a margin around each part; otherwise, a default
+ /// margin of 8 is used.
+ /// pack is treated as true if the value of pack iso a non-negative integer.
+ fn pack_int(&mut self, pack: u32) -> &mut Self {
+ self.add_attribute("pack", AttributeText::from(pack))
+ }
+
+ /// This indicates how connected components should be packed (cf. packMode).
+ /// Note that defining packmode will automatically turn on packing as though one had set pack=true.
+ fn pack_mode(&mut self, pack_mode: PackMode) -> &mut Self {
+ self.add_attribute("packmode", AttributeText::from(pack_mode))
+ }
+
+ /// Specifies how much, in inches, to extend the drawing area around the minimal area needed
+ /// to draw the graph.
+ /// Both the x and y pad values are set equal to the given value.
+ /// See [`crate::GraphAttributes::pad_point`]
+ fn pad(&mut self, pad: f32) -> &mut Self {
+ self.pad_point(Point::new_2d(pad, pad))
+ }
+
+ /// Specifies how much, in inches, to extend the drawing area around the minimal area needed to
+ /// draw the graph.
+ /// This area is part of the drawing and will be filled with the background color, if appropriate.
+ /// default: 0.0555
+ fn pad_point(&mut self, pad: Point) -> &mut Self {
+ self.add_attribute("pad", AttributeText::from(pad))
+ }
+
+ /// Width and height of output pages, in inches.
+ /// Value given is used for both the width and height.
+ fn page(&mut self, page: f32) -> &mut Self {
+ self.add_attribute("page", AttributeText::from(page))
+ }
+
+ /// Width and height of output pages, in inches.
+ fn page_point(&mut self, page: Point) -> &mut Self {
+ self.add_attribute("page", AttributeText::from(page))
+ }
+
+ /// The order in which pages are emitted.
+ /// Used only if page is set and applicable.
+ /// Limited to one of the 8 row or column major orders.
+ fn page_dir(&mut self, page_dir: PageDirection) -> &mut Self {
+ self.add_attribute("pagedir", AttributeText::from(page_dir))
+ }
+
+ // TODO: constrain
+ /// If quantum > 0.0, node label dimensions will be rounded to integral multiples of the quantum.
+ /// default: 0.0, minimum: 0.0
+ fn quantum(&mut self, quantum: f32) -> &mut Self {
+ self.add_attribute("quantum", AttributeText::from(quantum))
+ }
+
+ /// Sets direction of graph layout.
+ /// For example, if rankdir="LR", and barring cycles, an edge T -> H; will go from left to right.
+ /// By default, graphs are laid out from top to bottom.
+ /// This attribute also has a side-effect in determining how record nodes are interpreted.
+ /// See record shapes.
+ fn rank_dir(&mut self, rank_dir: RankDir) -> &mut Self {
+ self.add_attribute("rankdir", AttributeText::from(rank_dir))
+ }
+
+ /// sets the desired rank separation, in inches.
+ /// This is the minimum vertical distance between the bottom of the nodes in one rank
+ /// and the tops of nodes in the next. If the value contains equally,
+ /// the centers of all ranks are spaced equally apart.
+ /// Note that both settings are possible, e.g., ranksep="1.2 equally".
+ fn rank_sep(&mut self, rank_sep: String) -> &mut Self {
+ self.add_attribute("ranksep", AttributeText::attr(rank_sep))
+ }
+
+ /// Sets the aspect ratio (drawing height/drawing width) for the drawing.
+ /// Note that this is adjusted before the size attribute constraints are enforced.
+ fn ratio(&mut self, ratio: Ratio) -> &mut Self {
+ self.add_attribute("ratio", AttributeText::from(ratio))
+ }
+
+ /// If true and there are multiple clusters, run crossing minimization a second time.
+ fn remincross(&mut self, remincross: bool) -> &mut Self {
+ self.add_attribute("remincross", AttributeText::from(remincross))
+ }
+
+ /// If rotate=90, sets drawing orientation to landscape.
+ fn rotate(&mut self, rotate: u32) -> &mut Self {
+ self.add_attribute("rotate", AttributeText::from(rotate))
+ }
+
+ // TODO: constrain
+ /// Print guide boxes in PostScript at the beginning of routesplines if showboxes=1, or at
+ /// the end if showboxes=2.
+ /// (Debugging, TB mode only!)
+ /// default: 0, minimum: 0
+ fn show_boxes(&mut self, show_boxes: u32) -> &mut Self {
+ Attributes::show_boxes(self.get_attributes_mut(), show_boxes);
+ self
+ }
+
+ /// Maximum width and height of drawing, in inches.
+ /// Value used for both the width and the height.
+ /// If defined and the drawing is larger than the given size, the drawing
+ /// is uniformly scaled down so that it fits within the given size.
+ /// If desired_min is true, and both both dimensions of the drawing
+ /// are less than size, the drawing is scaled up uniformly until at
+ /// least one dimension equals its dimension in size.
+ /// See [`crate::GraphAttributes::size_point`]
+ fn size(&mut self, size: u32, desired_min: bool) -> &mut Self {
+ self.size_point(Point {
+ x: size as f32,
+ y: size as f32,
+ z: None,
+ force_pos: desired_min,
+ })
+ }
+
+ /// Maximum width and height of drawing, in inches.
+ /// If defined and the drawing is larger than the given size, the drawing
+ /// is uniformly scaled down so that it fits within the given size.
+ /// If desired_min is true, and both both dimensions of the drawing
+ /// are less than size, the drawing is scaled up uniformly until at
+ /// least one dimension equals its dimension in size.
+ fn size_point(&mut self, size: Point) -> &mut Self {
+ self.add_attribute("size", AttributeText::from(size))
+ }
+
+ /// If packmode indicates an array packing, sortv specifies an insertion order
+ /// among the components, with smaller values inserted first.
+ /// default: 0, minimum: 0
+ fn sortv(&mut self, sortv: u32) -> &mut Self {
+ Attributes::sortv(self.get_attributes_mut(), sortv);
+ self
+ }
+
+ /// Controls how, and if, edges are represented.
+ fn splines(&mut self, splines: Splines) -> &mut Self {
+ self.add_attribute("splines", AttributeText::from(splines))
+ }
+
+ /// Set style information for components of the graph.
+ fn style(&mut self, style: GraphStyle) -> &mut Self {
+ Attributes::style(self.get_attributes_mut(), Styles::Graph(style));
+ self
+ }
+
+ /// A URL or pathname specifying an XML style sheet, used in SVG output.
+ /// Combine with class to style elements using CSS selectors.
+ fn stylesheet(&mut self, stylesheet: String) -> &mut Self {
+ self.add_attribute("stylesheet", AttributeText::attr(stylesheet))
+ }
+
+ /// If the object has a URL, this attribute determines which window of the browser is used for the URL.
+ fn target(&mut self, target: String) -> &mut Self {
+ Attributes::target(self.get_attributes_mut(), target);
+ self
+ }
+
+ /// Whether internal bitmap rendering relies on a truecolor color model or uses a color palette.
+ /// If truecolor is unset, truecolor is not used unless there is a shapefile property
+ /// for some node in the graph.
+ /// The output model will use the input model when possible.
+ fn true_color(&mut self, true_color: bool) -> &mut Self {
+ self.add_attribute("truecolor", AttributeText::from(true_color))
+ }
+
+ /// Hyperlinks incorporated into device-dependent output.
+ fn url(&mut self, url: String) -> &mut Self {
+ Attributes::url(self.get_attributes_mut(), url);
+ self
+ }
+
+ // TODO: add a ViewPort Struct?
+ /// Clipping window on final drawing.
+ /// viewport supersedes any size attribute.
+ /// The width and height of the viewport specify precisely the final size of the output.
+ /// The viewPort W,H,Z,x,y or W,H,Z,N specifies a viewport for the final image.
+ /// The pair (W,H) gives the dimensions (width and height) of the final image, in points.
+ /// The optional Z is the zoom factor, i.e., the image in the original layout will be
+ /// W/Z by H/Z points in size. By default, Z is 1.
+ /// The optional last part is either a pair (x,y) giving a position in the original layout
+ /// of the graph,
+ /// in points, of the center of the viewport, or the name N of a node whose center should used
+ /// as the focus.
+ fn viewport(&mut self, viewport: String) -> &mut Self {
+ self.add_attribute("viewport", AttributeText::attr(viewport))
+ }
+
+ /// Add an attribute to the node.
+ fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self;
+
+ /// Add multiple attributes to the node.
+ fn add_attributes(
+ &'a mut self,
+ attributes: HashMap<String, AttributeText<'a>>,
+ ) -> &mut Self;
+
+ fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>>;
+}
+
+impl<'a> GraphAttributes<'a> for GraphAttributeStatementBuilder<'a> {
+ fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self {
+ self.attributes.insert(key.into(), value);
+ self
+ }
+
+ /// Add multiple attributes to the node.
+ fn add_attributes(
+ &'a mut self,
+ attributes: HashMap<String, AttributeText<'a>>,
+ ) -> &mut Self {
+ self.attributes.extend(attributes);
+ self
+ }
+
+ fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> {
+ &mut self.attributes
+ }
+}
+
+// I'm not a huge fan of needing this builder but having a hard time getting around &mut without it
+pub struct GraphAttributeStatementBuilder<'a> {
+ pub attributes: IndexMap<String, AttributeText<'a>>,
+}
+
+impl<'a> GraphAttributeStatementBuilder<'a> {
+ pub fn new() -> Self {
+ Self {
+ attributes: IndexMap::new(),
+ }
+ }
+
+ pub fn build(&self) -> GraphAttributeStatement<'a> {
+ GraphAttributeStatement {
+ attributes: self.attributes.clone(),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct GraphAttributeStatement<'a> {
+ pub attributes: IndexMap<String, AttributeText<'a>>,
+}
+
+impl<'a> GraphAttributeStatement<'a> {
+ pub fn new() -> Self {
+ Self {
+ attributes: IndexMap::new(),
+ }
+ }
+
+ pub fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self {
+ self.attributes.insert(key.into(), value);
+ self
+ }
+}
+
+impl<'a> AttributeStatement<'a> for GraphAttributeStatement<'a> {
+ fn get_attribute_statement_type(&self) -> &'static str {
+ "graph"
+ }
+
+ fn get_attributes(&self) -> &IndexMap<String, AttributeText<'a>> {
+ &self.attributes
+ }
+}
+
+pub(crate) struct Attributes;
+impl Attributes {
+ pub fn class(attributes: &mut IndexMap<String, AttributeText>, class: String) {
+ Self::add_attribute(attributes, "class", AttributeText::quoted(class))
+ }
+
+ pub fn color<'a>(
+ attributes: &mut IndexMap<String, AttributeText<'a>>,
+ color: Color<'a>,
+ ) {
+ Self::add_attribute(attributes, "color", AttributeText::from(color))
+ }
+
+ pub fn color_with_colorlist<'a>(
+ attributes: &mut IndexMap<String, AttributeText<'a>>,
+ color: ColorList<'a>,
+ ) {
+ Self::add_attribute(attributes, "color", AttributeText::from(color))
+ }
+
+ pub fn color_scheme(
+ attributes: &mut IndexMap<String, AttributeText>,
+ color_scheme: String,
+ ) {
+ Self::add_attribute(
+ attributes,
+ "colorscheme",
+ AttributeText::quoted(color_scheme),
+ )
+ }
+
+ pub fn comment(attributes: &mut IndexMap<String, AttributeText>, comment: String) {
+ Self::add_attribute(attributes, "comment", AttributeText::quoted(comment))
+ }
+
+ pub fn fill_color<'a>(
+ attributes: &mut IndexMap<String, AttributeText<'a>>,
+ fill_color: Color<'a>,
+ ) {
+ Self::add_attribute(attributes, "fillcolor", AttributeText::from(fill_color))
+ }
+
+ pub fn fill_color_with_colorlist<'a>(
+ attributes: &mut IndexMap<String, AttributeText<'a>>,
+ fill_colors: ColorList<'a>,
+ ) {
+ Self::add_attribute(attributes, "fillcolor", AttributeText::from(fill_colors))
+ }
+
+ pub fn fill_color_with_iter<'a, I>(
+ attributes: &mut IndexMap<String, AttributeText<'a>>,
+ fill_colors: I,
+ ) where
+ I: IntoIterator,
+ I::Item: IntoWeightedColor<'a>,
+ {
+ let colors: Vec<WeightedColor> = fill_colors
+ .into_iter()
+ .map(|e| e.into_weighted_color())
+ .collect();
+
+ let color_list = ColorList { colors };
+
+ Self::add_attribute(attributes, "fillcolor", AttributeText::from(color_list))
+ }
+
+ pub fn font_color<'a>(
+ attributes: &mut IndexMap<String, AttributeText<'a>>,
+ font_color: Color<'a>,
+ ) {
+ Self::add_attribute(attributes, "fontcolor", AttributeText::from(font_color))
+ }
+
+ pub fn font_name(
+ attributes: &mut IndexMap<String, AttributeText>,
+ font_name: String,
+ ) {
+ Self::add_attribute(attributes, "fontname", AttributeText::quoted(font_name))
+ }
+
+ pub fn font_size(attributes: &mut IndexMap<String, AttributeText>, font_size: f32) {
+ Self::add_attribute(attributes, "fontsize", AttributeText::from(font_size))
+ }
+
+ pub fn gradient_angle(
+ attributes: &mut IndexMap<String, AttributeText>,
+ gradient_angle: u32,
+ ) {
+ Self::add_attribute(
+ attributes,
+ "gradientangle",
+ AttributeText::from(gradient_angle),
+ )
+ }
+
+ pub fn label(attributes: &mut IndexMap<String, AttributeText>, text: String) {
+ Self::add_attribute(attributes, "label", AttributeText::quoted(text));
+ }
+
+ pub fn label_location(
+ attributes: &mut IndexMap<String, AttributeText>,
+ label_location: LabelLocation,
+ ) {
+ Self::add_attribute(attributes, "labelloc", AttributeText::from(label_location))
+ }
+
+ // TODO: layer struct
+ pub fn layer(attributes: &mut IndexMap<String, AttributeText>, layer: String) {
+ Self::add_attribute(attributes, "layer", AttributeText::attr(layer))
+ }
+
+ pub fn label_position(attributes: &mut IndexMap<String, AttributeText>, lp: Point) {
+ Self::add_attribute(attributes, "lp", AttributeText::from(lp))
+ }
+
+ pub fn margin(attributes: &mut IndexMap<String, AttributeText>, margin: Point) {
+ Self::add_attribute(attributes, "margin", AttributeText::from(margin))
+ }
+
+ pub fn no_justify(
+ attributes: &mut IndexMap<String, AttributeText>,
+ no_justify: bool,
+ ) {
+ Self::add_attribute(attributes, "nojustify", AttributeText::from(no_justify))
+ }
+
+ pub fn ordering(
+ attributes: &mut IndexMap<String, AttributeText>,
+ ordering: Ordering,
+ ) {
+ Self::add_attribute(attributes, "ordering", AttributeText::from(ordering))
+ }
+
+ pub fn orientation(
+ attributes: &mut IndexMap<String, AttributeText>,
+ orientation: f32,
+ ) {
+ Self::add_attribute(attributes, "orientation", AttributeText::from(orientation))
+ }
+
+ pub fn pen_width(attributes: &mut IndexMap<String, AttributeText>, pen_width: f32) {
+ Self::add_attribute(attributes, "penwidth", AttributeText::from(pen_width))
+ }
+
+ // TODO: splinetype
+ pub fn pos(attributes: &mut IndexMap<String, AttributeText>, pos: Point) {
+ Self::add_attribute(attributes, "pos", AttributeText::from(pos))
+ }
+
+ pub fn show_boxes(
+ attributes: &mut IndexMap<String, AttributeText>,
+ show_boxes: u32,
+ ) {
+ Self::add_attribute(attributes, "showboxes", AttributeText::from(show_boxes))
+ }
+
+ pub fn sortv(attributes: &mut IndexMap<String, AttributeText>, sortv: u32) {
+ Self::add_attribute(attributes, "sortv", AttributeText::from(sortv))
+ }
+
+ pub fn style(attributes: &mut IndexMap<String, AttributeText>, style: Styles) {
+ Self::add_attribute(attributes, "style", AttributeText::from(style))
+ }
+
+ pub fn target(attributes: &mut IndexMap<String, AttributeText>, target: String) {
+ Self::add_attribute(attributes, "target", AttributeText::escaped(target))
+ }
+
+ pub fn tooltip(attributes: &mut IndexMap<String, AttributeText>, tooltip: String) {
+ Self::add_attribute(attributes, "tooltip", AttributeText::escaped(tooltip))
+ }
+
+ pub fn url(attributes: &mut IndexMap<String, AttributeText>, url: String) {
+ Self::add_attribute(attributes, "url", AttributeText::escaped(url))
+ }
+
+ pub fn xlabel(attributes: &mut IndexMap<String, AttributeText>, width: String) {
+ Self::add_attribute(attributes, "xlabel", AttributeText::escaped(width))
+ }
+
+ pub fn xlp(attributes: &mut IndexMap<String, AttributeText>, xlp: Point) {
+ Self::add_attribute(attributes, "xlp", AttributeText::from(xlp))
+ }
+
+ pub fn add_attribute<'a, S: Into<String>>(
+ attributes: &mut IndexMap<String, AttributeText<'a>>,
+ key: S,
+ value: AttributeText<'a>,
+ ) {
+ attributes.insert(key.into(), value);
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::attributes::{
+ AttributeStatement, Color, GraphAttributeStatementBuilder, GraphAttributes,
+ };
+
+ #[test]
+ fn graph_attribute_colorlist_vec_dot_string() {
+ let graph_attributes = GraphAttributeStatementBuilder::new()
+ .fill_color_with_iter(&[
+ (Color::Named("yellow"), Some(0.3)),
+ (Color::Named("blue"), None),
+ ])
+ .build();
+
+ assert_eq!(
+ "graph [fillcolor=\"yellow;0.3:blue\"];",
+ graph_attributes.dot_string()
+ );
+ }
+}
diff --git a/src/attributes/ordering.rs b/src/attributes/ordering.rs
new file mode 100644
index 0000000..1cd13b6
--- /dev/null
+++ b/src/attributes/ordering.rs
@@ -0,0 +1,16 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub enum Ordering {
+ In,
+ Out,
+}
+
+impl<'a> DotString<'a> for Ordering {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ Ordering::In => "in".into(),
+ Ordering::Out => "out".into(),
+ }
+ }
+}
diff --git a/src/attributes/output_mode.rs b/src/attributes/output_mode.rs
new file mode 100644
index 0000000..5a6ab45
--- /dev/null
+++ b/src/attributes/output_mode.rs
@@ -0,0 +1,29 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+/// These specify the order in which nodes and edges are drawn in concrete output.
+///
+/// The default "breadthfirst" is the simplest, but when the graph layout does not avoid edge-node
+/// overlap, this mode will sometimes have edges drawn over nodes and sometimes on top of nodes.
+///
+/// If the mode "nodesfirst" is chosen, all nodes are drawn first, followed by the edges.
+/// This guarantees an edge-node overlap will not be mistaken for an edge ending at a node.
+///
+/// On the other hand, usually for aesthetic reasons, it may be desirable that all edges appear
+/// beneath nodes, even if the resulting drawing is ambiguous.
+/// This can be achieved by choosing "edgesfirst".
+pub enum OutputMode {
+ BreadthFirst,
+ NodesFirst,
+ EdgesFirst,
+}
+
+impl<'a> DotString<'a> for OutputMode {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ OutputMode::BreadthFirst => "breadthfirst".into(),
+ OutputMode::NodesFirst => "nodesfirst".into(),
+ OutputMode::EdgesFirst => "edgesfirst".into(),
+ }
+ }
+}
diff --git a/src/attributes/pack_mode.rs b/src/attributes/pack_mode.rs
new file mode 100644
index 0000000..3fb086e
--- /dev/null
+++ b/src/attributes/pack_mode.rs
@@ -0,0 +1,30 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+/// The modes "node", "clust" or "graph" specify that the components should be packed together
+/// tightly, using the specified granularity.
+pub enum PackMode {
+ /// causes packing at the node and edge level, with no overlapping of these objects.
+ /// This produces a layout with the least area, but it also allows interleaving,
+ /// where a node of one component may lie between two nodes in another component.
+ Node,
+
+ /// guarantees that top-level clusters are kept intact.
+ /// What effect a value has also depends on the layout algorithm.
+ Cluster,
+
+ /// does a packing using the bounding box of the component.
+ /// Thus, there will be a rectangular region around a component free of elements of any other component.
+ Graph,
+ // TODO: array - "array(_flags)?(%d)?"
+}
+
+impl<'a> DotString<'a> for PackMode {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ PackMode::Node => "node".into(),
+ PackMode::Cluster => "clust".into(),
+ PackMode::Graph => "graph".into(),
+ }
+ }
+}
diff --git a/src/attributes/page_direction.rs b/src/attributes/page_direction.rs
new file mode 100644
index 0000000..df501ba
--- /dev/null
+++ b/src/attributes/page_direction.rs
@@ -0,0 +1,33 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+/// These specify the 8 row or column major orders for traversing a rectangular array,
+/// the first character corresponding to the major order and the second to the minor order.
+/// Thus, for “BL”, the major order is from bottom to top, and the minor order is from left to right.
+/// This means the bottom row is traversed first, from left to right, then the next row up,
+/// from left to right, and so on, until the topmost row is traversed
+pub enum PageDirection {
+ BottomLeft,
+ BottomRight,
+ TopLeft,
+ TopRight,
+ RightBottom,
+ RightTop,
+ LeftBottom,
+ LeftTop,
+}
+
+impl<'a> DotString<'a> for PageDirection {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ PageDirection::BottomLeft => "BL".into(),
+ PageDirection::BottomRight => "BR".into(),
+ PageDirection::TopLeft => "TL".into(),
+ PageDirection::TopRight => "TR".into(),
+ PageDirection::RightBottom => "RB".into(),
+ PageDirection::RightTop => "RT".into(),
+ PageDirection::LeftBottom => "LB".into(),
+ PageDirection::LeftTop => "LT".into(),
+ }
+ }
+}
diff --git a/src/attributes/point.rs b/src/attributes/point.rs
new file mode 100644
index 0000000..819044e
--- /dev/null
+++ b/src/attributes/point.rs
@@ -0,0 +1,55 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub struct Point {
+ pub x: f32,
+ pub y: f32,
+ pub z: Option<f32>,
+
+ /// specify that the node position should not change.
+ pub force_pos: bool,
+}
+
+impl Point {
+ pub fn new_2d(x: f32, y: f32) -> Self {
+ Self::new(x, y, None, false)
+ }
+
+ pub fn new_3d(x: f32, y: f32, z: f32) -> Self {
+ Self::new(x, y, Some(z), false)
+ }
+
+ pub fn new(x: f32, y: f32, z: Option<f32>, force_pos: bool) -> Self {
+ Self { x, y, z, force_pos }
+ }
+}
+
+impl<'a> DotString<'a> for Point {
+ fn dot_string(&self) -> Cow<'a, str> {
+ let mut slice = format!("{:.1},{:.1}", self.x, self.y);
+ if self.z.is_some() {
+ slice.push_str(format!(",{:.1}", self.z.unwrap()).as_str());
+ }
+ if self.force_pos {
+ slice.push_str("!")
+ }
+ slice.into()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::attributes::Point;
+ use crate::DotString;
+
+ #[test]
+ fn dot_string() {
+ assert_eq!("1.0,2.0", Point::new_2d(1.0, 2.0).dot_string());
+ assert_eq!("1.0,2.0,0.0", Point::new_3d(1.0, 2.0, 0.0).dot_string());
+ assert_eq!("1.0,2.0!", Point::new(1.0, 2.0, None, true).dot_string());
+ assert_eq!(
+ "1.0,2.0,0.0!",
+ Point::new(1.0, 2.0, Some(0.0), true).dot_string()
+ );
+ }
+}
diff --git a/src/attributes/port_position.rs b/src/attributes/port_position.rs
new file mode 100644
index 0000000..a492b14
--- /dev/null
+++ b/src/attributes/port_position.rs
@@ -0,0 +1,66 @@
+use crate::attributes::compass_point::CompassPoint;
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+/// Modifier indicating where on a node an edge should be aimed.
+/// If Port is used, the corresponding node must either have record shape with one of its
+/// fields having the given portname, or have an HTML-like label, one of whose components has a
+/// PORT attribute set to portname.
+#[derive(Clone, PartialEq, Eq, Debug)]
+pub enum PortPosition {
+ Port {
+ port_name: String,
+ compass_point: Option<CompassPoint>,
+ },
+ Compass(CompassPoint),
+}
+
+// TODO: AsRef vs this?
+// See https://github.com/Peternator7/strum/blob/96ee0a9a307ec7d1a39809fb59037bd4e11557cc/strum/src/lib.rs
+impl<'a> DotString<'a> for PortPosition {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ PortPosition::Port {
+ port_name,
+ compass_point,
+ } => {
+ let mut dot_string = port_name.to_owned();
+ if let Some(compass_point) = compass_point {
+ dot_string
+ .push_str(format!(":{}", compass_point.dot_string()).as_str());
+ }
+ dot_string.into()
+ }
+ PortPosition::Compass(p) => p.dot_string().into(),
+ }
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::attributes::{PortPosition, CompassPoint};
+ use crate::DotString;
+
+ #[test]
+ fn port_dot_string() {
+ assert_eq!(
+ "port_0",
+ PortPosition::Port {
+ port_name: "port_0".to_string(),
+ compass_point: None
+ }.dot_string()
+ );
+ assert_eq!(
+ "port_0:ne",
+ PortPosition::Port {
+ port_name: "port_0".to_string(),
+ compass_point: Some(CompassPoint::NE)
+ }.dot_string()
+ );
+ }
+
+ #[test]
+ fn compass_dot_string() {
+ assert_eq!("ne", PortPosition::Compass(CompassPoint::NE).dot_string());
+ }
+} \ No newline at end of file
diff --git a/src/attributes/rankdir.rs b/src/attributes/rankdir.rs
new file mode 100644
index 0000000..58fdeff
--- /dev/null
+++ b/src/attributes/rankdir.rs
@@ -0,0 +1,22 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+/// Corresponding to directed graphs drawn from top to bottom, from left to right,
+/// from bottom to top, and from right to left, respectively.
+pub enum RankDir {
+ TopBottom,
+ LeftRight,
+ BottomTop,
+ RightLeft,
+}
+
+impl<'a> DotString<'a> for RankDir {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ RankDir::TopBottom => "TB".into(),
+ RankDir::LeftRight => "LR".into(),
+ RankDir::BottomTop => "BT".into(),
+ RankDir::RightLeft => "RL".into(),
+ }
+ }
+}
diff --git a/src/attributes/ratio.rs b/src/attributes/ratio.rs
new file mode 100644
index 0000000..a8fbad8
--- /dev/null
+++ b/src/attributes/ratio.rs
@@ -0,0 +1,22 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub enum Ratio {
+ Aspect(f32),
+ Fill,
+ Compress,
+ Expand,
+ Auto,
+}
+
+impl<'a> DotString<'a> for Ratio {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ Ratio::Aspect(aspect) => aspect.to_string().into(),
+ Ratio::Fill => "fill".into(),
+ Ratio::Compress => "compress".into(),
+ Ratio::Expand => "expand".into(),
+ Ratio::Auto => "auto".into(),
+ }
+ }
+}
diff --git a/src/attributes/rectangle.rs b/src/attributes/rectangle.rs
new file mode 100644
index 0000000..9b36ee2
--- /dev/null
+++ b/src/attributes/rectangle.rs
@@ -0,0 +1,33 @@
+use crate::attributes::point::Point;
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub struct Rectangle {
+ lower_left: Point,
+ upper_right: Point,
+}
+
+impl<'a> DotString<'a> for Rectangle {
+ fn dot_string(&self) -> Cow<'a, str> {
+ format!(
+ "{:.1},{:.1},{:.1},{:.1}",
+ self.lower_left.x, self.lower_left.y, self.upper_right.x, self.upper_right.y
+ )
+ .into()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::attributes::{Rectangle, Point};
+ use crate::DotString;
+
+ #[test]
+ fn dot_string() {
+ assert_eq!("0.0,0.0,1.0,1.0", Rectangle {
+ lower_left: Point::new_2d(0.0, 0.0),
+ upper_right: Point::new_2d(1.0, 1.0)
+ }.dot_string());
+ }
+
+} \ No newline at end of file
diff --git a/src/attributes/shape.rs b/src/attributes/shape.rs
new file mode 100644
index 0000000..be37ccc
--- /dev/null
+++ b/src/attributes/shape.rs
@@ -0,0 +1,132 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub enum Shape {
+ Box,
+ Polygon,
+ Ellipse,
+ Oval,
+ Circle,
+ Point,
+ Egg,
+ Triangle,
+ Plaintext,
+ Plain,
+ Diamond,
+ Trapezium,
+ Parallelogram,
+ House,
+ Pentagon,
+ Hexagon,
+ Septagon,
+ Octagon,
+ DoubleCircle,
+ DoubleOctagon,
+ TripleOctagon,
+ Invtriangle,
+ Invtrapezium,
+ Invhouse,
+ Mdiamond,
+ Msquare,
+ Mcircle,
+ Record,
+ Rect,
+ Rectangle,
+ Square,
+ Star,
+ None,
+ Underline,
+ Cylinder,
+ Note,
+ Tab,
+ Folder,
+ Box3D,
+ Component,
+ Promoter,
+ Cds,
+ Terminator,
+ Utr,
+ Primersite,
+ Restrictionsite,
+ FivePoverHang,
+ ThreePoverHang,
+ NoverHang,
+ Assemply,
+ Signature,
+ Insulator,
+ Ribosite,
+ Rnastab,
+ Proteasesite,
+ Proteinstab,
+ Rpromotor,
+ Rarrow,
+ Larrow,
+ Lpromotor,
+}
+
+impl<'a> DotString<'a> for Shape {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ Shape::Box => "box".into(),
+ Shape::Polygon => "polygon".into(),
+ Shape::Ellipse => "ellipse".into(),
+ Shape::Oval => "oval".into(),
+ Shape::Circle => "circle".into(),
+ Shape::Point => "point".into(),
+ Shape::Egg => "egg".into(),
+ Shape::Triangle => "triangle".into(),
+ Shape::Plaintext => "plaintext".into(),
+ Shape::Plain => "plain".into(),
+ Shape::Diamond => "diamond".into(),
+ Shape::Trapezium => "trapezium".into(),
+ Shape::Parallelogram => "parallelogram".into(),
+ Shape::House => "house".into(),
+ Shape::Pentagon => "pentagon".into(),
+ Shape::Hexagon => "hexagon".into(),
+ Shape::Septagon => "septagon".into(),
+ Shape::Octagon => "octagon".into(),
+ Shape::DoubleCircle => "doublecircle".into(),
+ Shape::DoubleOctagon => "doubleoctagon".into(),
+ Shape::TripleOctagon => "tripleocctagon".into(),
+ Shape::Invtriangle => "invtriangle".into(),
+ Shape::Invtrapezium => "invtrapezium".into(),
+ Shape::Invhouse => "invhouse".into(),
+ Shape::Mdiamond => "mdiamond".into(),
+ Shape::Msquare => "msquare".into(),
+ Shape::Mcircle => "mcircle".into(),
+ Shape::Record => "record".into(),
+ Shape::Rect => "rect".into(),
+ Shape::Rectangle => "rectangle".into(),
+ Shape::Square => "square".into(),
+ Shape::Star => "star".into(),
+ Shape::None => "none".into(),
+ Shape::Underline => "underline".into(),
+ Shape::Cylinder => "cylinder".into(),
+ Shape::Note => "note".into(),
+ Shape::Tab => "tab".into(),
+ Shape::Folder => "folder".into(),
+ Shape::Box3D => "box3d".into(),
+ Shape::Component => "component".into(),
+ Shape::Promoter => "promoter".into(),
+ Shape::Cds => "cds".into(),
+ Shape::Terminator => "terminator".into(),
+ Shape::Utr => "utr".into(),
+ Shape::Primersite => "primersite".into(),
+ Shape::Restrictionsite => "restrictionsite".into(),
+ Shape::FivePoverHang => "fivepoverhang".into(),
+ Shape::ThreePoverHang => "threepoverhang".into(),
+ Shape::NoverHang => "noverhang".into(),
+ Shape::Assemply => "assemply".into(),
+ Shape::Signature => "signature".into(),
+ Shape::Insulator => "insulator".into(),
+ Shape::Ribosite => "ribosite".into(),
+ Shape::Rnastab => "rnastab".into(),
+ Shape::Proteasesite => "proteasesite".into(),
+ Shape::Proteinstab => "proteinstab".into(),
+ Shape::Rpromotor => "rpromotor".into(),
+ Shape::Rarrow => "rarrow".into(),
+ Shape::Larrow => "larrow".into(),
+ Shape::Lpromotor => "lpromotor".into(),
+ }
+ }
+}
diff --git a/src/attributes/spline_type.rs b/src/attributes/spline_type.rs
new file mode 100644
index 0000000..259496a
--- /dev/null
+++ b/src/attributes/spline_type.rs
@@ -0,0 +1,110 @@
+use crate::attributes::point::Point;
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+/// The number of points in the list must be equivalent to 1 mod 3; note that this is not checked.
+/// TODO: should we check?
+pub struct SplineType {
+ pub start: Option<Point>,
+ pub end: Option<Point>,
+ pub spline_points: Vec<Point>,
+}
+
+impl<'a> DotString<'a> for SplineType {
+ fn dot_string(&self) -> Cow<'a, str> {
+ let mut dot_string = String::from("");
+
+ if let Some(end) = &self.end {
+ dot_string.push_str(format!("e,{:.1},{:.1} ", end.x, end.y).as_str());
+ }
+
+ if let Some(start) = &self.start {
+ dot_string.push_str(format!("s,{:.1},{:.1} ", start.x, start.y).as_str());
+ }
+
+ let mut iter = self.spline_points.iter();
+ let first = iter.next().unwrap();
+ dot_string.push_str(format!("{}", first.dot_string()).as_str());
+ for point in iter {
+ dot_string.push_str(" ");
+ dot_string.push_str(format!("{}", point.dot_string()).as_str());
+ }
+
+ dot_string.into()
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use crate::attributes::{Point, SplineType};
+ use crate::DotString;
+
+ #[test]
+ fn spline_type() {
+ let spline_type = SplineType {
+ end: None,
+ start: None,
+ spline_points: vec![
+ Point::new_2d(0.0, 0.0),
+ Point::new_2d(1.0, 1.0),
+ Point::new_2d(1.0, -1.0),
+ ],
+ };
+
+ assert_eq!("0.0,0.0 1.0,1.0 1.0,-1.0", spline_type.dot_string());
+ }
+
+ #[test]
+ fn spline_type_end() {
+ let spline_type = SplineType {
+ end: Some(Point::new_2d(2.0, 0.0)),
+ start: None,
+ spline_points: vec![
+ Point::new_2d(0.0, 0.0),
+ Point::new_2d(1.0, 1.0),
+ Point::new_2d(1.0, -1.0),
+ ],
+ };
+
+ assert_eq!(
+ "e,2.0,0.0 0.0,0.0 1.0,1.0 1.0,-1.0",
+ spline_type.dot_string()
+ );
+ }
+
+ #[test]
+ fn spline_type_start() {
+ let spline_type = SplineType {
+ end: None,
+ start: Some(Point::new_2d(-1.0, 0.0)),
+ spline_points: vec![
+ Point::new_2d(0.0, 0.0),
+ Point::new_2d(1.0, 1.0),
+ Point::new_2d(1.0, -1.0),
+ ],
+ };
+
+ assert_eq!(
+ "s,-1.0,0.0 0.0,0.0 1.0,1.0 1.0,-1.0",
+ spline_type.dot_string()
+ );
+ }
+
+ #[test]
+ fn spline_type_complete() {
+ let spline_type = SplineType {
+ end: Some(Point::new_2d(2.0, 0.0)),
+ start: Some(Point::new_2d(-1.0, 0.0)),
+ spline_points: vec![
+ Point::new_2d(0.0, 0.0),
+ Point::new_2d(1.0, 1.0),
+ Point::new_2d(1.0, -1.0),
+ ],
+ };
+
+ assert_eq!(
+ "e,2.0,0.0 s,-1.0,0.0 0.0,0.0 1.0,1.0 1.0,-1.0",
+ spline_type.dot_string()
+ );
+ }
+}
diff --git a/src/attributes/splines.rs b/src/attributes/splines.rs
new file mode 100644
index 0000000..5ac9b65
--- /dev/null
+++ b/src/attributes/splines.rs
@@ -0,0 +1,31 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+/// Spline, edges are drawn as splines routed around nodes
+/// Line, edges are drawn as line segments
+/// Polygon, specifies that edges should be drawn as polylines.
+/// Ortho, specifies edges should be routed as polylines of axis-aligned segments.
+/// Curved, specifies edges should be drawn as curved arcs.
+/// splines=line and splines=spline can be used as synonyms for
+/// splines=false and splines=true, respectively.
+pub enum Splines {
+ Line,
+ Spline,
+ None,
+ Curved,
+ Polyline,
+ Ortho,
+}
+
+impl<'a> DotString<'a> for Splines {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ Splines::Line => "line".into(),
+ Splines::Spline => "spline".into(),
+ Splines::None => "none".into(),
+ Splines::Curved => "curved".into(),
+ Splines::Polyline => "polyline".into(),
+ Splines::Ortho => "ortho".into(),
+ }
+ }
+}
diff --git a/src/attributes/style.rs b/src/attributes/style.rs
new file mode 100644
index 0000000..0b7fd2a
--- /dev/null
+++ b/src/attributes/style.rs
@@ -0,0 +1,92 @@
+use crate::dot::DotString;
+use std::borrow::Cow;
+
+pub enum NodeStyle {
+ Bold,
+ Dashed,
+ Diagonals,
+ Dotted,
+ Filled,
+ Invisible,
+ Rounded,
+ Solid,
+ Stripped,
+ Radical,
+ Wedged,
+}
+
+impl<'a> DotString<'a> for NodeStyle {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ NodeStyle::Bold => "bold".into(),
+ NodeStyle::Dashed => "dashed".into(),
+ NodeStyle::Diagonals => "diagonals".into(),
+ NodeStyle::Dotted => "dotted".into(),
+ NodeStyle::Filled => "filled".into(),
+ NodeStyle::Invisible => "invisible".into(),
+ NodeStyle::Rounded => "rounded".into(),
+ NodeStyle::Solid => "solid".into(),
+ NodeStyle::Stripped => "stripped".into(),
+ NodeStyle::Radical => "radical".into(),
+ NodeStyle::Wedged => "wedged".into(),
+ }
+ }
+}
+
+pub enum EdgeStyle {
+ Bold,
+ Dashed,
+ Dotted,
+ Invisible,
+ Solid,
+ Tapered,
+}
+
+impl<'a> DotString<'a> for EdgeStyle {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ EdgeStyle::Bold => "bold".into(),
+ EdgeStyle::Dashed => "dashed".into(),
+ EdgeStyle::Dotted => "dotted".into(),
+ EdgeStyle::Invisible => "invisible".into(),
+ EdgeStyle::Solid => "solid".into(),
+ EdgeStyle::Tapered => "tapered".into(),
+ }
+ }
+}
+
+pub enum GraphStyle {
+ Filled,
+ Radical,
+ Rounded,
+ Striped,
+}
+
+impl<'a> DotString<'a> for GraphStyle {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ GraphStyle::Filled => "filled".into(),
+ GraphStyle::Radical => "radical".into(),
+ GraphStyle::Rounded => "rounded".into(),
+ GraphStyle::Striped => "striped".into(),
+ }
+ }
+}
+
+// TODO: this might be a bit much to in order to avoid some duplication
+// probably not worth it but is pattern is cool nonetheless
+pub enum Styles {
+ Edge(EdgeStyle),
+ Node(NodeStyle),
+ Graph(GraphStyle),
+}
+
+impl<'a> DotString<'a> for Styles {
+ fn dot_string(&self) -> Cow<'a, str> {
+ match self {
+ Styles::Edge(s) => s.dot_string(),
+ Styles::Node(s) => s.dot_string(),
+ Styles::Graph(s) => s.dot_string(),
+ }
+ }
+}
diff --git a/src/dot.rs b/src/dot.rs
new file mode 100644
index 0000000..ae2ca76
--- /dev/null
+++ b/src/dot.rs
@@ -0,0 +1,1498 @@
+use crate::attributes::{
+ ArrowType, AttributeStatement, AttributeText, AttributeType, Attributes, Color,
+ ColorList, Direction, EdgeStyle, GraphAttributeStatement, ImagePosition, ImageScale,
+ IntoWeightedColor, LabelLocation, NodeStyle, Ordering, Point, PortPosition,
+ Rectangle, Shape, Styles,
+};
+use indexmap::IndexMap;
+use std::borrow::Cow;
+use std::collections::HashMap;
+use std::io;
+use std::io::prelude::*;
+
+static INDENT: &str = " ";
+
+pub trait DotString<'a> {
+ fn dot_string(&self) -> Cow<'a, str>;
+}
+
+// TODO: probably dont need this struct and can move impl methods into lib module
+pub struct Dot<'a> {
+ pub graph: Graph<'a>,
+}
+
+impl<'a> Dot<'a> {
+ /// Renders directed graph `g` into the writer `w` in DOT syntax.
+ /// (Simple wrapper around `render_opts` that passes a default set of options.)
+ //pub fn render<W>(self, g: Graph, w: &mut W) -> io::Result<()>
+ pub fn render<W>(self, w: &mut W) -> io::Result<()>
+ where
+ W: Write,
+ {
+ // TODO: use default_options?
+ self.render_opts(w, &[])
+ }
+
+ // io::Result<()> vs Result<(), Box<dyn Error>>
+ // https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#the--operator-can-be-used-in-functions-that-return-result
+ /// Renders directed graph `g` into the writer `w` in DOT syntax.
+ /// (Main entry point for the library.)
+ // pub fn render_opts<W>(self, graph: Graph, w: &mut W, options: &[RenderOption]) -> io::Result<()>
+ pub fn render_opts<W>(self, w: &mut W, options: &[RenderOption]) -> io::Result<()>
+ where
+ W: Write,
+ {
+ if let Some(comment) = &self.graph.comment {
+ // TODO: split comment into lines of 80 or so characters
+ writeln!(w, "// {}", comment)?;
+ }
+
+ let edge_op = &self.graph.edge_op();
+ let strict = if self.graph.strict { "strict " } else { "" };
+ write!(w, "{}{}", strict, &self.graph.graph_type())?;
+
+ if let Some(id) = &self.graph.id {
+ write!(w, " {}", id)?;
+ }
+
+ writeln!(w, " {{")?;
+
+ if let Some(graph_attributes) = self.graph.graph_attributes {
+ write!(w, "{}{}\n", INDENT, graph_attributes.dot_string())?;
+ }
+
+ if let Some(node_attributes) = self.graph.node_attributes {
+ write!(w, "{}{}\n", INDENT, node_attributes.dot_string())?;
+ }
+
+ if let Some(edge_attributes) = self.graph.edge_attributes {
+ write!(w, "{}{}\n", INDENT, edge_attributes.dot_string())?;
+ }
+
+ for n in self.graph.nodes {
+ // TODO: handle render options
+ // Are render options something we need?
+ // we could clone the node or and remove the attributes based on render options
+ // or maybe we keep a set of attributes to ignore based on the options
+ writeln!(w, "{}{}", INDENT, n.dot_string())?;
+ }
+
+ for e in self.graph.edges {
+ let mut edge_source = e.source;
+ if let Some(source_port_position) = e.source_port_position {
+ edge_source
+ .push_str(format!(":{}", source_port_position.dot_string()).as_str())
+ }
+
+ let mut edge_target = e.target;
+ if let Some(target_port_position) = e.target_port_position {
+ edge_target
+ .push_str(format!(":{}", target_port_position.dot_string()).as_str())
+ }
+
+ write!(w, "{}{} {} {}", INDENT, edge_source, edge_op, edge_target)?;
+ // TODO: render ops
+ if !e.attributes.is_empty() {
+ write!(w, " [")?;
+
+ let mut iter = e.attributes.iter();
+ let first = iter.next().unwrap();
+ write!(w, "{}={}", first.0, first.1.dot_string())?;
+ for (key, value) in iter {
+ write!(w, ", ")?;
+ write!(w, "{}={}", key, value.dot_string())?;
+ }
+ write!(w, "]")?;
+ }
+ writeln!(w, ";")?;
+ }
+
+ writeln!(w, "}}")
+ }
+}
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug)]
+pub enum RenderOption {
+ NoEdgeLabels,
+ NoNodeLabels,
+ NoEdgeStyles,
+ NoNodeStyles,
+
+ // TODO: replace with Fontname(String),
+ Monospace,
+}
+
+/// Returns vec holding all the default render options.
+pub fn default_options() -> Vec<RenderOption> {
+ vec![]
+}
+
+pub struct Graph<'a> {
+ pub id: Option<String>,
+
+ pub is_directed: bool,
+
+ pub strict: bool,
+
+ /// Comment added to the first line of the source.
+ pub comment: Option<String>,
+
+ pub graph_attributes: Option<GraphAttributeStatement<'a>>,
+
+ pub node_attributes: Option<NodeAttributeStatement<'a>>,
+
+ pub edge_attributes: Option<EdgeAttributeStatement<'a>>,
+
+ pub nodes: Vec<Node<'a>>,
+
+ pub edges: Vec<Edge<'a>>,
+}
+
+impl<'a> Graph<'a> {
+ pub fn new(
+ id: Option<String>,
+ is_directed: bool,
+ strict: bool,
+ comment: Option<String>,
+ graph_attributes: Option<GraphAttributeStatement<'a>>,
+ node_attributes: Option<NodeAttributeStatement<'a>>,
+ edge_attributes: Option<EdgeAttributeStatement<'a>>,
+ nodes: Vec<Node<'a>>,
+ edges: Vec<Edge<'a>>,
+ ) -> Self {
+ Self {
+ id,
+ is_directed,
+ strict,
+ comment,
+ graph_attributes,
+ node_attributes,
+ edge_attributes,
+ nodes,
+ edges,
+ }
+ }
+
+ pub fn graph_type(&self) -> &'static str {
+ if self.is_directed {
+ "digraph"
+ } else {
+ "graph"
+ }
+ }
+
+ pub fn edge_op(&self) -> &'static str {
+ if self.is_directed {
+ "->"
+ } else {
+ "--"
+ }
+ }
+}
+
+pub struct GraphBuilder<'a> {
+ id: Option<String>,
+
+ is_directed: bool,
+
+ strict: bool,
+
+ graph_attributes: Option<GraphAttributeStatement<'a>>,
+
+ node_attributes: Option<NodeAttributeStatement<'a>>,
+
+ edge_attributes: Option<EdgeAttributeStatement<'a>>,
+
+ nodes: Vec<Node<'a>>,
+
+ edges: Vec<Edge<'a>>,
+
+ comment: Option<String>,
+}
+
+// TODO: id should be an escString
+impl<'a> GraphBuilder<'a> {
+ pub fn new_directed(id: Option<String>) -> Self {
+ Self {
+ id,
+ is_directed: true,
+ strict: false,
+ graph_attributes: None,
+ node_attributes: None,
+ edge_attributes: None,
+ nodes: Vec::new(),
+ edges: Vec::new(),
+ comment: None,
+ }
+ }
+
+ pub fn new_undirected(id: Option<String>) -> Self {
+ Self {
+ id,
+ is_directed: false,
+ strict: false,
+ graph_attributes: None,
+ node_attributes: None,
+ edge_attributes: None,
+ nodes: Vec::new(),
+ edges: Vec::new(),
+ comment: None,
+ }
+ }
+
+ pub fn comment<S: Into<String>>(&mut self, comment: S) -> &mut Self {
+ self.comment = Some(comment.into());
+ self
+ }
+
+ pub fn add_graph_attributes(
+ &mut self,
+ graph_attributes: GraphAttributeStatement<'a>,
+ ) -> &mut Self {
+ self.graph_attributes = Some(graph_attributes);
+ self
+ }
+
+ pub fn add_node_attributes(
+ &mut self,
+ node_attributes: NodeAttributeStatement<'a>,
+ ) -> &mut Self {
+ self.node_attributes = Some(node_attributes);
+ self
+ }
+
+ pub fn add_edge_attributes(
+ &mut self,
+ edge_attributes: EdgeAttributeStatement<'a>,
+ ) -> &mut Self {
+ self.edge_attributes = Some(edge_attributes);
+ self
+ }
+
+ // TODO: update to insert into appropriate statement or remove?
+ // pub fn add_attribute(
+ // &mut self,
+ // attribute_type: AttributeType,
+ // key: String, value: AttributeText<'a>
+ // ) -> &mut Self {
+ // self.get_attributes(attribute_type).insert(key, value);
+ // self
+ // }
+ //
+ // pub fn add_attributes(
+ // &mut self,
+ // attribute_type: AttributeType,
+ // attributes: HashMap<String, AttributeText<'a>>
+ // ) -> &mut Self {
+ // self.get_attributes(attribute_type).extend(attributes);
+ // self
+ // }
+
+ pub fn add_attribute(
+ &mut self,
+ attribute_type: AttributeType,
+ key: String,
+ value: AttributeText<'a>,
+ ) -> &mut Self {
+ match attribute_type {
+ AttributeType::Graph => {
+ if self.graph_attributes.is_none() {
+ self.graph_attributes = Some(GraphAttributeStatement::new());
+ }
+ self.graph_attributes
+ .as_mut()
+ .unwrap()
+ .add_attribute(key, value);
+ }
+ AttributeType::Edge => {
+ if self.edge_attributes.is_none() {
+ self.edge_attributes = Some(EdgeAttributeStatement::new());
+ }
+ self.edge_attributes
+ .as_mut()
+ .unwrap()
+ .add_attribute(key, value);
+ }
+ AttributeType::Node => {
+ if self.node_attributes.is_none() {
+ self.node_attributes = Some(NodeAttributeStatement::new());
+ }
+ self.node_attributes
+ .as_mut()
+ .unwrap()
+ .add_attribute(key, value);
+ }
+ }
+ self
+ }
+
+ pub fn add_node(&mut self, node: Node<'a>) -> &mut Self {
+ self.nodes.push(node);
+ self
+ }
+
+ pub fn add_edge(&mut self, edge: Edge<'a>) -> &mut Self {
+ self.edges.push(edge);
+ self
+ }
+
+ pub fn strict(&mut self) -> &mut Self {
+ self.strict = true;
+ self
+ }
+
+ pub fn build(&self) -> Graph<'a> {
+ Graph {
+ id: self.id.to_owned(),
+ is_directed: self.is_directed,
+ strict: self.strict,
+ comment: self.comment.clone(), // TODO: is clone the only option here?
+ graph_attributes: self.graph_attributes.clone(),
+ node_attributes: self.node_attributes.clone(),
+ edge_attributes: self.edge_attributes.clone(),
+ nodes: self.nodes.clone(), // TODO: is clone the only option here?
+ edges: self.edges.clone(), // TODO: is clone the only option here?
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct Node<'a> {
+ pub id: String,
+ pub attributes: IndexMap<String, AttributeText<'a>>,
+}
+
+impl<'a> Node<'a> {
+ pub fn new(id: String) -> Node<'a> {
+ // TODO: constrain id
+ Node {
+ id,
+ attributes: IndexMap::new(),
+ }
+ }
+}
+
+impl<'a> DotString<'a> for Node<'a> {
+ fn dot_string(&self) -> Cow<'a, str> {
+ let mut dot_string = format!("{}", &self.id);
+ if !self.attributes.is_empty() {
+ dot_string.push_str(" [");
+ let mut iter = self.attributes.iter();
+ let first = iter.next().unwrap();
+ dot_string
+ .push_str(format!("{}={}", first.0, first.1.dot_string()).as_str());
+ for (key, value) in iter {
+ dot_string.push_str(", ");
+ dot_string.push_str(format!("{}={}", key, value.dot_string()).as_str());
+ }
+
+ dot_string.push_str("]");
+ }
+ dot_string.push_str(";");
+ dot_string.into()
+ }
+}
+
+pub struct NodeBuilder<'a> {
+ id: String,
+ attributes: IndexMap<String, AttributeText<'a>>,
+}
+
+impl<'a> NodeAttributes<'a> for NodeBuilder<'a> {
+ fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self {
+ self.attributes.insert(key.into(), value);
+ self
+ }
+
+ /// Add multiple attribures to the edge.
+ fn add_attributes(
+ &'a mut self,
+ attributes: HashMap<String, AttributeText<'a>>,
+ ) -> &mut Self {
+ self.attributes.extend(attributes);
+ self
+ }
+
+ fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> {
+ &mut self.attributes
+ }
+}
+
+impl<'a> NodeBuilder<'a> {
+ pub fn new(id: String) -> Self {
+ Self {
+ id,
+ attributes: IndexMap::new(),
+ }
+ }
+
+ pub fn build(&self) -> Node<'a> {
+ Node {
+ // TODO: are these to_owned and clones necessary?
+ id: self.id.to_owned(),
+ attributes: self.attributes.clone(),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct Edge<'a> {
+ pub source: String,
+ pub source_port_position: Option<PortPosition>,
+ pub target: String,
+ pub target_port_position: Option<PortPosition>,
+ pub attributes: IndexMap<String, AttributeText<'a>>,
+}
+
+impl<'a> Edge<'a> {
+ pub fn new(source: String, target: String) -> Self {
+ Self {
+ source,
+ source_port_position: None,
+ target,
+ target_port_position: None,
+ attributes: IndexMap::new(),
+ }
+ }
+
+ pub fn new_with_position(
+ source: String,
+ source_port_position: PortPosition,
+ target: String,
+ target_port_position: PortPosition,
+ ) -> Self {
+ Self {
+ source,
+ source_port_position: Some(source_port_position),
+ target,
+ target_port_position: Some(target_port_position),
+ attributes: IndexMap::new(),
+ }
+ }
+}
+
+pub struct EdgeBuilder<'a> {
+ pub source: String,
+ pub source_port_position: Option<PortPosition>,
+ pub target: String,
+ pub target_port_position: Option<PortPosition>,
+ attributes: IndexMap<String, AttributeText<'a>>,
+}
+
+impl<'a> EdgeAttributes<'a> for EdgeBuilder<'a> {
+ fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self {
+ self.attributes.insert(key.into(), value);
+ self
+ }
+
+ fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> {
+ &mut self.attributes
+ }
+
+ // /// Add multiple attributes to the edge.
+ // fn add_attributes(&'a mut self, attributes: HashMap<String, AttributeText<'a>>) -> &mut Self {
+ // self.attributes.extend(attributes);
+ // self
+ // }
+}
+
+impl<'a> EdgeBuilder<'a> {
+ pub fn new(source: String, target: String) -> Self {
+ Self {
+ source,
+ target,
+ source_port_position: None,
+ target_port_position: None,
+ attributes: IndexMap::new(),
+ }
+ }
+
+ pub fn new_with_port_position(
+ source: String,
+ source_port_position: PortPosition,
+ target: String,
+ target_port_position: PortPosition,
+ ) -> Self {
+ Self {
+ source,
+ target,
+ source_port_position: Some(source_port_position),
+ target_port_position: Some(target_port_position),
+ attributes: IndexMap::new(),
+ }
+ }
+
+ pub fn source_port_position(&mut self, port_position: PortPosition) -> &mut Self {
+ self.source_port_position = Some(port_position);
+ self
+ }
+
+ pub fn target_port_position(&mut self, port_position: PortPosition) -> &mut Self {
+ self.target_port_position = Some(port_position);
+ self
+ }
+ /// Add an attribute to the edge.
+ pub fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self {
+ self.attributes.insert(key.into(), value);
+ self
+ }
+
+ /// Add multiple attributes to the edge.
+ pub fn add_attributes(
+ &'a mut self,
+ attributes: HashMap<String, AttributeText<'a>>,
+ ) -> &mut Self {
+ self.attributes.extend(attributes);
+ self
+ }
+
+ pub fn build(&self) -> Edge<'a> {
+ Edge {
+ // TODO: are these to_owned and clones necessary?
+ source: self.source.to_owned(),
+ source_port_position: self.source_port_position.to_owned(),
+ target: self.target.to_owned(),
+ target_port_position: self.target_port_position.to_owned(),
+ attributes: self.attributes.clone(),
+ }
+ }
+}
+
+pub trait NodeAttributes<'a> {
+ // TODO: constrain
+ /// Indicates the preferred area for a node or empty cluster when laid out by patchwork.
+ /// default: 1.0, minimum: >0
+ fn area(&mut self, area: f32) -> &mut Self {
+ self.add_attribute("area", AttributeText::from(area))
+ }
+
+ /// Classnames to attach to the node’s SVG element.
+ /// Combine with stylesheet for styling SVG output using CSS classnames.
+ /// Multiple space-separated classes are supported.
+ fn class(&mut self, class: String) -> &mut Self {
+ Attributes::class(self.get_attributes_mut(), class);
+ self
+ }
+
+ /// Basic drawing color for graphics, not text. For the latter, use the fontcolor attribute.
+ fn color(&mut self, color: Color<'a>) -> &mut Self {
+ Attributes::color(self.get_attributes_mut(), color);
+ self
+ }
+
+ fn color_with_colorlist(&mut self, color: ColorList<'a>) -> &mut Self {
+ Attributes::color_with_colorlist(self.get_attributes_mut(), color);
+ self
+ }
+
+ /// This attribute specifies a color scheme namespace: the context for interpreting color names.
+ /// In particular, if a color value has form "xxx" or "//xxx", then the color xxx will be evaluated
+ /// according to the current color scheme. If no color scheme is set, the standard X11 naming is used.
+ /// For example, if colorscheme=bugn9, then color=7 is interpreted as color="/bugn9/7".
+ fn color_scheme(&mut self, color_scheme: String) -> &mut Self {
+ Attributes::color_scheme(self.get_attributes_mut(), color_scheme);
+ self
+ }
+
+ /// Comments are inserted into output. Device-dependent
+ fn comment(&mut self, comment: String) -> &mut Self {
+ Attributes::comment(self.get_attributes_mut(), comment);
+ self
+ }
+
+ /// Distortion factor for shape=polygon.
+ /// Positive values cause top part to be larger than bottom; negative values do the opposite.
+ fn distortion(&mut self, distortion: f32) -> &mut Self {
+ self.add_attribute("distortion", AttributeText::from(distortion))
+ }
+
+ /// Color used to fill the background of a node or cluster assuming style=filled, or a filled arrowhead.
+ fn fill_color(&mut self, fill_color: Color<'a>) -> &mut Self {
+ Attributes::fill_color(self.get_attributes_mut(), fill_color);
+ self
+ }
+
+ /// Color used to fill the background, with a gradient, of a node or cluster assuming
+ /// style=filled, or a filled arrowhead.
+ fn fill_color_with_colorlist(&mut self, fill_colors: ColorList<'a>) -> &mut Self {
+ Attributes::fill_color_with_colorlist(self.get_attributes_mut(), fill_colors);
+ self
+ }
+
+ /// Color used to fill the background, with a gradient, of a node or cluster assuming
+ /// style=filled, or a filled arrowhead.
+ /// TODO: example
+ fn fill_color_with_iter<I>(&mut self, fill_colors: I) -> &mut Self
+ where
+ I: IntoIterator,
+ I::Item: IntoWeightedColor<'a>,
+ {
+ Attributes::fill_color_with_iter(self.get_attributes_mut(), fill_colors);
+ self
+ }
+
+ /// If true, the node size is specified by the values of the width and height attributes only and
+ /// is not expanded to contain the text label.
+ /// There will be a warning if the label (with margin) cannot fit within these limits.
+ /// If false, the size of a node is determined by smallest width and height needed to contain its label
+ /// and image, if any, with a margin specified by the margin attribute.
+ fn fixed_size(&mut self, fixed_size: bool) -> &mut Self {
+ self.add_attribute("fixedsize", AttributeText::from(fixed_size))
+ }
+
+ /// Color used for text.
+ fn font_color(&mut self, font_color: Color<'a>) -> &mut Self {
+ Attributes::font_color(self.get_attributes_mut(), font_color);
+ self
+ }
+
+ /// Font used for text.
+ fn font_name(&mut self, font_name: String) -> &mut Self {
+ Attributes::font_name(self.get_attributes_mut(), font_name);
+ self
+ }
+
+ /// Font size, in points, used for text.
+ /// default: 14.0, minimum: 1.0
+ fn font_size(&mut self, font_size: f32) -> &mut Self {
+ Attributes::font_size(self.get_attributes_mut(), font_size);
+ self
+ }
+
+ /// If a gradient fill is being used, this determines the angle of the fill.
+ fn gradient_angle(&mut self, gradient_angle: u32) -> &mut Self {
+ Attributes::gradient_angle(self.get_attributes_mut(), gradient_angle);
+ self
+ }
+
+ /// If the end points of an edge belong to the same group, i.e., have the same group attribute,
+ /// parameters are set to avoid crossings and keep the edges straight.
+ fn group(&mut self, group: String) -> &mut Self {
+ self.add_attribute("group", AttributeText::attr(group))
+ }
+
+ // TODO: constrain
+ /// Height of node, in inches.
+ /// default: 0.5, minimum: 0.02
+ fn height(&mut self, height: f32) -> &mut Self {
+ self.add_attribute("height", AttributeText::from(height))
+ }
+
+ /// Gives the name of a file containing an image to be displayed inside a node.
+ /// The image file must be in one of the recognized formats,
+ /// typically JPEG, PNG, GIF, BMP, SVG, or Postscript, and be able to be converted
+ /// into the desired output format.
+ fn image(&mut self, image: String) -> &mut Self {
+ self.add_attribute("image", AttributeText::quoted(image))
+ }
+
+ /// Controls how an image is positioned within its containing node.
+ /// Only has an effect when the image is smaller than the containing node.
+ fn image_pos(&mut self, image_pos: ImagePosition) -> &mut Self {
+ self.add_attribute("imagepos", AttributeText::from(image_pos))
+ }
+
+ /// Controls how an image fills its containing node.
+ fn image_scale_bool(&mut self, image_scale: bool) -> &mut Self {
+ self.add_attribute("imagescale", AttributeText::from(image_scale))
+ }
+
+ /// Controls how an image fills its containing node.
+ fn image_scale(&mut self, image_scale: ImageScale) -> &mut Self {
+ self.add_attribute("imagescale", AttributeText::from(image_scale))
+ }
+
+ /// Text label attached to objects.
+ fn label<S: Into<Cow<'a, str>>>(&mut self, text: S) -> &mut Self {
+ self.add_attribute("label", AttributeText::quoted(text))
+ }
+
+ // Vertical placement of labels for nodes, root graphs and clusters.
+ // For graphs and clusters, only labelloc=t and labelloc=b are allowed, corresponding to placement at the top and bottom, respectively.
+ // By default, root graph labels go on the bottom and cluster labels go on the top.
+ // Note that a subgraph inherits attributes from its parent. Thus, if the root graph sets labelloc=b, the subgraph inherits this value.
+ // For nodes, this attribute is used only when the height of the node is larger than the height of its label.
+ // If labelloc=t, labelloc=c, labelloc=b, the label is aligned with the top, centered, or aligned with the bottom of the node, respectively.
+ // By default, the label is vertically centered.
+ fn label_location(&mut self, label_location: LabelLocation) -> &mut Self {
+ Attributes::label_location(self.get_attributes_mut(), label_location);
+ self
+ }
+
+ /// Specifies layers in which the node, edge or cluster is present.
+ fn layer(&mut self, layer: String) -> &mut Self {
+ Attributes::layer(self.get_attributes_mut(), layer);
+ self
+ }
+
+ /// Sets x and y margins of canvas, in inches.
+ /// Both margins are set equal to the given value.
+ /// See [`crate::NodeAttributes::margin_point`]
+ fn margin(&mut self, margin: f32) -> &mut Self {
+ self.margin_point(Point::new_2d(margin, margin))
+ }
+
+ /// Sets x and y margins of canvas, in inches.
+ /// Specifies space left around the node’s label.
+ /// Note that the margin is not part of the drawing but just empty space left around the drawing.
+ /// The margin basically corresponds to a translation of drawing, as would be necessary to
+ /// center a drawing on a page.
+ /// Nothing is actually drawn in the margin.
+ /// To actually extend the background of a drawing, see the pad attribute.
+ /// Whilst it is possible to create a Point value with either a third co-ordinate
+ /// or a forced position, these are ignored for printing.
+ /// By default, the value is 0.11,0.055.
+ fn margin_point(&mut self, margin: Point) -> &mut Self {
+ Attributes::margin(self.get_attributes_mut(), margin);
+ self
+ }
+
+ /// By default, the justification of multi-line labels is done within the largest context that makes sense.
+ /// Thus, in the label of a polygonal node, a left-justified line will align with the left side of the node (shifted by the prescribed margin).
+ /// In record nodes, left-justified line will line up with the left side of the enclosing column of fields.
+ /// If nojustify=true, multi-line labels will be justified in the context of itself.
+ /// For example, if nojustify is set, the first label line is long, and the second is shorter and left-justified,
+ /// the second will align with the left-most character in the first line, regardless of how large the node might be.
+ fn no_justify(&mut self, no_justify: bool) -> &mut Self {
+ Attributes::no_justify(self.get_attributes_mut(), no_justify);
+ self
+ }
+
+ /// If ordering="out", then the outedges of a node, that is, edges with the node as its tail
+ /// node, must appear left-to-right in the same order in which they are defined in the input.
+ ///
+ /// If ordering="in", then the inedges of a node must appear left-to-right in the same order in
+ /// which they are defined in the input.
+ ///
+ /// If defined as a graph or subgraph attribute, the value is applied to all nodes in the graph
+ /// or subgraph.
+ ///
+ /// Note that the graph attribute takes precedence over the node attribute.
+ fn ordering(&mut self, ordering: Ordering) -> &mut Self {
+ Attributes::ordering(self.get_attributes_mut(), ordering);
+ self
+ }
+
+ // TODO: constrain to 0 - 360. Docs say min is 360 which should be max right?
+ /// When used on nodes: Angle, in degrees, to rotate polygon node shapes.
+ /// For any number of polygon sides, 0 degrees rotation results in a flat base.
+ /// When used on graphs: If "[lL]*", sets graph orientation to landscape.
+ /// Used only if rotate is not defined.
+ /// Default: 0.0 and minimum: 360.0
+ fn orientation(&mut self, orientation: f32) -> &mut Self {
+ Attributes::orientation(self.get_attributes_mut(), orientation);
+ self
+ }
+
+ /// Specifies the width of the pen, in points, used to draw lines and curves,
+ /// including the boundaries of edges and clusters.
+ /// default: 1.0, minimum: 0.0
+ fn pen_width(&mut self, pen_width: f32) -> &mut Self {
+ Attributes::pen_width(self.get_attributes_mut(), pen_width);
+ self
+ }
+
+ /// Set number of peripheries used in polygonal shapes and cluster boundaries.
+ fn peripheries(&mut self, peripheries: u32) -> &mut Self {
+ self.add_attribute("penwidth", AttributeText::from(peripheries))
+ }
+
+ /// Position of node, or spline control points.
+ /// the position indicates the center of the node. On output, the coordinates are in points.
+ fn pos(&mut self, pos: Point) -> &mut Self {
+ Attributes::pos(self.get_attributes_mut(), pos);
+ self
+ }
+
+ // TODO: add post_spline
+
+ /// Rectangles for fields of records, in points.
+ fn rects(&mut self, rect: Rectangle) -> &mut Self {
+ self.add_attribute("rects", AttributeText::from(rect))
+ }
+
+ /// If true, force polygon to be regular, i.e., the vertices of the polygon will
+ /// lie on a circle whose center is the center of the node.
+ fn regular(&mut self, regular: bool) -> &mut Self {
+ self.add_attribute("regular", AttributeText::from(regular))
+ }
+
+ /// Gives the number of points used for a circle/ellipse node.
+ fn sample_points(&mut self, sample_points: u32) -> &mut Self {
+ self.add_attribute("samplepoints", AttributeText::from(sample_points))
+ }
+
+ /// Sets the shape of a node.
+ fn shape(&mut self, shape: Shape) -> &mut Self {
+ self.add_attribute("shape", AttributeText::from(shape))
+ }
+
+ // TODO: constrain
+ /// Print guide boxes in PostScript at the beginning of routesplines if showboxes=1, or at the end if showboxes=2.
+ /// (Debugging, TB mode only!)
+ /// default: 0, minimum: 0
+ fn show_boxes(&mut self, show_boxes: u32) -> &mut Self {
+ Attributes::show_boxes(self.get_attributes_mut(), show_boxes);
+ self
+ }
+
+ /// Number of sides when shape=polygon.
+ fn sides(&mut self, sides: u32) -> &mut Self {
+ self.add_attribute("sides", AttributeText::from(sides))
+ }
+
+ // TODO: constrain
+ /// Skew factor for shape=polygon.
+ /// Positive values skew top of polygon to right; negative to left.
+ /// default: 0.0, minimum: -100.0
+ fn skew(&mut self, skew: f32) -> &mut Self {
+ self.add_attribute("skew", AttributeText::from(skew))
+ }
+
+ /// If packmode indicates an array packing, sortv specifies an insertion order
+ /// among the components, with smaller values inserted first.
+ /// default: 0, minimum: 0
+ fn sortv(&mut self, sortv: u32) -> &mut Self {
+ Attributes::sortv(self.get_attributes_mut(), sortv);
+ self
+ }
+
+ /// Set style information for components of the graph.
+ fn style(&mut self, style: NodeStyle) -> &mut Self {
+ Attributes::style(self.get_attributes_mut(), Styles::Node(style));
+ self
+ }
+
+ /// If the object has a URL, this attribute determines which window of the browser is used for the URL.
+ fn target(&mut self, target: String) -> &mut Self {
+ Attributes::target(self.get_attributes_mut(), target);
+ self
+ }
+
+ /// Tooltip annotation attached to the node or edge.
+ /// If unset, Graphviz will use the object’s label if defined.
+ /// Note that if the label is a record specification or an HTML-like label,
+ /// the resulting tooltip may be unhelpful.
+ /// In this case, if tooltips will be generated, the user should set a tooltip attribute explicitly.
+ fn tooltip(&mut self, tooltip: String) -> &mut Self {
+ Attributes::tooltip(self.get_attributes_mut(), tooltip);
+ self
+ }
+
+ /// Hyperlinks incorporated into device-dependent output.
+ fn url(&mut self, url: String) -> &mut Self {
+ Attributes::url(self.get_attributes_mut(), url);
+ self
+ }
+
+ /// Sets the coordinates of the vertices of the node’s polygon, in inches.
+ /// A list of points, separated by spaces.
+ fn vertices(&mut self, vertices: String) -> &mut Self {
+ self.add_attribute("vertices", AttributeText::quoted(vertices))
+ }
+
+ /// Width of node, in inches.
+ /// This is taken as the initial, minimum width of the node.
+ /// If fixedsize is true, this will be the final width of the node.
+ /// Otherwise, if the node label requires more width to fit, the node’s
+ /// width will be increased to contain the label.
+ fn width(&mut self, width: f32) -> &mut Self {
+ self.add_attribute("width", AttributeText::from(width))
+ }
+
+ /// External label for a node or edge.
+ /// The label will be placed outside of the node but near it.
+ /// These labels are added after all nodes and edges have been placed.
+ /// The labels will be placed so that they do not overlap any node or label.
+ /// This means it may not be possible to place all of them.
+ /// To force placing all of them, set forcelabels=true.
+ fn xlabel(&mut self, xlabel: String) -> &mut Self {
+ Attributes::xlabel(self.get_attributes_mut(), xlabel);
+ self
+ }
+
+ /// Position of an exterior label, in points.
+ /// The position indicates the center of the label.
+ fn xlp(&mut self, xlp: Point) -> &mut Self {
+ Attributes::xlp(self.get_attributes_mut(), xlp);
+ self
+ }
+
+ /// Add an attribute to the node.
+ fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self;
+
+ /// Add multiple attribures to the node.
+ fn add_attributes(
+ &'a mut self,
+ attributes: HashMap<String, AttributeText<'a>>,
+ ) -> &mut Self;
+
+ fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>>;
+}
+
+impl<'a> NodeAttributes<'a> for NodeAttributeStatementBuilder<'a> {
+ fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self {
+ self.attributes.insert(key.into(), value);
+ self
+ }
+
+ /// Add multiple attributes to the node.
+ fn add_attributes(
+ &'a mut self,
+ attributes: HashMap<String, AttributeText<'a>>,
+ ) -> &mut Self {
+ self.attributes.extend(attributes);
+ self
+ }
+
+ fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> {
+ &mut self.attributes
+ }
+}
+
+// I'm not a huge fan of needing this builder but having a hard time getting around &mut without it
+pub struct NodeAttributeStatementBuilder<'a> {
+ pub attributes: IndexMap<String, AttributeText<'a>>,
+}
+
+impl<'a> NodeAttributeStatementBuilder<'a> {
+ pub fn new() -> Self {
+ Self {
+ attributes: IndexMap::new(),
+ }
+ }
+
+ pub fn build(&self) -> NodeAttributeStatement<'a> {
+ NodeAttributeStatement {
+ attributes: self.attributes.clone(),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct NodeAttributeStatement<'a> {
+ pub attributes: IndexMap<String, AttributeText<'a>>,
+}
+
+impl<'a> NodeAttributeStatement<'a> {
+ pub fn new() -> Self {
+ Self {
+ attributes: IndexMap::new(),
+ }
+ }
+
+ pub fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self {
+ self.attributes.insert(key.into(), value);
+ self
+ }
+}
+
+impl<'a> AttributeStatement<'a> for NodeAttributeStatement<'a> {
+ fn get_attribute_statement_type(&self) -> &'static str {
+ "node"
+ }
+
+ fn get_attributes(&self) -> &IndexMap<String, AttributeText<'a>> {
+ &self.attributes
+ }
+}
+
+pub trait EdgeAttributes<'a> {
+ /// Style of arrowhead on the head node of an edge.
+ /// This will only appear if the dir attribute is forward or both.
+ fn arrow_head(&mut self, arrowhead: ArrowType) -> &mut Self {
+ self.add_attribute("arrowhead", AttributeText::from(arrowhead))
+ }
+
+ // TODO: constrain
+ /// Multiplicative scale factor for arrowheads.
+ /// default: 1.0, minimum: 0.0
+ fn arrow_size(&mut self, arrow_size: f32) -> &mut Self {
+ self.add_attribute("arrowsize", AttributeText::from(arrow_size))
+ }
+
+ /// Style of arrowhead on the tail node of an edge.
+ /// This will only appear if the dir attribute is back or both.
+ fn arrowtail(&mut self, arrowtail: ArrowType) -> &mut Self {
+ self.add_attribute("arrowtail", AttributeText::from(arrowtail))
+ }
+
+ /// Classnames to attach to the edge’s SVG element.
+ /// Combine with stylesheet for styling SVG output using CSS classnames.
+ /// Multiple space-separated classes are supported.
+ fn class(&mut self, class: String) -> &mut Self {
+ Attributes::class(self.get_attributes_mut(), class);
+ self
+ }
+
+ /// Basic drawing color for graphics, not text. For the latter, use the fontcolor attribute.
+ fn color(&mut self, color: Color<'a>) -> &mut Self {
+ Attributes::color(self.get_attributes_mut(), color);
+ self
+ }
+
+ fn color_with_colorlist(&mut self, color: ColorList<'a>) -> &mut Self {
+ Attributes::color_with_colorlist(self.get_attributes_mut(), color);
+ self
+ }
+
+ /// This attribute specifies a color scheme namespace: the context for interpreting color names.
+ /// In particular, if a color value has form "xxx" or "//xxx", then the color xxx will be evaluated
+ /// according to the current color scheme. If no color scheme is set, the standard X11 naming is used.
+ /// For example, if colorscheme=bugn9, then color=7 is interpreted as color="/bugn9/7".
+ fn color_scheme(&mut self, color_scheme: String) -> &mut Self {
+ Attributes::color_scheme(self.get_attributes_mut(), color_scheme);
+ self
+ }
+
+ /// Comments are inserted into output. Device-dependent
+ fn comment(&mut self, comment: String) -> &mut Self {
+ self.add_attribute("comment", AttributeText::attr(comment));
+ self
+ }
+
+ /// If false, the edge is not used in ranking the nodes.
+ fn constriant(&mut self, constriant: bool) -> &mut Self {
+ self.add_attribute("constriant", AttributeText::from(constriant))
+ }
+
+ /// If true, attach edge label to edge by a 2-segment polyline, underlining the label,
+ /// then going to the closest point of spline.
+ fn decorate(&mut self, decorate: bool) -> &mut Self {
+ self.add_attribute("decorate", AttributeText::from(decorate))
+ }
+
+ /// Edge type for drawing arrowheads.
+ /// Indicates which ends of the edge should be decorated with an arrowhead.
+ /// The actual style of the arrowhead can be specified using the arrowhead and arrowtail attributes.
+ fn dir(&mut self, dir: Direction) -> &mut Self {
+ self.add_attribute("dir", AttributeText::from(dir))
+ }
+
+ /// If the edge has a URL or edgeURL attribute, edgetarget determines which window
+ /// of the browser is used for the URL attached to the non-label part of the edge.
+ /// Setting edgetarget=_graphviz will open a new window if it doesn’t already exist,
+ /// or reuse it if it does.
+ fn edge_target(&mut self, edge_target: String) -> &mut Self {
+ self.add_attribute("edgetarget", AttributeText::escaped(edge_target))
+ }
+
+ /// Tooltip annotation attached to the non-label part of an edge.
+ /// Used only if the edge has a URL or edgeURL attribute.
+ fn edge_tooltip(&mut self, edge_tooltip: String) -> &mut Self {
+ self.add_attribute("edgetooltip", AttributeText::escaped(edge_tooltip))
+ }
+
+ /// The link for the non-label parts of an edge.
+ /// edgeURL overrides any URL defined for the edge.
+ /// Also, edgeURL is used near the head or tail node unless overridden by headURL or tailURL, respectively.
+ fn edge_url(&mut self, edge_url: String) -> &mut Self {
+ self.add_attribute("edgeurl", AttributeText::escaped(edge_url))
+ }
+
+ // TODO: color list
+ /// Color used to fill the background of a node or cluster assuming style=filled, or a filled arrowhead.
+ fn fill_color(&mut self, fill_color: Color<'a>) -> &mut Self {
+ Attributes::fill_color(self.get_attributes_mut(), fill_color);
+ self
+ }
+
+ // TODO: color list
+ /// Color used for text.
+ fn font_color(&mut self, font_color: Color<'a>) -> &mut Self {
+ Attributes::font_color(self.get_attributes_mut(), font_color);
+ self
+ }
+
+ /// Font used for text.
+ fn font_name(&mut self, font_name: String) -> &mut Self {
+ Attributes::font_name(self.get_attributes_mut(), font_name);
+ self
+ }
+
+ /// Font size, in points, used for text.
+ /// default: 14.0, minimum: 1.0
+ fn font_size(&mut self, font_size: f32) -> &mut Self {
+ Attributes::font_size(self.get_attributes_mut(), font_size);
+ self
+ }
+
+ /// Position of an edge’s head label, in points. The position indicates the center of the label.
+ fn head_lp(&mut self, head_lp: Point) -> &mut Self {
+ self.add_attribute("head_lp", AttributeText::from(head_lp))
+ }
+
+ /// If true, the head of an edge is clipped to the boundary of the head node;
+ /// otherwise, the end of the edge goes to the center of the node, or the center
+ /// of a port, if applicable.
+ fn head_clip(&mut self, head_clip: bool) -> &mut Self {
+ self.add_attribute("headclip", AttributeText::from(head_clip))
+ }
+
+ /// Text label to be placed near head of edge.
+ fn head_label(&mut self, head_label: String) -> &mut Self {
+ self.add_attribute("headlabel", AttributeText::quoted(head_label))
+ }
+
+ /// Indicates where on the head node to attach the head of the edge.
+ /// In the default case, the edge is aimed towards the center of the node,
+ /// and then clipped at the node boundary.
+ fn head_port(&mut self, head_port: PortPosition) -> &mut Self {
+ self.add_attribute("headport", AttributeText::from(head_port))
+ }
+
+ /// If the edge has a headURL, headtarget determines which window of the browser is used for the URL.
+ /// Setting headURL=_graphviz will open a new window if the window doesn’t already exist,
+ /// or reuse the window if it does.
+ /// If undefined, the value of the target is used.
+ fn head_target(&mut self, head_target: String) -> &mut Self {
+ self.add_attribute("headtarget", AttributeText::escaped(head_target))
+ }
+
+ /// Tooltip annotation attached to the head of an edge.
+ /// Used only if the edge has a headURL attribute.
+ fn head_tooltip(&mut self, head_tooltip: String) -> &mut Self {
+ self.add_attribute("headtooltip", AttributeText::escaped(head_tooltip))
+ }
+
+ /// If defined, headURL is output as part of the head label of the edge.
+ /// Also, this value is used near the head node, overriding any URL value.
+ fn head_url(&mut self, head_url: String) -> &mut Self {
+ self.add_attribute("headURL", AttributeText::escaped(head_url))
+ }
+
+ /// An escString or an HTML label.
+ fn label(&mut self, label: String) -> &mut Self {
+ Attributes::label(self.get_attributes_mut(), label);
+ self
+ }
+
+ // TODO: constrain
+ /// Determines, along with labeldistance, where the headlabel / taillabel are
+ /// placed with respect to the head / tail in polar coordinates.
+ /// The origin in the coordinate system is the point where the edge touches the node.
+ /// The ray of 0 degrees goes from the origin back along the edge, parallel to the edge at the origin.
+ /// The angle, in degrees, specifies the rotation from the 0 degree ray,
+ /// with positive angles moving counterclockwise and negative angles moving clockwise.
+ /// default: -25.0, minimum: -180.0
+ fn label_angle(&mut self, label_angle: f32) -> &mut Self {
+ self.add_attribute("labelangle", AttributeText::from(label_angle))
+ }
+
+ /// Multiplicative scaling factor adjusting the distance that the headlabel / taillabel is from
+ /// the head / tail node.
+ /// default: 1.0, minimum: 0.0
+ fn label_distance(&mut self, label_distance: f32) -> &mut Self {
+ self.add_attribute("labeldistance", AttributeText::from(label_distance))
+ }
+
+ /// If true, allows edge labels to be less constrained in position.
+ /// In particular, it may appear on top of other edges.
+ fn label_float(&mut self, label_float: bool) -> &mut Self {
+ self.add_attribute("labelfloat", AttributeText::from(label_float))
+ }
+
+ /// Color used for headlabel and taillabel.
+ fn label_font_color(&mut self, label_font_color: Color<'a>) -> &mut Self {
+ self.add_attribute("labelfontcolor", AttributeText::from(label_font_color))
+ }
+
+ /// Font used for headlabel and taillabel.
+ /// If not set, defaults to edge’s fontname.
+ fn label_font_name(&mut self, label_font_name: String) -> &mut Self {
+ self.add_attribute("labelfontname", AttributeText::attr(label_font_name))
+ }
+
+ // TODO: constrains
+ /// Font size, in points, used for headlabel and taillabel.
+ /// If not set, defaults to edge’s fontsize.
+ /// default: 14.0, minimum: 1.0
+ fn label_font_size(&mut self, label_font_size: f32) -> &mut Self {
+ self.add_attribute("labelfontsize", AttributeText::from(label_font_size))
+ }
+
+ /// If the edge has a URL or labelURL attribute, this attribute determines
+ /// which window of the browser is used for the URL attached to the label.
+ fn label_target(&mut self, label_target: String) -> &mut Self {
+ self.add_attribute("labeltarget", AttributeText::escaped(label_target))
+ }
+
+ /// Tooltip annotation attached to label of an edge.
+ /// Used only if the edge has a URL or labelURL attribute.
+ fn label_tooltip(&mut self, label_tooltip: String) -> &mut Self {
+ self.add_attribute("labeltooltip", AttributeText::escaped(label_tooltip))
+ }
+
+ /// If defined, labelURL is the link used for the label of an edge.
+ /// labelURL overrides any URL defined for the edge.
+ fn label_url(&mut self, label_url: String) -> &mut Self {
+ self.add_attribute("labelurl", AttributeText::escaped(label_url))
+ }
+
+ fn layer(&mut self, layer: String) -> &mut Self {
+ Attributes::layer(self.get_attributes_mut(), layer);
+ self
+ }
+
+ fn lhead(&mut self, lhead: String) -> &mut Self {
+ self.add_attribute("lhead", AttributeText::quoted(lhead))
+ }
+
+ /// Label position
+ /// The position indicates the center of the label.
+ fn label_position(&mut self, lp: Point) -> &mut Self {
+ Attributes::label_position(self.get_attributes_mut(), lp);
+ self
+ }
+
+ /// Logical tail of an edge.
+ /// When compound=true, if ltail is defined and is the name of a cluster
+ /// containing the real tail, the edge is clipped to the boundary of the cluster.
+ fn ltail(&mut self, ltail: String) -> &mut Self {
+ self.add_attribute("ltail", AttributeText::quoted(ltail))
+ }
+
+ /// Minimum edge length (rank difference between head and tail).
+ fn min_len(&mut self, min_len: u32) -> &mut Self {
+ self.add_attribute("minlen", AttributeText::from(min_len))
+ }
+
+ fn no_justify(&mut self, no_justify: bool) -> &mut Self {
+ self.add_attribute("nojustify", AttributeText::from(no_justify))
+ }
+
+ fn pen_width(&mut self, pen_width: f32) -> &mut Self {
+ Attributes::pen_width(self.get_attributes_mut(), pen_width);
+ self
+ }
+
+ /// Position of node, or spline control points.
+ /// the position indicates the center of the node. On output, the coordinates are in points.
+ fn pos(&mut self, pos: Point) -> &mut Self {
+ Attributes::pos(self.get_attributes_mut(), pos);
+ self
+ }
+
+ /// Edges with the same head and the same samehead value are aimed at the same point on the head.
+ fn same_head(&mut self, same_head: String) -> &mut Self {
+ self.add_attribute("samehead", AttributeText::quoted(same_head))
+ }
+
+ /// Edges with the same tail and the same sametail value are aimed at the same point on the tail.
+ fn same_tail(&mut self, same_tail: String) -> &mut Self {
+ self.add_attribute("sametail", AttributeText::quoted(same_tail))
+ }
+
+ // TODO: constrain
+ /// Print guide boxes in PostScript at the beginning of routesplines if showboxes=1, or at the
+ /// end if showboxes=2.
+ /// (Debugging, TB mode only!)
+ /// default: 0, minimum: 0
+ fn show_boxes(&mut self, show_boxes: u32) -> &mut Self {
+ Attributes::show_boxes(self.get_attributes_mut(), show_boxes);
+ self
+ }
+
+ /// Set style information for components of the graph.
+ fn style(&mut self, style: EdgeStyle) -> &mut Self {
+ Attributes::style(self.get_attributes_mut(), Styles::Edge(style));
+ self
+ }
+
+ /// Position of an edge’s tail label, in points.
+ /// The position indicates the center of the label.
+ fn tail_lp(&mut self, tail_lp: Point) -> &mut Self {
+ self.add_attribute("tail_lp", AttributeText::from(tail_lp))
+ }
+
+ /// If true, the tail of an edge is clipped to the boundary of the tail node; otherwise,
+ /// the end of the edge goes to the center of the node, or the center of a port, if applicable.
+ fn tail_clip(&mut self, tail_clip: bool) -> &mut Self {
+ self.add_attribute("tailclip", AttributeText::from(tail_clip))
+ }
+
+ /// Text label to be placed near tail of edge.
+ fn tail_label(&mut self, tail_label: String) -> &mut Self {
+ self.add_attribute("taillabel", AttributeText::quoted(tail_label))
+ }
+
+ /// Indicates where on the tail node to attach the tail of the edge.
+ fn tail_port(&mut self, tail_port: PortPosition) -> &mut Self {
+ self.add_attribute("tailport", AttributeText::from(tail_port))
+ }
+
+ /// If the edge has a tailURL, tailtarget determines which window of the browser is used for the URL.
+ fn tail_target(&mut self, tail_target: String) -> &mut Self {
+ self.add_attribute("tailtarget", AttributeText::escaped(tail_target))
+ }
+
+ /// Tooltip annotation attached to the tail of an edge.
+ fn tail_tooltip(&mut self, tail_tooltip: String) -> &mut Self {
+ self.add_attribute("tailtooltip", AttributeText::escaped(tail_tooltip))
+ }
+
+ /// If defined, tailURL is output as part of the tail label of the edge.
+ /// Also, this value is used near the tail node, overriding any URL value.
+ fn tail_url(&mut self, tail_url: String) -> &mut Self {
+ self.add_attribute("tailURL", AttributeText::escaped(tail_url))
+ }
+
+ /// If the object has a URL, this attribute determines which window of the browser is used for the URL.
+ fn target(&mut self, target: String) -> &mut Self {
+ self.add_attribute("target", AttributeText::escaped(target))
+ }
+
+ /// Tooltip annotation attached to the node or edge.
+ /// If unset, Graphviz will use the object’s label if defined.
+ /// Note that if the label is a record specification or an HTML-like label,
+ /// the resulting tooltip may be unhelpful.
+ /// In this case, if tooltips will be generated, the user should set a tooltip attribute explicitly.
+ fn tooltip(&mut self, tooltip: String) -> &mut Self {
+ Attributes::tooltip(self.get_attributes_mut(), tooltip);
+ self
+ }
+
+ /// Hyperlinks incorporated into device-dependent output.
+ fn url(&mut self, url: String) -> &mut Self {
+ Attributes::url(self.get_attributes_mut(), url);
+ self
+ }
+
+ // TODO: contrain
+ /// Weight of edge.
+ /// The heavier the weight, the shorter, straighter and more vertical the edge is.
+ /// default: 1, minimum: 0
+ fn weight(&mut self, weight: u32) -> &mut Self {
+ self.add_attribute("weight", AttributeText::attr(weight.to_string()))
+ }
+
+ /// External label for a node or edge.
+ /// The label will be placed outside of the node but near it.
+ /// These labels are added after all nodes and edges have been placed.
+ /// The labels will be placed so that they do not overlap any node or label.
+ /// This means it may not be possible to place all of them.
+ /// To force placing all of them, set forcelabels=true.
+ fn xlabel(&mut self, xlabel: String) -> &mut Self {
+ Attributes::xlabel(self.get_attributes_mut(), xlabel);
+ self
+ }
+
+ /// Position of an exterior label, in points.
+ /// The position indicates the center of the label.
+ fn xlp(&mut self, xlp: Point) -> &mut Self {
+ Attributes::xlp(self.get_attributes_mut(), xlp);
+ self
+ }
+
+ fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self;
+
+ fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>>;
+
+ // fn add_attribute<S: Into<String>>(
+ // &self,
+ // key: S,
+ // value: AttributeText<'a>
+ // ) {
+ // self.get_attributes().insert(key.into(), value);
+ // }
+
+ // fn get_attributes(&self) -> IndexMap<String, AttributeText<'a>>;
+
+ // fn get_attributes_mut(&self) -> &mut IndexMap<String, AttributeText<'a>>;
+
+ // fn to_dot_string(&self) -> String;
+}
+
+impl<'a> EdgeAttributes<'a> for EdgeAttributeStatementBuilder<'a> {
+ fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self {
+ self.attributes.insert(key.into(), value);
+ self
+ }
+
+ fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> {
+ &mut self.attributes
+ }
+}
+
+impl<'a> AttributeStatement<'a> for EdgeAttributeStatement<'a> {
+ fn get_attribute_statement_type(&self) -> &'static str {
+ "edge"
+ }
+
+ fn get_attributes(&self) -> &IndexMap<String, AttributeText<'a>> {
+ &self.attributes
+ }
+}
+
+// I'm not a huge fan of needing this builder but having a hard time getting around &mut without it
+pub struct EdgeAttributeStatementBuilder<'a> {
+ pub attributes: IndexMap<String, AttributeText<'a>>,
+}
+
+impl<'a> EdgeAttributeStatementBuilder<'a> {
+ pub fn new() -> Self {
+ Self {
+ attributes: IndexMap::new(),
+ }
+ }
+
+ pub fn build(&self) -> EdgeAttributeStatement<'a> {
+ EdgeAttributeStatement {
+ attributes: self.attributes.clone(),
+ }
+ }
+}
+
+#[derive(Clone, Debug)]
+pub struct EdgeAttributeStatement<'a> {
+ pub attributes: IndexMap<String, AttributeText<'a>>,
+}
+
+impl<'a> EdgeAttributeStatement<'a> {
+ pub fn new() -> Self {
+ Self {
+ attributes: IndexMap::new(),
+ }
+ }
+
+ pub fn add_attribute<S: Into<String>>(
+ &mut self,
+ key: S,
+ value: AttributeText<'a>,
+ ) -> &mut Self {
+ self.attributes.insert(key.into(), value);
+ self
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 2e75d18..d29e7ad 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -1,14 +1,13 @@
//! Simple graphviz dot file format output.
-use indexmap::IndexMap;
-use std;
-use std::borrow::Cow;
-use std::collections::HashMap;
-use std::io;
-use std::io::prelude::*;
-use AttributeText::*;
+pub mod attributes;
+pub mod dot;
-static INDENT: &str = " ";
+pub use crate::dot::{
+ Dot, DotString, Edge, EdgeAttributeStatementBuilder, EdgeAttributes, EdgeBuilder,
+ Graph, GraphBuilder, Node, NodeAttributeStatementBuilder, NodeAttributes,
+ NodeBuilder,
+};
// TODO: support adding edge based on index of nodes?
// TODO: handle render options
@@ -25,3840 +24,3 @@ static INDENT: &str = " ";
// Double(f32),
// Point(Point),
// }
-
-
-trait DotString<'a> {
- fn dot_string(&self) -> Cow<'a, str>;
-}
-
-pub enum ArrowType {
- Normal,
- Dot,
- Odot,
- None,
- Empty,
- Diamond,
- Ediamond,
- Box,
- Open,
- Vee,
- Inv,
- Invdot,
- Invodot,
- Tee,
- Invempty,
- Odiamond,
- Crow,
- Obox,
- Halfopen,
-}
-
-impl<'a> DotString<'a> for ArrowType {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- ArrowType::Normal => "normal".into(),
- ArrowType::Dot => "dot".into(),
- ArrowType::Odot => "odot".into(),
- ArrowType::None => "none".into(),
- ArrowType::Empty => "empty".into(),
- ArrowType::Diamond => "diamond".into(),
- ArrowType::Ediamond => "ediamond".into(),
- ArrowType::Box => "box".into(),
- ArrowType::Open => "open".into(),
- ArrowType::Vee => "vee".into(),
- ArrowType::Inv => "inv".into(),
- ArrowType::Invdot => "invdot".into(),
- ArrowType::Invodot => "invodot".into(),
- ArrowType::Tee => "tee".into(),
- ArrowType::Invempty => "invempty".into(),
- ArrowType::Odiamond => "odiamond".into(),
- ArrowType::Crow => "crow".into(),
- ArrowType::Obox => "obox".into(),
- ArrowType::Halfopen => "halfopen".into(),
- }
- }
-}
-
-pub enum ClusterMode {
- Local,
- Global,
- None,
-}
-
-impl<'a> DotString<'a> for ClusterMode {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- ClusterMode::Local => "local".into(),
- ClusterMode::Global => "global".into(),
- ClusterMode::None => "none".into(),
- }
- }
-}
-
-#[derive(Copy, Clone, PartialEq, Debug)]
-pub enum Color<'a> {
- RGB {
- red: u8,
- green: u8,
- blue: u8,
- },
- RGBA {
- red: u8,
- green: u8,
- blue: u8,
- alpha: u8,
- },
- // TODO: constrain?
- // Hue-Saturation-Value (HSV) 0.0 <= H,S,V <= 1.0
- HSV {
- hue: f32,
- saturation: f32,
- value: f32,
- },
- Named(&'a str),
-}
-
-impl<'a> DotString<'a> for Color<'a> {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- Color::RGB { red, green, blue } => {
- format!("#{:02x?}{:02x?}{:02x?}", red, green, blue).into()
- }
- Color::RGBA {
- red,
- green,
- blue,
- alpha,
- } => {
- format!("#{:02x?}{:02x?}{:02x?}{:02x?}", red, green, blue, alpha).into()
- }
- Color::HSV {
- hue,
- saturation,
- value,
- } => format!("{} {} {}", hue, saturation, value).into(),
- Color::Named(color) => (*color).into(),
- }
- }
-}
-
-// The sum of the optional weightings must sum to at most 1.
-pub struct WeightedColor<'a> {
- color: Color<'a>,
-
- // TODO: constrain
- /// Must be in range 0 <= W <= 1.
- weight: Option<f32>,
-}
-
-impl<'a> DotString<'a> for WeightedColor<'a> {
- fn dot_string(&self) -> Cow<'a, str> {
- let mut dot_string = self.color.dot_string().to_string();
- if let Some(weight) = &self.weight {
- dot_string.push_str(format!(";{}", weight).as_str());
- }
- dot_string.into()
- }
-}
-
-pub struct ColorList<'a> {
- colors: Vec<WeightedColor<'a>>,
-}
-
-impl<'a> DotString<'a> for ColorList<'a> {
- /// A colon-separated list of weighted color values: WC(:WC)* where each WC has the form C(;F)?
- /// Ex: fillcolor=yellow;0.3:blue
- fn dot_string(&self) -> Cow<'a, str> {
- let mut dot_string = String::new();
- let mut iter = self.colors.iter();
- let first = iter.next();
- if first.is_none() {
- return dot_string.into();
- }
- dot_string.push_str(&first.unwrap().dot_string());
- for weighted_color in iter {
- dot_string.push_str(":");
- dot_string.push_str(&weighted_color.dot_string())
- }
-
- dot_string.into()
- }
-}
-
-/// Convert an element like `(i, j)` into a WeightedColor
-pub trait IntoWeightedColor<'a> {
- fn into_weighted_color(self) -> WeightedColor<'a>;
-}
-
-impl<'a> IntoWeightedColor<'a> for &'a (Color<'a>, Option<f32>) {
- fn into_weighted_color(self) -> WeightedColor<'a> {
- let (s, t) = *self;
- WeightedColor {
- color: s,
- weight: t,
- }
- }
-}
-
-
-// TODO: not sure we need this enum but should support setting nodeport either via
-// headport / tailport attributes e.g. a -> b [tailport=se]
-// or via edge declaration using the syntax node name:port_name e.g. a -> b:se
-// aka compass
-#[derive(Clone, PartialEq, Eq, Debug)]
-pub enum CompassPoint {
- N,
- NE,
- E,
- SE,
- S,
- SW,
- W,
- NW,
- C,
- // TODO: none might not be a good name
- // The compass point "_" specifies that an appropriate side of the port adjacent to the exterior
- // of the node should be used, if such exists. Otherwise, the center is used.
- // If no compass point is used with a portname, the default value is "_".
- None,
-}
-
-impl<'a> DotString<'a> for CompassPoint {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- CompassPoint::N => "n".into(),
- CompassPoint::NE => "ne".into(),
- CompassPoint::E => "e".into(),
- CompassPoint::SE => "se".into(),
- CompassPoint::S => "s".into(),
- CompassPoint::SW => "sw".into(),
- CompassPoint::W => "w".into(),
- CompassPoint::NW => "nw".into(),
- CompassPoint::C => "c".into(),
- CompassPoint::None => "_".into(),
- }
- }
-}
-
-pub enum Direction {
- Forward,
- Back,
- Both,
- None,
-}
-
-impl<'a> DotString<'a> for Direction {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- Direction::Forward => "forward".into(),
- Direction::Back => "back".into(),
- Direction::Both => "both".into(),
- Direction::None => "none".into(),
- }
- }
-}
-
-pub enum ImagePosition {
- TopLeft,
- TopCentered,
- TopRight,
- MiddleLeft,
- MiddleCentered,
- MiddleRight,
- BottomLeft,
- BottomCentered,
- BottomRight,
-}
-
-// TODO: change to AsRef<str>? What about something for &'static str?
-impl ImagePosition {
- pub fn as_slice(&self) -> &'static str {
- match self {
- ImagePosition::TopLeft => "tl",
- ImagePosition::TopCentered => "tc",
- ImagePosition::TopRight => "tr",
- ImagePosition::MiddleLeft => "ml",
- ImagePosition::MiddleCentered => "mc",
- ImagePosition::MiddleRight => "mr",
- ImagePosition::BottomLeft => "bl",
- ImagePosition::BottomCentered => "bc",
- ImagePosition::BottomRight => "br",
- }
- }
-}
-
-pub enum ImageScale {
- Width,
- Height,
- Both,
-}
-
-impl ImageScale {
- pub fn as_slice(&self) -> &'static str {
- match self {
- ImageScale::Width => "width",
- ImageScale::Height => "height",
- ImageScale::Both => "both",
- }
- }
-}
-
-pub enum LabelJustification {
- Left,
- Right,
- Center,
-}
-
-impl<'a> DotString<'a> for LabelJustification {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- LabelJustification::Left => "l".into(),
- LabelJustification::Right => "r".into(),
- LabelJustification::Center => "c".into(),
- }
- }
-}
-
-pub enum LabelLocation {
- Top,
- Center,
- Bottom,
-}
-
-impl<'a> DotString<'a> for LabelLocation {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- LabelLocation::Top => "t".into(),
- LabelLocation::Center => "c".into(),
- LabelLocation::Bottom => "b".into(),
- }
- }
-}
-
-pub enum Ordering {
- In,
- Out,
-}
-
-impl<'a> DotString<'a> for Ordering {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- Ordering::In => "in".into(),
- Ordering::Out => "out".into(),
- }
- }
-}
-
-/// These specify the order in which nodes and edges are drawn in concrete output.
-///
-/// The default "breadthfirst" is the simplest, but when the graph layout does not avoid edge-node
-/// overlap, this mode will sometimes have edges drawn over nodes and sometimes on top of nodes.
-///
-/// If the mode "nodesfirst" is chosen, all nodes are drawn first, followed by the edges.
-/// This guarantees an edge-node overlap will not be mistaken for an edge ending at a node.
-///
-/// On the other hand, usually for aesthetic reasons, it may be desirable that all edges appear
-/// beneath nodes, even if the resulting drawing is ambiguous.
-/// This can be achieved by choosing "edgesfirst".
-pub enum OutputMode {
- BreadthFirst,
- NodesFirst,
- EdgesFirst,
-}
-
-impl<'a> DotString<'a> for OutputMode {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- OutputMode::BreadthFirst => "breadthfirst".into(),
- OutputMode::NodesFirst => "nodesfirst".into(),
- OutputMode::EdgesFirst => "edgesfirst".into(),
- }
- }
-}
-
-/// The modes "node", "clust" or "graph" specify that the components should be packed together
-/// tightly, using the specified granularity.
-pub enum PackMode {
- /// causes packing at the node and edge level, with no overlapping of these objects.
- /// This produces a layout with the least area, but it also allows interleaving,
- /// where a node of one component may lie between two nodes in another component.
- Node,
-
- /// guarantees that top-level clusters are kept intact.
- /// What effect a value has also depends on the layout algorithm.
- Cluster,
-
- /// does a packing using the bounding box of the component.
- /// Thus, there will be a rectangular region around a component free of elements of any other component.
- Graph,
- // TODO: array - "array(_flags)?(%d)?"
-}
-
-impl<'a> DotString<'a> for PackMode {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- PackMode::Node => "node".into(),
- PackMode::Cluster => "clust".into(),
- PackMode::Graph => "graph".into(),
- }
- }
-}
-
-/// These specify the 8 row or column major orders for traversing a rectangular array,
-/// the first character corresponding to the major order and the second to the minor order.
-/// Thus, for “BL”, the major order is from bottom to top, and the minor order is from left to right.
-/// This means the bottom row is traversed first, from left to right, then the next row up,
-/// from left to right, and so on, until the topmost row is traversed
-pub enum PageDirection {
- BottomLeft,
- BottomRight,
- TopLeft,
- TopRight,
- RightBottom,
- RightTop,
- LeftBottom,
- LeftTop,
-}
-
-impl<'a> DotString<'a> for PageDirection {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- PageDirection::BottomLeft => "BL".into(),
- PageDirection::BottomRight => "BR".into(),
- PageDirection::TopLeft => "TL".into(),
- PageDirection::TopRight => "TR".into(),
- PageDirection::RightBottom => "RB".into(),
- PageDirection::RightTop => "RT".into(),
- PageDirection::LeftBottom => "LB".into(),
- PageDirection::LeftTop => "LT".into(),
- }
- }
-}
-
-/// Modifier indicating where on a node an edge should be aimed.
-/// If Port is used, the corresponding node must either have record shape with one of its
-/// fields having the given portname, or have an HTML-like label, one of whose components has a
-/// PORT attribute set to portname.
-#[derive(Clone, PartialEq, Eq, Debug)]
-pub enum PortPosition {
- Port {
- port_name: String,
- compass_point: Option<CompassPoint>,
- },
- Compass(CompassPoint),
-}
-
-// TODO: AsRef vs this?
-// See https://github.com/Peternator7/strum/blob/96ee0a9a307ec7d1a39809fb59037bd4e11557cc/strum/src/lib.rs
-impl<'a> DotString<'a> for PortPosition {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- PortPosition::Port {
- port_name,
- compass_point,
- } => {
- let mut dot_string = port_name.to_owned();
- if let Some(compass_point) = compass_point {
- dot_string
- .push_str(format!(":{}", compass_point.dot_string()).as_str());
- }
- dot_string.into()
- }
- PortPosition::Compass(p) => p.dot_string().into(),
- }
- }
-}
-
-/// Corresponding to directed graphs drawn from top to bottom, from left to right,
-/// from bottom to top, and from right to left, respectively.
-pub enum RankDir {
- TopBottom,
- LeftRight,
- BottomTop,
- RightLeft,
-}
-
-impl<'a> DotString<'a> for RankDir {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- RankDir::TopBottom => "TB".into(),
- RankDir::LeftRight => "LR".into(),
- RankDir::BottomTop => "BT".into(),
- RankDir::RightLeft => "RL".into(),
- }
- }
-}
-
-pub enum Ratio {
- Aspect(f32),
- Fill,
- Compress,
- Expand,
- Auto,
-}
-
-impl<'a> DotString<'a> for Ratio {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- Ratio::Aspect(aspect) => aspect.to_string().into(),
- Ratio::Fill => "fill".into(),
- Ratio::Compress => "compress".into(),
- Ratio::Expand => "expand".into(),
- Ratio::Auto => "auto".into(),
- }
- }
-}
-
-pub enum Shape {
- Box,
- Polygon,
- Ellipse,
- Oval,
- Circle,
- Point,
- Egg,
- Triangle,
- Plaintext,
- Plain,
- Diamond,
- Trapezium,
- Parallelogram,
- House,
- Pentagon,
- Hexagon,
- Septagon,
- Octagon,
- DoubleCircle,
- DoubleOctagon,
- TripleOctagon,
- Invtriangle,
- Invtrapezium,
- Invhouse,
- Mdiamond,
- Msquare,
- Mcircle,
- Record,
- Rect,
- Rectangle,
- Square,
- Star,
- None,
- Underline,
- Cylinder,
- Note,
- Tab,
- Folder,
- Box3D,
- Component,
- Promoter,
- Cds,
- Terminator,
- Utr,
- Primersite,
- Restrictionsite,
- FivePoverHang,
- ThreePoverHang,
- NoverHang,
- Assemply,
- Signature,
- Insulator,
- Ribosite,
- Rnastab,
- Proteasesite,
- Proteinstab,
- Rpromotor,
- Rarrow,
- Larrow,
- Lpromotor,
-}
-
-impl<'a> DotString<'a> for Shape {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- Shape::Box => "box".into(),
- Shape::Polygon => "polygon".into(),
- Shape::Ellipse => "ellipse".into(),
- Shape::Oval => "oval".into(),
- Shape::Circle => "circle".into(),
- Shape::Point => "point".into(),
- Shape::Egg => "egg".into(),
- Shape::Triangle => "triangle".into(),
- Shape::Plaintext => "plaintext".into(),
- Shape::Plain => "plain".into(),
- Shape::Diamond => "diamond".into(),
- Shape::Trapezium => "trapezium".into(),
- Shape::Parallelogram => "parallelogram".into(),
- Shape::House => "house".into(),
- Shape::Pentagon => "pentagon".into(),
- Shape::Hexagon => "hexagon".into(),
- Shape::Septagon => "septagon".into(),
- Shape::Octagon => "octagon".into(),
- Shape::DoubleCircle => "doublecircle".into(),
- Shape::DoubleOctagon => "doubleoctagon".into(),
- Shape::TripleOctagon => "tripleocctagon".into(),
- Shape::Invtriangle => "invtriangle".into(),
- Shape::Invtrapezium => "invtrapezium".into(),
- Shape::Invhouse => "invhouse".into(),
- Shape::Mdiamond => "mdiamond".into(),
- Shape::Msquare => "msquare".into(),
- Shape::Mcircle => "mcircle".into(),
- Shape::Record => "record".into(),
- Shape::Rect => "rect".into(),
- Shape::Rectangle => "rectangle".into(),
- Shape::Square => "square".into(),
- Shape::Star => "star".into(),
- Shape::None => "none".into(),
- Shape::Underline => "underline".into(),
- Shape::Cylinder => "cylinder".into(),
- Shape::Note => "note".into(),
- Shape::Tab => "tab".into(),
- Shape::Folder => "folder".into(),
- Shape::Box3D => "box3d".into(),
- Shape::Component => "component".into(),
- Shape::Promoter => "promoter".into(),
- Shape::Cds => "cds".into(),
- Shape::Terminator => "terminator".into(),
- Shape::Utr => "utr".into(),
- Shape::Primersite => "primersite".into(),
- Shape::Restrictionsite => "restrictionsite".into(),
- Shape::FivePoverHang => "fivepoverhang".into(),
- Shape::ThreePoverHang => "threepoverhang".into(),
- Shape::NoverHang => "noverhang".into(),
- Shape::Assemply => "assemply".into(),
- Shape::Signature => "signature".into(),
- Shape::Insulator => "insulator".into(),
- Shape::Ribosite => "ribosite".into(),
- Shape::Rnastab => "rnastab".into(),
- Shape::Proteasesite => "proteasesite".into(),
- Shape::Proteinstab => "proteinstab".into(),
- Shape::Rpromotor => "rpromotor".into(),
- Shape::Rarrow => "rarrow".into(),
- Shape::Larrow => "larrow".into(),
- Shape::Lpromotor => "lpromotor".into(),
- }
- }
-}
-
-/// Spline, edges are drawn as splines routed around nodes
-/// Line, edges are drawn as line segments
-/// Polygon, specifies that edges should be drawn as polylines.
-/// Ortho, specifies edges should be routed as polylines of axis-aligned segments.
-/// Curved, specifies edges should be drawn as curved arcs.
-/// splines=line and splines=spline can be used as synonyms for
-/// splines=false and splines=true, respectively.
-pub enum Splines {
- Line,
- Spline,
- None,
- Curved,
- Polyline,
- Ortho,
-}
-
-impl<'a> DotString<'a> for Splines {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- Splines::Line => "line".into(),
- Splines::Spline => "spline".into(),
- Splines::None => "none".into(),
- Splines::Curved => "curved".into(),
- Splines::Polyline => "polyline".into(),
- Splines::Ortho => "ortho".into(),
- }
- }
-}
-
-/// The text for a graphviz label on a node or edge.
-#[derive(Clone, PartialEq, Eq, Debug)]
-pub enum AttributeText<'a> {
- /// Preserves the text directly as is.
- AttrStr(Cow<'a, str>),
-
- /// This kind of label uses the graphviz label escString type:
- /// <http://www.graphviz.org/doc/info/attrs.html#k:escString>
- ///
- /// Occurrences of backslashes (`\`) are not escaped; instead they
- /// are interpreted as initiating an escString escape sequence.
- ///
- /// Escape sequences of particular interest: in addition to `\n`
- /// to break a line (centering the line preceding the `\n`), there
- /// are also the escape sequences `\l` which left-justifies the
- /// preceding line and `\r` which right-justifies it.
- EscStr(Cow<'a, str>),
-
- /// This uses a graphviz [HTML string label][html].
- /// The string is printed exactly as given, but between `<` and `>`.
- /// **No escaping is performed.**
- ///
- /// [html]: https://graphviz.org/doc/info/shapes.html#html
- HtmlStr(Cow<'a, str>),
-
- /// Preserves the text directly as is but wrapped in quotes.
- ///
- /// Occurrences of backslashes (`\`) are escaped, and thus appear
- /// as backslashes in the rendered label.
- QuotedStr(Cow<'a, str>),
-}
-
-impl<'a> AttributeText<'a> {
- pub fn attr<S: Into<Cow<'a, str>>>(s: S) -> AttributeText<'a> {
- AttrStr(s.into())
- }
-
- pub fn escaped<S: Into<Cow<'a, str>>>(s: S) -> AttributeText<'a> {
- EscStr(s.into())
- }
-
- pub fn html<S: Into<Cow<'a, str>>>(s: S) -> AttributeText<'a> {
- HtmlStr(s.into())
- }
-
- pub fn quoted<S: Into<Cow<'a, str>>>(s: S) -> AttributeText<'a> {
- QuotedStr(s.into())
- }
-
- fn escape_char<F>(c: char, mut f: F)
- where
- F: FnMut(char),
- {
- match c {
- // not escaping \\, since Graphviz escString needs to
- // interpret backslashes; see EscStr above.
- '\\' => f(c),
- _ => {
- for c in c.escape_default() {
- f(c)
- }
- }
- }
- }
-
- fn escape_str(s: &str) -> String {
- let mut out = String::with_capacity(s.len());
- for c in s.chars() {
- AttributeText::escape_char(c, |c| out.push(c));
- }
- out
- }
-
- /// Renders text as string suitable for a attribute in a .dot file.
- /// This includes quotes or suitable delimiters.
- pub fn dot_string(&self) -> String {
- match *self {
- AttrStr(ref s) => format!("{}", s),
- EscStr(ref s) => format!("\"{}\"", AttributeText::escape_str(&s)),
- HtmlStr(ref s) => format!("<{}>", s),
- QuotedStr(ref s) => format!("\"{}\"", s.escape_default()),
- }
- }
-}
-
-impl<'a> From<ArrowType> for AttributeText<'a> {
- fn from(arrow_type: ArrowType) -> Self {
- AttributeText::attr(arrow_type.dot_string())
- }
-}
-
-impl<'a> From<bool> for AttributeText<'a> {
- fn from(v: bool) -> Self {
- AttributeText::attr(v.to_string())
- }
-}
-
-impl<'a> From<ClusterMode> for AttributeText<'a> {
- fn from(mode: ClusterMode) -> Self {
- AttributeText::quoted(mode.dot_string())
- }
-}
-
-impl<'a> From<Color<'a>> for AttributeText<'a> {
- fn from(color: Color<'a>) -> Self {
- AttributeText::quoted(color.dot_string())
- }
-}
-
-impl<'a> From<ColorList<'a>> for AttributeText<'a> {
- fn from(color_list: ColorList<'a>) -> Self {
- AttributeText::quoted(color_list.dot_string())
- }
-}
-
-impl<'a> From<CompassPoint> for AttributeText<'a> {
- fn from(compass: CompassPoint) -> Self {
- AttributeText::quoted(compass.dot_string())
- }
-}
-
-impl<'a> From<Direction> for AttributeText<'a> {
- fn from(direction: Direction) -> Self {
- AttributeText::attr(direction.dot_string())
- }
-}
-
-impl<'a> From<EdgeStyle> for AttributeText<'a> {
- fn from(style: EdgeStyle) -> Self {
- AttributeText::attr(style.dot_string())
- }
-}
-
-impl<'a> From<f32> for AttributeText<'a> {
- fn from(v: f32) -> Self {
- AttributeText::attr(v.to_string())
- }
-}
-
-impl<'a> From<GraphStyle> for AttributeText<'a> {
- fn from(style: GraphStyle) -> Self {
- AttributeText::attr(style.dot_string())
- }
-}
-
-impl<'a> From<LabelJustification> for AttributeText<'a> {
- fn from(label_justification: LabelJustification) -> Self {
- AttributeText::attr(label_justification.dot_string())
- }
-}
-
-impl<'a> From<LabelLocation> for AttributeText<'a> {
- fn from(label_location: LabelLocation) -> Self {
- AttributeText::attr(label_location.dot_string())
- }
-}
-
-impl<'a> From<NodeStyle> for AttributeText<'a> {
- fn from(style: NodeStyle) -> Self {
- AttributeText::attr(style.dot_string())
- }
-}
-
-impl<'a> From<Ordering> for AttributeText<'a> {
- fn from(ordering: Ordering) -> Self {
- AttributeText::quoted(ordering.dot_string())
- }
-}
-
-impl<'a> From<OutputMode> for AttributeText<'a> {
- fn from(mode: OutputMode) -> Self {
- AttributeText::quoted(mode.dot_string())
- }
-}
-
-impl<'a> From<PackMode> for AttributeText<'a> {
- fn from(mode: PackMode) -> Self {
- AttributeText::quoted(mode.dot_string())
- }
-}
-
-impl<'a> From<PageDirection> for AttributeText<'a> {
- fn from(page_direction: PageDirection) -> Self {
- AttributeText::attr(page_direction.dot_string())
- }
-}
-
-impl<'a> From<Point> for AttributeText<'a> {
- fn from(point: Point) -> Self {
- AttributeText::quoted(point.dot_string())
- }
-}
-
-impl<'a> From<PortPosition> for AttributeText<'a> {
- fn from(port_position: PortPosition) -> Self {
- AttributeText::quoted(port_position.dot_string())
- }
-}
-
-impl<'a> From<RankDir> for AttributeText<'a> {
- fn from(rank_dir: RankDir) -> Self {
- AttributeText::attr(rank_dir.dot_string())
- }
-}
-
-impl<'a> From<Ratio> for AttributeText<'a> {
- fn from(ratio: Ratio) -> Self {
- match ratio {
- Ratio::Aspect(_aspect) => AttributeText::attr(ratio.dot_string()),
- _ => AttributeText::quoted(ratio.dot_string()),
- }
- }
-}
-
-impl<'a> From<Rectangle> for AttributeText<'a> {
- fn from(rectangle: Rectangle) -> Self {
- AttributeText::quoted(rectangle.dot_string())
- }
-}
-
-impl<'a> From<Shape> for AttributeText<'a> {
- fn from(shape: Shape) -> Self {
- AttributeText::attr(shape.dot_string())
- }
-}
-
-impl<'a> From<Splines> for AttributeText<'a> {
- fn from(splines: Splines) -> Self {
- AttributeText::quoted(splines.dot_string())
- }
-}
-
-impl<'a> From<SplineType> for AttributeText<'a> {
- fn from(spline_type: SplineType) -> Self {
- AttributeText::quoted(spline_type.dot_string())
- }
-}
-
-impl<'a> From<Styles> for AttributeText<'a> {
- fn from(styles: Styles) -> Self {
- match styles {
- Styles::Edge(s) => AttributeText::from(s),
- Styles::Node(s) => AttributeText::from(s),
- Styles::Graph(s) => AttributeText::from(s),
- }
- }
-}
-
-impl<'a> From<u32> for AttributeText<'a> {
- fn from(v: u32) -> Self {
- AttributeText::attr(v.to_string())
- }
-}
-
-
-// TODO: probably dont need this struct and can move impl methods into lib module
-pub struct Dot<'a> {
- graph: Graph<'a>,
-}
-
-impl<'a> Dot<'a> {
- /// Renders directed graph `g` into the writer `w` in DOT syntax.
- /// (Simple wrapper around `render_opts` that passes a default set of options.)
- //pub fn render<W>(self, g: Graph, w: &mut W) -> io::Result<()>
- pub fn render<W>(self, w: &mut W) -> io::Result<()>
- where
- W: Write,
- {
- // TODO: use default_options?
- self.render_opts(w, &[])
- }
-
- // io::Result<()> vs Result<(), Box<dyn Error>>
- // https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html#the--operator-can-be-used-in-functions-that-return-result
- /// Renders directed graph `g` into the writer `w` in DOT syntax.
- /// (Main entry point for the library.)
- // pub fn render_opts<W>(self, graph: Graph, w: &mut W, options: &[RenderOption]) -> io::Result<()>
- pub fn render_opts<W>(self, w: &mut W, options: &[RenderOption]) -> io::Result<()>
- where
- W: Write,
- {
- if let Some(comment) = &self.graph.comment {
- // TODO: split comment into lines of 80 or so characters
- writeln!(w, "// {}", comment)?;
- }
-
- let edge_op = &self.graph.edge_op();
- let strict = if self.graph.strict { "strict " } else { "" };
- write!(w, "{}{}", strict, &self.graph.graph_type())?;
-
- if let Some(id) = &self.graph.id {
- write!(w, " {}", id)?;
- }
-
- writeln!(w, " {{")?;
-
- if let Some(graph_attributes) = self.graph.graph_attributes {
- write!(w, "{}{}\n", INDENT, graph_attributes.dot_string())?;
- }
-
- if let Some(node_attributes) = self.graph.node_attributes {
- write!(w, "{}{}\n", INDENT, node_attributes.dot_string())?;
- }
-
- if let Some(edge_attributes) = self.graph.edge_attributes {
- write!(w, "{}{}\n", INDENT, edge_attributes.dot_string())?;
- }
-
- for n in self.graph.nodes {
- // TODO: handle render options
- // Are render options something we need?
- // we could clone the node or and remove the attributes based on render options
- // or maybe we keep a set of attributes to ignore based on the options
- writeln!(w, "{}{}", INDENT, n.dot_string())?;
- }
-
- for e in self.graph.edges {
- let mut edge_source = e.source;
- if let Some(source_port_position) = e.source_port_position {
- edge_source
- .push_str(format!(":{}", source_port_position.dot_string()).as_str())
- }
-
- let mut edge_target = e.target;
- if let Some(target_port_position) = e.target_port_position {
- edge_target
- .push_str(format!(":{}", target_port_position.dot_string()).as_str())
- }
-
- write!(w, "{}{} {} {}", INDENT, edge_source, edge_op, edge_target)?;
- // TODO: render ops
- if !e.attributes.is_empty() {
- write!(w, " [")?;
-
- let mut iter = e.attributes.iter();
- let first = iter.next().unwrap();
- write!(w, "{}={}", first.0, first.1.dot_string())?;
- for (key, value) in iter {
- write!(w, ", ")?;
- write!(w, "{}={}", key, value.dot_string())?;
- }
- write!(w, "]")?;
- }
- writeln!(w, ";")?;
- }
-
- writeln!(w, "}}")
- }
-}
-
-#[derive(Hash, Eq, PartialEq, PartialOrd, Ord, Debug, Clone)]
-pub enum AttributeType {
- Graph,
- Node,
- Edge,
-}
-
-pub struct Graph<'a> {
- pub id: Option<String>,
-
- pub is_directed: bool,
-
- pub strict: bool,
-
- /// Comment added to the first line of the source.
- pub comment: Option<String>,
-
- pub graph_attributes: Option<GraphAttributeStatement<'a>>,
-
- pub node_attributes: Option<NodeAttributeStatement<'a>>,
-
- pub edge_attributes: Option<EdgeAttributeStatement<'a>>,
-
- pub nodes: Vec<Node<'a>>,
-
- pub edges: Vec<Edge<'a>>,
-}
-
-impl<'a> Graph<'a> {
- pub fn new(
- id: Option<String>,
- is_directed: bool,
- strict: bool,
- comment: Option<String>,
- graph_attributes: Option<GraphAttributeStatement<'a>>,
- node_attributes: Option<NodeAttributeStatement<'a>>,
- edge_attributes: Option<EdgeAttributeStatement<'a>>,
- nodes: Vec<Node<'a>>,
- edges: Vec<Edge<'a>>,
- ) -> Self {
- Self {
- id,
- is_directed,
- strict,
- comment,
- graph_attributes,
- node_attributes,
- edge_attributes,
- nodes,
- edges,
- }
- }
-
- pub fn graph_type(&self) -> &'static str {
- if self.is_directed {
- "digraph"
- } else {
- "graph"
- }
- }
-
- pub fn edge_op(&self) -> &'static str {
- if self.is_directed {
- "->"
- } else {
- "--"
- }
- }
-}
-
-pub struct GraphBuilder<'a> {
- id: Option<String>,
-
- is_directed: bool,
-
- strict: bool,
-
- graph_attributes: Option<GraphAttributeStatement<'a>>,
-
- node_attributes: Option<NodeAttributeStatement<'a>>,
-
- edge_attributes: Option<EdgeAttributeStatement<'a>>,
-
- nodes: Vec<Node<'a>>,
-
- edges: Vec<Edge<'a>>,
-
- comment: Option<String>,
-}
-
-// TODO: id should be an escString
-impl<'a> GraphBuilder<'a> {
- pub fn new_directed(id: Option<String>) -> Self {
- Self {
- id,
- is_directed: true,
- strict: false,
- graph_attributes: None,
- node_attributes: None,
- edge_attributes: None,
- nodes: Vec::new(),
- edges: Vec::new(),
- comment: None,
- }
- }
-
- pub fn new_undirected(id: Option<String>) -> Self {
- Self {
- id,
- is_directed: false,
- strict: false,
- graph_attributes: None,
- node_attributes: None,
- edge_attributes: None,
- nodes: Vec::new(),
- edges: Vec::new(),
- comment: None,
- }
- }
-
- pub fn comment<S: Into<String>>(&mut self, comment: S) -> &mut Self {
- self.comment = Some(comment.into());
- self
- }
-
- pub fn add_graph_attributes(
- &mut self,
- graph_attributes: GraphAttributeStatement<'a>,
- ) -> &mut Self {
- self.graph_attributes = Some(graph_attributes);
- self
- }
-
- pub fn add_node_attributes(
- &mut self,
- node_attributes: NodeAttributeStatement<'a>,
- ) -> &mut Self {
- self.node_attributes = Some(node_attributes);
- self
- }
-
- pub fn add_edge_attributes(
- &mut self,
- edge_attributes: EdgeAttributeStatement<'a>,
- ) -> &mut Self {
- self.edge_attributes = Some(edge_attributes);
- self
- }
-
- // TODO: update to insert into appropriate statement or remove?
- // pub fn add_attribute(
- // &mut self,
- // attribute_type: AttributeType,
- // key: String, value: AttributeText<'a>
- // ) -> &mut Self {
- // self.get_attributes(attribute_type).insert(key, value);
- // self
- // }
- //
- // pub fn add_attributes(
- // &mut self,
- // attribute_type: AttributeType,
- // attributes: HashMap<String, AttributeText<'a>>
- // ) -> &mut Self {
- // self.get_attributes(attribute_type).extend(attributes);
- // self
- // }
-
- pub fn add_attribute(
- &mut self,
- attribute_type: AttributeType,
- key: String,
- value: AttributeText<'a>,
- ) -> &mut Self {
- match attribute_type {
- AttributeType::Graph => {
- if self.graph_attributes.is_none() {
- self.graph_attributes = Some(GraphAttributeStatement::new());
- }
- self.graph_attributes
- .as_mut()
- .unwrap()
- .add_attribute(key, value);
- }
- AttributeType::Edge => {
- if self.edge_attributes.is_none() {
- self.edge_attributes = Some(EdgeAttributeStatement::new());
- }
- self.edge_attributes
- .as_mut()
- .unwrap()
- .add_attribute(key, value);
- }
- AttributeType::Node => {
- if self.node_attributes.is_none() {
- self.node_attributes = Some(NodeAttributeStatement::new());
- }
- self.node_attributes
- .as_mut()
- .unwrap()
- .add_attribute(key, value);
- }
- }
- self
- }
-
- pub fn add_node(&mut self, node: Node<'a>) -> &mut Self {
- self.nodes.push(node);
- self
- }
-
- pub fn add_edge(&mut self, edge: Edge<'a>) -> &mut Self {
- self.edges.push(edge);
- self
- }
-
- pub fn strict(&mut self) -> &mut Self {
- self.strict = true;
- self
- }
-
- pub fn build(&self) -> Graph<'a> {
- Graph {
- id: self.id.to_owned(),
- is_directed: self.is_directed,
- strict: self.strict,
- comment: self.comment.clone(), // TODO: is clone the only option here?
- graph_attributes: self.graph_attributes.clone(),
- node_attributes: self.node_attributes.clone(),
- edge_attributes: self.edge_attributes.clone(),
- nodes: self.nodes.clone(), // TODO: is clone the only option here?
- edges: self.edges.clone(), // TODO: is clone the only option here?
- }
- }
-}
-
-pub trait GraphAttributes<'a> {
- fn background(&mut self, background: String) -> &mut Self {
- self.add_attribute("_background", AttributeText::attr(background))
- }
-
- /// The color used as the background for entire canvas.
- fn background_color(&mut self, background_color: Color<'a>) -> &mut Self {
- self.add_attribute("bgcolor", AttributeText::from(background_color))
- }
-
- // TODO: constrain
- /// The color used as the background for entire canvas with a gradient fill.
- /// A colon-separated list of weighted color values: WC(:WC)* where each WC has the form C(;F)?
- /// with C a color value and the optional F a floating-point number, 0 ≤ F ≤ 1.
- /// The sum of the floating-point numbers in a colorList must sum to at most 1.
- fn background_colorlist(&mut self, background_colors: ColorList<'a>) -> &mut Self {
- self.add_attribute("bgcolor", AttributeText::from(background_colors))
- }
-
- /// Type: rect which is "%f,%f,%f,%f"
- /// The rectangle llx,lly,urx,ury gives the coordinates, in points, of the lower-left corner (llx,lly)
- /// and the upper-right corner (urx,ury).
- fn bounding_box(&mut self, bounding_box: String) -> &mut Self {
- self.add_attribute("bb", AttributeText::quoted(bounding_box))
- }
-
- /// If true, the drawing is centered in the output canvas.
- fn center(&mut self, center: bool) -> &mut Self {
- self.add_attribute("center", AttributeText::from(center))
- }
-
- /// Specifies the character encoding used when interpreting string input as a text label.
- fn charset(&mut self, charset: String) -> &mut Self {
- self.add_attribute("charset", AttributeText::quoted(charset))
- }
-
- /// Classnames to attach to the node, edge, graph, or cluster’s SVG element.
- /// Combine with stylesheet for styling SVG output using CSS classnames.
- /// Multiple space-separated classes are supported.
- fn class(&mut self, class: String) -> &mut Self {
- Attributes::class(self.get_attributes_mut(), class);
- self
- }
-
- /// Mode used for handling clusters.
- /// If clusterrank=local, a subgraph whose name begins with cluster is given special treatment.
- /// The subgraph is laid out separately, and then integrated as a unit into its parent graph,
- /// with a bounding rectangle drawn about it.
- /// If the cluster has a label parameter, this label is displayed within the rectangle.
- /// Note also that there can be clusters within clusters.
- /// The modes clusterrank=global and clusterrank=none appear to be identical, both turning off the special cluster processing.
- fn cluster_rank(&mut self, cluster_rank: ClusterMode) -> &mut Self {
- self.add_attribute("clusterrank", AttributeText::from(cluster_rank))
- }
-
- /// This attribute specifies a color scheme namespace: the context for interpreting color names.
- /// In particular, if a color value has form "xxx" or "//xxx", then the color xxx will be evaluated
- /// according to the current color scheme. If no color scheme is set, the standard X11 naming is used.
- /// For example, if colorscheme=bugn9, then color=7 is interpreted as color="/bugn9/7".
- fn color_scheme(&mut self, color_scheme: String) -> &mut Self {
- Attributes::color_scheme(self.get_attributes_mut(), color_scheme);
- self
- }
-
- /// Comments are inserted into output. Device-dependent
- fn comment(&mut self, comment: String) -> &mut Self {
- Attributes::comment(self.get_attributes_mut(), comment);
- self
- }
-
- fn compound(&mut self, compound: String) -> &mut Self {
- self.add_attribute("compound", AttributeText::quoted(compound))
- }
-
- fn concentrate(&mut self, concentrate: String) -> &mut Self {
- self.add_attribute("concentrate", AttributeText::quoted(concentrate))
- }
-
- /// Specifies the expected number of pixels per inch on a display device.
- /// Also known as resolution
- fn dpi(&mut self, dpi: f32) -> &mut Self {
- self.add_attribute("dpi", AttributeText::from(dpi))
- }
-
- /// Color used to fill the background of a node or cluster assuming style=filled, or a filled arrowhead.
- fn fill_color(&mut self, fill_color: Color<'a>) -> &mut Self {
- Attributes::fill_color(self.get_attributes_mut(), fill_color);
- self
- }
-
- /// Color used to fill the background, with a gradient, of a node or cluster assuming
- /// style=filled, or a filled arrowhead.
- fn fill_color_with_colorlist(&mut self, fill_colors: ColorList<'a>) -> &mut Self {
- Attributes::fill_color_with_colorlist(self.get_attributes_mut(), fill_colors);
- self
- }
-
- /// Color used to fill the background, with a gradient, of a node or cluster assuming
- /// style=filled, or a filled arrowhead.
- /// TODO: example
- /// [crate::GraphAttributes::dpi]
- fn fill_color_with_iter<I>(&mut self, fill_colors: I) -> &mut Self
- where
- I: IntoIterator,
- I::Item: IntoWeightedColor<'a>,
- {
- Attributes::fill_color_with_iter(self.get_attributes_mut(), fill_colors);
- self
- }
-
- /// Color used for text.
- fn font_color(&mut self, font_color: Color<'a>) -> &mut Self {
- Attributes::font_color(self.get_attributes_mut(), font_color);
- self
- }
-
- /// Font used for text.
- fn font_name(&mut self, font_name: String) -> &mut Self {
- Attributes::font_name(self.get_attributes_mut(), font_name);
- self
- }
-
- fn font_names(&mut self, font_names: String) -> &mut Self {
- self.add_attribute("fontnames", AttributeText::quoted(font_names))
- }
-
- fn font_path(&mut self, font_path: String) -> &mut Self {
- self.add_attribute("fontpath", AttributeText::quoted(font_path))
- }
-
- // TODO: constrain
- /// Font size, in points, used for text.
- /// default: 14.0, minimum: 1.0
- fn font_size(&mut self, font_size: f32) -> &mut Self {
- Attributes::font_size(self.get_attributes_mut(), font_size);
- self
- }
-
- fn force_label(&mut self, force_label: bool) -> &mut Self {
- self.add_attribute("forcelabel", AttributeText::from(force_label))
- }
-
- /// If a gradient fill is being used, this determines the angle of the fill.
- fn gradient_angle(&mut self, gradient_angle: u32) -> &mut Self {
- Attributes::gradient_angle(self.get_attributes_mut(), gradient_angle);
- self
- }
-
- fn image_path(&mut self, image_path: String) -> &mut Self {
- self.add_attribute("imagepath", AttributeText::escaped(image_path))
- }
-
- /// An escString or an HTML label.
- fn label(&mut self, label: String) -> &mut Self {
- Attributes::label(self.get_attributes_mut(), label);
- self
- }
-
- /// If labeljust=r, the label is right-justified within bounding rectangle
- /// If labeljust=l, left-justified
- /// Else the label is centered.
- fn label_justification(
- &mut self,
- label_justification: LabelJustification,
- ) -> &mut Self {
- self.add_attribute("labeljust", AttributeText::from(label_justification))
- }
-
- // Vertical placement of labels for nodes, root graphs and clusters.
- // For graphs and clusters, only labelloc=t and labelloc=b are allowed, corresponding to
- // placement at the top and bottom, respectively.
- // By default, root graph labels go on the bottom and cluster labels go on the top.
- // Note that a subgraph inherits attributes from its parent. Thus, if the root graph sets
- // labelloc=b, the subgraph inherits this value.
- // For nodes, this attribute is used only when the height of the node is larger than the height
- // of its label.
- // If labelloc=t, labelloc=c, labelloc=b, the label is aligned with the top, centered, or
- // aligned with the bottom of the node, respectively.
- // By default, the label is vertically centered.
- fn label_location(&mut self, label_location: LabelLocation) -> &mut Self {
- Attributes::label_location(self.get_attributes_mut(), label_location);
- self
- }
-
- fn landscape(&mut self, landscape: bool) -> &mut Self {
- self.add_attribute("landscape", AttributeText::from(landscape))
- }
-
- /// Specifies the separator characters used to split an attribute of type layerRange into a
- /// list of ranges.
- fn layer_list_sep(&mut self, layer_list_sep: String) -> &mut Self {
- self.add_attribute("layerlistsep", AttributeText::attr(layer_list_sep))
- }
-
- /// Specifies a linearly ordered list of layer names attached to the graph
- /// The graph is then output in separate layers.
- /// Only those components belonging to the current output layer appear.
- fn layers(&mut self, layers: String) -> &mut Self {
- Attributes::layer(self.get_attributes_mut(), layers);
- self
- }
-
- /// Selects a list of layers to be emitted.
- fn layer_select(&mut self, layer_select: String) -> &mut Self {
- self.add_attribute("layerselect", AttributeText::attr(layer_select))
- }
-
- /// Specifies the separator characters used to split the layers attribute into a list of layer names.
- /// default: ":\t "
- fn layer_sep(&mut self, layer_sep: String) -> &mut Self {
- self.add_attribute("layersep", AttributeText::attr(layer_sep))
- }
-
- /// Height of graph or cluster label, in inches.
- fn lheight(&mut self, lheight: f32) -> &mut Self {
- self.add_attribute("lheight", AttributeText::from(lheight))
- }
-
- /// Label position
- /// The position indicates the center of the label.
- fn label_position(&mut self, lp: Point) -> &mut Self {
- Attributes::label_position(self.get_attributes_mut(), lp);
- self
- }
-
- /// Width of graph or cluster label, in inches.
- fn lwidth(&mut self, lwidth: f32) -> &mut Self {
- self.add_attribute("lwidth", AttributeText::from(lwidth))
- }
-
- /// Sets x and y margins of canvas, in inches.
- /// Both margins are set equal to the given value.
- /// See [`crate::GraphAttributes::margin_point`]
- fn margin(&mut self, margin: f32) -> &mut Self {
- self.margin_point(Point::new_2d(margin, margin))
- }
-
- /// Sets x and y margins of canvas, in inches.
- /// Note that the margin is not part of the drawing but just empty space left around the drawing.
- /// The margin basically corresponds to a translation of drawing, as would be necessary to
- /// center a drawing on a page. Nothing is actually drawn in the margin.
- /// To actually extend the background of a drawing, see the pad attribute.
- /// Whilst it is possible to create a Point value with either a third co-ordinate
- /// or a forced position, these are ignored for printing.
- /// By default, the value is 0.11,0.055.
- fn margin_point(&mut self, margin: Point) -> &mut Self {
- Attributes::margin(self.get_attributes_mut(), margin);
- self
- }
-
- /// Multiplicative scale factor used to alter the MinQuit (default = 8) and
- /// MaxIter (default = 24) parameters used during crossing minimization.
- /// These correspond to the number of tries without improvement before quitting and the
- /// maximum number of iterations in each pass.
- fn mclimit(&mut self, mclimit: f32) -> &mut Self {
- self.add_attribute("mclimit", AttributeText::from(mclimit))
- }
-
- /// Specifies the minimum separation between all nodes.
- fn mindist(&mut self, mindist: u32) -> &mut Self {
- self.add_attribute("mindist", AttributeText::from(mindist))
- }
-
- /// Whether to use a single global ranking, ignoring clusters.
- /// The original ranking algorithm in dot is recursive on clusters.
- /// This can produce fewer ranks and a more compact layout, but sometimes at the cost of a
- /// head node being place on a higher rank than the tail node.
- /// It also assumes that a node is not constrained in separate, incompatible subgraphs.
- /// For example, a node cannot be in a cluster and also be constrained by rank=same with
- /// a node not in the cluster.
- /// This allows nodes to be subject to multiple constraints.
- /// Rank constraints will usually take precedence over edge constraints.
- fn newrank(&mut self, newrank: bool) -> &mut Self {
- self.add_attribute("newrank", AttributeText::from(newrank))
- }
-
- // TODO: add constraint
- /// specifies the minimum space between two adjacent nodes in the same rank, in inches.
- /// default: 0.25, minimum: 0.02
- fn nodesep(&mut self, nodesep: f32) -> &mut Self {
- self.add_attribute("nodesep", AttributeText::from(nodesep))
- }
-
- /// By default, the justification of multi-line labels is done within the largest context that makes sense.
- /// Thus, in the label of a polygonal node, a left-justified line will align with the left side
- /// of the node (shifted by the prescribed margin).
- /// In record nodes, left-justified line will line up with the left side of the enclosing column
- /// of fields.
- /// If nojustify=true, multi-line labels will be justified in the context of itself.
- /// For example, if nojustify is set, the first label line is long, and the second is shorter
- /// and left-justified,
- /// the second will align with the left-most character in the first line, regardless of how
- /// large the node might be.
- fn no_justify(&mut self, no_justify: bool) -> &mut Self {
- Attributes::no_justify(self.get_attributes_mut(), no_justify);
- self
- }
-
- /// Sets number of iterations in network simplex applications.
- /// nslimit is used in computing node x coordinates.
- /// If defined, # iterations = nslimit * # nodes; otherwise, # iterations = MAXINT.
- fn nslimit(&mut self, nslimit: f32) -> &mut Self {
- self.add_attribute("nslimit", AttributeText::from(nslimit))
- }
-
- /// If ordering="out", then the outedges of a node, that is, edges with the node as its tail
- /// node, must appear left-to-right in the same order in which they are defined in the input.
- ///
- /// If ordering="in", then the inedges of a node must appear left-to-right in the same order in
- /// which they are defined in the input.
- ///
- /// If defined as a graph or subgraph attribute, the value is applied to all nodes in the graph
- /// or subgraph.
- ///
- /// Note that the graph attribute takes precedence over the node attribute.
- fn ordering(&mut self, ordering: Ordering) -> &mut Self {
- Attributes::ordering(self.get_attributes_mut(), ordering);
- self
- }
-
- // TODO: constrain to 0 - 360. Docs say min is 360 which should be max right?
- /// When used on nodes: Angle, in degrees, to rotate polygon node shapes.
- /// For any number of polygon sides, 0 degrees rotation results in a flat base.
- /// When used on graphs: If "[lL]*", sets graph orientation to landscape.
- /// Used only if rotate is not defined.
- /// Default: 0.0 and minimum: 360.0
- fn orientation(&mut self, orientation: f32) -> &mut Self {
- Attributes::orientation(self.get_attributes_mut(), orientation);
- self
- }
-
- /// Specify order in which nodes and edges are drawn.
- /// default: breadthfirst
- fn output_order(&mut self, output_order: OutputMode) -> &mut Self {
- self.add_attribute("outputorder", AttributeText::from(output_order))
- }
-
- /// Whether each connected component of the graph should be laid out separately, and then the
- /// graphs packed together.
- /// If false, the entire graph is laid out together.
- /// The granularity and method of packing is influenced by the packmode attribute.
- fn pack(&mut self, pack: bool) -> &mut Self {
- self.add_attribute("pack", AttributeText::from(pack))
- }
-
- // TODO: constrain to non-negative integer.
- /// Whether each connected component of the graph should be laid out separately, and then
- /// the graphs packed together.
- /// This is used as the size, in points,of a margin around each part; otherwise, a default
- /// margin of 8 is used.
- /// pack is treated as true if the value of pack iso a non-negative integer.
- fn pack_int(&mut self, pack: u32) -> &mut Self {
- self.add_attribute("pack", AttributeText::from(pack))
- }
-
- /// This indicates how connected components should be packed (cf. packMode).
- /// Note that defining packmode will automatically turn on packing as though one had set pack=true.
- fn pack_mode(&mut self, pack_mode: PackMode) -> &mut Self {
- self.add_attribute("packmode", AttributeText::from(pack_mode))
- }
-
- /// Specifies how much, in inches, to extend the drawing area around the minimal area needed
- /// to draw the graph.
- /// Both the x and y pad values are set equal to the given value.
- /// See [`crate::GraphAttributes::pad_point`]
- fn pad(&mut self, pad: f32) -> &mut Self {
- self.pad_point(Point::new_2d(pad, pad))
- }
-
- /// Specifies how much, in inches, to extend the drawing area around the minimal area needed to
- /// draw the graph.
- /// This area is part of the drawing and will be filled with the background color, if appropriate.
- /// default: 0.0555
- fn pad_point(&mut self, pad: Point) -> &mut Self {
- self.add_attribute("pad", AttributeText::from(pad))
- }
-
- /// Width and height of output pages, in inches.
- /// Value given is used for both the width and height.
- fn page(&mut self, page: f32) -> &mut Self {
- self.add_attribute("page", AttributeText::from(page))
- }
-
- /// Width and height of output pages, in inches.
- fn page_point(&mut self, page: Point) -> &mut Self {
- self.add_attribute("page", AttributeText::from(page))
- }
-
- /// The order in which pages are emitted.
- /// Used only if page is set and applicable.
- /// Limited to one of the 8 row or column major orders.
- fn page_dir(&mut self, page_dir: PageDirection) -> &mut Self {
- self.add_attribute("pagedir", AttributeText::from(page_dir))
- }
-
- // TODO: constrain
- /// If quantum > 0.0, node label dimensions will be rounded to integral multiples of the quantum.
- /// default: 0.0, minimum: 0.0
- fn quantum(&mut self, quantum: f32) -> &mut Self {
- self.add_attribute("quantum", AttributeText::from(quantum))
- }
-
- /// Sets direction of graph layout.
- /// For example, if rankdir="LR", and barring cycles, an edge T -> H; will go from left to right.
- /// By default, graphs are laid out from top to bottom.
- /// This attribute also has a side-effect in determining how record nodes are interpreted.
- /// See record shapes.
- fn rank_dir(&mut self, rank_dir: RankDir) -> &mut Self {
- self.add_attribute("rankdir", AttributeText::from(rank_dir))
- }
-
- /// sets the desired rank separation, in inches.
- /// This is the minimum vertical distance between the bottom of the nodes in one rank
- /// and the tops of nodes in the next. If the value contains equally,
- /// the centers of all ranks are spaced equally apart.
- /// Note that both settings are possible, e.g., ranksep="1.2 equally".
- fn rank_sep(&mut self, rank_sep: String) -> &mut Self {
- self.add_attribute("ranksep", AttributeText::attr(rank_sep))
- }
-
- /// Sets the aspect ratio (drawing height/drawing width) for the drawing.
- /// Note that this is adjusted before the size attribute constraints are enforced.
- fn ratio(&mut self, ratio: Ratio) -> &mut Self {
- self.add_attribute("ratio", AttributeText::from(ratio))
- }
-
- /// If true and there are multiple clusters, run crossing minimization a second time.
- fn remincross(&mut self, remincross: bool) -> &mut Self {
- self.add_attribute("remincross", AttributeText::from(remincross))
- }
-
- /// If rotate=90, sets drawing orientation to landscape.
- fn rotate(&mut self, rotate: u32) -> &mut Self {
- self.add_attribute("rotate", AttributeText::from(rotate))
- }
-
- // TODO: constrain
- /// Print guide boxes in PostScript at the beginning of routesplines if showboxes=1, or at
- /// the end if showboxes=2.
- /// (Debugging, TB mode only!)
- /// default: 0, minimum: 0
- fn show_boxes(&mut self, show_boxes: u32) -> &mut Self {
- Attributes::show_boxes(self.get_attributes_mut(), show_boxes);
- self
- }
-
- /// Maximum width and height of drawing, in inches.
- /// Value used for both the width and the height.
- /// If defined and the drawing is larger than the given size, the drawing
- /// is uniformly scaled down so that it fits within the given size.
- /// If desired_min is true, and both both dimensions of the drawing
- /// are less than size, the drawing is scaled up uniformly until at
- /// least one dimension equals its dimension in size.
- /// See [`crate::GraphAttributes::size_point`]
- fn size(&mut self, size: u32, desired_min: bool) -> &mut Self {
- self.size_point(Point {
- x: size as f32,
- y: size as f32,
- z: None,
- force_pos: desired_min,
- })
- }
-
- /// Maximum width and height of drawing, in inches.
- /// If defined and the drawing is larger than the given size, the drawing
- /// is uniformly scaled down so that it fits within the given size.
- /// If desired_min is true, and both both dimensions of the drawing
- /// are less than size, the drawing is scaled up uniformly until at
- /// least one dimension equals its dimension in size.
- fn size_point(&mut self, size: Point) -> &mut Self {
- self.add_attribute("size", AttributeText::from(size))
- }
-
- /// If packmode indicates an array packing, sortv specifies an insertion order
- /// among the components, with smaller values inserted first.
- /// default: 0, minimum: 0
- fn sortv(&mut self, sortv: u32) -> &mut Self {
- Attributes::sortv(self.get_attributes_mut(), sortv);
- self
- }
-
- /// Controls how, and if, edges are represented.
- fn splines(&mut self, splines: Splines) -> &mut Self {
- self.add_attribute("splines", AttributeText::from(splines))
- }
-
- /// Set style information for components of the graph.
- fn style(&mut self, style: GraphStyle) -> &mut Self {
- Attributes::style(self.get_attributes_mut(), Styles::Graph(style));
- self
- }
-
- /// A URL or pathname specifying an XML style sheet, used in SVG output.
- /// Combine with class to style elements using CSS selectors.
- fn stylesheet(&mut self, stylesheet: String) -> &mut Self {
- self.add_attribute("stylesheet", AttributeText::attr(stylesheet))
- }
-
- /// If the object has a URL, this attribute determines which window of the browser is used for the URL.
- fn target(&mut self, target: String) -> &mut Self {
- Attributes::target(self.get_attributes_mut(), target);
- self
- }
-
- /// Whether internal bitmap rendering relies on a truecolor color model or uses a color palette.
- /// If truecolor is unset, truecolor is not used unless there is a shapefile property
- /// for some node in the graph.
- /// The output model will use the input model when possible.
- fn true_color(&mut self, true_color: bool) -> &mut Self {
- self.add_attribute("truecolor", AttributeText::from(true_color))
- }
-
- /// Hyperlinks incorporated into device-dependent output.
- fn url(&mut self, url: String) -> &mut Self {
- Attributes::url(self.get_attributes_mut(), url);
- self
- }
-
- // TODO: add a ViewPort Struct?
- /// Clipping window on final drawing.
- /// viewport supersedes any size attribute.
- /// The width and height of the viewport specify precisely the final size of the output.
- /// The viewPort W,H,Z,x,y or W,H,Z,N specifies a viewport for the final image.
- /// The pair (W,H) gives the dimensions (width and height) of the final image, in points.
- /// The optional Z is the zoom factor, i.e., the image in the original layout will be
- /// W/Z by H/Z points in size. By default, Z is 1.
- /// The optional last part is either a pair (x,y) giving a position in the original layout
- /// of the graph,
- /// in points, of the center of the viewport, or the name N of a node whose center should used
- /// as the focus.
- fn viewport(&mut self, viewport: String) -> &mut Self {
- self.add_attribute("viewport", AttributeText::attr(viewport))
- }
-
- /// Add an attribute to the node.
- fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self;
-
- /// Add multiple attributes to the node.
- fn add_attributes(
- &'a mut self,
- attributes: HashMap<String, AttributeText<'a>>,
- ) -> &mut Self;
-
- fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>>;
-}
-
-impl<'a> GraphAttributes<'a> for GraphAttributeStatementBuilder<'a> {
- fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self {
- self.attributes.insert(key.into(), value);
- self
- }
-
- /// Add multiple attributes to the node.
- fn add_attributes(
- &'a mut self,
- attributes: HashMap<String, AttributeText<'a>>,
- ) -> &mut Self {
- self.attributes.extend(attributes);
- self
- }
-
- fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> {
- &mut self.attributes
- }
-}
-
-// I'm not a huge fan of needing this builder but having a hard time getting around &mut without it
-pub struct GraphAttributeStatementBuilder<'a> {
- pub attributes: IndexMap<String, AttributeText<'a>>,
-}
-
-impl<'a> GraphAttributeStatementBuilder<'a> {
- pub fn new() -> Self {
- Self {
- attributes: IndexMap::new(),
- }
- }
-
- pub fn build(&self) -> GraphAttributeStatement<'a> {
- GraphAttributeStatement {
- attributes: self.attributes.clone(),
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct GraphAttributeStatement<'a> {
- pub attributes: IndexMap<String, AttributeText<'a>>,
-}
-
-impl<'a> GraphAttributeStatement<'a> {
- pub fn new() -> Self {
- Self {
- attributes: IndexMap::new(),
- }
- }
-
- pub fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self {
- self.attributes.insert(key.into(), value);
- self
- }
-}
-
-impl<'a> AttributeStatement<'a> for GraphAttributeStatement<'a> {
- fn get_attribute_statement_type(&self) -> &'static str {
- "graph"
- }
-
- fn get_attributes(&self) -> &IndexMap<String, AttributeText<'a>> {
- &self.attributes
- }
-}
-
-// The optional '!' indicates the node position should not change (input-only).
-pub struct Point {
- pub x: f32,
- pub y: f32,
- pub z: Option<f32>,
-
- /// specify that the node position should not change.
- pub force_pos: bool,
-}
-
-impl Point {
- pub fn new_2d(x: f32, y: f32) -> Self {
- Self::new(x, y, None, false)
- }
-
- pub fn new_3d(x: f32, y: f32, z: f32) -> Self {
- Self::new(x, y, Some(z), false)
- }
-
- pub fn new(x: f32, y: f32, z: Option<f32>, force_pos: bool) -> Self {
- Self { x, y, z, force_pos }
- }
-}
-
-impl<'a> DotString<'a> for Point {
- fn dot_string(&self) -> Cow<'a, str> {
- let mut slice = format!("{:.1},{:.1}", self.x, self.y);
- if self.z.is_some() {
- slice.push_str(format!(",{:.1}", self.z.unwrap()).as_str());
- }
- if self.force_pos {
- slice.push_str("!")
- }
- slice.into()
- }
-}
-
-pub struct Rectangle {
- lower_left: Point,
- upper_right: Point,
-}
-
-impl<'a> DotString<'a> for Rectangle {
- fn dot_string(&self) -> Cow<'a, str> {
- format!(
- "{:.1},{:.1},{:.1},{:.1}",
- self.lower_left.x, self.lower_left.y, self.upper_right.x, self.upper_right.y
- )
- .into()
- }
-}
-
-/// The number of points in the list must be equivalent to 1 mod 3; note that this is not checked.
-/// TODO: should we check?
-pub struct SplineType {
- start: Option<Point>,
- end: Option<Point>,
- spline_points: Vec<Point>,
-}
-
-impl<'a> DotString<'a> for SplineType {
- fn dot_string(&self) -> Cow<'a, str> {
- let mut dot_string = String::from("");
-
- if let Some(end) = &self.end {
- dot_string.push_str(format!("e,{:.1},{:.1} ", end.x, end.y).as_str());
- }
-
- if let Some(start) = &self.start {
- dot_string.push_str(format!("s,{:.1},{:.1} ", start.x, start.y).as_str());
- }
-
- let mut iter = self.spline_points.iter();
- let first = iter.next().unwrap();
- dot_string.push_str(format!("{}", first.dot_string()).as_str());
- for point in iter {
- dot_string.push_str(" ");
- dot_string.push_str(format!("{}", point.dot_string()).as_str());
- }
-
- dot_string.into()
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Node<'a> {
- pub id: String,
- pub attributes: IndexMap<String, AttributeText<'a>>,
-}
-
-impl<'a> Node<'a> {
- pub fn new(id: String) -> Node<'a> {
- // TODO: constrain id
- Node {
- id,
- attributes: IndexMap::new(),
- }
- }
-}
-
-impl<'a> DotString<'a> for Node<'a> {
- fn dot_string(&self) -> Cow<'a, str> {
- let mut dot_string = format!("{}", &self.id);
- if !self.attributes.is_empty() {
- dot_string.push_str(" [");
- let mut iter = self.attributes.iter();
- let first = iter.next().unwrap();
- dot_string
- .push_str(format!("{}={}", first.0, first.1.dot_string()).as_str());
- for (key, value) in iter {
- dot_string.push_str(", ");
- dot_string.push_str(format!("{}={}", key, value.dot_string()).as_str());
- }
-
- dot_string.push_str("]");
- }
- dot_string.push_str(";");
- dot_string.into()
- }
-}
-
-pub struct NodeBuilder<'a> {
- id: String,
- attributes: IndexMap<String, AttributeText<'a>>,
-}
-
-impl<'a> NodeAttributes<'a> for NodeBuilder<'a> {
- fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self {
- self.attributes.insert(key.into(), value);
- self
- }
-
- /// Add multiple attribures to the edge.
- fn add_attributes(
- &'a mut self,
- attributes: HashMap<String, AttributeText<'a>>,
- ) -> &mut Self {
- self.attributes.extend(attributes);
- self
- }
-
- fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> {
- &mut self.attributes
- }
-}
-
-impl<'a> NodeBuilder<'a> {
- pub fn new(id: String) -> Self {
- Self {
- id,
- attributes: IndexMap::new(),
- }
- }
-
- pub fn build(&self) -> Node<'a> {
- Node {
- // TODO: are these to_owned and clones necessary?
- id: self.id.to_owned(),
- attributes: self.attributes.clone(),
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct Edge<'a> {
- pub source: String,
- pub source_port_position: Option<PortPosition>,
- pub target: String,
- pub target_port_position: Option<PortPosition>,
- pub attributes: IndexMap<String, AttributeText<'a>>,
-}
-
-impl<'a> Edge<'a> {
- pub fn new(source: String, target: String) -> Self {
- Self {
- source,
- source_port_position: None,
- target,
- target_port_position: None,
- attributes: IndexMap::new(),
- }
- }
-
- pub fn new_with_position(
- source: String,
- source_port_position: PortPosition,
- target: String,
- target_port_position: PortPosition,
- ) -> Self {
- Self {
- source,
- source_port_position: Some(source_port_position),
- target,
- target_port_position: Some(target_port_position),
- attributes: IndexMap::new(),
- }
- }
-}
-
-pub struct EdgeBuilder<'a> {
- pub source: String,
- pub source_port_position: Option<PortPosition>,
- pub target: String,
- pub target_port_position: Option<PortPosition>,
- attributes: IndexMap<String, AttributeText<'a>>,
-}
-
-impl<'a> EdgeAttributes<'a> for EdgeBuilder<'a> {
- fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self {
- self.attributes.insert(key.into(), value);
- self
- }
-
- fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> {
- &mut self.attributes
- }
-
- // /// Add multiple attributes to the edge.
- // fn add_attributes(&'a mut self, attributes: HashMap<String, AttributeText<'a>>) -> &mut Self {
- // self.attributes.extend(attributes);
- // self
- // }
-}
-
-impl<'a> EdgeBuilder<'a> {
- pub fn new(source: String, target: String) -> Self {
- Self {
- source,
- target,
- source_port_position: None,
- target_port_position: None,
- attributes: IndexMap::new(),
- }
- }
-
- pub fn new_with_port_position(
- source: String,
- source_port_position: PortPosition,
- target: String,
- target_port_position: PortPosition,
- ) -> Self {
- Self {
- source,
- target,
- source_port_position: Some(source_port_position),
- target_port_position: Some(target_port_position),
- attributes: IndexMap::new(),
- }
- }
-
- pub fn source_port_position(&mut self, port_position: PortPosition) -> &mut Self {
- self.source_port_position = Some(port_position);
- self
- }
-
- pub fn target_port_position(&mut self, port_position: PortPosition) -> &mut Self {
- self.target_port_position = Some(port_position);
- self
- }
- /// Add an attribute to the edge.
- pub fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self {
- self.attributes.insert(key.into(), value);
- self
- }
-
- /// Add multiple attributes to the edge.
- pub fn add_attributes(
- &'a mut self,
- attributes: HashMap<String, AttributeText<'a>>,
- ) -> &mut Self {
- self.attributes.extend(attributes);
- self
- }
-
- pub fn build(&self) -> Edge<'a> {
- Edge {
- // TODO: are these to_owned and clones necessary?
- source: self.source.to_owned(),
- source_port_position: self.source_port_position.to_owned(),
- target: self.target.to_owned(),
- target_port_position: self.target_port_position.to_owned(),
- attributes: self.attributes.clone(),
- }
- }
-}
-
-trait AttributeStatement<'a> {
- fn get_attribute_statement_type(&self) -> &'static str;
-
- fn get_attributes(&self) -> &IndexMap<String, AttributeText<'a>>;
-
- fn dot_string(&self) -> String {
- if self.get_attributes().is_empty() {
- return String::from("");
- }
- let mut dot_string = format!("{} [", self.get_attribute_statement_type());
- let attributes = &self.get_attributes();
- let mut iter = attributes.iter();
- let first = iter.next().unwrap();
- dot_string.push_str(format!("{}={}", first.0, first.1.dot_string()).as_str());
- for (key, value) in iter {
- dot_string.push_str(", ");
- dot_string.push_str(format!("{}={}", key, value.dot_string()).as_str());
- }
- dot_string.push_str("];");
- dot_string.to_string()
- }
-}
-
-trait NodeAttributes<'a> {
- // TODO: constrain
- /// Indicates the preferred area for a node or empty cluster when laid out by patchwork.
- /// default: 1.0, minimum: >0
- fn area(&mut self, area: f32) -> &mut Self {
- self.add_attribute("area", AttributeText::attr(area.to_string()))
- }
-
- /// Classnames to attach to the node’s SVG element.
- /// Combine with stylesheet for styling SVG output using CSS classnames.
- /// Multiple space-separated classes are supported.
- fn class(&mut self, class: String) -> &mut Self {
- Attributes::class(self.get_attributes_mut(), class);
- self
- }
-
- /// Basic drawing color for graphics, not text. For the latter, use the fontcolor attribute.
- fn color(&mut self, color: Color<'a>) -> &mut Self {
- Attributes::color(self.get_attributes_mut(), color);
- self
- }
-
- fn color_with_colorlist(&mut self, color: ColorList<'a>) -> &mut Self {
- Attributes::color_with_colorlist(self.get_attributes_mut(), color);
- self
- }
-
- /// This attribute specifies a color scheme namespace: the context for interpreting color names.
- /// In particular, if a color value has form "xxx" or "//xxx", then the color xxx will be evaluated
- /// according to the current color scheme. If no color scheme is set, the standard X11 naming is used.
- /// For example, if colorscheme=bugn9, then color=7 is interpreted as color="/bugn9/7".
- fn color_scheme(&mut self, color_scheme: String) -> &mut Self {
- Attributes::color_scheme(self.get_attributes_mut(), color_scheme);
- self
- }
-
- /// Comments are inserted into output. Device-dependent
- fn comment(&mut self, comment: String) -> &mut Self {
- Attributes::comment(self.get_attributes_mut(), comment);
- self
- }
-
- /// Distortion factor for shape=polygon.
- /// Positive values cause top part to be larger than bottom; negative values do the opposite.
- fn distortion(&mut self, distortion: f32) -> &mut Self {
- self.add_attribute("distortion", AttributeText::attr(distortion.to_string()))
- }
-
- /// Color used to fill the background of a node or cluster assuming style=filled, or a filled arrowhead.
- fn fill_color(&mut self, fill_color: Color<'a>) -> &mut Self {
- Attributes::fill_color(self.get_attributes_mut(), fill_color);
- self
- }
-
- /// Color used to fill the background, with a gradient, of a node or cluster assuming
- /// style=filled, or a filled arrowhead.
- fn fill_color_with_colorlist(&mut self, fill_colors: ColorList<'a>) -> &mut Self {
- Attributes::fill_color_with_colorlist(self.get_attributes_mut(), fill_colors);
- self
- }
-
- /// Color used to fill the background, with a gradient, of a node or cluster assuming
- /// style=filled, or a filled arrowhead.
- /// TODO: example
- fn fill_color_with_iter<I>(&mut self, fill_colors: I) -> &mut Self
- where
- I: IntoIterator,
- I::Item: IntoWeightedColor<'a>,
- {
- Attributes::fill_color_with_iter(self.get_attributes_mut(), fill_colors);
- self
- }
-
- /// If true, the node size is specified by the values of the width and height attributes only and
- /// is not expanded to contain the text label.
- /// There will be a warning if the label (with margin) cannot fit within these limits.
- /// If false, the size of a node is determined by smallest width and height needed to contain its label
- /// and image, if any, with a margin specified by the margin attribute.
- fn fixed_size(&mut self, fixed_size: bool) -> &mut Self {
- self.add_attribute("fixedsize", AttributeText::quoted(fixed_size.to_string()))
- }
-
- /// Color used for text.
- fn font_color(&mut self, font_color: Color<'a>) -> &mut Self {
- Attributes::font_color(self.get_attributes_mut(), font_color);
- self
- }
-
- /// Font used for text.
- fn font_name(&mut self, font_name: String) -> &mut Self {
- Attributes::font_name(self.get_attributes_mut(), font_name);
- self
- }
-
- /// Font size, in points, used for text.
- /// default: 14.0, minimum: 1.0
- fn font_size(&mut self, font_size: f32) -> &mut Self {
- Attributes::font_size(self.get_attributes_mut(), font_size);
- self
- }
-
- /// If a gradient fill is being used, this determines the angle of the fill.
- fn gradient_angle(&mut self, gradient_angle: u32) -> &mut Self {
- Attributes::gradient_angle(self.get_attributes_mut(), gradient_angle);
- self
- }
-
- /// If the end points of an edge belong to the same group, i.e., have the same group attribute,
- /// parameters are set to avoid crossings and keep the edges straight.
- fn group(&mut self, group: String) -> &mut Self {
- self.add_attribute("group", AttributeText::attr(group))
- }
-
- // TODO: constrain
- /// Height of node, in inches.
- /// default: 0.5, minimum: 0.02
- fn height(&mut self, height: f32) -> &mut Self {
- self.add_attribute("height", AttributeText::attr(height.to_string()))
- }
-
- /// Gives the name of a file containing an image to be displayed inside a node.
- /// The image file must be in one of the recognized formats,
- /// typically JPEG, PNG, GIF, BMP, SVG, or Postscript, and be able to be converted
- /// into the desired output format.
- fn image(&mut self, image: String) -> &mut Self {
- self.add_attribute("image", AttributeText::quoted(image))
- }
-
- /// Controls how an image is positioned within its containing node.
- /// Only has an effect when the image is smaller than the containing node.
- fn image_pos(&mut self, image_pos: ImagePosition) -> &mut Self {
- self.add_attribute("imagepos", AttributeText::quoted(image_pos.as_slice()))
- }
-
- /// Controls how an image fills its containing node.
- fn image_scale_bool(&mut self, image_scale: bool) -> &mut Self {
- self.add_attribute("imagescale", AttributeText::quoted(image_scale.to_string()))
- }
-
- /// Controls how an image fills its containing node.
- fn image_scale(&mut self, image_scale: ImageScale) -> &mut Self {
- self.add_attribute("imagescale", AttributeText::quoted(image_scale.as_slice()))
- }
-
- /// Text label attached to objects.
- fn label<S: Into<Cow<'a, str>>>(&mut self, text: S) -> &mut Self {
- self.add_attribute("label", AttributeText::quoted(text))
- }
-
- // Vertical placement of labels for nodes, root graphs and clusters.
- // For graphs and clusters, only labelloc=t and labelloc=b are allowed, corresponding to placement at the top and bottom, respectively.
- // By default, root graph labels go on the bottom and cluster labels go on the top.
- // Note that a subgraph inherits attributes from its parent. Thus, if the root graph sets labelloc=b, the subgraph inherits this value.
- // For nodes, this attribute is used only when the height of the node is larger than the height of its label.
- // If labelloc=t, labelloc=c, labelloc=b, the label is aligned with the top, centered, or aligned with the bottom of the node, respectively.
- // By default, the label is vertically centered.
- fn label_location(&mut self, label_location: LabelLocation) -> &mut Self {
- Attributes::label_location(self.get_attributes_mut(), label_location);
- self
- }
-
- /// Specifies layers in which the node, edge or cluster is present.
- fn layer(&mut self, layer: String) -> &mut Self {
- Attributes::layer(self.get_attributes_mut(), layer);
- self
- }
-
- /// Sets x and y margins of canvas, in inches.
- /// Both margins are set equal to the given value.
- /// See [`crate::NodeAttributes::margin_point`]
- fn margin(&mut self, margin: f32) -> &mut Self {
- self.margin_point(Point::new_2d(margin, margin))
- }
-
- /// Sets x and y margins of canvas, in inches.
- /// Specifies space left around the node’s label.
- /// Note that the margin is not part of the drawing but just empty space left around the drawing.
- /// The margin basically corresponds to a translation of drawing, as would be necessary to
- /// center a drawing on a page.
- /// Nothing is actually drawn in the margin.
- /// To actually extend the background of a drawing, see the pad attribute.
- /// Whilst it is possible to create a Point value with either a third co-ordinate
- /// or a forced position, these are ignored for printing.
- /// By default, the value is 0.11,0.055.
- fn margin_point(&mut self, margin: Point) -> &mut Self {
- Attributes::margin(self.get_attributes_mut(), margin);
- self
- }
-
- /// By default, the justification of multi-line labels is done within the largest context that makes sense.
- /// Thus, in the label of a polygonal node, a left-justified line will align with the left side of the node (shifted by the prescribed margin).
- /// In record nodes, left-justified line will line up with the left side of the enclosing column of fields.
- /// If nojustify=true, multi-line labels will be justified in the context of itself.
- /// For example, if nojustify is set, the first label line is long, and the second is shorter and left-justified,
- /// the second will align with the left-most character in the first line, regardless of how large the node might be.
- fn no_justify(&mut self, no_justify: bool) -> &mut Self {
- Attributes::no_justify(self.get_attributes_mut(), no_justify);
- self
- }
-
- /// If ordering="out", then the outedges of a node, that is, edges with the node as its tail
- /// node, must appear left-to-right in the same order in which they are defined in the input.
- ///
- /// If ordering="in", then the inedges of a node must appear left-to-right in the same order in
- /// which they are defined in the input.
- ///
- /// If defined as a graph or subgraph attribute, the value is applied to all nodes in the graph
- /// or subgraph.
- ///
- /// Note that the graph attribute takes precedence over the node attribute.
- fn ordering(&mut self, ordering: Ordering) -> &mut Self {
- Attributes::ordering(self.get_attributes_mut(), ordering);
- self
- }
-
- // TODO: constrain to 0 - 360. Docs say min is 360 which should be max right?
- /// When used on nodes: Angle, in degrees, to rotate polygon node shapes.
- /// For any number of polygon sides, 0 degrees rotation results in a flat base.
- /// When used on graphs: If "[lL]*", sets graph orientation to landscape.
- /// Used only if rotate is not defined.
- /// Default: 0.0 and minimum: 360.0
- fn orientation(&mut self, orientation: f32) -> &mut Self {
- Attributes::orientation(self.get_attributes_mut(), orientation);
- self
- }
-
- /// Specifies the width of the pen, in points, used to draw lines and curves,
- /// including the boundaries of edges and clusters.
- /// default: 1.0, minimum: 0.0
- fn pen_width(&mut self, pen_width: f32) -> &mut Self {
- Attributes::pen_width(self.get_attributes_mut(), pen_width);
- self
- }
-
- /// Set number of peripheries used in polygonal shapes and cluster boundaries.
- fn peripheries(&mut self, peripheries: u32) -> &mut Self {
- self.add_attribute("penwidth", AttributeText::from(peripheries))
- }
-
- /// Position of node, or spline control points.
- /// the position indicates the center of the node. On output, the coordinates are in points.
- fn pos(&mut self, pos: Point) -> &mut Self {
- Attributes::pos(self.get_attributes_mut(), pos);
- self
- }
-
- // TODO: add post_spline
-
- /// Rectangles for fields of records, in points.
- fn rects(&mut self, rect: Rectangle) -> &mut Self {
- self.add_attribute("rects", AttributeText::from(rect))
- }
-
- /// If true, force polygon to be regular, i.e., the vertices of the polygon will
- /// lie on a circle whose center is the center of the node.
- fn regular(&mut self, regular: bool) -> &mut Self {
- self.add_attribute("regular", AttributeText::from(regular))
- }
-
- /// Gives the number of points used for a circle/ellipse node.
- fn sample_points(&mut self, sample_points: u32) -> &mut Self {
- self.add_attribute("samplepoints", AttributeText::from(sample_points))
- }
-
- /// Sets the shape of a node.
- fn shape(&mut self, shape: Shape) -> &mut Self {
- self.add_attribute("shape", AttributeText::from(shape))
- }
-
- // TODO: constrain
- /// Print guide boxes in PostScript at the beginning of routesplines if showboxes=1, or at the end if showboxes=2.
- /// (Debugging, TB mode only!)
- /// default: 0, minimum: 0
- fn show_boxes(&mut self, show_boxes: u32) -> &mut Self {
- Attributes::show_boxes(self.get_attributes_mut(), show_boxes);
- self
- }
-
- /// Number of sides when shape=polygon.
- fn sides(&mut self, sides: u32) -> &mut Self {
- self.add_attribute("sides", AttributeText::from(sides))
- }
-
- // TODO: constrain
- /// Skew factor for shape=polygon.
- /// Positive values skew top of polygon to right; negative to left.
- /// default: 0.0, minimum: -100.0
- fn skew(&mut self, skew: f32) -> &mut Self {
- self.add_attribute("skew", AttributeText::from(skew))
- }
-
- /// If packmode indicates an array packing, sortv specifies an insertion order
- /// among the components, with smaller values inserted first.
- /// default: 0, minimum: 0
- fn sortv(&mut self, sortv: u32) -> &mut Self {
- Attributes::sortv(self.get_attributes_mut(), sortv);
- self
- }
-
- /// Set style information for components of the graph.
- fn style(&mut self, style: NodeStyle) -> &mut Self {
- Attributes::style(self.get_attributes_mut(), Styles::Node(style));
- self
- }
-
- /// If the object has a URL, this attribute determines which window of the browser is used for the URL.
- fn target(&mut self, target: String) -> &mut Self {
- Attributes::target(self.get_attributes_mut(), target);
- self
- }
-
- /// Tooltip annotation attached to the node or edge.
- /// If unset, Graphviz will use the object’s label if defined.
- /// Note that if the label is a record specification or an HTML-like label,
- /// the resulting tooltip may be unhelpful.
- /// In this case, if tooltips will be generated, the user should set a tooltip attribute explicitly.
- fn tooltip(&mut self, tooltip: String) -> &mut Self {
- Attributes::tooltip(self.get_attributes_mut(), tooltip);
- self
- }
-
- /// Hyperlinks incorporated into device-dependent output.
- fn url(&mut self, url: String) -> &mut Self {
- Attributes::url(self.get_attributes_mut(), url);
- self
- }
-
- /// Sets the coordinates of the vertices of the node’s polygon, in inches.
- /// A list of points, separated by spaces.
- fn vertices(&mut self, vertices: String) -> &mut Self {
- self.add_attribute("vertices", AttributeText::quoted(vertices))
- }
-
- /// Width of node, in inches.
- /// This is taken as the initial, minimum width of the node.
- /// If fixedsize is true, this will be the final width of the node.
- /// Otherwise, if the node label requires more width to fit, the node’s
- /// width will be increased to contain the label.
- fn width(&mut self, width: f32) -> &mut Self {
- self.add_attribute("width", AttributeText::from(width))
- }
-
- /// External label for a node or edge.
- /// The label will be placed outside of the node but near it.
- /// These labels are added after all nodes and edges have been placed.
- /// The labels will be placed so that they do not overlap any node or label.
- /// This means it may not be possible to place all of them.
- /// To force placing all of them, set forcelabels=true.
- fn xlabel(&mut self, xlabel: String) -> &mut Self {
- Attributes::xlabel(self.get_attributes_mut(), xlabel);
- self
- }
-
- /// Position of an exterior label, in points.
- /// The position indicates the center of the label.
- fn xlp(&mut self, xlp: Point) -> &mut Self {
- Attributes::xlp(self.get_attributes_mut(), xlp);
- self
- }
-
- /// Add an attribute to the node.
- fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self;
-
- /// Add multiple attribures to the node.
- fn add_attributes(
- &'a mut self,
- attributes: HashMap<String, AttributeText<'a>>,
- ) -> &mut Self;
-
- fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>>;
-}
-
-impl<'a> NodeAttributes<'a> for NodeAttributeStatementBuilder<'a> {
- fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self {
- self.attributes.insert(key.into(), value);
- self
- }
-
- /// Add multiple attributes to the node.
- fn add_attributes(
- &'a mut self,
- attributes: HashMap<String, AttributeText<'a>>,
- ) -> &mut Self {
- self.attributes.extend(attributes);
- self
- }
-
- fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> {
- &mut self.attributes
- }
-}
-
-// I'm not a huge fan of needing this builder but having a hard time getting around &mut without it
-pub struct NodeAttributeStatementBuilder<'a> {
- pub attributes: IndexMap<String, AttributeText<'a>>,
-}
-
-impl<'a> NodeAttributeStatementBuilder<'a> {
- pub fn new() -> Self {
- Self {
- attributes: IndexMap::new(),
- }
- }
-
- pub fn build(&self) -> NodeAttributeStatement<'a> {
- NodeAttributeStatement {
- attributes: self.attributes.clone(),
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct NodeAttributeStatement<'a> {
- pub attributes: IndexMap<String, AttributeText<'a>>,
-}
-
-impl<'a> NodeAttributeStatement<'a> {
- pub fn new() -> Self {
- Self {
- attributes: IndexMap::new(),
- }
- }
-
- pub fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self {
- self.attributes.insert(key.into(), value);
- self
- }
-}
-
-impl<'a> AttributeStatement<'a> for NodeAttributeStatement<'a> {
- fn get_attribute_statement_type(&self) -> &'static str {
- "node"
- }
-
- fn get_attributes(&self) -> &IndexMap<String, AttributeText<'a>> {
- &self.attributes
- }
-}
-
-trait EdgeAttributes<'a> {
- /// Style of arrowhead on the head node of an edge.
- /// This will only appear if the dir attribute is forward or both.
- fn arrow_head(&mut self, arrowhead: ArrowType) -> &mut Self {
- self.add_attribute("arrowhead", AttributeText::from(arrowhead))
- }
-
- // TODO: constrain
- /// Multiplicative scale factor for arrowheads.
- /// default: 1.0, minimum: 0.0
- fn arrow_size(&mut self, arrow_size: f32) -> &mut Self {
- self.add_attribute("arrowsize", AttributeText::from(arrow_size))
- }
-
- /// Style of arrowhead on the tail node of an edge.
- /// This will only appear if the dir attribute is back or both.
- fn arrowtail(&mut self, arrowtail: ArrowType) -> &mut Self {
- self.add_attribute("arrowtail", AttributeText::from(arrowtail))
- }
-
- /// Classnames to attach to the edge’s SVG element.
- /// Combine with stylesheet for styling SVG output using CSS classnames.
- /// Multiple space-separated classes are supported.
- fn class(&mut self, class: String) -> &mut Self {
- Attributes::class(self.get_attributes_mut(), class);
- self
- }
-
- /// Basic drawing color for graphics, not text. For the latter, use the fontcolor attribute.
- fn color(&mut self, color: Color<'a>) -> &mut Self {
- Attributes::color(self.get_attributes_mut(), color);
- self
- }
-
- fn color_with_colorlist(&mut self, color: ColorList<'a>) -> &mut Self {
- Attributes::color_with_colorlist(self.get_attributes_mut(), color);
- self
- }
-
- /// This attribute specifies a color scheme namespace: the context for interpreting color names.
- /// In particular, if a color value has form "xxx" or "//xxx", then the color xxx will be evaluated
- /// according to the current color scheme. If no color scheme is set, the standard X11 naming is used.
- /// For example, if colorscheme=bugn9, then color=7 is interpreted as color="/bugn9/7".
- fn color_scheme(&mut self, color_scheme: String) -> &mut Self {
- Attributes::color_scheme(self.get_attributes_mut(), color_scheme);
- self
- }
-
- /// Comments are inserted into output. Device-dependent
- fn comment(&mut self, comment: String) -> &mut Self {
- self.add_attribute("comment", AttributeText::attr(comment));
- self
- }
-
- /// If false, the edge is not used in ranking the nodes.
- fn constriant(&mut self, constriant: bool) -> &mut Self {
- self.add_attribute("constriant", AttributeText::from(constriant))
- }
-
- /// If true, attach edge label to edge by a 2-segment polyline, underlining the label,
- /// then going to the closest point of spline.
- fn decorate(&mut self, decorate: bool) -> &mut Self {
- self.add_attribute("decorate", AttributeText::from(decorate))
- }
-
- /// Edge type for drawing arrowheads.
- /// Indicates which ends of the edge should be decorated with an arrowhead.
- /// The actual style of the arrowhead can be specified using the arrowhead and arrowtail attributes.
- fn dir(&mut self, dir: Direction) -> &mut Self {
- self.add_attribute("dir", AttributeText::from(dir))
- }
-
- /// If the edge has a URL or edgeURL attribute, edgetarget determines which window
- /// of the browser is used for the URL attached to the non-label part of the edge.
- /// Setting edgetarget=_graphviz will open a new window if it doesn’t already exist,
- /// or reuse it if it does.
- fn edge_target(&mut self, edge_target: String) -> &mut Self {
- self.add_attribute("edgetarget", AttributeText::escaped(edge_target))
- }
-
- /// Tooltip annotation attached to the non-label part of an edge.
- /// Used only if the edge has a URL or edgeURL attribute.
- fn edge_tooltip(&mut self, edge_tooltip: String) -> &mut Self {
- self.add_attribute("edgetooltip", AttributeText::escaped(edge_tooltip))
- }
-
- /// The link for the non-label parts of an edge.
- /// edgeURL overrides any URL defined for the edge.
- /// Also, edgeURL is used near the head or tail node unless overridden by headURL or tailURL, respectively.
- fn edge_url(&mut self, edge_url: String) -> &mut Self {
- self.add_attribute("edgeurl", AttributeText::escaped(edge_url))
- }
-
- // TODO: color list
- /// Color used to fill the background of a node or cluster assuming style=filled, or a filled arrowhead.
- fn fill_color(&mut self, fill_color: Color<'a>) -> &mut Self {
- Attributes::fill_color(self.get_attributes_mut(), fill_color);
- self
- }
-
- // TODO: color list
- /// Color used for text.
- fn font_color(&mut self, font_color: Color<'a>) -> &mut Self {
- Attributes::font_color(self.get_attributes_mut(), font_color);
- self
- }
-
- /// Font used for text.
- fn font_name(&mut self, font_name: String) -> &mut Self {
- Attributes::font_name(self.get_attributes_mut(), font_name);
- self
- }
-
- /// Font size, in points, used for text.
- /// default: 14.0, minimum: 1.0
- fn font_size(&mut self, font_size: f32) -> &mut Self {
- Attributes::font_size(self.get_attributes_mut(), font_size);
- self
- }
-
- /// Position of an edge’s head label, in points. The position indicates the center of the label.
- fn head_lp(&mut self, head_lp: Point) -> &mut Self {
- self.add_attribute("head_lp", AttributeText::from(head_lp))
- }
-
- /// If true, the head of an edge is clipped to the boundary of the head node;
- /// otherwise, the end of the edge goes to the center of the node, or the center
- /// of a port, if applicable.
- fn head_clip(&mut self, head_clip: bool) -> &mut Self {
- self.add_attribute("headclip", AttributeText::from(head_clip))
- }
-
- /// Text label to be placed near head of edge.
- fn head_label(&mut self, head_label: String) -> &mut Self {
- self.add_attribute("headlabel", AttributeText::quoted(head_label))
- }
-
- /// Indicates where on the head node to attach the head of the edge.
- /// In the default case, the edge is aimed towards the center of the node,
- /// and then clipped at the node boundary.
- fn head_port(&mut self, head_port: PortPosition) -> &mut Self {
- self.add_attribute("headport", AttributeText::from(head_port))
- }
-
- /// If the edge has a headURL, headtarget determines which window of the browser is used for the URL.
- /// Setting headURL=_graphviz will open a new window if the window doesn’t already exist,
- /// or reuse the window if it does.
- /// If undefined, the value of the target is used.
- fn head_target(&mut self, head_target: String) -> &mut Self {
- self.add_attribute("headtarget", AttributeText::escaped(head_target))
- }
-
- /// Tooltip annotation attached to the head of an edge.
- /// Used only if the edge has a headURL attribute.
- fn head_tooltip(&mut self, head_tooltip: String) -> &mut Self {
- self.add_attribute("headtooltip", AttributeText::escaped(head_tooltip))
- }
-
- /// If defined, headURL is output as part of the head label of the edge.
- /// Also, this value is used near the head node, overriding any URL value.
- fn head_url(&mut self, head_url: String) -> &mut Self {
- self.add_attribute("headURL", AttributeText::escaped(head_url))
- }
-
- /// An escString or an HTML label.
- fn label(&mut self, label: String) -> &mut Self {
- Attributes::label(self.get_attributes_mut(), label);
- self
- }
-
- // TODO: constrain
- /// Determines, along with labeldistance, where the headlabel / taillabel are
- /// placed with respect to the head / tail in polar coordinates.
- /// The origin in the coordinate system is the point where the edge touches the node.
- /// The ray of 0 degrees goes from the origin back along the edge, parallel to the edge at the origin.
- /// The angle, in degrees, specifies the rotation from the 0 degree ray,
- /// with positive angles moving counterclockwise and negative angles moving clockwise.
- /// default: -25.0, minimum: -180.0
- fn label_angle(&mut self, label_angle: f32) -> &mut Self {
- self.add_attribute("labelangle", AttributeText::from(label_angle))
- }
-
- /// Multiplicative scaling factor adjusting the distance that the headlabel / taillabel is from
- /// the head / tail node.
- /// default: 1.0, minimum: 0.0
- fn label_distance(&mut self, label_distance: f32) -> &mut Self {
- self.add_attribute("labeldistance", AttributeText::from(label_distance))
- }
-
- /// If true, allows edge labels to be less constrained in position.
- /// In particular, it may appear on top of other edges.
- fn label_float(&mut self, label_float: bool) -> &mut Self {
- self.add_attribute("labelfloat", AttributeText::from(label_float))
- }
-
- /// Color used for headlabel and taillabel.
- fn label_font_color(&mut self, label_font_color: Color<'a>) -> &mut Self {
- self.add_attribute("labelfontcolor", AttributeText::from(label_font_color))
- }
-
- /// Font used for headlabel and taillabel.
- /// If not set, defaults to edge’s fontname.
- fn label_font_name(&mut self, label_font_name: String) -> &mut Self {
- self.add_attribute("labelfontname", AttributeText::attr(label_font_name))
- }
-
- // TODO: constrains
- /// Font size, in points, used for headlabel and taillabel.
- /// If not set, defaults to edge’s fontsize.
- /// default: 14.0, minimum: 1.0
- fn label_font_size(&mut self, label_font_size: f32) -> &mut Self {
- self.add_attribute("labelfontsize", AttributeText::from(label_font_size))
- }
-
- /// If the edge has a URL or labelURL attribute, this attribute determines
- /// which window of the browser is used for the URL attached to the label.
- fn label_target(&mut self, label_target: String) -> &mut Self {
- self.add_attribute("labeltarget", AttributeText::escaped(label_target))
- }
-
- /// Tooltip annotation attached to label of an edge.
- /// Used only if the edge has a URL or labelURL attribute.
- fn label_tooltip(&mut self, label_tooltip: String) -> &mut Self {
- self.add_attribute("labeltooltip", AttributeText::escaped(label_tooltip))
- }
-
- /// If defined, labelURL is the link used for the label of an edge.
- /// labelURL overrides any URL defined for the edge.
- fn label_url(&mut self, label_url: String) -> &mut Self {
- self.add_attribute("labelurl", AttributeText::escaped(label_url))
- }
-
- fn layer(&mut self, layer: String) -> &mut Self {
- Attributes::layer(self.get_attributes_mut(), layer);
- self
- }
-
- fn lhead(&mut self, lhead: String) -> &mut Self {
- self.add_attribute("lhead", AttributeText::quoted(lhead))
- }
-
- /// Label position
- /// The position indicates the center of the label.
- fn label_position(&mut self, lp: Point) -> &mut Self {
- Attributes::label_position(self.get_attributes_mut(), lp);
- self
- }
-
- /// Logical tail of an edge.
- /// When compound=true, if ltail is defined and is the name of a cluster
- /// containing the real tail, the edge is clipped to the boundary of the cluster.
- fn ltail(&mut self, ltail: String) -> &mut Self {
- self.add_attribute("ltail", AttributeText::quoted(ltail))
- }
-
- /// Minimum edge length (rank difference between head and tail).
- fn min_len(&mut self, min_len: u32) -> &mut Self {
- self.add_attribute("minlen", AttributeText::from(min_len))
- }
-
- fn no_justify(&mut self, no_justify: bool) -> &mut Self {
- self.add_attribute("nojustify", AttributeText::from(no_justify))
- }
-
- fn pen_width(&mut self, pen_width: f32) -> &mut Self {
- Attributes::pen_width(self.get_attributes_mut(), pen_width);
- self
- }
-
- /// Position of node, or spline control points.
- /// the position indicates the center of the node. On output, the coordinates are in points.
- fn pos(&mut self, pos: Point) -> &mut Self {
- Attributes::pos(self.get_attributes_mut(), pos);
- self
- }
-
- /// Edges with the same head and the same samehead value are aimed at the same point on the head.
- fn same_head(&mut self, same_head: String) -> &mut Self {
- self.add_attribute("samehead", AttributeText::quoted(same_head))
- }
-
- /// Edges with the same tail and the same sametail value are aimed at the same point on the tail.
- fn same_tail(&mut self, same_tail: String) -> &mut Self {
- self.add_attribute("sametail", AttributeText::quoted(same_tail))
- }
-
- // TODO: constrain
- /// Print guide boxes in PostScript at the beginning of routesplines if showboxes=1, or at the
- /// end if showboxes=2.
- /// (Debugging, TB mode only!)
- /// default: 0, minimum: 0
- fn show_boxes(&mut self, show_boxes: u32) -> &mut Self {
- Attributes::show_boxes(self.get_attributes_mut(), show_boxes);
- self
- }
-
- /// Set style information for components of the graph.
- fn style(&mut self, style: EdgeStyle) -> &mut Self {
- Attributes::style(self.get_attributes_mut(), Styles::Edge(style));
- self
- }
-
- /// Position of an edge’s tail label, in points.
- /// The position indicates the center of the label.
- fn tail_lp(&mut self, tail_lp: Point) -> &mut Self {
- self.add_attribute("tail_lp", AttributeText::from(tail_lp))
- }
-
- /// If true, the tail of an edge is clipped to the boundary of the tail node; otherwise,
- /// the end of the edge goes to the center of the node, or the center of a port, if applicable.
- fn tail_clip(&mut self, tail_clip: bool) -> &mut Self {
- self.add_attribute("tailclip", AttributeText::from(tail_clip))
- }
-
- /// Text label to be placed near tail of edge.
- fn tail_label(&mut self, tail_label: String) -> &mut Self {
- self.add_attribute("taillabel", AttributeText::quoted(tail_label))
- }
-
- /// Indicates where on the tail node to attach the tail of the edge.
- fn tail_port(&mut self, tail_port: PortPosition) -> &mut Self {
- self.add_attribute("tailport", AttributeText::from(tail_port))
- }
-
- /// If the edge has a tailURL, tailtarget determines which window of the browser is used for the URL.
- fn tail_target(&mut self, tail_target: String) -> &mut Self {
- self.add_attribute("tailtarget", AttributeText::escaped(tail_target))
- }
-
- /// Tooltip annotation attached to the tail of an edge.
- fn tail_tooltip(&mut self, tail_tooltip: String) -> &mut Self {
- self.add_attribute("tailtooltip", AttributeText::escaped(tail_tooltip))
- }
-
- /// If defined, tailURL is output as part of the tail label of the edge.
- /// Also, this value is used near the tail node, overriding any URL value.
- fn tail_url(&mut self, tail_url: String) -> &mut Self {
- self.add_attribute("tailURL", AttributeText::escaped(tail_url))
- }
-
- /// If the object has a URL, this attribute determines which window of the browser is used for the URL.
- fn target(&mut self, target: String) -> &mut Self {
- self.add_attribute("target", AttributeText::escaped(target))
- }
-
- /// Tooltip annotation attached to the node or edge.
- /// If unset, Graphviz will use the object’s label if defined.
- /// Note that if the label is a record specification or an HTML-like label,
- /// the resulting tooltip may be unhelpful.
- /// In this case, if tooltips will be generated, the user should set a tooltip attribute explicitly.
- fn tooltip(&mut self, tooltip: String) -> &mut Self {
- Attributes::tooltip(self.get_attributes_mut(), tooltip);
- self
- }
-
- /// Hyperlinks incorporated into device-dependent output.
- fn url(&mut self, url: String) -> &mut Self {
- Attributes::url(self.get_attributes_mut(), url);
- self
- }
-
- // TODO: contrain
- /// Weight of edge.
- /// The heavier the weight, the shorter, straighter and more vertical the edge is.
- /// default: 1, minimum: 0
- fn weight(&mut self, weight: u32) -> &mut Self {
- self.add_attribute("weight", AttributeText::attr(weight.to_string()))
- }
-
- /// External label for a node or edge.
- /// The label will be placed outside of the node but near it.
- /// These labels are added after all nodes and edges have been placed.
- /// The labels will be placed so that they do not overlap any node or label.
- /// This means it may not be possible to place all of them.
- /// To force placing all of them, set forcelabels=true.
- fn xlabel(&mut self, xlabel: String) -> &mut Self {
- Attributes::xlabel(self.get_attributes_mut(), xlabel);
- self
- }
-
- /// Position of an exterior label, in points.
- /// The position indicates the center of the label.
- fn xlp(&mut self, xlp: Point) -> &mut Self {
- Attributes::xlp(self.get_attributes_mut(), xlp);
- self
- }
-
- fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self;
-
- fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>>;
-
- // fn add_attribute<S: Into<String>>(
- // &self,
- // key: S,
- // value: AttributeText<'a>
- // ) {
- // self.get_attributes().insert(key.into(), value);
- // }
-
- // fn get_attributes(&self) -> IndexMap<String, AttributeText<'a>>;
-
- // fn get_attributes_mut(&self) -> &mut IndexMap<String, AttributeText<'a>>;
-
- // fn to_dot_string(&self) -> String;
-}
-
-impl<'a> EdgeAttributes<'a> for EdgeAttributeStatementBuilder<'a> {
- fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self {
- self.attributes.insert(key.into(), value);
- self
- }
-
- fn get_attributes_mut(&mut self) -> &mut IndexMap<String, AttributeText<'a>> {
- &mut self.attributes
- }
-}
-
-impl<'a> AttributeStatement<'a> for EdgeAttributeStatement<'a> {
- fn get_attribute_statement_type(&self) -> &'static str {
- "edge"
- }
-
- fn get_attributes(&self) -> &IndexMap<String, AttributeText<'a>> {
- &self.attributes
- }
-}
-
-// I'm not a huge fan of needing this builder but having a hard time getting around &mut without it
-pub struct EdgeAttributeStatementBuilder<'a> {
- pub attributes: IndexMap<String, AttributeText<'a>>,
-}
-
-impl<'a> EdgeAttributeStatementBuilder<'a> {
- pub fn new() -> Self {
- Self {
- attributes: IndexMap::new(),
- }
- }
-
- pub fn build(&self) -> EdgeAttributeStatement<'a> {
- EdgeAttributeStatement {
- attributes: self.attributes.clone(),
- }
- }
-}
-
-#[derive(Clone, Debug)]
-pub struct EdgeAttributeStatement<'a> {
- pub attributes: IndexMap<String, AttributeText<'a>>,
-}
-
-impl<'a> EdgeAttributeStatement<'a> {
- pub fn new() -> Self {
- Self {
- attributes: IndexMap::new(),
- }
- }
-
- pub fn add_attribute<S: Into<String>>(
- &mut self,
- key: S,
- value: AttributeText<'a>,
- ) -> &mut Self {
- self.attributes.insert(key.into(), value);
- self
- }
-}
-
-#[derive(Copy, Clone, PartialEq, Eq, Debug)]
-pub enum RenderOption {
- NoEdgeLabels,
- NoNodeLabels,
- NoEdgeStyles,
- NoNodeStyles,
-
- // TODO: replace with Fontname(String),
- Monospace,
-}
-
-/// Returns vec holding all the default render options.
-pub fn default_options() -> Vec<RenderOption> {
- vec![]
-}
-
-pub enum NodeStyle {
- Bold,
- Dashed,
- Diagonals,
- Dotted,
- Filled,
- Invisible,
- Rounded,
- Solid,
- Stripped,
- Radical,
- Wedged,
-}
-
-impl<'a> DotString<'a> for NodeStyle {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- NodeStyle::Bold => "bold".into(),
- NodeStyle::Dashed => "dashed".into(),
- NodeStyle::Diagonals => "diagonals".into(),
- NodeStyle::Dotted => "dotted".into(),
- NodeStyle::Filled => "filled".into(),
- NodeStyle::Invisible => "invisible".into(),
- NodeStyle::Rounded => "rounded".into(),
- NodeStyle::Solid => "solid".into(),
- NodeStyle::Stripped => "stripped".into(),
- NodeStyle::Radical => "radical".into(),
- NodeStyle::Wedged => "wedged".into(),
- }
- }
-}
-
-// TODO: this might be a bit much to in order to avoid some duplication
-// probably not worth it but is pattern is cool nonetheless
-pub enum Styles {
- Edge(EdgeStyle),
- Node(NodeStyle),
- Graph(GraphStyle),
-}
-
-impl<'a> DotString<'a> for Styles {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- Styles::Edge(s) => s.dot_string(),
- Styles::Node(s) => s.dot_string(),
- Styles::Graph(s) => s.dot_string(),
- }
- }
-}
-
-pub enum EdgeStyle {
- Bold,
- Dashed,
- Dotted,
- Invisible,
- Solid,
- Tapered,
-}
-
-impl<'a> DotString<'a> for EdgeStyle {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- EdgeStyle::Bold => "bold".into(),
- EdgeStyle::Dashed => "dashed".into(),
- EdgeStyle::Dotted => "dotted".into(),
- EdgeStyle::Invisible => "invisible".into(),
- EdgeStyle::Solid => "solid".into(),
- EdgeStyle::Tapered => "tapered".into(),
- }
- }
-}
-
-pub enum GraphStyle {
- Filled,
- Radical,
- Rounded,
- Striped,
-}
-
-impl<'a> DotString<'a> for GraphStyle {
- fn dot_string(&self) -> Cow<'a, str> {
- match self {
- GraphStyle::Filled => "filled".into(),
- GraphStyle::Radical => "radical".into(),
- GraphStyle::Rounded => "rounded".into(),
- GraphStyle::Striped => "striped".into(),
- }
- }
-}
-
-struct Attributes;
-impl Attributes {
- fn class(attributes: &mut IndexMap<String, AttributeText>, class: String) {
- Self::add_attribute(attributes, "class", AttributeText::quoted(class))
- }
-
- fn color<'a>(
- attributes: &mut IndexMap<String, AttributeText<'a>>,
- color: Color<'a>,
- ) {
- Self::add_attribute(attributes, "color", AttributeText::from(color))
- }
-
- fn color_with_colorlist<'a>(
- attributes: &mut IndexMap<String, AttributeText<'a>>,
- color: ColorList<'a>,
- ) {
- Self::add_attribute(attributes, "color", AttributeText::from(color))
- }
-
- fn color_scheme(
- attributes: &mut IndexMap<String, AttributeText>,
- color_scheme: String,
- ) {
- Self::add_attribute(
- attributes,
- "colorscheme",
- AttributeText::quoted(color_scheme),
- )
- }
-
- fn comment(attributes: &mut IndexMap<String, AttributeText>, comment: String) {
- Self::add_attribute(attributes, "comment", AttributeText::quoted(comment))
- }
-
- fn fill_color<'a>(
- attributes: &mut IndexMap<String, AttributeText<'a>>,
- fill_color: Color<'a>,
- ) {
- Self::add_attribute(attributes, "fillcolor", AttributeText::from(fill_color))
- }
-
- fn fill_color_with_colorlist<'a>(
- attributes: &mut IndexMap<String, AttributeText<'a>>,
- fill_colors: ColorList<'a>,
- ) {
- Self::add_attribute(attributes, "fillcolor", AttributeText::from(fill_colors))
- }
-
- fn fill_color_with_iter<'a, I>(
- attributes: &mut IndexMap<String, AttributeText<'a>>,
- fill_colors: I,
- ) where
- I: IntoIterator,
- I::Item: IntoWeightedColor<'a>,
- {
- let colors: Vec<WeightedColor> = fill_colors
- .into_iter()
- .map(|e| e.into_weighted_color())
- .collect();
-
- let color_list = ColorList { colors };
-
- Self::add_attribute(attributes, "fillcolor", AttributeText::from(color_list))
- }
-
- fn font_color<'a>(
- attributes: &mut IndexMap<String, AttributeText<'a>>,
- font_color: Color<'a>,
- ) {
- Self::add_attribute(attributes, "fontcolor", AttributeText::from(font_color))
- }
-
- fn font_name(attributes: &mut IndexMap<String, AttributeText>, font_name: String) {
- Self::add_attribute(attributes, "fontname", AttributeText::quoted(font_name))
- }
-
- fn font_size(attributes: &mut IndexMap<String, AttributeText>, font_size: f32) {
- Self::add_attribute(attributes, "fontsize", AttributeText::from(font_size))
- }
-
- fn gradient_angle(
- attributes: &mut IndexMap<String, AttributeText>,
- gradient_angle: u32,
- ) {
- Self::add_attribute(
- attributes,
- "gradientangle",
- AttributeText::from(gradient_angle),
- )
- }
-
- fn label(attributes: &mut IndexMap<String, AttributeText>, text: String) {
- Self::add_attribute(attributes, "label", AttributeText::quoted(text));
- }
-
- fn label_location(
- attributes: &mut IndexMap<String, AttributeText>,
- label_location: LabelLocation,
- ) {
- Self::add_attribute(attributes, "labelloc", AttributeText::from(label_location))
- }
-
- // TODO: layer struct
- fn layer(attributes: &mut IndexMap<String, AttributeText>, layer: String) {
- Self::add_attribute(attributes, "layer", AttributeText::attr(layer))
- }
-
- fn label_position(attributes: &mut IndexMap<String, AttributeText>, lp: Point) {
- Self::add_attribute(attributes, "lp", AttributeText::from(lp))
- }
-
- fn margin(attributes: &mut IndexMap<String, AttributeText>, margin: Point) {
- Self::add_attribute(attributes, "margin", AttributeText::from(margin))
- }
-
- fn no_justify(attributes: &mut IndexMap<String, AttributeText>, no_justify: bool) {
- Self::add_attribute(attributes, "nojustify", AttributeText::from(no_justify))
- }
-
- fn ordering(attributes: &mut IndexMap<String, AttributeText>, ordering: Ordering) {
- Self::add_attribute(attributes, "ordering", AttributeText::from(ordering))
- }
-
- fn orientation(attributes: &mut IndexMap<String, AttributeText>, orientation: f32) {
- Self::add_attribute(attributes, "orientation", AttributeText::from(orientation))
- }
-
- fn pen_width(attributes: &mut IndexMap<String, AttributeText>, pen_width: f32) {
- Self::add_attribute(attributes, "penwidth", AttributeText::from(pen_width))
- }
-
- // TODO: splinetype
- fn pos(attributes: &mut IndexMap<String, AttributeText>, pos: Point) {
- Self::add_attribute(attributes, "pos", AttributeText::from(pos))
- }
-
- fn show_boxes(attributes: &mut IndexMap<String, AttributeText>, show_boxes: u32) {
- Self::add_attribute(attributes, "showboxes", AttributeText::from(show_boxes))
- }
-
- fn sortv(attributes: &mut IndexMap<String, AttributeText>, sortv: u32) {
- Self::add_attribute(attributes, "sortv", AttributeText::from(sortv))
- }
-
- fn style(attributes: &mut IndexMap<String, AttributeText>, style: Styles) {
- Self::add_attribute(attributes, "style", AttributeText::from(style))
- }
-
- fn target(attributes: &mut IndexMap<String, AttributeText>, target: String) {
- Self::add_attribute(attributes, "target", AttributeText::escaped(target))
- }
-
- fn tooltip(attributes: &mut IndexMap<String, AttributeText>, tooltip: String) {
- Self::add_attribute(attributes, "tooltip", AttributeText::escaped(tooltip))
- }
-
- fn url(attributes: &mut IndexMap<String, AttributeText>, url: String) {
- Self::add_attribute(attributes, "url", AttributeText::escaped(url))
- }
-
- fn xlabel(attributes: &mut IndexMap<String, AttributeText>, width: String) {
- Self::add_attribute(attributes, "xlabel", AttributeText::escaped(width))
- }
-
- fn xlp(attributes: &mut IndexMap<String, AttributeText>, xlp: Point) {
- Self::add_attribute(attributes, "xlp", AttributeText::from(xlp))
- }
-
- fn add_attribute<'a, S: Into<String>>(
- attributes: &mut IndexMap<String, AttributeText<'a>>,
- key: S,
- value: AttributeText<'a>,
- ) {
- attributes.insert(key.into(), value);
- }
-}
-
-fn test_input(g: Graph) -> io::Result<String> {
- let mut writer = Vec::new();
- let dot = Dot { graph: g };
-
- dot.render(&mut writer).unwrap();
- let mut s = String::new();
- Read::read_to_string(&mut &*writer, &mut s)?;
- Ok(s)
-}
-
-#[test]
-fn empty_digraph_without_id() {
- // TODO: support both String and &str
- let g = GraphBuilder::new_directed(None).build();
- let r = test_input(g);
- assert_eq!(
- r.unwrap(),
- r#"digraph {
-}
-"#
- );
-}
-
-#[test]
-fn graph_comment() {
- // TODO: support both String and &str
- let g = GraphBuilder::new_directed(None)
- .comment("Comment goes here")
- .build();
- let r = test_input(g);
- assert_eq!(
- r.unwrap(),
- r#"// Comment goes here
-digraph {
-}
-"#
- );
-}
-
-#[test]
-fn empty_digraph() {
- // TODO: support both String and &str
- let g = GraphBuilder::new_directed(Some("empty_graph".to_string())).build();
- let r = test_input(g);
- assert_eq!(
- r.unwrap(),
- r#"digraph empty_graph {
-}
-"#
- );
-}
-
-#[test]
-fn empty_undirected_graph() {
- // TODO: support both String and &str
- let g = GraphBuilder::new_undirected(Some("empty_graph".to_string())).build();
- let r = test_input(g);
- assert_eq!(
- r.unwrap(),
- r#"graph empty_graph {
-}
-"#
- );
-}
-
-#[test]
-fn single_node() {
- let g = GraphBuilder::new_directed(Some("single_node".to_string()))
- .add_node(Node::new("N0".to_string()))
- .build();
- let r = test_input(g);
- assert_eq!(
- r.unwrap(),
- r#"digraph single_node {
- N0;
-}
-"#
- );
-}
-
-#[test]
-fn single_node_with_style() {
- let node = NodeBuilder::new("N0".to_string())
- .add_attribute("style", AttributeText::quoted("dashed"))
- .build();
-
- let g = GraphBuilder::new_directed(Some("single_node".to_string()))
- .add_node(node)
- .build();
-
- let r = test_input(g);
- assert_eq!(
- r.unwrap(),
- r#"digraph single_node {
- N0 [style="dashed"];
-}
-"#
- );
-}
-
-#[test]
-fn support_non_inline_builder() {
- let mut g = GraphBuilder::new_directed(Some("single_node".to_string()));
-
- // TODO: having to split this is stupid. am i doing something wrong?
- let mut node_builder = NodeBuilder::new("N0".to_string());
- node_builder.add_attribute("style", AttributeText::quoted("dashed"));
-
- if true {
- node_builder.add_attribute("foo", AttributeText::quoted("baz"));
- }
-
- let node = node_builder.build();
- g.add_node(node);
-
- let r = test_input(g.build());
- assert_eq!(
- r.unwrap(),
- r#"digraph single_node {
- N0 [style="dashed", foo="baz"];
-}
-"#
- );
-}
-
-#[test]
-fn builder_support_shape() {
- let node = NodeBuilder::new("N0".to_string())
- .shape(Shape::Note)
- .build();
-
- let g = GraphBuilder::new_directed(Some("node_shape".to_string()))
- .add_node(node)
- .build();
-
- let r = test_input(g);
- assert_eq!(
- r.unwrap(),
- r#"digraph node_shape {
- N0 [shape=note];
-}
-"#
- );
-}
-
-#[test]
-fn single_edge() {
- let g = GraphBuilder::new_directed(Some("single_edge".to_string()))
- .add_node(Node::new("N0".to_string()))
- .add_node(Node::new("N1".to_string()))
- .add_edge(Edge::new("N0".to_string(), "N1".to_string()))
- .build();
-
- let r = test_input(g);
-
- assert_eq!(
- r.unwrap(),
- r#"digraph single_edge {
- N0;
- N1;
- N0 -> N1;
-}
-"#
- );
-}
-
-#[test]
-fn single_edge_with_style() {
- let edge = EdgeBuilder::new("N0".to_string(), "N1".to_string())
- .add_attribute("style", AttributeText::quoted("bold"))
- .build();
-
- let g = GraphBuilder::new_directed(Some("single_edge".to_string()))
- .add_node(Node::new("N0".to_string()))
- .add_node(Node::new("N1".to_string()))
- .add_edge(edge)
- .build();
-
- let r = test_input(g);
-
- assert_eq!(
- r.unwrap(),
- r#"digraph single_edge {
- N0;
- N1;
- N0 -> N1 [style="bold"];
-}
-"#
- );
-}
-
-// yellow;0.3:blue
-#[test]
-fn colorlist_dot_string() {
- let yellow = WeightedColor {
- color: Color::Named("yellow"),
- weight: Some(0.3),
- };
-
- let blue = WeightedColor {
- color: Color::Named("blue"),
- weight: None,
- };
-
- let color_list = ColorList {
- colors: vec![yellow, blue],
- };
-
- let dot_string = color_list.dot_string();
-
- assert_eq!("yellow;0.3:blue", dot_string);
-}
-
-#[test]
-fn color_rbg_dot_string() {
- let color = Color::RGB {
- red: 160,
- green: 82,
- blue: 45,
- };
- assert_eq!("#a0522d", color.dot_string());
-}
-
-#[test]
-fn color_rbga_dot_string() {
- let color = Color::RGBA {
- red: 160,
- green: 82,
- blue: 45,
- alpha: 10,
- };
- assert_eq!("#a0522d0a", color.dot_string());
-}
-
-#[test]
-fn color_hsv_dot_string() {
- let color = Color::HSV {
- hue: 0.051,
- saturation: 0.718,
- value: 0.627,
- };
- assert_eq!("0.051 0.718 0.627", color.dot_string());
-}
-
-#[test]
-fn spline_type() {
- let spline_type = SplineType {
- end: None,
- start: None,
- spline_points: vec![
- Point::new_2d(0.0, 0.0),
- Point::new_2d(1.0, 1.0),
- Point::new_2d(1.0, -1.0),
- ],
- };
-
- assert_eq!("0.0,0.0 1.0,1.0 1.0,-1.0", spline_type.dot_string());
-}
-
-#[test]
-fn spline_type_end() {
- let spline_type = SplineType {
- end: Some(Point::new_2d(2.0, 0.0)),
- start: None,
- spline_points: vec![
- Point::new_2d(0.0, 0.0),
- Point::new_2d(1.0, 1.0),
- Point::new_2d(1.0, -1.0),
- ],
- };
-
- assert_eq!(
- "e,2.0,0.0 0.0,0.0 1.0,1.0 1.0,-1.0",
- spline_type.dot_string()
- );
-}
-
-#[test]
-fn spline_type_start() {
- let spline_type = SplineType {
- end: None,
- start: Some(Point::new_2d(-1.0, 0.0)),
- spline_points: vec![
- Point::new_2d(0.0, 0.0),
- Point::new_2d(1.0, 1.0),
- Point::new_2d(1.0, -1.0),
- ],
- };
-
- assert_eq!(
- "s,-1.0,0.0 0.0,0.0 1.0,1.0 1.0,-1.0",
- spline_type.dot_string()
- );
-}
-
-#[test]
-fn spline_type_complete() {
- let spline_type = SplineType {
- end: Some(Point::new_2d(2.0, 0.0)),
- start: Some(Point::new_2d(-1.0, 0.0)),
- spline_points: vec![
- Point::new_2d(0.0, 0.0),
- Point::new_2d(1.0, 1.0),
- Point::new_2d(1.0, -1.0),
- ],
- };
-
- assert_eq!(
- "e,2.0,0.0 s,-1.0,0.0 0.0,0.0 1.0,1.0 1.0,-1.0",
- spline_type.dot_string()
- );
-}
-
-#[test]
-fn point_string() {
- assert_eq!("1.0,2.0", Point::new_2d(1.0, 2.0).dot_string());
- assert_eq!("1.0,2.0,0.0", Point::new_3d(1.0, 2.0, 0.0).dot_string());
- assert_eq!("1.0,2.0!", Point::new(1.0, 2.0, None, true).dot_string());
- assert_eq!(
- "1.0,2.0,0.0!",
- Point::new(1.0, 2.0, Some(0.0), true).dot_string()
- );
-}
-
-#[test]
-fn graph_attribute_colorlist_vec_dot_string() {
- let graph_attributes = GraphAttributeStatementBuilder::new()
- .fill_color_with_iter(&[
- (Color::Named("yellow"), Some(0.3)),
- (Color::Named("blue"), None),
- ])
- .build();
-
- assert_eq!(
- "graph [fillcolor=\"yellow;0.3:blue\"];",
- graph_attributes.dot_string()
- );
-}
-
-#[test]
-fn edge_statement_port_position() {
- let node_0 = NodeBuilder::new("N0".to_string())
- .shape(Shape::Record)
- .label("a|<port0>b")
- .build();
-
- let node_1 = NodeBuilder::new("N1".to_string())
- .shape(Shape::Record)
- .label("e|<port1>f")
- .build();
-
- let edge = EdgeBuilder::new("N0".to_string(), "N1".to_string())
- .source_port_position(PortPosition::Port {
- port_name: "port0".to_string(),
- compass_point: Some(CompassPoint::SW),
- })
- .target_port_position(PortPosition::Port {
- port_name: "port1".to_string(),
- compass_point: Some(CompassPoint::NE),
- })
- .build();
-
- let g = GraphBuilder::new_directed(Some("edge_statement_port_position".to_string()))
- .add_node(node_0)
- .add_node(node_1)
- .add_edge(edge)
- .build();
-
- let r = test_input(g);
-
- assert_eq!(
- r.unwrap(),
- r#"digraph edge_statement_port_position {
- N0 [shape=record, label="a|<port0>b"];
- N1 [shape=record, label="e|<port1>f"];
- N0:port0:sw -> N1:port1:ne;
-}
-"#
- );
-}
-
-#[test]
-fn port_position_attribute() {
- let node_0 = NodeBuilder::new("N0".to_string())
- .shape(Shape::Record)
- .label("a|<port0>b")
- .build();
-
- let node_1 = NodeBuilder::new("N1".to_string())
- .shape(Shape::Record)
- .label("e|<port1>f")
- .build();
-
- let edge = EdgeBuilder::new("N0".to_string(), "N1".to_string())
- .tail_port(PortPosition::Port {
- port_name: "port0".to_string(),
- compass_point: Some(CompassPoint::SW),
- })
- .head_port(PortPosition::Port {
- port_name: "port1".to_string(),
- compass_point: Some(CompassPoint::NE),
- })
- .build();
-
- let g = GraphBuilder::new_directed(Some("port_position_attribute".to_string()))
- .add_node(node_0)
- .add_node(node_1)
- .add_edge(edge)
- .build();
-
- let r = test_input(g);
-
- assert_eq!(
- r.unwrap(),
- r#"digraph port_position_attribute {
- N0 [shape=record, label="a|<port0>b"];
- N1 [shape=record, label="e|<port1>f"];
- N0 -> N1 [tailport="port0:sw", headport="port1:ne"];
-}
-"#
- );
-}
-
-#[test]
-fn graph_attributes() {
- let graph_attributes = GraphAttributeStatementBuilder::new()
- .rank_dir(RankDir::LeftRight)
- .build();
- let node_attributes = NodeAttributeStatementBuilder::new()
- .style(NodeStyle::Filled)
- .build();
- let edge_attributes = EdgeAttributeStatementBuilder::new().min_len(1).build();
-
- let g = GraphBuilder::new_directed(Some("graph_attributes".to_string()))
- .add_graph_attributes(graph_attributes)
- .add_node_attributes(node_attributes)
- .add_edge_attributes(edge_attributes)
- .build();
-
- let r = test_input(g);
-
- assert_eq!(
- r.unwrap(),
- r#"digraph graph_attributes {
- graph [rankdir=LR];
- node [style=filled];
- edge [minlen=1];
-}
-"#
- );
-}
diff --git a/tests/dot.rs b/tests/dot.rs
new file mode 100644
index 0000000..fdd9545
--- /dev/null
+++ b/tests/dot.rs
@@ -0,0 +1,307 @@
+use dotavious::attributes::{AttributeText, CompassPoint, GraphAttributeStatementBuilder, GraphAttributes, NodeStyle, PortPosition, RankDir, Shape, EdgeStyle};
+use dotavious::{
+ Dot, Edge, EdgeAttributeStatementBuilder, EdgeAttributes, EdgeBuilder, Graph,
+ GraphBuilder, Node, NodeAttributeStatementBuilder, NodeAttributes, NodeBuilder,
+};
+use std::io;
+use std::io::Read;
+
+fn test_input(g: Graph) -> io::Result<String> {
+ let mut writer = Vec::new();
+ let dot = Dot { graph: g };
+
+ dot.render(&mut writer).unwrap();
+ let mut s = String::new();
+ Read::read_to_string(&mut &*writer, &mut s)?;
+ Ok(s)
+}
+
+#[test]
+fn empty_digraph_without_id() {
+ let g = GraphBuilder::new_directed(None).build();
+ let r = test_input(g);
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph {
+}
+"#
+ );
+}
+
+#[test]
+fn graph_comment() {
+ let g = GraphBuilder::new_directed(None)
+ .comment("Comment goes here")
+ .build();
+ let r = test_input(g);
+ assert_eq!(
+ r.unwrap(),
+ r#"// Comment goes here
+digraph {
+}
+"#
+ );
+}
+
+#[test]
+fn empty_digraph() {
+ let g = GraphBuilder::new_directed(Some("empty_graph".to_string())).build();
+ let r = test_input(g);
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph empty_graph {
+}
+"#
+ );
+}
+
+#[test]
+fn empty_undirected_graph() {
+ let g = GraphBuilder::new_undirected(Some("empty_graph".to_string())).build();
+ let r = test_input(g);
+ assert_eq!(
+ r.unwrap(),
+ r#"graph empty_graph {
+}
+"#
+ );
+}
+
+#[test]
+fn single_node() {
+ let g = GraphBuilder::new_directed(Some("single_node".to_string()))
+ .add_node(Node::new("N0".to_string()))
+ .build();
+ let r = test_input(g);
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph single_node {
+ N0;
+}
+"#
+ );
+}
+
+#[test]
+fn single_node_with_style() {
+ let node = NodeBuilder::new("N0".to_string())
+ .style(NodeStyle::Dashed)
+ .build();
+
+ let g = GraphBuilder::new_directed(Some("single_node".to_string()))
+ .add_node(node)
+ .build();
+
+ let r = test_input(g);
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph single_node {
+ N0 [style=dashed];
+}
+"#
+ );
+}
+
+#[test]
+fn support_non_inline_builder() {
+ let mut g = GraphBuilder::new_directed(Some("single_node".to_string()));
+
+ // TODO: having to split this is stupid. am i doing something wrong?
+ let mut node_builder = NodeBuilder::new("N0".to_string());
+ node_builder.style(NodeStyle::Dashed);
+
+ if true {
+ node_builder.add_attribute("foo", AttributeText::quoted("baz"));
+ }
+
+ let node = node_builder.build();
+ g.add_node(node);
+
+ let r = test_input(g.build());
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph single_node {
+ N0 [style=dashed, foo="baz"];
+}
+"#
+ );
+}
+
+#[test]
+fn builder_support_shape() {
+ let node = NodeBuilder::new("N0".to_string())
+ .shape(Shape::Note)
+ .build();
+
+ let g = GraphBuilder::new_directed(Some("node_shape".to_string()))
+ .add_node(node)
+ .build();
+
+ let r = test_input(g);
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph node_shape {
+ N0 [shape=note];
+}
+"#
+ );
+}
+
+#[test]
+fn single_edge() {
+ let g = GraphBuilder::new_directed(Some("single_edge".to_string()))
+ .add_node(Node::new("N0".to_string()))
+ .add_node(Node::new("N1".to_string()))
+ .add_edge(Edge::new("N0".to_string(), "N1".to_string()))
+ .build();
+
+ let r = test_input(g);
+
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph single_edge {
+ N0;
+ N1;
+ N0 -> N1;
+}
+"#
+ );
+}
+
+#[test]
+fn single_edge_with_style() {
+ let edge = EdgeBuilder::new("N0".to_string(), "N1".to_string())
+ .style(EdgeStyle::Bold)
+ .build();
+
+ let g = GraphBuilder::new_directed(Some("single_edge".to_string()))
+ .add_node(Node::new("N0".to_string()))
+ .add_node(Node::new("N1".to_string()))
+ .add_edge(edge)
+ .build();
+
+ let r = test_input(g);
+
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph single_edge {
+ N0;
+ N1;
+ N0 -> N1 [style=bold];
+}
+"#
+ );
+}
+
+#[test]
+fn edge_statement_port_position() {
+ let node_0 = NodeBuilder::new("N0".to_string())
+ .shape(Shape::Record)
+ .label("a|<port0>b")
+ .build();
+
+ let node_1 = NodeBuilder::new("N1".to_string())
+ .shape(Shape::Record)
+ .label("e|<port1>f")
+ .build();
+
+ let edge = EdgeBuilder::new("N0".to_string(), "N1".to_string())
+ .source_port_position(PortPosition::Port {
+ port_name: "port0".to_string(),
+ compass_point: Some(CompassPoint::SW),
+ })
+ .target_port_position(PortPosition::Port {
+ port_name: "port1".to_string(),
+ compass_point: Some(CompassPoint::NE),
+ })
+ .build();
+
+ let g = GraphBuilder::new_directed(Some("edge_statement_port_position".to_string()))
+ .add_node(node_0)
+ .add_node(node_1)
+ .add_edge(edge)
+ .build();
+
+ let r = test_input(g);
+
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph edge_statement_port_position {
+ N0 [shape=record, label="a|<port0>b"];
+ N1 [shape=record, label="e|<port1>f"];
+ N0:port0:sw -> N1:port1:ne;
+}
+"#
+ );
+}
+
+#[test]
+fn port_position_attribute() {
+ let node_0 = NodeBuilder::new("N0".to_string())
+ .shape(Shape::Record)
+ .label("a|<port0>b")
+ .build();
+
+ let node_1 = NodeBuilder::new("N1".to_string())
+ .shape(Shape::Record)
+ .label("e|<port1>f")
+ .build();
+
+ let edge = EdgeBuilder::new("N0".to_string(), "N1".to_string())
+ .tail_port(PortPosition::Port {
+ port_name: "port0".to_string(),
+ compass_point: Some(CompassPoint::SW),
+ })
+ .head_port(PortPosition::Port {
+ port_name: "port1".to_string(),
+ compass_point: Some(CompassPoint::NE),
+ })
+ .build();
+
+ let g = GraphBuilder::new_directed(Some("port_position_attribute".to_string()))
+ .add_node(node_0)
+ .add_node(node_1)
+ .add_edge(edge)
+ .build();
+
+ let r = test_input(g);
+
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph port_position_attribute {
+ N0 [shape=record, label="a|<port0>b"];
+ N1 [shape=record, label="e|<port1>f"];
+ N0 -> N1 [tailport="port0:sw", headport="port1:ne"];
+}
+"#
+ );
+}
+
+#[test]
+fn graph_attributes() {
+ let graph_attributes = GraphAttributeStatementBuilder::new()
+ .rank_dir(RankDir::LeftRight)
+ .build();
+ let node_attributes = NodeAttributeStatementBuilder::new()
+ .style(NodeStyle::Filled)
+ .build();
+ let edge_attributes = EdgeAttributeStatementBuilder::new().min_len(1).build();
+
+ let g = GraphBuilder::new_directed(Some("graph_attributes".to_string()))
+ .add_graph_attributes(graph_attributes)
+ .add_node_attributes(node_attributes)
+ .add_edge_attributes(edge_attributes)
+ .build();
+
+ let r = test_input(g);
+
+ assert_eq!(
+ r.unwrap(),
+ r#"digraph graph_attributes {
+ graph [rankdir=LR];
+ node [style=filled];
+ edge [minlen=1];
+}
+"#
+ );
+}