graphomaton 1.0.0 → 1.2.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 (52) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +112 -8
  3. data/README.md +427 -44
  4. data/SECURITY.md +47 -0
  5. data/docs/architecture.md +30 -0
  6. data/docs/cli.md +28 -0
  7. data/docs/custom-exporters.md +39 -0
  8. data/docs/exporters.md +19 -0
  9. data/docs/input-schema.md +27 -0
  10. data/docs/migration-1.1.md +19 -0
  11. data/docs/performance.md +19 -0
  12. data/docs/releasing.md +19 -0
  13. data/exe/graphomaton +9 -0
  14. data/lib/graphomaton/atomic_file.rb +30 -0
  15. data/lib/graphomaton/cli/config.rb +102 -0
  16. data/lib/graphomaton/cli.rb +863 -0
  17. data/lib/graphomaton/errors.rb +11 -0
  18. data/lib/graphomaton/exporter_registry.rb +130 -0
  19. data/lib/graphomaton/exporters/dot.rb +255 -18
  20. data/lib/graphomaton/exporters/mermaid.rb +705 -25
  21. data/lib/graphomaton/exporters/pdf.rb +131 -0
  22. data/lib/graphomaton/exporters/plantuml.rb +250 -13
  23. data/lib/graphomaton/exporters/png.rb +176 -0
  24. data/lib/graphomaton/exporters/svg.rb +2808 -231
  25. data/lib/graphomaton/exporters/webp.rb +185 -0
  26. data/lib/graphomaton/exporters.rb +11 -4
  27. data/lib/graphomaton/identifier_allocator.rb +33 -0
  28. data/lib/graphomaton/input_policy.rb +83 -0
  29. data/lib/graphomaton/layout/force_tree.rb +127 -0
  30. data/lib/graphomaton/model.rb +230 -0
  31. data/lib/graphomaton/process_runner.rb +153 -0
  32. data/lib/graphomaton/url_policy.rb +40 -0
  33. data/lib/graphomaton/version.rb +1 -1
  34. data/lib/graphomaton.rb +2933 -54
  35. data/sig/graphomaton.rbs +129 -0
  36. metadata +38 -25
  37. data/.codespellignore +0 -0
  38. data/.rspec +0 -1
  39. data/CODE_OF_CONDUCT.md +0 -132
  40. data/Rakefile +0 -8
  41. data/sample/basic.rb +0 -30
  42. data/sample/complex.rb +0 -32
  43. data/sample/long_names.rb +0 -20
  44. data/sample/nfa.rb +0 -28
  45. data/sample/skip_states.rb +0 -23
  46. data/spec/exporters/dot_spec.rb +0 -146
  47. data/spec/exporters/mermaid_spec.rb +0 -154
  48. data/spec/exporters/plantuml_spec.rb +0 -144
  49. data/spec/exporters/svg_spec.rb +0 -314
  50. data/spec/graphomaton_edge_cases_spec.rb +0 -322
  51. data/spec/graphomaton_spec.rb +0 -371
  52. data/spec/spec_helper.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 01b9006b89b4b5e93348f4dd47e9c8173ed036a8cffb357a165f68d1048e8be6
4
- data.tar.gz: 2b8c1975ce405f8ffa6a2f550431281cc82605f536b76efa72d6e092e05902bc
3
+ metadata.gz: 168a8c5641230bf2da705a71bbc3156757a0cc820047d47f18c1c69f3ab08f0a
4
+ data.tar.gz: 52152b9aded87cdae3bca6f47bddd5cded06c91f7c7c4ca8450c0e1b51401084
5
5
  SHA512:
6
- metadata.gz: 538893d73e2cdbcfe697bcf194ab259c7c796d6efdb9cd100b810ed7aa5363144819880d6d813ec76dc9350d455bdb232608b7b0ec39c00efa664cba54bcc2bd
7
- data.tar.gz: 41e703f607023ceebd0aba66965084f3466cd60b414a3ba14670cc0f4e6d82391e18cd64d47911d80efa28dbb4f05378ea693de3ebb0461f84a465e9721de9ca
6
+ metadata.gz: 3cb2d71e65478459dc6cd4968cffcaeb13681124143116f73a5c175de394f55dc14b1335b6a2cae111f07903ddbd2235aa9b62fd18fed233bcde2b3f266f0b7e
7
+ data.tar.gz: c86a638ddb2d0c98bb05a39e396abbe541e483de706dba43437a368462cc3f9d2c2d0460b0152780dccf553d3bfa2d25b9f2d2a6dae5dff7a6cfdaa33ce7b5c3
data/CHANGELOG.md CHANGED
@@ -1,16 +1,120 @@
1
- # Change log
1
+ # Changelog
2
2
 
