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 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
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2025-10-29
4
+
5
+ - Initial release
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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
@@ -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,6 @@
1
+ graph TD
2
+ A[Start] --> B{Is it working?}
3
+ B -->|Yes| C[Great!]
4
+ B -->|No| D[Debug]
5
+ D --> A
6
+ C --> E[End]
@@ -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,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/mmd2svg'
5
+ require_relative '../lib/mmd2svg/cli'
6
+
7
+ Mmd2svg::CLI.start(ARGV)
@@ -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
@@ -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,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mmd2svg
4
+ class Error < StandardError; end
5
+ class RenderError < Error; end
6
+ class ConfigError < Error; end
7
+ class FileNotFoundError < Error; end
8
+ class PuppeteerError < Error; end
9
+ 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Mmd2svg
4
+ VERSION = '0.1.0'
5
+ 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
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mmd2svg'
4
+
5
+ RSpec.configure do |config|
6
+ config.example_status_persistence_file_path = '.rspec_status'
7
+ config.disable_monkey_patching!
8
+ config.expect_with :rspec do |c|
9
+ c.syntax = :expect
10
+ end
11
+ end
@@ -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: []