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.
Files changed (144) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.adoc +364 -0
  3. data/LICENSE +25 -0
  4. data/README.adoc +695 -557
  5. data/exe/elkrb +36 -2
  6. data/lib/elkrb/atomic_destination.rb +53 -0
  7. data/lib/elkrb/best_effort_write.rb +49 -0
  8. data/lib/elkrb/cli/layout_flag_options.rb +54 -0
  9. data/lib/elkrb/cli.rb +155 -82
  10. data/lib/elkrb/command_resolver.rb +186 -0
  11. data/lib/elkrb/commands/batch_command.rb +28 -5
  12. data/lib/elkrb/commands/convert_command.rb +22 -52
  13. data/lib/elkrb/commands/diagram_command.rb +234 -102
  14. data/lib/elkrb/commands/options_command.rb +87 -0
  15. data/lib/elkrb/commands/render_command.rb +6 -1
  16. data/lib/elkrb/commands/validate_command.rb +146 -91
  17. data/lib/elkrb/errors.rb +20 -1
  18. data/lib/elkrb/format_sniffer.rb +362 -0
  19. data/lib/elkrb/geometry/bezier.rb +14 -4
  20. data/lib/elkrb/geometry/point.rb +5 -6
  21. data/lib/elkrb/geometry/rectangle.rb +2 -2
  22. data/lib/elkrb/graph/deep_stringify_keys.rb +30 -0
  23. data/lib/elkrb/graph/edge.rb +89 -3
  24. data/lib/elkrb/graph/graph.rb +43 -15
  25. data/lib/elkrb/graph/label.rb +16 -2
  26. data/lib/elkrb/graph/node.rb +13 -2
  27. data/lib/elkrb/graph/node_constraints.rb +139 -8
  28. data/lib/elkrb/graph/normalize_option_keys.rb +39 -0
  29. data/lib/elkrb/graph/port.rb +29 -21
  30. data/lib/elkrb/graph/read_only_mapping.rb +21 -0
  31. data/lib/elkrb/graphviz_wrapper.rb +152 -44
  32. data/lib/elkrb/layout/algorithm_registry.rb +65 -8
  33. data/lib/elkrb/layout/algorithms/base_algorithm.rb +74 -74
  34. data/lib/elkrb/layout/algorithms/box.rb +47 -20
  35. data/lib/elkrb/layout/algorithms/disco.rb +150 -61
  36. data/lib/elkrb/layout/algorithms/fixed.rb +70 -10
  37. data/lib/elkrb/layout/algorithms/force.rb +145 -86
  38. data/lib/elkrb/layout/algorithms/layered/bk/aligner.rb +104 -0
  39. data/lib/elkrb/layout/algorithms/layered/bk/compactor.rb +87 -0
  40. data/lib/elkrb/layout/algorithms/layered/bk/view.rb +103 -0
  41. data/lib/elkrb/layout/algorithms/layered/bk_node_placer.rb +170 -0
  42. data/lib/elkrb/layout/algorithms/layered/components.rb +170 -0
  43. data/lib/elkrb/layout/algorithms/layered/crossing_minimizer.rb +306 -0
  44. data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +118 -49
  45. data/lib/elkrb/layout/algorithms/layered/discovery_order.rb +75 -0
  46. data/lib/elkrb/layout/algorithms/layered/dummy_slot.rb +31 -0
  47. data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +188 -48
  48. data/lib/elkrb/layout/algorithms/layered/layer_constraints.rb +82 -0
  49. data/lib/elkrb/layout/algorithms/layered/layer_sweep.rb +195 -0
  50. data/lib/elkrb/layout/algorithms/layered/node_placer.rb +214 -36
  51. data/lib/elkrb/layout/algorithms/layered/orthogonal/gap_solver.rb +219 -0
  52. data/lib/elkrb/layout/algorithms/layered/orthogonal/hyper_edge_segment.rb +152 -0
  53. data/lib/elkrb/layout/algorithms/layered/orthogonal/junction_points.rb +58 -0
  54. data/lib/elkrb/layout/algorithms/layered/orthogonal/segment_order.rb +134 -0
  55. data/lib/elkrb/layout/algorithms/layered/orthogonal_router.rb +253 -0
  56. data/lib/elkrb/layout/algorithms/layered/port_order.rb +188 -0
  57. data/lib/elkrb/layout/algorithms/layered/port_spread.rb +42 -0
  58. data/lib/elkrb/layout/algorithms/layered.rb +548 -13
  59. data/lib/elkrb/layout/algorithms/libavoid.rb +951 -187
  60. data/lib/elkrb/layout/algorithms/mrtree.rb +585 -65
  61. data/lib/elkrb/layout/algorithms/radial.rb +188 -29
  62. data/lib/elkrb/layout/algorithms/random.rb +131 -17
  63. data/lib/elkrb/layout/algorithms/rectpacking.rb +28 -40
  64. data/lib/elkrb/layout/algorithms/spore_compaction.rb +57 -100
  65. data/lib/elkrb/layout/algorithms/spore_overlap.rb +78 -84
  66. data/lib/elkrb/layout/algorithms/stress.rb +391 -84
  67. data/lib/elkrb/layout/algorithms/topdown_packing.rb +95 -31
  68. data/lib/elkrb/layout/algorithms/vertiflex.rb +12 -47
  69. data/lib/elkrb/layout/constraints/alignment_constraint.rb +49 -12
  70. data/lib/elkrb/layout/constraints/base_constraint.rb +3 -15
  71. data/lib/elkrb/layout/constraints/constraint_processor.rb +9 -23
  72. data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +22 -13
  73. data/lib/elkrb/layout/constraints/layer_constraint.rb +10 -33
  74. data/lib/elkrb/layout/constraints/relative_position_constraint.rb +94 -50
  75. data/lib/elkrb/layout/edge_router.rb +533 -337
  76. data/lib/elkrb/layout/hierarchical_processor.rb +19 -276
  77. data/lib/elkrb/layout/java_random.rb +59 -0
  78. data/lib/elkrb/layout/label_placer.rb +165 -157
  79. data/lib/elkrb/layout/layout_engine.rb +46 -23
  80. data/lib/elkrb/layout/node_index.rb +149 -0
  81. data/lib/elkrb/layout/polyomino/component_compactor.rb +138 -0
  82. data/lib/elkrb/layout/polyomino/footprint.rb +94 -0
  83. data/lib/elkrb/layout/polyomino/grid.rb +106 -0
  84. data/lib/elkrb/layout/polyomino/packer.rb +95 -0
  85. data/lib/elkrb/layout/polyomino/piece.rb +52 -0
  86. data/lib/elkrb/layout/port_constraint_processor.rb +131 -44
  87. data/lib/elkrb/layout/spore/body.rb +65 -0
  88. data/lib/elkrb/layout/spore/depth_first_compaction.rb +127 -0
  89. data/lib/elkrb/layout/spore/edge_cost.rb +69 -0
  90. data/lib/elkrb/layout/spore/gap.rb +76 -0
  91. data/lib/elkrb/layout/spore/geometry.rb +104 -0
  92. data/lib/elkrb/layout/spore/overlap_sweep.rb +90 -0
  93. data/lib/elkrb/layout/spore/placement.rb +57 -0
  94. data/lib/elkrb/layout/spore/rays.rb +119 -0
  95. data/lib/elkrb/layout/spore/spanning_tree.rb +53 -0
  96. data/lib/elkrb/layout/spore/triangulation.rb +159 -0
  97. data/lib/elkrb/layout_flags.rb +69 -0
  98. data/lib/elkrb/options/decimal.rb +50 -0
  99. data/lib/elkrb/options/element_options.rb +43 -0
  100. data/lib/elkrb/options/elk_padding.rb +20 -14
  101. data/lib/elkrb/options/k_vector.rb +6 -7
  102. data/lib/elkrb/options/k_vector_chain.rb +12 -20
  103. data/lib/elkrb/options/option_map.rb +85 -0
  104. data/lib/elkrb/options/registry.rb +335 -0
  105. data/lib/elkrb/options/resolver.rb +151 -0
  106. data/lib/elkrb/options/spellings.rb +62 -0
  107. data/lib/elkrb/options/unhonoured_report.rb +194 -0
  108. data/lib/elkrb/parsers/elkt/lexer.rb +309 -0
  109. data/lib/elkrb/parsers/elkt/parser.rb +424 -0
  110. data/lib/elkrb/parsers/elkt/resolver.rb +142 -0
  111. data/lib/elkrb/parsers/elkt/token.rb +40 -0
  112. data/lib/elkrb/parsers/elkt_parser.rb +32 -230
  113. data/lib/elkrb/serializers/dot_serializer.rb +342 -54
  114. data/lib/elkrb/serializers/elkt_serializer.rb +386 -155
  115. data/lib/elkrb/version.rb +1 -1
  116. data/lib/elkrb.rb +60 -61
  117. metadata +71 -49
  118. data/.rspec +0 -3
  119. data/.rubocop.yml +0 -11
  120. data/Gemfile +0 -13
  121. data/Rakefile +0 -64
  122. data/benchmarks/README.md +0 -172
  123. data/benchmarks/elkjs_benchmark.js +0 -140
  124. data/benchmarks/elkrb_benchmark.rb +0 -145
  125. data/benchmarks/fixtures/graphs.json +0 -10777
  126. data/benchmarks/generate_report.rb +0 -241
  127. data/benchmarks/generate_test_graphs.rb +0 -154
  128. data/benchmarks/results/elkrb_results.json +0 -280
  129. data/benchmarks/results/elkrb_summary.json +0 -285
  130. data/elkrb.gemspec +0 -39
  131. data/examples/dot_export_demo.rb +0 -133
  132. data/examples/hierarchical_graph.rb +0 -19
  133. data/examples/layout_constraints_demo.rb +0 -272
  134. data/examples/port_constraints_demo.rb +0 -291
  135. data/examples/self_loop_demo.rb +0 -391
  136. data/examples/simple_graph.rb +0 -50
  137. data/examples/spline_routing_demo.rb +0 -235
  138. data/lib/elkrb/graph/layout_options.rb +0 -247
  139. data/sig/elkrb/constraints.rbs +0 -114
  140. data/sig/elkrb/geometry.rbs +0 -61
  141. data/sig/elkrb/graph.rbs +0 -112
  142. data/sig/elkrb/layout.rbs +0 -107
  143. data/sig/elkrb/options.rbs +0 -81
  144. data/sig/elkrb.rbs +0 -32
