mmd2svg 0.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.
- checksums.yaml +7 -0
- data/.rspec +1 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +262 -0
- data/Rakefile +8 -0
- data/examples/class.mmd +21 -0
- data/examples/demo.rb +87 -0
- data/examples/flowchart.mmd +6 -0
- data/examples/sequence.mmd +11 -0
- data/exe/mmd2svg +7 -0
- data/lib/mmd2svg/batch_renderer.rb +51 -0
- data/lib/mmd2svg/cli.rb +196 -0
- data/lib/mmd2svg/config.rb +88 -0
- data/lib/mmd2svg/errors.rb +9 -0
- data/lib/mmd2svg/file_finder.rb +40 -0
- data/lib/mmd2svg/renderer.rb +95 -0
- data/lib/mmd2svg/version.rb +5 -0
- data/lib/mmd2svg.rb +74 -0
- data/spec/mermaid2svg_spec.rb +34 -0
- data/spec/spec_helper.rb +11 -0
- data/templates/render.html +43 -0
- metadata +95 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f32fd990c261c1696e29f9a804f588ffdb36bcc5228c5b5c0d34501c3c1d1c26
|
|
4
|
+
data.tar.gz: f8420494972f1952b2524c46be923fdb6fd9697f6a41d79ab0d68f6f582b67d1
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 0b9d96109ce33815b20097a96ba74075a4725a83e4115b08826d948874f36266782a9a140da36b9bef5e50d00edb7a196d1e0ac152c16545a5429f95d8f776d5
|
|
7
|
+
data.tar.gz: b66781a0bff7d71725d0514a56b99a216f7179b3a55633d8d707720f2bcfc4593a1b953b85481a051251769208e21183c9142bcf12390e270a9e5a587cd813d7
|
data/.rspec
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--require spec_helper
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Yudai Takada
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# mermaid2svg
|
|
2
|
+
|
|
3
|
+
Convert Mermaid diagrams to SVG files using Puppeteer. Supports both CLI and programmatic usage with batch conversion capabilities.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
Add this line to your application's Gemfile:
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
gem 'mermaid2svg'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
And then execute:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bundle install
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Or install it yourself as:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
gem install mermaid2svg
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
### Command Line Interface
|
|
28
|
+
|
|
29
|
+
#### Basic Usage
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# Convert a single file
|
|
33
|
+
mermaid2svg diagram.mmd -o output.svg
|
|
34
|
+
|
|
35
|
+
# Convert with options
|
|
36
|
+
mermaid2svg diagram.mmd -o output.svg --theme dark --background transparent
|
|
37
|
+
|
|
38
|
+
# Batch conversion from directory
|
|
39
|
+
mermaid2svg diagrams/ -o output/
|
|
40
|
+
|
|
41
|
+
# Batch conversion with glob pattern
|
|
42
|
+
mermaid2svg diagrams/*.mmd -o output/
|
|
43
|
+
|
|
44
|
+
# Recursive directory conversion
|
|
45
|
+
mermaid2svg diagrams/ -o output/ --recursive
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
#### CLI Options
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Options:
|
|
52
|
+
-o, --output PATH Output file or directory path (required)
|
|
53
|
+
-t, --theme THEME Theme: default, dark, forest, neutral (default: default)
|
|
54
|
+
-b, --background COLOR Background color: transparent, white, or #hexcode (default: white)
|
|
55
|
+
-w, --width WIDTH Output width in pixels
|
|
56
|
+
-h, --height HEIGHT Output height in pixels
|
|
57
|
+
-c, --config FILE Config file path (default: .mermaid2svg.yml)
|
|
58
|
+
-r, --recursive Process directories recursively
|
|
59
|
+
--timeout MILLISECONDS Puppeteer timeout in ms (default: 30000)
|
|
60
|
+
--skip-errors Continue processing even if errors occur
|
|
61
|
+
--version Show version
|
|
62
|
+
--help Show help message
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Ruby API
|
|
66
|
+
|
|
67
|
+
#### Single File Conversion
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
require 'mermaid2svg'
|
|
71
|
+
|
|
72
|
+
# Render from code string
|
|
73
|
+
mermaid_code = <<~MERMAID
|
|
74
|
+
graph TD
|
|
75
|
+
A[Start] --> B[Process]
|
|
76
|
+
B --> C[End]
|
|
77
|
+
MERMAID
|
|
78
|
+
|
|
79
|
+
Mermaid2svg.render(mermaid_code, output: 'diagram.svg')
|
|
80
|
+
|
|
81
|
+
# Render from file
|
|
82
|
+
Mermaid2svg.render('diagram.mmd', output: 'output.svg')
|
|
83
|
+
|
|
84
|
+
# Get SVG as string
|
|
85
|
+
svg_string = Mermaid2svg.render_to_string(mermaid_code)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
#### Batch Conversion
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
# Convert all .mmd files in a directory
|
|
92
|
+
results = Mermaid2svg.render_batch('diagrams/', output_dir: 'output/')
|
|
93
|
+
|
|
94
|
+
# With glob pattern
|
|
95
|
+
results = Mermaid2svg.render_batch('diagrams/*.mmd', output_dir: 'output/')
|
|
96
|
+
|
|
97
|
+
# Recursive conversion
|
|
98
|
+
results = Mermaid2svg.render_batch(
|
|
99
|
+
'diagrams/',
|
|
100
|
+
output_dir: 'output/',
|
|
101
|
+
recursive: true
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Check results
|
|
105
|
+
puts "Succeeded: #{results[:succeeded].count}"
|
|
106
|
+
puts "Failed: #{results[:failed].count}"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
#### With Options
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
Mermaid2svg.render(
|
|
113
|
+
mermaid_code,
|
|
114
|
+
output: 'diagram.svg',
|
|
115
|
+
theme: 'dark',
|
|
116
|
+
background_color: 'transparent',
|
|
117
|
+
width: 800,
|
|
118
|
+
height: 600
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Global Configuration
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
Mermaid2svg.configure do |config|
|
|
126
|
+
config.theme = 'dark'
|
|
127
|
+
config.background_color = 'transparent'
|
|
128
|
+
config.puppeteer_timeout = 60000
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Now all renders use these settings
|
|
132
|
+
Mermaid2svg.render(code, output: 'diagram.svg')
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Configuration File
|
|
136
|
+
|
|
137
|
+
Create a `.mermaid2svg.yml` file in your project root:
|
|
138
|
+
|
|
139
|
+
```yaml
|
|
140
|
+
# Theme setting
|
|
141
|
+
theme: default # default, dark, forest, neutral
|
|
142
|
+
|
|
143
|
+
# Background color
|
|
144
|
+
background_color: white # transparent, white, or #hexcode
|
|
145
|
+
|
|
146
|
+
# Output size (optional)
|
|
147
|
+
# width: 800
|
|
148
|
+
# height: 600
|
|
149
|
+
|
|
150
|
+
# Puppeteer settings
|
|
151
|
+
puppeteer:
|
|
152
|
+
headless: true
|
|
153
|
+
timeout: 30000
|
|
154
|
+
args:
|
|
155
|
+
- '--no-sandbox'
|
|
156
|
+
- '--disable-setuid-sandbox'
|
|
157
|
+
|
|
158
|
+
# Mermaid.js settings
|
|
159
|
+
mermaid:
|
|
160
|
+
securityLevel: 'loose'
|
|
161
|
+
startOnLoad: true
|
|
162
|
+
theme: default
|
|
163
|
+
logLevel: 'error'
|
|
164
|
+
|
|
165
|
+
# Batch conversion settings
|
|
166
|
+
batch:
|
|
167
|
+
recursive: false
|
|
168
|
+
overwrite: true
|
|
169
|
+
skip_errors: false
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Supported Mermaid Diagram Types
|
|
173
|
+
|
|
174
|
+
This gem supports all diagram types that Mermaid.js supports:
|
|
175
|
+
|
|
176
|
+
- Flowchart
|
|
177
|
+
- Sequence Diagram
|
|
178
|
+
- Class Diagram
|
|
179
|
+
- State Diagram
|
|
180
|
+
- Entity Relationship Diagram
|
|
181
|
+
- User Journey
|
|
182
|
+
- Gantt Chart
|
|
183
|
+
- Pie Chart
|
|
184
|
+
- Git Graph
|
|
185
|
+
- And more...
|
|
186
|
+
|
|
187
|
+
## Error Handling
|
|
188
|
+
|
|
189
|
+
The gem provides custom exceptions for different error scenarios:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
begin
|
|
193
|
+
Mermaid2svg.render('invalid.mmd', output: 'out.svg')
|
|
194
|
+
rescue Mermaid2svg::RenderError => e
|
|
195
|
+
puts "Render failed: #{e.message}"
|
|
196
|
+
rescue Mermaid2svg::FileNotFoundError => e
|
|
197
|
+
puts "File not found: #{e.message}"
|
|
198
|
+
rescue Mermaid2svg::PuppeteerError => e
|
|
199
|
+
puts "Puppeteer error: #{e.message}"
|
|
200
|
+
rescue Mermaid2svg::ConfigError => e
|
|
201
|
+
puts "Configuration error: #{e.message}"
|
|
202
|
+
end
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Examples
|
|
206
|
+
|
|
207
|
+
### Example 1: Simple Flowchart
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
code = <<~MERMAID
|
|
211
|
+
graph LR
|
|
212
|
+
A[Square Rect] --> B((Circle))
|
|
213
|
+
A --> C(Round Rect)
|
|
214
|
+
B --> D{Rhombus}
|
|
215
|
+
C --> D
|
|
216
|
+
MERMAID
|
|
217
|
+
|
|
218
|
+
Mermaid2svg.render(code, output: 'flowchart.svg')
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
### Example 2: Sequence Diagram with Dark Theme
|
|
222
|
+
|
|
223
|
+
```ruby
|
|
224
|
+
code = <<~MERMAID
|
|
225
|
+
sequenceDiagram
|
|
226
|
+
Alice->>John: Hello John, how are you?
|
|
227
|
+
John-->>Alice: Great!
|
|
228
|
+
Alice-)John: See you later!
|
|
229
|
+
MERMAID
|
|
230
|
+
|
|
231
|
+
Mermaid2svg.render(
|
|
232
|
+
code,
|
|
233
|
+
output: 'sequence.svg',
|
|
234
|
+
theme: 'dark',
|
|
235
|
+
background_color: 'transparent'
|
|
236
|
+
)
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### Example 3: Batch Convert Project Diagrams
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
results = Mermaid2svg.render_batch(
|
|
243
|
+
'docs/diagrams/',
|
|
244
|
+
output_dir: 'public/images/',
|
|
245
|
+
recursive: true,
|
|
246
|
+
theme: 'forest'
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
results[:failed].each do |failure|
|
|
250
|
+
puts "Failed: #{failure[:file]} - #{failure[:error]}"
|
|
251
|
+
end
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
257
|
+
|
|
258
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/examples/class.mmd
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
classDiagram
|
|
2
|
+
Animal <|-- Duck
|
|
3
|
+
Animal <|-- Fish
|
|
4
|
+
Animal <|-- Zebra
|
|
5
|
+
Animal : +int age
|
|
6
|
+
Animal : +String gender
|
|
7
|
+
Animal: +isMammal()
|
|
8
|
+
Animal: +mate()
|
|
9
|
+
class Duck{
|
|
10
|
+
+String beakColor
|
|
11
|
+
+swim()
|
|
12
|
+
+quack()
|
|
13
|
+
}
|
|
14
|
+
class Fish{
|
|
15
|
+
-int sizeInFeet
|
|
16
|
+
-canEat()
|
|
17
|
+
}
|
|
18
|
+
class Zebra{
|
|
19
|
+
+bool is_wild
|
|
20
|
+
+run()
|
|
21
|
+
}
|
data/examples/demo.rb
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative '../lib/mmd2svg'
|
|
5
|
+
|
|
6
|
+
puts '=== Mmd2svg Demo ==='
|
|
7
|
+
puts ''
|
|
8
|
+
|
|
9
|
+
# Example 1: Simple render to string
|
|
10
|
+
puts '1. Rendering a simple diagram to string...'
|
|
11
|
+
simple_diagram = <<~MERMAID
|
|
12
|
+
graph LR
|
|
13
|
+
A[Hello] --> B[World]
|
|
14
|
+
MERMAID
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
svg = Mmd2svg.render_to_string(simple_diagram)
|
|
18
|
+
puts "✓ Successfully generated SVG (#{svg.length} characters)"
|
|
19
|
+
rescue StandardError => e
|
|
20
|
+
puts "✗ Error: #{e.message}"
|
|
21
|
+
end
|
|
22
|
+
puts ''
|
|
23
|
+
|
|
24
|
+
# Example 2: Render to file
|
|
25
|
+
puts '2. Rendering to file...'
|
|
26
|
+
begin
|
|
27
|
+
Mmd2svg.render(simple_diagram, output: 'demo_output.svg')
|
|
28
|
+
puts '✓ Saved to demo_output.svg'
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
puts "✗ Error: #{e.message}"
|
|
31
|
+
end
|
|
32
|
+
puts ''
|
|
33
|
+
|
|
34
|
+
# Example 3: Using configuration
|
|
35
|
+
puts '3. Using custom configuration...'
|
|
36
|
+
Mmd2svg.configure do |config|
|
|
37
|
+
config.theme = 'dark'
|
|
38
|
+
config.background_color = 'transparent'
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
complex_diagram = <<~MERMAID
|
|
42
|
+
sequenceDiagram
|
|
43
|
+
participant A as Alice
|
|
44
|
+
participant B as Bob
|
|
45
|
+
A->>B: Hello Bob!
|
|
46
|
+
B->>A: Hello Alice!
|
|
47
|
+
MERMAID
|
|
48
|
+
|
|
49
|
+
begin
|
|
50
|
+
Mmd2svg.render(complex_diagram, output: 'demo_dark.svg')
|
|
51
|
+
puts '✓ Saved dark theme diagram to demo_dark.svg'
|
|
52
|
+
rescue StandardError => e
|
|
53
|
+
puts "✗ Error: #{e.message}"
|
|
54
|
+
end
|
|
55
|
+
puts ''
|
|
56
|
+
|
|
57
|
+
# Example 4: Batch conversion
|
|
58
|
+
puts '4. Batch conversion of examples...'
|
|
59
|
+
begin
|
|
60
|
+
results = Mmd2svg.render_batch(
|
|
61
|
+
'../examples/',
|
|
62
|
+
output_dir: 'demo_batch_output/',
|
|
63
|
+
theme: 'forest'
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
puts '✓ Batch conversion completed:'
|
|
67
|
+
puts " - Succeeded: #{results[:succeeded].count}"
|
|
68
|
+
puts " - Failed: #{results[:failed].count}"
|
|
69
|
+
|
|
70
|
+
results[:succeeded].each do |result|
|
|
71
|
+
puts " ✓ #{result[:input]} → #{result[:output]}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
results[:failed].each do |result|
|
|
75
|
+
puts " ✗ #{result[:file]}: #{result[:error]}"
|
|
76
|
+
end
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
puts "✗ Error: #{e.message}"
|
|
79
|
+
end
|
|
80
|
+
puts ''
|
|
81
|
+
|
|
82
|
+
puts '=== Demo Complete ==='
|
|
83
|
+
puts ''
|
|
84
|
+
puts 'Check the generated files:'
|
|
85
|
+
puts ' - demo_output.svg'
|
|
86
|
+
puts ' - demo_dark.svg'
|
|
87
|
+
puts ' - demo_batch_output/'
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
sequenceDiagram
|
|
2
|
+
participant Alice
|
|
3
|
+
participant Bob
|
|
4
|
+
Alice->>John: Hello John, how are you?
|
|
5
|
+
loop Healthcheck
|
|
6
|
+
John->>John: Fight against hypochondria
|
|
7
|
+
end
|
|
8
|
+
Note right of John: Rational thoughts<br/>prevail!
|
|
9
|
+
John-->>Alice: Great!
|
|
10
|
+
John->>Bob: How about you?
|
|
11
|
+
Bob-->>John: Jolly good!
|
data/exe/mmd2svg
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
|
|
5
|
+
module Mmd2svg
|
|
6
|
+
class BatchRenderer
|
|
7
|
+
def initialize(config = Config.new)
|
|
8
|
+
@config = config
|
|
9
|
+
@renderer = Renderer.new(config)
|
|
10
|
+
@results = { succeeded: [], failed: [] }
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def render_batch(input, output_dir:)
|
|
14
|
+
files = FileFinder.find_files(input, recursive: @config.recursive)
|
|
15
|
+
raise FileNotFoundError, "No Mermaid files found in: #{input}" if files.empty?
|
|
16
|
+
|
|
17
|
+
base_dir = File.directory?(input) ? input : nil
|
|
18
|
+
files.each do |file|
|
|
19
|
+
process_file(file, output_dir, base_dir)
|
|
20
|
+
end
|
|
21
|
+
@results
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def process_file(input_file, output_dir, base_dir)
|
|
27
|
+
output_file = FileFinder.output_path(input_file, output_dir, base_dir)
|
|
28
|
+
FileUtils.mkdir_p(File.dirname(output_file))
|
|
29
|
+
if !@config.overwrite && File.exist?(output_file)
|
|
30
|
+
@results[:failed] << {
|
|
31
|
+
file: input_file,
|
|
32
|
+
error: "Output file already exists: #{output_file}"
|
|
33
|
+
}
|
|
34
|
+
return
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
mermaid_code = File.read(input_file)
|
|
38
|
+
@renderer.render(mermaid_code, output: output_file)
|
|
39
|
+
@results[:succeeded] << {
|
|
40
|
+
input: input_file,
|
|
41
|
+
output: output_file
|
|
42
|
+
}
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
@results[:failed] << {
|
|
45
|
+
file: input_file,
|
|
46
|
+
error: e.message
|
|
47
|
+
}
|
|
48
|
+
raise unless @config.skip_errors
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
data/lib/mmd2svg/cli.rb
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
module Mmd2svg
|
|
7
|
+
class CLI < Thor
|
|
8
|
+
def self.exit_on_failure?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
class_option :config,
|
|
13
|
+
type: :string,
|
|
14
|
+
aliases: '-c',
|
|
15
|
+
desc: 'Config file path (default: .mermaid2svg.yml)'
|
|
16
|
+
|
|
17
|
+
desc 'INPUT', 'Convert Mermaid diagram(s) to SVG'
|
|
18
|
+
option :output,
|
|
19
|
+
type: :string,
|
|
20
|
+
aliases: '-o',
|
|
21
|
+
desc: 'Output file or directory path (default: INPUT.svg)'
|
|
22
|
+
option :theme,
|
|
23
|
+
type: :string,
|
|
24
|
+
aliases: '-t',
|
|
25
|
+
desc: 'Theme: default, dark, forest, neutral'
|
|
26
|
+
option :background,
|
|
27
|
+
type: :string,
|
|
28
|
+
aliases: '-b',
|
|
29
|
+
desc: 'Background color (transparent, white, or hex)'
|
|
30
|
+
option :width,
|
|
31
|
+
type: :numeric,
|
|
32
|
+
aliases: '-w',
|
|
33
|
+
desc: 'Output width in pixels'
|
|
34
|
+
option :height,
|
|
35
|
+
type: :numeric,
|
|
36
|
+
aliases: '-h',
|
|
37
|
+
desc: 'Output height in pixels'
|
|
38
|
+
option :recursive,
|
|
39
|
+
type: :boolean,
|
|
40
|
+
aliases: '-r',
|
|
41
|
+
default: false,
|
|
42
|
+
desc: 'Process directories recursively'
|
|
43
|
+
option :timeout,
|
|
44
|
+
type: :numeric,
|
|
45
|
+
desc: 'Puppeteer timeout in milliseconds'
|
|
46
|
+
option :skip_errors,
|
|
47
|
+
type: :boolean,
|
|
48
|
+
default: false,
|
|
49
|
+
desc: 'Continue processing even if errors occur'
|
|
50
|
+
|
|
51
|
+
def convert(input)
|
|
52
|
+
config = load_config
|
|
53
|
+
apply_options_to_config(config)
|
|
54
|
+
output = determine_output(input)
|
|
55
|
+
if batch_conversion?(input, output)
|
|
56
|
+
perform_batch_conversion(input, output, config)
|
|
57
|
+
else
|
|
58
|
+
perform_single_conversion(input, output, config)
|
|
59
|
+
end
|
|
60
|
+
rescue Mermaid2svg::Error => e
|
|
61
|
+
error_exit(e.message)
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
error_exit("Unexpected error: #{e.message}")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
desc 'version', 'Show version'
|
|
67
|
+
def version
|
|
68
|
+
puts "mermaid2svg version #{Mermaid2svg::VERSION}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
desc 'help [COMMAND]', 'Describe available commands or one specific command'
|
|
72
|
+
def help(command = nil)
|
|
73
|
+
if command.nil?
|
|
74
|
+
print_usage
|
|
75
|
+
else
|
|
76
|
+
super
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
default_task :convert
|
|
81
|
+
|
|
82
|
+
def self.start(given_args = ARGV, config = {})
|
|
83
|
+
if given_args.any? && !%w[convert version help].include?(given_args.first) && !given_args.first.start_with?('-')
|
|
84
|
+
given_args = ['convert'] + given_args
|
|
85
|
+
end
|
|
86
|
+
super(given_args, config)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def print_usage
|
|
92
|
+
puts "Usage: mermaid2svg INPUT [OPTIONS]"
|
|
93
|
+
puts ""
|
|
94
|
+
puts "Convert Mermaid diagram(s) to SVG"
|
|
95
|
+
puts ""
|
|
96
|
+
puts "Arguments:"
|
|
97
|
+
puts " INPUT Input file, directory, or glob pattern"
|
|
98
|
+
puts ""
|
|
99
|
+
puts "Options:"
|
|
100
|
+
puts " -o, --output=PATH Output file or directory path (default: INPUT.svg)"
|
|
101
|
+
puts " -t, --theme=THEME Theme: default, dark, forest, neutral"
|
|
102
|
+
puts " -b, --background=COLOR Background color (transparent, white, or hex)"
|
|
103
|
+
puts " -w, --width=WIDTH Output width in pixels"
|
|
104
|
+
puts " -h, --height=HEIGHT Output height in pixels"
|
|
105
|
+
puts " -r, --recursive Process directories recursively"
|
|
106
|
+
puts " --timeout=MILLISECONDS Puppeteer timeout in milliseconds (default: 30000)"
|
|
107
|
+
puts " --skip-errors Continue processing even if errors occur"
|
|
108
|
+
puts " -c, --config=FILE Config file path (default: .mermaid2svg.yml)"
|
|
109
|
+
puts ""
|
|
110
|
+
puts "Commands:"
|
|
111
|
+
puts " mermaid2svg version Show version"
|
|
112
|
+
puts " mermaid2svg help Show this help message"
|
|
113
|
+
puts ""
|
|
114
|
+
puts "Examples:"
|
|
115
|
+
puts " mermaid2svg diagram.mmd # Convert to diagram.svg"
|
|
116
|
+
puts " mermaid2svg diagram.mmd -o output.svg # Specify output file"
|
|
117
|
+
puts " mermaid2svg diagram.mmd --theme dark # Use dark theme"
|
|
118
|
+
puts " mermaid2svg examples/ -o output/ # Batch conversion"
|
|
119
|
+
puts " mermaid2svg examples/ -o output/ --recursive # Recursive batch conversion"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def determine_output(input)
|
|
123
|
+
return options[:output] if options[:output]
|
|
124
|
+
|
|
125
|
+
if File.file?(input)
|
|
126
|
+
input.sub(/\.(mmd|mermaid)$/i, '.svg')
|
|
127
|
+
else
|
|
128
|
+
'output'
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def load_config
|
|
133
|
+
config_file = options[:config] || Config.find_config_file
|
|
134
|
+
if config_file && File.exist?(config_file)
|
|
135
|
+
puts "Loading config from: #{config_file}" if options[:verbose]
|
|
136
|
+
Config.load_from_file(config_file)
|
|
137
|
+
else
|
|
138
|
+
Config.new
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def apply_options_to_config(config)
|
|
143
|
+
config.theme = options[:theme] if options[:theme]
|
|
144
|
+
config.background_color = options[:background] if options[:background]
|
|
145
|
+
config.width = options[:width] if options[:width]
|
|
146
|
+
config.height = options[:height] if options[:height]
|
|
147
|
+
config.puppeteer_timeout = options[:timeout] if options[:timeout]
|
|
148
|
+
config.recursive = options[:recursive]
|
|
149
|
+
config.skip_errors = options[:skip_errors]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def batch_conversion?(input, output)
|
|
153
|
+
File.directory?(input) || input.include?('*') ||
|
|
154
|
+
(File.exist?(input) && File.exist?(output) && File.directory?(output))
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def perform_batch_conversion(input, output, config)
|
|
158
|
+
output_dir = output
|
|
159
|
+
|
|
160
|
+
puts "Processing files from: #{input}"
|
|
161
|
+
puts "Output directory: #{output_dir}"
|
|
162
|
+
puts ''
|
|
163
|
+
|
|
164
|
+
batch_renderer = BatchRenderer.new(config)
|
|
165
|
+
results = batch_renderer.render_batch(input, output_dir: output_dir)
|
|
166
|
+
|
|
167
|
+
display_batch_results(results)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def perform_single_conversion(input, output, config)
|
|
171
|
+
raise FileNotFoundError, "Input file not found: #{input}" unless File.exist?(input)
|
|
172
|
+
|
|
173
|
+
mermaid_code = File.read(input)
|
|
174
|
+
renderer = Renderer.new(config)
|
|
175
|
+
renderer.render(mermaid_code, output: output)
|
|
176
|
+
puts "✓ #{input} → #{output}"
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def display_batch_results(results)
|
|
180
|
+
results[:succeeded].each do |result|
|
|
181
|
+
puts "✓ #{result[:input]} → #{result[:output]}"
|
|
182
|
+
end
|
|
183
|
+
results[:failed].each do |result|
|
|
184
|
+
puts "✗ #{result[:file]} → Error: #{result[:error]}"
|
|
185
|
+
end
|
|
186
|
+
puts ''
|
|
187
|
+
puts "#{results[:succeeded].count} succeeded, #{results[:failed].count} failed"
|
|
188
|
+
exit(1) if results[:failed].any? && !options[:skip_errors]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def error_exit(message)
|
|
192
|
+
warn "Error: #{message}"
|
|
193
|
+
exit(1)
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'yaml'
|
|
4
|
+
|
|
5
|
+
module Mmd2svg
|
|
6
|
+
class Config
|
|
7
|
+
DEFAULT_CONFIG = {
|
|
8
|
+
'theme' => 'default',
|
|
9
|
+
'background_color' => 'white',
|
|
10
|
+
'puppeteer' => {
|
|
11
|
+
'headless' => true,
|
|
12
|
+
'timeout' => 30_000,
|
|
13
|
+
'args' => ['--no-sandbox', '--disable-setuid-sandbox']
|
|
14
|
+
},
|
|
15
|
+
'mermaid' => {
|
|
16
|
+
'securityLevel' => 'loose',
|
|
17
|
+
'startOnLoad' => true,
|
|
18
|
+
'logLevel' => 'error'
|
|
19
|
+
},
|
|
20
|
+
'batch' => {
|
|
21
|
+
'recursive' => false,
|
|
22
|
+
'overwrite' => true,
|
|
23
|
+
'skip_errors' => false
|
|
24
|
+
}
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
attr_accessor :theme, :background_color, :width, :height,
|
|
28
|
+
:puppeteer_timeout, :puppeteer_headless, :puppeteer_args,
|
|
29
|
+
:mermaid_config, :recursive, :overwrite, :skip_errors
|
|
30
|
+
|
|
31
|
+
def initialize(config_hash = {})
|
|
32
|
+
merged_config = DEFAULT_CONFIG.merge(config_hash)
|
|
33
|
+
|
|
34
|
+
@theme = merged_config['theme']
|
|
35
|
+
@background_color = merged_config['background_color']
|
|
36
|
+
@width = merged_config['width']
|
|
37
|
+
@height = merged_config['height']
|
|
38
|
+
|
|
39
|
+
@puppeteer_headless = merged_config['puppeteer']['headless']
|
|
40
|
+
@puppeteer_timeout = merged_config['puppeteer']['timeout']
|
|
41
|
+
@puppeteer_args = merged_config['puppeteer']['args']
|
|
42
|
+
|
|
43
|
+
@mermaid_config = merged_config['mermaid']
|
|
44
|
+
|
|
45
|
+
@recursive = merged_config['batch']['recursive']
|
|
46
|
+
@overwrite = merged_config['batch']['overwrite']
|
|
47
|
+
@skip_errors = merged_config['batch']['skip_errors']
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.load_from_file(file_path)
|
|
51
|
+
return new unless File.exist?(file_path)
|
|
52
|
+
|
|
53
|
+
config_hash = YAML.load_file(file_path)
|
|
54
|
+
new(config_hash)
|
|
55
|
+
rescue StandardError => e
|
|
56
|
+
raise ConfigError, "Failed to load config file: #{e.message}"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.find_config_file
|
|
60
|
+
current_dir = Dir.pwd
|
|
61
|
+
config_file = File.join(current_dir, '.mmd2svg.yml')
|
|
62
|
+
|
|
63
|
+
return config_file if File.exist?(config_file)
|
|
64
|
+
|
|
65
|
+
nil
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def to_h
|
|
69
|
+
{
|
|
70
|
+
theme: @theme,
|
|
71
|
+
background_color: @background_color,
|
|
72
|
+
width: @width,
|
|
73
|
+
height: @height,
|
|
74
|
+
puppeteer_timeout: @puppeteer_timeout,
|
|
75
|
+
puppeteer_headless: @puppeteer_headless,
|
|
76
|
+
puppeteer_args: @puppeteer_args,
|
|
77
|
+
mermaid_config: @mermaid_config,
|
|
78
|
+
recursive: @recursive,
|
|
79
|
+
overwrite: @overwrite,
|
|
80
|
+
skip_errors: @skip_errors
|
|
81
|
+
}
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def dup
|
|
85
|
+
Config.new(DEFAULT_CONFIG.merge(to_h.transform_keys(&:to_s)))
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Mmd2svg
|
|
4
|
+
class FileFinder
|
|
5
|
+
MERMAID_EXTENSIONS = %w[.mmd .mermaid].freeze
|
|
6
|
+
|
|
7
|
+
def self.find_files(input, recursive: false)
|
|
8
|
+
if File.directory?(input)
|
|
9
|
+
find_in_directory(input, recursive)
|
|
10
|
+
elsif input.include?('*')
|
|
11
|
+
Dir.glob(input).select { |f| File.file?(f) && mermaid_file?(f) }
|
|
12
|
+
elsif File.file?(input)
|
|
13
|
+
mermaid_file?(input) ? [input] : []
|
|
14
|
+
else
|
|
15
|
+
raise FileNotFoundError, "Input not found: #{input}"
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.find_in_directory(dir, recursive)
|
|
20
|
+
pattern = recursive ? File.join(dir, '**', '*') : File.join(dir, '*')
|
|
21
|
+
Dir.glob(pattern).select { |f| File.file?(f) && mermaid_file?(f) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.mermaid_file?(path)
|
|
25
|
+
MERMAID_EXTENSIONS.include?(File.extname(path).downcase)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.output_path(input_path, output_dir, base_dir = nil)
|
|
29
|
+
relative_path =
|
|
30
|
+
if base_dir
|
|
31
|
+
input_path.sub(%r{^#{Regexp.escape(base_dir)}/}, '')
|
|
32
|
+
else
|
|
33
|
+
File.basename(input_path)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
output_file = relative_path.sub(/\.(mmd|mermaid)$/i, '.svg')
|
|
37
|
+
File.join(output_dir, output_file)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'puppeteer'
|
|
4
|
+
require 'tempfile'
|
|
5
|
+
|
|
6
|
+
module Mmd2svg
|
|
7
|
+
class Renderer
|
|
8
|
+
def initialize(config = Config.new)
|
|
9
|
+
@config = config
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def render(mermaid_code, output: nil)
|
|
13
|
+
svg_content = render_to_string(mermaid_code)
|
|
14
|
+
|
|
15
|
+
if output
|
|
16
|
+
File.write(output, svg_content)
|
|
17
|
+
output
|
|
18
|
+
else
|
|
19
|
+
svg_content
|
|
20
|
+
end
|
|
21
|
+
rescue StandardError => e
|
|
22
|
+
raise RenderError, "Failed to render Mermaid diagram: #{e.message}"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render_to_string(mermaid_code)
|
|
26
|
+
Puppeteer.launch(
|
|
27
|
+
headless: @config.puppeteer_headless,
|
|
28
|
+
args: @config.puppeteer_args
|
|
29
|
+
) do |browser|
|
|
30
|
+
page = browser.new_page
|
|
31
|
+
|
|
32
|
+
html_content = build_html(mermaid_code)
|
|
33
|
+
temp_file = create_temp_html(html_content)
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
page.goto("file://#{temp_file.path}", wait_until: 'networkidle0')
|
|
37
|
+
page.wait_for_function('() => window.mermaidReady === true', timeout: @config.puppeteer_timeout)
|
|
38
|
+
escaped_code = escape_js(mermaid_code)
|
|
39
|
+
svg_content = page.evaluate(<<~JS)
|
|
40
|
+
async () => {
|
|
41
|
+
const code = `#{escaped_code}`;
|
|
42
|
+
return await window.renderMermaid(code);
|
|
43
|
+
}
|
|
44
|
+
JS
|
|
45
|
+
|
|
46
|
+
apply_size(svg_content) if @config.width || @config.height
|
|
47
|
+
|
|
48
|
+
svg_content
|
|
49
|
+
ensure
|
|
50
|
+
temp_file.close
|
|
51
|
+
temp_file.unlink
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
rescue Puppeteer::TimeoutError
|
|
55
|
+
raise PuppeteerError, "Puppeteer timeout after #{@config.puppeteer_timeout}ms"
|
|
56
|
+
rescue StandardError => e
|
|
57
|
+
raise RenderError, "Failed to render: #{e.message}"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def build_html(_mermaid_code)
|
|
63
|
+
template = File.read(template_path)
|
|
64
|
+
template.gsub('%<theme>s', @config.theme)
|
|
65
|
+
.gsub('%<security_level>s', @config.mermaid_config['securityLevel'])
|
|
66
|
+
.gsub('%<log_level>s', @config.mermaid_config['logLevel'])
|
|
67
|
+
.gsub('%<background_color>s', @config.background_color)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def template_path
|
|
71
|
+
File.expand_path('../../templates/render.html', __dir__)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def create_temp_html(content)
|
|
75
|
+
temp = Tempfile.new(['mermaid', '.html'])
|
|
76
|
+
temp.write(content)
|
|
77
|
+
temp.flush
|
|
78
|
+
temp
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def escape_js(str)
|
|
82
|
+
str.gsub('\\', '\\\\\\\\')
|
|
83
|
+
.gsub('`', '\\`')
|
|
84
|
+
.gsub('$', '\\$')
|
|
85
|
+
.gsub("\n", '\\n')
|
|
86
|
+
.gsub("\r", '\\r')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def apply_size(svg_content)
|
|
90
|
+
svg_content.sub!(/width="[^"]*"/, %(width="#{@config.width}")) if @config.width
|
|
91
|
+
svg_content.sub!(/height="[^"]*"/, %(height="#{@config.height}")) if @config.height
|
|
92
|
+
svg_content
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
data/lib/mmd2svg.rb
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'mmd2svg/version'
|
|
4
|
+
require_relative 'mmd2svg/errors'
|
|
5
|
+
require_relative 'mmd2svg/config'
|
|
6
|
+
require_relative 'mmd2svg/renderer'
|
|
7
|
+
require_relative 'mmd2svg/file_finder'
|
|
8
|
+
require_relative 'mmd2svg/batch_renderer'
|
|
9
|
+
|
|
10
|
+
module Mmd2svg
|
|
11
|
+
class << self
|
|
12
|
+
attr_writer :config
|
|
13
|
+
|
|
14
|
+
def config
|
|
15
|
+
@config ||= Config.new
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield(config) if block_given?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Render a single Mermaid diagram
|
|
23
|
+
# @param code [String] Mermaid diagram code or file path
|
|
24
|
+
# @param output [String, nil] Output file path (optional)
|
|
25
|
+
# @param options [Hash] Rendering options
|
|
26
|
+
# @return [String] SVG content or output file path
|
|
27
|
+
def render(code, output: nil, **options)
|
|
28
|
+
local_config = build_config(options)
|
|
29
|
+
renderer = Renderer.new(local_config)
|
|
30
|
+
|
|
31
|
+
mermaid_code = File.exist?(code) ? File.read(code) : code
|
|
32
|
+
renderer.render(mermaid_code, output: output)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Render Mermaid diagram to string
|
|
36
|
+
# @param code [String] Mermaid diagram code
|
|
37
|
+
# @param options [Hash] Rendering options
|
|
38
|
+
# @return [String] SVG content
|
|
39
|
+
def render_to_string(code, **options)
|
|
40
|
+
local_config = build_config(options)
|
|
41
|
+
renderer = Renderer.new(local_config)
|
|
42
|
+
|
|
43
|
+
renderer.render_to_string(code)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Batch render multiple Mermaid files
|
|
47
|
+
# @param input [String] Input directory or glob pattern
|
|
48
|
+
# @param output_dir [String] Output directory
|
|
49
|
+
# @param options [Hash] Rendering options
|
|
50
|
+
# @return [Hash] Results with :succeeded and :failed arrays
|
|
51
|
+
def render_batch(input, output_dir:, **options)
|
|
52
|
+
local_config = build_config(options)
|
|
53
|
+
batch_renderer = BatchRenderer.new(local_config)
|
|
54
|
+
|
|
55
|
+
batch_renderer.render_batch(input, output_dir: output_dir)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def build_config(options)
|
|
61
|
+
local_config = config.dup
|
|
62
|
+
|
|
63
|
+
local_config.theme = options[:theme] if options[:theme]
|
|
64
|
+
local_config.background_color = options[:background_color] if options[:background_color]
|
|
65
|
+
local_config.width = options[:width] if options[:width]
|
|
66
|
+
local_config.height = options[:height] if options[:height]
|
|
67
|
+
local_config.puppeteer_timeout = options[:timeout] if options[:timeout]
|
|
68
|
+
local_config.recursive = options[:recursive] if options.key?(:recursive)
|
|
69
|
+
local_config.skip_errors = options[:skip_errors] if options.key?(:skip_errors)
|
|
70
|
+
|
|
71
|
+
local_config
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
RSpec.describe Mmd2svg do
|
|
4
|
+
it 'has a version number' do
|
|
5
|
+
expect(Mmd2svg::VERSION).not_to be nil
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
describe '.render_to_string' do
|
|
9
|
+
it 'renders a simple graph to SVG string' do
|
|
10
|
+
mermaid_code = <<~MERMAID
|
|
11
|
+
graph TD
|
|
12
|
+
A[Start] --> B[End]
|
|
13
|
+
MERMAID
|
|
14
|
+
|
|
15
|
+
svg = Mmd2svg.render_to_string(mermaid_code)
|
|
16
|
+
|
|
17
|
+
expect(svg).to be_a(String)
|
|
18
|
+
expect(svg).to include('<svg')
|
|
19
|
+
expect(svg).to include('</svg>')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
describe '.configure' do
|
|
24
|
+
it 'allows configuration via block' do
|
|
25
|
+
Mmd2svg.configure do |config|
|
|
26
|
+
config.theme = 'dark'
|
|
27
|
+
config.background_color = 'transparent'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
expect(Mmd2svg.config.theme).to eq('dark')
|
|
31
|
+
expect(Mmd2svg.config.background_color).to eq('transparent')
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/spec/spec_helper.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<script type="module">
|
|
7
|
+
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
|
|
8
|
+
|
|
9
|
+
const config = {
|
|
10
|
+
startOnLoad: false,
|
|
11
|
+
theme: '%<theme>s',
|
|
12
|
+
securityLevel: '%<security_level>s',
|
|
13
|
+
logLevel: '%<log_level>s'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
mermaid.initialize(config);
|
|
17
|
+
|
|
18
|
+
window.renderMermaid = async function (code) {
|
|
19
|
+
try {
|
|
20
|
+
const { svg } = await mermaid.render('mermaid-diagram', code);
|
|
21
|
+
return svg;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new Error('Mermaid rendering failed: ' + error.message);
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
window.mermaidReady = true;
|
|
28
|
+
</script>
|
|
29
|
+
<style>
|
|
30
|
+
body {
|
|
31
|
+
margin: 0;
|
|
32
|
+
padding: 20px;
|
|
33
|
+
background-color: %<background_color>s;
|
|
34
|
+
}
|
|
35
|
+
#container {
|
|
36
|
+
display: inline-block;
|
|
37
|
+
}
|
|
38
|
+
</style>
|
|
39
|
+
</head>
|
|
40
|
+
<body>
|
|
41
|
+
<div id="container"></div>
|
|
42
|
+
</body>
|
|
43
|
+
</html>
|
metadata
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mmd2svg
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Yudai Takada
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: puppeteer-ruby
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.45'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.45'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: thor
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.3'
|
|
40
|
+
description: A command-line tool and library to convert Mermaid diagram definitions
|
|
41
|
+
into SVG files.
|
|
42
|
+
email:
|
|
43
|
+
- t.yudai92@gmail.com
|
|
44
|
+
executables:
|
|
45
|
+
- mmd2svg
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- ".rspec"
|
|
50
|
+
- CHANGELOG.md
|
|
51
|
+
- LICENSE.txt
|
|
52
|
+
- README.md
|
|
53
|
+
- Rakefile
|
|
54
|
+
- examples/class.mmd
|
|
55
|
+
- examples/demo.rb
|
|
56
|
+
- examples/flowchart.mmd
|
|
57
|
+
- examples/sequence.mmd
|
|
58
|
+
- exe/mmd2svg
|
|
59
|
+
- lib/mmd2svg.rb
|
|
60
|
+
- lib/mmd2svg/batch_renderer.rb
|
|
61
|
+
- lib/mmd2svg/cli.rb
|
|
62
|
+
- lib/mmd2svg/config.rb
|
|
63
|
+
- lib/mmd2svg/errors.rb
|
|
64
|
+
- lib/mmd2svg/file_finder.rb
|
|
65
|
+
- lib/mmd2svg/renderer.rb
|
|
66
|
+
- lib/mmd2svg/version.rb
|
|
67
|
+
- spec/mermaid2svg_spec.rb
|
|
68
|
+
- spec/spec_helper.rb
|
|
69
|
+
- templates/render.html
|
|
70
|
+
homepage: https://github.com/ydah/mmd2svg
|
|
71
|
+
licenses:
|
|
72
|
+
- MIT
|
|
73
|
+
metadata:
|
|
74
|
+
homepage_uri: https://github.com/ydah/mmd2svg
|
|
75
|
+
source_code_uri: https://github.com/ydah/mmd2svg
|
|
76
|
+
changelog_uri: https://github.com/ydah/mmd2svg/blob/main/CHANGELOG.md
|
|
77
|
+
rubygems_mfa_required: 'true'
|
|
78
|
+
rdoc_options: []
|
|
79
|
+
require_paths:
|
|
80
|
+
- lib
|
|
81
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: 3.2.0
|
|
86
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
87
|
+
requirements:
|
|
88
|
+
- - ">="
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '0'
|
|
91
|
+
requirements: []
|
|
92
|
+
rubygems_version: 3.7.2
|
|
93
|
+
specification_version: 4
|
|
94
|
+
summary: Convert Mermaid diagrams to SVG
|
|
95
|
+
test_files: []
|