graphomaton 1.0.0 → 1.1.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 +74 -8
  3. data/README.md +426 -44
  4. data/SECURITY.md +47 -0
  5. data/docs/architecture.md +30 -0
  6. data/docs/cli.md +27 -0
  7. data/docs/custom-exporters.md +36 -0
  8. data/docs/exporters.md +17 -0
  9. data/docs/input-schema.md +26 -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 +26 -0
  15. data/lib/graphomaton/cli/config.rb +102 -0
  16. data/lib/graphomaton/cli.rb +841 -0
  17. data/lib/graphomaton/errors.rb +11 -0
  18. data/lib/graphomaton/exporter_registry.rb +127 -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 +172 -0
  24. data/lib/graphomaton/exporters/svg.rb +2775 -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 +82 -0
  29. data/lib/graphomaton/layout/force_tree.rb +127 -0
  30. data/lib/graphomaton/model.rb +218 -0
  31. data/lib/graphomaton/process_runner.rb +154 -0
  32. data/lib/graphomaton/url_policy.rb +40 -0
  33. data/lib/graphomaton/version.rb +1 -1
  34. data/lib/graphomaton.rb +2869 -54
  35. data/sig/graphomaton.rbs +127 -0
  36. metadata +34 -24
  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: 8992f0a2ab8bf57fc92a7480dc29c39fa94188e952bef61a865f421fde5a9b91
4
+ data.tar.gz: 1f7b78192ad4e692cff03ce8c45bb0cacf33195d7294612ca06a9bf695b409b3
5
5
  SHA512:
6
- metadata.gz: 538893d73e2cdbcfe697bcf194ab259c7c796d6efdb9cd100b810ed7aa5363144819880d6d813ec76dc9350d455bdb232608b7b0ec39c00efa664cba54bcc2bd
7
- data.tar.gz: 41e703f607023ceebd0aba66965084f3466cd60b414a3ba14670cc0f4e6d82391e18cd64d47911d80efa28dbb4f05378ea693de3ebb0461f84a465e9721de9ca
6
+ metadata.gz: 7360eaf0c5ce3676e505559b066d429fa1f1df0219e922c2cacc2d62578a4bf94f07ac51d385abdcf61a5c03979def0f98088ac28e7081841722a2f975b39984
7
+ data.tar.gz: d8bacee19d445a50d62449970b435ab5917093da8a27b7df7c41b7d59f8aa46c419de2754ddacbddae67b18bc00060e28f204a5c8a377b95c306b8203cef1abb
data/CHANGELOG.md CHANGED
@@ -1,16 +1,82 @@
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.1.0] - 2026-08-13
9
10
 
10
- ## 0.1.1 (2025-08-26)
11
+ ### Added
11
12
 
12
- Fix gemspec dependency declaration for 'rexml'.
13
+ - Immutable model records, update/remove APIs, graph revision caches, structured
14
+ labels, diagnostics, validation profiles, exporter capabilities, semantic-loss
15
+ reporting, render results, IO output, and RBS signatures.
16
+ - Obstacle-aware curves, adaptive self-loops, spatial label indexing, Barnes-Hut
17
+ force approximation, deterministic force-layout separation, and convergence
18
+ detection.
19
+ - CLI commands for validation, discovery, diagnostics, health checks, config
20
+ files, shell completions, and man-page output.
21
+ - Bounded stdin/stdout workflows, no-clobber protection, format-specific option
22
+ errors, structured exit statuses, and `--version`.
23
+ - Nonce-based CSP support, trusted local asset inlining for self-contained HTML,
24
+ and deterministic HTML asset loading with localized generated UI.
25
+ - Restricted gem packaging, gem installation smoke tests, renderer integration
26
+ jobs, release automation, and immutable GitHub Actions pins.
13
27
 