@@ -1,247 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../errors"
4
+ require_relative "elkt/token"
5
+ require_relative "elkt/lexer"
6
+ require_relative "elkt/resolver"
7
+ require_relative "elkt/parser"
8
+
3
9
  module Elkrb
4
10
  module Parsers
5
- # Parser for ELKT (ELK Text) format
6
- # Parses textual graph definitions into ELK graph structures
11
+ # Parser for the ELKT (ELK Text) format.
12
+ #
13
+ # Tokenizes, then parses by recursive descent, then resolves endpoints and
14
+ # edge ids. Raises Elkrb::ParseError, carrying a line and column, for input
15
+ # that is not valid ELKT.
16
+ #
17
+ # The returned Hash is the full ELK graph. `Graph.from_hash` reads the
18
+ # parts the models have today: ids, sizes, positions, layout options,
19
+ # children, labels, ports and edges. Labels nested inside a label and edge
20
+ # section options are parsed but the models drop them.
21
+ #
22
+ # @example
23
+ # Elkrb::Parsers::ElktParser.parse("node n1\nnode n2\nedge n1 -> n2\n")
24
+ # # => {id: "root", layoutOptions: {}, children: [...], edges: [...]}
25
+ #
26
+ # @param input [String] ELKT source
27
+ # @return [Hash] an ELK graph Hash
28
+ # @raise [Elkrb::ParseError] if the input is not valid ELKT
7
29
  class ElktParser
