rendry 0.1.0 → 0.2.1
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 +101 -25
- 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: 043bde156ddb2a1af19b6c8507e676330b4ad40a079cc8850ced810396559dfa
|
|
4
|
+
data.tar.gz: bf349c54a7025081672066beb879e8ca8fa4d5e2eecf81c80c4f23f2cec85d50
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 8912d9c980908d713576dc50307c2ef7b9bd09a9f0be7a3072ea8507b6d491c6af0236f84a428f9adc384702195fdfb6a966322a9f5422f709462699a9ad910b
|
|
7
|
+
data.tar.gz: 1dd76f09af3eccbf79b653abcbc0c1371b27a4f281699a35157264244e637799ac1124d50dfb8343fde8866225a2401d13de16dae7c39502ddb5a87780067e6d
|
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,7 +68,8 @@ module Rendry
|
|
|
64
68
|
|
|
65
69
|
if block
|
|
66
70
|
children = Builder.new(enabled: @enabled)
|
|
67
|
-
Rendry.evaluate(children, &block)
|
|
71
|
+
result = Rendry.evaluate(children, &block)
|
|
72
|
+
children.append_result(result)
|
|
68
73
|
content = SafeHTML.new(children.render)
|
|
69
74
|
end
|
|
70
75
|
|
|
@@ -113,6 +118,9 @@ module Rendry
|
|
|
113
118
|
end
|
|
114
119
|
|
|
115
120
|
def component(component_instance, refresh: nil, &block)
|
|
121
|
+
if component_instance.respond_to?(:view_context=)
|
|
122
|
+
component_instance.view_context = @context
|
|
123
|
+
end
|
|
116
124
|
if block && component_instance.respond_to?(:block=) && !component_instance.instance_variable_get(:@block)
|
|
117
125
|
component_instance.block = block
|
|
118
126
|
end
|
|
@@ -134,7 +142,20 @@ module Rendry
|
|
|
134
142
|
|
|
135
143
|
def method_missing(name, content = nil, **attributes, &block)
|
|
136
144
|
if name.to_s.match?(/\A[a-z][a-z0-9_]*\z/)
|
|
137
|
-
|
|
145
|
+
context = @context
|
|
146
|
+
helper = context && context.respond_to?(name) ? context.method(name) : nil
|
|
147
|
+
has_arguments = !(content.nil? && attributes.empty? && !block)
|
|
148
|
+
if helper && !(helper.arity == 0 && has_arguments)
|
|
149
|
+
if content.nil? && attributes.empty? && !block
|
|
150
|
+
context.public_send(name)
|
|
151
|
+
elsif content.nil?
|
|
152
|
+
context.public_send(name, **attributes, &block)
|
|
153
|
+
else
|
|
154
|
+
context.public_send(name, content, **attributes, &block)
|
|
155
|
+
end
|
|
156
|
+
else
|
|
157
|
+
tag(name.to_s.tr("_", "-"), content, **attributes, &block)
|
|
158
|
+
end
|
|
138
159
|
else
|
|
139
160
|
super
|
|
140
161
|
end
|
|
@@ -144,6 +165,15 @@ module Rendry
|
|
|
144
165
|
name.to_s.match?(/\A[a-z][a-z0-9_]*\z/) || super
|
|
145
166
|
end
|
|
146
167
|
|
|
168
|
+
def append_result(result)
|
|
169
|
+
if result.is_a?(SafeHTML)
|
|
170
|
+
write result
|
|
171
|
+
elsif Rendry.appendable_result?(result)
|
|
172
|
+
write CGI.escapeHTML(result.to_s)
|
|
173
|
+
end
|
|
174
|
+
self
|
|
175
|
+
end
|
|
176
|
+
|
|
147
177
|
private
|
|
148
178
|
|
|
149
179
|
def write(string)
|
|
@@ -169,7 +199,20 @@ module Rendry
|
|
|
169
199
|
attributes.each_with_object(+"") do |(key, value), result|
|
|
170
200
|
next if value.nil? || value == false
|
|
171
201
|
|
|
202
|
+
if (key == :data || key == :aria) && value.is_a?(Hash)
|
|
203
|
+
value.each do |nested_key, nested_value|
|
|
204
|
+
next if nested_value.nil? || nested_value == false
|
|
205
|
+
|
|
206
|
+
result << %( #{key}-#{nested_key.to_s.tr('_', '-')}="#{CGI.escapeHTML(nested_value.to_s)}")
|
|
207
|
+
end
|
|
208
|
+
next
|
|
209
|
+
end
|
|
210
|
+
|
|
172
211
|
attribute = key.to_s.tr("_", "-")
|
|
212
|
+
if Rendry.sanitize_urls && URL_ATTRIBUTES.include?(attribute) &&
|
|
213
|
+
value.to_s.match?(UNSAFE_URL_SCHEME)
|
|
214
|
+
value = "#"
|
|
215
|
+
end
|
|
173
216
|
if BOOLEAN_ATTRIBUTES.include?(key.to_sym)
|
|
174
217
|
result << " #{attribute}"
|
|
175
218
|
else
|
|
@@ -181,6 +224,7 @@ module Rendry
|
|
|
181
224
|
|
|
182
225
|
class Component
|
|
183
226
|
attr_writer :block
|
|
227
|
+
attr_accessor :view_context
|
|
184
228
|
|
|
185
229
|
class << self
|
|
186
230
|
def refreshes(**config)
|
|
@@ -190,10 +234,21 @@ module Rendry
|
|
|
190
234
|
def refresh_config
|
|
191
235
|
@refresh_config
|
|
192
236
|
end
|
|
237
|
+
|
|
238
|
+
def template(&block)
|
|
239
|
+
@template_block = block if block
|
|
240
|
+
@template_block || (superclass.template if superclass.respond_to?(:template))
|
|
241
|
+
end
|
|
193
242
|
end
|
|
194
243
|
|
|
195
244
|
def render
|
|
196
|
-
|
|
245
|
+
block = self.class.template
|
|
246
|
+
raise NotImplementedError, "#{self.class} must implement #render or declare a template" unless block
|
|
247
|
+
|
|
248
|
+
builder = Builder.new
|
|
249
|
+
result = Rendry.evaluate(builder, self, context: view_context, &block)
|
|
250
|
+
builder.append_result(result)
|
|
251
|
+
SafeHTML.new(builder.render)
|
|
197
252
|
end
|
|
198
253
|
end
|
|
199
254
|
|
|
@@ -355,8 +410,23 @@ module Rendry
|
|
|
355
410
|
component Alert
|
|
356
411
|
end
|
|
357
412
|
|
|
413
|
+
UNSAFE_URL_SCHEME = /\A\s*(javascript|vbscript|data(?!:image\/))\s*:/i
|
|
414
|
+
URL_ATTRIBUTES = %w[href src action formaction xlink:href].freeze
|
|
415
|
+
|
|
358
416
|
module_function
|
|
359
417
|
|
|
418
|
+
def sanitize_urls
|
|
419
|
+
@sanitize_urls != false
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def sanitize_urls=(value)
|
|
423
|
+
@sanitize_urls = value
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
def appendable_result?(value)
|
|
427
|
+
value.is_a?(String) || value.is_a?(Numeric) || value.is_a?(Symbol)
|
|
428
|
+
end
|
|
429
|
+
|
|
360
430
|
def hx(**attributes)
|
|
361
431
|
attributes.each_with_object({}) do |(key, value), result|
|
|
362
432
|
value = JSON.generate(value) if value.is_a?(Hash) || value.is_a?(Array)
|
|
@@ -369,40 +439,46 @@ module Rendry
|
|
|
369
439
|
end
|
|
370
440
|
|
|
371
441
|
def current_fragments
|
|
372
|
-
|
|
442
|
+
Thread.current[:rendry_current_fragments]
|
|
373
443
|
end
|
|
374
444
|
|
|
375
445
|
def root_builder
|
|
376
|
-
|
|
446
|
+
Thread.current[:rendry_root_builder]
|
|
377
447
|
end
|
|
378
448
|
|
|
379
449
|
def render(fragments: nil, &block)
|
|
380
|
-
filter = fragments ||
|
|
450
|
+
filter = fragments || current_fragments
|
|
381
451
|
builder = Builder.new(enabled: filter.nil?)
|
|
382
452
|
if fragments
|
|
383
|
-
previous_fragments =
|
|
384
|
-
previous_root =
|
|
385
|
-
|
|
386
|
-
|
|
453
|
+
previous_fragments = current_fragments
|
|
454
|
+
previous_root = root_builder
|
|
455
|
+
Thread.current[:rendry_current_fragments] = fragments.map(&:to_s)
|
|
456
|
+
Thread.current[:rendry_root_builder] = builder
|
|
387
457
|
begin
|
|
388
|
-
evaluate(builder, &block)
|
|
458
|
+
result = evaluate(builder, &block)
|
|
389
459
|
ensure
|
|
390
|
-
|
|
391
|
-
|
|
460
|
+
Thread.current[:rendry_current_fragments] = previous_fragments
|
|
461
|
+
Thread.current[:rendry_root_builder] = previous_root
|
|
392
462
|
end
|
|
393
463
|
else
|
|
394
|
-
evaluate(builder, &block)
|
|
464
|
+
result = evaluate(builder, &block)
|
|
395
465
|
end
|
|
466
|
+
builder.append_result(result)
|
|
396
467
|
SafeHTML.new(builder.render)
|
|
397
468
|
end
|
|
398
469
|
|
|
399
|
-
def evaluate(builder, &block)
|
|
400
|
-
owner
|
|
470
|
+
def evaluate(builder, owner = nil, context: nil, &block)
|
|
471
|
+
owner ||= block&.binding&.receiver
|
|
472
|
+
raise ArgumentError, "Rendry.evaluate requires a block" unless block && owner
|
|
473
|
+
|
|
474
|
+
inherited = owner.instance_variable_get(:@context) if owner.is_a?(Builder)
|
|
401
475
|
owner.instance_variables.each do |ivar|
|
|
402
|
-
next if ivar == :@output || ivar == :@enabled
|
|
476
|
+
next if ivar == :@output || ivar == :@enabled || ivar == :@context
|
|
403
477
|
|
|
404
478
|
builder.instance_variable_set(ivar, owner.instance_variable_get(ivar))
|
|
405
479
|
end
|
|
480
|
+
resolved = context || inherited || (owner unless owner.is_a?(Builder))
|
|
481
|
+
builder.instance_variable_set(:@context, resolved)
|
|
406
482
|
if block.lambda? && block.arity.zero?
|
|
407
483
|
builder.instance_exec(&block)
|
|
408
484
|
else
|