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,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
|