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.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +11 -0
  4. data/Gemfile +13 -0
  5. data/README.adoc +1028 -0
  6. data/Rakefile +64 -0
  7. data/benchmarks/README.md +172 -0
  8. data/benchmarks/elkjs_benchmark.js +140 -0
  9. data/benchmarks/elkrb_benchmark.rb +145 -0
  10. data/benchmarks/fixtures/graphs.json +10777 -0
  11. data/benchmarks/generate_report.rb +241 -0
  12. data/benchmarks/generate_test_graphs.rb +154 -0
  13. data/benchmarks/results/elkrb_results.json +280 -0
  14. data/benchmarks/results/elkrb_summary.json +285 -0
  15. data/elkrb.gemspec +39 -0
  16. data/examples/dot_export_demo.rb +133 -0
  17. data/examples/hierarchical_graph.rb +19 -0
  18. data/examples/layout_constraints_demo.rb +272 -0
  19. data/examples/port_constraints_demo.rb +291 -0
  20. data/examples/self_loop_demo.rb +391 -0
  21. data/examples/simple_graph.rb +50 -0
  22. data/examples/spline_routing_demo.rb +235 -0
  23. data/exe/elkrb +8 -0
  24. data/lib/elkrb/cli.rb +224 -0
  25. data/lib/elkrb/commands/batch_command.rb +66 -0
  26. data/lib/elkrb/commands/convert_command.rb +130 -0
  27. data/lib/elkrb/commands/diagram_command.rb +208 -0
  28. data/lib/elkrb/commands/render_command.rb +52 -0
  29. data/lib/elkrb/commands/validate_command.rb +241 -0
  30. data/lib/elkrb/errors.rb +30 -0
  31. data/lib/elkrb/geometry/bezier.rb +163 -0
  32. data/lib/elkrb/geometry/dimension.rb +32 -0
  33. data/lib/elkrb/geometry/point.rb +68 -0
  34. data/lib/elkrb/geometry/rectangle.rb +86 -0
  35. data/lib/elkrb/geometry/vector.rb +67 -0
  36. data/lib/elkrb/graph/edge.rb +95 -0
  37. data/lib/elkrb/graph/graph.rb +90 -0
  38. data/lib/elkrb/graph/label.rb +45 -0
  39. data/lib/elkrb/graph/layout_options.rb +247 -0
  40. data/lib/elkrb/graph/node.rb +79 -0
  41. data/lib/elkrb/graph/node_constraints.rb +107 -0
  42. data/lib/elkrb/graph/port.rb +104 -0
  43. data/lib/elkrb/graphviz_wrapper.rb +133 -0
  44. data/lib/elkrb/layout/algorithm_registry.rb +57 -0
  45. data/lib/elkrb/layout/algorithms/base_algorithm.rb +208 -0
  46. data/lib/elkrb/layout/algorithms/box.rb +47 -0
  47. data/lib/elkrb/layout/algorithms/disco.rb +206 -0
  48. data/lib/elkrb/layout/algorithms/fixed.rb +32 -0
  49. data/lib/elkrb/layout/algorithms/force.rb +165 -0
  50. data/lib/elkrb/layout/algorithms/layered/cycle_breaker.rb +86 -0
  51. data/lib/elkrb/layout/algorithms/layered/layer_assigner.rb +96 -0
  52. data/lib/elkrb/layout/algorithms/layered/node_placer.rb +77 -0
  53. data/lib/elkrb/layout/algorithms/layered.rb +49 -0
  54. data/lib/elkrb/layout/algorithms/libavoid.rb +389 -0
  55. data/lib/elkrb/layout/algorithms/mrtree.rb +144 -0
  56. data/lib/elkrb/layout/algorithms/radial.rb +64 -0
  57. data/lib/elkrb/layout/algorithms/random.rb +43 -0
  58. data/lib/elkrb/layout/algorithms/rectpacking.rb +93 -0
  59. data/lib/elkrb/layout/algorithms/spore_compaction.rb +139 -0
  60. data/lib/elkrb/layout/algorithms/spore_overlap.rb +117 -0
  61. data/lib/elkrb/layout/algorithms/stress.rb +176 -0
  62. data/lib/elkrb/layout/algorithms/topdown_packing.rb +183 -0
  63. data/lib/elkrb/layout/algorithms/vertiflex.rb +174 -0
  64. data/lib/elkrb/layout/constraints/alignment_constraint.rb +150 -0
  65. data/lib/elkrb/layout/constraints/base_constraint.rb +72 -0
  66. data/lib/elkrb/layout/constraints/constraint_processor.rb +134 -0
  67. data/lib/elkrb/layout/constraints/fixed_position_constraint.rb +87 -0
  68. data/lib/elkrb/layout/constraints/layer_constraint.rb +71 -0
  69. data/lib/elkrb/layout/constraints/relative_position_constraint.rb +110 -0
  70. data/lib/elkrb/layout/edge_router.rb +935 -0
  71. data/lib/elkrb/layout/hierarchical_processor.rb +299 -0
  72. data/lib/elkrb/layout/label_placer.rb +338 -0
  73. data/lib/elkrb/layout/layout_engine.rb +170 -0
  74. data/lib/elkrb/layout/port_constraint_processor.rb +173 -0
  75. data/lib/elkrb/options/elk_padding.rb +94 -0
  76. data/lib/elkrb/options/k_vector.rb +100 -0
  77. data/lib/elkrb/options/k_vector_chain.rb +135 -0
  78. data/lib/elkrb/parsers/elkt_parser.rb +248 -0
  79. data/lib/elkrb/serializers/dot_serializer.rb +339 -0
  80. data/lib/elkrb/serializers/elkt_serializer.rb +236 -0
  81. data/lib/elkrb/version.rb +5 -0
  82. data/lib/elkrb.rb +509 -0
  83. data/sig/elkrb/constraints.rbs +114 -0
  84. data/sig/elkrb/geometry.rbs +61 -0
  85. data/sig/elkrb/graph.rbs +112 -0
  86. data/sig/elkrb/layout.rbs +107 -0
  87. data/sig/elkrb/options.rbs +81 -0
  88. data/sig/elkrb.rbs +32 -0
  89. 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