8
- class ParseError < StandardError; end
30
+ # Kept so old code that rescues ElktParser::ParseError keeps working.
31
+ ParseError = Elkrb::ParseError
9
32
 
10
33
  def self.parse(input)
11
- new(input).parse
34
+ Elkt::Parser.new(Elkt::Lexer.new(input).tokenize).parse
12
35
  end
13
36
 
37
+ # The instance API of the old parser. It is kept so
38
+ # `ElktParser.new(source).parse` still works.
14
39
  def initialize(input)
15
40
  @input = input
16
- @line_number = 0
17
- @graph = {
18
- id: "root",
19
- layoutOptions: {},
20
- children: [],
21
- edges: [],
22
- }
23
- @node_map = {}
24
- @current_context = [@graph]
25
- end
26
-
27
- def parse
28
- lines = preprocess(@input)
29
- parse_lines(lines)
30
- @graph
31
- end
32
-
33
- private
34
-
35
- def preprocess(input)
36
- # Remove block comments
37
- input = input.gsub(%r{/\*.*?\*/}m, "")
38
-
39
- # Split into lines and remove line comments
40
- lines = input.split("\n").map do |line|
41
- line.sub(%r{//.*$}, "").strip
42
- end
43
-
44
- # Remove empty lines
45
- lines.reject(&:empty?)
46
- end
47
-
48
- def parse_lines(lines)
49
- lines.each_with_index do |line, idx|
50
- @line_number = idx + 1
51
- parse_line(line)
52
- end
53
- end
54
-
55
- def parse_line(line)
56
- case line
57
- when /^algorithm:\s*(.+)$/
58
- parse_algorithm($1.strip)
59
- when /^direction:\s*(.+)$/
60
- parse_direction($1.strip)
61
- when /^([\w.]+):\s*(.+)$/
62
- parse_property($1.strip, $2.strip)
63
- when /^node\s+(\w+)\s*\{/
64
- parse_node_with_block($1)
65
- when /^node\s+(\w+)\s*$/
66
- parse_simple_node($1)
67
- when /^\}/
68
- close_block
69
- when /^edge\s+(.+)$/
70
- parse_edge($1.strip)
71
- when /^layout\s*\[\s*(.+?)\s*\]/
72
- parse_layout_block($1)
73
- when /^port\s+(\w+)/
74
- parse_port(line)
75
- when /^label\s+"([^"]+)"/
76
- parse_label($1)
77
- else
78
- # Ignore unknown lines or handle as needed
79
- end
80
41
  end
