elkrb 1.0.0 → 2.0.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 +4 -4
- data/CHANGELOG.adoc +364 -0
- data/LICENSE +25 -0
- data/README.adoc +695 -557
- data/exe/elkrb +36 -2
- data/lib/elkrb/atomic_destination.rb +53 -0
- data/lib/elkrb/best_effort_write.rb +49 -0
- data/lib/elkrb/cli/layout_flag_options.rb +54 -0
- data/lib/elkrb/cli.rb +155 -82
- data/lib/elkrb/command_resolver.rb +186 -0
- data/lib/elkrb/commands/batch_command.rb +28 -5
- data/lib/elkrb/commands/convert_command.rb +22 -52
- data/lib/elkrb/commands/diagram_command.rb +234 -102
- data/lib/elkrb/commands/options_command.rb +87 -0
- data/lib/elkrb/commands/render_command.rb +6 -1
- data/lib/elkrb/commands/validate_command.rb +146 -91
- data/lib/elkrb/errors.rb +20 -1
- data/lib/elkrb/format_sniffer.rb +362 -0
- data/lib/elkrb/geometry/bezier.rb +14 -4
- data/lib/elkrb/geometry/point.rb +5 -6
- data/lib/elkrb/geometry/rectangle.rb +2 -2
- data/lib/elkrb/graph/deep_stringify_keys.rb +30 -0
- data/lib/elkrb/graph/edge.rb +89 -3
- data/lib/elkrb/graph/graph.rb +43 -15
- data/lib/elkrb/graph/label.rb +16 -2
- data/lib/elkrb/graph/node.rb +13 -2
- data/lib/elkrb/graph/node_constraints.rb +139 -8
- data/lib/elkrb/graph/normalize_option_keys.rb +39 -0
- data/lib/elkrb/graph/port.rb +29 -21
- data/lib/elkrb/graph/read_only_mapping.rb +21 -0
- data/lib/elkrb/graphviz_wrapper.rb +152 -44
- data/lib/elkrb/layout/algorithm_registry.rb +65 -8
- data/lib/elkrb/layout/algorithms/base_algorithm.rb +74 -74
- data/lib/elkrb/layout/algorithms/box.rb +47 -20
- data/lib/elkrb/layout/algorithms/disco.rb +150 -61
- data/lib/elkrb/layout/algorithms/fixed.rb +70 -10
- data/lib/elkrb/layout/algorithms/force.rb +145 -86
- data/lib/elkrb/layout/algorithms/layered/bk/aligner.rb +104 -0
- data/lib/elkrb/layout/algorithms/layered/bk/compactor.rb +87 -0
- data/lib/elkrb/layout/algorithms/layered/bk/view.rb +103 -0
- data/lib/elkrb/layout/algorithms/layered/bk_node_placer.rb +170 -0
- data/lib/elkrb/layout/algorithms/layered/components.rb +170 -0
- data/lib/elkrb/layout/algorithms/layered/crossing_minimizer.rb +306 -0
- data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +118 -49
- data/lib/elkrb/layout/algorithms/layered/discovery_order.rb +75 -0
- data/lib/elkrb/layout/algorithms/layered/dummy_slot.rb +31 -0
- data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +188 -48
- data/lib/elkrb/layout/algorithms/layered/layer_constraints.rb +82 -0
- data/lib/elkrb/layout/algorithms/layered/layer_sweep.rb +195 -0
- data/lib/elkrb/layout/algorithms/layered/node_placer.rb +214 -36
- data/lib/elkrb/layout/algorithms/layered/orthogonal/gap_solver.rb +219 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/hyper_edge_segment.rb +152 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/junction_points.rb +58 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal/segment_order.rb +134 -0
- data/lib/elkrb/layout/algorithms/layered/orthogonal_router.rb +253 -0
- data/lib/elkrb/layout/algorithms/layered/port_order.rb +188 -0
- data/lib/elkrb/layout/algorithms/layered/port_spread.rb +42 -0
- data/lib/elkrb/layout/algorithms/layered.rb +548 -13
- data/lib/elkrb/layout/algorithms/libavoid.rb +951 -187
- data/lib/elkrb/layout/algorithms/mrtree.rb +585 -65
- data/lib/elkrb/layout/algorithms/radial.rb +188 -29
- data/lib/elkrb/layout/algorithms/random.rb +131 -17
- data/lib/elkrb/layout/algorithms/rectpacking.rb +28 -40
- data/lib/elkrb/layout/algorithms/spore_compaction.rb +57 -100
- data/lib/elkrb/layout/algorithms/spore_overlap.rb +78 -84
- data/lib/elkrb/layout/algorithms/stress.rb +391 -84
- data/lib/elkrb/layout/algorithms/topdown_packing.rb +95 -31
- data/lib/elkrb/layout/algorithms/vertiflex.rb +12 -47
- data/lib/elkrb/layout/constraints/alignment_constraint.rb +49 -12
- data/lib/elkrb/layout/constraints/base_constraint.rb +3 -15
- data/lib/elkrb/layout/constraints/constraint_processor.rb +9 -23
- data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +22 -13
- data/lib/elkrb/layout/constraints/layer_constraint.rb +10 -33
- data/lib/elkrb/layout/constraints/relative_position_constraint.rb +94 -50
- data/lib/elkrb/layout/edge_router.rb +533 -337
- data/lib/elkrb/layout/hierarchical_processor.rb +19 -276
- data/lib/elkrb/layout/java_random.rb +59 -0
- data/lib/elkrb/layout/label_placer.rb +165 -157
- data/lib/elkrb/layout/layout_engine.rb +46 -23
- data/lib/elkrb/layout/node_index.rb +149 -0
- data/lib/elkrb/layout/polyomino/component_compactor.rb +138 -0
- data/lib/elkrb/layout/polyomino/footprint.rb +94 -0
- data/lib/elkrb/layout/polyomino/grid.rb +106 -0
- data/lib/elkrb/layout/polyomino/packer.rb +95 -0
- data/lib/elkrb/layout/polyomino/piece.rb +52 -0
- data/lib/elkrb/layout/port_constraint_processor.rb +131 -44
- data/lib/elkrb/layout/spore/body.rb +65 -0
- data/lib/elkrb/layout/spore/depth_first_compaction.rb +127 -0
- data/lib/elkrb/layout/spore/edge_cost.rb +69 -0
- data/lib/elkrb/layout/spore/gap.rb +76 -0
- data/lib/elkrb/layout/spore/geometry.rb +104 -0
- data/lib/elkrb/layout/spore/overlap_sweep.rb +90 -0
- data/lib/elkrb/layout/spore/placement.rb +57 -0
- data/lib/elkrb/layout/spore/rays.rb +119 -0
- data/lib/elkrb/layout/spore/spanning_tree.rb +53 -0
- data/lib/elkrb/layout/spore/triangulation.rb +159 -0
- data/lib/elkrb/layout_flags.rb +69 -0
- data/lib/elkrb/options/decimal.rb +50 -0
- data/lib/elkrb/options/element_options.rb +43 -0
- data/lib/elkrb/options/elk_padding.rb +20 -14
- data/lib/elkrb/options/k_vector.rb +6 -7
- data/lib/elkrb/options/k_vector_chain.rb +12 -20
- data/lib/elkrb/options/option_map.rb +85 -0
- data/lib/elkrb/options/registry.rb +335 -0
- data/lib/elkrb/options/resolver.rb +151 -0
- data/lib/elkrb/options/spellings.rb +62 -0
- data/lib/elkrb/options/unhonoured_report.rb +194 -0
- data/lib/elkrb/parsers/elkt/lexer.rb +309 -0
- data/lib/elkrb/parsers/elkt/parser.rb +424 -0
- data/lib/elkrb/parsers/elkt/resolver.rb +142 -0
- data/lib/elkrb/parsers/elkt/token.rb +40 -0
- data/lib/elkrb/parsers/elkt_parser.rb +32 -230
- data/lib/elkrb/serializers/dot_serializer.rb +342 -54
- data/lib/elkrb/serializers/elkt_serializer.rb +386 -155
- data/lib/elkrb/version.rb +1 -1
- data/lib/elkrb.rb +60 -61
- metadata +71 -49
- data/.rspec +0 -3
- data/.rubocop.yml +0 -11
- data/Gemfile +0 -13
- data/Rakefile +0 -64
- data/benchmarks/README.md +0 -172
- data/benchmarks/elkjs_benchmark.js +0 -140
- data/benchmarks/elkrb_benchmark.rb +0 -145
- data/benchmarks/fixtures/graphs.json +0 -10777
- data/benchmarks/generate_report.rb +0 -241
- data/benchmarks/generate_test_graphs.rb +0 -154
- data/benchmarks/results/elkrb_results.json +0 -280
- data/benchmarks/results/elkrb_summary.json +0 -285
- data/elkrb.gemspec +0 -39
- data/examples/dot_export_demo.rb +0 -133
- data/examples/hierarchical_graph.rb +0 -19
- data/examples/layout_constraints_demo.rb +0 -272
- data/examples/port_constraints_demo.rb +0 -291
- data/examples/self_loop_demo.rb +0 -391
- data/examples/simple_graph.rb +0 -50
- data/examples/spline_routing_demo.rb +0 -235
- data/lib/elkrb/graph/layout_options.rb +0 -247
- data/sig/elkrb/constraints.rbs +0 -114
- data/sig/elkrb/geometry.rbs +0 -61
- data/sig/elkrb/graph.rbs +0 -112
- data/sig/elkrb/layout.rbs +0 -107
- data/sig/elkrb/options.rbs +0 -81
- data/sig/elkrb.rbs +0 -32
|
@@ -1,235 +1,466 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require_relative "../errors"
|
|
4
5
|
|
|
5
6
|
module Elkrb
|
|
6
7
|
module Serializers
|
|
7
|
-
#
|
|
8
|
-
# Converts ELK graph structures to textual ELKT representation
|
|
8
|
+
# Serializes graph models and Hashes to ELK Text.
|
|
9
9
|
class ElktSerializer
|
|
10
|
+
IDENTIFIER = /\A[A-Za-z_]\w*\z/
|
|
11
|
+
PROPERTY_SEGMENT = /\A[A-Za-z_]\w*\z/
|
|
12
|
+
KEYWORDS = %w[
|
|
13
|
+
graph node port label edge layout section position size
|
|
14
|
+
start end bends incoming outgoing true false null
|
|
15
|
+
].freeze
|
|
16
|
+
LABEL_ESCAPES = {
|
|
17
|
+
"\\" => "\\\\", '"' => '\\"', "\n" => "\\n", "\r" => "\\r",
|
|
18
|
+
"\t" => "\\t", "\b" => "\\b", "\f" => "\\f"
|
|
19
|
+
}.freeze
|
|
20
|
+
private_constant :IDENTIFIER, :PROPERTY_SEGMENT, :KEYWORDS, :LABEL_ESCAPES
|
|
21
|
+
|
|
10
22
|
def initialize(options = {})
|
|
11
23
|
@indent_size = options[:indent_size] || 2
|
|
12
|
-
@include_comments = options.fetch(:include_comments, true)
|
|
13
24
|
end
|
|
14
25
|
|
|
15
26
|
def serialize(graph, _options = {})
|
|
16
27
|
@indent_level = 0
|
|
17
28
|
@output = []
|
|
29
|
+
@graph_hash = graph_hash(graph)
|
|
30
|
+
@scope_stack = []
|
|
31
|
+
prepare_ids(@graph_hash)
|
|
32
|
+
emit_root(@graph_hash)
|
|
33
|
+
"#{@output.join("\n")}\n"
|
|
34
|
+
end
|
|
18
35
|
|
|
19
|
-
|
|
20
|
-
@graph_hash = if graph.is_a?(Hash)
|
|
21
|
-
graph
|
|
22
|
-
elsif graph.respond_to?(:to_json)
|
|
23
|
-
JSON.parse(graph.to_json, symbolize_names: true)
|
|
24
|
-
else
|
|
25
|
-
graph
|
|
26
|
-
end
|
|
36
|
+
private
|
|
27
37
|
|
|
28
|
-
|
|
38
|
+
def graph_hash(graph)
|
|
39
|
+
return graph if graph.is_a?(Hash)
|
|
40
|
+
return JSON.parse(graph.to_json, symbolize_names: true) if
|
|
41
|
+
graph.respond_to?(:to_json)
|
|
29
42
|
|
|
30
|
-
|
|
43
|
+
graph
|
|
31
44
|
end
|
|
32
45
|
|
|
33
|
-
|
|
46
|
+
def emit_root(graph)
|
|
47
|
+
id = value(graph, :id)
|
|
48
|
+
@output << "graph #{mapped_id(id)}" if id
|
|
49
|
+
with_scope(graph) do
|
|
50
|
+
serialize_shape_layout(graph)
|
|
51
|
+
serialize_graph(graph)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
34
54
|
|
|
35
55
|
def serialize_graph(graph)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
56
|
+
options = value(graph, :layoutOptions) || {}
|
|
57
|
+
serialize_layout_options(options)
|
|
58
|
+
@output << "" if options.any?
|
|
59
|
+
|
|
60
|
+
labels = value(graph, :labels) || []
|
|
61
|
+
ports = value(graph, :ports) || []
|
|
62
|
+
children = value(graph, :children) || []
|
|
63
|
+
edges = value(graph, :edges) || []
|
|
64
|
+
labels.each { |label| serialize_label(label) }
|
|
65
|
+
ports.each { |port| serialize_port(port) }
|
|
66
|
+
children.each { |node| serialize_node(node) }
|
|
67
|
+
@output << "" if children.any? && edges.any?
|
|
68
|
+
edges.each { |edge| serialize_edge(edge) }
|
|
69
|
+
end
|
|
39
70
|
|
|
40
|
-
|
|
41
|
-
|
|
71
|
+
def serialize_layout_options(options)
|
|
72
|
+
indent = indentation
|
|
73
|
+
options.each do |key, option_value|
|
|
74
|
+
@output << "#{indent}#{property_key(key)}: " \
|
|
75
|
+
"#{format_value(option_value)}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
42
78
|
|
|
43
|
-
|
|
44
|
-
(
|
|
45
|
-
|
|
79
|
+
def serialize_node(node)
|
|
80
|
+
id = mapped_id(value(node, :id))
|
|
81
|
+
if node_has_block?(node)
|
|
82
|
+
open_block("node #{id}") { serialize_node_block(node) }
|
|
83
|
+
else
|
|
84
|
+
@output << "#{indentation}node #{id}"
|
|
46
85
|
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def node_has_block?(node)
|
|
89
|
+
shape_layout_parts(node).any? ||
|
|
90
|
+
collection?(node, :labels) || collection?(node, :ports) ||
|
|
91
|
+
collection?(node, :children) || collection?(node, :edges) ||
|
|
92
|
+
hash_present?(node, :layoutOptions)
|
|
93
|
+
end
|
|
47
94
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
95
|
+
def serialize_node_block(node)
|
|
96
|
+
with_scope(node) do
|
|
97
|
+
serialize_shape_layout(node)
|
|
98
|
+
serialize_layout_options(value(node, :layoutOptions) || {})
|
|
99
|
+
(value(node, :labels) || []).each { |label| serialize_label(label) }
|
|
100
|
+
(value(node, :ports) || []).each { |port| serialize_port(port) }
|
|
101
|
+
(value(node, :children) || []).each { |child| serialize_node(child) }
|
|
102
|
+
(value(node, :edges) || []).each { |edge| serialize_edge(edge) }
|
|
51
103
|
end
|
|
104
|
+
end
|
|
52
105
|
|
|
53
|
-
|
|
54
|
-
(
|
|
55
|
-
|
|
106
|
+
def serialize_port(port)
|
|
107
|
+
id = mapped_id(value(port, :id))
|
|
108
|
+
if port_has_block?(port)
|
|
109
|
+
open_block("port #{id}") { serialize_port_block(port) }
|
|
110
|
+
else
|
|
111
|
+
@output << "#{indentation}port #{id}"
|
|
56
112
|
end
|
|
57
113
|
end
|
|
58
114
|
|
|
59
|
-
def
|
|
60
|
-
|
|
115
|
+
def port_has_block?(port)
|
|
116
|
+
shape_layout_parts(port).any? || collection?(port, :labels) ||
|
|
117
|
+
hash_present?(port, :layoutOptions)
|
|
118
|
+
end
|
|
61
119
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
120
|
+
def serialize_port_block(port)
|
|
121
|
+
serialize_shape_layout(port)
|
|
122
|
+
serialize_layout_options(value(port, :layoutOptions) || {})
|
|
123
|
+
(value(port, :labels) || []).each { |label| serialize_label(label) }
|
|
124
|
+
end
|
|
65
125
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
126
|
+
def serialize_edge(edge)
|
|
127
|
+
sources = serialized_endpoints(edge, :source)
|
|
128
|
+
targets = serialized_endpoints(edge, :target)
|
|
129
|
+
id = value(edge, :id)
|
|
130
|
+
prefix = id ? "#{mapped_id(id)}: " : ""
|
|
131
|
+
declaration = "edge #{prefix}#{sources.join(', ')} -> " \
|
|
132
|
+
"#{targets.join(', ')}"
|
|
133
|
+
|
|
134
|
+
if edge_has_block?(edge)
|
|
135
|
+
open_block(declaration) { serialize_edge_block(edge) }
|
|
136
|
+
else
|
|
137
|
+
@output << "#{indentation}#{declaration}"
|
|
74
138
|
end
|
|
75
139
|
end
|
|
76
140
|
|
|
77
|
-
def
|
|
78
|
-
|
|
141
|
+
def edge_has_block?(edge)
|
|
142
|
+
hash_present?(edge, :layoutOptions) || collection?(edge, :labels) ||
|
|
143
|
+
collection?(edge, :sections)
|
|
144
|
+
end
|
|
79
145
|
|
|
80
|
-
|
|
81
|
-
|
|
146
|
+
def serialize_edge_block(edge)
|
|
147
|
+
emit_sections(value(edge, :sections)) if collection?(edge, :sections)
|
|
148
|
+
serialize_layout_options(value(edge, :layoutOptions) || {})
|
|
149
|
+
(value(edge, :labels) || []).each { |label| serialize_label(label) }
|
|
150
|
+
end
|
|
82
151
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
152
|
+
def emit_sections(sections)
|
|
153
|
+
@output << "#{indentation}layout ["
|
|
154
|
+
@indent_level += 1
|
|
155
|
+
used_ids = sections.filter_map do |section|
|
|
156
|
+
id = value(section, :id)
|
|
157
|
+
mapped_id(id) if id
|
|
158
|
+
end.to_h { |id| [id, true] }
|
|
159
|
+
sections.each_with_index do |section, index|
|
|
160
|
+
emit_section(section, sections.length, index, used_ids)
|
|
161
|
+
end
|
|
162
|
+
@indent_level -= 1
|
|
163
|
+
@output << "#{indentation}]"
|
|
164
|
+
end
|
|
86
165
|
|
|
87
|
-
|
|
166
|
+
def emit_section(section, count, index, used_ids)
|
|
167
|
+
id = value(section, :id)
|
|
168
|
+
return emit_section_body(section) if count == 1 && !id
|
|
169
|
+
|
|
170
|
+
id ||= available_section_id(index + 1, used_ids)
|
|
171
|
+
used_ids[mapped_id(id)] = true
|
|
172
|
+
outgoing = Array(value(section, :outgoingSections))
|
|
173
|
+
declaration = "section #{mapped_id(id)}"
|
|
174
|
+
if outgoing.any?
|
|
175
|
+
refs = outgoing.map { |ref| mapped_id(ref) }.join(", ")
|
|
176
|
+
declaration = "#{declaration} -> #{refs}"
|
|
177
|
+
end
|
|
178
|
+
@output << "#{indentation}#{declaration} ["
|
|
179
|
+
@indent_level += 1
|
|
180
|
+
emit_section_body(section)
|
|
181
|
+
@indent_level -= 1
|
|
182
|
+
@output << "#{indentation}]"
|
|
183
|
+
end
|
|
88
184
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
185
|
+
def emit_section_body(section)
|
|
186
|
+
emit_shape_reference(section, :incomingShape, "incoming")
|
|
187
|
+
emit_shape_reference(section, :outgoingShape, "outgoing")
|
|
188
|
+
emit_section_point(section, :startPoint, "start")
|
|
189
|
+
emit_section_point(section, :endPoint, "end")
|
|
190
|
+
bends = Array(value(section, :bendPoints))
|
|
191
|
+
unless bends.empty?
|
|
192
|
+
points = bends.map { |point| formatted_point(point) }.join(" | ")
|
|
193
|
+
@output << "#{indentation}bends: #{points}"
|
|
93
194
|
end
|
|
195
|
+
serialize_layout_options(value(section, :layoutOptions) || {})
|
|
94
196
|
end
|
|
95
197
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
198
|
+
def available_section_id(index, used_ids)
|
|
199
|
+
loop do
|
|
200
|
+
candidate = "_section#{index}"
|
|
201
|
+
return candidate unless used_ids.key?(mapped_id(candidate))
|
|
202
|
+
|
|
203
|
+
index += 1
|
|
204
|
+
end
|
|
205
|
+
end
|
|
103
206
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
has_labels = (node[:labels] || []).any?
|
|
108
|
-
has_ports = (node[:ports] || []).any?
|
|
109
|
-
has_children = (node[:children] || []).any?
|
|
110
|
-
has_edges = (node[:edges] || []).any?
|
|
207
|
+
def emit_shape_reference(section, key, name)
|
|
208
|
+
reference = value(section, key)
|
|
209
|
+
return unless reference
|
|
111
210
|
|
|
112
|
-
|
|
113
|
-
has_children || has_edges
|
|
211
|
+
@output << "#{indentation}#{name}: #{endpoint(reference)}"
|
|
114
212
|
end
|
|
115
213
|
|
|
116
|
-
def
|
|
117
|
-
|
|
214
|
+
def emit_section_point(section, key, name)
|
|
215
|
+
point = value(section, key)
|
|
216
|
+
return unless point
|
|
217
|
+
|
|
218
|
+
@output << "#{indentation}#{name}: #{formatted_point(point)}"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def formatted_point(point)
|
|
222
|
+
"#{format_number(value(point, :x))}, #{format_number(value(point, :y))}"
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def serialize_label(label)
|
|
226
|
+
id = value(label, :id)
|
|
227
|
+
prefix = id ? "#{mapped_id(id)}: " : ""
|
|
228
|
+
declaration = "label #{prefix}\"#{escape_label(value(label, :text))}\""
|
|
229
|
+
|
|
230
|
+
if label_has_block?(label)
|
|
231
|
+
open_block(declaration) do
|
|
232
|
+
serialize_shape_layout(label)
|
|
233
|
+
serialize_layout_options(value(label, :layoutOptions) || {})
|
|
234
|
+
end
|
|
235
|
+
else
|
|
236
|
+
@output << "#{indentation}#{declaration}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def label_has_block?(label)
|
|
241
|
+
shape_layout_parts(label).any? || hash_present?(label, :layoutOptions)
|
|
242
|
+
end
|
|
118
243
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
244
|
+
def serialize_shape_layout(shape)
|
|
245
|
+
parts = shape_layout_parts(shape)
|
|
246
|
+
@output << "#{indentation}layout [ #{parts.join(' ')} ]" if parts.any?
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Position first, then size: this is the order ELK itself writes.
|
|
250
|
+
def shape_layout_parts(shape)
|
|
251
|
+
parts = []
|
|
252
|
+
x = value(shape, :x)
|
|
253
|
+
y = value(shape, :y)
|
|
254
|
+
width = value(shape, :width)
|
|
255
|
+
height = value(shape, :height)
|
|
256
|
+
parts << "position: #{format_number(x)}, #{format_number(y)}" if x && y
|
|
257
|
+
if width && height
|
|
258
|
+
parts << "size: #{format_number(width)}, #{format_number(height)}"
|
|
124
259
|
end
|
|
260
|
+
parts
|
|
261
|
+
end
|
|
125
262
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
263
|
+
def open_block(declaration)
|
|
264
|
+
@output << "#{indentation}#{declaration} {"
|
|
265
|
+
@indent_level += 1
|
|
266
|
+
yield
|
|
267
|
+
@indent_level -= 1
|
|
268
|
+
@output << "#{indentation}}"
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def endpoint(id)
|
|
272
|
+
@scope_stack.reverse_each do |scope|
|
|
273
|
+
return scope[id.to_s] if scope.key?(id.to_s)
|
|
130
274
|
end
|
|
275
|
+
mapped_id(id)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def serialized_endpoints(edge, direction)
|
|
279
|
+
port = value(edge, :"#{direction}Port")
|
|
280
|
+
node = Array(value(edge, :"#{direction}s")).first ||
|
|
281
|
+
value(edge, direction)
|
|
282
|
+
return ["#{mapped_id(node)}.#{mapped_id(port)}"] if port
|
|
283
|
+
|
|
284
|
+
Array(value(edge, :"#{direction}s")).map { |id| endpoint(id) }
|
|
285
|
+
end
|
|
131
286
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
287
|
+
def prepare_ids(graph)
|
|
288
|
+
ids = []
|
|
289
|
+
graph_id = value(graph, :id)&.to_s
|
|
290
|
+
ids << graph_id if graph_id
|
|
291
|
+
collect_ids(graph, ids)
|
|
292
|
+
collect_root_port_ids(graph, ids)
|
|
293
|
+
@id_map = {}
|
|
294
|
+
used = {}
|
|
295
|
+
|
|
296
|
+
valid, invalid = ids.uniq.partition { |id| valid_identifier?(id) }
|
|
297
|
+
valid.each do |id|
|
|
298
|
+
@id_map[id] = id
|
|
299
|
+
used[id] = true
|
|
135
300
|
end
|
|
136
301
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
302
|
+
invalid.each do |id|
|
|
303
|
+
base = sanitized_id(id)
|
|
304
|
+
candidate = base
|
|
305
|
+
counter = 2
|
|
306
|
+
while used[candidate]
|
|
307
|
+
candidate = "#{base}_#{counter}"
|
|
308
|
+
counter += 1
|
|
309
|
+
end
|
|
310
|
+
@id_map[id] = candidate
|
|
311
|
+
used[candidate] = true
|
|
140
312
|
end
|
|
313
|
+
end
|
|
141
314
|
|
|
142
|
-
|
|
143
|
-
(
|
|
144
|
-
|
|
315
|
+
def collect_ids(container, ids = [])
|
|
316
|
+
collect_member_ids(value(container, :labels), ids)
|
|
317
|
+
collect_edge_ids(value(container, :edges), ids)
|
|
318
|
+
Array(value(container, :children)).each do |node|
|
|
319
|
+
node_id = value(node, :id)&.to_s
|
|
320
|
+
ids << node_id if node_id
|
|
321
|
+
Array(value(node, :ports)).each do |port|
|
|
322
|
+
port_id = value(port, :id)&.to_s
|
|
323
|
+
next unless port_id
|
|
324
|
+
|
|
325
|
+
ids << port_id
|
|
326
|
+
collect_member_ids(value(port, :labels), ids)
|
|
327
|
+
end
|
|
328
|
+
collect_ids(node, ids)
|
|
145
329
|
end
|
|
330
|
+
ids
|
|
331
|
+
end
|
|
146
332
|
|
|
147
|
-
|
|
148
|
-
(
|
|
149
|
-
|
|
333
|
+
def collect_root_port_ids(graph, ids)
|
|
334
|
+
Array(value(graph, :ports)).each do |port|
|
|
335
|
+
port_id = value(port, :id)&.to_s
|
|
336
|
+
ids << port_id if port_id
|
|
337
|
+
collect_member_ids(value(port, :labels), ids)
|
|
150
338
|
end
|
|
151
339
|
end
|
|
152
340
|
|
|
153
|
-
def
|
|
154
|
-
|
|
341
|
+
def collect_edge_ids(edges, ids)
|
|
342
|
+
Array(edges).each do |edge|
|
|
343
|
+
edge_id = value(edge, :id)&.to_s
|
|
344
|
+
ids << edge_id if edge_id
|
|
345
|
+
collect_member_ids(value(edge, :labels), ids)
|
|
346
|
+
collect_member_ids(value(edge, :sections), ids)
|
|
347
|
+
end
|
|
348
|
+
end
|
|
155
349
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
350
|
+
def with_scope(container)
|
|
351
|
+
@scope_stack << endpoint_scope(container)
|
|
352
|
+
yield
|
|
353
|
+
ensure
|
|
354
|
+
@scope_stack.pop
|
|
355
|
+
end
|
|
159
356
|
|
|
160
|
-
|
|
357
|
+
def endpoint_scope(container)
|
|
358
|
+
scope = {}
|
|
359
|
+
children = Array(value(container, :children))
|
|
360
|
+
children.each do |child|
|
|
361
|
+
id = value(child, :id)
|
|
362
|
+
scope[id.to_s] ||= mapped_id(id) if id
|
|
363
|
+
end
|
|
364
|
+
Array(value(container, :ports)).each do |port|
|
|
365
|
+
id = value(port, :id)
|
|
366
|
+
scope[id.to_s] ||= mapped_id(id) if id
|
|
367
|
+
end
|
|
368
|
+
children.each do |child|
|
|
369
|
+
owner = value(child, :id)
|
|
370
|
+
Array(value(child, :ports)).each do |port|
|
|
371
|
+
id = value(port, :id)
|
|
372
|
+
next unless owner && id
|
|
373
|
+
|
|
374
|
+
scope[id.to_s] ||= "#{mapped_id(owner)}.#{mapped_id(id)}"
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
scope
|
|
378
|
+
end
|
|
161
379
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
380
|
+
def collect_member_ids(members, ids)
|
|
381
|
+
Array(members).each do |member|
|
|
382
|
+
id = value(member, :id)&.to_s
|
|
383
|
+
ids << id if id
|
|
166
384
|
end
|
|
167
385
|
end
|
|
168
386
|
|
|
169
|
-
def
|
|
170
|
-
(
|
|
171
|
-
(port[:labels] || []).any?
|
|
387
|
+
def mapped_id(id)
|
|
388
|
+
@id_map.fetch(id.to_s, sanitized_id(id.to_s))
|
|
172
389
|
end
|
|
173
390
|
|
|
174
|
-
def
|
|
175
|
-
|
|
391
|
+
def valid_identifier?(id)
|
|
392
|
+
IDENTIFIER.match?(id) && !KEYWORDS.include?(id)
|
|
393
|
+
end
|
|
176
394
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
395
|
+
def sanitized_id(id)
|
|
396
|
+
sanitized = id.gsub(/\W/, "_")
|
|
397
|
+
sanitized = "_#{sanitized}" unless sanitized.match?(/\A[A-Za-z_]/)
|
|
398
|
+
sanitized = "_#{sanitized}" if KEYWORDS.include?(sanitized)
|
|
399
|
+
sanitized.empty? ? "_id" : sanitized
|
|
400
|
+
end
|
|
401
|
+
|
|
402
|
+
def property_key(key)
|
|
403
|
+
segments = key.to_s.split(".", -1)
|
|
404
|
+
unless segments.all? { |segment| PROPERTY_SEGMENT.match?(segment) }
|
|
405
|
+
raise Elkrb::ValidationError,
|
|
406
|
+
"ELKT cannot represent option key #{key.inspect}"
|
|
181
407
|
end
|
|
182
408
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
409
|
+
segments.map do |segment|
|
|
410
|
+
KEYWORDS.include?(segment) ? "^#{segment}" : segment
|
|
411
|
+
end.join(".")
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def format_value(option_value)
|
|
415
|
+
case option_value
|
|
416
|
+
when Float then format_number(option_value)
|
|
417
|
+
when Integer, TrueClass, FalseClass then option_value.to_s
|
|
418
|
+
when NilClass then "null"
|
|
419
|
+
else format_string_value(option_value.to_s)
|
|
186
420
|
end
|
|
187
421
|
end
|
|
188
422
|
|
|
189
|
-
def
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
# Include edge ID if it's not auto-generated
|
|
209
|
-
@output << if edge[:id] && !edge[:id].to_s.match?(/^e\d+$/)
|
|
210
|
-
"#{indent}edge #{edge[:id]}: #{source_ref} -> #{target_ref}"
|
|
211
|
-
else
|
|
212
|
-
"#{indent}edge #{source_ref} -> #{target_ref}"
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def format_value(value)
|
|
217
|
-
case value
|
|
218
|
-
when Float
|
|
219
|
-
format_number(value)
|
|
220
|
-
when Integer
|
|
221
|
-
value
|
|
222
|
-
when TrueClass, FalseClass
|
|
223
|
-
value
|
|
224
|
-
else
|
|
225
|
-
value.to_s
|
|
423
|
+
def format_string_value(text)
|
|
424
|
+
return text if valid_value_identifier?(text)
|
|
425
|
+
|
|
426
|
+
"\"#{escape_label(text)}\""
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def valid_value_identifier?(text)
|
|
430
|
+
text.split(".").all? { |part| valid_identifier?(part) }
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def escape_label(text)
|
|
434
|
+
text.to_s.gsub(/[\\"\n\r\t\x08\f]/) { |char| LABEL_ESCAPES.fetch(char) }
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
def format_number(number)
|
|
438
|
+
unless number.respond_to?(:finite?) && number.finite?
|
|
439
|
+
raise Elkrb::ValidationError,
|
|
440
|
+
"ELKT numbers must be finite, got #{number.inspect}"
|
|
226
441
|
end
|
|
442
|
+
|
|
443
|
+
return number.to_i.to_s if number.is_a?(Float) && number == number.to_i
|
|
444
|
+
|
|
445
|
+
number.to_s
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def indentation
|
|
449
|
+
" " * (@indent_level * @indent_size)
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def collection?(object, key)
|
|
453
|
+
Array(value(object, key)).any?
|
|
227
454
|
end
|
|
228
455
|
|
|
229
|
-
def
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
456
|
+
def hash_present?(object, key)
|
|
457
|
+
(value(object, key) || {}).any?
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def value(object, key)
|
|
461
|
+
return object[key] if object.key?(key)
|
|
462
|
+
|
|
463
|
+
object[key.to_s]
|
|
233
464
|
end
|
|
234
465
|
end
|
|
235
466
|
end
|
data/lib/elkrb/version.rb
CHANGED