CREATE_TEST(GVLayout_construction)
CREATE_TEST(GVLayout_render)
CREATE_TEST(neatopack)
+CREATE_TEST(node_penwidth)
CREATE_TEST(rankdir)
CREATE_TEST(simple)
CREATE_TEST(subgraph_layout)
SVG::SVGPoint GraphvizNode::center() const { return bbox().center(); }
+double GraphvizNode::penwidth() const {
+ return m_svg_g_element.attribute_from_subtree<double>(
+ &SVG::SVGAttributes::stroke_width, &SVG::SVGElement::is_shape_element, 1);
+}
+
std::string_view GraphvizNode::node_id() const { return m_node_id; }
SVG::SVGRect GraphvizNode::bbox() const { return m_svg_g_element.bbox(); }
SVG::SVGPoint center() const;
/// Return the node's `node_id` as defined by the DOT language
std::string_view node_id() const;
+ /// Return the node's `penwidth` attribute
+ double penwidth() const;
/// Return a non-mutable reference to the SVG `g` element corresponding to the
/// node
const SVG::SVGElement &svg_g_element() const;
return px * 3 / 4;
}
+bool SVG::SVGElement::is_shape_element() const {
+ switch (type) {
+ case SVG::SVGElementType::Circle:
+ case SVG::SVGElementType::Ellipse:
+ case SVG::SVGElementType::Line:
+ case SVG::SVGElementType::Path:
+ case SVG::SVGElementType::Polygon:
+ case SVG::SVGElementType::Polyline:
+ case SVG::SVGElementType::Rect:
+ return true;
+ default:
+ return false;
+ }
+}
+
static std::string xml_encode(const std::string &text) {
std::string out;
for (const char &ch : text) {
#include <cmath>
#include <optional>
+#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
+#include <fmt/format.h>
+
namespace SVG {
struct SVGPoint {
SVGElement() = delete;
explicit SVGElement(SVG::SVGElementType type);
+ /// \brief Return the value of an attribute retrieved from the element and its
+ /// children
+ ///
+ /// @param attribute_ptr pointer to a member of the `SVGAttributes` class.
+ /// @param predicate pointer to a member function of the `SVGElement` class
+ /// which returns `true` for elements on which the attribute exists.
+ /// @param default_value the value to return if the attribute does not exist
+ /// on any of the elements.
+ ///
+ /// Returns the value of the attribute if it has the same value on all
+ /// elements on which it exists and throws an exception if the value is not
+ /// consistent on those elements. Returns the default value if the attribute
+ /// does not exist on any of the elements.
+ template <typename T>
+ T attribute_from_subtree(T SVGAttributes::*attribute_ptr,
+ bool (SVGElement::*predicate)() const,
+ T default_value) const {
+ std::optional<T> attribute;
+ if ((this->*predicate)()) {
+ attribute = this->attributes.*attribute_ptr;
+ }
+ for (const auto &child : children) {
+ if (!(child.*predicate)()) {
+ continue;
+ }
+ const auto child_attribute = child.attributes.*attribute_ptr;
+ if (!attribute.has_value()) {
+ attribute = child_attribute;
+ continue;
+ }
+ if (attribute.value() != child_attribute) {
+ throw std::runtime_error{fmt::format(
+ "Inconsistent value of attribute: current {}: {}, child {}: {}",
+ tag(type), attribute.value(), tag(child.type), child_attribute)};
+ }
+ }
+ return attribute.value_or(default_value);
+ }
/// Return the bounding box of the element and its children. The bounding box
/// is calculated and stored the first time this function is called and later
/// calls will return the already calculated value. If this function is called
/// throw an exception unless the `throw_if_bbox_not_defined` parameter is
/// `false`.
SVG::SVGRect bbox(bool throw_if_bbox_not_defined = true);
+ bool is_shape_element() const;
std::string to_string(std::size_t indent_size) const;
SVGAttributes attributes;
--- /dev/null
+#include <string_view>
+
+#include <catch2/catch.hpp>
+#include <fmt/format.h>
+
+#include "svg_analyzer.h"
+#include "test_utilities.h"
+
+TEST_CASE("Node penwidth",
+ "Test that the Graphviz 'penwidth' attribute is used to set the "
+ "'stroke-width' attribute correctly for nodes in the generated SVG") {
+
+ const auto shape = GENERATE(filter(
+ [](std::string_view shape) {
+ return !node_shapes_without_svg_shape.contains(shape);
+ },
+ from_range(all_node_shapes)));
+ INFO(fmt::format("Shape: {}", shape));
+
+ const auto node_penwidth = GENERATE(0.5, 1.0, 2.0);
+ INFO(fmt::format("Node penwidth: {}", node_penwidth));
+
+ auto dot = fmt::format("digraph g1 {{node [shape={} penwidth={}]; a -> b}}",
+ shape, node_penwidth);
+
+ const auto engine = "dot";
+ auto svg_analyzer = SVGAnalyzer::make_from_dot(dot, engine);
+
+ for (const auto &graph : svg_analyzer.graphs()) {
+ for (const auto &node : graph.nodes()) {
+ CHECK(node.penwidth() == node_penwidth);
+ }
+ }
+}