elkrb 1.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.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +11 -0
  4. data/Gemfile +13 -0
  5. data/README.adoc +1028 -0
  6. data/Rakefile +64 -0
  7. data/benchmarks/README.md +172 -0
  8. data/benchmarks/elkjs_benchmark.js +140 -0
  9. data/benchmarks/elkrb_benchmark.rb +145 -0
  10. data/benchmarks/fixtures/graphs.json +10777 -0
  11. data/benchmarks/generate_report.rb +241 -0
  12. data/benchmarks/generate_test_graphs.rb +154 -0
  13. data/benchmarks/results/elkrb_results.json +280 -0
  14. data/benchmarks/results/elkrb_summary.json +285 -0
  15. data/elkrb.gemspec +39 -0
  16. data/examples/dot_export_demo.rb +133 -0
  17. data/examples/hierarchical_graph.rb +19 -0
  18. data/examples/layout_constraints_demo.rb +272 -0
  19. data/examples/port_constraints_demo.rb +291 -0
  20. data/examples/self_loop_demo.rb +391 -0
  21. data/examples/simple_graph.rb +50 -0
  22. data/examples/spline_routing_demo.rb +235 -0
  23. data/exe/elkrb +8 -0
  24. data/lib/elkrb/cli.rb +224 -0
  25. data/lib/elkrb/commands/batch_command.rb +66 -0
  26. data/lib/elkrb/commands/convert_command.rb +130 -0
  27. data/lib/elkrb/commands/diagram_command.rb +208 -0
  28. data/lib/elkrb/commands/render_command.rb +52 -0
  29. data/lib/elkrb/commands/validate_command.rb +241 -0
  30. data/lib/elkrb/errors.rb +30 -0
  31. data/lib/elkrb/geometry/bezier.rb +163 -0
  32. data/lib/elkrb/geometry/dimension.rb +32 -0
  33. data/lib/elkrb/geometry/point.rb +68 -0
  34. data/lib/elkrb/geometry/rectangle.rb +86 -0
  35. data/lib/elkrb/geometry/vector.rb +67 -0
  36. data/lib/elkrb/graph/edge.rb +95 -0
  37. data/lib/elkrb/graph/graph.rb +90 -0
  38. data/lib/elkrb/graph/label.rb +45 -0
  39. data/lib/elkrb/graph/layout_options.rb +247 -0
  40. data/lib/elkrb/graph/node.rb +79 -0
  41. data/lib/elkrb/graph/node_constraints.rb +107 -0
  42. data/lib/elkrb/graph/port.rb +104 -0
  43. data/lib/elkrb/graphviz_wrapper.rb +133 -0
  44. data/lib/elkrb/layout/algorithm_registry.rb +57 -0
  45. data/lib/elkrb/layout/algorithms/base_algorithm.rb +208 -0
  46. data/lib/elkrb/layout/algorithms/box.rb +47 -0
  47. data/lib/elkrb/layout/algorithms/disco.rb +206 -0
  48. data/lib/elkrb/layout/algorithms/fixed.rb +32 -0
  49. data/lib/elkrb/layout/algorithms/force.rb +165 -0
  50. data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +86 -0
  51. data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +96 -0
  52. data/lib/elkrb/layout/algorithms/layered/node_placer.rb +77 -0
  53. data/lib/elkrb/layout/algorithms/layered.rb +49 -0
  54. data/lib/elkrb/layout/algorithms/libavoid.rb +389 -0
  55. data/lib/elkrb/layout/algorithms/mrtree.rb +144 -0
  56. data/lib/elkrb/layout/algorithms/radial.rb +64 -0
  57. data/lib/elkrb/layout/algorithms/random.rb +43 -0
  58. data/lib/elkrb/layout/algorithms/rectpacking.rb +93 -0
  59. data/lib/elkrb/layout/algorithms/spore_compaction.rb +139 -0
  60. data/lib/elkrb/layout/algorithms/spore_overlap.rb +117 -0
  61. data/lib/elkrb/layout/algorithms/stress.rb +176 -0
  62. data/lib/elkrb/layout/algorithms/topdown_packing.rb +183 -0
  63. data/lib/elkrb/layout/algorithms/vertiflex.rb +174 -0
  64. data/lib/elkrb/layout/constraints/alignment_constraint.rb +150 -0
  65. data/lib/elkrb/layout/constraints/base_constraint.rb +72 -0
  66. data/lib/elkrb/layout/constraints/constraint_processor.rb +134 -0
  67. data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +87 -0
  68. data/lib/elkrb/layout/constraints/layer_constraint.rb +71 -0
  69. data/lib/elkrb/layout/constraints/relative_position_constraint.rb +110 -0
  70. data/lib/elkrb/layout/edge_router.rb +935 -0
  71. data/lib/elkrb/layout/hierarchical_processor.rb +299 -0
  72. data/lib/elkrb/layout/label_placer.rb +338 -0
  73. data/lib/elkrb/layout/layout_engine.rb +170 -0
  74. data/lib/elkrb/layout/port_constraint_processor.rb +173 -0
  75. data/lib/elkrb/options/elk_padding.rb +94 -0
  76. data/lib/elkrb/options/k_vector.rb +100 -0
  77. data/lib/elkrb/options/k_vector_chain.rb +135 -0
  78. data/lib/elkrb/parsers/elkt_parser.rb +248 -0
  79. data/lib/elkrb/serializers/dot_serializer.rb +339 -0
  80. data/lib/elkrb/serializers/elkt_serializer.rb +236 -0
  81. data/lib/elkrb/version.rb +5 -0
  82. data/lib/elkrb.rb +509 -0
  83. data/sig/elkrb/constraints.rbs +114 -0
  84. data/sig/elkrb/geometry.rbs +61 -0
  85. data/sig/elkrb/graph.rbs +112 -0
  86. data/sig/elkrb/layout.rbs +107 -0
  87. data/sig/elkrb/options.rbs +81 -0
  88. data/sig/elkrb.rbs +32 -0
  89. metadata +179 -0
