rails-mermaid_erd 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 05ed1d4ef4a6d7343bd286c7644ad8e5b41acd27743dc5af7d387a919c2fa4a8
4
+ data.tar.gz: 0f88664e5879bdbd2a3ff79c01976cf016871a8ab8b16af777d6a19a5d78ba87
5
+ SHA512:
6
+ metadata.gz: 430cd950e08598728145f1f51c83a5a35700e82c4e2f5274b8b68906e0ed1c7f67a44046ce0eee533b57c17623367149173c6385c884183f0e395cb04601089a
7
+ data.tar.gz: 39d7fee31257a11024666023100c3ac55e3d2b17513f48fbff5bd8da305acc84bd36d538cb47fd32c649315e0e650e0ce623d2df3d6acc0845c05fa06982f31b
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2022
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Rails Mermaid ERD
2
+
3
+ Generate [Mermaid ERD](https://mermaid-js.github.io/mermaid/#/entityRelationshipDiagram) from your Ruby on Rails application.
4
+
5
+ [<img src="./docs/screen_shot.png" width="50%">](./docs/screen_shot.png)
6
+
7
+ [Demo Page](https://koedame.github.io/rails-mermaid_erd/example.html)
8
+
9
+ Mermaid ERD can be generated at will.
10
+ The generated ERD can be copied in Markdown format, so they can be easily shared on GitHub.
11
+ You can also save it as an image, so it can be used in environments where Mermaid is not available.
12
+ The editor is a single HTML file, so the entire editor can be shared.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```ruby
19
+ gem "rails-mermaid_erd", group: :development
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ $ bundle install
26
+ ```
27
+
28
+ ## Usage
29
+
30
+ Run rake task `mermaid_erd` will generate `<app_root>/mermaid_erd/index.html`.
31
+
32
+ ```bash
33
+ $ bundle exec rails mermaid_erd
34
+ # or
35
+ $ bundle exec rake mermaid_erd
36
+ ```
37
+
38
+ Simply open the generated `<app_root>/mermaid_erd/index.html` in your browser.
39
+
40
+ This file is not required for Git management, so you can add it to `.gitignore` if necessary
41
+
42
+ ```.gitignore
43
+ mermaid_erd
44
+ ```
45
+
46
+ `<app_root>/mermaid_erd/index.html` is single HTML file.
47
+ If you share this file, it can be used by those who do not have Ruby on Rails environment. Or, you can upload the file to a web server and share it with the same URL.
48
+
49
+ It would be very smart to generate it automatically using CI.
50
+
51
+ <!--
52
+ TODO:
53
+ ## Contributing
54
+
55
+ Contribution directions go here.
56
+ -->
57
+
58
+ ## License
59
+ 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,3 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
@@ -0,0 +1,15 @@
1
+ <svg width="276" height="276" viewBox="0 0 276 276" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6">
2
+ <g clip-path="url(#clip0_3_2)">
3
+ <rect width="276" height="276" rx="40" fill="#DC2626"/>
4
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M94 60.88C94 54.8443 98.9263 50 104.943 50H170.313C176.33 50 181.208 54.8929 181.208 60.9286L181.257 107.071C181.257 113.107 176.379 118 170.362 118C151.8 118 123.554 118 104.992 118C98.9748 118 94.0485 113.058 94.0485 107.023L94 60.88Z" fill="white"/>
5
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M32 168.88C32 162.844 36.9263 158 42.9434 158H108.313C114.33 158 119.208 162.893 119.208 168.929L119.257 215.071C119.257 221.107 114.379 226 108.362 226C89.8 226 61.5537 226 42.9919 226C36.9748 226 32.0485 221.058 32.0485 215.023L32 168.88Z" fill="white"/>
6
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M156 168.88C156 162.844 160.926 158 166.943 158H232.313C238.33 158 243.208 162.893 243.208 168.929L243.257 215.071C243.257 221.107 238.379 226 232.362 226C213.8 226 185.554 226 166.992 226C160.975 226 156.048 221.058 156.048 215.023L156 168.88Z" fill="white"/>
7
+ <path d="M134.142 109.071L141.213 102L226.066 186.853L218.995 193.924L134.142 109.071Z" fill="white"/>
8
+ <path d="M133.853 102L140.924 109.071L56.0711 193.924L49 186.853L133.853 102Z" fill="white"/>
9
+ </g>
10
+ <defs>
11
+ <clipPath id="clip0_3_2">
12
+ <rect width="276" height="276" fill="white"/>
13
+ </clipPath>
14
+ </defs>
15
+ </svg>
@@ -0,0 +1,3 @@
1
+ module RailsMermaidErd
2
+ VERSION = "0.1.1"
3
+ end
@@ -0,0 +1,202 @@
1
+ require "erb"
2
+ require "fileutils"
3
+ require "rake"
4
+ require "rake/dsl_definition"
5
+ require "rails-mermaid_erd/version"
6
+
7
+ module RailsMermaidErd
8
+ extend Rake::DSL
9
+
10
+ desc "Generate Mermaid ERD."
11
+ task mermaid_erd: :environment do
12
+ result = {
13
+ Models: [],
14
+ Relations: []
15
+ }
16
+
17
+ ::Rails.application.eager_load!
18
+ ::ActiveRecord::Base.descendants.each do |defined_model|
19
+ next unless defined_model.table_exists?
20
+ model = {
21
+ TableName: defined_model.table_name,
22
+ ModelName: defined_model.name,
23
+ IsModelExist: true,
24
+ Columns: []
25
+ }
26
+
27
+ next if defined_model.table_name.blank?
28
+
29
+ foreign_keys = ::ActiveRecord::Schema.foreign_keys(defined_model.table_name).map { |k| k.options[:column] }
30
+ primary_key = defined_model.primary_key
31
+
32
+ defined_model.columns.each do |column|
33
+ key = ""
34
+ if column.name == primary_key
35
+ key = "PK"
36
+ elsif foreign_keys.include?(column.name)
37
+ key = "FK"
38
+ end
39
+ model[:Columns] << {
40
+ name: column.name,
41
+ type: column.type,
42
+ key: key,
43
+ comment: column.comment
44
+ }
45
+ end
46
+
47
+ result[:Models] << model
48
+
49
+ next unless model[:IsModelExist]
50
+
51
+ defined_model.reflect_on_all_associations(:has_many).each do |h|
52
+ if h.options[:through]
53
+ next
54
+ end
55
+ if h.options[:class_name]
56
+ reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.options[:class_name] }
57
+ if reverse_relation
58
+ reverse_relation[:Comment] = if reverse_relation[:Comment] == ""
59
+ "has_many #{h.name}"
60
+ else
61
+ "#{reverse_relation[:Comment]} : has_many #{h.name}"
62
+ end
63
+ else
64
+ result[:Relations] << {
65
+ LeftModelName: model[:ModelName],
66
+ LeftValue: "||",
67
+ RightModelName: h.name.to_s.classify,
68
+ RightValue: "o{",
69
+ Comment: "has_many #{h.name}"
70
+ }
71
+ end
72
+ else
73
+ reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.name.to_s.classify }
74
+ if reverse_relation
75
+ else
76
+ result[:Relations] << {
77
+ LeftModelName: model[:ModelName],
78
+ LeftValue: "||",
79
+ RightModelName: h.name.to_s.classify,
80
+ RightValue: "o{",
81
+ Comment: ""
82
+ }
83
+ end
84
+ end
85
+ end
86
+
87
+ defined_model.reflect_on_all_associations(:belongs_to).each do |h|
88
+ if h.options[:class_name]
89
+ reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.options[:class_name] }
90
+
91
+ if reverse_relation
92
+ if (::Rails.application.config.active_record.belongs_to_required_by_default && h.options[:optional]) || (!::Rails.application.config.active_record.belongs_to_required_by_default && !h.options[:requried])
93
+ reverse_relation[:LeftValue] = "|o"
94
+ end
95
+ reverse_relation[:Comment] = if reverse_relation[:Comment] == ""
96
+ "belongs_to #{h.name}"
97
+ else
98
+ "#{reverse_relation[:Comment]} : belongs_to #{h.name}"
99
+ end
100
+ else
101
+ result[:Relations] << if (::Rails.application.config.active_record.belongs_to_required_by_default && h.options[:optional]) || (!::Rails.application.config.active_record.belongs_to_required_by_default && !h.options[:requried])
102
+ {
103
+ LeftModelName: model[:ModelName],
104
+ LeftValue: "}o",
105
+ RightModelName: h.options[:class_name],
106
+ RightValue: "o|",
107
+ Comment: "belongs_to #{h.name}"
108
+ }
109
+ else
110
+ {
111
+ LeftModelName: model[:ModelName],
112
+ LeftValue: "}o",
113
+ RightModelName: h.options[:class_name],
114
+ RightValue: "||",
115
+ Comment: "belongs_to #{h.name}"
116
+ }
117
+ end
118
+ end
119
+ else
120
+ reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.name.to_s.classify }
121
+
122
+ if reverse_relation
123
+ if (::Rails.application.config.active_record.belongs_to_required_by_default && h.options[:optional]) || (!::Rails.application.config.active_record.belongs_to_required_by_default && !h.options[:requried])
124
+ reverse_relation[:LeftValue] = "|o"
125
+ end
126
+ else
127
+ result[:Relations] << if (::Rails.application.config.active_record.belongs_to_required_by_default && h.options[:optional]) || (!::Rails.application.config.active_record.belongs_to_required_by_default && !h.options[:requried])
128
+ {
129
+ LeftModelName: model[:ModelName],
130
+ LeftValue: "}o",
131
+ RightModelName: h.name.to_s.classify,
132
+ RightValue: "o|",
133
+ Comment: ""
134
+ }
135
+ else
136
+ {
137
+ LeftModelName: model[:ModelName],
138
+ LeftValue: "}o",
139
+ RightModelName: h.name.to_s.classify,
140
+ RightValue: "||",
141
+ Comment: ""
142
+ }
143
+ end
144
+ end
145
+ end
146
+ end
147
+
148
+ defined_model.reflect_on_all_associations(:has_one).each do |h|
149
+ if h.options[:class_name]
150
+ reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.options[:class_name] }
151
+ if reverse_relation
152
+ reverse_relation[:LeftValue] = "|o"
153
+ reverse_relation[:Comment] = if reverse_relation[:Comment] == ""
154
+ "has_one #{h.name}"
155
+ else
156
+ "#{reverse_relation[:Comment]} : has_one #{h.name}"
157
+ end
158
+
159
+ if h.options[:through]
160
+ next
161
+ end
162
+ else
163
+ result[:Relations] << {
164
+ LeftModelName: model[:ModelName],
165
+ LeftValue: "||",
166
+ RightModelName: h.options[:class_name],
167
+ RightValue: "o|",
168
+ Comment: "has_one #{h.name}"
169
+ }
170
+ end
171
+ else
172
+ reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.name.to_s.classify }
173
+ if reverse_relation
174
+ reverse_relation[:LeftValue] = "|o"
175
+ if h.options[:through]
176
+ next
177
+ end
178
+ else
179
+ result[:Relations] << {
180
+ LeftModelName: model[:ModelName],
181
+ LeftValue: "||",
182
+ RightModelName: h.name.to_s.classify,
183
+ RightValue: "o|",
184
+ Comment: "has_one #{h.name}"
185
+ }
186
+ end
187
+ end
188
+ end
189
+ end
190
+
191
+ version = VERSION
192
+ app_name = ::Rails.application.class.try(:parent_name) || ::Rails.application.class.try(:module_parent_name)
193
+ logo = File.read(File.expand_path("./assets/logo.svg", __dir__))
194
+ erb = ERB.new(File.read(File.expand_path("./templates/index.html.erb", __dir__)))
195
+ result_html = erb.result(binding)
196
+
197
+ result_dir = Rails.root.join("./mermaid_erd")
198
+ result_file = result_dir.join("./index.html")
199
+ FileUtils.mkdir_p(result_dir)
200
+ File.write(result_file, result_html)
201
+ end
202
+ end
@@ -0,0 +1,584 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title><%= app_name %> - Rails Mermaid ERD</title>
6
+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8
+ <link rel="icon" href="data:image/svg+xml;base64,<%= Base64.encode64(logo) %>">
9
+ <style>
10
+ #mermaid-erd {
11
+ fill: #9c9c9c !important;
12
+ }
13
+ </style>
14
+ </head>
15
+ <body>
16
+ <div id="app">
17
+ <header class="bg-red-600">
18
+ <nav class="mx-auto px-4">
19
+ <div class="flex w-full items-center justify-between py-4">
20
+ <h1 class="text-white inline-flex space-x-1 items-center">
21
+ <%= logo %>
22
+ <span class="font-bold">Rails Mermaid ERD</span>
23
+ </h1>
24
+ <div class="space-x-4 inline-flex items-center">
25
+ <select v-model="language" @change="setLanguage(language)" class="text-xs border-0 rounded py-1 pl-3 pr-8 focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-red-600">
26
+ <option value="en">English</option>
27
+ <option value="ja">Japanese - 日本語</option>
28
+ </select>
29
+ <a href="https://github.com/koedame/rails-mermaid_erd" class="text-white hover:text-red-200" target="_blank" rel="noopener noreferrer">
30
+ <svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
31
+ <path
32
+ fillRule="evenodd"
33
+ d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
34
+ clipRule="evenodd"
35
+ />
36
+ </svg>
37
+ </a>
38
+ </div>
39
+ </div>
40
+ </nav>
41
+ </header>
42
+
43
+ <div class="flex">
44
+ <div class="w-[250px] border-r border-gray-300 h-full flex-none p-2">
45
+ <div class="p-2">
46
+ <h2 class="font-medium text-gray-900 mb-1">{{i18n[language]["actions"]["title"]}}</h2>
47
+
48
+ <div class="space-y-2">
49
+ <button @click="reset" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
50
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
51
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
52
+ </svg>
53
+ <span>{{i18n[language]["actions"]["reset"]}}</span>
54
+ </button>
55
+
56
+ <button @click="onCopyUrl" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
57
+ <template v-if="isCopiedUrl">
58
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
59
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
60
+ </svg>
61
+ <span>{{i18n[language]["actions"]["copied_url"]}}</span>
62
+ </template>
63
+ <template v-else>
64
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
65
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
66
+ </svg>
67
+ <span>{{i18n[language]["actions"]["copy_url"]}}</span>
68
+ </template>
69
+ </button>
70
+
71
+ <button @click="onCopyMermaid" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
72
+ <template v-if="isCopiedMermaid">
73
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
74
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
75
+ </svg>
76
+ <span>{{i18n[language]["actions"]["copied_mermaid_code"]}}</span>
77
+ </template>
78
+ <template v-else>
79
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
80
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
81
+ </svg>
82
+ <span>{{i18n[language]["actions"]["copy_mermaid_code"]}}</span>
83
+ </template>
84
+ </button>
85
+
86
+ <button @click="onCopyMarkdown" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
87
+ <template v-if="isCopiedMarkdown">
88
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
89
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
90
+ </svg>
91
+ <span>{{i18n[language]["actions"]["copied_markdown_code"]}}</span>
92
+ </template>
93
+ <template v-else>
94
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
95
+ <path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
96
+ </svg>
97
+ <span>{{i18n[language]["actions"]["copy_markdown_code"]}}</span>
98
+ </template>
99
+ </button>
100
+
101
+ <button @click="onDownloadSvg" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
102
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
103
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
104
+ </svg>
105
+ <span>{{i18n[language]["actions"]["download_svg_file"]}}</span>
106
+ </button>
107
+
108
+ <button @click="onDownloadPng" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
109
+ <svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
110
+ <path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
111
+ </svg>
112
+ <span>{{i18n[language]["actions"]["download_png_file"]}}</span>
113
+ </button>
114
+ </div>
115
+ </div>
116
+
117
+ <div class="p-2">
118
+ <h2 class="font-medium text-gray-900 mb-1">{{i18n[language]["options"]["title"]}}</h2>
119
+
120
+ <div class="space-y-1">
121
+ <div class="min-w-0 flex-1 text-sm">
122
+ <label class="relative flex items-start cursor-pointer hover:bg-gray-100 rounded">
123
+ <input
124
+ type="checkbox"
125
+ class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600"
126
+ v-model="isPreviewRelations"
127
+ @change="updateHash"
128
+ />
129
+ <span class="text-xs text-gray-900">{{i18n[language]["options"]["preview_relationships"]}}</span>
130
+ </label>
131
+ </div>
132
+
133
+ <div class="min-w-0 flex-1 text-sm">
134
+ <label class="relative flex items-start cursor-pointer hover:bg-gray-100 rounded">
135
+ <input
136
+ type="checkbox"
137
+ class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600"
138
+ v-model="isHideColumns"
139
+ @change="updateHash"
140
+ />
141
+ <span class="text-xs text-gray-900">{{i18n[language]["options"]["hide_columns"]}}</span>
142
+ </label>
143
+ </div>
144
+
145
+ <div class="min-w-0 flex-1 text-sm">
146
+ <label :class="`relative flex items-start hover:bg-gray-100 rounded ${ isHideColumns ? 'cursor-not-allowed' : 'cursor-pointer' }`">
147
+ <input
148
+ type="checkbox"
149
+ :class="`h-4 w-4 rounded border-gray-300 mr-2 focus:ring-red-600 ${ isHideColumns ? 'text-gray-400' : 'text-red-600'}`"
150
+ v-model="isShowKey"
151
+ @change="updateHash"
152
+ :disabled="isHideColumns"
153
+ />
154
+ <span :class="`text-xs ${ isHideColumns ? 'text-gray-400' : 'text-gray-900'}`">{{i18n[language]["options"]["show_key"]}}</span>
155
+ </label>
156
+ </div>
157
+
158
+ <div class="min-w-0 flex-1 text-sm">
159
+ <label :class="`relative flex items-start hover:bg-gray-100 rounded ${ isHideColumns ? 'cursor-not-allowed' : 'cursor-pointer' }`">
160
+ <input
161
+ type="checkbox"
162
+ :class="`h-4 w-4 rounded border-gray-300 mr-2 focus:ring-red-600 ${ isHideColumns ? 'text-gray-400' : 'text-red-600'}`"
163
+ v-model="isShowComment"
164
+ @change="updateHash"
165
+ :disabled="isHideColumns"
166
+ />
167
+ <span :class="`text-xs ${ isHideColumns ? 'text-gray-400' : 'text-gray-900'}`">{{i18n[language]["options"]["show_column_comment"]}}</span>
168
+ </label>
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ <div class="p-2">
174
+ <h2 class="font-medium text-gray-900 mb-1">{{i18n[language]["models"]["title"]}}</h2>
175
+
176
+ <div>
177
+ <input type="search" class="rounded text-xs w-full py-1 px-2 border border-gray-300 bg-gray-100 focus:border-red-600 focus:ring-red-600" :placeholder="i18n[language]['models']['filter']" v-model="filterText">
178
+ </div>
179
+
180
+ <div class="space-x-1 text-xs text-gray-900 my-1">
181
+ <span>{{i18n[language]["models"]["select"]}}</span>
182
+ <button @click="onSelectAll" class="text-red-600 font-bold hover:text-red-900 rounded focus:ring-2 focus:ring-red-600 focus:ring-offset-1">{{i18n[language]["models"]["all"]}}</button>
183
+ <span>/</span>
184
+ <button @click="onUnselectAll" class="text-red-600 font-bold hover:text-red-900 rounded focus:ring-2 focus:ring-red-600 focus:ring-offset-1">{{i18n[language]["models"]["none"]}}</button>
185
+ </div>
186
+
187
+ <div class="space-y-1">
188
+ <div class="inline-flex items-center text-xs text-gray-700" v-if="isExistsNoModelTable">
189
+ <svg class="text-orange-900 mr-1 h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
190
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
191
+ </svg>
192
+ <span>{{i18n[language]["models"]["no_model_file"]}}</span>
193
+ </div>
194
+
195
+ <label class="relative flex items-start cursor-pointer hover:bg-gray-100 rounded" v-for="model in filteredModelList">
196
+ <div class="min-w-0 flex-1 text-xs text-gray-900 inline-flex items-center">
197
+ <input
198
+ type="checkbox"
199
+ class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600"
200
+ :value="model.ModelName"
201
+ :checked="selectModels.includes(model.ModelName)"
202
+ @change="onChange"
203
+ />
204
+ <svg v-if="!model.IsModelExist" class="text-orange-900 mr-1 h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
205
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
206
+ </svg>
207
+ <span>{{model.ModelName}}</span>
208
+ </div>
209
+ </label>
210
+ </div>
211
+ </div>
212
+ </div>
213
+
214
+ <div class="flex-1 bg-gray-900">
215
+ <div class="space-x-2 inline-flex p-4">
216
+ <button @click="tab = 'erd'" :class="`text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 ${tab === 'erd' ? 'bg-white text-gray-900' : 'bg-gray-400 text-gray-900'}`">{{i18n[language]["tab"]["erd"]}}</button>
217
+ <button @click="tab = 'code'" :class="`text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 ${tab === 'code' ? 'bg-white text-gray-900' : 'bg-gray-400 text-gray-900'}`">{{i18n[language]["tab"]["code"]}}</button>
218
+ </div>
219
+ <div v-show="tab === 'erd'" class="px-4 w-full min-h-[calc(100vh-56px-32px-56px)]" id="preview"></div>
220
+ <textarea v-show="tab === 'code'" class="px-4 bg-gray-900 text-gray-300 font-mono w-full text-xs min-h-[calc(100vh-56px-32px-56px)] border-0 focus:ring-0" readonly v-model="mermaidErd"></textarea>
221
+ </div>
222
+ </div>
223
+
224
+ <footer class="bg-gray-100">
225
+ <p class="text-center text-xs text-gray-600 py-2">
226
+ <a href="https://github.com/koedame/rails-mermaid_erd" class="hover:text-gray-400" target="_blank" rel="noopener noreferrer">
227
+ Rails Mermaid ERD v<%= version %>
228
+ </a>
229
+ </p>
230
+ </footer>
231
+ </div>
232
+
233
+ <script src="https://cdn.tailwindcss.com/3.1.8?plugins=forms@0.5.2,typography@0.5.4"></script>
234
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/9.1.7/mermaid.min.js"></script>
235
+ <script src="https://unpkg.com/vue@3.2.40/dist/vue.global.js"></script>
236
+
237
+ <script>window.SCHEMA_DATA=<%= result.to_json %></script>
238
+ <script>
239
+ window.i18n = {
240
+ en: {
241
+ actions: {
242
+ title: 'Actions',
243
+ reset: 'Reset',
244
+ copy_mermaid_code: 'Copy Mermaid Code',
245
+ copied_mermaid_code: 'Copied Mermaid Code',
246
+ copy_markdown_code: 'Copy Markdown Code',
247
+ copied_markdown_code: 'Copied Markdown Code',
248
+ copy_url: 'Copy Link for Sharing',
249
+ copied_url: 'Copied Link for Sharing',
250
+ download_svg_file: 'Download SVG File',
251
+ download_png_file: 'Download PNG File',
252
+ },
253
+ options: {
254
+ title: 'Options',
255
+ preview_relationships: 'Preview Relationships',
256
+ show_key: 'Show Key',
257
+ show_column_comment: 'Show Column Comment',
258
+ hide_columns: 'Hide Columns',
259
+ },
260
+ models: {
261
+ title: 'Models',
262
+ select: 'Select',
263
+ all: 'All',
264
+ none: 'None',
265
+ filter: 'Filter',
266
+ no_model_file: 'No Model File',
267
+ },
268
+ tab: {
269
+ erd: 'ERD',
270
+ code: 'Code',
271
+ },
272
+ },
273
+ ja: {
274
+ actions: {
275
+ title: '操作',
276
+ reset: 'リセット',
277
+ copy_mermaid_code: 'Mermaid コードをコピー',
278
+ copied_mermaid_code: 'コピーしました',
279
+ copy_markdown_code: 'Markdown コードをコピー',
280
+ copied_markdown_code: 'コピーしました',
281
+ copy_url: '共有リンクをコピー',
282
+ copied_url: 'コピーしました',
283
+ download_svg_file: 'SVG ファイルをダウンロード',
284
+ download_png_file: 'PNG ファイルをダウンロード',
285
+ },
286
+ options: {
287
+ title: '設定',
288
+ preview_relationships: 'リレーションをプレビュー',
289
+ show_key: 'キーを表示',
290
+ show_column_comment: 'コメントを表示',
291
+ hide_columns: 'カラムを非表示',
292
+ },
293
+ models: {
294
+ title: 'モデル',
295
+ select: '選択',
296
+ all: '全て',
297
+ none: 'なし',
298
+ filter: '絞り込み',
299
+ no_model_file: 'モデル未定義',
300
+ },
301
+ tab: {
302
+ erd: 'ER図',
303
+ code: 'コード',
304
+ },
305
+ }
306
+ }
307
+ </script>
308
+ <script>
309
+ const App = {
310
+ setup(){
311
+ const language = Vue.ref('en')
312
+ const isCopiedMermaid = Vue.ref(false)
313
+ const isCopiedMarkdown = Vue.ref(false)
314
+ const tab = Vue.ref('erd')
315
+
316
+ const schemaData = {
317
+ ...window.SCHEMA_DATA,
318
+ Models: window.SCHEMA_DATA.Models.sort((a, b) => a.ModelName < b.ModelName ? -1 : 1)
319
+ }
320
+ const selectModels = Vue.ref([])
321
+ const isPreviewRelations = Vue.ref(false)
322
+ const isShowKey = Vue.ref(false)
323
+ const isShowComment = Vue.ref(false)
324
+ const isHideColumns = Vue.ref(false)
325
+
326
+ const restoreFromHash = () => {
327
+ try {
328
+ const h = location.hash.substr(1, location.hash.length)
329
+ const parsedData = JSON.parse(atob(h))
330
+ selectModels.value = parsedData.selectModels || []
331
+ isPreviewRelations.value = !!parsedData.isPreviewRelations
332
+ isShowKey.value = !!parsedData.isShowKey
333
+ isShowComment.value = !!parsedData.isShowComment
334
+ isHideColumns.value = !!parsedData.isHideColumns
335
+ } catch {
336
+ reset()
337
+ }
338
+ }
339
+
340
+ const reset = () => {
341
+ selectModels.value = []
342
+ schemaData.Models.forEach((model) => {
343
+ selectModels.value.push(model.ModelName)
344
+ })
345
+ isPreviewRelations.value = false
346
+ isShowKey.value = false
347
+ isShowComment.value = false
348
+ isHideColumns.value = false
349
+ updateHash()
350
+ }
351
+
352
+ const updateHash = () => {
353
+ location.hash = btoa(JSON.stringify({
354
+ selectModels: selectModels.value,
355
+ isPreviewRelations: isPreviewRelations.value,
356
+ isShowKey: isShowKey.value,
357
+ isShowComment: isShowComment.value,
358
+ isHideColumns: isHideColumns.value,
359
+ }))
360
+ }
361
+
362
+ const onChange = (e) => {
363
+ if (e.target.checked) {
364
+ selectModels.value.push(e.target.value)
365
+ } else {
366
+ selectModels.value = selectModels.value.filter((selectModel) => selectModel !== e.target.value)
367
+ }
368
+ selectModels.value
369
+ updateHash()
370
+ }
371
+
372
+ const onSelectAll = () => {
373
+ filteredModelList.value.forEach((model) => {
374
+ if (!selectModels.value.includes(model.ModelName)) {
375
+ selectModels.value.push(model.ModelName)
376
+ }
377
+ })
378
+ updateHash()
379
+ }
380
+
381
+ const onUnselectAll = () => {
382
+ selectModels.value = selectModels.value.filter((model) => {
383
+ return !filteredModelList.value.some((fm) => fm.ModelName === model)
384
+ })
385
+ updateHash()
386
+ }
387
+
388
+ isCopiedUrl = Vue.ref(false)
389
+ const onCopyUrl = () => {
390
+ navigator.clipboard.writeText(location.href);
391
+ isCopiedUrl.value = true
392
+
393
+ setTimeout(() => {
394
+ if (isCopiedUrl) {
395
+ isCopiedUrl.value = false
396
+ }
397
+ }, 1000)
398
+ }
399
+
400
+ const mermaidErd = Vue.computed(() => {
401
+ const lines = []
402
+ lines.push('erDiagram')
403
+ lines.push(' %% --------------------------------------------------------')
404
+ lines.push(' %% Generated by "Rails Mermaid ERD"')
405
+ lines.push(' %% https://github.com/koedame/rails-mermaid_erd')
406
+ lines.push(` %% Restore Hash: ${location.hash}`)
407
+ lines.push(' %% --------------------------------------------------------')
408
+ lines.push('')
409
+
410
+ filteredData.value.Models.forEach(model => {
411
+ lines.push(` %% table name: ${model.TableName}`)
412
+ lines.push(` ${model.ModelName.replace(/:/g, '-')} {`)
413
+
414
+ if (!isHideColumns.value) {
415
+ model.Columns.forEach(column => {
416
+ lines.push(` ${column.type} ${column.name} ${isShowKey.value ? column.key : ''} ${isShowComment.value ? `"${column.comment || ''}"` : ''}`)
417
+ })
418
+ }
419
+
420
+ lines.push(` }`)
421
+ lines.push('')
422
+ });
423
+
424
+ filteredData.value.Relations.forEach(relation => {
425
+ lines.push(` ${relation.LeftModelName.replace(/:/g, '-')} ${relation.LeftValue}--${relation.RightValue} ${relation.RightModelName.replace(/:/g, '-')} : "${relation.Comment}"`)
426
+ });
427
+
428
+ return lines.join("\n")
429
+ })
430
+
431
+ const filteredData = Vue.computed(() => {
432
+ let relations
433
+ if (isPreviewRelations.value) {
434
+ relations = schemaData.Relations.filter(relation => {
435
+ return selectModels.value.includes(relation.LeftModelName) || selectModels.value.includes(relation.RightModelName)
436
+ })
437
+ } else {
438
+ relations = schemaData.Relations.filter(relation => {
439
+ return selectModels.value.includes(relation.LeftModelName) && selectModels.value.includes(relation.RightModelName)
440
+ })
441
+ }
442
+
443
+ return {
444
+ Models: schemaData.Models.filter(model => selectModels.value.includes(model.ModelName)),
445
+ Relations: relations
446
+ }
447
+ })
448
+
449
+ const graph = Vue.ref()
450
+
451
+ const filterText = Vue.ref('')
452
+
453
+ const reRender = () => {
454
+ mermaid.initialize({theme: 'dark'})
455
+ mermaid.init()
456
+ graph.value = mermaid.mermaidAPI.render("mermaid-erd", mermaidErd.value);
457
+ document.getElementById('preview').innerHTML = graph.value
458
+ }
459
+
460
+ const onCopyMermaid = () => {
461
+ navigator.clipboard.writeText(mermaidErd.value);
462
+ isCopiedMermaid.value = true
463
+
464
+ setTimeout(() => {
465
+ if (isCopiedMermaid) {
466
+ isCopiedMermaid.value = false
467
+ }
468
+ }, 1000)
469
+ }
470
+
471
+ const onCopyMarkdown = () => {
472
+ navigator.clipboard.writeText(`\`\`\`mermaid\n${mermaidErd.value}\n\`\`\`\n`);
473
+ isCopiedMarkdown.value = true
474
+
475
+ setTimeout(() => {
476
+ if (isCopiedMarkdown) {
477
+ isCopiedMarkdown.value = false
478
+ }
479
+ }, 1000)
480
+ }
481
+
482
+ const onDownloadSvg = () => {
483
+ const svgBlob = new Blob([graph.value], { type: 'image/svg+xml;charset=utf-8' });
484
+ const svgUrl = URL.createObjectURL(svgBlob);
485
+
486
+ const a = document.createElement("a");
487
+ a.href = svgUrl;
488
+ a.setAttribute("download", "mermaid_erd.svg");
489
+ a.dispatchEvent(new MouseEvent("click"));
490
+
491
+ URL.revokeObjectURL(svgUrl);
492
+ }
493
+
494
+ const onDownloadPng = () => {
495
+ const svg = document.querySelector("#preview > svg");
496
+ const svgData = new XMLSerializer().serializeToString(svg);
497
+ const canvas = document.createElement("canvas");
498
+ canvas.width = svg.width.baseVal.value;
499
+ canvas.height = svg.height.baseVal.value;
500
+
501
+ const ctx = canvas.getContext("2d");
502
+ const image = new Image;
503
+ image.onload = () => {
504
+ ctx.drawImage(image, 0, 0);
505
+ const a = document.createElement("a");
506
+ a.href = canvas.toDataURL("image/png");
507
+ a.setAttribute("download", "mermaid_erd.png");
508
+ a.dispatchEvent(new MouseEvent("click"));
509
+ }
510
+ image.src = "data:image/svg+xml;charset=utf-8;base64," + btoa(unescape(encodeURIComponent(svgData)));
511
+ }
512
+
513
+ const filteredModelList = Vue.computed(() => {
514
+ return schemaData.Models.filter((model) => {
515
+ if (model.ModelName.toLowerCase().includes(filterText.value.toLowerCase())) {
516
+ return true
517
+ }
518
+ if (model.TableName.toLowerCase().includes(filterText.value.toLowerCase())) {
519
+ return true
520
+ }
521
+ })
522
+ })
523
+
524
+ const isExistsNoModelTable = Vue.computed(() => {
525
+ return filteredModelList.value.some((model) => !model.IsModelExist)
526
+ })
527
+
528
+ const setLanguage = (l) => {
529
+ if (Object.keys(window.i18n).includes(l)) {
530
+ language.value = l
531
+ document.documentElement.lang = l
532
+ } else {
533
+ language.value = 'en'
534
+ document.documentElement.lang = 'en'
535
+ }
536
+ }
537
+
538
+ Vue.onMounted(() => {
539
+ setLanguage(window.navigator.language)
540
+ restoreFromHash()
541
+ reRender()
542
+ })
543
+
544
+ window.addEventListener('hashchange', () => {
545
+ restoreFromHash()
546
+ reRender()
547
+ }, false);
548
+
549
+ return {
550
+ filteredModelList,
551
+ filterText,
552
+ i18n: window.i18n,
553
+ isCopiedMarkdown,
554
+ isCopiedMermaid,
555
+ isCopiedUrl,
556
+ isExistsNoModelTable,
557
+ isPreviewRelations,
558
+ isShowComment,
559
+ isShowKey,
560
+ language,
561
+ mermaidErd,
562
+ onChange,
563
+ onCopyMarkdown,
564
+ onCopyMermaid,
565
+ onCopyUrl,
566
+ onDownloadPng,
567
+ onDownloadSvg,
568
+ onSelectAll,
569
+ onUnselectAll,
570
+ reset,
571
+ schemaData,
572
+ selectModels,
573
+ setLanguage,
574
+ tab,
575
+ updateHash,
576
+ isHideColumns,
577
+ }
578
+ }
579
+ }
580
+
581
+ Vue.createApp(App).mount('#app')
582
+ </script>
583
+ </body>
584
+ </html>
metadata ADDED
@@ -0,0 +1,65 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails-mermaid_erd
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - 肥溜め
8
+ - "\U0001F4A9"
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2022-10-08 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: activerecord
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - ">="
19
+ - !ruby/object:Gem::Version
20
+ version: '4.2'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - ">="
26
+ - !ruby/object:Gem::Version
27
+ version: '4.2'
28
+ description:
29
+ email: []
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - MIT-LICENSE
35
+ - README.md
36
+ - Rakefile
37
+ - lib/assets/logo.svg
38
+ - lib/rails-mermaid_erd.rb
39
+ - lib/rails-mermaid_erd/version.rb
40
+ - lib/templates/index.html.erb
41
+ homepage: https://github.com/koedame/rails-mermaid_erd
42
+ licenses:
43
+ - MIT
44
+ metadata:
45
+ homepage_uri: https://github.com/koedame/rails-mermaid_erd
46
+ post_install_message:
47
+ rdoc_options: []
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ required_rubygems_version: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ requirements: []
61
+ rubygems_version: 3.3.7
62
+ signing_key:
63
+ specification_version: 4
64
+ summary: Generate Mermaid ERD for Ruby on Rails
65
+ test_files: []