14
- ## 0.1.0 (2025-08-26)
28
+ ### Changed
29
+
30
+ - Curved and orthogonal edges are routed from geometry instead of insertion
31
+ order, and SVG viewBoxes include rendered paths, shapes, rotated labels, and
32
+ text content.
33
+ - PNG scaling is separated from logical layout and SVG geometry; layout
34
+ diagnostics and metadata survive PNG, PDF, and WebP conversion.
35
+ - SCC analysis is iterative and linear-time. Fixed manual positions remain clear
36
+ of automatic layouts, and dense graphs receive adaptive spacing.
37
+ - Structured transition labels and format-independent pseudostate kinds are kept
38
+ in the model, while custom exporter registration now produces renderable
39
+ exporters and rejects ambiguous schema aliases.
40
+ - HTML pan/zoom controls are more accessible, and text exporters terminate
41
+ output with a newline after normalizing CR/LF label boundaries.
42
+
43
+ ### Fixed
44
+
45
+ - Corrected force-layout attraction direction, boundary clamping, and analysis
46
+ of undefined transition endpoints.
47
+ - Isolated states are declared in DOT, Mermaid, and PlantUML output.
48
+ - DOT, Mermaid, PlantUML, and Graphviz layout identifiers are collision-free,
49
+ including reserved names, hostile text, mixed-type model IDs, and groups.
50
+ - SVG parallel-transition merging preserves presentation metadata and uses
51
+ tuple endpoint keys; edge endpoints now respect ellipse, diamond, bar, and
52
+ rounded-rectangle boundaries.
53
+ - Input validation now rejects duplicate states, malformed transition tuples,
54
+ conflicting initial states, invalid state hierarchies, ignored value types,
55
+ and non-finite rendering numbers.
56
+ - Partial state upserts preserve omitted coordinates and effective no-op updates
57
+ do not advance the graph revision.
58
+
59
+ ### Security
60
+
61
+ - Added URL, HTML JavaScript, theme, and SVG style policies, including safe
62
+ handling of JavaScript strings, trusted local assets, Windows asset paths, and
63
+ unsafe URL schemes; Mermaid.js is pinned to 10.9.8.
64
+ - External processes are bounded by timeout and stdout/stderr limits, with
65
+ portable executable discovery and process termination on Windows.
66
+ - JSON and YAML input, state and transition counts, metadata depth, label size,
67
+ hierarchy depth, and converter resources are bounded.
68
+ - Exported files are written atomically, and embedded scripts enforce size,
69
+ encoding, and digest checks.
70
+
71
+ ## [1.0.0] - 2025-12-23
72
+
73
+ - Add support for multiple output styles, including DOT, Mermaid, and PlantUML.
74
+ - Improve SVG output formatting.
75
+
76
+ ## [0.1.1] - 2025-08-26
77
+
78
+ - Fix the gemspec dependency declaration for `rexml`.
79
+
80
+ ## [0.1.0] - 2025-08-26
15
81
 
16
82
  - Initial release