3
- ## Unreleased
3
+ All notable changes to Graphomaton are documented here. The format is based on
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the project
5
+ follows [Semantic Versioning](https://semver.org/).
4
6
 
5
- ## 1.0.0 (2025-12-23)
7
+ ## [Unreleased]
6
8
 
7
- - Add support multiple style outputs, including .dot, mermaid, and .plantuml formats.
8
- - Improve output format for SVG files.
9
+ ## [1.2.0] - 2026-09-08
9
10
 
10
- ## 0.1.1 (2025-08-26)
11
+ ### Added
11
12
 
12
- Fix gemspec dependency declaration for 'rexml'.
13
+ - Added a public GitHub Pages landing page for the project.
14
+ - Added CLI selection for reference, FSM-semantic, DFA, and combined validation
15
+ profiles.
16
+ - Added structured mixed epsilon/symbol labels that retain their semantics
17
+ through Hash, JSON, and YAML round trips.
13
18
 
14
- ## 0.1.0 (2025-08-26)
19
+ ### Changed
20
+
21
+ - Stronger CLI validation profiles now include reference checks, and exporter
22
+ semantic diagnostics distinguish state and transition tooltips.
23
+ - SVG rendering now reuses immutable state snapshots and linear state ordering
24
+ while preserving folded-state kinds and unrelated self-loops.
25
+ - Updated the RubyGems trusted-publishing action to 1.4.1.
26
+
27
+ ### Fixed
28
+
29
+ - Corrected long-word wrapping, explicit-position tracking, duplicate SVG IDs,
30
+ numeric-root style scoping, auto-sized bounds, and stale clipping diagnostics.
31
+ - Enforced final SVG and scaled PNG dimension limits before conversion, including
32
+ overflow and non-real numeric inputs.
33
+ - Made metadata-depth checks independent of Hash insertion order and rejected
34
+ boolean state identifiers consistently at input boundaries.
35
+ - Preserved option-dependent tooltip semantics in strict rendering and retained
36
+ epsilon meaning in array transition labels.
37
+ - Made `--no-clobber` atomic, including theme-gallery output races and friendly
38
+ CLI failures.
39
+
40
+ ### Security
41
+
42
+ - Rejected escaped, comment-obfuscated, and alternate CSS resource functions in
43
+ themes and per-element SVG styles.
44
+ - Ensured timed-out Unix process groups are terminated even when the direct
45
+ parent exits first.
46
+
47
+ ## [1.1.0] - 2026-08-13
48
+
49
+ ### Added
50
+
51
+ - Immutable model records, update/remove APIs, graph revision caches, structured
52
+ labels, diagnostics, validation profiles, exporter capabilities, semantic-loss
53
+ reporting, render results, IO output, and RBS signatures.
54
+ - Obstacle-aware curves, adaptive self-loops, spatial label indexing, Barnes-Hut
55
+ force approximation, deterministic force-layout separation, and convergence
56
+ detection.
57
+ - CLI commands for validation, discovery, diagnostics, health checks, config
58
+ files, shell completions, and man-page output.
59
+ - Bounded stdin/stdout workflows, no-clobber protection, format-specific option
60
+ errors, structured exit statuses, and `--version`.
61
+ - Nonce-based CSP support, trusted local asset inlining for self-contained HTML,
62
+ and deterministic HTML asset loading with localized generated UI.
63
+ - Restricted gem packaging, gem installation smoke tests, renderer integration
64
+ jobs, release automation, and immutable GitHub Actions pins.
65
+
66
+ ### Changed
67
+
68
+ - Curved and orthogonal edges are routed from geometry instead of insertion
69
+ order, and SVG viewBoxes include rendered paths, shapes, rotated labels, and
70
+ text content.
71
+ - PNG scaling is separated from logical layout and SVG geometry; layout
72
+ diagnostics and metadata survive PNG, PDF, and WebP conversion.
73
+ - SCC analysis is iterative and linear-time. Fixed manual positions remain clear
74
+ of automatic layouts, and dense graphs receive adaptive spacing.
75
+ - Structured transition labels and format-independent pseudostate kinds are kept
76
+ in the model, while custom exporter registration now produces renderable
77
+ exporters and rejects ambiguous schema aliases.
78
+ - HTML pan/zoom controls are more accessible, and text exporters terminate
79
+ output with a newline after normalizing CR/LF label boundaries.
80
+
81
+ ### Fixed
82
+
83
+ - Corrected force-layout attraction direction, boundary clamping, and analysis
84
+ of undefined transition endpoints.
85
+ - Isolated states are declared in DOT, Mermaid, and PlantUML output.
86
+ - DOT, Mermaid, PlantUML, and Graphviz layout identifiers are collision-free,
87
+ including reserved names, hostile text, mixed-type model IDs, and groups.
88
+ - SVG parallel-transition merging preserves presentation metadata and uses
89
+ tuple endpoint keys; edge endpoints now respect ellipse, diamond, bar, and
90
+ rounded-rectangle boundaries.
91
+ - Input validation now rejects duplicate states, malformed transition tuples,
92
+ conflicting initial states, invalid state hierarchies, ignored value types,
93
+ and non-finite rendering numbers.
94
+ - Partial state upserts preserve omitted coordinates and effective no-op updates
95
+ do not advance the graph revision.
96
+
97
+ ### Security
98
+
99
+ - Added URL, HTML JavaScript, theme, and SVG style policies, including safe
100
+ handling of JavaScript strings, trusted local assets, Windows asset paths, and
101
+ unsafe URL schemes; Mermaid.js is pinned to 10.9.8.
102
+ - External processes are bounded by timeout and stdout/stderr limits, with
103
+ portable executable discovery and process termination on Windows.
104
+ - JSON and YAML input, state and transition counts, metadata depth, label size,
105
+ hierarchy depth, and converter resources are bounded.
106
+ - Exported files are written atomically, and embedded scripts enforce size,
107
+ encoding, and digest checks.
108
+
109
+ ## [1.0.0] - 2025-12-23
110
+
111
+ - Add support for multiple output styles, including DOT, Mermaid, and PlantUML.
112
+ - Improve SVG output formatting.
113
+
114
+ ## [0.1.1] - 2025-08-26
115
+
116
+ - Fix the gemspec dependency declaration for `rexml`.
117
+
118
+ ## [0.1.0] - 2025-08-26
15
119
 
16
120
  - Initial release
data/README.md CHANGED
@@ -1,105 +1,488 @@
1
1
  # Graphomaton [![Gem Version](https://badge.fury.io/rb/graphomaton.svg?icon=si%3Arubygems)](https://badge.fury.io/rb/graphomaton) [![CI](https://github.com/ydah/graphomaton/actions/workflows/ci.yml/badge.svg)](https://github.com/ydah/graphomaton/actions/workflows/ci.yml)
2
2
 
3
- A tiny Ruby library for generating finite state machine (automaton) diagrams in multiple formats: SVG, HTML (Mermaid.js), GraphViz (DOT), and PlantUML.
3
+ A Ruby toolkit for validating, analyzing, laying out, and exporting finite state machines as SVG, PNG, PDF, WebP, Mermaid.js HTML, GraphViz DOT, and PlantUML.
4
4
 
5
5
  ![Image](https://github.com/user-attachments/assets/6907869c-1077-4a73-8394-4117f25adc17)
6
6
 
7
7
  ## Installation
8
8
 
9
- Add this line to your application's Gemfile:
10
-
11
9
  ```ruby
12
10
  gem 'graphomaton'
13
11
  ```
14
12
 
15
- And then execute:
16
-
17
13
  ```bash
18
14
  bundle install
19
- ```
20
-
21
- Or install it yourself as:
22
-
23
- ```bash
15
+ # or
24
16
  gem install graphomaton
25
17
  ```
26
18
 
27
- ## Usage
19
+ ## Quick start
28
20
 
29
21
  ```ruby
30
22
  require 'graphomaton'
31
23
 
32
- # Create a DFA that accepts strings ending with 'ab'
33
24
  automaton = Graphomaton.new
34
-
35
- # Add states
36
- automaton.add_state('q0')
25
+ automaton.add_state('q0', label: 'Start')
37
26
  automaton.add_state('q1')
38
- automaton.add_state('q2')
39
-
40
- # Set initial and final states
27
+ automaton.add_state('q2', label: 'Accept')
41
28
  automaton.set_initial('q0')
42
29
  automaton.add_final('q2')
43
30
 
44
- # Add transitions
45
31
  automaton.add_transition('q0', 'q1', 'a')
46
32
  automaton.add_transition('q1', 'q2', 'b')
47
- automaton.add_transition('q0', 'q0', 'b')
48
- automaton.add_transition('q1', 'q0', 'a')
49
- automaton.add_transition('q2', 'q0', 'b')
50
- automaton.add_transition('q2', 'q1', 'a')
33
+ automaton.add_transition('q0', 'q2', :epsilon)
34
+
35
+ automaton.save_svg('diagram.svg')
36
+ automaton.save_html('diagram.html')
37
+ automaton.save_dot('diagram.dot')
38
+ automaton.save_plantuml('diagram.puml')
39
+ ```
40
+
41
+ Use `render` and `save` when the format is selected dynamically:
42
+
43
+ ```ruby
44
+ automaton.render(format: :svg, width: 800, height: 600)
45
+ automaton.save('diagram.svg', format: :svg, width: 800, height: 600)
46
+ ```
51
47
 
52
- # Save in different formats
53
- automaton.save_svg('output.svg') # SVG format
54
- automaton.save_html('output.html') # HTML with Mermaid.js (requires internet)
55
- automaton.save_dot('output.dot') # GraphViz DOT format
56
- automaton.save_plantuml('output.puml') # PlantUML format
48
+ Validate and inspect the automaton before rendering:
49
+
50
+ ```ruby
51
+ automaton.validate!
52
+ automaton.layout_warnings(800, 600)
53
+ automaton.reachable_states
54
+ automaton.dead_states
55
+ automaton.trap_states
57
56
  ```
58
57
 
59
- ### Output Formats
58
+ ## Loading data
60
59
 
61
- Graphomaton supports multiple output formats:
60
+ Build an automaton from Hash, JSON, or YAML:
62
61
 
63
- #### 1. SVG (Native)
64
62
  ```ruby
65
- automaton.save_svg('diagram.svg', width = 800, height = 600)
63
+ automaton = Graphomaton.from_hash(
64
+ states: [
65
+ { id: 'q0', label: 'Start', initial: true },
66
+ { id: 'q1', final: true }
67
+ ],
68
+ transitions: [
69
+ { from: 'q0', to: 'q1', label: 'a', line_style: 'dashed' }
70
+ ]
71
+ )
72
+
73
+ Graphomaton.from_json(File.read('automaton.json'))
74
+ Graphomaton.from_yaml(File.read('automaton.yml'))
66
75
  ```
67
- Generates a standalone SVG file with custom rendering.
68
76
 
69
- #### 2. HTML (Mermaid.js)
77
+ JSON and YAML parsing is bounded by default to 10 MiB, 10,000 states, and
78
+ 100,000 transitions. Lower or raise those limits explicitly for trusted input:
79
+
80
+ ```ruby
81
+ Graphomaton.from_json(
82
+ File.open('automaton.json'),
83
+ max_input_bytes: 2 * 1024 * 1024,
84
+ max_states: 2_000,
85
+ max_transitions: 20_000
86
+ )
87
+ ```
88
+
89
+ YAML aliases are disabled by default. Only enable `aliases: true` for trusted
90
+ documents that require them.
91
+
92
+ ## Examples
93
+
94
+ ### Styled SVG with metadata
95
+
96
+ ```ruby
97
+ automaton = Graphomaton.new
98
+ automaton.add_state(
99
+ 'idle',
100
+ label: 'Idle',
101
+ metadata: { tooltip: 'Waiting for work', group: 'runtime', icon: 'I' }
102
+ )
103
+ automaton.add_state(
104
+ 'running',
105
+ label: 'Running',
106
+ metadata: { tooltip: 'Processing job', group: 'runtime', url: 'https://example.com/runbook' }
107
+ )
108
+ automaton.add_state('failed', label: 'Failed', style: { fill: '#fee2e2', stroke: '#dc2626' })
109
+
110
+ automaton.set_initial('idle')
111
+ automaton.add_transition('idle', 'running', 'start', metadata: { bundle: 'happy-path' })
112
+ automaton.add_transition('running', 'idle', 'finish', metadata: { bundle: 'happy-path' })
113
+ automaton.add_transition('running', 'failed', 'error', line_style: :dashed)
114
+
115
+ automaton.save_svg(
116
+ 'runtime.svg',
117
+ 900,
118
+ 500,
119
+ layout: :layered,
120
+ direction: :lr,
121
+ theme: :ocean,
122
+ edge_style: :spline,
123
+ label_tooltips: true,
124
+ html_tooltips: true
125
+ )
126
+ ```
127
+
128
+ ### Manual coordinates
129
+
130
+ ```ruby
131
+ automaton = Graphomaton.new
132
+ automaton.add_state('north', 300, 80)
133
+ automaton.add_state('east', 520, 260)
134
+ automaton.add_state('south', 300, 440)
135
+ automaton.add_state('west', 80, 260)
136
+
137
+ automaton.add_transition('north', 'east', 'turn')
138
+ automaton.add_transition('east', 'south', 'turn')
139
+ automaton.add_transition('south', 'west', 'turn')
140
+ automaton.add_transition('west', 'north', 'turn')
141
+
142
+ automaton.save_svg('manual.svg', 600, 520, layout: :manual, fit: :contain)
143
+ ```
144
+
145
+ ### Folded groups
146
+
147
+ ```ruby
148
+ automaton = Graphomaton.new
149
+ automaton.add_state('parse', metadata: { group: 'frontend' })
150
+ automaton.add_state('validate', metadata: { group: 'frontend' })
151
+ automaton.add_state('execute', metadata: { group: 'backend' })
152
+ automaton.add_state('persist', metadata: { group: 'backend' })
153
+
154
+ automaton.add_transition('parse', 'validate', 'ok')
155
+ automaton.add_transition('validate', 'execute', 'accepted')
156
+ automaton.add_transition('execute', 'persist', 'done')
157
+
158
+ automaton.save_svg('folded.svg', layout: :layered, fold_groups: true)
159
+ ```
160
+
161
+ ### YAML input for the CLI
162
+
163
+ ```yaml
164
+ states:
165
+ - id: idle
166
+ label: Idle
167
+ initial: true
168
+ metadata:
169
+ group: runtime
170
+ tooltip: Waiting for work
171
+ - id: running
172
+ label: Running
173
+ metadata:
174
+ group: runtime
175
+ - id: done
176
+ final: true
177
+ transitions:
178
+ - from: idle
179
+ to: running
180
+ label: start
181
+ - from: running
182
+ to: done
183
+ label: finish
184
+ line_style: dashed
185
+ ```
186
+
187
+ ```bash
188
+ graphomaton --input automaton.yml --output runtime.svg --layout layered --direction lr --theme ocean
189
+ ```
190
+
191
+ ### Export several formats
192
+
193
+ ```ruby
194
+ outputs = {
195
+ svg: 'diagram.svg',
196
+ html: 'diagram.html',
197
+ dot: 'diagram.dot',
198
+ plantuml: 'diagram.puml'
199
+ }
200
+
201
+ outputs.each do |format, path|
202
+ automaton.save(path, format: format)
203
+ end
204
+ ```
205
+
206
+ ## CLI
207
+
208
+ ```bash
209
+ graphomaton --input automaton.yml --output diagram.svg
210
+ graphomaton --input automaton.yml --output diagram.svg --layout-warnings
211
+ graphomaton --input automaton.json --output diagram.png --format png --theme dark --scale 2 --timeout 15 --max-output-bytes 33554432
212
+ graphomaton --input automaton.yml --output diagram.html --title "Automaton" --show-source --pan-zoom
213
+ graphomaton --input automaton.yml --output diagram.dot --rank-constraints
214
+ cat automaton.yml | graphomaton --input - --output - --format svg > diagram.svg
215
+ graphomaton --version
216
+ ```
217
+
218
+ The command interface also supports structured workflows:
219
+
220
+ ```bash
221
+ graphomaton render automaton.yml diagram.svg
222
+ graphomaton validate automaton.yml --diagnostics json
223
+ graphomaton validate automaton.yml --profile fsm_semantics --fail-on-warning
224
+ graphomaton validate automaton.yml --profile dfa
225
+ graphomaton list formats
226
+ graphomaton list layouts
227
+ graphomaton doctor
228
+ graphomaton completion zsh > _graphomaton
229
+ graphomaton man > graphomaton.1
230
+ ```
231
+
232
+ Project defaults may be stored in `.graphomaton.yml`; command-line options override
233
+ environment variables, which override the config file:
234
+
235
+ ```yaml
236
+ svg:
237
+ theme: dark
238
+ layout: layered
239
+ labels:
240
+ wrap: true
241
+ ```
242
+
243
+ The CLI validates state references and hierarchy by default. The `validate` command also defaults to that `references` profile; use `--profile fsm_semantics`, `--profile dfa`, or `--profile all` for stronger checks. `--no-validate` is available for intentionally deferred or partial diagrams. Standard input auto-detects JSON documents that start with `{` or `[`, otherwise YAML is assumed; use `--input-format` to override it. `--format` is required when `--output -` is used. Input limits can be lowered with `--max-input-bytes`, `--max-states`, `--max-transitions`, `--max-metadata-depth`, `--max-label-length`, and `--max-group-depth`. Use `--no-clobber` to protect an existing output (and `--force` to opt back into replacement). Usage, parse, validation, conversion, and security failures use distinct exit statuses and are reported without backtraces.
244
+
245
+ Common SVG options:
246
+
247
+ ```bash
248
+ graphomaton --input automaton.yml --output diagram.svg --layout layered --direction lr
249
+ graphomaton --input automaton.yml --output diagram.svg --layout force --node-spacing 140 --force-iterations 80 --layout-seed 42
250
+ graphomaton --input automaton.yml --output diagram.svg --layout graphviz --graphviz-command dot
251
+ graphomaton --input automaton.yml --output diagram.svg --responsive --fit cover --auto-size
252
+ graphomaton --input automaton.yml --output diagram.svg --state-shape ellipse --edge-style spline --arrow-shape vee
253
+ graphomaton --input automaton.yml --output diagram.svg --wrap-labels --state-wrap --label-tooltips --html-tooltips
254
+ graphomaton --input automaton.yml --output diagram.svg --highlight-unreachable --unreachable-zone right --highlight-dead-states
255
+ graphomaton --input automaton.yml --output diagram.svg --scc-groups --fold-groups
256
+ graphomaton --input automaton.yml --output diagram.svg --theme-file theme.yml
257
+ ```
258
+
259
+ Theme utilities:
260
+
261
+ ```bash
262
+ graphomaton --list-themes
263
+ graphomaton --theme-gallery --output theme_gallery.html
264
+ graphomaton --theme-gallery --theme-gallery-animated --theme-file theme.yml --output theme_gallery.html
265
+ ```
266
+
267
+ For a self-contained HTML file, provide trusted local classic-script assets:
268
+
269
+ ```bash
270
+ graphomaton render automaton.yml diagram.html \
271
+ --cdn ./vendor/mermaid.min.js \
272
+ --self-contained --mermaid-sha256 HEX_DIGEST \
273
+ --nonce generated-nonce-123 --csp
274
+ ```
275
+
276
+ `--self-contained` does not download assets. It embeds the local files named by
277
+ `--cdn` and, when MathJax is enabled, `--mathjax-cdn`. These files execute with the
278
+ document's authority and must be trusted. `--nonce` plus `--csp` applies the nonce
279
+ to every generated script and style and emits a CSP meta tag. The optional
280
+ `--mermaid-sha256` and `--mathjax-sha256` values detect local asset replacement
281
+ before inlining.
282
+
283
+ ## Themes
284
+
285
+ Native SVG and SVG-backed outputs support named or custom themes:
286
+
287
+ ```ruby
288
+ automaton.save_svg('diagram.svg', theme: :dark)
289
+ automaton.save_png('diagram.png', theme: :forest)
290
+ automaton.save_svg('diagram.svg', theme: :auto) # follows prefers-color-scheme
291
+
292
+ theme = Graphomaton.theme_from_yaml(File.read('theme.yml'))
293
+ automaton.save_svg('diagram.svg', theme: theme)
294
+
295
+ Graphomaton::Theme.save_gallery_html('theme_gallery.html')
296
+ ```
297
+
298
+ Built-in themes:
299
+
300
+ ```text
301
+ light, dark, forest, ocean, high_contrast, color_blind, print, minimal, academic, presentation, auto
302
+ ```
303
+
304
+ ## SVG output
305
+
306
+ SVG is Graphomaton's native renderer. It supports multiple layouts, styling options, metadata-driven annotations, and converter-backed raster/vector outputs.
307
+
308
+ ### Layouts
309
+
310
+ ```ruby
311
+ automaton.save_svg('diagram.svg', 800, 600, layout: :linear, direction: :lr)
312
+ automaton.save_svg('diagram.svg', 800, 600, layout: :circle)
313
+ automaton.save_svg('diagram.svg', 800, 600, layout: :grid)
314
+ automaton.save_svg('diagram.svg', 800, 600, layout: :layered)
315
+ automaton.save_svg('diagram.svg', 800, 600, layout: :bfs)
316
+ automaton.save_svg('diagram.svg', 800, 600, layout: :force, layout_seed: 42)
317
+ automaton.save_svg('diagram.svg', 800, 600, layout: :graphviz, graphviz_command: 'dot')
318
+ automaton.save_svg('diagram.svg', 800, 600, layout: :manual)
319
+ ```
320
+
321
+ Layout notes:
322
+
323
+ - `direction` accepts `:lr`, `:tb`, `:rl`, and `:bt`.
324
+ - `layout` accepts `:linear`, `:circle`, `:grid`, `:layered`, `:bfs`, `:force`, `:graphviz`, `:dot`, and `:manual`.
325
+ - `:layered` and `:bfs` use deterministic barycenter ordering to reduce crossings.
326
+ - `:force` accepts `padding`, `node_spacing`, `rank_spacing`, `force_iterations`, and `layout_seed`.
327
+ - `:graphviz` and `:dot` call `dot -Tplain` through `graphviz_command:` and normalize returned node coordinates into the SVG canvas.
328
+ - `preserve_manual_positions: false` lets automatic layouts reposition states with explicit coordinates.
329
+ - `fit: :contain` fits positions into the canvas; `fit: :cover` stretches positions to use the canvas.
330
+ - `auto_size: true` expands the SVG viewport around rendered positions.
331
+
332
+ ### Styling and labels
333
+
334
+ ```ruby
335
+ automaton.save_svg('diagram.svg', state_shape: :ellipse)
336
+ automaton.save_svg('diagram.svg', state_stroke_width: 3, transition_stroke_width: 2)
337
+ automaton.save_svg('diagram.svg', edge_style: :orthogonal)
338
+ automaton.save_svg('diagram.svg', arrow_shape: :vee, arrow_size: 14)
339
+ automaton.save_svg('diagram.svg', state_effect: :shadow)
340
+ automaton.save_svg('diagram.svg', font_family: '"Noto Sans JP", sans-serif', state_font_weight: 700)
341
+
342
+ automaton.save_svg('diagram.svg', wrap: true, max_transition_label_width: 120)
343
+ automaton.save_svg('diagram.svg', state_wrap: true, max_state_label_width: 120)
344
+ automaton.save_svg('diagram.svg', label_tooltips: true, html_tooltips: true)
345
+ automaton.save_svg('diagram.svg', rotate_labels: true)
346
+ automaton.save_svg('diagram.svg', label_background: false)
347
+ automaton.save_svg('diagram.svg', label_padding: 16, label_radius: 8, label_border: true)
348
+ ```
349
+
350
+ Other SVG options:
351
+
352
+ - Default SVG root and marker IDs are deterministic for the graph, layout, and theme. `svg_id:` sets an explicit prefix and should be unique when embedding duplicate diagrams in one HTML document.
353
+ - Embedded CSS selectors and animation names are scoped to the SVG root ID.
354
+ - `css_variables: true` emits theme values as CSS variables.
355
+ - `embed_styles: false` skips the embedded style block.
356
+ - `xml_declaration: true`, `pretty: true`, and `minify: true` control serialization.
357
+ - `initial_arrow_length`, `initial_arrow_label`, `final_arrow_length`, `final_arrow_label`, and `show_final_arrows` control native start/end arrows.
358
+
359
+ ### Metadata
360
+
361
+ State and transition metadata can enrich generated diagrams without changing state IDs:
362
+
363
+ ```ruby
364
+ automaton.add_state(
365
+ 'q0',
366
+ label: 'Start',
367
+ kind: 'choice',
368
+ metadata: {
369
+ tooltip: 'Entry point',
370
+ url: 'https://example.com',
371
+ group: 'main',
372
+ icon: 'S'
373
+ }
374
+ )
375
+
376
+ automaton.add_transition(
377
+ 'q0',
378
+ 'q1',
379
+ 'next',
380
+ metadata: {
381
+ tooltip: 'Main path',
382
+ bundle: 'primary'
383
+ }
384
+ )
385
+ ```
386
+
387
+ `kind:` is format-independent and accepts `normal`, `choice`, `fork`, or `join`.
388
+ Exporter-specific shape overrides belong under `metadata.svg`, `metadata.dot`,
389
+ `metadata.mermaid`, or `metadata.plantuml` and do not affect other formats.
390
+
391
+ Metadata behavior:
392
+
393
+ - `label` changes the display name while preserving the state ID for transitions.
394
+ - `tooltip` or `description` becomes SVG tooltip text.
395
+ - `url` or `href` creates SVG links and DOT URL attributes.
396
+ - Link URLs may use `https`, `http`, `mailto`, a relative path, or a fragment. Executable and local-file schemes such as `javascript:`, `data:`, and `file:` are rejected.
397
+ - SVG `style` accepts `fill`, `stroke`, stroke settings, opacity settings, font settings, and `color`; raw CSS properties, `url(...)`, and declaration-breaking values are rejected.
398
+ - `group` or `cluster` renders SVG background groups, Mermaid/PlantUML composite states, and DOT clusters.
399
+ - `icon` renders a compact SVG icon label inside the state.
400
+ - `bundle` routes native SVG edges through a shared control point and emits `data-bundle`.
401
+ - `kind` defines a format-independent `choice`, `fork`, or `join` pseudostate. Format-specific nested metadata remains available for overrides.
402
+ - `parent` defines semantic hierarchy and `group` defines visual grouping; validation rejects setting both on one state, missing parents, and parent cycles.
403
+ - `fold_groups: true` collapses grouped SVG states into compound nodes, hides internal transitions, and rewrites external transitions to the folded node.
404
+ - `scc_groups: true` renders SVG groups around strongly connected components.
405
+
406
+ ## Output formats
407
+
408
+ | Format | Method | Notes |
409
+ | --- | --- | --- |
410
+ | SVG | `save_svg` | Native renderer. |
411
+ | PNG | `save_png` | Converts native SVG. Requires `rsvg-convert`, `magick`, or `convert`. |
412
+ | PDF | `save_pdf` | Converts native SVG. Requires `rsvg-convert`, `magick`, or `convert`. |
413
+ | WebP | `save_webp` | Converts native SVG. Uses librsvg plus ImageMagick when available, or ImageMagick directly. |
414
+ | HTML | `save_html` | Mermaid.js state diagram in an HTML page. |
415
+ | DOT | `save_dot` | GraphViz DOT source. |
416
+ | PlantUML | `save_plantuml` | PlantUML state diagram source. |
417
+
418
+ ### Converter-backed formats
419
+
420
+ ```ruby
421
+ automaton.save_png('diagram.png', 800, 600, scale: 2.0, converter: :magick)
422
+ automaton.save_pdf('diagram.pdf', 800, 600, converter: :magick, timeout: 15, max_output_bytes: 32 * 1024 * 1024)
423
+ automaton.save_webp('diagram.webp', 800, 600, converter: :magick)
424
+ automaton.save_webp('diagram.webp', 800, 600, converter: :rsvg_magick)
425
+
426
+ Graphomaton.png_available?(converter: :auto)
427
+ Graphomaton.pdf_available?(converter: :auto)
428
+ Graphomaton.webp_available?(converter: :auto)
429
+ ```
430
+
431
+ PNG `scale:` changes only output pixel density. The logical SVG viewBox, state size, and layout remain unchanged.
432
+ Converter processes default to a 30-second timeout, a 64 MiB output limit, and a 1 MiB diagnostic limit. Timeout and output-limit failures terminate the process group and raise the format-specific `ConversionError`.
433
+
434
+ ### HTML with Mermaid.js
435
+
70
436
  ```ruby
71
437
  automaton.save_html('diagram.html')
438
+ automaton.save_html('diagram.html', show_source: true)
439
+ automaton.save_html('diagram.html', theme: :auto)
440
+ automaton.save_html('diagram.html', cdn: './mermaid.min.js', inline_mermaid: true)
441
+ automaton.save_html('diagram.html', pan_zoom: true)
442
+ automaton.save_html('diagram.html', mathjax: true)
443
+ automaton.save_html('diagram.html', notes: true, class_defs: true)
72
444
  ```
73
- Generates an HTML file with embedded Mermaid.js state diagram. The diagram is rendered in the browser using Mermaid.js from CDN.
74
445
 
75
- **Note:** Requires internet connection to load Mermaid.js from CDN. Does not work in offline environments.
446
+ By default, HTML output uses the pinned Mermaid.js 10.9.8 module with Mermaid's strict security level. Remote asset URLs must use HTTPS; relative and absolute local paths are also accepted. `offline: true` requires `cdn:` to name a local classic `.js` build; ES module `.mjs` assets are rejected in offline and inline modes. `inline_mermaid: true` reads and embeds the complete file named by `cdn:`, so use it only with a trusted local JavaScript file. Generated UI is English by default and supports `lang: 'en'` or `lang: 'ja'`.
447
+
448
+ ### GraphViz DOT
76
449
 
77
- #### 3. GraphViz (DOT)
78
450
  ```ruby
79
451
  automaton.save_dot('diagram.dot')
452
+ automaton.save_dot('diagram.dot', theme: :ocean)
453
+ automaton.save_dot('diagram.dot', rank_constraints: true)
80
454
  ```
81
- Generates a DOT file that can be converted to images using GraphViz:
455
+
82
456
  ```bash
83
457
  dot -Tpng diagram.dot -o diagram.png
84
458
  dot -Tsvg diagram.dot -o diagram.svg
85
459
  dot -Tpdf diagram.dot -o diagram.pdf
460
+ neato -Tsvg diagram.dot -o diagram-neato.svg
461
+ sfdp -Tsvg diagram.dot -o diagram-sfdp.svg
86
462
  ```
87
463
 
88
- #### 4. PlantUML
464
+ Use `dot` for ranked state-machine layouts. Use `neato` or `sfdp` for dense graphs where free spreading is preferred.
465
+
466
+ ### PlantUML
467
+
89
468
  ```ruby
90
469
  automaton.save_plantuml('diagram.puml')
470
+ automaton.save_plantuml('diagram.puml', theme: :forest)
471
+ automaton.save_plantuml('diagram.puml', notes: true)
91
472
  ```
92
- Generates a PlantUML file that can be converted to images using PlantUML server or JAR:
473
+
93
474
  ```bash
94
- # Using PlantUML JAR
95
475
  java -jar plantuml.jar diagram.puml
96
-
97
- # Using online server
98
476
  curl -X POST --data-binary @diagram.puml https://www.plantuml.com/plantuml/png > diagram.png
99
477
  ```
100
478
 
101
479
  ## Contributing
102
480
 
481
+ Architecture, input schema, CLI, exporter capabilities, performance limits,
482
+ migration, release operations, and custom exporter integration are documented in
483
+ the [`docs/`](docs/) directory. See [Custom exporters](docs/custom-exporters.md)
484
+ when adding an application-specific output format.
485
+
103
486
  Bug reports and pull requests are welcome on GitHub at https://github.com/ydah/graphomaton.
104
487
 
105
488
  ## License
@@ -108,4 +491,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
108
491
 
109
492
  ## Code of Conduct
110
493
 
111
- Everyone interacting in the Graphomaton project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/ydah/graphomaton/blob/main/CODE_OF_CONDUCT.md).
494
+ Everyone interacting in the Graphomaton project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the [code of conduct](https://github.com/ydah/graphomaton/blob/main/CODE_OF_CONDUCT.md).