elkrb 1.0.2 → 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
@@ -0,0 +1,424 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../errors"
4
+ require_relative "lexer"
5
+ require_relative "resolver"
6
+
7
+ module Elkrb
8
+ module Parsers
9
+ module Elkt
10
+ # Recursive-descent parser over the Lexer's token array.
11
+ #
12
+ # Nesting is token-driven, never line-driven: a block is entered on a
13
+ # `{` wherever it appears. Each container admits only what its own Xtext
14
+ # production admits -- a label or port body holds labels, not nodes.
15
+ #
16
+ # @api private
17
+ class Parser
18
+ # Every keyword in ElkGraph.xtext. An unescaped occurrence of one is
19
+ # not a legal ID, which is why ELK's own corpus writes `^port.side`
20
+ # and `^position` 700 times over and bare `port:` zero times.
21
+ KEYWORDS = %w[
22
+ graph node port label edge layout section position size
23
+ start end bends incoming outgoing true false null
24
+ ].freeze
25
+ GEOMETRY = %w[incoming outgoing start end].freeze
26
+ LITERALS = { "true" => true, "false" => false,
27
+ "null" => nil }.freeze
28
+ ESCAPES = { "\\" => "\\", '"' => '"', "'" => "'", "n" => "\n",
29
+ "t" => "\t", "r" => "\r", "b" => "\b",
30
+ "f" => "\f" }.freeze
31
+
32
+ def initialize(tokens)
33
+ @tokens = tokens
34
+ @pos = 0
35
+ @declared_ids = []
36
+ @edge_refs = []
37
+ @graph_id = nil
38
+ end
39
+
40
+ def parse
41
+ tree = parse_root
42
+ Resolver.new(tree, @declared_ids, @edge_refs, @graph_id).resolve!
43
+ tree
44
+ end
45
+
46
+ private
47
+
48
+ def parse_root
49
+ root = { id: "root", layoutOptions: {}, children: [], edges: [] }
50
+ root[:id] = parse_graph_header if keyword?("graph")
51
+ parse_shape_layout(root) if keyword?("layout")
52
+ parse_properties(root)
53
+ parse_members(root, %w[node port label edge])
54
+ expect_eof
55
+ root
56
+ end
57
+
58
+ def parse_graph_header
59
+ advance
60
+ @graph_id = single_id
61
+ end
62
+
63
+ def parse_members(container, allowed)
64
+ until %i[rbrace eof].include?(peek.type)
65
+ name = allowed.find { |key| peek.keyword?(key) }
66
+ raise_at(peek, "expected #{allowed.join(', ')}") unless name
67
+
68
+ parse_member(name, container)
69
+ end
70
+ end
71
+
72
+ def parse_member(name, container)
73
+ case name
74
+ when "node" then parse_node(container)
75
+ when "port" then parse_port(container)
76
+ when "label" then parse_label(container)
77
+ when "edge" then parse_edge(container)
78
+ end
79
+ end
80
+
81
+ def parse_body(container, allowed)
82
+ expect(:lbrace)
83
+ parse_shape_layout(container) if keyword?("layout")
84
+ parse_properties(container)
85
+ parse_members(container, allowed)
86
+ expect(:rbrace)
87
+ end
88
+
89
+ def parse_node(container)
90
+ advance
91
+ node = { id: single_id, width: 40, height: 40, layoutOptions: {} }
92
+ (container[:children] ||= []) << node
93
+ parse_body(node, %w[node port label edge]) if brace?
94
+ end
95
+
96
+ def parse_port(container)
97
+ advance
98
+ port = { id: single_id, layoutOptions: {} }
99
+ (container[:ports] ||= []) << port
100
+ parse_body(port, %w[label]) if brace?
101
+ end
102
+
103
+ def parse_label(container)
104
+ advance
105
+ id = optional_prefix_id
106
+ token = peek
107
+ text = decode_label(expect(:string), token)
108
+ label = { text: text, width: label_width(text), height: 14.0 }
109
+ label[:id] = id if id
110
+ (container[:labels] ||= []) << label
111
+ parse_body(label, %w[label]) if brace?
112
+ end
113
+
114
+ # Java's String#length counts UTF-16 code units, so that is the unit
115
+ # ELK measures a label in. Ruby's String#length counts characters --
116
+ # and counts BYTES once the string is not valid UTF-8, which billed a
117
+ # single accepted lone surrogate as three characters.
118
+ def label_width(text)
119
+ units = text.unpack("U*").sum { |point| point > 0xFFFF ? 2 : 1 }
120
+ units * 7.0
121
+ end
122
+
123
+ def parse_edge(container)
124
+ advance
125
+ edge = { id: optional_prefix_id }
126
+ edge[:sources] = parse_endpoints
127
+ expect(:arrow)
128
+ edge[:targets] = parse_endpoints
129
+ (container[:edges] ||= []) << edge
130
+ @edge_refs << edge
131
+ parse_edge_body(edge) if brace?
132
+ end
133
+
134
+ def parse_edge_body(edge)
135
+ expect(:lbrace)
136
+ parse_edge_layout(edge) if keyword?("layout")
137
+ parse_properties(edge)
138
+ parse_members(edge, %w[label])
139
+ expect(:rbrace)
140
+ end
141
+
142
+ # `^section` is not the keyword, so an escaped one falls through to the
143
+ # unnamed single section and is read as one of its properties.
144
+ def parse_edge_layout(edge)
145
+ advance
146
+ expect(:lbracket)
147
+ if keyword?("section")
148
+ parse_section(edge) while keyword?("section")
149
+ else
150
+ (edge[:sections] ||= []) << parse_section_body({})
151
+ end
152
+ expect(:rbracket)
153
+ end
154
+
155
+ def parse_section(edge)
156
+ advance
157
+ section = { id: single_id }
158
+ section[:outgoingSections] = parse_outgoing_refs if
159
+ peek.type == :arrow
160
+ expect(:lbracket)
161
+ parse_section_body(section)
162
+ expect(:rbracket)
163
+ (edge[:sections] ||= []) << section
164
+ end
165
+
166
+ # These are references to sibling sections, not declarations, so they
167
+ # are not recorded in `declared_ids` -- but they must be KEPT: parsing
168
+ # them and dropping them lost every link ELK retains.
169
+ def parse_outgoing_refs
170
+ advance
171
+ refs = [single_id(record: false)]
172
+ refs << single_id(record: false) while consume?(:comma)
173
+ refs
174
+ end
175
+
176
+ # Ordered phases, matching ElkGraph.xtext: an unordered geometry group
177
+ # with each member at most once, then at most one bends clause, then
178
+ # properties. Geometry after bends is malformed.
179
+ def parse_section_body(section)
180
+ seen = []
181
+ parse_geometry(section, seen) while geometry?
182
+ parse_bends(section) if keyword?("bends")
183
+ parse_properties(section)
184
+ section
185
+ end
186
+
187
+ def geometry?
188
+ GEOMETRY.any? { |key| peek.keyword?(key) }
189
+ end
190
+
191
+ def parse_geometry(section, seen)
192
+ name = GEOMETRY.find { |key| peek.keyword?(key) }
193
+ raise_at(peek, "duplicate #{name}") if seen.include?(name)
194
+
195
+ seen << name
196
+ advance
197
+ expect(:colon)
198
+ assign_geometry(section, name)
199
+ end
200
+
201
+ def assign_geometry(section, name)
202
+ case name
203
+ when "incoming" then section[:incomingShape] = qualified_id
204
+ when "outgoing" then section[:outgoingShape] = qualified_id
205
+ when "start" then section[:startPoint] = point
206
+ else section[:endPoint] = point
207
+ end
208
+ end
209
+
210
+ def parse_bends(section)
211
+ advance
212
+ expect(:colon)
213
+ points = [point]
214
+ points << point while consume?(:pipe)
215
+ section[:bendPoints] = points
216
+ end
217
+
218
+ # ElkGraph.xtext types Number as EDouble, so `position: 1, 2` is
219
+ # 1.0, 2.0 -- not Integer, whatever the source spelling.
220
+ def point
221
+ x = expect(:number).to_f
222
+ expect(:comma)
223
+ { x: x, y: expect(:number).to_f }
224
+ end
225
+
226
+ # `position` and `size` are an Xtext unordered group: either order,
227
+ # each at most once, and both applied -- never one or the other.
228
+ def parse_shape_layout(container)
229
+ advance
230
+ expect(:lbracket)
231
+ seen = []
232
+ parse_shape_member(container, seen) while peek.type != :rbracket
233
+ expect(:rbracket)
234
+ end
235
+
236
+ def parse_shape_member(container, seen)
237
+ name = %w[position size].find { |key| peek.keyword?(key) }
238
+ raise_at(peek, "expected position or size") unless name
239
+ raise_at(peek, "duplicate #{name}") if seen.include?(name)
240
+
241
+ seen << name
242
+ advance
243
+ expect(:colon)
244
+ assign_shape(container, name)
245
+ end
246
+
247
+ def assign_shape(container, name)
248
+ pair = point
249
+ if name == "position"
250
+ container[:x] = pair[:x]
251
+ container[:y] = pair[:y]
252
+ else
253
+ container[:width] = pair[:x]
254
+ container[:height] = pair[:y]
255
+ end
256
+ end
257
+
258
+ def parse_properties(container)
259
+ parse_property(container) while property?
260
+ end
261
+
262
+ def property?
263
+ peek.type == :identifier && peek(1).type == :colon
264
+ end
265
+
266
+ def parse_property(container)
267
+ token = expect_identifier
268
+ expect(:colon)
269
+ (container[:layoutOptions] ||= {})[token.value] = parse_value
270
+ end
271
+
272
+ def parse_value
273
+ case peek.type
274
+ when :string, :number then advance.value
275
+ when :identifier then identifier_value
276
+ else raise_at(peek, "expected a value")
277
+ end
278
+ end
279
+
280
+ def identifier_value
281
+ token = peek
282
+ return LITERALS.fetch(token.value).tap { advance } if literal?(token)
283
+
284
+ qualified_id
285
+ end
286
+
287
+ def literal?(token)
288
+ token.single_segment? && LITERALS.key?(token.value) &&
289
+ !token.segments.first.escaped
290
+ end
291
+
292
+ def parse_endpoints
293
+ list = [qualified_id]
294
+ list << qualified_id while consume?(:comma)
295
+ list
296
+ end
297
+
298
+ # QualifiedId carries no hidden() override, so whitespace between
299
+ # segments is legal here -- unlike a property key, where maximal munch
300
+ # already guarantees one token.
301
+ def qualified_id
302
+ parts = [expect_identifier.value]
303
+ parts << expect_identifier.value while consume?(:dot)
304
+ parts.join(".")
305
+ end
306
+
307
+ def optional_prefix_id
308
+ return nil unless property?
309
+
310
+ id = single_id
311
+ expect(:colon)
312
+ id
313
+ end
314
+
315
+ def single_id(record: true)
316
+ token = expect_identifier
317
+ raise_at(token, "expected a single identifier") unless
318
+ token.single_segment?
319
+
320
+ @declared_ids << token.value if record
321
+ token.value
322
+ end
323
+
324
+ # A surrogate pair is matched as ONE unit, before the single-unit
325
+ # alternative: decoding `\uD83D` and `\uDE00` separately packs two
326
+ # lone surrogates and yields a String that is not valid UTF-8.
327
+ def decode_label(raw, token)
328
+ raw.gsub(/\\(u[dD][89abAB]\h{2}\\u[dD][c-fC-F]\h{2}|u\h{4}|.)/m) do
329
+ decode_escape(Regexp.last_match(1), token)
330
+ end
331
+ end
332
+
333
+ # Xtext's STRINGValueConverter rejects an unknown escape outright; the
334
+ # bare character is only the recovery value on its exception.
335
+ def decode_escape(sequence, token)
336
+ return decode_surrogate_pair(sequence) if sequence.length == 11
337
+ return decode_code_unit(sequence) if sequence.length == 5
338
+
339
+ ESCAPES.fetch(sequence) do
340
+ raise_at(token, "invalid escape \\#{sequence} in label text")
341
+ end
342
+ end
343
+
344
+ def decode_surrogate_pair(sequence)
345
+ high = sequence[1, 4].hex
346
+ low = sequence[7, 4].hex
347
+ [0x10000 + ((high - 0xD800) << 10) + (low - 0xDC00)].pack("U")
348
+ end
349
+
350
+ # An unpaired surrogate is accepted: ELK emits the single UTF-16 code
351
+ # unit with no diagnostic, so raising here rejected valid input --
352
+ # exactly the failure this rewrite exists to remove. Ruby packs it to
353
+ # CESU-8 bytes, giving a String that is not valid UTF-8, so anything
354
+ # serializing it downstream must expect that.
355
+ def decode_code_unit(sequence)
356
+ [sequence[1..].hex].pack("U")
357
+ end
358
+
359
+ def expect_identifier
360
+ token = peek
361
+ raise_at(token, "expected an identifier") unless
362
+ token.type == :identifier
363
+
364
+ validate_segments(token)
365
+ advance
366
+ end
367
+
368
+ def validate_segments(token)
369
+ token.segments.each do |segment|
370
+ next if segment.escaped || !KEYWORDS.include?(segment.name)
371
+
372
+ raise_at(token,
373
+ "`#{segment.name}` is a reserved keyword; " \
374
+ "write `^#{segment.name}`")
375
+ end
376
+ end
377
+
378
+ def keyword?(name)
379
+ peek.keyword?(name)
380
+ end
381
+
382
+ def brace?
383
+ peek.type == :lbrace
384
+ end
385
+
386
+ def peek(offset = 0)
387
+ @tokens[@pos + offset] || @tokens.last
388
+ end
389
+
390
+ def advance
391
+ token = peek
392
+ @pos += 1
393
+ token
394
+ end
395
+
396
+ def consume?(type)
397
+ return false unless peek.type == type
398
+
399
+ advance
400
+ true
401
+ end
402
+
403
+ def expect(type)
404
+ token = peek
405
+ raise_at(token, "expected #{type}") unless token.type == type
406
+
407
+ advance
408
+ token.value
409
+ end
410
+
411
+ def expect_eof
412
+ raise_at(peek, "unexpected `#{peek}`") unless peek.type == :eof
413
+ end
414
+
415
+ def raise_at(token, message)
416
+ raise Elkrb::ParseError.new(
417
+ "#{message} at line #{token.line}, column #{token.column}",
418
+ line: token.line, column: token.column,
419
+ )
420
+ end
421
+ end
422
+ end
423
+ end
424
+ end
@@ -0,0 +1,142 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elkrb
4
+ module Parsers
5
+ module Elkt
6
+ # Finishes a parsed tree: resolves dotted endpoints and allocates the
7
+ # automatic edge ids.
8
+ #
9
+ # Neither pass can run during the parse. ELKT permits forward references
10
+ # -- five files in ELK's own corpus declare an edge before its nodes --
11
+ # so endpoints resolve only once the whole tree exists. And the tree has
12
+ # already split `children` from `edges`, losing source interleaving, so
13
+ # ids come from `edge_refs`, which the parser appended in document order.
14
+ #
15
+ # @api private
16
+ class Resolver
17
+ def initialize(tree, declared_ids, edge_refs, graph_id = nil)
18
+ @tree = tree
19
+ @declared_ids = declared_ids
20
+ @edge_refs = edge_refs
21
+ @graph_id = graph_id
22
+ end
23
+
24
+ def resolve!
25
+ resolve_container(@tree, root_scope)
26
+ assign_edge_ids
27
+ @tree
28
+ end
29
+
30
+ private
31
+
32
+ # `graph G` names the root, and ELK resolves `G.a.p` through it. Whether
33
+ # a header was written is tracked as its own fact: inferring it from
34
+ # `declared_ids` gave a document containing `node root` a root scope it
35
+ # never declared.
36
+ def root_scope
37
+ return [] unless @graph_id
38
+
39
+ [{ @graph_id => [@tree] }]
40
+ end
41
+
42
+ # `scopes` is the enclosing chain, innermost first. ELKT binds Xtext's
43
+ # ImportedNamespaceAwareLocalScopeProvider, so a name unresolved in the
44
+ # edge's own container falls outward to the enclosing ones -- while a
45
+ # local match still wins, which is what keeps a repeated id at another
46
+ # level from binding to the wrong node.
47
+ def resolve_container(container, outer)
48
+ scopes = [build_index(container), *outer]
49
+ (container[:edges] || []).each { |edge| resolve_edge(edge, scopes) }
50
+ (container[:children] || []).each do |child|
51
+ resolve_container(child, scopes)
52
+ end
53
+ end
54
+
55
+ def resolve_edge(edge, scopes)
56
+ %i[sources targets].each do |key|
57
+ edge[key] = edge[key].map { |ref| resolve_ref(ref, scopes) }
58
+ end
59
+ (edge[:sections] || []).each { |s| resolve_section(s, scopes) }
60
+ end
61
+
62
+ # incoming/outgoing are cross-references to ElkConnectableShape, the
63
+ # same as an endpoint; ELK's JsonExporter writes the terminal id.
64
+ def resolve_section(section, scopes)
65
+ %i[incomingShape outgoingShape].each do |key|
66
+ next unless section[key]
67
+
68
+ section[key] = resolve_ref(section[key], scopes)
69
+ end
70
+ end
71
+
72
+ # An unresolvable reference is kept verbatim rather than collapsed to
73
+ # its head segment, which would silently point the edge at a different,
74
+ # real node.
75
+ def resolve_ref(reference, scopes)
76
+ scopes.each do |index|
77
+ resolved = resolve_in(reference, index)
78
+ return resolved if resolved
79
+ end
80
+ reference
81
+ end
82
+
83
+ def resolve_in(reference, index)
84
+ parts = reference.split(".")
85
+ index.fetch(parts.first, []).each do |entry|
86
+ resolved = resolve_path(entry, parts.drop(1))
87
+ return resolved if resolved
88
+ end
89
+ nil
90
+ end
91
+
92
+ # Backtracks rather than committing to the first candidate segment.
93
+ # ELK builds a fully qualified name from the containing elements, so a
94
+ # COMPLETE node chain beats a port whose prefix matches but whose path
95
+ # dead-ends. Measured against ELK 0.12.0: with `port b` and
96
+ # `node b { node c }` in one node, `a.b.c` resolves to `c`.
97
+ def resolve_path(node, parts)
98
+ return node[:id] if parts.empty?
99
+
100
+ head = parts.first
101
+ rest = parts.drop(1)
102
+ candidates(node, head).each do |entry|
103
+ resolved = resolve_path(entry, rest)
104
+ return resolved if resolved
105
+ end
106
+ nil
107
+ end
108
+
109
+ # Children first: a port is a leaf, so it can only ever satisfy the
110
+ # final segment, and preferring it earlier truncates a longer path.
111
+ def candidates(node, name)
112
+ (node[:children] || []).select { |entry| entry[:id] == name } +
113
+ (node[:ports] || []).select { |entry| entry[:id] == name }
114
+ end
115
+
116
+ # Every candidate for an id, not one winner: the first segment needs
117
+ # the same backtracking as every later one, so a contested name keeps
118
+ # both the child and the port in play.
119
+ def build_index(container)
120
+ members = (container[:children] || []) + (container[:ports] || [])
121
+ members.group_by { |member| member[:id] }
122
+ end
123
+
124
+ # One graph-wide counter over a taken set seeded with every declared id
125
+ # -- the root graph's own id included -- so an automatic id can collide
126
+ # with neither an explicit edge id nor a node, port, label or section.
127
+ def assign_edge_ids
128
+ taken = @declared_ids.to_h { |id| [id, true] }
129
+ counter = 0
130
+ @edge_refs.each do |edge|
131
+ next if edge[:id]
132
+
133
+ counter += 1 while taken.key?("e#{counter}")
134
+ edge[:id] = "e#{counter}"
135
+ taken[edge[:id]] = true
136
+ counter += 1
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elkrb
4
+ module Parsers
5
+ module Elkt
6
+ # One dotted part of an identifier, caret stripped. `escaped` is true
7
+ # when the source wrote `^name`.
8
+ #
9
+ # @api private
10
+ Segment = Data.define(:name, :escaped)
11
+
12
+ # A single ELKT token.
13
+ #
14
+ # `segments` is populated for `:identifier` only: one Segment per dotted
15
+ # part. `value` is the joined semantic name. Both are needed because
16
+ # ELKT's leading-caret escape is per segment --
17
+ # `org.eclipse.elk.^port.side` escapes only its fourth -- and because a
18
+ # declaration site accepts a single segment only.
19
+ #
20
+ # A `:string` value is the unquoted RAW inner lexeme. Labels and property
21
+ # values decode differently upstream, so decoding happens at the parse
22
+ # site, never here.
23
+ #
24
+ # @api private
25
+ Token = Data.define(:type, :value, :segments, :line, :column) do
26
+ def keyword?(name)
27
+ type == :identifier && value == name && !segments.first.escaped
28
+ end
29
+
30
+ def single_segment?
31
+ type == :identifier && segments.length == 1
32
+ end
33
+
34
+ def to_s
35
+ type == :eof ? "end of input" : value.to_s
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end