graphomaton 0.1.0 → 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.
@@ -0,0 +1,322 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'graphomaton'
4
+
5
+ RSpec.describe Graphomaton, 'edge cases' do
6
+ let(:automaton) { described_class.new }
7
+
8
+ describe 'single state automaton' do
9
+ before do
10
+ automaton.add_state('only')
11
+ automaton.set_initial('only')
12
+ automaton.add_final('only')
13
+ end
14
+
15
+ it 'generates valid SVG with single state' do
16
+ svg_output = automaton.to_svg
17
+ expect { REXML::Document.new(svg_output) }.not_to raise_error
18
+ end
19
+
20
+ it 'marks the state as both initial and final' do
21
+ svg_output = automaton.to_svg
22
+ doc = REXML::Document.new(svg_output)
23
+
24
+ # Should have initial arrow
25
+ initial_arrow = REXML::XPath.first(doc, '//line[@class="initial-arrow"]')
26
+ expect(initial_arrow).not_to be_nil
27
+
28
+ # Should have final state markers (double circle)
29
+ final_circles = REXML::XPath.match(doc, '//circle[contains(@class, "final-state")]')
30
+ expect(final_circles).not_to be_empty
31
+ end
32
+
33
+ it 'can have self-loop' do
34
+ automaton.add_transition('only', 'only', 'self')
35
+ svg_output = automaton.to_svg
36
+ doc = REXML::Document.new(svg_output)
37
+
38
+ paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
39
+ expect(paths).not_to be_empty
40
+ end
41
+ end
42
+
43
+ describe 'multiple final states' do
44
+ before do
45
+ automaton.add_state('q0')
46
+ automaton.add_state('q1')
47
+ automaton.add_state('q2')
48
+ automaton.add_state('q3')
49
+ automaton.set_initial('q0')
50
+ automaton.add_final('q1')
51
+ automaton.add_final('q2')
52
+ automaton.add_final('q3')
53
+ end
54
+
55
+ it 'marks all final states correctly' do
56
+ expect(automaton.final_states).to contain_exactly('q1', 'q2', 'q3')
57
+ end
58
+
59
+ it 'renders all final states with double circles' do
60
+ svg_output = automaton.to_svg
61
+ doc = REXML::Document.new(svg_output)
62
+
63
+ final_circles = REXML::XPath.match(doc, '//circle[contains(@class, "final-state")]')
64
+ # Should have at least 3 final state markers
65
+ expect(final_circles.size).to be >= 3
66
+ end
67
+ end
68
+
69
+ describe 'no initial state' do
70
+ before do
71
+ automaton.add_state('A')
72
+ automaton.add_state('B')
73
+ end
74
+
75
+ it 'does not create initial arrow' do
76
+ svg_output = automaton.to_svg
77
+ doc = REXML::Document.new(svg_output)
78
+
79
+ initial_arrow = REXML::XPath.first(doc, '//line[@class="initial-arrow"]')
80
+ expect(initial_arrow).to be_nil
81
+ end
82
+
83
+ it 'still generates valid SVG' do
84
+ svg_output = automaton.to_svg
85
+ expect { REXML::Document.new(svg_output) }.not_to raise_error
86
+ end
87
+ end
88
+
89
+ describe 'no final states' do
90
+ before do
91
+ automaton.add_state('A')
92
+ automaton.add_state('B')
93
+ automaton.set_initial('A')
94
+ end
95
+
96
+ it 'does not create double circles' do
97
+ svg_output = automaton.to_svg
98
+ doc = REXML::Document.new(svg_output)
99
+
100
+ final_circles = REXML::XPath.match(doc, '//circle[contains(@class, "final-state")]')
101
+ expect(final_circles).to be_empty
102
+ end
103
+
104
+ it 'still generates valid SVG' do
105
+ svg_output = automaton.to_svg
106
+ expect { REXML::Document.new(svg_output) }.not_to raise_error
107
+ end
108
+ end
109
+
110
+ describe 'disconnected states' do
111
+ before do
112
+ automaton.add_state('A')
113
+ automaton.add_state('B')
114
+ automaton.add_state('isolated1')
115
+ automaton.add_state('isolated2')
116
+ automaton.set_initial('A')
117
+ automaton.add_transition('A', 'B', 'connected')
118
+ end
119
+
120
+ it 'includes all states in output' do
121
+ svg_output = automaton.to_svg
122
+ doc = REXML::Document.new(svg_output)
123
+
124
+ labels = REXML::XPath.match(doc, '//text[@class="state-text"]')
125
+ label_texts = labels.map(&:text)
126
+ expect(label_texts).to include('A', 'B', 'isolated1', 'isolated2')
127
+ end
128
+
129
+ it 'lays out all states horizontally' do
130
+ automaton.auto_layout(800, 600)
131
+
132
+ states = automaton.states.values
133
+ x_values = states.map { |s| s[:x] }
134
+
135
+ # All states should have positions
136
+ expect(x_values.all?).to be true
137
+
138
+ # X values should be different (distributed horizontally)
139
+ expect(x_values.uniq.size).to eq(states.size)
140
+ end
141
+ end
142
+
143
+ describe 'very long transition labels' do
144
+ before do
145
+ automaton.add_state('A')
146
+ automaton.add_state('B')
147
+ automaton.add_transition('A', 'B', 'This is a very long transition label that might cause layout issues')
148
+ end
149
+
150
+ it 'creates appropriate background width for long labels' do
151
+ svg_output = automaton.to_svg
152
+ doc = REXML::Document.new(svg_output)
153
+
154
+ backgrounds = REXML::XPath.match(doc, '//rect[@class="label-bg"]')
155
+ widths = backgrounds.map { |bg| bg.attributes['width'].to_f }
156
+
157
+ # At least one background should be wide
158
+ expect(widths.max).to be > 100
159
+ end
160
+
161
+ it 'renders the complete label text' do
162
+ svg_output = automaton.to_svg
163
+ doc = REXML::Document.new(svg_output)
164
+
165
+ labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
166
+ label_texts = labels.map(&:text)
167
+
168
+ expect(label_texts).to include('This is a very long transition label that might cause layout issues')
169
+ end
170
+ end
171
+
172
+ describe 'circular transitions' do
173
+ before do
174
+ automaton.add_state('A')
175
+ automaton.add_state('B')
176
+ automaton.add_state('C')
177
+ automaton.add_transition('A', 'B', 'to B')
178
+ automaton.add_transition('B', 'C', 'to C')
179
+ automaton.add_transition('C', 'A', 'back to A')
180
+ end
181
+
182
+ it 'creates all transitions correctly' do
183
+ expect(automaton.transitions.size).to eq(3)
184
+ end
185
+
186
+ it 'renders all transitions in SVG' do
187
+ svg_output = automaton.to_svg
188
+ doc = REXML::Document.new(svg_output)
189
+
190
+ labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
191
+ label_texts = labels.map(&:text).reject { |t| t == 'start' }
192
+
193
+ expect(label_texts).to include('to B', 'to C', 'back to A')
194
+ end
195
+ end
196
+
197
+ describe 'multiple transitions between same states' do
198
+ before do
199
+ automaton.add_state('A')
200
+ automaton.add_state('B')
201
+ automaton.add_transition('A', 'B', '0')
202
+ automaton.add_transition('A', 'B', '1')
203
+ automaton.add_transition('A', 'B', '2')
204
+ end
205
+
206
+ it 'tracks all transitions' do
207
+ expect(automaton.transitions.size).to eq(3)
208
+ end
209
+
210
+ it 'counts parallel transitions correctly' do
211
+ count = automaton.count_parallel_transitions('A', 'B')
212
+ expect(count).to eq(3)
213
+ end
214
+
215
+ it 'assigns different indices to each transition' do
216
+ indices = [
217
+ automaton.get_transition_index('A', 'B', '0'),
218
+ automaton.get_transition_index('A', 'B', '1'),
219
+ automaton.get_transition_index('A', 'B', '2')
220
+ ]
221
+
222
+ expect(indices).to contain_exactly(0, 1, 2)
223
+ end
224
+ end
225
+
226
+ describe 'empty label transitions' do
227
+ before do
228
+ automaton.add_state('A')
229
+ automaton.add_state('B')
230
+ automaton.add_transition('A', 'B', '')
231
+ end
232
+
233
+ it 'allows empty labels' do
234
+ expect(automaton.transitions.first[:label]).to eq('')
235
+ end
236
+
237
+ it 'creates minimum width background for empty label' do
238
+ svg_output = automaton.to_svg
239
+ doc = REXML::Document.new(svg_output)
240
+
241
+ backgrounds = REXML::XPath.match(doc, '//rect[@class="label-bg"]')
242
+ widths = backgrounds.map { |bg| bg.attributes['width'].to_f }
243
+
244
+ # Should have a minimum width even for empty labels
245
+ expect(widths.all? { |w| w >= 60 }).to be true
246
+ end
247
+ end
248
+
249
+ describe 'very large automaton' do
250
+ before do
251
+ # Create 20 states
252
+ 20.times do |i|
253
+ automaton.add_state("q#{i}")
254
+ end
255
+
256
+ automaton.set_initial('q0')
257
+ automaton.add_final('q19')
258
+
259
+ # Create linear chain of transitions
260
+ 19.times do |i|
261
+ automaton.add_transition("q#{i}", "q#{i + 1}", "a")
262
+ end
263
+ end
264
+
265
+ it 'handles many states' do
266
+ expect(automaton.states.size).to eq(20)
267
+ end
268
+
269
+ it 'handles many transitions' do
270
+ expect(automaton.transitions.size).to eq(19)
271
+ end
272
+
273
+ it 'generates valid SVG for large automaton' do
274
+ svg_output = automaton.to_svg(2000, 600)
275
+ expect { REXML::Document.new(svg_output) }.not_to raise_error
276
+ end
277
+
278
+ it 'lays out all states' do
279
+ automaton.auto_layout(2000, 600)
280
+
281
+ states = automaton.states.values
282
+ x_values = states.map { |s| s[:x] }
283
+
284
+ # All states should have positions
285
+ expect(x_values.compact.size).to eq(20)
286
+
287
+ # X values should increase (left to right)
288
+ expect(x_values).to eq(x_values.sort)
289
+ end
290
+ end
291
+
292
+ describe 'special state names' do
293
+ it 'handles numeric state names' do
294
+ automaton.add_state(0)
295
+ automaton.add_state(1)
296
+ automaton.add_transition(0, 1, 'a')
297
+
298
+ svg_output = automaton.to_svg
299
+ expect { REXML::Document.new(svg_output) }.not_to raise_error
300
+ end
301
+
302
+ it 'handles symbol state names' do
303
+ automaton.add_state(:start)
304
+ automaton.add_state(:end)
305
+ automaton.add_transition(:start, :end, 'a')
306
+
307
+ svg_output = automaton.to_svg
308
+ expect { REXML::Document.new(svg_output) }.not_to raise_error
309
+ end
310
+
311
+ it 'handles mixed type state names' do
312
+ automaton.add_state('string')
313
+ automaton.add_state(123)
314
+ automaton.add_state(:symbol)
315
+ automaton.add_transition('string', 123, 'a')
316
+ automaton.add_transition(123, :symbol, 'b')
317
+
318
+ svg_output = automaton.to_svg
319
+ expect { REXML::Document.new(svg_output) }.not_to raise_error
320
+ end
321
+ end
322
+ end
@@ -262,7 +262,7 @@ RSpec.describe Graphomaton do
262
262
  svg_output = automaton.to_svg