data/README.md CHANGED
@@ -1,105 +1,487 @@
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 --fail-on-warning
224
+ graphomaton list formats
225
+ graphomaton list layouts
226
+ graphomaton doctor
227
+ graphomaton completion zsh > _graphomaton
228
+ graphomaton man > graphomaton.1
229
+ ```
230
+
231
+ Project defaults may be stored in `.graphomaton.yml`; command-line options override
232
+ environment variables, which override the config file:
233
+
234
+ ```yaml
235
+ svg:
236
+ theme: dark
237
+ layout: layered
238
+ labels:
239
+ wrap: true
240
+ ```
241
+
242
+ The CLI validates state references and hierarchy by default. `--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.
243
+
244
+ Common SVG options:
245
+
246
+ ```bash
247
+ graphomaton --input automaton.yml --output diagram.svg --layout layered --direction lr
248
+ graphomaton --input automaton.yml --output diagram.svg --layout force --node-spacing 140 --force-iterations 80 --layout-seed 42
249
+ graphomaton --input automaton.yml --output diagram.svg --layout graphviz --graphviz-command dot
250
+ graphomaton --input automaton.yml --output diagram.svg --responsive --fit cover --auto-size
251
+ graphomaton --input automaton.yml --output diagram.svg --state-shape ellipse --edge-style spline --arrow-shape vee
252
+ graphomaton --input automaton.yml --output diagram.svg --wrap-labels --state-wrap --label-tooltips --html-tooltips
253
+ graphomaton --input automaton.yml --output diagram.svg --highlight-unreachable --unreachable-zone right --highlight-dead-states
254
+ graphomaton --input automaton.yml --output diagram.svg --scc-groups --fold-groups
255
+ graphomaton --input automaton.yml --output diagram.svg --theme-file theme.yml
256
+ ```
257
+
258
+ Theme utilities:
259
+
260
+ ```bash
261
+ graphomaton --list-themes
262
+ graphomaton --theme-gallery --output theme_gallery.html
263
+ graphomaton --theme-gallery --theme-gallery-animated --theme-file theme.yml --output theme_gallery.html
264
+ ```
265
+
266
+ For a self-contained HTML file, provide trusted local classic-script assets:
267
+
268
+ ```bash
269
+ graphomaton render automaton.yml diagram.html \
270
+ --cdn ./vendor/mermaid.min.js \
271
+ --self-contained --mermaid-sha256 HEX_DIGEST \
272
+ --nonce generated-nonce-123 --csp
273
+ ```
274
+
275
+ `--self-contained` does not download assets. It embeds the local files named by
276
+ `--cdn` and, when MathJax is enabled, `--mathjax-cdn`. These files execute with the
277
+ document's authority and must be trusted. `--nonce` plus `--csp` applies the nonce
278
+ to every generated script and style and emits a CSP meta tag. The optional
279
+ `--mermaid-sha256` and `--mathjax-sha256` values detect local asset replacement
280
+ before inlining.
281
+
282
+ ## Themes
283
+
284
+ Native SVG and SVG-backed outputs support named or custom themes:
285
+
286
+ ```ruby
287
+ automaton.save_svg('diagram.svg', theme: :dark)
288
+ automaton.save_png('diagram.png', theme: :forest)
289
+ automaton.save_svg('diagram.svg', theme: :auto) # follows prefers-color-scheme
290
+
291
+ theme = Graphomaton.theme_from_yaml(File.read('theme.yml'))
292
+ automaton.save_svg('diagram.svg', theme: theme)
293
+
294
+ Graphomaton::Theme.save_gallery_html('theme_gallery.html')
295
+ ```
296
+
297
+ Built-in themes:
298
+
299
+ ```text
300
+ light, dark, forest, ocean, high_contrast, color_blind, print, minimal, academic, presentation, auto
301
+ ```
302
+
303
+ ## SVG output
304
+
305
+ SVG is Graphomaton's native renderer. It supports multiple layouts, styling options, metadata-driven annotations, and converter-backed raster/vector outputs.
306
+
307
+ ### Layouts
308
+
309
+ ```ruby
310
+ automaton.save_svg('diagram.svg', 800, 600, layout: :linear, direction: :lr)
311
+ automaton.save_svg('diagram.svg', 800, 600, layout: :circle)
312
+ automaton.save_svg('diagram.svg', 800, 600, layout: :grid)
313
+ automaton.save_svg('diagram.svg', 800, 600, layout: :layered)
314
+ automaton.save_svg('diagram.svg', 800, 600, layout: :bfs)
315
+ automaton.save_svg('diagram.svg', 800, 600, layout: :force, layout_seed: 42)
316
+ automaton.save_svg('diagram.svg', 800, 600, layout: :graphviz, graphviz_command: 'dot')
317
+ automaton.save_svg('diagram.svg', 800, 600, layout: :manual)
318
+ ```
319
+
320
+ Layout notes:
321
+
322
+ - `direction` accepts `:lr`, `:tb`, `:rl`, and `:bt`.
323
+ - `layout` accepts `:linear`, `:circle`, `:grid`, `:layered`, `:bfs`, `:force`, `:graphviz`, `:dot`, and `:manual`.
324
+ - `:layered` and `:bfs` use deterministic barycenter ordering to reduce crossings.
325
+ - `:force` accepts `padding`, `node_spacing`, `rank_spacing`, `force_iterations`, and `layout_seed`.
326
+ - `:graphviz` and `:dot` call `dot -Tplain` through `graphviz_command:` and normalize returned node coordinates into the SVG canvas.
327
+ - `preserve_manual_positions: false` lets automatic layouts reposition states with explicit coordinates.
328
+ - `fit: :contain` fits positions into the canvas; `fit: :cover` stretches positions to use the canvas.
329
+ - `auto_size: true` expands the SVG viewport around rendered positions.
330
+
331
+ ### Styling and labels
332
+
333
+ ```ruby
334
+ automaton.save_svg('diagram.svg', state_shape: :ellipse)
335
+ automaton.save_svg('diagram.svg', state_stroke_width: 3, transition_stroke_width: 2)
336
+ automaton.save_svg('diagram.svg', edge_style: :orthogonal)
337
+ automaton.save_svg('diagram.svg', arrow_shape: :vee, arrow_size: 14)
338
+ automaton.save_svg('diagram.svg', state_effect: :shadow)
339
+ automaton.save_svg('diagram.svg', font_family: '"Noto Sans JP", sans-serif', state_font_weight: 700)
340
+
341
+ automaton.save_svg('diagram.svg', wrap: true, max_transition_label_width: 120)
342
+ automaton.save_svg('diagram.svg', state_wrap: true, max_state_label_width: 120)
343
+ automaton.save_svg('diagram.svg', label_tooltips: true, html_tooltips: true)
344
+ automaton.save_svg('diagram.svg', rotate_labels: true)
345
+ automaton.save_svg('diagram.svg', label_background: false)
346
+ automaton.save_svg('diagram.svg', label_padding: 16, label_radius: 8, label_border: true)
347
+ ```
348
+
349
+ Other SVG options:
350
+
351
+ - 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.
352
+ - Embedded CSS selectors and animation names are scoped to the SVG root ID.
353
+ - `css_variables: true` emits theme values as CSS variables.
354
+ - `embed_styles: false` skips the embedded style block.
355
+ - `xml_declaration: true`, `pretty: true`, and `minify: true` control serialization.
356
+ - `initial_arrow_length`, `initial_arrow_label`, `final_arrow_length`, `final_arrow_label`, and `show_final_arrows` control native start/end arrows.
357
+
358
+ ### Metadata
359
+
360
+ State and transition metadata can enrich generated diagrams without changing state IDs:
361
+
362
+ ```ruby
363
+ automaton.add_state(
364
+ 'q0',
365
+ label: 'Start',
366
+ kind: 'choice',
367
+ metadata: {
368
+ tooltip: 'Entry point',
369
+ url: 'https://example.com',
370
+ group: 'main',
371
+ icon: 'S'
372
+ }
373
+ )
374
+
375
+ automaton.add_transition(
376
+ 'q0',
377
+ 'q1',
378
+ 'next',
379
+ metadata: {
380
+ tooltip: 'Main path',
381
+ bundle: 'primary'
382
+ }
383
+ )
384
+ ```
385
+
386
+ `kind:` is format-independent and accepts `normal`, `choice`, `fork`, or `join`.
387
+ Exporter-specific shape overrides belong under `metadata.svg`, `metadata.dot`,
388
+ `metadata.mermaid`, or `metadata.plantuml` and do not affect other formats.
389
+
390
+ Metadata behavior:
391
+
392
+ - `label` changes the display name while preserving the state ID for transitions.
393
+ - `tooltip` or `description` becomes SVG tooltip text.
394
+ - `url` or `href` creates SVG links and DOT URL attributes.
395
+ - 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.
396
+ - SVG `style` accepts `fill`, `stroke`, stroke settings, opacity settings, font settings, and `color`; raw CSS properties, `url(...)`, and declaration-breaking values are rejected.
397
+ - `group` or `cluster` renders SVG background groups, Mermaid/PlantUML composite states, and DOT clusters.
398
+ - `icon` renders a compact SVG icon label inside the state.
399
+ - `bundle` routes native SVG edges through a shared control point and emits `data-bundle`.
400
+ - `kind` defines a format-independent `choice`, `fork`, or `join` pseudostate. Format-specific nested metadata remains available for overrides.
401
+ - `parent` defines semantic hierarchy and `group` defines visual grouping; validation rejects setting both on one state, missing parents, and parent cycles.
402
+ - `fold_groups: true` collapses grouped SVG states into compound nodes, hides internal transitions, and rewrites external transitions to the folded node.
403
+ - `scc_groups: true` renders SVG groups around strongly connected components.
404
+
405
+ ## Output formats
406
+
407
+ | Format | Method | Notes |
408
+ | --- | --- | --- |
409
+ | SVG | `save_svg` | Native renderer. |
410
+ | PNG | `save_png` | Converts native SVG. Requires `rsvg-convert`, `magick`, or `convert`. |
411
+ | PDF | `save_pdf` | Converts native SVG. Requires `rsvg-convert`, `magick`, or `convert`. |
412
+ | WebP | `save_webp` | Converts native SVG. Uses librsvg plus ImageMagick when available, or ImageMagick directly. |
413
+ | HTML | `save_html` | Mermaid.js state diagram in an HTML page. |
414
+ | DOT | `save_dot` | GraphViz DOT source. |
415
+ | PlantUML | `save_plantuml` | PlantUML state diagram source. |
416
+
417
+ ### Converter-backed formats
418
+
419
+ ```ruby
420
+ automaton.save_png('diagram.png', 800, 600, scale: 2.0, converter: :magick)
421
+ automaton.save_pdf('diagram.pdf', 800, 600, converter: :magick, timeout: 15, max_output_bytes: 32 * 1024 * 1024)
422
+ automaton.save_webp('diagram.webp', 800, 600, converter: :magick)
423
+ automaton.save_webp('diagram.webp', 800, 600, converter: :rsvg_magick)
424
+
425
+ Graphomaton.png_available?(converter: :auto)
426
+ Graphomaton.pdf_available?(converter: :auto)
427
+ Graphomaton.webp_available?(converter: :auto)
428
+ ```
429
+
430
+ PNG `scale:` changes only output pixel density. The logical SVG viewBox, state size, and layout remain unchanged.
431
+ 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`.
432
+
433
+ ### HTML with Mermaid.js
434
+
70
435
  ```ruby