81
42
 
82
- def parse_algorithm(value)
83
- current_node[:layoutOptions] ||= {}
84
- current_node[:layoutOptions]["elk.algorithm"] = value
85
- end
86
-
87
- def parse_direction(value)
88
- current_node[:layoutOptions] ||= {}
89
- current_node[:layoutOptions]["elk.direction"] = value
90
- end
91
-
92
- def parse_property(key, value)
93
- current_node[:layoutOptions] ||= {}
94
-
95
- # Convert property name to ELK format
96
- elk_key = key.start_with?("elk.") ? key : "elk.#{key}"
97
-
98
- # Parse value
99
- parsed_value = parse_value(value)
100
- current_node[:layoutOptions][elk_key] = parsed_value
101
- end
102
-
103
- def parse_value(value)
104
- case value
105
- when /^-?\d+\.\d+$/
106
- value.to_f
107
- when /^-?\d+$/
108
- value.to_i
109
- when /^true$/i
110
- true
111
- when /^false$/i
112
- false
113
- else
114
- value
115
- end
116
- end
117
-
118
- def parse_simple_node(node_id)
119
- node = create_node(node_id)
120
- current_node[:children] ||= []
121
- current_node[:children] << node
122
- @node_map[node_id] = node
123
- end
124
-
125
- def parse_node_with_block(node_id)
126
- node = create_node(node_id)
127
- current_node[:children] ||= []
128
- current_node[:children] << node
129
- @node_map[node_id] = node
130
- @current_context.push(node)
131
- end
43
+ attr_reader :input
132
44
 
