rendry 0.1.0 → 0.2.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 +4 -4
- data/README.md +43 -5
- data/lib/rendry/version.rb +1 -1
- data/lib/rendry.rb +111 -24
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 8fda28c5a0d8a7165d77da8ad9eac00950005d67a91905ced6f2db63dceca246
|
|
4
|
+
data.tar.gz: 56072ac6f66932de96031dfea4168313db30f48a708397201463821f08122e0e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 29490b541d776a03fdab7b7feedf00c5033d8c1e985f01c2c69c7735e4acefa769b6e522c4a3de13289a81bcecc901a053c89f62140393ceedb7546fb3fdc73f
|
|
7
|
+
data.tar.gz: 620c4bb000dfc5b6ce4834bf2156b8b790c3a9480b98f3d6f8866d3a8acbdda87e30652625473ee4c475c4f81c86dac51198e149ad3b04c68947f07dec690d98
|
data/README.md
CHANGED
|
@@ -20,23 +20,38 @@ puts html
|
|
|
20
20
|
|
|
21
21
|
Values passed to `text` and element content are escaped automatically. Use `raw` only for an HTML fragment fully controlled by your application.
|
|
22
22
|
|
|
23
|
+
A block's return value is appended as content when it is a `String`, `Numeric`, or `Symbol` — so `div { task.title }` works, and strings are escaped unless they are `SafeHTML`.
|
|
24
|
+
|
|
25
|
+
Helpers from the surrounding view context (e.g. Sinatra's `session`, `params`, or your own methods) are reachable inside DSL blocks and are written as escaped text:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
get "/" do
|
|
29
|
+
Rendry.render do
|
|
30
|
+
p { flash } # calls your `flash` helper
|
|
31
|
+
session[:count] # calls `session`
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`data:` and `aria:` attributes accept nested hashes and are flattened (`data: { toggle: "x" }` → `data-toggle="x"`). URLs with `javascript:`/`vbscript:`/non-image `data:` schemes in `href`, `src`, `action`, `formaction`, or `xlink:href` are rewritten to `#` — disable with `Rendry.sanitize_urls = false`.
|
|
37
|
+
|
|
23
38
|
## Components
|
|
24
39
|
|
|
40
|
+
The lightest form is a `template` block — no `render` method needed:
|
|
41
|
+
|
|
25
42
|
```ruby
|
|
26
43
|
class Badge < Rendry::Component
|
|
27
44
|
def initialize(label)
|
|
28
45
|
@label = label
|
|
29
46
|
end
|
|
30
47
|
|
|
31
|
-
|
|
32
|
-
|
|
48
|
+
template do
|
|
49
|
+
span(@label, class: "badge")
|
|
33
50
|
end
|
|
34
51
|
end
|
|
35
|
-
|
|
36
|
-
Rendry.render { component Badge.new("Ready") }
|
|
37
52
|
```
|
|
38
53
|
|
|
39
|
-
|
|
54
|
+
For full control, implement `#render` returning `Rendry.render { ... }` or a `SafeHTML`. Components also receive the view context (`view_context`) when rendered through `component`, so helpers work inside `template` blocks too.
|
|
40
55
|
|
|
41
56
|
## Features
|
|
42
57
|
|
|
@@ -120,3 +135,26 @@ The core targets four properties:
|
|
|
120
135
|
- no mandatory frontend dependency.
|
|
121
136
|
|
|
122
137
|
This is not a complete web framework yet. Routing, forms, state management, and layout rendering currently belong to the host application, such as Sinatra.
|
|
138
|
+
|
|
139
|
+
## Contributing
|
|
140
|
+
|
|
141
|
+
How about contributing to the project? I would be truly delighted to have contributors and people who are interested in the project.
|
|
142
|
+
|
|
143
|
+
Rendry is deliberately small — contributions that keep it small are the most welcome:
|
|
144
|
+
|
|
145
|
+
- **Bug reports and fixes** — the DSL has many edge cases (`method_missing` delegation, fragment filtering, context bridging); reproductions are gold.
|
|
146
|
+
- **New built-in components** — `Rendry::Components` is a kit; adding a component is a few lines plus a CSS class in `assets/rendry.css`.
|
|
147
|
+
- **Examples** — a new `examples/NN_name/` mini-project showing a real use case is the best documentation.
|
|
148
|
+
- **Docs** — README sections, yard comments on public API.
|
|
149
|
+
|
|
150
|
+
Please keep the zero-runtime-dependency spirit: `cgi`, `json`, and `set` are the only requires. No frontend build step, no required JS.
|
|
151
|
+
|
|
152
|
+
```sh
|
|
153
|
+
git clone https://github.com/NICE-DEV226/rendry-ui.git
|
|
154
|
+
cd rendry-ui
|
|
155
|
+
bundle install
|
|
156
|
+
bundle exec rake # run the test suite
|
|
157
|
+
bundle exec rake examples # smoke-run the examples
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Open an issue first for anything non-trivial so we can align on direction.
|
data/lib/rendry/version.rb
CHANGED
data/lib/rendry.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "cgi"
|
|
4
4
|
require "json"
|
|
5
|
+
require "set"
|
|
5
6
|
require_relative "rendry/version"
|
|
6
7
|
|
|
7
8
|
module Rendry
|
|
@@ -11,26 +12,29 @@ module Rendry
|
|
|
11
12
|
end
|
|
12
13
|
end
|
|
13
14
|
|
|
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
|
|
15
|
+
VOID_ELEMENTS = Set.new(%w[area base br col embed hr img input link meta param source track wbr]).freeze
|
|
16
|
+
BOOLEAN_ATTRIBUTES = Set.new(%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
|
|
|
17
18
|
class Cache
|
|
18
19
|
def initialize(limit: 128)
|
|
19
20
|
@store = {}
|
|
20
21
|
@limit = limit
|
|
22
|
+
@mutex = Mutex.new
|
|
21
23
|
end
|
|
22
24
|
|
|
23
25
|
def fetch(key)
|
|
24
|
-
|
|
26
|
+
@mutex.synchronize do
|
|
27
|
+
return @store[key] if @store.key?(key)
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
value = yield
|
|
30
|
+
@store[key] = value
|
|
31
|
+
@store.shift while @store.size > @limit
|
|
32
|
+
value
|
|
33
|
+
end
|
|
30
34
|
end
|
|
31
35
|
|
|
32
36
|
def clear
|
|
33
|
-
@store.clear
|
|
37
|
+
@mutex.synchronize { @store.clear }
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
|
|
@@ -64,8 +68,16 @@ module Rendry
|
|
|
64
68
|
|
|
65
69
|
if block
|
|
66
70
|
children = Builder.new(enabled: @enabled)
|
|
67
|
-
Rendry.evaluate(children, &block)
|
|
68
|
-
|
|
71
|
+
result = Rendry.evaluate(children, &block)
|
|
72
|
+
output = children.render
|
|
73
|
+
if Rendry.appendable_result?(result) && !result.is_a?(SafeHTML)
|
|
74
|
+
output = output.dup if output.frozen?
|
|
75
|
+
output << CGI.escapeHTML(result.to_s)
|
|
76
|
+
elsif result.is_a?(SafeHTML)
|
|
77
|
+
output = output.dup if output.frozen?
|
|
78
|
+
output << result
|
|
79
|
+
end
|
|
80
|
+
content = SafeHTML.new(output)
|
|
69
81
|
end
|
|
70
82
|
|
|
71
83
|
output = +"<#{name}#{format_attributes(attributes)}>"
|
|
@@ -113,6 +125,9 @@ module Rendry
|
|
|
113
125
|
end
|
|
114
126
|
|
|
115
127
|
def component(component_instance, refresh: nil, &block)
|
|
128
|
+
if component_instance.respond_to?(:view_context=)
|
|
129
|
+
component_instance.view_context = @context
|
|
130
|
+
end
|
|
116
131
|
if block && component_instance.respond_to?(:block=) && !component_instance.instance_variable_get(:@block)
|
|
117
132
|
component_instance.block = block
|
|
118
133
|
end
|
|
@@ -134,7 +149,22 @@ module Rendry
|
|
|
134
149
|
|
|
135
150
|
def method_missing(name, content = nil, **attributes, &block)
|
|
136
151
|
if name.to_s.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
137
|
-
|
|
152
|
+
context = @context
|
|
153
|
+
helper = context && context.respond_to?(name) ? context.method(name) : nil
|
|
154
|
+
has_arguments = !(content.nil? && attributes.empty? && !block)
|
|
155
|
+
if helper && !(helper.arity == 0 && has_arguments)
|
|
156
|
+
result = if content.nil? && attributes.empty? && !block
|
|
157
|
+
context.public_send(name)
|
|
158
|
+
elsif content.nil?
|
|
159
|
+
context.public_send(name, **attributes, &block)
|
|
160
|
+
else
|
|
161
|
+
context.public_send(name, content, **attributes, &block)
|
|
162
|
+
end
|
|
163
|
+
write_escaped_result(result)
|
|
164
|
+
self
|
|
165
|
+
else
|
|
166
|
+
tag(name.to_s.tr("_", "-"), content, **attributes, &block)
|
|
167
|
+
end
|
|
138
168
|
else
|
|
139
169
|
super
|
|
140
170
|
end
|
|
@@ -150,6 +180,14 @@ module Rendry
|
|
|
150
180
|
@output << string if @enabled
|
|
151
181
|
end
|
|
152
182
|
|
|
183
|
+
def write_escaped_result(result)
|
|
184
|
+
if result.is_a?(SafeHTML)
|
|
185
|
+
write result
|
|
186
|
+
elsif Rendry.appendable_result?(result)
|
|
187
|
+
write CGI.escapeHTML(result.to_s)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
153
191
|
def refresh_wrapper(html, config)
|
|
154
192
|
attributes = {
|
|
155
193
|
class: "rendry-refresh",
|
|
@@ -169,7 +207,20 @@ module Rendry
|
|
|
169
207
|
attributes.each_with_object(+"") do |(key, value), result|
|
|
170
208
|
next if value.nil? || value == false
|
|
171
209
|
|
|
210
|
+
if (key == :data || key == :aria) && value.is_a?(Hash)
|
|
211
|
+
value.each do |nested_key, nested_value|
|
|
212
|
+
next if nested_value.nil? || nested_value == false
|
|
213
|
+
|
|
214
|
+
result << %( #{key}-#{nested_key.to_s.tr('_', '-')}="#{CGI.escapeHTML(nested_value.to_s)}")
|
|
215
|
+
end
|
|
216
|
+
next
|
|
217
|
+
end
|
|
218
|
+
|
|
172
219
|
attribute = key.to_s.tr("_", "-")
|
|
220
|
+
if Rendry.sanitize_urls && URL_ATTRIBUTES.include?(attribute) &&
|
|
221
|
+
value.to_s.match?(UNSAFE_URL_SCHEME)
|
|
222
|
+
value = "#"
|
|
223
|
+
end
|
|
173
224
|
if BOOLEAN_ATTRIBUTES.include?(key.to_sym)
|
|
174
225
|
result << " #{attribute}"
|
|
175
226
|
else
|
|
@@ -181,6 +232,7 @@ module Rendry
|
|
|
181
232
|
|
|
182
233
|
class Component
|
|
183
234
|
attr_writer :block
|
|
235
|
+
attr_accessor :view_context
|
|
184
236
|
|
|
185
237
|
class << self
|
|
186
238
|
def refreshes(**config)
|
|
@@ -190,10 +242,25 @@ module Rendry
|
|
|
190
242
|
def refresh_config
|
|
191
243
|
@refresh_config
|
|
192
244
|
end
|
|
245
|
+
|
|
246
|
+
def template(&block)
|
|
247
|
+
@template_block = block if block
|
|
248
|
+
@template_block || (superclass.template if superclass.respond_to?(:template))
|
|
249
|
+
end
|
|
193
250
|
end
|
|
194
251
|
|
|
195
252
|
def render
|
|
196
|
-
|
|
253
|
+
block = self.class.template
|
|
254
|
+
raise NotImplementedError, "#{self.class} must implement #render or declare a template" unless block
|
|
255
|
+
|
|
256
|
+
builder = Builder.new
|
|
257
|
+
result = Rendry.evaluate(builder, self, context: view_context, &block)
|
|
258
|
+
if result.is_a?(SafeHTML)
|
|
259
|
+
builder.output << result
|
|
260
|
+
elsif Rendry.appendable_result?(result)
|
|
261
|
+
builder.output << CGI.escapeHTML(result.to_s)
|
|
262
|
+
end
|
|
263
|
+
SafeHTML.new(builder.render)
|
|
197
264
|
end
|
|
198
265
|
end
|
|
199
266
|
|
|
@@ -355,8 +422,23 @@ module Rendry
|
|
|
355
422
|
component Alert
|
|
356
423
|
end
|
|
357
424
|
|
|
425
|
+
UNSAFE_URL_SCHEME = /\A\s*(javascript|vbscript|data(?!:image\/))\s*:/i
|
|
426
|
+
URL_ATTRIBUTES = %w[href src action formaction xlink:href].freeze
|
|
427
|
+
|
|
358
428
|
module_function
|
|
359
429
|
|
|
430
|
+
def sanitize_urls
|
|
431
|
+
@sanitize_urls != false
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
def sanitize_urls=(value)
|
|
435
|
+
@sanitize_urls = value
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def appendable_result?(value)
|
|
439
|
+
value.is_a?(String) || value.is_a?(Numeric) || value.is_a?(Symbol)
|
|
440
|
+
end
|
|
441
|
+
|
|
360
442
|
def hx(**attributes)
|
|
361
443
|
attributes.each_with_object({}) do |(key, value), result|
|
|
362
444
|
value = JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Array)
|
|
@@ -369,26 +451,26 @@ module Rendry
|
|
|
369
451
|
end
|
|
370
452
|
|
|
371
453
|
def current_fragments
|
|
372
|
-
|
|
454
|
+
Thread.current[:rendry_current_fragments]
|
|
373
455
|
end
|
|
374
456
|
|
|
375
457
|
def root_builder
|
|
376
|
-
|
|
458
|
+
Thread.current[:rendry_root_builder]
|
|
377
459
|
end
|
|
378
460
|
|
|
379
461
|
def render(fragments: nil, &block)
|
|
380
|
-
filter = fragments ||
|
|
462
|
+
filter = fragments || current_fragments
|
|
381
463
|
builder = Builder.new(enabled: filter.nil?)
|
|
382
464
|
if fragments
|
|
383
|
-
previous_fragments =
|
|
384
|
-
previous_root =
|
|
385
|
-
|
|
386
|
-
|
|
465
|
+
previous_fragments = current_fragments
|
|
466
|
+
previous_root = root_builder
|
|
467
|
+
Thread.current[:rendry_current_fragments] = fragments.map(&:to_s)
|
|
468
|
+
Thread.current[:rendry_root_builder] = builder
|
|
387
469
|
begin
|
|
388
470
|
evaluate(builder, &block)
|
|
389
471
|
ensure
|
|
390
|
-
|
|
391
|
-
|
|
472
|
+
Thread.current[:rendry_current_fragments] = previous_fragments
|
|
473
|
+
Thread.current[:rendry_root_builder] = previous_root
|
|
392
474
|
end
|
|
393
475
|
else
|
|
394
476
|
evaluate(builder, &block)
|
|
@@ -396,13 +478,18 @@ module Rendry
|
|
|
396
478
|
SafeHTML.new(builder.render)
|
|
397
479
|
end
|
|
398
480
|
|
|
399
|
-
def evaluate(builder, &block)
|
|
400
|
-
owner
|
|
481
|
+
def evaluate(builder, owner = nil, context: nil, &block)
|
|
482
|
+
owner ||= block&.binding&.receiver
|
|
483
|
+
raise ArgumentError, "Rendry.evaluate requires a block" unless block && owner
|
|
484
|
+
|
|
485
|
+
inherited = owner.instance_variable_get(:@context) if owner.is_a?(Builder)
|
|
401
486
|
owner.instance_variables.each do |ivar|
|
|
402
|
-
next if ivar == :@output || ivar == :@enabled
|
|
487
|
+
next if ivar == :@output || ivar == :@enabled || ivar == :@context
|
|
403
488
|
|
|
404
489
|
builder.instance_variable_set(ivar, owner.instance_variable_get(ivar))
|
|
405
490
|
end
|
|
491
|
+
resolved = context || inherited || (owner unless owner.is_a?(Builder))
|
|
492
|
+
builder.instance_variable_set(:@context, resolved)
|
|
406
493
|
if block.lambda? && block.arity.zero?
|
|
407
494
|
builder.instance_exec(&block)
|
|
408
495
|
else
|