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.
- 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
|
@@ -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
|