133
- def create_node(node_id)
134
- {
135
- id: node_id,
136
- width: 40,
137
- height: 40,
138
- layoutOptions: {},
139
- }
140
- end
141
-
142
- def parse_layout_block(content)
143
- # Parse layout [ size: 30, 30 ] or similar
144
- if content =~ /size:\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)/
145
- width = $1.to_f
146
- height = $2.to_f
147
- current_node[:width] = width
148
- current_node[:height] = height
149
- elsif content =~ /position:\s*(\d+(?:\.\d+)?)\s*,\s*(\d+(?:\.\d+)?)/
150
- x = $1.to_f
151
- y = $2.to_f
152
- current_node[:x] = x
153
- current_node[:y] = y
154
- end
155
- end
156
-
157
- def parse_edge(edge_spec)
158
- # Parse: node1 -> node2
159
- # Or: edge_id: node1 -> node2
160
- # Or: node1.port1 -> node2.port2
161
-
162
- if edge_spec =~ /^(\w+):\s*(.+)$/
163
- edge_id = $1
164
- edge_spec = $2
165
- else
166
- edge_id = generate_edge_id
167
- end
168
-
169
- unless edge_spec =~ /^(.+?)\s*->\s*(.+)$/
170
- raise ParseError,
171
- "Invalid edge syntax at line #{@line_number}: #{edge_spec}"
172
- end
173
-
174
- source_spec = $1.strip
175
- target_spec = $2.strip
176
-
177
- edge = create_edge(edge_id, source_spec, target_spec)
178
- current_node[:edges] ||= []
179
- current_node[:edges] << edge
180
- end
181
-
182
- def create_edge(edge_id, source_spec, target_spec)
183
- source_parts = source_spec.split(".")
184
- target_parts = target_spec.split(".")
185
-
186
- edge = {
187
- id: edge_id,
188
- sources: [source_parts[0]],
189
- targets: [target_parts[0]],
190
- }
191
-
192
- # Add port references if present
193
- if source_parts.length > 1
194
- edge[:sourcePort] = source_parts[1]
195
- end
196
-
197
- if target_parts.length > 1
198
- edge[:targetPort] = target_parts[1]
199
- end
200
-
201
- edge
202
- end
203
-
204
- def parse_port(line)
205
- # Parse: port port_id { ... }
206
- if line =~ /^port\s+(\w+)\s*\{/
207
- port_id = $1
208
- port = {
209
- id: port_id,
210
- layoutOptions: {},
211
- }
212
- current_node[:ports] ||= []
213
- current_node[:ports] << port
214
- @current_context.push(port)
215
- elsif line =~ /^port\s+(\w+)\s*$/
216
- port_id = $1
217
- port = {
218
- id: port_id,
219
- }
220
- current_node[:ports] ||= []
221
- current_node[:ports] << port
222
- end
223
- end
224
-
225
- def parse_label(text)
226
- label = {
227
- text: text,
228
- width: text.length * 7.0,
229
- height: 14.0,
230
- }
231
- current_node[:labels] ||= []
232
- current_node[:labels] << label
233
- end
234
-
235
- def close_block
236
- @current_context.pop if @current_context.length > 1
237
- end
238
-
239
- def current_node
240
- @current_context.last
241
- end
242
-
243
- def generate_edge_id
244
- "e#{current_node[:edges]&.length || 0}"
45
+ def parse
46
+ self.class.parse(@input)
245
47
  end
246
48
  end
247
49
  end