salvia_rb 0.1.5
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/LICENSE.txt +22 -0
- data/README.md +235 -0
- data/assets/javascripts/islands.js +26 -0
- data/assets/scripts/build_ssr.ts +261 -0
- data/exe/salvia +7 -0
- data/lib/salvia_rb/application.rb +431 -0
- data/lib/salvia_rb/assets.rb +74 -0
- data/lib/salvia_rb/assets_middleware.rb +46 -0
- data/lib/salvia_rb/cli.rb +1398 -0
- data/lib/salvia_rb/component.rb +74 -0
- data/lib/salvia_rb/controller.rb +277 -0
- data/lib/salvia_rb/csrf.rb +130 -0
- data/lib/salvia_rb/database.rb +176 -0
- data/lib/salvia_rb/flash.rb +43 -0
- data/lib/salvia_rb/helpers/component.rb +31 -0
- data/lib/salvia_rb/helpers/csrf.rb +47 -0
- data/lib/salvia_rb/helpers/inspector.rb +192 -0
- data/lib/salvia_rb/helpers/island.rb +143 -0
- data/lib/salvia_rb/helpers/tag.rb +90 -0
- data/lib/salvia_rb/helpers.rb +11 -0
- data/lib/salvia_rb/plugins/base.rb +59 -0
- data/lib/salvia_rb/router.rb +181 -0
- data/lib/salvia_rb/ssr/quickjs.rb +404 -0
- data/lib/salvia_rb/ssr.rb +119 -0
- data/lib/salvia_rb/test.rb +20 -0
- data/lib/salvia_rb/version.rb +5 -0
- data/lib/salvia_rb.rb +250 -0
- metadata +344 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Salvia
|
|
4
|
+
module Helpers
|
|
5
|
+
module Component
|
|
6
|
+
# View Component をレンダリングする
|
|
7
|
+
#
|
|
8
|
+
# @param name [String, Symbol] コンポーネント名 (例: "user_card")
|
|
9
|
+
# @param kwargs [Hash] コンポーネントに渡す引数
|
|
10
|
+
# @param block [Proc] コンテンツブロック
|
|
11
|
+
# @return [String] レンダリング結果
|
|
12
|
+
def component(name, **kwargs, &block)
|
|
13
|
+
# "user_card" -> "UserCardComponent"
|
|
14
|
+
class_name = "#{name.to_s.camelize}Component"
|
|
15
|
+
|
|
16
|
+
begin
|
|
17
|
+
klass = Object.const_get(class_name)
|
|
18
|
+
rescue NameError
|
|
19
|
+
raise NameError, "Component class not found: #{class_name} (expected app/components/#{name}_component.rb)"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
unless klass < Salvia::Component
|
|
23
|
+
raise ArgumentError, "#{class_name} must inherit from Salvia::Component"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
instance = klass.new(**kwargs)
|
|
27
|
+
instance.render_in(self, &block)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Salvia
|
|
4
|
+
module Helpers
|
|
5
|
+
# CSRF 保護用ヘルパー
|
|
6
|
+
module CSRF
|
|
7
|
+
# CSRF トークンを取得
|
|
8
|
+
#
|
|
9
|
+
# @return [String] CSRF トークン
|
|
10
|
+
#
|
|
11
|
+
# @example
|
|
12
|
+
# <input type="hidden" name="authenticity_token" value="<%= csrf_token %>">
|
|
13
|
+
#
|
|
14
|
+
def csrf_token
|
|
15
|
+
Salvia::CSRF.token(session)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# CSRF meta タグを生成
|
|
19
|
+
#
|
|
20
|
+
# @return [String] meta タグ HTML
|
|
21
|
+
#
|
|
22
|
+
# @example
|
|
23
|
+
# <head>
|
|
24
|
+
# <%= csrf_meta_tag %>
|
|
25
|
+
# </head>
|
|
26
|
+
#
|
|
27
|
+
def csrf_meta_tag
|
|
28
|
+
%(<meta name="csrf-token" content="#{csrf_token}">)
|
|
29
|
+
end
|
|
30
|
+
alias csrf_meta_tags csrf_meta_tag
|
|
31
|
+
|
|
32
|
+
# CSRF hidden input を生成
|
|
33
|
+
#
|
|
34
|
+
# @return [String] hidden input HTML
|
|
35
|
+
#
|
|
36
|
+
# @example
|
|
37
|
+
# <form method="post">
|
|
38
|
+
# <%= csrf_field %>
|
|
39
|
+
# ...
|
|
40
|
+
# </form>
|
|
41
|
+
#
|
|
42
|
+
def csrf_field
|
|
43
|
+
%(<input type="hidden" name="authenticity_token" value="#{csrf_token}">)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Salvia
|
|
4
|
+
module Helpers
|
|
5
|
+
# Island Inspector ヘルパー
|
|
6
|
+
#
|
|
7
|
+
# 開発モードで Island コンポーネントをデバッグするための
|
|
8
|
+
# ツールを提供します。
|
|
9
|
+
module Inspector
|
|
10
|
+
# Island Inspector のスクリプトとスタイルを読み込むタグを生成
|
|
11
|
+
#
|
|
12
|
+
# @return [String] script タグと style タグ
|
|
13
|
+
def island_inspector_tags
|
|
14
|
+
return "" unless Salvia.development? && Salvia.config.island_inspector?
|
|
15
|
+
|
|
16
|
+
css = inspector_css
|
|
17
|
+
js = inspector_js
|
|
18
|
+
|
|
19
|
+
"<style>#{css}</style>\n<script>#{js}</script>"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def inspector_css
|
|
25
|
+
<<~CSS
|
|
26
|
+
.salvia-island-highlight {
|
|
27
|
+
outline: 2px dashed #6B46C1 !important;
|
|
28
|
+
outline-offset: 2px;
|
|
29
|
+
position: relative;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.salvia-island-highlight::after {
|
|
33
|
+
content: attr(data-island);
|
|
34
|
+
position: absolute;
|
|
35
|
+
top: -20px;
|
|
36
|
+
left: 0;
|
|
37
|
+
background: #6B46C1;
|
|
38
|
+
color: white;
|
|
39
|
+
font-size: 10px;
|
|
40
|
+
padding: 2px 6px;
|
|
41
|
+
border-radius: 3px;
|
|
42
|
+
font-family: ui-monospace, monospace;
|
|
43
|
+
z-index: 10000;
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.salvia-inspector-panel {
|
|
48
|
+
position: fixed;
|
|
49
|
+
bottom: 20px;
|
|
50
|
+
right: 20px;
|
|
51
|
+
width: 320px;
|
|
52
|
+
max-height: 400px;
|
|
53
|
+
background: #1e1e1e;
|
|
54
|
+
color: #d4d4d4;
|
|
55
|
+
border-radius: 8px;
|
|
56
|
+
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
|
57
|
+
font-family: ui-monospace, monospace;
|
|
58
|
+
font-size: 12px;
|
|
59
|
+
z-index: 10001;
|
|
60
|
+
overflow: hidden;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.salvia-inspector-header {
|
|
64
|
+
background: #6B46C1;
|
|
65
|
+
color: white;
|
|
66
|
+
padding: 8px 12px;
|
|
67
|
+
font-weight: bold;
|
|
68
|
+
display: flex;
|
|
69
|
+
justify-content: space-between;
|
|
70
|
+
align-items: center;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.salvia-inspector-close {
|
|
74
|
+
background: none;
|
|
75
|
+
border: none;
|
|
76
|
+
color: white;
|
|
77
|
+
cursor: pointer;
|
|
78
|
+
font-size: 16px;
|
|
79
|
+
padding: 0 4px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.salvia-inspector-content {
|
|
83
|
+
padding: 12px;
|
|
84
|
+
max-height: 340px;
|
|
85
|
+
overflow-y: auto;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.salvia-inspector-section {
|
|
89
|
+
margin-bottom: 12px;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.salvia-inspector-label {
|
|
93
|
+
color: #9cdcfe;
|
|
94
|
+
margin-bottom: 4px;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.salvia-inspector-value {
|
|
98
|
+
background: #2d2d2d;
|
|
99
|
+
padding: 6px 8px;
|
|
100
|
+
border-radius: 4px;
|
|
101
|
+
word-break: break-all;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.salvia-inspector-json {
|
|
105
|
+
white-space: pre-wrap;
|
|
106
|
+
color: #ce9178;
|
|
107
|
+
}
|
|
108
|
+
CSS
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def inspector_js
|
|
112
|
+
<<~JS
|
|
113
|
+
(function() {
|
|
114
|
+
let currentPanel = null;
|
|
115
|
+
|
|
116
|
+
// Alt + Click でパネルを表示
|
|
117
|
+
document.addEventListener('click', function(e) {
|
|
118
|
+
if (!e.altKey) return;
|
|
119
|
+
|
|
120
|
+
const island = e.target.closest('[data-island]');
|
|
121
|
+
if (!island) return;
|
|
122
|
+
|
|
123
|
+
e.preventDefault();
|
|
124
|
+
e.stopPropagation();
|
|
125
|
+
|
|
126
|
+
showInspectorPanel(island);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ホバー時にハイライト
|
|
130
|
+
document.addEventListener('mouseover', function(e) {
|
|
131
|
+
const island = e.target.closest('[data-island]');
|
|
132
|
+
if (island) {
|
|
133
|
+
island.classList.add('salvia-island-highlight');
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
document.addEventListener('mouseout', function(e) {
|
|
138
|
+
const island = e.target.closest('[data-island]');
|
|
139
|
+
if (island) {
|
|
140
|
+
island.classList.remove('salvia-island-highlight');
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
function showInspectorPanel(island) {
|
|
145
|
+
if (currentPanel) {
|
|
146
|
+
currentPanel.remove();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const name = island.dataset.island;
|
|
150
|
+
const props = JSON.parse(island.dataset.props || '{}');
|
|
151
|
+
const ssr = island.dataset.ssr === 'true';
|
|
152
|
+
const hydrated = island.dataset.hydrated === 'true';
|
|
153
|
+
|
|
154
|
+
const panel = document.createElement('div');
|
|
155
|
+
panel.className = 'salvia-inspector-panel';
|
|
156
|
+
panel.innerHTML = `
|
|
157
|
+
<div class="salvia-inspector-header">
|
|
158
|
+
<span>🏝️ \${name}</span>
|
|
159
|
+
<button class="salvia-inspector-close">×</button>
|
|
160
|
+
</div>
|
|
161
|
+
<div class="salvia-inspector-content">
|
|
162
|
+
<div class="salvia-inspector-section">
|
|
163
|
+
<div class="salvia-inspector-label">SSR</div>
|
|
164
|
+
<div class="salvia-inspector-value">\${ssr ? '✅ Yes' : '❌ No'}</div>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="salvia-inspector-section">
|
|
167
|
+
<div class="salvia-inspector-label">Hydrated</div>
|
|
168
|
+
<div class="salvia-inspector-value">\${hydrated ? '✅ Yes' : '⏳ Pending'}</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="salvia-inspector-section">
|
|
171
|
+
<div class="salvia-inspector-label">Props</div>
|
|
172
|
+
<div class="salvia-inspector-value salvia-inspector-json">\${JSON.stringify(props, null, 2)}</div>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
`;
|
|
176
|
+
|
|
177
|
+
panel.querySelector('.salvia-inspector-close').addEventListener('click', function() {
|
|
178
|
+
panel.remove();
|
|
179
|
+
currentPanel = null;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
document.body.appendChild(panel);
|
|
183
|
+
currentPanel = panel;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log('🔍 Salvia Island Inspector: Alt+Click on any island to inspect');
|
|
187
|
+
})();
|
|
188
|
+
JS
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
end
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Salvia
|
|
6
|
+
module Helpers
|
|
7
|
+
module Island
|
|
8
|
+
# Island マニフェストをキャッシュ
|
|
9
|
+
@manifest = nil
|
|
10
|
+
@manifest_mtime = nil
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
# マニフェストを読み込む
|
|
14
|
+
def load_manifest
|
|
15
|
+
manifest_path = File.join(Dir.pwd, "vendor/server/manifest.json")
|
|
16
|
+
return {} unless File.exist?(manifest_path)
|
|
17
|
+
|
|
18
|
+
mtime = File.mtime(manifest_path)
|
|
19
|
+
if @manifest.nil? || @manifest_mtime != mtime
|
|
20
|
+
@manifest = JSON.parse(File.read(manifest_path))
|
|
21
|
+
@manifest_mtime = mtime
|
|
22
|
+
end
|
|
23
|
+
@manifest
|
|
24
|
+
rescue => e
|
|
25
|
+
{}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Island が client only かどうか
|
|
29
|
+
def client_only?(name)
|
|
30
|
+
manifest = load_manifest
|
|
31
|
+
manifest.dig(name, "clientOnly") == true
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Island コンポーネントをレンダリングする
|
|
36
|
+
#
|
|
37
|
+
# SSR が有効な場合はサーバーサイドで HTML を生成し、
|
|
38
|
+
# クライアントサイドでハイドレーションを行います。
|
|
39
|
+
#
|
|
40
|
+
# @param name [String] コンポーネント名 (例: "Counter")
|
|
41
|
+
# @param props [Hash] コンポーネントに渡すプロパティ
|
|
42
|
+
# @param options [Hash] オプション
|
|
43
|
+
# @option options [String] :id 要素のID
|
|
44
|
+
# @option options [String] :tag ラッパータグ (デフォルト: div)
|
|
45
|
+
# @option options [Boolean] :ssr SSR を有効にするか (デフォルト: auto)
|
|
46
|
+
# @option options [Boolean] :hydrate クライアントサイドでハイドレーションするか (デフォルト: true)
|
|
47
|
+
# @return [String] レンダリングされた HTML
|
|
48
|
+
#
|
|
49
|
+
# @example 基本的な使用法
|
|
50
|
+
# <%= island "Counter", count: 5 %>
|
|
51
|
+
#
|
|
52
|
+
# @example SSR を明示的に無効化
|
|
53
|
+
# <%= island "HeavyChart", data: @data, ssr: false %>
|
|
54
|
+
#
|
|
55
|
+
# @example ハイドレーションを無効化 (静的 HTML のみ)
|
|
56
|
+
# <%= island "StaticCard", title: "Hello", hydrate: false %>
|
|
57
|
+
#
|
|
58
|
+
def island(name, props = {}, options = {})
|
|
59
|
+
tag_name = options.delete(:tag) || :div
|
|
60
|
+
hydrate = options.fetch(:hydrate, true)
|
|
61
|
+
|
|
62
|
+
# SSR 有効/無効の判定
|
|
63
|
+
# 1. options[:ssr] が明示的に指定されていればそれを使う
|
|
64
|
+
# 2. マニフェストで "client only" ならば SSR 無効
|
|
65
|
+
# 3. デフォルトは SSR 有効
|
|
66
|
+
ssr_enabled = if options.key?(:ssr)
|
|
67
|
+
options[:ssr]
|
|
68
|
+
else
|
|
69
|
+
!Island.client_only?(name)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# 開発モードかどうか
|
|
73
|
+
development = defined?(Salvia.env) ? Salvia.env == "development" : true
|
|
74
|
+
|
|
75
|
+
# SSR でコンテンツを生成
|
|
76
|
+
inner_html = ""
|
|
77
|
+
begin
|
|
78
|
+
if ssr_enabled && defined?(Salvia::SSR) && Salvia::SSR.respond_to?(:configured?) && Salvia::SSR.configured?
|
|
79
|
+
inner_html = Salvia::SSR.render(name, props)
|
|
80
|
+
end
|
|
81
|
+
rescue => e
|
|
82
|
+
# SSR 失敗時はエラーをログに出力し、CSR にフォールバック
|
|
83
|
+
if development
|
|
84
|
+
inner_html = ssr_error_inline(name, e.message)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# データ属性を構築
|
|
89
|
+
data_attrs = {}
|
|
90
|
+
|
|
91
|
+
if hydrate
|
|
92
|
+
data_attrs[:island] = name
|
|
93
|
+
data_attrs[:props] = props.to_json
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# 開発モードではデバッグ用の属性を追加
|
|
97
|
+
if development
|
|
98
|
+
data_attrs[:salvia_debug] = true
|
|
99
|
+
data_attrs[:salvia_component] = name
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# HTML オプションを構築
|
|
103
|
+
html_options = options.dup
|
|
104
|
+
html_options.delete(:ssr)
|
|
105
|
+
html_options.delete(:hydrate)
|
|
106
|
+
html_options[:data] = (html_options[:data] || {}).merge(data_attrs)
|
|
107
|
+
|
|
108
|
+
# 開発モードではインスペクター用のクラスを追加
|
|
109
|
+
if development
|
|
110
|
+
html_options[:class] = [html_options[:class], "salvia-island"].compact.join(" ")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
tag(tag_name, html_options) { inner_html }
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
private
|
|
117
|
+
|
|
118
|
+
# インラインエラー表示 (開発モード用、軽量版)
|
|
119
|
+
def ssr_error_inline(name, message)
|
|
120
|
+
<<~HTML
|
|
121
|
+
<div style="
|
|
122
|
+
background: #fee;
|
|
123
|
+
border: 1px solid #fcc;
|
|
124
|
+
border-radius: 4px;
|
|
125
|
+
padding: 8px 12px;
|
|
126
|
+
font-size: 12px;
|
|
127
|
+
color: #900;
|
|
128
|
+
">
|
|
129
|
+
<strong>⚠️ SSR Error:</strong> #{escape_html(name)} - #{escape_html(message)}
|
|
130
|
+
</div>
|
|
131
|
+
HTML
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def escape_html(str)
|
|
135
|
+
str.to_s
|
|
136
|
+
.gsub("&", "&")
|
|
137
|
+
.gsub("<", "<")
|
|
138
|
+
.gsub(">", ">")
|
|
139
|
+
.gsub('"', """)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Salvia
|
|
4
|
+
module Helpers
|
|
5
|
+
module Tag
|
|
6
|
+
# HTMLタグを生成する
|
|
7
|
+
#
|
|
8
|
+
# @param name [String, Symbol] タグ名
|
|
9
|
+
# @param options [Hash] 属性
|
|
10
|
+
# @param block [Proc] コンテンツブロック
|
|
11
|
+
# @return [String]
|
|
12
|
+
def tag(name, options = {}, &block)
|
|
13
|
+
html_options = options.dup
|
|
14
|
+
data = html_options.delete(:data)
|
|
15
|
+
|
|
16
|
+
attributes = html_options.map { |k, v| "#{k}=\"#{v.to_s.gsub('"', '"')}\"" }
|
|
17
|
+
|
|
18
|
+
if data
|
|
19
|
+
data.each do |k, v|
|
|
20
|
+
key = k.to_s.tr("_", "-") # dasherize
|
|
21
|
+
attributes << "data-#{key}=\"#{v.to_s.gsub('"', '"')}\""
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
attributes = attributes.join(" ")
|
|
26
|
+
attributes = " " + attributes unless attributes.empty?
|
|
27
|
+
|
|
28
|
+
if block_given?
|
|
29
|
+
content = block.call
|
|
30
|
+
"<#{name}#{attributes}>#{content}</#{name}>"
|
|
31
|
+
else
|
|
32
|
+
"<#{name}#{attributes}>"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# リンクタグを生成する
|
|
37
|
+
#
|
|
38
|
+
# @param name [String] リンクテキスト
|
|
39
|
+
# @param url [String] URL
|
|
40
|
+
# @param options [Hash] 属性
|
|
41
|
+
# @return [String]
|
|
42
|
+
def link_to(name, url, options = {})
|
|
43
|
+
tag(:a, options.merge(href: url)) { name }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# フォーム開始タグを生成する
|
|
47
|
+
#
|
|
48
|
+
# @param url [String] アクションURL
|
|
49
|
+
# @param options [Hash] オプション
|
|
50
|
+
# @option options [Symbol] :method HTTPメソッド (デフォルト: :post)
|
|
51
|
+
# @return [String]
|
|
52
|
+
def form_tag(url, options = {})
|
|
53
|
+
html_options = options.dup
|
|
54
|
+
method = html_options.delete(:method) || :post
|
|
55
|
+
|
|
56
|
+
# method が get/post 以外の場合、_method パラメータを使用
|
|
57
|
+
real_method = method.to_s.downcase
|
|
58
|
+
form_method = %w[get post].include?(real_method) ? real_method : "post"
|
|
59
|
+
|
|
60
|
+
html_options[:action] = url
|
|
61
|
+
html_options[:method] = form_method
|
|
62
|
+
|
|
63
|
+
# 属性を文字列化
|
|
64
|
+
attributes = html_options.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")
|
|
65
|
+
attributes = " " + attributes unless attributes.empty?
|
|
66
|
+
|
|
67
|
+
html = ["<form#{attributes}>"]
|
|
68
|
+
|
|
69
|
+
# CSRF トークン (GET 以外)
|
|
70
|
+
if real_method != "get" && respond_to?(:csrf_token) && csrf_token
|
|
71
|
+
html << tag(:input, type: "hidden", name: "authenticity_token", value: csrf_token)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Method Override
|
|
75
|
+
if real_method != form_method
|
|
76
|
+
html << tag(:input, type: "hidden", name: "_method", value: real_method)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
html.join("\n")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# フォーム終了タグを生成する
|
|
83
|
+
#
|
|
84
|
+
# @return [String]
|
|
85
|
+
def form_close
|
|
86
|
+
"</form>"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Salvia
|
|
4
|
+
module Plugins
|
|
5
|
+
# プラグイン管理モジュール
|
|
6
|
+
#
|
|
7
|
+
# Salvia のオプショナル機能をプラグインとして管理します。
|
|
8
|
+
#
|
|
9
|
+
# @example プラグインの有効化
|
|
10
|
+
# Salvia.configure do |config|
|
|
11
|
+
# config.plugins << :htmx
|
|
12
|
+
# end
|
|
13
|
+
#
|
|
14
|
+
module Base
|
|
15
|
+
class << self
|
|
16
|
+
# 登録済みプラグイン
|
|
17
|
+
def registry
|
|
18
|
+
@registry ||= {}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# プラグインを登録
|
|
22
|
+
def register(name, mod)
|
|
23
|
+
registry[name.to_sym] = mod
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# プラグインを取得
|
|
27
|
+
def get(name)
|
|
28
|
+
registry[name.to_sym]
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# プラグインが登録されているか確認
|
|
32
|
+
def registered?(name)
|
|
33
|
+
registry.key?(name.to_sym)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# 有効なプラグイン一覧
|
|
37
|
+
def enabled
|
|
38
|
+
@enabled ||= []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# プラグインを有効化
|
|
42
|
+
def enable(name)
|
|
43
|
+
plugin = get(name)
|
|
44
|
+
raise ArgumentError, "Unknown plugin: #{name}" unless plugin
|
|
45
|
+
|
|
46
|
+
enabled << name.to_sym unless enabled.include?(name.to_sym)
|
|
47
|
+
|
|
48
|
+
# プラグインの初期化メソッドがあれば呼び出す
|
|
49
|
+
plugin.setup if plugin.respond_to?(:setup)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# プラグインが有効か確認
|
|
53
|
+
def enabled?(name)
|
|
54
|
+
enabled.include?(name.to_sym)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|