rendry 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +122 -0
- data/lib/rendry/version.rb +5 -0
- data/lib/rendry.rb +412 -0
- metadata +51 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 020307f7389d6a1dcdde28eb61c2eaf4367c9d1848584d67b7665fe45a9804a5
|
|
4
|
+
data.tar.gz: 578e5969022b18f937daa40e5fd6419937c8afb906ee0614b1500bc95820c769
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e8c0cd070c058b4ea029a9ffd2c831c50ae7547ac3006f65bf282899aec0e089cf09049dcd3996c370a6ca83abc51dd19aeda17a9829755c17c2e7cd6bba9fd8
|
|
7
|
+
data.tar.gz: 9aa71174fe35e25263de312dd46ff6dc3af4a6e45ca3dbde64a6d66f61476b6d37061eccf689a96bf6d3420adb009e0bca930c63f6bc8c5e0ea09cf0ba96a899
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rendry contributors
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Rendry UI
|
|
2
|
+
|
|
3
|
+
Rendry UI is a lightweight server-rendered UI framework for Ruby. It provides a small DSL for composing HTML with Ruby — explicit markup, safe escaping by default, testable components, and optional hypermedia interactivity with zero mandatory JavaScript.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require_relative "lib/rendry"
|
|
9
|
+
|
|
10
|
+
html = Rendry.render do
|
|
11
|
+
main(class: "page") do
|
|
12
|
+
h1 { text "Tasks" }
|
|
13
|
+
p "A simple Ruby interface"
|
|
14
|
+
button "Create task", type: "submit"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
puts html
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Values passed to `text` and element content are escaped automatically. Use `raw` only for an HTML fragment fully controlled by your application.
|
|
22
|
+
|
|
23
|
+
## Components
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
class Badge < Rendry::Component
|
|
27
|
+
def initialize(label)
|
|
28
|
+
@label = label
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def render
|
|
32
|
+
Rendry.render { span(@label, class: "badge") }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Rendry.render { component Badge.new("Ready") }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Components keep access to their instance variables inside DSL blocks, and accept a content block via `do...end` or `{ }`.
|
|
40
|
+
|
|
41
|
+
## Features
|
|
42
|
+
|
|
43
|
+
### Layout
|
|
44
|
+
|
|
45
|
+
`Rendry::Components::Layout` renders a full HTML document with `title:`, an optional `head:` block, and `view_transitions: true` for native cross-page transitions (pure CSS, no JS).
|
|
46
|
+
|
|
47
|
+
### Hypermedia (`hx`)
|
|
48
|
+
|
|
49
|
+
Build htmx attributes inline — Hash/Array values become JSON:
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
button("Delete", **hx(delete: "/tasks/1", target: "closest .task"))
|
|
53
|
+
# => hx-delete="/tasks/1" hx-target="closest .task"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Also available as `Rendry.hx` outside render blocks.
|
|
57
|
+
|
|
58
|
+
### Selective fragments
|
|
59
|
+
|
|
60
|
+
`fragment("name")` is transparent in normal renders. `Rendry.render(fragments: ["name"])` emits only the matching fragments — even nested inside components — for htmx swap endpoints.
|
|
61
|
+
|
|
62
|
+
### Fragment cache
|
|
63
|
+
|
|
64
|
+
`cache(*keys) { ... }` memoizes a DSL block in an in-memory FIFO store (`Rendry.cache_store`).
|
|
65
|
+
|
|
66
|
+
### Refreshing components
|
|
67
|
+
|
|
68
|
+
Declare polling once on the component class:
|
|
69
|
+
|
|
70
|
+
```ruby
|
|
71
|
+
class Comments < Rendry::Component
|
|
72
|
+
refreshes every: "10s", url: "/comments", target: "#comments"
|
|
73
|
+
end
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`component Comments.new(list)` is then wrapped in a `display:contents` div carrying the `hx-*` attributes. Override per call with `component X.new, refresh: { url: "/x" }`.
|
|
77
|
+
|
|
78
|
+
### Kits
|
|
79
|
+
|
|
80
|
+
Package components into a module:
|
|
81
|
+
|
|
82
|
+
```ruby
|
|
83
|
+
module Storefront
|
|
84
|
+
extend Rendry::Kit
|
|
85
|
+
component PriceTag # => Storefront[:price_tag]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
Storefront.render(:price_tag, "19.99")
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`Rendry::Components` is itself a kit: `Rendry::Components[:button]`.
|
|
92
|
+
|
|
93
|
+
## Examples
|
|
94
|
+
|
|
95
|
+
`examples/` contains numbered, standalone mini-projects — see `examples/README.md`:
|
|
96
|
+
|
|
97
|
+
| # | Project | Demo |
|
|
98
|
+
|---|---------|------|
|
|
99
|
+
| 01 | `01_static_page/` | DSL basics |
|
|
100
|
+
| 02 | `02_component_gallery/` | Components + built-in kit |
|
|
101
|
+
| 03 | `03_layout_site/` | Multi-page site, view transitions |
|
|
102
|
+
| 04 | `04_tasks_sinatra/` | Sinatra app, server state |
|
|
103
|
+
| 05 | `05_htmx_tasks/` | htmx fragment swaps |
|
|
104
|
+
| 06 | `06_fragments_cache/` | fragments + cache + `refreshes` |
|
|
105
|
+
| 07 | `07_component_kit/` | `Rendry::Kit` packaging |
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
bundle exec rake # tests
|
|
109
|
+
bundle exec rake examples # smoke-run static examples
|
|
110
|
+
ruby -Ilib test/test_rendry.rb
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Project direction
|
|
114
|
+
|
|
115
|
+
The core targets four properties:
|
|
116
|
+
|
|
117
|
+
- explicit, readable HTML;
|
|
118
|
+
- safe escaping by default;
|
|
119
|
+
- Ruby components that are easy to test;
|
|
120
|
+
- no mandatory frontend dependency.
|
|
121
|
+
|
|
122
|
+
This is not a complete web framework yet. Routing, forms, state management, and layout rendering currently belong to the host application, such as Sinatra.
|
data/lib/rendry.rb
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "cgi"
|
|
4
|
+
require "json"
|
|
5
|
+
require_relative "rendry/version"
|
|
6
|
+
|
|
7
|
+
module Rendry
|
|
8
|
+
class SafeHTML < String
|
|
9
|
+
def html_safe?
|
|
10
|
+
true
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
VOID_ELEMENTS = %w[area base br col embed hr img input link meta param source track wbr].freeze
|
|
15
|
+
BOOLEAN_ATTRIBUTES = %i[allowfullscreen async autofocus autoplay checked controls defer disabled formnovalidate hidden inert loop multiple muted nomodule novalidate open playsinline readonly required reversed selected].freeze
|
|
16
|
+
|
|
17
|
+
class Cache
|
|
18
|
+
def initialize(limit: 128)
|
|
19
|
+
@store = {}
|
|
20
|
+
@limit = limit
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def fetch(key)
|
|
24
|
+
return @store[key] if @store.key?(key)
|
|
25
|
+
|
|
26
|
+
value = yield
|
|
27
|
+
@store[key] = value
|
|
28
|
+
@store.shift while @store.size > @limit
|
|
29
|
+
value
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def clear
|
|
33
|
+
@store.clear
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
class Builder
|
|
38
|
+
attr_reader :output
|
|
39
|
+
|
|
40
|
+
def initialize(enabled: true)
|
|
41
|
+
@output = +""
|
|
42
|
+
@enabled = enabled
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render
|
|
46
|
+
@output
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def text(value)
|
|
50
|
+
write CGI.escapeHTML(value.to_s)
|
|
51
|
+
self
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def raw(value)
|
|
55
|
+
write value.to_s
|
|
56
|
+
self
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def tag(name, content = nil, **attributes, &block)
|
|
60
|
+
if content.is_a?(Hash) && attributes.empty?
|
|
61
|
+
attributes = content
|
|
62
|
+
content = nil
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if block
|
|
66
|
+
children = Builder.new(enabled: @enabled)
|
|
67
|
+
Rendry.evaluate(children, &block)
|
|
68
|
+
content = SafeHTML.new(children.render)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
output = +"<#{name}#{format_attributes(attributes)}>"
|
|
72
|
+
unless VOID_ELEMENTS.include?(name.to_s)
|
|
73
|
+
output << (content.is_a?(SafeHTML) ? content : CGI.escapeHTML(content.to_s))
|
|
74
|
+
output << "</#{name}>"
|
|
75
|
+
end
|
|
76
|
+
write output
|
|
77
|
+
self
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def fragment(name, &block)
|
|
81
|
+
filter = Rendry.current_fragments
|
|
82
|
+
if filter
|
|
83
|
+
return self unless filter.include?(name.to_s)
|
|
84
|
+
|
|
85
|
+
target = Rendry.root_builder || self
|
|
86
|
+
previous = target.instance_variable_get(:@enabled)
|
|
87
|
+
target.instance_variable_set(:@enabled, true)
|
|
88
|
+
begin
|
|
89
|
+
Rendry.evaluate(target, &block)
|
|
90
|
+
ensure
|
|
91
|
+
target.instance_variable_set(:@enabled, previous)
|
|
92
|
+
end
|
|
93
|
+
else
|
|
94
|
+
Rendry.evaluate(self, &block)
|
|
95
|
+
end
|
|
96
|
+
self
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def cache(*keys, **_options, &block)
|
|
100
|
+
location = block.source_location.join(":")
|
|
101
|
+
key = "#{location}:#{keys.inspect}"
|
|
102
|
+
html = Rendry.cache_store.fetch(key) do
|
|
103
|
+
child = Builder.new(enabled: @enabled)
|
|
104
|
+
Rendry.evaluate(child, &block)
|
|
105
|
+
SafeHTML.new(child.render)
|
|
106
|
+
end
|
|
107
|
+
write html
|
|
108
|
+
self
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def hx(**attributes)
|
|
112
|
+
Rendry.hx(**attributes)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def component(component_instance, refresh: nil, &block)
|
|
116
|
+
if block && component_instance.respond_to?(:block=) && !component_instance.instance_variable_get(:@block)
|
|
117
|
+
component_instance.block = block
|
|
118
|
+
end
|
|
119
|
+
rendered = component_instance.render
|
|
120
|
+
html = rendered.is_a?(SafeHTML) ? rendered : SafeHTML.new(CGI.escapeHTML(rendered.to_s))
|
|
121
|
+
config = refresh || (component_instance.class.refresh_config if component_instance.class.respond_to?(:refresh_config))
|
|
122
|
+
html = refresh_wrapper(html, config) if config
|
|
123
|
+
if @enabled
|
|
124
|
+
write html
|
|
125
|
+
elsif Rendry.current_fragments
|
|
126
|
+
@output << html
|
|
127
|
+
end
|
|
128
|
+
self
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def p(content = nil, **attributes, &block)
|
|
132
|
+
tag("p", content, **attributes, &block)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def method_missing(name, content = nil, **attributes, &block)
|
|
136
|
+
if name.to_s.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
137
|
+
tag(name.to_s.tr("_", "-"), content, **attributes, &block)
|
|
138
|
+
else
|
|
139
|
+
super
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def respond_to_missing?(name, include_private = false)
|
|
144
|
+
name.to_s.match?(/\A[a-z][a-z0-9_]*\z/) || super
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def write(string)
|
|
150
|
+
@output << string if @enabled
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def refresh_wrapper(html, config)
|
|
154
|
+
attributes = {
|
|
155
|
+
class: "rendry-refresh",
|
|
156
|
+
style: "display:contents",
|
|
157
|
+
hx_get: config[:url],
|
|
158
|
+
hx_trigger: config[:trigger] || (config[:every] && "every #{config[:every]}") || "load",
|
|
159
|
+
hx_target: config[:target] || "this",
|
|
160
|
+
hx_swap: config[:swap] || "outerHTML"
|
|
161
|
+
}.compact
|
|
162
|
+
output = +"<div#{format_attributes(attributes)}>"
|
|
163
|
+
output << html
|
|
164
|
+
output << "</div>"
|
|
165
|
+
SafeHTML.new(output)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def format_attributes(attributes)
|
|
169
|
+
attributes.each_with_object(+"") do |(key, value), result|
|
|
170
|
+
next if value.nil? || value == false
|
|
171
|
+
|
|
172
|
+
attribute = key.to_s.tr("_", "-")
|
|
173
|
+
if BOOLEAN_ATTRIBUTES.include?(key.to_sym)
|
|
174
|
+
result << " #{attribute}"
|
|
175
|
+
else
|
|
176
|
+
result << %( #{attribute}="#{CGI.escapeHTML(value.to_s)}")
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
class Component
|
|
183
|
+
attr_writer :block
|
|
184
|
+
|
|
185
|
+
class << self
|
|
186
|
+
def refreshes(**config)
|
|
187
|
+
@refresh_config = config
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def refresh_config
|
|
191
|
+
@refresh_config
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def render
|
|
196
|
+
raise NotImplementedError, "#{self.class} must implement #render"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
module Kit
|
|
201
|
+
def self.extended(base)
|
|
202
|
+
base.instance_variable_set(:@kit_components, {})
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def component(klass, as: nil)
|
|
206
|
+
key = (as || klass.name.to_s.split("::").last).to_s.gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym
|
|
207
|
+
kit_components[key] = klass
|
|
208
|
+
klass
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def [](name)
|
|
212
|
+
kit_components.fetch(name.to_sym) do
|
|
213
|
+
raise KeyError, "unknown component #{name.inspect} in #{self}"
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def components
|
|
218
|
+
kit_components.dup
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def render(name, *args, **kwargs, &block)
|
|
222
|
+
self[name].new(*args, **kwargs, &block).render
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private
|
|
226
|
+
|
|
227
|
+
def kit_components
|
|
228
|
+
@kit_components ||= {}
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Built-in Standard UI Components Kit
|
|
233
|
+
module Components
|
|
234
|
+
class Layout < Component
|
|
235
|
+
def initialize(title: "Rendry", head: nil, view_transitions: false, **attributes, &block)
|
|
236
|
+
@title = title
|
|
237
|
+
@head = head
|
|
238
|
+
@view_transitions = view_transitions
|
|
239
|
+
@attributes = attributes
|
|
240
|
+
@block = block
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def render
|
|
244
|
+
Rendry.render do
|
|
245
|
+
html(lang: "en", **@attributes) do
|
|
246
|
+
head do
|
|
247
|
+
meta(charset: "utf-8")
|
|
248
|
+
meta(name: "viewport", content: "width=device-width, initial-scale=1")
|
|
249
|
+
title(@title)
|
|
250
|
+
if @view_transitions
|
|
251
|
+
style { raw "@view-transition { navigation: auto; }" }
|
|
252
|
+
end
|
|
253
|
+
Rendry.evaluate(self, &@head) if @head
|
|
254
|
+
end
|
|
255
|
+
body do
|
|
256
|
+
Rendry.evaluate(self, &@block) if @block
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
class Button < Component
|
|
264
|
+
def initialize(label = nil, href: nil, variant: "primary", type: "button", **attributes, &block)
|
|
265
|
+
@label = label
|
|
266
|
+
@href = href
|
|
267
|
+
@variant = variant
|
|
268
|
+
@type = type
|
|
269
|
+
@attributes = attributes
|
|
270
|
+
@block = block
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def render
|
|
274
|
+
css = "rendry-btn rendry-btn-#{@variant} #{@attributes[:class]}".strip
|
|
275
|
+
attrs = @attributes.except(:class)
|
|
276
|
+
if @href
|
|
277
|
+
Rendry.render do
|
|
278
|
+
a(href: @href, class: css, **attrs) do
|
|
279
|
+
@label ? text(@label) : (@block && Rendry.evaluate(self, &@block))
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
else
|
|
283
|
+
Rendry.render do
|
|
284
|
+
button(type: @type, class: css, **attrs) do
|
|
285
|
+
@label ? text(@label) : (@block && Rendry.evaluate(self, &@block))
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
class Badge < Component
|
|
293
|
+
def initialize(label, variant: "default", **attributes)
|
|
294
|
+
@label = label
|
|
295
|
+
@variant = variant
|
|
296
|
+
@attributes = attributes
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def render
|
|
300
|
+
css = "rendry-badge rendry-badge-#{@variant} #{@attributes[:class]}".strip
|
|
301
|
+
Rendry.render do
|
|
302
|
+
span(@label, class: css, **@attributes.except(:class))
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
class Card < Component
|
|
308
|
+
def initialize(title: nil, **attributes, &block)
|
|
309
|
+
@title = title
|
|
310
|
+
@attributes = attributes
|
|
311
|
+
@block = block
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def render
|
|
315
|
+
css = "rendry-card #{@attributes[:class]}".strip
|
|
316
|
+
Rendry.render do
|
|
317
|
+
div(class: css, **@attributes.except(:class)) do
|
|
318
|
+
h3(@title, class: "rendry-card-title") if @title
|
|
319
|
+
div(class: "rendry-card-body") do
|
|
320
|
+
if @block
|
|
321
|
+
Rendry.evaluate(self, &@block)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
class Alert < Component
|
|
330
|
+
def initialize(message = nil, variant: "info", **attributes, &block)
|
|
331
|
+
@message = message
|
|
332
|
+
@variant = variant
|
|
333
|
+
@attributes = attributes
|
|
334
|
+
@block = block
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def render
|
|
338
|
+
css = "rendry-alert rendry-alert-#{@variant} #{@attributes[:class]}".strip
|
|
339
|
+
Rendry.render do
|
|
340
|
+
div(class: css, role: "alert", **@attributes.except(:class)) do
|
|
341
|
+
text(@message) if @message
|
|
342
|
+
if @block
|
|
343
|
+
Rendry.evaluate(self, &@block)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
extend Kit
|
|
351
|
+
component Layout
|
|
352
|
+
component Button
|
|
353
|
+
component Badge
|
|
354
|
+
component Card
|
|
355
|
+
component Alert
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
module_function
|
|
359
|
+
|
|
360
|
+
def hx(**attributes)
|
|
361
|
+
attributes.each_with_object({}) do |(key, value), result|
|
|
362
|
+
value = JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Array)
|
|
363
|
+
result[:"hx-#{key.to_s.tr('_', '-')}"] = value
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def cache_store
|
|
368
|
+
@cache_store ||= Cache.new
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def current_fragments
|
|
372
|
+
@current_fragments
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def root_builder
|
|
376
|
+
@root_builder
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def render(fragments: nil, &block)
|
|
380
|
+
filter = fragments || @current_fragments
|
|
381
|
+
builder = Builder.new(enabled: filter.nil?)
|
|
382
|
+
if fragments
|
|
383
|
+
previous_fragments = @current_fragments
|
|
384
|
+
previous_root = @root_builder
|
|
385
|
+
@current_fragments = fragments.map(&:to_s)
|
|
386
|
+
@root_builder = builder
|
|
387
|
+
begin
|
|
388
|
+
evaluate(builder, &block)
|
|
389
|
+
ensure
|
|
390
|
+
@current_fragments = previous_fragments
|
|
391
|
+
@root_builder = previous_root
|
|
392
|
+
end
|
|
393
|
+
else
|
|
394
|
+
evaluate(builder, &block)
|
|
395
|
+
end
|
|
396
|
+
SafeHTML.new(builder.render)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
def evaluate(builder, &block)
|
|
400
|
+
owner = block.binding.receiver
|
|
401
|
+
owner.instance_variables.each do |ivar|
|
|
402
|
+
next if ivar == :@output || ivar == :@enabled
|
|
403
|
+
|
|
404
|
+
builder.instance_variable_set(ivar, owner.instance_variable_get(ivar))
|
|
405
|
+
end
|
|
406
|
+
if block.lambda? && block.arity.zero?
|
|
407
|
+
builder.instance_exec(&block)
|
|
408
|
+
else
|
|
409
|
+
builder.instance_eval(&block)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rendry
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- NICE-DEV226
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
Rendry UI is a small DSL for composing HTML with Ruby: explicit markup,
|
|
14
|
+
safe escaping by default, testable components, optional htmx
|
|
15
|
+
interactivity, selective fragments, fragment caching, and component kits —
|
|
16
|
+
with no mandatory JavaScript.
|
|
17
|
+
email:
|
|
18
|
+
- NICE-DEV226@users.noreply.github.com
|
|
19
|
+
executables: []
|
|
20
|
+
extensions: []
|
|
21
|
+
extra_rdoc_files: []
|
|
22
|
+
files:
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.md
|
|
25
|
+
- lib/rendry.rb
|
|
26
|
+
- lib/rendry/version.rb
|
|
27
|
+
homepage: https://github.com/NICE-DEV226/rendry-ui
|
|
28
|
+
licenses:
|
|
29
|
+
- MIT
|
|
30
|
+
metadata:
|
|
31
|
+
homepage_uri: https://github.com/NICE-DEV226/rendry-ui
|
|
32
|
+
source_code_uri: https://github.com/NICE-DEV226/rendry-ui
|
|
33
|
+
changelog_uri: https://github.com/NICE-DEV226/rendry-ui/releases
|
|
34
|
+
rdoc_options: []
|
|
35
|
+
require_paths:
|
|
36
|
+
- lib
|
|
37
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
38
|
+
requirements:
|
|
39
|
+
- - ">="
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '3.1'
|
|
42
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
requirements: []
|
|
48
|
+
rubygems_version: 3.6.9
|
|
49
|
+
specification_version: 4
|
|
50
|
+
summary: Lightweight server-rendered HTML DSL for Ruby
|
|
51
|
+
test_files: []
|