elkrb 1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +11 -0
- data/Gemfile +13 -0
- data/README.adoc +1028 -0
- data/Rakefile +64 -0
- data/benchmarks/README.md +172 -0
- data/benchmarks/elkjs_benchmark.js +140 -0
- data/benchmarks/elkrb_benchmark.rb +145 -0
- data/benchmarks/fixtures/graphs.json +10777 -0
- data/benchmarks/generate_report.rb +241 -0
- data/benchmarks/generate_test_graphs.rb +154 -0
- data/benchmarks/results/elkrb_results.json +280 -0
- data/benchmarks/results/elkrb_summary.json +285 -0
- data/elkrb.gemspec +39 -0
- data/examples/dot_export_demo.rb +133 -0
- data/examples/hierarchical_graph.rb +19 -0
- data/examples/layout_constraints_demo.rb +272 -0
- data/examples/port_constraints_demo.rb +291 -0
- data/examples/self_loop_demo.rb +391 -0
- data/examples/simple_graph.rb +50 -0
- data/examples/spline_routing_demo.rb +235 -0
- data/exe/elkrb +8 -0
- data/lib/elkrb/cli.rb +224 -0
- data/lib/elkrb/commands/batch_command.rb +66 -0
- data/lib/elkrb/commands/convert_command.rb +130 -0
- data/lib/elkrb/commands/diagram_command.rb +208 -0
- data/lib/elkrb/commands/render_command.rb +52 -0
- data/lib/elkrb/commands/validate_command.rb +241 -0
- data/lib/elkrb/errors.rb +30 -0
- data/lib/elkrb/geometry/bezier.rb +163 -0
- data/lib/elkrb/geometry/dimension.rb +32 -0
- data/lib/elkrb/geometry/point.rb +68 -0
- data/lib/elkrb/geometry/rectangle.rb +86 -0
- data/lib/elkrb/geometry/vector.rb +67 -0
- data/lib/elkrb/graph/edge.rb +95 -0
- data/lib/elkrb/graph/graph.rb +90 -0
- data/lib/elkrb/graph/label.rb +45 -0
- data/lib/elkrb/graph/layout_options.rb +247 -0
- data/lib/elkrb/graph/node.rb +79 -0
- data/lib/elkrb/graph/node_constraints.rb +107 -0
- data/lib/elkrb/graph/port.rb +104 -0
- data/lib/elkrb/graphviz_wrapper.rb +133 -0
- data/lib/elkrb/layout/algorithm_registry.rb +57 -0
- data/lib/elkrb/layout/algorithms/base_algorithm.rb +208 -0
- data/lib/elkrb/layout/algorithms/box.rb +47 -0
- data/lib/elkrb/layout/algorithms/disco.rb +206 -0
- data/lib/elkrb/layout/algorithms/fixed.rb +32 -0
- data/lib/elkrb/layout/algorithms/force.rb +165 -0
- data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +86 -0
- data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +96 -0
- data/lib/elkrb/layout/algorithms/layered/node_placer.rb +77 -0
- data/lib/elkrb/layout/algorithms/layered.rb +49 -0
- data/lib/elkrb/layout/algorithms/libavoid.rb +389 -0
- data/lib/elkrb/layout/algorithms/mrtree.rb +144 -0
- data/lib/elkrb/layout/algorithms/radial.rb +64 -0
- data/lib/elkrb/layout/algorithms/random.rb +43 -0
- data/lib/elkrb/layout/algorithms/rectpacking.rb +93 -0
- data/lib/elkrb/layout/algorithms/spore_compaction.rb +139 -0
- data/lib/elkrb/layout/algorithms/spore_overlap.rb +117 -0
- data/lib/elkrb/layout/algorithms/stress.rb +176 -0
- data/lib/elkrb/layout/algorithms/topdown_packing.rb +183 -0
- data/lib/elkrb/layout/algorithms/vertiflex.rb +174 -0
- data/lib/elkrb/layout/constraints/alignment_constraint.rb +150 -0
- data/lib/elkrb/layout/constraints/base_constraint.rb +72 -0
- data/lib/elkrb/layout/constraints/constraint_processor.rb +134 -0
- data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +87 -0
- data/lib/elkrb/layout/constraints/layer_constraint.rb +71 -0
- data/lib/elkrb/layout/constraints/relative_position_constraint.rb +110 -0
- data/lib/elkrb/layout/edge_router.rb +935 -0
- data/lib/elkrb/layout/hierarchical_processor.rb +299 -0
- data/lib/elkrb/layout/label_placer.rb +338 -0
- data/lib/elkrb/layout/layout_engine.rb +170 -0
- data/lib/elkrb/layout/port_constraint_processor.rb +173 -0
- data/lib/elkrb/options/elk_padding.rb +94 -0
- data/lib/elkrb/options/k_vector.rb +100 -0
- data/lib/elkrb/options/k_vector_chain.rb +135 -0
- data/lib/elkrb/parsers/elkt_parser.rb +248 -0
- data/lib/elkrb/serializers/dot_serializer.rb +339 -0
- data/lib/elkrb/serializers/elkt_serializer.rb +236 -0
- data/lib/elkrb/version.rb +5 -0
- data/lib/elkrb.rb +509 -0
- data/sig/elkrb/constraints.rbs +114 -0
- data/sig/elkrb/geometry.rbs +61 -0
- data/sig/elkrb/graph.rbs +112 -0
- data/sig/elkrb/layout.rbs +107 -0
- data/sig/elkrb/options.rbs +81 -0
- data/sig/elkrb.rbs +32 -0
- metadata +179 -0
data/lib/elkrb/cli.rb
ADDED
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "json"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module Elkrb
|
|
8
|
+
# Command-line interface for elkrb
|
|
9
|
+
#
|
|
10
|
+
# Provides commands for laying out graphs from the command line.
|
|
11
|
+
# Supports JSON and YAML input/output formats.
|
|
12
|
+
class Cli < Thor
|
|
13
|
+
class_option :verbose, type: :boolean, default: false,
|
|
14
|
+
desc: "Enable verbose output"
|
|
15
|
+
|
|
16
|
+
desc "layout FILE", "Layout a graph from a JSON or YAML file"
|
|
17
|
+
option :algorithm, type: :string, default: "layered",
|
|
18
|
+
desc: "Layout algorithm to use"
|
|
19
|
+
option :output, type: :string, aliases: "-o",
|
|
20
|
+
desc: "Output file (default: stdout)"
|
|
21
|
+
option :format, type: :string, default: "json",
|
|
22
|
+
enum: %w[json yaml],
|
|
23
|
+
desc: "Output format"
|
|
24
|
+
option :spacing, type: :numeric,
|
|
25
|
+
desc: "Node spacing"
|
|
26
|
+
option :layer_spacing, type: :numeric,
|
|
27
|
+
desc: "Layer spacing (for layered algorithm)"
|
|
28
|
+
option :padding_top, type: :numeric,
|
|
29
|
+
desc: "Top padding"
|
|
30
|
+
option :padding_bottom, type: :numeric,
|
|
31
|
+
desc: "Bottom padding"
|
|
32
|
+
option :padding_left, type: :numeric,
|
|
33
|
+
desc: "Left padding"
|
|
34
|
+
option :padding_right, type: :numeric,
|
|
35
|
+
desc: "Right padding"
|
|
36
|
+
def layout(file)
|
|
37
|
+
verbose_output "Loading graph from #{file}..."
|
|
38
|
+
|
|
39
|
+
# Read input file
|
|
40
|
+
graph_data = read_input_file(file)
|
|
41
|
+
|
|
42
|
+
# Build layout options
|
|
43
|
+
layout_options = build_layout_options
|
|
44
|
+
|
|
45
|
+
verbose_output "Using algorithm: #{layout_options[:algorithm]}"
|
|
46
|
+
|
|
47
|
+
# Perform layout
|
|
48
|
+
result = Layout::LayoutEngine.layout(graph_data, layout_options)
|
|
49
|
+
|
|
50
|
+
# Output result
|
|
51
|
+
output_result(result)
|
|
52
|
+
|
|
53
|
+
verbose_output "Layout complete!"
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
error_output "Error: #{e.message}"
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
desc "algorithms", "List available layout algorithms"
|
|
60
|
+
def algorithms
|
|
61
|
+
algos = Layout::LayoutEngine.known_layout_algorithms
|
|
62
|
+
|
|
63
|
+
say "Available Layout Algorithms:", :green
|
|
64
|
+
say ""
|
|
65
|
+
|
|
66
|
+
algos.each do |algo|
|
|
67
|
+
say " #{algo[:id]}", :cyan
|
|
68
|
+
say " Name: #{algo[:name]}"
|
|
69
|
+
say " Description: #{algo[:description]}"
|
|
70
|
+
say " Category: #{algo[:category]}" if algo[:category] != "general"
|
|
71
|
+
say " Supports Hierarchy: Yes" if algo[:supports_hierarchy]
|
|
72
|
+
say ""
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
desc "diagram FILE", "Create diagram from ELK graph file"
|
|
77
|
+
option :algorithm, type: :string, default: "layered",
|
|
78
|
+
desc: "Layout algorithm to use"
|
|
79
|
+
option :direction, type: :string,
|
|
80
|
+
desc: "Layout direction (e.g., DOWN, RIGHT)"
|
|
81
|
+
option :spacing, type: :numeric,
|
|
82
|
+
desc: "Node spacing"
|
|
83
|
+
option :edge_routing, type: :string,
|
|
84
|
+
desc: "Edge routing strategy"
|
|
85
|
+
option :output, type: :string, aliases: "-o", required: true,
|
|
86
|
+
desc: "Output file path"
|
|
87
|
+
option :format, type: :string,
|
|
88
|
+
desc: "Output format (auto-detected from extension)"
|
|
89
|
+
option :preview, type: :boolean, default: false,
|
|
90
|
+
desc: "Open result in default viewer"
|
|
91
|
+
def diagram(file)
|
|
92
|
+
require_relative "commands/diagram_command"
|
|
93
|
+
Commands::DiagramCommand.new(file, options).run
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
error_output "Error: #{e.message}"
|
|
96
|
+
exit 1
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
desc "convert FILE", "Convert between formats (JSON/YAML/DOT/ELKT)"
|
|
100
|
+
option :output, type: :string, aliases: "-o", required: true,
|
|
101
|
+
desc: "Output file path"
|
|
102
|
+
option :format, type: :string,
|
|
103
|
+
desc: "Output format (auto-detected from extension)"
|
|
104
|
+
def convert(file)
|
|
105
|
+
require_relative "commands/convert_command"
|
|
106
|
+
Commands::ConvertCommand.new(file, options).run
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
error_output "Error: #{e.message}"
|
|
109
|
+
exit 1
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
desc "render DOT_FILE", "Render DOT to image (requires Graphviz)"
|
|
113
|
+
option :output, type: :string, aliases: "-o", required: true,
|
|
114
|
+
desc: "Output image file path"
|
|
115
|
+
option :engine, type: :string, default: "dot",
|
|
116
|
+
desc: "Graphviz engine (dot, neato, fdp, etc.)"
|
|
117
|
+
option :dpi, type: :numeric, default: 96,
|
|
118
|
+
desc: "Image resolution in DPI"
|
|
119
|
+
def render(dot_file)
|
|
120
|
+
require_relative "commands/render_command"
|
|
121
|
+
Commands::RenderCommand.new(dot_file, options).run
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
error_output "Error: #{e.message}"
|
|
124
|
+
exit 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
desc "validate FILE", "Validate ELK graph structure"
|
|
128
|
+
option :strict, type: :boolean, default: false,
|
|
129
|
+
desc: "Enable strict validation"
|
|
130
|
+
def validate(file)
|
|
131
|
+
require_relative "commands/validate_command"
|
|
132
|
+
Commands::ValidateCommand.new(file, options).run
|
|
133
|
+
rescue StandardError => e
|
|
134
|
+
error_output "Error: #{e.message}"
|
|
135
|
+
exit 1
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
desc "batch DIR", "Process multiple files in a directory"
|
|
139
|
+
option :output_dir, type: :string, required: true,
|
|
140
|
+
desc: "Output directory for generated files"
|
|
141
|
+
option :format, type: :string, default: "svg",
|
|
142
|
+
desc: "Output format for all files"
|
|
143
|
+
option :algorithm, type: :string, default: "layered",
|
|
144
|
+
desc: "Layout algorithm to use"
|
|
145
|
+
def batch(directory)
|
|
146
|
+
require_relative "commands/batch_command"
|
|
147
|
+
Commands::BatchCommand.new(directory, options).run
|
|
148
|
+
rescue StandardError => e
|
|
149
|
+
error_output "Error: #{e.message}"
|
|
150
|
+
exit 1
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
desc "version", "Show elkrb version"
|
|
154
|
+
def version
|
|
155
|
+
say "elkrb version #{Elkrb::VERSION}", :green
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
private
|
|
159
|
+
|
|
160
|
+
def read_input_file(file)
|
|
161
|
+
require_relative "graph/graph"
|
|
162
|
+
content = File.read(file)
|
|
163
|
+
|
|
164
|
+
case File.extname(file).downcase
|
|
165
|
+
when ".json"
|
|
166
|
+
Elkrb::Graph::Graph.from_json(content)
|
|
167
|
+
when ".yml", ".yaml"
|
|
168
|
+
Elkrb::Graph::Graph.from_yaml(content)
|
|
169
|
+
else
|
|
170
|
+
# Try JSON first, then YAML
|
|
171
|
+
begin
|
|
172
|
+
Elkrb::Graph::Graph.from_json(content)
|
|
173
|
+
rescue JSON::ParserError
|
|
174
|
+
Elkrb::Graph::Graph.from_yaml(content)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_layout_options
|
|
180
|
+
opts = { algorithm: options[:algorithm] }
|
|
181
|
+
|
|
182
|
+
# Add spacing options
|
|
183
|
+
opts[:spacing_node_node] = options[:spacing] if options[:spacing]
|
|
184
|
+
opts[:layer_spacing] = options[:layer_spacing] if options[:layer_spacing]
|
|
185
|
+
|
|
186
|
+
# Add padding options
|
|
187
|
+
if options[:padding_top] || options[:padding_bottom] ||
|
|
188
|
+
options[:padding_left] || options[:padding_right]
|
|
189
|
+
opts[:padding] = {
|
|
190
|
+
top: options[:padding_top] || 12,
|
|
191
|
+
bottom: options[:padding_bottom] || 12,
|
|
192
|
+
left: options[:padding_left] || 12,
|
|
193
|
+
right: options[:padding_right] || 12,
|
|
194
|
+
}
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
opts
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def output_result(result)
|
|
201
|
+
output = case options[:format]
|
|
202
|
+
when "yaml"
|
|
203
|
+
result.to_yaml
|
|
204
|
+
else
|
|
205
|
+
result.to_json
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
if options[:output]
|
|
209
|
+
File.write(options[:output], output)
|
|
210
|
+
verbose_output "Output written to #{options[:output]}"
|
|
211
|
+
else
|
|
212
|
+
say output
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def verbose_output(message)
|
|
217
|
+
say message, :yellow if options[:verbose]
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def error_output(message)
|
|
221
|
+
say message, :red
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
module Elkrb
|
|
6
|
+
module Commands
|
|
7
|
+
# Command for processing multiple graph files in batch
|
|
8
|
+
# Useful for generating diagrams for entire directories of graph files
|
|
9
|
+
class BatchCommand
|
|
10
|
+
def initialize(directory, options)
|
|
11
|
+
@directory = directory
|
|
12
|
+
@options = options
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
unless Dir.exist?(@directory)
|
|
17
|
+
raise ArgumentError,
|
|
18
|
+
"Directory not found: #{@directory}"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Find all graph files
|
|
22
|
+
pattern = File.join(@directory, "*.{json,yml,yaml,elkt}")
|
|
23
|
+
files = Dir.glob(pattern, File::FNM_EXTGLOB)
|
|
24
|
+
|
|
25
|
+
if files.empty?
|
|
26
|
+
puts "No graph files found in #{@directory}"
|
|
27
|
+
return
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Create output directory
|
|
31
|
+
FileUtils.mkdir_p(@options[:output_dir])
|
|
32
|
+
|
|
33
|
+
# Process each file
|
|
34
|
+
success_count = 0
|
|
35
|
+
error_count = 0
|
|
36
|
+
|
|
37
|
+
files.each do |file|
|
|
38
|
+
process_file(file)
|
|
39
|
+
success_count += 1
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
error_count += 1
|
|
42
|
+
warn "⚠ Error processing #{file}: #{e.message}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Summary
|
|
46
|
+
puts ""
|
|
47
|
+
puts "✓ Processed #{success_count} file(s) → #{@options[:output_dir]}"
|
|
48
|
+
puts "⚠ #{error_count} error(s)" if error_count.positive?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def process_file(file)
|
|
54
|
+
basename = File.basename(file, File.extname(file))
|
|
55
|
+
output_file = File.join(@options[:output_dir],
|
|
56
|
+
"#{basename}.#{@options[:format]}")
|
|
57
|
+
|
|
58
|
+
# Use DiagramCommand for each file
|
|
59
|
+
opts = @options.merge(output: output_file)
|
|
60
|
+
|
|
61
|
+
require_relative "diagram_command"
|
|
62
|
+
DiagramCommand.new(file, opts).run
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Elkrb
|
|
8
|
+
module Commands
|
|
9
|
+
# Command for converting between graph formats
|
|
10
|
+
# Supports JSON, YAML, DOT, and ELKT formats
|
|
11
|
+
class ConvertCommand
|
|
12
|
+
def initialize(file, options)
|
|
13
|
+
@file = file
|
|
14
|
+
@options = options
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
# Load source file
|
|
19
|
+
graph = load_any_format(@file)
|
|
20
|
+
|
|
21
|
+
# Detect target format
|
|
22
|
+
target_format = detect_format(@options[:output])
|
|
23
|
+
|
|
24
|
+
# Convert
|
|
25
|
+
content = export_to_format(graph, target_format)
|
|
26
|
+
|
|
27
|
+
# Write output
|
|
28
|
+
write_output(content, @options[:output])
|
|
29
|
+
|
|
30
|
+
puts "✓ Converted #{@file} → #{@options[:output]} (#{target_format})"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def load_any_format(file)
|
|
36
|
+
raise ArgumentError, "File not found: #{file}" unless File.exist?(file)
|
|
37
|
+
|
|
38
|
+
content = File.read(file)
|
|
39
|
+
ext = File.extname(file).downcase
|
|
40
|
+
|
|
41
|
+
case ext
|
|
42
|
+
when ".json"
|
|
43
|
+
require_relative "../graph/graph"
|
|
44
|
+
Elkrb::Graph::Graph.from_json(content)
|
|
45
|
+
when ".yml", ".yaml"
|
|
46
|
+
require_relative "../graph/graph"
|
|
47
|
+
Elkrb::Graph::Graph.from_yaml(content)
|
|
48
|
+
when ".elkt"
|
|
49
|
+
require_relative "../parsers/elkt_parser"
|
|
50
|
+
Elkrb::Parsers::ElktParser.parse(content)
|
|
51
|
+
when ".dot", ".gv"
|
|
52
|
+
raise ArgumentError,
|
|
53
|
+
"DOT format input not yet supported. Use JSON, YAML, or ELKT."
|
|
54
|
+
else
|
|
55
|
+
detect_and_parse(content)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def detect_and_parse(content)
|
|
60
|
+
require_relative "../graph/graph"
|
|
61
|
+
|
|
62
|
+
# Try JSON first
|
|
63
|
+
begin
|
|
64
|
+
return Elkrb::Graph::Graph.from_json(content)
|
|
65
|
+
rescue JSON::ParserError
|
|
66
|
+
# Not JSON
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Try YAML
|
|
70
|
+
begin
|
|
71
|
+
return Elkrb::Graph::Graph.from_yaml(content)
|
|
72
|
+
rescue Psych::SyntaxError
|
|
73
|
+
# Not YAML
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Try ELKT
|
|
77
|
+
begin
|
|
78
|
+
require_relative "../parsers/elkt_parser"
|
|
79
|
+
Elkrb::Parsers::ElktParser.parse(content)
|
|
80
|
+
rescue StandardError
|
|
81
|
+
raise ArgumentError,
|
|
82
|
+
"Unable to parse input file. Supported formats: JSON, YAML, ELKT"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def detect_format(filename)
|
|
87
|
+
ext = File.extname(filename).downcase
|
|
88
|
+
|
|
89
|
+
case ext
|
|
90
|
+
when ".json" then :json
|
|
91
|
+
when ".yml", ".yaml" then :yaml
|
|
92
|
+
when ".dot", ".gv" then :dot
|
|
93
|
+
when ".elkt" then :elkt
|
|
94
|
+
else
|
|
95
|
+
# Use explicit format option if provided
|
|
96
|
+
if @options[:format]
|
|
97
|
+
@options[:format].to_sym
|
|
98
|
+
else
|
|
99
|
+
raise ArgumentError,
|
|
100
|
+
"Cannot detect output format from extension: #{ext}"
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def export_to_format(graph, format)
|
|
106
|
+
case format
|
|
107
|
+
when :json
|
|
108
|
+
graph.to_json
|
|
109
|
+
when :yaml
|
|
110
|
+
graph.to_yaml
|
|
111
|
+
when :dot
|
|
112
|
+
require_relative "../serializers/dot_serializer"
|
|
113
|
+
Elkrb::Serializers::DotSerializer.new.serialize(graph)
|
|
114
|
+
when :elkt
|
|
115
|
+
require_relative "../serializers/elkt_serializer"
|
|
116
|
+
Elkrb::Serializers::ElktSerializer.new.serialize(graph)
|
|
117
|
+
else
|
|
118
|
+
raise ArgumentError, "Unsupported output format: #{format}"
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def write_output(content, filename)
|
|
123
|
+
dir = File.dirname(filename)
|
|
124
|
+
FileUtils.mkdir_p(dir)
|
|
125
|
+
|
|
126
|
+
File.write(filename, content)
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "yaml"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
|
|
7
|
+
module Elkrb
|
|
8
|
+
module Commands
|
|
9
|
+
# Command for creating diagrams from ELK graph files
|
|
10
|
+
# Supports multiple input formats (JSON, YAML, ELKT) and output formats (DOT, PNG, SVG, PDF)
|
|
11
|
+
class DiagramCommand
|
|
12
|
+
def initialize(file, options)
|
|
13
|
+
@file = file
|
|
14
|
+
@options = options
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run
|
|
18
|
+
# Load graph
|
|
19
|
+
graph = load_graph(@file)
|
|
20
|
+
|
|
21
|
+
# Apply layout
|
|
22
|
+
layout_options = build_layout_options
|
|
23
|
+
result = Elkrb::Layout::LayoutEngine.layout(graph, layout_options)
|
|
24
|
+
|
|
25
|
+
# Determine output format
|
|
26
|
+
output_format = detect_format(@options[:output])
|
|
27
|
+
|
|
28
|
+
# Export to format
|
|
29
|
+
content = export_to_format(result, output_format)
|
|
30
|
+
|
|
31
|
+
# Write output
|
|
32
|
+
write_output(content, @options[:output])
|
|
33
|
+
|
|
34
|
+
# Render to image if needed
|
|
35
|
+
if image_format?(output_format)
|
|
36
|
+
render_to_image(@options[:output], output_format)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Preview if requested
|
|
40
|
+
preview(@options[:output]) if @options[:preview]
|
|
41
|
+
|
|
42
|
+
puts "✓ Diagram created: #{@options[:output]}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def load_graph(file)
|
|
48
|
+
raise ArgumentError, "File not found: #{file}" unless File.exist?(file)
|
|
49
|
+
|
|
50
|
+
content = File.read(file)
|
|
51
|
+
ext = File.extname(file).downcase
|
|
52
|
+
|
|
53
|
+
case ext
|
|
54
|
+
when ".json"
|
|
55
|
+
require_relative "../graph/graph"
|
|
56
|
+
Elkrb::Graph::Graph.from_json(content)
|
|
57
|
+
when ".yml", ".yaml"
|
|
58
|
+
require_relative "../graph/graph"
|
|
59
|
+
Elkrb::Graph::Graph.from_yaml(content)
|
|
60
|
+
when ".elkt"
|
|
61
|
+
require_relative "../parsers/elkt_parser"
|
|
62
|
+
Elkrb::Parsers::ElktParser.parse(content)
|
|
63
|
+
else
|
|
64
|
+
detect_and_parse(content)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def detect_and_parse(content)
|
|
69
|
+
require_relative "../graph/graph"
|
|
70
|
+
|
|
71
|
+
# Try JSON first
|
|
72
|
+
begin
|
|
73
|
+
return Elkrb::Graph::Graph.from_json(content)
|
|
74
|
+
rescue JSON::ParserError
|
|
75
|
+
# Not JSON
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Try YAML
|
|
79
|
+
begin
|
|
80
|
+
return Elkrb::Graph::Graph.from_yaml(content)
|
|
81
|
+
rescue Psych::SyntaxError
|
|
82
|
+
# Not YAML
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Try ELKT
|
|
86
|
+
begin
|
|
87
|
+
require_relative "../parsers/elkt_parser"
|
|
88
|
+
Elkrb::Parsers::ElktParser.parse(content)
|
|
89
|
+
rescue StandardError
|
|
90
|
+
raise ArgumentError,
|
|
91
|
+
"Unable to parse input file. Supported formats: JSON, YAML, ELKT"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def build_layout_options
|
|
96
|
+
opts = {}
|
|
97
|
+
|
|
98
|
+
opts[:algorithm] = @options[:algorithm] if @options[:algorithm]
|
|
99
|
+
opts[:direction] = @options[:direction] if @options[:direction]
|
|
100
|
+
opts[:spacing_node_node] = @options[:spacing] if @options[:spacing]
|
|
101
|
+
opts[:edge_routing] = @options[:edge_routing] if @options[:edge_routing]
|
|
102
|
+
|
|
103
|
+
opts
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def detect_format(filename)
|
|
107
|
+
ext = File.extname(filename).downcase
|
|
108
|
+
|
|
109
|
+
case ext
|
|
110
|
+
when ".json" then :json
|
|
111
|
+
when ".yml", ".yaml" then :yaml
|
|
112
|
+
when ".dot", ".gv" then :dot
|
|
113
|
+
when ".elkt" then :elkt
|
|
114
|
+
when ".png" then :png
|
|
115
|
+
when ".svg" then :svg
|
|
116
|
+
when ".pdf" then :pdf
|
|
117
|
+
when ".ps" then :ps
|
|
118
|
+
when ".eps" then :eps
|
|
119
|
+
else
|
|
120
|
+
# Use explicit format option if provided
|
|
121
|
+
if @options[:format]
|
|
122
|
+
@options[:format].to_sym
|
|
123
|
+
else
|
|
124
|
+
:dot # Default to DOT
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def export_to_format(result, format)
|
|
130
|
+
case format
|
|
131
|
+
when :json
|
|
132
|
+
# Use Lutaml-model's to_json for proper serialization
|
|
133
|
+
result.to_json
|
|
134
|
+
when :yaml
|
|
135
|
+
# Use Lutaml-model's to_yaml for proper serialization
|
|
136
|
+
result.to_yaml
|
|
137
|
+
when :dot, :png, :svg, :pdf, :ps, :eps
|
|
138
|
+
require_relative "../serializers/dot_serializer"
|
|
139
|
+
Elkrb::Serializers::DotSerializer.new.serialize(result)
|
|
140
|
+
when :elkt
|
|
141
|
+
require_relative "../serializers/elkt_serializer"
|
|
142
|
+
Elkrb::Serializers::ElktSerializer.new.serialize(result)
|
|
143
|
+
else
|
|
144
|
+
raise ArgumentError, "Unsupported format: #{format}"
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def write_output(content, filename)
|
|
149
|
+
dir = File.dirname(filename)
|
|
150
|
+
FileUtils.mkdir_p(dir)
|
|
151
|
+
|
|
152
|
+
File.write(filename, content)
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def image_format?(format)
|
|
156
|
+
%i[png svg pdf ps eps].include?(format)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def render_to_image(output_file, format)
|
|
160
|
+
# For image formats, we need to render DOT -> image
|
|
161
|
+
# First write DOT to temp file
|
|
162
|
+
dot_file = "#{output_file}.tmp.dot"
|
|
163
|
+
|
|
164
|
+
# Re-read the content we just wrote (which is DOT)
|
|
165
|
+
dot_content = File.read(output_file)
|
|
166
|
+
|
|
167
|
+
# If output is not already DOT, we need to export it
|
|
168
|
+
if File.extname(output_file).downcase == ".dot"
|
|
169
|
+
dot_file = output_file
|
|
170
|
+
else
|
|
171
|
+
File.write(dot_file, dot_content)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# Render using Graphviz
|
|
175
|
+
begin
|
|
176
|
+
require_relative "../graphviz_wrapper"
|
|
177
|
+
graphviz = Elkrb::GraphvizWrapper.new
|
|
178
|
+
|
|
179
|
+
unless graphviz.available?
|
|
180
|
+
warn "⚠ Graphviz not found. Cannot render to #{format}."
|
|
181
|
+
warn " Install Graphviz or export to DOT format instead."
|
|
182
|
+
return
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
graphviz.render(dot_file, output_file, format, engine: "dot", dpi: 96)
|
|
186
|
+
|
|
187
|
+
# Clean up temp DOT file if we created one
|
|
188
|
+
File.delete(dot_file) if dot_file != output_file && File.exist?(dot_file)
|
|
189
|
+
rescue Elkrb::GraphvizWrapper::GraphvizNotFoundError => e
|
|
190
|
+
warn "⚠ #{e.message}"
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def preview(file)
|
|
195
|
+
case RbConfig::CONFIG["host_os"]
|
|
196
|
+
when /darwin/
|
|
197
|
+
system("open", file)
|
|
198
|
+
when /linux/
|
|
199
|
+
system("xdg-open", file)
|
|
200
|
+
when /mswin|mingw|cygwin/
|
|
201
|
+
system("start", file)
|
|
202
|
+
else
|
|
203
|
+
warn "Preview not supported on this platform"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Elkrb
|
|
4
|
+
module Commands
|
|
5
|
+
# Command for rendering DOT files to images using Graphviz
|
|
6
|
+
class RenderCommand
|
|
7
|
+
def initialize(dot_file, options)
|
|
8
|
+
@dot_file = dot_file
|
|
9
|
+
@options = options
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
unless File.exist?(@dot_file)
|
|
14
|
+
raise ArgumentError,
|
|
15
|
+
"File not found: #{@dot_file}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Detect image format
|
|
19
|
+
format = detect_image_format(@options[:output])
|
|
20
|
+
|
|
21
|
+
# Render using Graphviz
|
|
22
|
+
require_relative "../graphviz_wrapper"
|
|
23
|
+
graphviz = Elkrb::GraphvizWrapper.new
|
|
24
|
+
|
|
25
|
+
engine = @options[:engine] || "dot"
|
|
26
|
+
dpi = @options[:dpi] || 96
|
|
27
|
+
|
|
28
|
+
graphviz.render(@dot_file, @options[:output], format, engine: engine,
|
|
29
|
+
dpi: dpi)
|
|
30
|
+
|
|
31
|
+
puts "✓ Rendered #{@dot_file} → #{@options[:output]}"
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def detect_image_format(filename)
|
|
37
|
+
ext = File.extname(filename).downcase
|
|
38
|
+
|
|
39
|
+
case ext
|
|
40
|
+
when ".png" then :png
|
|
41
|
+
when ".svg" then :svg
|
|
42
|
+
when ".pdf" then :pdf
|
|
43
|
+
when ".ps" then :ps
|
|
44
|
+
when ".eps" then :eps
|
|
45
|
+
else
|
|
46
|
+
raise ArgumentError, "Cannot detect image format from extension: #{ext}. " \
|
|
47
|
+
"Supported: .png, .svg, .pdf, .ps, .eps"
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|