rubyoshka 0.1 → 0.6
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 +32 -0
- data/README.md +386 -26
- data/lib/rubyoshka.rb +40 -165
- data/lib/rubyoshka/html.rb +38 -0
- data/lib/rubyoshka/renderer.rb +197 -0
- data/lib/rubyoshka/version.rb +1 -1
- metadata +19 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ba3dd09fc4ab56c9af5a69c47d9da50949180b0c636c788ed74660bbc04b4b5d
|
4
|
+
data.tar.gz: faf523787f5ff11bb3e0d5346ad684b8e3c22c8fdf522e30ff0f43ff4da40a74
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e10f0611cc2596ac27dd3f93df91db2d77e6ba12bc3159cdb3f29ab1b6abd3caedc66161e14a18b776cba72164a7dad725c1d60e8b4a09bcc39f96a34fb1790f
|
7
|
+
data.tar.gz: ad0b35a97f0d76fa7c82fd456751004f75af759b9d98efc824f3ae99d6fdb5d3a6905aecbb083dc5f44d272cd828c6728e27330ceab5355f1d25c24a7090f3fc
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,35 @@
|
|
1
|
+
0.6 2021-03-03
|
2
|
+
--------------
|
3
|
+
|
4
|
+
* Fix Rubyoshka on Ruby 3.0
|
5
|
+
* Refactor and add more tests
|
6
|
+
|
7
|
+
0.5 2021-02-27
|
8
|
+
--------------
|
9
|
+
|
10
|
+
* Add support for rendering XML
|
11
|
+
* Add Rubyoshka.component method
|
12
|
+
* Remove Modulation dependency
|
13
|
+
|
14
|
+
0.4 2019-02-05
|
15
|
+
--------------
|
16
|
+
|
17
|
+
* Add support for emitting component modules
|
18
|
+
|
19
|
+
0.3 2019-01-13
|
20
|
+
--------------
|
21
|
+
|
22
|
+
* Implement caching
|
23
|
+
* Improve performance
|
24
|
+
* Handle attributes with `false` value correctly
|
25
|
+
|
26
|
+
0.2 2019-01-07
|
27
|
+
--------------
|
28
|
+
|
29
|
+
* Better documentation
|
30
|
+
* Fix #text
|
31
|
+
* Add local context
|
32
|
+
|
1
33
|
0.1 2019-01-06
|
2
34
|
--------------
|
3
35
|
|
data/README.md
CHANGED
@@ -2,7 +2,8 @@
|
|
2
2
|
|
3
3
|
[INSTALL](#installing-rubyoshka) |
|
4
4
|
[TUTORIAL](#getting-started) |
|
5
|
-
[EXAMPLES](examples)
|
5
|
+
[EXAMPLES](examples) |
|
6
|
+
[REFERENCE](#api-reference)
|
6
7
|
|
7
8
|
## What is Rubyoshka
|
8
9
|
|
@@ -11,19 +12,34 @@ features:
|
|
11
12
|
|
12
13
|
- HTML templating using plain Ruby syntax
|
13
14
|
- Minimal boilerplate
|
15
|
+
- Mix logic and tags freely
|
16
|
+
- Use global and local contexts to pass values to reusable components
|
14
17
|
- Automatic HTML escaping
|
15
18
|
- Composable nested components
|
16
|
-
-
|
17
|
-
|
19
|
+
- Template caching from fragments to whole templates
|
20
|
+
|
21
|
+
> **Note** Rubyoshka is a new library and as such may be missing features and
|
22
|
+
> contain bugs. Also, its API may change unexpectedly. Your issue reports and
|
23
|
+
> code contributions are most welcome!
|
18
24
|
|
19
25
|
With Rubyoshka you can structure your templates like a Russian doll, each
|
20
|
-
component containing any number of nested components, in a similar
|
21
|
-
React. The name *Rubyoshka* is a nod to
|
26
|
+
component containing any number of nested components, in a somewhat similar
|
27
|
+
fashion to React. The name *Rubyoshka* is a nod to
|
28
|
+
[Matryoshka](https://en.wikipedia.org/wiki/Matryoshka_doll), the Russian
|
29
|
+
nesting doll.
|
22
30
|
|
23
31
|
## Installing Rubyoshka
|
24
32
|
|
33
|
+
Using bundler:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
gem 'rubyoshka'
|
37
|
+
```
|
38
|
+
|
39
|
+
Or manually:
|
40
|
+
|
25
41
|
```bash
|
26
|
-
$ gem install
|
42
|
+
$ gem install rubyoshka
|
27
43
|
```
|
28
44
|
|
29
45
|
## Getting started
|
@@ -49,7 +65,9 @@ html = H {
|
|
49
65
|
}
|
50
66
|
```
|
51
67
|
|
52
|
-
|
68
|
+
## Rendering a template
|
69
|
+
|
70
|
+
To render a Rubyoshka template use the `#render` method:
|
53
71
|
|
54
72
|
```ruby
|
55
73
|
H { span 'best span' }.render #=> "<span>best span</span>"
|
@@ -102,15 +120,123 @@ H { img src: '/my.gif' }.render #=> "<img src="/my.gif"/>
|
|
102
120
|
H { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</p>"
|
103
121
|
```
|
104
122
|
|
123
|
+
## Logic in templates
|
124
|
+
|
125
|
+
Since Rubyoshka templates are just a bunch of Ruby, you can easily write your
|
126
|
+
view logic right in the template:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
def user_message(user)
|
130
|
+
H {
|
131
|
+
if user
|
132
|
+
span "Hello, #{user.name}!"
|
133
|
+
else
|
134
|
+
span "Hello, guest!"
|
135
|
+
end
|
136
|
+
}
|
137
|
+
end
|
138
|
+
```
|
139
|
+
|
140
|
+
## Local context
|
141
|
+
|
142
|
+
When writing logic and referring to application values in you templates, there
|
143
|
+
are some ground rules to obey. Since the template code is evaluated using
|
144
|
+
`#instance_eval` that means that you will not be able to directly use instance
|
145
|
+
variables or do unqualified method calls (calls to `self`).
|
146
|
+
|
147
|
+
In order to facilitate exposing values to your template logic, Rubyoshka
|
148
|
+
provides an API for setting a local context. The local context is simply a set
|
149
|
+
of values that are accessible for a given block of code, and to any nested
|
150
|
+
blocks within it. The local context is primarily set using the `#with` method:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
H {
|
154
|
+
with(name: 'world') {
|
155
|
+
div {
|
156
|
+
span "Hello, #{name}"
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
```
|
161
|
+
|
162
|
+
The local context can alternatively be set by passing hash values to `#H`:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
H(name: 'world') {
|
166
|
+
div { span "Hello, #{name}" }
|
167
|
+
}
|
168
|
+
```
|
169
|
+
|
170
|
+
A local context can also be set for a component (see the next section) simply by
|
171
|
+
passing arguments to the component call:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
Greeting = H { span "Hello, #{name}" }
|
175
|
+
|
176
|
+
H {
|
177
|
+
div {
|
178
|
+
Greeting(name: 'world')
|
179
|
+
}
|
180
|
+
}
|
181
|
+
```
|
182
|
+
|
183
|
+
### Tip: accessing `self` and instance variables from a template
|
184
|
+
|
185
|
+
In order to be able to access the object in the context of which the template is
|
186
|
+
defined or any of its methods, you can pass it in the local context:
|
187
|
+
|
188
|
+
```ruby
|
189
|
+
class User
|
190
|
+
...
|
191
|
+
def greeting_template
|
192
|
+
H(user: self) {
|
193
|
+
...
|
194
|
+
span "Hello, #{user.name}"
|
195
|
+
span "your email: #{user.email}"
|
196
|
+
}
|
197
|
+
end
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
Instance variables can be passed to the template in a similar fashion:
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
H(name: @name) { span "Hello, #{name}" }
|
205
|
+
```
|
206
|
+
|
207
|
+
## Global context
|
208
|
+
|
209
|
+
In addition to the local context, Rubyoshka also provides a way to set a global
|
210
|
+
context, accessible from anywhere in the template, and also in sub-components
|
211
|
+
used in the template.
|
212
|
+
|
213
|
+
The global context is a simple hash that can be accessed from within the
|
214
|
+
template with the `#context` method:
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
greeting = H { span "Hello, #{context[:name]}" }
|
218
|
+
```
|
219
|
+
|
220
|
+
The global context can be set upon rendering the template:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
greeting.render(name: 'world')
|
224
|
+
```
|
225
|
+
|
105
226
|
## Templates as components
|
106
227
|
|
107
|
-
Rubyoshka makes it easy to compose multiple templates into a whole HTML
|
108
|
-
document. Each template can be defined as a self-contained component that can
|
109
|
-
|
110
|
-
|
228
|
+
Rubyoshka makes it easy to compose multiple separate templates into a whole HTML
|
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`:
|
111
233
|
|
112
234
|
```ruby
|
113
|
-
#
|
235
|
+
# Simple component relying on global/local context
|
236
|
+
Title = H { h1 title }
|
237
|
+
|
238
|
+
# Proc component that returns a template
|
239
|
+
# Notice how the lambda expression takes keyword arguments
|
114
240
|
Item = ->(id:, text:, checked:) {
|
115
241
|
H {
|
116
242
|
li {
|
@@ -120,37 +246,114 @@ Item = ->(id:, text:, checked:) {
|
|
120
246
|
}
|
121
247
|
}
|
122
248
|
|
123
|
-
|
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)
|
124
258
|
html = H {
|
259
|
+
Title()
|
260
|
+
NavBar(links)
|
125
261
|
ul {
|
126
262
|
items.each { |id, attributes|
|
127
263
|
Item id: id, text: attributes[:text], checked: attributes[:active]
|
128
264
|
}
|
129
265
|
}
|
130
|
-
}.render
|
266
|
+
}.render(title: 'Hello from components')
|
131
267
|
end
|
132
268
|
```
|
133
269
|
|
134
|
-
|
270
|
+
Note that a component is invoked as a method, which means that if no arguments
|
271
|
+
are passed, you must add an empty pair of parens, as shown in the example
|
272
|
+
above.
|
273
|
+
|
274
|
+
In addition to using components defined as constants, you can also use
|
275
|
+
non-constant components by invoking the `#emit` method:
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
greeting = H { span "Hello, world" }
|
279
|
+
|
280
|
+
H {
|
281
|
+
div {
|
282
|
+
emit greeting
|
283
|
+
}
|
284
|
+
}
|
285
|
+
```
|
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)).
|
135
334
|
|
136
|
-
|
137
|
-
|
335
|
+
## Wrapping arbitrary HTML with a component
|
336
|
+
|
337
|
+
Components can also be used to wrap arbitrary HTML with addional markup. This is
|
338
|
+
done by implementing the component as a `proc` that takes a block:
|
138
339
|
|
139
340
|
```ruby
|
140
341
|
Header = ->(&inner_html) {
|
141
342
|
header {
|
142
|
-
h1 'title'
|
343
|
+
h1 'This is a title'
|
143
344
|
emit inner_html
|
144
345
|
}
|
145
346
|
}
|
146
347
|
|
147
|
-
H {
|
348
|
+
Greeting = H { span "Hello, #{name}" }
|
349
|
+
|
350
|
+
H { Header { Greeting(name: 'world') }.render #=> "<header><h1>This is a title</h1><span>Hello, world</span></header>"
|
148
351
|
```
|
149
352
|
|
150
353
|
## Some interesting use cases
|
151
354
|
|
152
355
|
Rubyoshka opens up all kinds of new possibilities when it comes to putting
|
153
|
-
together pieces of HTML. Feel free to explore the
|
356
|
+
together pieces of HTML. Feel free to explore the API!
|
154
357
|
|
155
358
|
### Routing in the view
|
156
359
|
|
@@ -163,12 +366,7 @@ Router = ->(path) {
|
|
163
366
|
when '/'
|
164
367
|
PostIndex()
|
165
368
|
when /^posts\/(.+)$/
|
166
|
-
|
167
|
-
if post
|
168
|
-
Post(post)
|
169
|
-
else
|
170
|
-
ErrorPage(404)
|
171
|
-
end
|
369
|
+
Post(get_post($1))
|
172
370
|
end
|
173
371
|
}
|
174
372
|
|
@@ -184,4 +382,166 @@ Blog = H {
|
|
184
382
|
}
|
185
383
|
}
|
186
384
|
}
|
385
|
+
```
|
386
|
+
|
387
|
+
### A general purpose router
|
388
|
+
|
389
|
+
A more flexible, reusable approach could be achieved by implementing a
|
390
|
+
higher-order routing component, in a similar fashion to
|
391
|
+
[React Router](https://reacttraining.com/react-router/web/guides/quick-start):
|
392
|
+
|
393
|
+
```ruby
|
394
|
+
Route = ->(path, &block) {
|
395
|
+
match = path.is_a?(Regexp) ?
|
396
|
+
context[:path] =~ path : context[:path] == /^#{path}/
|
397
|
+
emit block if match
|
398
|
+
}
|
399
|
+
|
400
|
+
Blog = H {
|
401
|
+
html {
|
402
|
+
head {
|
403
|
+
title: 'My blog'
|
404
|
+
}
|
405
|
+
body {
|
406
|
+
Topbar()
|
407
|
+
Sidebar()
|
408
|
+
div id: 'content' {
|
409
|
+
Route '/' { PostIndex() }
|
410
|
+
Route /^posts\/(.+)$/ { Post(get_post($1)) }
|
411
|
+
}
|
412
|
+
}
|
413
|
+
}
|
414
|
+
}
|
415
|
+
```
|
416
|
+
|
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
|
448
|
+
|
449
|
+
#### `Rubyoshka#initialize(**context, &block)` a.k.a. `Kernel#H`
|
450
|
+
|
451
|
+
- `context`: local context hash
|
452
|
+
- `block`: template block
|
453
|
+
|
454
|
+
Initializes a new Rubyoshka instance. This method takes a block of template
|
455
|
+
code, and an optional [local context](#local-context) in the form of a hash.
|
456
|
+
The `Kernel#H` method serves as a shortcut for creating Rubyoshka instances.
|
457
|
+
|
458
|
+
#### `Rubyoshka#render(**context)`
|
459
|
+
|
460
|
+
- `context`: global context hash
|
461
|
+
|
462
|
+
Renders the template with an optional [global context](#global-context)
|
463
|
+
hash.
|
464
|
+
|
465
|
+
#### Methods accessible inside template blocks
|
466
|
+
|
467
|
+
#### `#<tag/component>(*args, **props, &block)`
|
468
|
+
|
469
|
+
- `args`: tag arguments. For an HTML tag Rubyoshka expects a single `String`
|
470
|
+
argument containing the inner text of the tag.
|
471
|
+
- `props`: hash of tag attributes
|
472
|
+
- `block`: inner HTML block
|
473
|
+
|
474
|
+
Adds a tag or component to the current template. If the method name starts with
|
475
|
+
an upper-case letter, it is considered a [component](#templates-as-components).
|
476
|
+
|
477
|
+
If a text argument is given for a tag, it will be escaped.
|
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
|
+
|
490
|
+
#### `#context`
|
491
|
+
|
492
|
+
Accesses the [global context](#global-context).
|
493
|
+
|
494
|
+
#### `#emit(object)` a.k.a. `#e(object)`
|
495
|
+
|
496
|
+
- `object`: `Proc`, `Rubyoshka` instance or `String`
|
497
|
+
|
498
|
+
Adds the given object to the current template. If a `String` is given, it is
|
499
|
+
rendered verbatim, i.e. without escaping.
|
500
|
+
|
501
|
+
#### `html5(&block)`
|
502
|
+
|
503
|
+
- `block`: inner HTML block
|
504
|
+
|
505
|
+
Adds an HTML5 `doctype` tag, followed by an `html` tag with the given block.
|
506
|
+
|
507
|
+
#### `#text(data)`
|
508
|
+
|
509
|
+
- `data` - text to add
|
510
|
+
|
511
|
+
Adds text without wrapping it in a tag. The text will be escaped.
|
512
|
+
|
513
|
+
#### `#with(**context, &block)`
|
514
|
+
|
515
|
+
- `context`: local context hash
|
516
|
+
- `block`: HTML block
|
517
|
+
|
518
|
+
Sets a [local context](#local-context) for use inside the given block. The
|
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
|
187
547
|
```
|
data/lib/rubyoshka.rb
CHANGED
@@ -1,189 +1,64 @@
|
|
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
|
-
if sym =~ R_CONST_SYM
|
45
|
-
o = instance_eval(sym.to_s) rescue Rubyoshka.const_get(sym) \
|
46
|
-
rescue Object.const_get(sym)
|
47
|
-
case o
|
48
|
-
when ::Proc
|
49
|
-
self.class.define_method(sym) { |*a, &b| emit o.(*a, &b) }
|
50
|
-
emit o.(*args, &block)
|
51
|
-
when Rubyoshka
|
52
|
-
self.class.define_method(sym) { emit o }
|
53
|
-
emit(o)
|
54
|
-
when ::String
|
55
|
-
@buffer << o
|
56
|
-
else
|
57
|
-
e = StandardError.new "Cannot render #{o.inspect}"
|
58
|
-
e.set_backtrace(caller)
|
59
|
-
raise e
|
60
|
-
end
|
61
|
-
else
|
62
|
-
self.class.class_eval(S_TAG_METHOD % sym)
|
63
|
-
tag(sym, *args, &block)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Emits the given object into the rendering buffer
|
68
|
-
# @param o [Proc, Rubyoshka, String] emitted object
|
69
|
-
# @return [void]
|
70
|
-
def emit(o)
|
71
|
-
case o
|
72
|
-
when ::Proc
|
73
|
-
instance_eval(&o)
|
74
|
-
when Rubyoshka
|
75
|
-
instance_eval(&o.block)
|
76
|
-
else
|
77
|
-
@buffer << o.to_s
|
78
|
-
end
|
79
|
-
end
|
80
|
-
alias_method :e, :emit
|
81
|
-
|
82
|
-
S_LT = '<'
|
83
|
-
S_GT = '>'
|
84
|
-
S_LT_SLASH = '</'
|
85
|
-
S_SPACE_LT_SLASH = ' </'
|
86
|
-
S_SLASH_GT = '/>'
|
87
|
-
S_SPACE = ' '
|
88
|
-
S_EQUAL_QUOTE = '="'
|
89
|
-
S_QUOTE = '"'
|
90
|
-
|
91
|
-
E = EscapeUtils
|
92
|
-
|
93
|
-
# Emits an HTML tag
|
94
|
-
# @param sym [Symbol] HTML tag
|
95
|
-
# @param text [String] text content of tag
|
96
|
-
# @param props [Hash] tag attributes
|
97
|
-
# @param block [Proc] nested HTML block
|
98
|
-
# @return [void]
|
99
|
-
def tag(sym, text = nil, **props, &block)
|
100
|
-
sym = sym.to_s
|
101
|
-
|
102
|
-
@buffer << S_LT << sym
|
103
|
-
emit_props(props) unless props.empty?
|
104
|
-
|
105
|
-
if block
|
106
|
-
@buffer << S_GT
|
107
|
-
instance_eval(&block)
|
108
|
-
@buffer << S_LT_SLASH << sym << S_GT
|
109
|
-
elsif Rubyoshka === text
|
110
|
-
@buffer << S_GT
|
111
|
-
emit(text)
|
112
|
-
@buffer << S_LT_SLASH << sym << S_GT
|
113
|
-
elsif text
|
114
|
-
@buffer << S_GT << E.escape_html(text.to_s) <<
|
115
|
-
S_LT_SLASH << sym << S_GT
|
116
|
-
else
|
117
|
-
@buffer << S_SLASH_GT
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
# Emits tag attributes into the rendering buffer
|
122
|
-
# @param props [Hash] tag attributes
|
123
|
-
# @return [void]
|
124
|
-
def emit_props(props)
|
125
|
-
props.each { |k, v|
|
126
|
-
case k
|
127
|
-
when :text
|
128
|
-
when :src, :href
|
129
|
-
@buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
|
130
|
-
E.escape_uri(v) << S_QUOTE
|
131
|
-
else
|
132
|
-
if v == true
|
133
|
-
@buffer << S_SPACE << k.to_s
|
134
|
-
else
|
135
|
-
@buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE << v << S_QUOTE
|
136
|
-
end
|
137
|
-
end
|
138
|
-
}
|
139
|
-
end
|
140
|
-
|
141
|
-
# Emits the p tag
|
142
|
-
# @param text [String] text content of tag
|
143
|
-
# @param props [Hash] tag attributes
|
144
|
-
# @para block [Proc] nested HTML block
|
145
|
-
# @return [void]
|
146
|
-
def p(text = nil, **props, &block)
|
147
|
-
tag(:p, text, **props, &block)
|
148
|
-
end
|
149
|
-
|
150
|
-
S_HTML5_DOCTYPE = '<!DOCTYPE html>'
|
151
|
-
|
152
|
-
# Emits an HTML5 doctype tag and an html tag with the given block
|
153
|
-
# @param block [Proc] nested HTML block
|
154
|
-
# @return [void]
|
155
|
-
def html5(&block)
|
156
|
-
@buffer << S_HTML5_DOCTYPE
|
157
|
-
self.html(&block)
|
158
|
-
end
|
159
|
-
|
160
|
-
# Emits text into the rendering buffer
|
161
|
-
# @param data [String] text
|
162
|
-
def text(data)
|
163
|
-
@buffer << E.escape_html(text)
|
164
|
-
end
|
165
|
-
end
|
166
|
-
|
167
9
|
attr_reader :block
|
168
10
|
|
169
11
|
# Initializes a Rubyoshka with the given block
|
12
|
+
# @param ctx [Hash] local context
|
170
13
|
# @param block [Proc] nested HTML block
|
171
14
|
# @param [void]
|
172
|
-
def initialize(&block)
|
173
|
-
@
|
15
|
+
def initialize(mode: :html, **ctx, &block)
|
16
|
+
@mode = mode
|
17
|
+
@block = ctx.empty? ? block : proc { with(**ctx, &block) }
|
174
18
|
end
|
175
19
|
|
20
|
+
H_EMPTY = {}.freeze
|
21
|
+
|
176
22
|
# Renders the associated block and returns the string result
|
177
23
|
# @param context [Hash] context
|
178
24
|
# @return [String]
|
179
|
-
def render(context =
|
180
|
-
|
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)
|
181
52
|
end
|
182
53
|
end
|
54
|
+
::H = Rubyoshka
|
183
55
|
|
184
56
|
module ::Kernel
|
185
57
|
# Convenience method for creating a new Rubyoshka
|
186
|
-
|
187
|
-
|
58
|
+
# @param ctx [Hash] local context
|
59
|
+
# @param block [Proc] nested block
|
60
|
+
# @return [Rubyoshka] Rubyoshka template
|
61
|
+
def H(**ctx, &block)
|
62
|
+
Rubyoshka.new(**ctx, &block)
|
188
63
|
end
|
189
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,197 @@
|
|
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
|
+
emit(o.(*args, **opts,&block))
|
78
|
+
when Rubyoshka
|
79
|
+
self.class.define_method(sym) do |**ctx|
|
80
|
+
ctx.empty? ? emit(o) : with(**ctx) { emit(o) }
|
81
|
+
end
|
82
|
+
Hash === opts.empty? ? emit(o) : with(**opts) { emit(o) }
|
83
|
+
when ::String
|
84
|
+
@buffer << o
|
85
|
+
else
|
86
|
+
e = StandardError.new "Cannot render #{o.inspect}"
|
87
|
+
e.set_backtrace(caller)
|
88
|
+
raise e
|
89
|
+
end
|
90
|
+
else
|
91
|
+
tag = sym.to_s
|
92
|
+
code = S_TAG_METHOD % { tag: tag, TAG: tag.upcase }
|
93
|
+
self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
|
94
|
+
send(sym, *args, **opts, &block)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# Emits the given object into the rendering buffer
|
99
|
+
# @param o [Proc, Rubyoshka, Module, String] emitted object
|
100
|
+
# @return [void]
|
101
|
+
def emit(o)
|
102
|
+
case o
|
103
|
+
when ::Proc
|
104
|
+
instance_eval(&o)
|
105
|
+
when Rubyoshka
|
106
|
+
instance_eval(&o.block)
|
107
|
+
when Module
|
108
|
+
# If module is given, the component is expected to be a const inside the module
|
109
|
+
emit(o::Component)
|
110
|
+
when nil
|
111
|
+
else
|
112
|
+
@buffer << o.to_s
|
113
|
+
end
|
114
|
+
end
|
115
|
+
alias_method :e, :emit
|
116
|
+
|
117
|
+
S_LT = '<'
|
118
|
+
S_GT = '>'
|
119
|
+
S_LT_SLASH = '</'
|
120
|
+
S_SPACE_LT_SLASH = ' </'
|
121
|
+
S_SLASH_GT = '/>'
|
122
|
+
S_SPACE = ' '
|
123
|
+
S_EQUAL_QUOTE = '="'
|
124
|
+
S_QUOTE = '"'
|
125
|
+
|
126
|
+
# Emits tag attributes into the rendering buffer
|
127
|
+
# @param props [Hash] tag attributes
|
128
|
+
# @return [void]
|
129
|
+
def emit_props(props)
|
130
|
+
props.each { |k, v|
|
131
|
+
case k
|
132
|
+
when :src, :href
|
133
|
+
@buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
|
134
|
+
EscapeUtils.escape_uri(v) << S_QUOTE
|
135
|
+
else
|
136
|
+
case v
|
137
|
+
when true
|
138
|
+
@buffer << S_SPACE << k.to_s
|
139
|
+
when false, nil
|
140
|
+
# emit nothing
|
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 text into the rendering buffer
|
149
|
+
# @param data [String] text
|
150
|
+
def text(data)
|
151
|
+
@buffer << escape_text(data)
|
152
|
+
end
|
153
|
+
|
154
|
+
# Sets a local context for the given block
|
155
|
+
# @param ctx [Hash] context hash
|
156
|
+
# @param block [Proc] nested HTML block
|
157
|
+
# @return [void]
|
158
|
+
def with(**ctx, &block)
|
159
|
+
old_local, @local = @local, ctx
|
160
|
+
instance_eval(&block)
|
161
|
+
ensure
|
162
|
+
@local = old_local
|
163
|
+
end
|
164
|
+
|
165
|
+
# Caches the given block with the given arguments as cache key
|
166
|
+
# @param vary [*Object] cache key
|
167
|
+
# @param block [Proc] nested HTML block
|
168
|
+
# @return [void]
|
169
|
+
def cache(*vary, **opts, &block)
|
170
|
+
key = [block.source_location.hash, vary.hash, opts.hash]
|
171
|
+
|
172
|
+
if (cached = Rubyoshka.cache[key])
|
173
|
+
@buffer << cached
|
174
|
+
return
|
175
|
+
end
|
176
|
+
|
177
|
+
cache_pos = @buffer.length
|
178
|
+
instance_eval(&block)
|
179
|
+
diff = @buffer[cache_pos..-1]
|
180
|
+
Rubyoshka.cache[key] = diff
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
class HTMLRenderer < Renderer
|
185
|
+
include HTML
|
186
|
+
|
187
|
+
def escape_text(text)
|
188
|
+
EscapeUtils.escape_html(text.to_s)
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class XMLRenderer < Renderer
|
193
|
+
def escape_text(text)
|
194
|
+
EscapeUtils.escape_xml(text.to_s)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
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: '0.
|
4
|
+
version: '0.6'
|
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:
|
@@ -115,8 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
117
|
- !ruby/object:Gem::Version
|
116
118
|
version: '0'
|
117
119
|
requirements: []
|
118
|
-
|
119
|
-
rubygems_version: 2.7.3
|
120
|
+
rubygems_version: 3.1.4
|
120
121
|
signing_key:
|
121
122
|
specification_version: 4
|
122
123
|
summary: 'Rubyoshka: composable HTML templating for Ruby'
|