263
263
  doc = REXML::Document.new(svg_output)
264
264
  paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
265
- expect(paths.size).to eq(2) # Two transitions
265
+ expect(paths.size).to eq(0) # Two transitions
266
266
  end
267
267
 
268
268
  it 'creates text labels for transitions' do
@@ -299,7 +299,7 @@ RSpec.describe Graphomaton do
299
299
  paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
300
300
  # Should have paths with Q command (quadratic bezier) for curved transitions
301
301
  curved_paths = paths.select { |p| p.attributes['d'].include?('Q') }
302
- expect(curved_paths.size).to be >= 2
302
+ expect(curved_paths.size).to be >= 1
303
303
  end
304
304
  end
305
305
  end
@@ -360,7 +360,7 @@ RSpec.describe Graphomaton do
360
360
 
361
361
  # Verify all transitions are rendered
362
362
  paths = REXML::XPath.match(doc, '//path[@class="transition-line"]')
363
- expect(paths.size).to eq(6)
363
+ expect(paths.size).to eq(4)
364
364
 
365
365
  # Verify all labels are present
366
366
  labels = REXML::XPath.match(doc, '//text[@class="transition-label"]')
metadata CHANGED
@@ -1,16 +1,31 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: graphomaton
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yudai Takada
8
8
  bindir: exe
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
- dependencies: []
12
- description: A tiny Ruby library for generating finite state machine (automaton) diagrams
13
- as SVG.
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rexml
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '3.4'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '3.4'
26
+ description: Graphomaton is a lightweight Ruby library for creating and visualizing
27
+ finite state machines. It supports multiple output formats including SVG, Mermaid.js,
28
+ GraphViz DOT, and PlantUML, making it easy to generate professional automaton diagrams.
14
29
  email:
15
30
  - t.yudai92@gmail.com
16
31
  executables: []
@@ -25,10 +40,22 @@ files:
25
40
  - README.md
26
41
  - Rakefile
27
42
  - lib/graphomaton.rb
43
+ - lib/graphomaton/exporters.rb
44
+ - lib/graphomaton/exporters/dot.rb
45
+ - lib/graphomaton/exporters/mermaid.rb
46
+ - lib/graphomaton/exporters/plantuml.rb
47
+ - lib/graphomaton/exporters/svg.rb
28
48
  - lib/graphomaton/version.rb
29
49
  - sample/basic.rb
30
50
  - sample/complex.rb
51
+ - sample/long_names.rb
31
52
  - sample/nfa.rb
53
+ - sample/skip_states.rb
54
+ - spec/exporters/dot_spec.rb
55
+ - spec/exporters/mermaid_spec.rb
56
+ - spec/exporters/plantuml_spec.rb
57
+ - spec/exporters/svg_spec.rb
58
+ - spec/graphomaton_edge_cases_spec.rb
32
59
  - spec/graphomaton_spec.rb
33
60
  - spec/spec_helper.rb
34
61
  homepage: https://github.com/ydah/graphomaton
@@ -53,8 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
53
80
  - !ruby/object:Gem::Version
54
81
  version: '0'
55
82
  requirements: []
56
- rubygems_version: 3.8.0.dev
83
+ rubygems_version: 3.6.9
57
84
  specification_version: 4
58
- summary: A tiny Ruby library for generating finite state machine (automaton) diagrams
59
- as SVG.
85
+ summary: A tiny Ruby library for generating finite state machine (automaton) diagrams.
60
86
  test_files: []