lutaml-graphviz 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/lutaml/graphviz/formatter/graphviz/document_formatter.rb +89 -0
- data/lib/lutaml/graphviz/formatter/graphviz/html_builder.rb +72 -0
- data/lib/lutaml/graphviz/formatter/graphviz/node_formatter.rb +89 -0
- data/lib/lutaml/graphviz/formatter/graphviz/relationship_formatter.rb +130 -0
- data/lib/lutaml/graphviz/formatter/graphviz.rb +90 -0
- data/lib/lutaml/graphviz/layout/graph_viz_engine.rb +19 -0
- data/lib/lutaml/graphviz/version.rb +7 -0
- data/lib/lutaml/graphviz.rb +27 -0
- metadata +81 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 2f8d38e9554f3ba105ab977a65c8fae46fa9bc9a75b66548dcd0cfa9bc8ee96d
|
|
4
|
+
data.tar.gz: 6734497fb339f562370efd5415af737ea9cf5bf8a5748efbbea36061f723eb32
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6f382d5798769a1a90a5b70c71db72f278d1f9750a1227ae4084e9e435f64bfd53a1b701d8bab464d504e6a7bbfa20f416aeb167eba7f390dc0ae052baad6721
|
|
7
|
+
data.tar.gz: 63f8b3eeb80e2262110ed15a18a5b15a2dce97060519c8727a2657149b5db83c852f417a05ba5b6af0335e3080ae8bf413fd3a09eb140543fb5624a0ddc2e50e
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Formatter
|
|
5
|
+
class Graphviz < Base
|
|
6
|
+
module DocumentFormatter
|
|
7
|
+
def format_document(node)
|
|
8
|
+
@fontname = node.fontname || DEFAULT_CLASS_FONT
|
|
9
|
+
@node["fontname"] = "#{@fontname}-bold"
|
|
10
|
+
|
|
11
|
+
hide_members, hide_other_classes = extract_fidelity_options(node)
|
|
12
|
+
classes = format_all_classes(node, hide_members)
|
|
13
|
+
associations = build_associations(node, hide_other_classes)
|
|
14
|
+
|
|
15
|
+
build_digraph(classes, associations)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def extract_fidelity_options(node)
|
|
19
|
+
if node.fidelity
|
|
20
|
+
[node.fidelity.hideMembers, node.fidelity.hideOtherClasses]
|
|
21
|
+
else
|
|
22
|
+
[nil, nil]
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def format_all_classes(node, hide_members)
|
|
27
|
+
node.all_classes.map do |class_node|
|
|
28
|
+
graph_node_name = generate_graph_name(class_node.name)
|
|
29
|
+
<<~HEREDOC
|
|
30
|
+
#{graph_node_name} [
|
|
31
|
+
shape="plain"
|
|
32
|
+
fontname="#{@fontname || DEFAULT_CLASS_FONT}"
|
|
33
|
+
label=<#{format_class(class_node, hide_members)}>]
|
|
34
|
+
HEREDOC
|
|
35
|
+
end.join("\n")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def build_associations(node, hide_other_classes)
|
|
39
|
+
associations = collect_all_associations(node)
|
|
40
|
+
associations = sort_by_document_grouping(node.groups, associations) if node.groups
|
|
41
|
+
classes_names = node.classes.map(&:name)
|
|
42
|
+
format_filtered_associations(associations, classes_names, hide_other_classes)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def collect_all_associations(node)
|
|
46
|
+
class_level = node.classifiable_classes
|
|
47
|
+
Set.new
|
|
48
|
+
all = class_level.filter_map(&:associations).flatten + node.associations
|
|
49
|
+
all.uniq { |a| association_key(a) }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def format_filtered_associations(associations, classes_names, hide_other_classes)
|
|
53
|
+
associations.filter_map do |assoc_node|
|
|
54
|
+
next if hide_other_classes && !classes_names.include?(assoc_node.member_end)
|
|
55
|
+
|
|
56
|
+
format_relationship(assoc_node)
|
|
57
|
+
end.join("\n")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_digraph(classes, associations)
|
|
61
|
+
indented_classes = indent_lines(classes)
|
|
62
|
+
indented_assocs = indent_lines(associations)
|
|
63
|
+
|
|
64
|
+
<<~HEREDOC
|
|
65
|
+
digraph G {
|
|
66
|
+
graph [#{@graph}]
|
|
67
|
+
edge [#{@edge}]
|
|
68
|
+
node [#{@node}]
|
|
69
|
+
|
|
70
|
+
#{indented_classes}
|
|
71
|
+
|
|
72
|
+
#{indented_assocs}
|
|
73
|
+
}
|
|
74
|
+
HEREDOC
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def indent_lines(text)
|
|
78
|
+
text.lines.map { |line| " #{line}" }.join.chomp
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private
|
|
82
|
+
|
|
83
|
+
def association_key(assoc)
|
|
84
|
+
[assoc.owner_end.to_s, assoc.member_end.to_s, assoc.name.to_s]
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Formatter
|
|
5
|
+
class Graphviz < Base
|
|
6
|
+
module HtmlBuilder
|
|
7
|
+
EMPTY_MEMBER_TABLE = <<~HEREDOC.chomp
|
|
8
|
+
<TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
|
|
9
|
+
<TR><TD ALIGN="LEFT"></TD></TR>
|
|
10
|
+
</TABLE>
|
|
11
|
+
HEREDOC
|
|
12
|
+
|
|
13
|
+
def escape_html_chars(text)
|
|
14
|
+
text
|
|
15
|
+
.gsub("&", "&")
|
|
16
|
+
.gsub("<", "<")
|
|
17
|
+
.gsub(">", ">")
|
|
18
|
+
.gsub("[", "[")
|
|
19
|
+
.gsub("]", "]")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def format_member_rows(members, hide_members, &formatter)
|
|
23
|
+
return EMPTY_MEMBER_TABLE if hide_members || !members&.any?
|
|
24
|
+
|
|
25
|
+
formatter ||= method(:dispatch_format)
|
|
26
|
+
field_rows = members.map do |member|
|
|
27
|
+
%(<TR><TD ALIGN="LEFT">#{formatter.call(member)}</TD></TR>)
|
|
28
|
+
end
|
|
29
|
+
build_member_table(field_rows)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def format_enum_member(member)
|
|
33
|
+
enum_literal?(member) ? format_enum_literal(member) : dispatch_format(member)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def enum_literal?(node)
|
|
37
|
+
node.is_a?(Lml::TopElementAttribute) &&
|
|
38
|
+
node.type.nil? && node.cardinality.nil?
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def build_member_table(field_rows)
|
|
42
|
+
indented = field_rows.map { |row| (" " * 10) + row }.join("\n")
|
|
43
|
+
<<~HEREDOC.chomp
|
|
44
|
+
<TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
|
|
45
|
+
#{indented}
|
|
46
|
+
</TABLE>
|
|
47
|
+
HEREDOC
|
|
48
|
+
.concat("\n")
|
|
49
|
+
.concat(" " * 6)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_name_table(name_parts)
|
|
53
|
+
<<~HEREDOC
|
|
54
|
+
<TABLE BORDER="0" CELLPADDING="0" CELLSPACING="0">
|
|
55
|
+
#{name_parts.map { |n| %(<TR><TD ALIGN="CENTER">#{n}</TD></TR>) }.join("\n")}
|
|
56
|
+
</TABLE>
|
|
57
|
+
HEREDOC
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def build_table_body(name_html, field_table, method_table)
|
|
61
|
+
[name_html, field_table, method_table].compact.filter_map do |type|
|
|
62
|
+
<<~TEXT
|
|
63
|
+
<TR>
|
|
64
|
+
<TD>#{type}</TD>
|
|
65
|
+
</TR>
|
|
66
|
+
TEXT
|
|
67
|
+
end.join("\n")
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Formatter
|
|
5
|
+
class Graphviz < Base
|
|
6
|
+
module NodeFormatter
|
|
7
|
+
ACCESS_SYMBOLS = {
|
|
8
|
+
"public" => "+",
|
|
9
|
+
"protected" => "#",
|
|
10
|
+
"private" => "-",
|
|
11
|
+
"package" => "~"
|
|
12
|
+
}.freeze
|
|
13
|
+
|
|
14
|
+
def format_attribute(node)
|
|
15
|
+
symbol = ACCESS_SYMBOLS[node.visibility]
|
|
16
|
+
result = "#{symbol}#{node.name}"
|
|
17
|
+
if node.type
|
|
18
|
+
stereotype = node.stereotype.empty? ? "" : node.stereotype.map { |s| "«#{s}»" }.join
|
|
19
|
+
result += " : #{stereotype}#{node.type}"
|
|
20
|
+
end
|
|
21
|
+
result += format_cardinality_bounds(node.cardinality) if node.cardinality
|
|
22
|
+
result = escape_html_chars(result)
|
|
23
|
+
result = "<U>#{result}</U>" if node.static
|
|
24
|
+
result
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def format_cardinality_bounds(cardinality)
|
|
28
|
+
min = cardinality.min || "*"
|
|
29
|
+
max = cardinality.max || "*"
|
|
30
|
+
"[#{min}..#{max}]"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def format_operation(node)
|
|
34
|
+
symbol = ACCESS_SYMBOLS[node.visibility]
|
|
35
|
+
params = format_operation_params(node.owned_parameter)
|
|
36
|
+
result = "#{symbol} #{node.name}(#{params})"
|
|
37
|
+
result << " : #{node.return_type}" if node.return_type
|
|
38
|
+
result = escape_html_chars(result)
|
|
39
|
+
result = "<U>#{result}</U>" if node.is_static
|
|
40
|
+
result = "<I>#{result}</I>" if node.is_abstract
|
|
41
|
+
result
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def format_operation_params(params)
|
|
45
|
+
return "" unless params&.any?
|
|
46
|
+
|
|
47
|
+
params.map do |param|
|
|
48
|
+
param.type ? "#{param.name} : #{param.type}" : param.name.to_s
|
|
49
|
+
end.join(", ")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Kind labels rendered above the name: guillemet stereotypes first,
|
|
53
|
+
# then the entity-kind label (enums/data types/primitives carry no
|
|
54
|
+
# user stereotypes of their own in the grammar).
|
|
55
|
+
KIND_LABELS = {
|
|
56
|
+
enums: "enumeration",
|
|
57
|
+
data_types: "dataType",
|
|
58
|
+
primitives: "primitive"
|
|
59
|
+
}.freeze
|
|
60
|
+
|
|
61
|
+
def format_class(node, hide_members = nil)
|
|
62
|
+
name = ["<B>#{escape_html_chars(node.name)}</B>"]
|
|
63
|
+
node.stereotype.each do |stereotype|
|
|
64
|
+
name.unshift("«#{escape_html_chars(stereotype)}»")
|
|
65
|
+
end
|
|
66
|
+
if (kind = KIND_LABELS[node.class.entity_type])
|
|
67
|
+
name.unshift("«#{kind}»")
|
|
68
|
+
end
|
|
69
|
+
name_html = build_name_table(name)
|
|
70
|
+
|
|
71
|
+
member_formatter = node.is_a?(Lml::Enum) ? method(:format_enum_member) : nil
|
|
72
|
+
field_table = format_member_rows(node.attributes, hide_members, &member_formatter)
|
|
73
|
+
method_table = format_member_rows(node.operations, hide_members) if node.operations&.any?
|
|
74
|
+
table_body = build_table_body(name_html, field_table, method_table)
|
|
75
|
+
|
|
76
|
+
<<~HEREDOC.chomp
|
|
77
|
+
<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="10">
|
|
78
|
+
#{table_body}
|
|
79
|
+
</TABLE>
|
|
80
|
+
HEREDOC
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def format_enum_literal(node)
|
|
84
|
+
"<I>#{escape_html_chars(node.name.to_s)}</I>"
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Lutaml
|
|
4
|
+
module Formatter
|
|
5
|
+
class Graphviz < Base
|
|
6
|
+
module RelationshipFormatter
|
|
7
|
+
ARROW_TYPES = {
|
|
8
|
+
"composition" => "diamond",
|
|
9
|
+
"aggregation" => "odiamond",
|
|
10
|
+
"direct" => "vee"
|
|
11
|
+
}.freeze
|
|
12
|
+
|
|
13
|
+
DASHED_TYPES = %w[dependency realizes].freeze
|
|
14
|
+
|
|
15
|
+
DIRECTION_LABELS = {
|
|
16
|
+
"target" => "%s ▶",
|
|
17
|
+
"source" => "◀ %s"
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def format_relationship(node)
|
|
21
|
+
graph_parent_name = generate_graph_name(node.owner_end)
|
|
22
|
+
graph_node_name = generate_graph_name(node.member_end)
|
|
23
|
+
attributes = build_edge_attributes(node)
|
|
24
|
+
graph_attributes = " [#{attributes}]" unless attributes.empty?
|
|
25
|
+
|
|
26
|
+
"#{graph_parent_name} -> #{graph_node_name}#{graph_attributes}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def build_edge_attributes(node)
|
|
30
|
+
attrs = Attributes.new
|
|
31
|
+
|
|
32
|
+
apply_edge_style(attrs, node)
|
|
33
|
+
apply_edge_direction(attrs, node)
|
|
34
|
+
apply_edge_labels(attrs, node)
|
|
35
|
+
apply_arrow_types(attrs, node)
|
|
36
|
+
maybe_swap_labels(attrs)
|
|
37
|
+
|
|
38
|
+
attrs
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def format_label(name, cardinality = nil)
|
|
42
|
+
res = "+#{name}"
|
|
43
|
+
return res if cardinality.nil? || cardinality.min.nil? || cardinality.max.nil?
|
|
44
|
+
|
|
45
|
+
"#{res} #{cardinality.min}..#{cardinality.max}"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def sort_by_document_grouping(groups, associations)
|
|
51
|
+
result = []
|
|
52
|
+
seen = Set.new
|
|
53
|
+
|
|
54
|
+
groups.each do |group|
|
|
55
|
+
group.values.each do |group_name| # rubocop:disable Style/HashEachMethods
|
|
56
|
+
matches = associations.select { |a| a.owner_end == group_name }
|
|
57
|
+
matches.each { |a| add_unseen(a, result, seen) }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
associations.each { |a| add_unseen(a, result, seen) }
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def add_unseen(association, result, seen)
|
|
65
|
+
return if seen.include?(association)
|
|
66
|
+
|
|
67
|
+
result.push(association)
|
|
68
|
+
seen.add(association)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def apply_edge_style(attrs, node)
|
|
72
|
+
attrs["style"] = "dashed" if DASHED_TYPES.include?(node.member_end_type)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def apply_edge_direction(attrs, node)
|
|
76
|
+
attrs["dir"] = if node.owner_end_type && node.member_end_type
|
|
77
|
+
"both"
|
|
78
|
+
elsif node.owner_end_type
|
|
79
|
+
"back"
|
|
80
|
+
else
|
|
81
|
+
"direct"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def apply_edge_labels(attrs, node)
|
|
86
|
+
apply_verb_label(attrs, node)
|
|
87
|
+
apply_direction_label(attrs, node)
|
|
88
|
+
apply_endpoint_labels(attrs, node)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def apply_verb_label(attrs, node)
|
|
92
|
+
attrs["label"] = node.action.verb if node&.action&.verb
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def apply_direction_label(attrs, node)
|
|
96
|
+
return unless node&.action&.direction
|
|
97
|
+
|
|
98
|
+
template = DIRECTION_LABELS[node.action.direction]
|
|
99
|
+
attrs["label"] = template % attrs["label"] if template
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def apply_endpoint_labels(attrs, node)
|
|
103
|
+
if node.owner_end_attribute_name
|
|
104
|
+
attrs["headlabel"] = format_label(
|
|
105
|
+
node.owner_end_attribute_name, node.owner_end_cardinality
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return unless node.member_end_attribute_name
|
|
110
|
+
|
|
111
|
+
attrs["taillabel"] = format_label(
|
|
112
|
+
node.member_end_attribute_name, node.member_end_cardinality
|
|
113
|
+
)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def apply_arrow_types(attrs, node)
|
|
117
|
+
attrs["arrowtail"] = ARROW_TYPES.fetch(node.owner_end_type, "onormal")
|
|
118
|
+
attrs["arrowhead"] = ARROW_TYPES.fetch(node.member_end_type, "onormal")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def maybe_swap_labels(attrs)
|
|
122
|
+
return unless attrs["dir"] == "back" && attrs["arrowtail"] != "vee"
|
|
123
|
+
|
|
124
|
+
attrs["arrowhead"], attrs["arrowtail"] = [attrs["arrowtail"], attrs["arrowhead"]]
|
|
125
|
+
attrs["headlabel"], attrs["taillabel"] = [attrs["taillabel"], attrs["headlabel"]]
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "open3"
|
|
4
|
+
require "set"
|
|
5
|
+
|
|
6
|
+
module Lutaml
|
|
7
|
+
module Formatter
|
|
8
|
+
class Graphviz < Base
|
|
9
|
+
autoload :HtmlBuilder, "lutaml/graphviz/formatter/graphviz/html_builder"
|
|
10
|
+
autoload :NodeFormatter, "lutaml/graphviz/formatter/graphviz/node_formatter"
|
|
11
|
+
autoload :RelationshipFormatter, "lutaml/graphviz/formatter/graphviz/relationship_formatter"
|
|
12
|
+
autoload :DocumentFormatter, "lutaml/graphviz/formatter/graphviz/document_formatter"
|
|
13
|
+
|
|
14
|
+
include HtmlBuilder
|
|
15
|
+
include NodeFormatter
|
|
16
|
+
include RelationshipFormatter
|
|
17
|
+
include DocumentFormatter
|
|
18
|
+
|
|
19
|
+
class Attributes < Hash
|
|
20
|
+
def to_s
|
|
21
|
+
to_a
|
|
22
|
+
.reject { |(_k, val)| val.nil? }
|
|
23
|
+
.map { |(a, b)| "#{a}=#{b.inspect}" }
|
|
24
|
+
.join(" ")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
DEFAULT_CLASS_FONT = "Helvetica"
|
|
29
|
+
|
|
30
|
+
VALID_TYPES = %i[
|
|
31
|
+
dot xdot ps pdf svg svgz fig png gif jpg jpeg json imap cmapx
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
def initialize(attributes = {})
|
|
35
|
+
super
|
|
36
|
+
setup_default_attributes
|
|
37
|
+
@type = :dot
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attr_reader :graph, :edge, :node
|
|
41
|
+
|
|
42
|
+
def type=(value)
|
|
43
|
+
super
|
|
44
|
+
@type = :dot unless VALID_TYPES.include?(@type)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def format(node)
|
|
48
|
+
dot = super.lines.map(&:rstrip).join("\n")
|
|
49
|
+
generate_from_dot(dot)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def setup_default_attributes
|
|
53
|
+
@graph = build_graph_attrs
|
|
54
|
+
@edge = build_edge_attrs
|
|
55
|
+
@node = build_node_attrs
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def build_graph_attrs
|
|
59
|
+
Attributes.new.tap do |g|
|
|
60
|
+
g["splines"] = "ortho"
|
|
61
|
+
g["pad"] = "0.5"
|
|
62
|
+
g["ranksep"] = "1.2"
|
|
63
|
+
g["nodesep"] = "1.2"
|
|
64
|
+
g["rankdir"] = "BT"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def build_edge_attrs
|
|
69
|
+
Attributes.new.tap { |e| e["color"] = "gray50" }
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def build_node_attrs
|
|
73
|
+
Attributes.new.tap { |n| n["shape"] = "box" }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
private :setup_default_attributes, :build_graph_attrs,
|
|
77
|
+
:build_edge_attrs, :build_node_attrs
|
|
78
|
+
|
|
79
|
+
protected
|
|
80
|
+
|
|
81
|
+
def generate_from_dot(input)
|
|
82
|
+
Lutaml::Layout::GraphVizEngine.new(input: input).render(@type)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def generate_graph_name(name)
|
|
86
|
+
name.gsub(/[^0-9a-zA-Z]/i, "")
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ruby-graphviz"
|
|
4
|
+
require "open3"
|
|
5
|
+
|
|
6
|
+
module Lutaml
|
|
7
|
+
module Layout
|
|
8
|
+
class GraphVizEngine < Engine
|
|
9
|
+
def render(type)
|
|
10
|
+
Open3.popen3("dot", "-T#{type}") do |stdin, stdout, _stderr, _wait|
|
|
11
|
+
stdout.binmode
|
|
12
|
+
stdin.puts(input)
|
|
13
|
+
stdin.close
|
|
14
|
+
stdout.read
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/lml"
|
|
4
|
+
require_relative "graphviz/version"
|
|
5
|
+
|
|
6
|
+
module Lutaml
|
|
7
|
+
# Graphviz diagram rendering for LutaML models. The renderers register
|
|
8
|
+
# themselves against the model-driven formatter registry owned by
|
|
9
|
+
# lutaml (Lutaml::Formatter.all / .find_by_name), so requiring this file
|
|
10
|
+
# is all a consumer does to make the `graphviz` formatter available.
|
|
11
|
+
# ruby-graphviz and the `dot` toolchain live in this gem only.
|
|
12
|
+
module Graphviz
|
|
13
|
+
class Error < Lutaml::Error; end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module Formatter
|
|
17
|
+
autoload :Graphviz, "lutaml/graphviz/formatter/graphviz"
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module Layout
|
|
21
|
+
autoload :GraphVizEngine, "lutaml/graphviz/layout/graph_viz_engine"
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Force the autoload so the formatter registers in Lutaml::Formatter.all
|
|
26
|
+
# as soon as the gem is required.
|
|
27
|
+
Lutaml::Formatter::Graphviz
|
metadata
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lutaml-graphviz
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ribose Inc.
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: lutaml
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: ruby-graphviz
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
description: Renders LutaML (LML) documents as Graphviz diagrams. Owns the ruby-graphviz
|
|
41
|
+
dependency so the lutaml core gemspecs never carry an optional render toolchain.
|
|
42
|
+
email:
|
|
43
|
+
- open.source@ribose.com
|
|
44
|
+
executables: []
|
|
45
|
+
extensions: []
|
|
46
|
+
extra_rdoc_files: []
|
|
47
|
+
files:
|
|
48
|
+
- lib/lutaml/graphviz.rb
|
|
49
|
+
- lib/lutaml/graphviz/formatter/graphviz.rb
|
|
50
|
+
- lib/lutaml/graphviz/formatter/graphviz/document_formatter.rb
|
|
51
|
+
- lib/lutaml/graphviz/formatter/graphviz/html_builder.rb
|
|
52
|
+
- lib/lutaml/graphviz/formatter/graphviz/node_formatter.rb
|
|
53
|
+
- lib/lutaml/graphviz/formatter/graphviz/relationship_formatter.rb
|
|
54
|
+
- lib/lutaml/graphviz/layout/graph_viz_engine.rb
|
|
55
|
+
- lib/lutaml/graphviz/version.rb
|
|
56
|
+
homepage: https://github.com/lutaml/lutaml-graphviz
|
|
57
|
+
licenses:
|
|
58
|
+
- MIT
|
|
59
|
+
metadata:
|
|
60
|
+
homepage_uri: https://github.com/lutaml/lutaml-graphviz
|
|
61
|
+
source_code_uri: https://github.com/lutaml/lutaml-graphviz
|
|
62
|
+
changelog_uri: https://github.com/lutaml/lutaml-graphviz/releases
|
|
63
|
+
rubygems_mfa_required: 'true'
|
|
64
|
+
rdoc_options: []
|
|
65
|
+
require_paths:
|
|
66
|
+
- lib
|
|
67
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: '3.0'
|
|
72
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
73
|
+
requirements:
|
|
74
|
+
- - ">="
|
|
75
|
+
- !ruby/object:Gem::Version
|
|
76
|
+
version: '0'
|
|
77
|
+
requirements: []
|
|
78
|
+
rubygems_version: 4.0.23
|
|
79
|
+
specification_version: 4
|
|
80
|
+
summary: Graphviz diagram rendering for LutaML models
|
|
81
|
+
test_files: []
|