71
436
  automaton.save_html('diagram.html')
437
+ automaton.save_html('diagram.html', show_source: true)
438
+ automaton.save_html('diagram.html', theme: :auto)
439
+ automaton.save_html('diagram.html', cdn: './mermaid.min.js', inline_mermaid: true)
440
+ automaton.save_html('diagram.html', pan_zoom: true)
441
+ automaton.save_html('diagram.html', mathjax: true)
442
+ automaton.save_html('diagram.html', notes: true, class_defs: true)
72
443
  ```
73
- Generates an HTML file with embedded Mermaid.js state diagram. The diagram is rendered in the browser using Mermaid.js from CDN.
74
444
 
75
- **Note:** Requires internet connection to load Mermaid.js from CDN. Does not work in offline environments.
445
+ 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'`.
446
+
447
+ ### GraphViz DOT
76
448
 
77
- #### 3. GraphViz (DOT)
78
449
  ```ruby
79
450
  automaton.save_dot('diagram.dot')
451
+ automaton.save_dot('diagram.dot', theme: :ocean)
452
+ automaton.save_dot('diagram.dot', rank_constraints: true)
80
453
  ```
81
- Generates a DOT file that can be converted to images using GraphViz:
454
+
82
455
  ```bash
83
456
  dot -Tpng diagram.dot -o diagram.png
84
457
  dot -Tsvg diagram.dot -o diagram.svg
85
458
  dot -Tpdf diagram.dot -o diagram.pdf
459
+ neato -Tsvg diagram.dot -o diagram-neato.svg
460
+ sfdp -Tsvg diagram.dot -o diagram-sfdp.svg
86
461
  ```
