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
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require_relative "../errors"
5
+ require_relative "decimal"
6
+ require_relative "element_options"
7
+ require_relative "option_map"
8
+ require_relative "spellings"
9
+ require_relative "unhonoured_report"
10
+
11
+ module Elkrb
12
+ module Options
13
+ # Reads layout options in one order: element layoutOptions, element
14
+ # properties, call-level options, registry default. The caller names the
15
+ # elements to consult; nothing is inherited from a parent. Every spelling
16
+ # the registry knows reaches the same value, and an explicit false or 0 is
17
+ # a value, not a miss.
18
+ #
19
+ # @example
20
+ # resolver = Elkrb::Options::Resolver.new(spacing_node_node: 40)
21
+ # resolver.get("elk.spacing.nodeNode", node, graph) # => 40.0
22
+ class Resolver
23
+ NUMERIC_TYPES = %i[float integer].freeze
24
+ private_constant :NUMERIC_TYPES
25
+
26
+ # @param call_options [Hash] the options passed to the layout call, under
27
+ # any key spelling; Symbol keys are read like String keys
28
+ def initialize(call_options = {})
29
+ @spellings = Spellings.new
30
+ @call = OptionMap.new(call_options, @spellings)
31
+ end
32
+
33
+ # @param key [String, Symbol] any registry id or alias, or a custom key
34
+ # @param elements [Array<#layout_options, #properties, nil>] consulted
35
+ # in the order given
36
+ # @param default [Object] returned when nothing names the key;
37
+ # :registry means the registry default, nil means nil
38
+ # @return [Object] the value, coerced to the registry type for a known
39
+ # id and left as written for an unknown one
40
+ # @raise [Elkrb::ValidationError] when the value cannot be coerced to
41
+ # the registered type, or is a non-finite number
42
+ def get(key, *elements, default: :registry)
43
+ id = @spellings.id(key)
44
+
45
+ elements.each do |element|
46
+ value = element_value(element, id)
47
+ return coerce(id, value) unless value.nil?
48
+ end
49
+
50
+ value = @call.value(id)
51
+ return coerce(id, value) unless value.nil?
52
+
53
+ default == :registry ? Registry.default(id) : default
54
+ end
55
+
56
+ # True when the call asked for strict option handling.
57
+ #
58
+ # @raise [Elkrb::ValidationError] when strict: is neither true nor
59
+ # false, so a typo cannot silently turn strictness off
60
+ def strict?
61
+ case (strict = @call.value(@spellings.id("strict")))
62
+ when true then true
63
+ when false, nil then false
64
+ else
65
+ raise ValidationError,
66
+ "option strict must be true or false, got #{strict.inspect}"
67
+ end
68
+ end
69
+
70
+ # Reports every key in the graph's layoutOptions, at any level, that the
71
+ # registry does not know or does not fully honour, and every registered
72
+ # key in an element's properties or the call that it does not. A key
73
+ # only some algorithms read is reported only where the algorithm laying
74
+ # out its element does not read it. Unknown property names and call keys
75
+ # are not reported. Warns once per key; with strict: true it raises
76
+ # before anything is logged.
77
+ #
78
+ # @param graph [Elkrb::Graph::Graph]
79
+ # @param algorithm_name [#call] name as written => registered name or nil
80
+ # @raise [Elkrb::Error] in strict mode, naming each such key
81
+ def report_unhonoured(graph,
82
+ algorithm_name: UnhonouredReport::AS_WRITTEN)
83
+ strict = strict?
84
+ report = UnhonouredReport.new(
85
+ graph, @spellings, @call,
86
+ root_algorithm: algorithm_name.call(get("elk.algorithm", graph)),
87
+ algorithm_name: algorithm_name
88
+ )
89
+ return if report.empty?
90
+
91
+ raise Error, report.strict_message if strict
92
+
93
+ report.log
94
+ end
95
+
96
+ private
97
+
98
+ def element_value(element, id)
99
+ ElementOptions.new(element, @spellings).value(id)
100
+ end
101
+
102
+ def coerce(id, value)
103
+ coerced = coerce_value(id, numeric_input(id, value))
104
+ unless finite?(coerced)
105
+ raise ValidationError,
106
+ "option #{id} must be a finite number, got #{value.inspect}"
107
+ end
108
+
109
+ coerced
110
+ end
111
+
112
+ # A string for a numeric option must be plain decimal, so "abc" and
113
+ # "0x10" raise instead of becoming 0.0. It is parsed here, once.
114
+ def numeric_input(id, value)
115
+ return value unless value.is_a?(String)
116
+ return value unless NUMERIC_TYPES.include?(Registry.all.dig(id, :type))
117
+
118
+ Decimal.parse(value) ||
119
+ raise(ValidationError,
120
+ "invalid value #{value.inspect} for option #{id}")
121
+ end
122
+
123
+ # The one delegate call the rescue covers. Registry.coerce raises
124
+ # NoMethodError/TypeError for a value of the wrong shape (an Array for
125
+ # a Float), ArgumentError for malformed padding, and RangeError
126
+ # (FloatDomainError, Complex) for a number that cannot be an Integer or
127
+ # Float; all are bad input here, and the caller needs the option's name.
128
+ def coerce_value(id, value)
129
+ Registry.coerce(id, value)
130
+ rescue ArgumentError, TypeError, NoMethodError, RangeError, EncodingError
131
+ raise ValidationError,
132
+ "invalid value #{value.inspect} for option #{id}"
133
+ end
134
+
135
+ # Infinity or NaN anywhere in a coerced number, padding or vector.
136
+ def finite?(coerced)
137
+ components(coerced).none? { |n| n.is_a?(Float) && !n.finite? }
138
+ end
139
+
140
+ def components(coerced)
141
+ case coerced
142
+ when Float then [coerced]
143
+ when Options::ElkPadding then coerced.to_h.values
144
+ when Options::KVector then coerced.to_a
145
+ when Options::KVectorChain then coerced.vectors.flat_map(&:to_a)
146
+ else []
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+
5
+ module Elkrb
6
+ module Options
7
+ # @api private
8
+ #
9
+ # Resolves an option key, as a Symbol or String, to its canonical id and
10
+ # ranks how canonical that spelling is. Registry.canonical scans every
11
+ # registered id for a suffix match, so each distinct name is resolved once.
12
+ class Spellings
13
+ ELK_LONG_PREFIX = "org.eclipse.elk."
14
+ ELK_SHORT_PREFIX = "elk."
15
+ private_constant :ELK_LONG_PREFIX, :ELK_SHORT_PREFIX
16
+
17
+ def initialize
18
+ @resolved = {}
19
+ end
20
+
21
+ # @param key [String, Symbol]
22
+ # @return [Array(String, Integer)] the canonical id (an unknown key
23
+ # stays under its own name) and the spelling's rank, lowest best:
24
+ # the id, then its org.eclipse.elk. form, then each alias in
25
+ # registry order, then the shorter dot-suffixes of the id, the
26
+ # longer first. No two spellings of one id share a rank.
27
+ def resolve(key)
28
+ name = key.to_s
29
+ @resolved[name] ||= begin
30
+ id = Registry.canonical(name) || name
31
+ [id, rank(name, id)]
32
+ end
33
+ end
34
+
35
+ def id(key)
36
+ resolve(key).first
37
+ end
38
+
39
+ private
40
+
41
+ def rank(name, id)
42
+ return 0 if name == id
43
+ return 1 if name == long_form(id)
44
+
45
+ 2 + other_rank(name, id)
46
+ end
47
+
48
+ # An alias by its place in the registry, else a dot-suffix of the id
49
+ # after all the aliases, the longer suffix first.
50
+ def other_rank(name, id)
51
+ aliases = Array(Registry.all[id]&.fetch(:aliases, nil))
52
+ aliases.index(name) || (aliases.size + id.length - name.length)
53
+ end
54
+
55
+ def long_form(id)
56
+ return id unless id.start_with?(ELK_SHORT_PREFIX)
57
+
58
+ ELK_LONG_PREFIX + id.delete_prefix(ELK_SHORT_PREFIX)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "element_options"
4
+ require_relative "option_map"
5
+ require_relative "registry"
6
+ require_relative "spellings"
7
+
8
+ module Elkrb
9
+ module Options
10
+ # @api private
11
+ #
12
+ # The option keys in a graph's elements, at any level, that the registry
13
+ # does not know or does not fully honour, plus the call's own registered
14
+ # keys that are not fully honoured. An element's sources are those of
15
+ # ElementOptions. A key that is partial across algorithms (see
16
+ # Registry's readers) is judged against the algorithm that lays out the
17
+ # element carrying it, and a call key against every algorithm in play.
18
+ # Built and read by Resolver#report_unhonoured.
19
+ class UnhonouredReport
20
+ NESTED_COLLECTIONS = %i[children edges ports labels].freeze
21
+ CONTROL_CHARACTERS = /[[:cntrl:]]/
22
+ ALGORITHM_ID = "elk.algorithm"
23
+ # Takes an algorithm name as written; answers the name it is
24
+ # registered under, or nil when it is not registered.
25
+ AS_WRITTEN = :to_s.to_proc
26
+ private_constant :NESTED_COLLECTIONS, :CONTROL_CHARACTERS, :ALGORITHM_ID
27
+
28
+ # @param graph [Elkrb::Graph::Graph]
29
+ # @param spellings [Spellings]
30
+ # @param call [OptionMap] the options passed to the layout call
31
+ # @param root_algorithm [String, nil] the algorithm laying out the
32
+ # graph, by registered name
33
+ # @param algorithm_name [#call] resolves a name written in the options
34
+ # to a registered name; AS_WRITTEN takes every name as registered
35
+ def initialize(graph, spellings = Spellings.new,
36
+ call = OptionMap.new({}, spellings),
37
+ root_algorithm: nil, algorithm_name: AS_WRITTEN)
38
+ @spellings = spellings
39
+ @graph = graph
40
+ @root_algorithm = root_algorithm
41
+ @algorithm_name = algorithm_name
42
+ @findings = findings_for(graph, call)
43
+ end
44
+
45
+ def empty?
46
+ @findings.empty?
47
+ end
48
+
49
+ # @return [String] one message naming every finding
50
+ def strict_message
51
+ described = @findings.map do |id, status|
52
+ "#{printable(id)} " \
53
+ "(#{status ? "#{status}, not honoured" : 'unknown'})"
54
+ end
55
+ "strict mode: unknown or unhonoured layout option(s): " \
56
+ "#{described.join(', ')}"
57
+ end
58
+
59
+ # Partly honoured and accepted keys warn; unknown keys log at DEBUG
60
+ # because elkjs ignores them too.
61
+ def log
62
+ @findings.each do |id, status|
63
+ if status.nil?
64
+ Elkrb.logger.debug(
65
+ "elkrb: unknown option #{printable(id)}: stored and echoed",
66
+ )
67
+ else
68
+ Elkrb.logger.warn(warning_for(id, status))
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ # { canonical id => status }; status is nil for an unknown key. An
76
+ # unknown key in the call is not reported: the call also carries engine
77
+ # flags such as strict. The call's nested properties
78
+ # map is never read as options, so it is not reported either.
79
+ def findings_for(graph, call)
80
+ occurrences(laid_out_elements(graph), call)
81
+ .each_with_object({}) do |(id, algorithms), findings|
82
+ status = Registry.status(id)
83
+ findings[id] = status if unread?(id, status, algorithms)
84
+ end
85
+ end
86
+
87
+ # [[canonical id, algorithm/carrier routes that would read it]], one per
88
+ # place a key is written. A call key reaches every level, so any
89
+ # algorithm in play may read it.
90
+ def occurrences(levels, call)
91
+ in_play = levels.filter_map { |_, algorithm, _| algorithm }.uniq
92
+ written = written_occurrences(levels)
93
+ called = call.own_ids.select { |id| Registry.status(id) }
94
+ call_routes = in_play.map { |algorithm| [algorithm, :call] }
95
+ (written + called.map { |id| [id, call_routes] }).uniq
96
+ end
97
+
98
+ def written_occurrences(levels)
99
+ levels.flat_map do |options, _, routes|
100
+ options.ids.map { |id| [id, routes] }
101
+ end
102
+ end
103
+
104
+ def unread?(id, status, routes)
105
+ status != :honoured &&
106
+ routes.none? do |algorithm, carrier|
107
+ Registry.read_by?(id, algorithm, carrier: carrier)
108
+ end
109
+ end
110
+
111
+ # [[ElementOptions, algorithm, carrier]] for every element reachable
112
+ # from the graph, breadth first. Leaf elements inherit the algorithm of
113
+ # their enclosing layout level. Iterative, so a deeply nested graph
114
+ # cannot overflow the stack here; an element reachable by two paths is
115
+ # visited once for each algorithm that would lay out the level above it.
116
+ def laid_out_elements(graph)
117
+ visits = [[graph, @root_algorithm]]
118
+ seen = {}
119
+ visits.filter_map do |element, enclosing| # visits grows as it is walked
120
+ next if seen.key?(key = [element.object_id, enclosing])
121
+
122
+ seen[key] = true
123
+ level_of(element, enclosing).tap do |level|
124
+ nested_elements(element).each { |c| visits << [c, level[1]] }
125
+ end
126
+ end
127
+ end
128
+
129
+ def level_of(element, enclosing)
130
+ options = ElementOptions.new(element, @spellings)
131
+ algorithm = layout_algorithm(element, options, enclosing)
132
+ [options, algorithm, routes_for(element, algorithm, enclosing)]
133
+ end
134
+
135
+ # The algorithm a compound node is laid out by is its own registered
136
+ # elk.algorithm, else the one laying out the level it sits in, the way
137
+ # HierarchicalProcessor picks it.
138
+ def layout_algorithm(element, options, enclosing)
139
+ return enclosing if element.equal?(@graph)
140
+ return enclosing unless compound?(element)
141
+
142
+ own = options.value(ALGORITHM_ID)
143
+ (own && @algorithm_name.call(own)) || enclosing
144
+ end
145
+
146
+ def compound?(element)
147
+ element.respond_to?(:children) && !Array(element.children).empty?
148
+ end
149
+
150
+ # A compound is both the graph laid out by its own algorithm and a node
151
+ # routed by the enclosing algorithm. Its options can therefore be read
152
+ # in either role.
153
+ def routes_for(element, algorithm, enclosing)
154
+ if compound?(element) && !element.equal?(@graph)
155
+ [[algorithm, :compound], [enclosing, :node]].uniq
156
+ else
157
+ [[algorithm, carrier(element)]]
158
+ end
159
+ end
160
+
161
+ def carrier(element)
162
+ return :root if element.equal?(@graph)
163
+ return :compound if compound?(element)
164
+ return :edge if element.respond_to?(:sources)
165
+ return :port if element.respond_to?(:side)
166
+ return :label if element.respond_to?(:text)
167
+
168
+ :node
169
+ end
170
+
171
+ def nested_elements(element)
172
+ NESTED_COLLECTIONS.flat_map do |collection|
173
+ next [] unless element.respond_to?(collection)
174
+
175
+ Array(element.public_send(collection))
176
+ end
177
+ end
178
+
179
+ # An unknown key comes from the input file; keep control characters
180
+ # and invalid bytes out of the log and the error message.
181
+ def printable(id)
182
+ id.to_s.scrub.gsub(CONTROL_CHARACTERS) { |char| char.dump[1..-2] }
183
+ end
184
+
185
+ def warning_for(id, status)
186
+ if status == :partial
187
+ "elkrb: option #{id} is partially honoured: #{Registry.note(id)}"
188
+ else
189
+ "elkrb: option #{id} is accepted but not honoured in this version"
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end