rubyoshka 0.2 → 0.6.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/CHANGELOG.md +30 -0
- data/README.md +152 -16
- data/lib/rubyoshka.rb +35 -182
- data/lib/rubyoshka/html.rb +38 -0
- data/lib/rubyoshka/renderer.rb +198 -0
- data/lib/rubyoshka/version.rb +1 -1
- metadata +20 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ae561e0250334107b9b82a72fc72ab672622a7f037044e8e18154e79b71a54b5
|
4
|
+
data.tar.gz: e5cddea7df6231799b38339bc014373a6c9d6a96bb60d3c7b7107e48321b3c6d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 675b83551aa791f6bb1539c80c40d267e3caa1aceed3748af04ca1d2a49bcac3ad237a8d8c0f0dd69d24714eadbb47bf4a929af77c3ef2c71155e806ff611c66
|
7
|
+
data.tar.gz: 7dd19daa9df716417c894130ff8fdae0505f83a63de17709634b2c7c83384f56453c13adbee109e4961f44a64f1c46e151752d1fc5bf690ad6738402b023f9bf
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,33 @@
|
|
1
|
+
0.6.1 2021-03-03
|
2
|
+
----------------
|
3
|
+
|
4
|
+
* Remove support for Ruby 2.6
|
5
|
+
|
6
|
+
0.6 2021-03-03
|
7
|
+
--------------
|
8
|
+
|
9
|
+
* Fix Rubyoshka on Ruby 3.0
|
10
|
+
* Refactor and add more tests
|
11
|
+
|
12
|
+
0.5 2021-02-27
|
13
|
+
--------------
|
14
|
+
|
15
|
+
* Add support for rendering XML
|
16
|
+
* Add Rubyoshka.component method
|
17
|
+
* Remove Modulation dependency
|
18
|
+
|
19
|
+
0.4 2019-02-05
|
20
|
+
--------------
|
21
|
+
|
22
|
+
* Add support for emitting component modules
|
23
|
+
|
24
|
+
0.3 2019-01-13
|
25
|
+
--------------
|
26
|
+
|
27
|
+
* Implement caching
|
28
|
+
* Improve performance
|
29
|
+
* Handle attributes with `false` value correctly
|
30
|
+
|
1
31
|
0.2 2019-01-07
|
2
32
|
--------------
|
3
33
|
|
data/README.md
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
[INSTALL](#installing-rubyoshka) |
|
4
4
|
[TUTORIAL](#getting-started) |
|
5
5
|
[EXAMPLES](examples) |
|
6
|
-
[REFERENCE](reference)
|
6
|
+
[REFERENCE](#api-reference)
|
7
7
|
|
8
8
|
## What is Rubyoshka
|
9
9
|
|
@@ -16,21 +16,30 @@ features:
|
|
16
16
|
- Use global and local contexts to pass values to reusable components
|
17
17
|
- Automatic HTML escaping
|
18
18
|
- Composable nested components
|
19
|
-
-
|
20
|
-
- About 4 times faster than ERubis (see [benchmark](examples/perf.rb)).
|
19
|
+
- Template caching from fragments to whole templates
|
21
20
|
|
22
21
|
> **Note** Rubyoshka is a new library and as such may be missing features and
|
23
|
-
> contain bugs.
|
22
|
+
> contain bugs. Also, its API may change unexpectedly. Your issue reports and
|
23
|
+
> code contributions are most welcome!
|
24
24
|
|
25
25
|
With Rubyoshka you can structure your templates like a Russian doll, each
|
26
26
|
component containing any number of nested components, in a somewhat similar
|
27
|
-
fashion to React. The name *Rubyoshka* is a nod to
|
27
|
+
fashion to React. The name *Rubyoshka* is a nod to
|
28
|
+
[Matryoshka](https://en.wikipedia.org/wiki/Matryoshka_doll), the Russian
|
28
29
|
nesting doll.
|
29
30
|
|
30
31
|
## Installing Rubyoshka
|
31
32
|
|
33
|
+
Using bundler:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
gem 'rubyoshka'
|
37
|
+
```
|
38
|
+
|
39
|
+
Or manually:
|
40
|
+
|
32
41
|
```bash
|
33
|
-
$ gem install
|
42
|
+
$ gem install rubyoshka
|
34
43
|
```
|
35
44
|
|
36
45
|
## Getting started
|
@@ -217,16 +226,17 @@ greeting.render(name: 'world')
|
|
217
226
|
## Templates as components
|
218
227
|
|
219
228
|
Rubyoshka makes it easy to compose multiple separate templates into a whole HTML
|
220
|
-
document. Each template can be defined as a self-contained component that can
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
returns a Rubyoshka instance:
|
229
|
+
document. Each template can be defined as a self-contained component that can be
|
230
|
+
reused inside other components. Components can be defined as either a Rubyoshka
|
231
|
+
instance (using `#H`), a `proc` that returns a Rubyoshka instance, or using
|
232
|
+
`Rubyoshka.component`:
|
225
233
|
|
226
234
|
```ruby
|
235
|
+
# Simple component relying on global/local context
|
227
236
|
Title = H { h1 title }
|
228
237
|
|
229
|
-
#
|
238
|
+
# Proc component that returns a template
|
239
|
+
# Notice how the lambda expression takes keyword arguments
|
230
240
|
Item = ->(id:, text:, checked:) {
|
231
241
|
H {
|
232
242
|
li {
|
@@ -236,20 +246,29 @@ Item = ->(id:, text:, checked:) {
|
|
236
246
|
}
|
237
247
|
}
|
238
248
|
|
239
|
-
|
249
|
+
# Components using Rubyoshka.component (or H.component) are a bit more compact.
|
250
|
+
# Any parameters are passed as arguments to the block.
|
251
|
+
NavBar = Rubyoshka.component do |links|
|
252
|
+
div {
|
253
|
+
links.each { |l| a l[:title], href: l[:url] }
|
254
|
+
}
|
255
|
+
end
|
256
|
+
|
257
|
+
def render_items(items, links)
|
240
258
|
html = H {
|
241
259
|
Title()
|
260
|
+
NavBar(links)
|
242
261
|
ul {
|
243
262
|
items.each { |id, attributes|
|
244
263
|
Item id: id, text: attributes[:text], checked: attributes[:active]
|
245
264
|
}
|
246
265
|
}
|
247
|
-
}.render
|
266
|
+
}.render(title: 'Hello from components')
|
248
267
|
end
|
249
268
|
```
|
250
269
|
|
251
270
|
Note that a component is invoked as a method, which means that if no arguments
|
252
|
-
are passed, you
|
271
|
+
are passed, you must add an empty pair of parens, as shown in the example
|
253
272
|
above.
|
254
273
|
|
255
274
|
In addition to using components defined as constants, you can also use
|
@@ -265,6 +284,54 @@ H {
|
|
265
284
|
}
|
266
285
|
```
|
267
286
|
|
287
|
+
## Fragment caching
|
288
|
+
|
289
|
+
Any part of a Rubyoshka template can be cached - a fragment, a component, or a
|
290
|
+
whole template. It is up to you, the user, to determine which parts of the
|
291
|
+
template to cache. By default, a call to `#cache` creates a cache entry based on
|
292
|
+
the source location of the cached block:
|
293
|
+
|
294
|
+
```ruby
|
295
|
+
Head = H {
|
296
|
+
cache {
|
297
|
+
head {
|
298
|
+
title 'My app'
|
299
|
+
style "@import '/app.css';"
|
300
|
+
}
|
301
|
+
}
|
302
|
+
}
|
303
|
+
```
|
304
|
+
|
305
|
+
However, if your template references local or global variables, you'll want to
|
306
|
+
take those into account when caching. This is done by passing any variables used
|
307
|
+
in the template to `#cache` in order to create separate cache entries for each
|
308
|
+
discrete value or combination of values:
|
309
|
+
|
310
|
+
```ruby
|
311
|
+
Greeting = H {
|
312
|
+
cache(name) {
|
313
|
+
div {
|
314
|
+
span "Hello, #{name}"
|
315
|
+
}
|
316
|
+
}
|
317
|
+
}
|
318
|
+
|
319
|
+
names = %w{tommy dolly world}
|
320
|
+
App = H {
|
321
|
+
names.each { |n| Greeting(name: n) }
|
322
|
+
}
|
323
|
+
```
|
324
|
+
|
325
|
+
In the above example a separate cache entry will be created for each name. The
|
326
|
+
use of caching in components is especially beneficial since components may be
|
327
|
+
reused in multiple different templates in your app.
|
328
|
+
|
329
|
+
### Changing the cache store
|
330
|
+
|
331
|
+
Rubyoshka ships with a naïve in-memory cache store built-in. You can use
|
332
|
+
another cache store by overriding the `Rubyoshka.cache` method (see API
|
333
|
+
[reference](#rubyoshkacache)).
|
334
|
+
|
268
335
|
## Wrapping arbitrary HTML with a component
|
269
336
|
|
270
337
|
Components can also be used to wrap arbitrary HTML with addional markup. This is
|
@@ -347,7 +414,37 @@ Blog = H {
|
|
347
414
|
}
|
348
415
|
```
|
349
416
|
|
350
|
-
|
417
|
+
### A higher-order list component
|
418
|
+
|
419
|
+
Here's another demonstration of a higher-order component, a list component that
|
420
|
+
takes an item component as an argument. The `List` component can be reused for
|
421
|
+
rendering any kind of unordered list, and with any kind of item component:
|
422
|
+
|
423
|
+
```ruby
|
424
|
+
List = ->(items, item_component) {
|
425
|
+
H {
|
426
|
+
ul {
|
427
|
+
items.each { |item|
|
428
|
+
with(item: item) {
|
429
|
+
li { emit item_component }
|
430
|
+
}
|
431
|
+
}
|
432
|
+
}
|
433
|
+
}
|
434
|
+
}
|
435
|
+
|
436
|
+
TodoItem = H {
|
437
|
+
span item.text, class: item.completed ? 'completed' : 'pending'
|
438
|
+
}
|
439
|
+
|
440
|
+
def todo_list(items)
|
441
|
+
H {
|
442
|
+
div { List(items, TodoItem) }
|
443
|
+
}
|
444
|
+
end
|
445
|
+
```
|
446
|
+
|
447
|
+
## API Reference
|
351
448
|
|
352
449
|
#### `Rubyoshka#initialize(**context, &block)` a.k.a. `Kernel#H`
|
353
450
|
|
@@ -379,6 +476,17 @@ an upper-case letter, it is considered a [component](#templates-as-components).
|
|
379
476
|
|
380
477
|
If a text argument is given for a tag, it will be escaped.
|
381
478
|
|
479
|
+
#### `#cache(*vary, &block)`
|
480
|
+
|
481
|
+
- `vary`: variables used in cached block. The given values will be used to
|
482
|
+
create a separate cache entry.
|
483
|
+
- `block`: inner HTML block
|
484
|
+
|
485
|
+
Caches the markup in the given block, storing it in the Rubyoshka cache store.
|
486
|
+
If a cache entry for the given block is found, it will be used instead of
|
487
|
+
invoking the block. If one or more variables given, those will be used to create
|
488
|
+
a separate cache entry.
|
489
|
+
|
382
490
|
#### `#context`
|
383
491
|
|
384
492
|
Accesses the [global context](#global-context).
|
@@ -409,3 +517,31 @@ Adds text without wrapping it in a tag. The text will be escaped.
|
|
409
517
|
|
410
518
|
Sets a [local context](#local-context) for use inside the given block. The
|
411
519
|
previous local context will be restored upon exiting the given block.
|
520
|
+
|
521
|
+
#### `Rubyoshka.cache`
|
522
|
+
|
523
|
+
Returns the cache store. A cache store should implement two methods - `#[]` and
|
524
|
+
`#[]=`. Here's an example implementing a Redis-based cache store:
|
525
|
+
|
526
|
+
```ruby
|
527
|
+
class RedisTemplateCache
|
528
|
+
def initialize(conn, prefix)
|
529
|
+
@conn = conn
|
530
|
+
@prefix = prefix
|
531
|
+
end
|
532
|
+
|
533
|
+
def [](key)
|
534
|
+
@conn.get("#{prefix}:#{key}")
|
535
|
+
end
|
536
|
+
|
537
|
+
def []=(key, value)
|
538
|
+
@conn.set("#{prefix}:#{key}", value)
|
539
|
+
end
|
540
|
+
end
|
541
|
+
|
542
|
+
TEMPLATE_CACHE = RedisTemplaceCache.new(redis_conn, "templates:cache")
|
543
|
+
|
544
|
+
def Rubyoshka.cache
|
545
|
+
TEMPLATE_CACHE
|
546
|
+
end
|
547
|
+
```
|
data/lib/rubyoshka.rb
CHANGED
@@ -1,204 +1,57 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'modulation/gem'
|
4
3
|
require 'escape_utils'
|
5
4
|
|
6
|
-
|
5
|
+
require_relative 'rubyoshka/renderer'
|
7
6
|
|
8
7
|
# A Rubyoshka is a template representing a piece of HTML
|
9
8
|
class Rubyoshka
|
10
|
-
# A Rendering is a rendering of a Rubyoshka
|
11
|
-
class Rendering
|
12
|
-
attr_reader :context
|
13
|
-
|
14
|
-
# Initializes attributes and renders the given block
|
15
|
-
# @param context [Hash] rendering context
|
16
|
-
# @param block [Proc] template block
|
17
|
-
# @return [void]
|
18
|
-
def initialize(context, &block)
|
19
|
-
@context = context
|
20
|
-
@buffer = +''
|
21
|
-
instance_eval(&block)
|
22
|
-
end
|
23
|
-
|
24
|
-
# Returns the result of the rendering
|
25
|
-
# @return [String]
|
26
|
-
def to_s
|
27
|
-
@buffer
|
28
|
-
end
|
29
|
-
|
30
|
-
S_TAG_METHOD = <<~EOF
|
31
|
-
def %1$s(*args, &block)
|
32
|
-
tag(:%1$s, *args, &block)
|
33
|
-
end
|
34
|
-
EOF
|
35
|
-
|
36
|
-
R_CONST_SYM = /^[A-Z]/
|
37
|
-
|
38
|
-
# Catches undefined tag method call and handles them by defining the method
|
39
|
-
# @param sym [Symbol] HTML tag or component identifier
|
40
|
-
# @param args [Array] method call arguments
|
41
|
-
# @param block [Proc] block passed to method call
|
42
|
-
# @return [void]
|
43
|
-
def method_missing(sym, *args, &block)
|
44
|
-
value = @local && @local[sym]
|
45
|
-
return value if value
|
46
|
-
|
47
|
-
if sym =~ R_CONST_SYM
|
48
|
-
o = instance_eval(sym.to_s) rescue Rubyoshka.const_get(sym) \
|
49
|
-
rescue Object.const_get(sym)
|
50
|
-
case o
|
51
|
-
when ::Proc
|
52
|
-
self.class.define_method(sym) { |*a, &b| emit(o.(*a, &b)) }
|
53
|
-
emit(o.(*args, &block))
|
54
|
-
when Rubyoshka
|
55
|
-
self.class.define_method(sym) { |**ctx|
|
56
|
-
ctx.empty? ? emit(o) : with(ctx) { emit(o) }
|
57
|
-
}
|
58
|
-
ctx = args.first
|
59
|
-
Hash === ctx ? with(ctx) { emit(o) } : emit(o)
|
60
|
-
when ::String
|
61
|
-
@buffer << o
|
62
|
-
else
|
63
|
-
e = StandardError.new "Cannot render #{o.inspect}"
|
64
|
-
e.set_backtrace(caller)
|
65
|
-
raise e
|
66
|
-
end
|
67
|
-
else
|
68
|
-
self.class.class_eval(S_TAG_METHOD % sym)
|
69
|
-
tag(sym, *args, &block)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
# Emits the given object into the rendering buffer
|
74
|
-
# @param o [Proc, Rubyoshka, String] emitted object
|
75
|
-
# @return [void]
|
76
|
-
def emit(o)
|
77
|
-
case o
|
78
|
-
when ::Proc
|
79
|
-
instance_eval(&o)
|
80
|
-
when Rubyoshka
|
81
|
-
instance_eval(&o.block)
|
82
|
-
when nil
|
83
|
-
else
|
84
|
-
@buffer << o.to_s
|
85
|
-
end
|
86
|
-
end
|
87
|
-
alias_method :e, :emit
|
88
|
-
|
89
|
-
S_LT = '<'
|
90
|
-
S_GT = '>'
|
91
|
-
S_LT_SLASH = '</'
|
92
|
-
S_SPACE_LT_SLASH = ' </'
|
93
|
-
S_SLASH_GT = '/>'
|
94
|
-
S_SPACE = ' '
|
95
|
-
S_EQUAL_QUOTE = '="'
|
96
|
-
S_QUOTE = '"'
|
97
|
-
|
98
|
-
E = EscapeUtils
|
99
|
-
|
100
|
-
# Emits an HTML tag
|
101
|
-
# @param sym [Symbol] HTML tag
|
102
|
-
# @param text [String] text content of tag
|
103
|
-
# @param props [Hash] tag attributes
|
104
|
-
# @param block [Proc] nested HTML block
|
105
|
-
# @return [void]
|
106
|
-
def tag(sym, text = nil, **props, &block)
|
107
|
-
sym = sym.to_s
|
108
|
-
|
109
|
-
@buffer << S_LT << sym
|
110
|
-
emit_props(props) unless props.empty?
|
111
|
-
|
112
|
-
if block
|
113
|
-
@buffer << S_GT
|
114
|
-
instance_eval(&block)
|
115
|
-
@buffer << S_LT_SLASH << sym << S_GT
|
116
|
-
elsif Rubyoshka === text
|
117
|
-
@buffer << S_GT
|
118
|
-
emit(text)
|
119
|
-
@buffer << S_LT_SLASH << sym << S_GT
|
120
|
-
elsif text
|
121
|
-
@buffer << S_GT << E.escape_html(text.to_s) <<
|
122
|
-
S_LT_SLASH << sym << S_GT
|
123
|
-
else
|
124
|
-
@buffer << S_SLASH_GT
|
125
|
-
end
|
126
|
-
end
|
127
|
-
|
128
|
-
# Emits tag attributes into the rendering buffer
|
129
|
-
# @param props [Hash] tag attributes
|
130
|
-
# @return [void]
|
131
|
-
def emit_props(props)
|
132
|
-
props.each { |k, v|
|
133
|
-
case k
|
134
|
-
when :text
|
135
|
-
when :src, :href
|
136
|
-
@buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
|
137
|
-
E.escape_uri(v) << S_QUOTE
|
138
|
-
else
|
139
|
-
if v == true
|
140
|
-
@buffer << S_SPACE << k.to_s
|
141
|
-
else
|
142
|
-
@buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE << v << S_QUOTE
|
143
|
-
end
|
144
|
-
end
|
145
|
-
}
|
146
|
-
end
|
147
|
-
|
148
|
-
# Emits the p tag
|
149
|
-
# @param text [String] text content of tag
|
150
|
-
# @param props [Hash] tag attributes
|
151
|
-
# @para block [Proc] nested HTML block
|
152
|
-
# @return [void]
|
153
|
-
def p(text = nil, **props, &block)
|
154
|
-
tag(:p, text, **props, &block)
|
155
|
-
end
|
156
|
-
|
157
|
-
S_HTML5_DOCTYPE = '<!DOCTYPE html>'
|
158
|
-
|
159
|
-
# Emits an HTML5 doctype tag and an html tag with the given block
|
160
|
-
# @param block [Proc] nested HTML block
|
161
|
-
# @return [void]
|
162
|
-
def html5(&block)
|
163
|
-
@buffer << S_HTML5_DOCTYPE
|
164
|
-
self.html(&block)
|
165
|
-
end
|
166
|
-
|
167
|
-
# Emits text into the rendering buffer
|
168
|
-
# @param data [String] text
|
169
|
-
def text(data)
|
170
|
-
@buffer << E.escape_html(data)
|
171
|
-
end
|
172
|
-
|
173
|
-
# Sets a local context for the given block
|
174
|
-
# @param ctx [Hash] context hash
|
175
|
-
# @param block [Proc] nested HTML block
|
176
|
-
# @return [void]
|
177
|
-
def with(**ctx, &block)
|
178
|
-
old_local, @local = @local, ctx
|
179
|
-
instance_eval(&block)
|
180
|
-
ensure
|
181
|
-
@local = old_local
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
9
|
attr_reader :block
|
186
10
|
|
187
11
|
# Initializes a Rubyoshka with the given block
|
188
12
|
# @param ctx [Hash] local context
|
189
13
|
# @param block [Proc] nested HTML block
|
190
14
|
# @param [void]
|
191
|
-
def initialize(**ctx, &block)
|
192
|
-
@
|
15
|
+
def initialize(mode: :html, **ctx, &block)
|
16
|
+
@mode = mode
|
17
|
+
@block = ctx.empty? ? block : proc { with(**ctx, &block) }
|
193
18
|
end
|
194
19
|
|
20
|
+
H_EMPTY = {}.freeze
|
21
|
+
|
195
22
|
# Renders the associated block and returns the string result
|
196
23
|
# @param context [Hash] context
|
197
24
|
# @return [String]
|
198
|
-
def render(
|
199
|
-
|
25
|
+
def render(context = H_EMPTY)
|
26
|
+
renderer_class.new(context, &block).to_s
|
27
|
+
end
|
28
|
+
|
29
|
+
def renderer_class
|
30
|
+
case @mode
|
31
|
+
when :html
|
32
|
+
HTMLRenderer
|
33
|
+
when :xml
|
34
|
+
XMLRenderer
|
35
|
+
else
|
36
|
+
raise "Invalid mode #{@mode.inspect}"
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
@@cache = {}
|
41
|
+
|
42
|
+
def self.cache
|
43
|
+
@@cache
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.component(&block)
|
47
|
+
proc { |*args| new { instance_exec(*args, &block) } }
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.xml(**ctx, &block)
|
51
|
+
new(mode: :xml, **ctx, &block)
|
200
52
|
end
|
201
53
|
end
|
54
|
+
::H = Rubyoshka
|
202
55
|
|
203
56
|
module ::Kernel
|
204
57
|
# Convenience method for creating a new Rubyoshka
|
@@ -206,6 +59,6 @@ module ::Kernel
|
|
206
59
|
# @param block [Proc] nested block
|
207
60
|
# @return [Rubyoshka] Rubyoshka template
|
208
61
|
def H(**ctx, &block)
|
209
|
-
Rubyoshka.new(ctx, &block)
|
62
|
+
Rubyoshka.new(**ctx, &block)
|
210
63
|
end
|
211
64
|
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './html'
|
4
|
+
|
5
|
+
class Rubyoshka
|
6
|
+
# Markup extensions
|
7
|
+
module HTML
|
8
|
+
# Emits the p tag (overrides Object#p)
|
9
|
+
# @param text [String] text content of tag
|
10
|
+
# @param props [Hash] tag attributes
|
11
|
+
# @para block [Proc] nested HTML block
|
12
|
+
# @return [void]
|
13
|
+
def p(text = nil, **props, &block)
|
14
|
+
method_missing(:p, text, **props, &block)
|
15
|
+
end
|
16
|
+
|
17
|
+
S_HTML5_DOCTYPE = '<!DOCTYPE html>'
|
18
|
+
|
19
|
+
# Emits an HTML5 doctype tag and an html tag with the given block
|
20
|
+
# @param block [Proc] nested HTML block
|
21
|
+
# @return [void]
|
22
|
+
def html5(&block)
|
23
|
+
@buffer << S_HTML5_DOCTYPE
|
24
|
+
self.html(&block)
|
25
|
+
end
|
26
|
+
|
27
|
+
def link_stylesheet(href, custom_attributes = nil)
|
28
|
+
attributes = {
|
29
|
+
rel: 'stylesheet',
|
30
|
+
href: href
|
31
|
+
}
|
32
|
+
if custom_attributes
|
33
|
+
attributes = custom_attributes.merge(attributes)
|
34
|
+
end
|
35
|
+
link(**attributes)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,198 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './html'
|
4
|
+
|
5
|
+
class Rubyoshka
|
6
|
+
# A Renderer is a rendering of a Rubyoshka
|
7
|
+
class Renderer
|
8
|
+
attr_reader :context
|
9
|
+
|
10
|
+
# Initializes attributes and renders the given block
|
11
|
+
# @param context [Hash] rendering context
|
12
|
+
# @param block [Proc] template block
|
13
|
+
# @return [void]
|
14
|
+
def initialize(context, &block)
|
15
|
+
@context = context
|
16
|
+
@buffer = +''
|
17
|
+
instance_eval(&block)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Returns the result of the rendering
|
21
|
+
# @return [String]
|
22
|
+
def to_s
|
23
|
+
@buffer
|
24
|
+
end
|
25
|
+
|
26
|
+
def escape_text(text)
|
27
|
+
raise NotImplementedError
|
28
|
+
end
|
29
|
+
|
30
|
+
def escape_uri(uri)
|
31
|
+
EscapeUtils.escape_uri(v)
|
32
|
+
end
|
33
|
+
|
34
|
+
S_TAG_METHOD_LINE = __LINE__ + 1
|
35
|
+
S_TAG_METHOD = <<~EOF
|
36
|
+
S_TAG_%<TAG>s_PRE = '<%<tag>s'
|
37
|
+
S_TAG_%<TAG>s_CLOSE = '</%<tag>s>'
|
38
|
+
|
39
|
+
def %<tag>s(text = nil, **props, &block)
|
40
|
+
@buffer << S_TAG_%<TAG>s_PRE
|
41
|
+
emit_props(props) unless props.empty?
|
42
|
+
|
43
|
+
if block
|
44
|
+
@buffer << S_GT
|
45
|
+
instance_eval(&block)
|
46
|
+
@buffer << S_TAG_%<TAG>s_CLOSE
|
47
|
+
elsif Rubyoshka === text
|
48
|
+
@buffer << S_GT
|
49
|
+
emit(text)
|
50
|
+
@buffer << S_TAG_%<TAG>s_CLOSE
|
51
|
+
elsif text
|
52
|
+
@buffer << S_GT << escape_text(text.to_s) << S_TAG_%<TAG>s_CLOSE
|
53
|
+
else
|
54
|
+
@buffer << S_SLASH_GT
|
55
|
+
end
|
56
|
+
end
|
57
|
+
EOF
|
58
|
+
|
59
|
+
R_CONST_SYM = /^[A-Z]/
|
60
|
+
|
61
|
+
# Catches undefined tag method call and handles them by defining the method
|
62
|
+
# @param sym [Symbol] HTML tag or component identifier
|
63
|
+
# @param args [Array] method call arguments
|
64
|
+
# @param block [Proc] block passed to method call
|
65
|
+
# @return [void]
|
66
|
+
def method_missing(sym, *args, **opts, &block)
|
67
|
+
value = @local && @local[sym]
|
68
|
+
return value if value
|
69
|
+
|
70
|
+
if sym =~ R_CONST_SYM
|
71
|
+
# Component reference (capitalized method name)
|
72
|
+
o = instance_eval(sym.to_s) rescue Rubyoshka.const_get(sym) \
|
73
|
+
rescue Object.const_get(sym)
|
74
|
+
case o
|
75
|
+
when ::Proc
|
76
|
+
self.class.define_method(sym) { |*a, **c, &b| emit(o.(*a, **c, &b)) }
|
77
|
+
STDOUT.puts({o: o, args: args, opts: opts, block: block}.inspect)
|
78
|
+
emit(o.(*args, **opts, &block))
|
79
|
+
when Rubyoshka
|
80
|
+
self.class.define_method(sym) do |**ctx|
|
81
|
+
ctx.empty? ? emit(o) : with(**ctx) { emit(o) }
|
82
|
+
end
|
83
|
+
Hash === opts.empty? ? emit(o) : with(**opts) { emit(o) }
|
84
|
+
when ::String
|
85
|
+
@buffer << o
|
86
|
+
else
|
87
|
+
e = StandardError.new "Cannot render #{o.inspect}"
|
88
|
+
e.set_backtrace(caller)
|
89
|
+
raise e
|
90
|
+
end
|
91
|
+
else
|
92
|
+
tag = sym.to_s
|
93
|
+
code = S_TAG_METHOD % { tag: tag, TAG: tag.upcase }
|
94
|
+
self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
|
95
|
+
send(sym, *args, **opts, &block)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Emits the given object into the rendering buffer
|
100
|
+
# @param o [Proc, Rubyoshka, Module, String] emitted object
|
101
|
+
# @return [void]
|
102
|
+
def emit(o)
|
103
|
+
case o
|
104
|
+
when ::Proc
|
105
|
+
instance_eval(&o)
|
106
|
+
when Rubyoshka
|
107
|
+
instance_eval(&o.block)
|
108
|
+
when Module
|
109
|
+
# If module is given, the component is expected to be a const inside the module
|
110
|
+
emit(o::Component)
|
111
|
+
when nil
|
112
|
+
else
|
113
|
+
@buffer << o.to_s
|
114
|
+
end
|
115
|
+
end
|
116
|
+
alias_method :e, :emit
|
117
|
+
|
118
|
+
S_LT = '<'
|
119
|
+
S_GT = '>'
|
120
|
+
S_LT_SLASH = '</'
|
121
|
+
S_SPACE_LT_SLASH = ' </'
|
122
|
+
S_SLASH_GT = '/>'
|
123
|
+
S_SPACE = ' '
|
124
|
+
S_EQUAL_QUOTE = '="'
|
125
|
+
S_QUOTE = '"'
|
126
|
+
|
127
|
+
# Emits tag attributes into the rendering buffer
|
128
|
+
# @param props [Hash] tag attributes
|
129
|
+
# @return [void]
|
130
|
+
def emit_props(props)
|
131
|
+
props.each { |k, v|
|
132
|
+
case k
|
133
|
+
when :src, :href
|
134
|
+
@buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
|
135
|
+
EscapeUtils.escape_uri(v) << S_QUOTE
|
136
|
+
else
|
137
|
+
case v
|
138
|
+
when true
|
139
|
+
@buffer << S_SPACE << k.to_s
|
140
|
+
when false, nil
|
141
|
+
# emit nothing
|
142
|
+
else
|
143
|
+
@buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE << v << S_QUOTE
|
144
|
+
end
|
145
|
+
end
|
146
|
+
}
|
147
|
+
end
|
148
|
+
|
149
|
+
# Emits text into the rendering buffer
|
150
|
+
# @param data [String] text
|
151
|
+
def text(data)
|
152
|
+
@buffer << escape_text(data)
|
153
|
+
end
|
154
|
+
|
155
|
+
# Sets a local context for the given block
|
156
|
+
# @param ctx [Hash] context hash
|
157
|
+
# @param block [Proc] nested HTML block
|
158
|
+
# @return [void]
|
159
|
+
def with(**ctx, &block)
|
160
|
+
old_local, @local = @local, ctx
|
161
|
+
instance_eval(&block)
|
162
|
+
ensure
|
163
|
+
@local = old_local
|
164
|
+
end
|
165
|
+
|
166
|
+
# Caches the given block with the given arguments as cache key
|
167
|
+
# @param vary [*Object] cache key
|
168
|
+
# @param block [Proc] nested HTML block
|
169
|
+
# @return [void]
|
170
|
+
def cache(*vary, **opts, &block)
|
171
|
+
key = [block.source_location.hash, vary.hash, opts.hash]
|
172
|
+
|
173
|
+
if (cached = Rubyoshka.cache[key])
|
174
|
+
@buffer << cached
|
175
|
+
return
|
176
|
+
end
|
177
|
+
|
178
|
+
cache_pos = @buffer.length
|
179
|
+
instance_eval(&block)
|
180
|
+
diff = @buffer[cache_pos..-1]
|
181
|
+
Rubyoshka.cache[key] = diff
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
class HTMLRenderer < Renderer
|
186
|
+
include HTML
|
187
|
+
|
188
|
+
def escape_text(text)
|
189
|
+
EscapeUtils.escape_html(text.to_s)
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class XMLRenderer < Renderer
|
194
|
+
def escape_text(text)
|
195
|
+
EscapeUtils.escape_xml(text.to_s)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
data/lib/rubyoshka/version.rb
CHANGED
metadata
CHANGED
@@ -1,29 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubyoshka
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.6.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-03-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
|
-
- !ruby/object:Gem::Dependency
|
14
|
-
name: modulation
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - '='
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '0.18'
|
20
|
-
type: :runtime
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - '='
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '0.18'
|
27
13
|
- !ruby/object:Gem::Dependency
|
28
14
|
name: escape_utils
|
29
15
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +66,20 @@ dependencies:
|
|
80
66
|
- - '='
|
81
67
|
- !ruby/object:Gem::Version
|
82
68
|
version: 2.7.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: tilt
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - '='
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 2.0.9
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - '='
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 2.0.9
|
83
83
|
description:
|
84
84
|
email: ciconia@gmail.com
|
85
85
|
executables: []
|
@@ -90,6 +90,8 @@ files:
|
|
90
90
|
- CHANGELOG.md
|
91
91
|
- README.md
|
92
92
|
- lib/rubyoshka.rb
|
93
|
+
- lib/rubyoshka/html.rb
|
94
|
+
- lib/rubyoshka/renderer.rb
|
93
95
|
- lib/rubyoshka/version.rb
|
94
96
|
homepage: http://github.com/digital-fabric/rubyoshka
|
95
97
|
licenses:
|
@@ -108,14 +110,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
110
|
requirements:
|
109
111
|
- - ">="
|
110
112
|
- !ruby/object:Gem::Version
|
111
|
-
version: '
|
113
|
+
version: '2.7'
|
112
114
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
115
|
requirements:
|
114
116
|
- - ">="
|
115
117
|
- !ruby/object:Gem::Version
|
116
118
|
version: '0'
|
117
119
|
requirements: []
|
118
|
-
rubygems_version: 3.
|
120
|
+
rubygems_version: 3.1.4
|
119
121
|
signing_key:
|
120
122
|
specification_version: 4
|
121
123
|
summary: 'Rubyoshka: composable HTML templating for Ruby'
|