87
462
 
88
- #### 4. PlantUML
463
+ Use `dot` for ranked state-machine layouts. Use `neato` or `sfdp` for dense graphs where free spreading is preferred.
464
+
465
+ ### PlantUML
466
+
89
467
  ```ruby
90
468
  automaton.save_plantuml('diagram.puml')
469
+ automaton.save_plantuml('diagram.puml', theme: :forest)
470
+ automaton.save_plantuml('diagram.puml', notes: true)
91
471
  ```
92
- Generates a PlantUML file that can be converted to images using PlantUML server or JAR:
472
+
93
473
  ```bash
94
- # Using PlantUML JAR
95
474
  java -jar plantuml.jar diagram.puml
96
-
97
- # Using online server
98
475
  curl -X POST --data-binary @diagram.puml https://www.plantuml.com/plantuml/png > diagram.png
99
476
  ```
100
477
 
101
478
  ## Contributing
102
479
 
480
+ Architecture, input schema, CLI, exporter capabilities, performance limits,
481
+ migration, release operations, and custom exporter integration are documented in
482
+ the [`docs/`](docs/) directory. See [Custom exporters](docs/custom-exporters.md)
483
+ when adding an application-specific output format.
484
+
103
485
  Bug reports and pull requests are welcome on GitHub at https://github.com/ydah/graphomaton.
104
486
 
105
487
  ## License
@@ -108,4 +490,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
108
490
 
109
491
  ## Code of Conduct
110
492
 
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).
493
+ 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).