@@ -0,0 +1,236 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Elkrb
6
+ module Serializers
7
+ # Serializer for ELKT (ELK Text) format
8
+ # Converts ELK graph structures to textual ELKT representation
9
+ class ElktSerializer
10
+ def initialize(options = {})
11
+ @indent_size = options[:indent_size] || 2
12
+ @include_comments = options.fetch(:include_comments, true)
13
+ end
14
+
15
+ def serialize(graph, _options = {})
16
+ @indent_level = 0
17
+ @output = []
18
+
19
+ # Convert to hash using JSON round-trip for Lutaml models
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
27
+
28
+ serialize_graph(@graph_hash)
29
+
30
+ "#{@output.join("\n")}\n"
31
+ end
32
+
33
+ private
34
+
35
+ def serialize_graph(graph)
36
+ # Serialize graph-level layout options
37
+ layout_opts = graph[:layoutOptions] || graph["layoutOptions"] || {}
38
+ serialize_layout_options(layout_opts)
39
+
40
+ # Add blank line after options if present
41
+ @output << "" if (graph[:layoutOptions] || {}).any?
42
+
43
+ # Serialize nodes
44
+ (graph[:children] || []).each do |node|
45
+ serialize_node(node)
46
+ end
47
+
48
+ # Add blank line before edges if both nodes and edges exist
49
+ if (graph[:children] || []).any? && (graph[:edges] || []).any?
50
+ @output << ""
51
+ end
52
+
53
+ # Serialize edges
54
+ (graph[:edges] || []).each do |edge|
55
+ serialize_edge(edge)
56
+ end
57
+ end
58
+
59
+ def serialize_layout_options(options)
60
+ return if options.empty?
61
+
62
+ options.each do |key, value|
63
+ # Remove elk. prefix for cleaner output
64
+ display_key = key.to_s.start_with?("elk.") ? key.to_s[4..] : key.to_s
65
+
66
+ # Special handling for algorithm and direction
67
+ @output << if display_key == "algorithm"
68
+ "algorithm: #{value}"
69
+ elsif display_key == "direction"
70
+ "direction: #{value}"
71
+ else
72
+ "#{display_key}: #{format_value(value)}"
73
+ end
74
+ end
75
+ end
76
+
77
+ def serialize_node(node)
78
+ indent = " " * (@indent_level * @indent_size)
79
+
80
+ # Check if node has attributes to serialize in a block
81
+ has_block = node_has_block?(node)
82
+
83
+ if has_block
84
+ @output << "#{indent}node #{node[:id]} {"
85
+ @indent_level += 1
86
+
87
+ serialize_node_block(node)
88
+
89
+ @indent_level -= 1
90
+ @output << "#{indent}}"
91
+ else
92
+ @output << "#{indent}node #{node[:id]}"
93
+ end
94
+ end
95
+
96
+ def node_has_block?(node)
97
+ # Node needs a block if it has:
98
+ # - Layout attributes (size, position)
99
+ # - Labels
100
+ # - Ports
101
+ # - Children (nested nodes)
102
+ # - Non-default dimensions
103
+
104
+ has_layout = node[:width] && node[:height] &&
105
+ (node[:width] != 40 || node[:height] != 40)
106
+ has_position = node[:x] || node[:y]
107
+ has_labels = (node[:labels] || []).any?
108
+ has_ports = (node[:ports] || []).any?
109
+ has_children = (node[:children] || []).any?
110
+ has_edges = (node[:edges] || []).any?
111
+
112
+ has_layout || has_position || has_labels || has_ports ||
113
+ has_children || has_edges
114
+ end
115
+
116
+ def serialize_node_block(node)
117
+ indent = " " * (@indent_level * @indent_size)
118
+
119
+ # Serialize layout attributes
120
+ if node[:width] && node[:height]
121
+ width = format_number(node[:width])
122
+ height = format_number(node[:height])
123
+ @output << "#{indent}layout [ size: #{width}, #{height} ]"
124
+ end
125
+
126
+ if node[:x] && node[:y]
127
+ x = format_number(node[:x])
128
+ y = format_number(node[:y])
129
+ @output << "#{indent}layout [ position: #{x}, #{y} ]"
130
+ end
131
+
132
+ # Serialize labels
133
+ (node[:labels] || []).each do |label|
134
+ @output << "#{indent}label \"#{label[:text]}\""
135
+ end
136
+
137
+ # Serialize ports
138
+ (node[:ports] || []).each do |port|
139
+ serialize_port(port)
140
+ end
141
+
142
+ # Serialize nested nodes
143
+ (node[:children] || []).each do |child|
144
+ serialize_node(child)
145
+ end
146
+
147
+ # Serialize nested edges
148
+ (node[:edges] || []).each do |edge|
149
+ serialize_edge(edge)
150
+ end
151
+ end
152
+
153
+ def serialize_port(port)
154
+ indent = " " * (@indent_level * @indent_size)
155
+
156
+ if port_has_block?(port)
157
+ @output << "#{indent}port #{port[:id]} {"
158
+ @indent_level += 1
159
+
160
+ serialize_port_block(port)
161
+
162
+ @indent_level -= 1
163
+ @output << "#{indent}}"
164
+ else
165
+ @output << "#{indent}port #{port[:id]}"
166
+ end
167
+ end
168
+
169
+ def port_has_block?(port)
170
+ (port[:layoutOptions] || {}).any? ||
171
+ (port[:labels] || []).any?
172
+ end
173
+
174
+ def serialize_port_block(port)
175
+ indent = " " * (@indent_level * @indent_size)
176
+
177
+ # Serialize port layout options
178
+ (port[:layoutOptions] || {}).each do |key, value|
179
+ display_key = key.to_s.start_with?("elk.") ? key.to_s[4..] : key.to_s
180
+ @output << "#{indent}#{display_key}: #{format_value(value)}"
181
+ end
182
+
183
+ # Serialize port labels
184
+ (port[:labels] || []).each do |label|
185
+ @output << "#{indent}label \"#{label[:text]}\""
186
+ end
187
+ end
188
+
189
+ def serialize_edge(edge)
190
+ indent = " " * (@indent_level * @indent_size)
191
+
192
+ source = edge[:sources]&.first || edge[:source]
193
+ target = edge[:targets]&.first || edge[:target]
194
+
195
+ # Add port references if present
196
+ source_ref = if edge[:sourcePort]
197
+ "#{source}.#{edge[:sourcePort]}"
198
+ else
199
+ source
200
+ end
201
+
202
+ target_ref = if edge[:targetPort]
203
+ "#{target}.#{edge[:targetPort]}"
204
+ else
205
+ target
206
+ end
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
226
+ end
227
+ end
228
+
229
+ def format_number(num)
230
+ # Remove trailing zeros and decimal point if integer
231
+ formatted = format("%.2f", num).sub(/\.?0+$/, "")
232
+ formatted.empty? ? "0" : formatted
233
+ end
234
+ end
235
+ end
236
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elkrb
4
+ VERSION = "1.0.0"
5
+ end