rubyoshka 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +244 -20
- data/lib/rubyoshka/version.rb +1 -1
- data/lib/rubyoshka.rb +32 -10
- metadata +3 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff842a3a48141bcef19e1298cb26ff59cd9dbb0a58075efe50b3f73892a75c2f
|
4
|
+
data.tar.gz: 4556f2fc468d48d9a98b4582ab25f0068d814d933e0e0c198d45fce9f692960b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 79e2fd4e9f970201191e576f1b4bd971bef99cd1a7fbcbaee2102d6be8b8e0ec59e6ad0d26f6770ba756bd2e7b92ab84128a6bded62219417af94e1f9dbce159
|
7
|
+
data.tar.gz: 9d78ee6402ddc3e873d7b47073c655cb6501c46f91e8f4f6bb2041581367ee7cc5c661cc63e6f356e17a8d9cab59f9cf719e4497166f3fa890b4b320c62473c0
|
data/CHANGELOG.md
CHANGED
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](reference)
|
6
7
|
|
7
8
|
## What is Rubyoshka
|
8
9
|
|
@@ -11,14 +12,20 @@ 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
19
|
- Access to a global context from anywhere in the component hierarchy
|
17
|
-
-
|
20
|
+
- About 4 times faster than ERubis (see [benchmark](examples/perf.rb)).
|
21
|
+
|
22
|
+
> **Note** Rubyoshka is a new library and as such may be missing features and
|
23
|
+
> contain bugs. Your issue reports and code conributions 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 Matryoshka, the Russian
|
26
|
+
component containing any number of nested components, in a somewhat similar
|
27
|
+
fashion to React. The name *Rubyoshka* is a nod to Matryoshka, the Russian
|
28
|
+
nesting doll.
|
22
29
|
|
23
30
|
## Installing Rubyoshka
|
24
31
|
|
@@ -49,7 +56,9 @@ html = H {
|
|
49
56
|
}
|
50
57
|
```
|
51
58
|
|
52
|
-
|
59
|
+
## Rendering a template
|
60
|
+
|
61
|
+
To render a Rubyoshka template use the `#render` method:
|
53
62
|
|
54
63
|
```ruby
|
55
64
|
H { span 'best span' }.render #=> "<span>best span</span>"
|
@@ -102,14 +111,121 @@ H { img src: '/my.gif' }.render #=> "<img src="/my.gif"/>
|
|
102
111
|
H { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</p>"
|
103
112
|
```
|
104
113
|
|
114
|
+
## Logic in templates
|
115
|
+
|
116
|
+
Since Rubyoshka templates are just a bunch of Ruby, you can easily write your
|
117
|
+
view logic right in the template:
|
118
|
+
|
119
|
+
```ruby
|
120
|
+
def user_message(user)
|
121
|
+
H {
|
122
|
+
if user
|
123
|
+
span "Hello, #{user.name}!"
|
124
|
+
else
|
125
|
+
span "Hello, guest!"
|
126
|
+
end
|
127
|
+
}
|
128
|
+
end
|
129
|
+
```
|
130
|
+
|
131
|
+
## Local context
|
132
|
+
|
133
|
+
When writing logic and referring to application values in you templates, there
|
134
|
+
are some ground rules to obey. Since the template code is evaluated using
|
135
|
+
`#instance_eval` that means that you will not be able to directly use instance
|
136
|
+
variables or do unqualified method calls (calls to `self`).
|
137
|
+
|
138
|
+
In order to facilitate exposing values to your template logic, Rubyoshka
|
139
|
+
provides an API for setting a local context. The local context is simply a set
|
140
|
+
of values that are accessible for a given block of code, and to any nested
|
141
|
+
blocks within it. The local context is primarily set using the `#with` method:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
H {
|
145
|
+
with(name: 'world') {
|
146
|
+
div {
|
147
|
+
span "Hello, #{name}"
|
148
|
+
}
|
149
|
+
}
|
150
|
+
}
|
151
|
+
```
|
152
|
+
|
153
|
+
The local context can alternatively be set by passing hash values to `#H`:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
H(name: 'world') {
|
157
|
+
div { span "Hello, #{name}" }
|
158
|
+
}
|
159
|
+
```
|
160
|
+
|
161
|
+
A local context can also be set for a component (see the next section) simply by
|
162
|
+
passing arguments to the component call:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
Greeting = H { span "Hello, #{name}" }
|
166
|
+
|
167
|
+
H {
|
168
|
+
div {
|
169
|
+
Greeting(name: 'world')
|
170
|
+
}
|
171
|
+
}
|
172
|
+
```
|
173
|
+
|
174
|
+
### Tip: accessing `self` and instance variables from a template
|
175
|
+
|
176
|
+
In order to be able to access the object in the context of which the template is
|
177
|
+
defined or any of its methods, you can pass it in the local context:
|
178
|
+
|
179
|
+
```ruby
|
180
|
+
class User
|
181
|
+
...
|
182
|
+
def greeting_template
|
183
|
+
H(user: self) {
|
184
|
+
...
|
185
|
+
span "Hello, #{user.name}"
|
186
|
+
span "your email: #{user.email}"
|
187
|
+
}
|
188
|
+
end
|
189
|
+
end
|
190
|
+
```
|
191
|
+
|
192
|
+
Instance variables can be passed to the template in a similar fashion:
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
H(name: @name) { span "Hello, #{name}" }
|
196
|
+
```
|
197
|
+
|
198
|
+
## Global context
|
199
|
+
|
200
|
+
In addition to the local context, Rubyoshka also provides a way to set a global
|
201
|
+
context, accessible from anywhere in the template, and also in sub-components
|
202
|
+
used in the template.
|
203
|
+
|
204
|
+
The global context is a simple hash that can be accessed from within the
|
205
|
+
template with the `#context` method:
|
206
|
+
|
207
|
+
```ruby
|
208
|
+
greeting = H { span "Hello, #{context[:name]}" }
|
209
|
+
```
|
210
|
+
|
211
|
+
The global context can be set upon rendering the template:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
greeting.render(name: 'world')
|
215
|
+
```
|
216
|
+
|
105
217
|
## Templates as components
|
106
218
|
|
107
|
-
Rubyoshka makes it easy to compose multiple templates into a whole HTML
|
219
|
+
Rubyoshka makes it easy to compose multiple separate templates into a whole HTML
|
108
220
|
document. Each template can be defined as a self-contained component that can
|
109
221
|
be reused inside other components. Components should be defined as constants,
|
110
|
-
either in the global namespace, or on the Rubyoshka namespace
|
222
|
+
either in the global namespace, or on the `Rubyoshka` namespace. Each component
|
223
|
+
can be defined as either a Rubyoshka instance (using `#H`) or as a `proc` that
|
224
|
+
returns a Rubyoshka instance:
|
111
225
|
|
112
226
|
```ruby
|
227
|
+
Title = H { h1 title }
|
228
|
+
|
113
229
|
# Item is actually a Proc that returns a template
|
114
230
|
Item = ->(id:, text:, checked:) {
|
115
231
|
H {
|
@@ -122,6 +238,7 @@ Item = ->(id:, text:, checked:) {
|
|
122
238
|
|
123
239
|
def render_items(items)
|
124
240
|
html = H {
|
241
|
+
Title()
|
125
242
|
ul {
|
126
243
|
items.each { |id, attributes|
|
127
244
|
Item id: id, text: attributes[:text], checked: attributes[:active]
|
@@ -131,26 +248,45 @@ def render_items(items)
|
|
131
248
|
end
|
132
249
|
```
|
133
250
|
|
134
|
-
|
251
|
+
Note that a component is invoked as a method, which means that if no arguments
|
252
|
+
are passed, you should add an empty pair of parens, as shown in the example
|
253
|
+
above.
|
135
254
|
|
136
|
-
|
137
|
-
|
255
|
+
In addition to using components defined as constants, you can also use
|
256
|
+
non-constant components by invoking the `#emit` method:
|
257
|
+
|
258
|
+
```ruby
|
259
|
+
greeting = H { span "Hello, world" }
|
260
|
+
|
261
|
+
H {
|
262
|
+
div {
|
263
|
+
emit greeting
|
264
|
+
}
|
265
|
+
}
|
266
|
+
```
|
267
|
+
|
268
|
+
## Wrapping arbitrary HTML with a component
|
269
|
+
|
270
|
+
Components can also be used to wrap arbitrary HTML with addional markup. This is
|
271
|
+
done by implementing the component as a `proc` that takes a block:
|
138
272
|
|
139
273
|
```ruby
|
140
274
|
Header = ->(&inner_html) {
|
141
275
|
header {
|
142
|
-
h1 'title'
|
276
|
+
h1 'This is a title'
|
143
277
|
emit inner_html
|
144
278
|
}
|
145
279
|
}
|
146
280
|
|
147
|
-
H {
|
281
|
+
Greeting = H { span "Hello, #{name}" }
|
282
|
+
|
283
|
+
H { Header { Greeting(name: 'world') }.render #=> "<header><h1>This is a title</h1><span>Hello, world</span></header>"
|
148
284
|
```
|
149
285
|
|
150
286
|
## Some interesting use cases
|
151
287
|
|
152
288
|
Rubyoshka opens up all kinds of new possibilities when it comes to putting
|
153
|
-
together pieces of HTML. Feel free to explore the
|
289
|
+
together pieces of HTML. Feel free to explore the API!
|
154
290
|
|
155
291
|
### Routing in the view
|
156
292
|
|
@@ -163,12 +299,7 @@ Router = ->(path) {
|
|
163
299
|
when '/'
|
164
300
|
PostIndex()
|
165
301
|
when /^posts\/(.+)$/
|
166
|
-
|
167
|
-
if post
|
168
|
-
Post(post)
|
169
|
-
else
|
170
|
-
ErrorPage(404)
|
171
|
-
end
|
302
|
+
Post(get_post($1))
|
172
303
|
end
|
173
304
|
}
|
174
305
|
|
@@ -184,4 +315,97 @@ Blog = H {
|
|
184
315
|
}
|
185
316
|
}
|
186
317
|
}
|
187
|
-
```
|
318
|
+
```
|
319
|
+
|
320
|
+
### A general purpose router
|
321
|
+
|
322
|
+
A more flexible, reusable approach could be achieved by implementing a
|
323
|
+
higher-order routing component, in a similar fashion to
|
324
|
+
[React Router](https://reacttraining.com/react-router/web/guides/quick-start):
|
325
|
+
|
326
|
+
```ruby
|
327
|
+
Route = ->(path, &block) {
|
328
|
+
match = path.is_a?(Regexp) ?
|
329
|
+
context[:path] =~ path : context[:path] == /^#{path}/
|
330
|
+
emit block if match
|
331
|
+
}
|
332
|
+
|
333
|
+
Blog = H {
|
334
|
+
html {
|
335
|
+
head {
|
336
|
+
title: 'My blog'
|
337
|
+
}
|
338
|
+
body {
|
339
|
+
Topbar()
|
340
|
+
Sidebar()
|
341
|
+
div id: 'content' {
|
342
|
+
Route '/' { PostIndex() }
|
343
|
+
Route /^posts\/(.+)$/ { Post(get_post($1)) }
|
344
|
+
}
|
345
|
+
}
|
346
|
+
}
|
347
|
+
}
|
348
|
+
```
|
349
|
+
|
350
|
+
## Reference
|
351
|
+
|
352
|
+
#### `Rubyoshka#initialize(**context, &block)` a.k.a. `Kernel#H`
|
353
|
+
|
354
|
+
- `context`: local context hash
|
355
|
+
- `block`: template block
|
356
|
+
|
357
|
+
Initializes a new Rubyoshka instance. This method takes a block of template
|
358
|
+
code, and an optional [local context](#local-context) in the form of a hash.
|
359
|
+
The `Kernel#H` method serves as a shortcut for creating Rubyoshka instances.
|
360
|
+
|
361
|
+
#### `Rubyoshka#render(**context)`
|
362
|
+
|
363
|
+
- `context`: global context hash
|
364
|
+
|
365
|
+
Renders the template with an optional [global context](#global-context)
|
366
|
+
hash.
|
367
|
+
|
368
|
+
#### Methods accessible inside template blocks
|
369
|
+
|
370
|
+
#### `#<tag/component>(*args, **props, &block)`
|
371
|
+
|
372
|
+
- `args`: tag arguments. For an HTML tag Rubyoshka expects a single `String`
|
373
|
+
argument containing the inner text of the tag.
|
374
|
+
- `props`: hash of tag attributes
|
375
|
+
- `block`: inner HTML block
|
376
|
+
|
377
|
+
Adds a tag or component to the current template. If the method name starts with
|
378
|
+
an upper-case letter, it is considered a [component](#templates-as-components).
|
379
|
+
|
380
|
+
If a text argument is given for a tag, it will be escaped.
|
381
|
+
|
382
|
+
#### `#context`
|
383
|
+
|
384
|
+
Accesses the [global context](#global-context).
|
385
|
+
|
386
|
+
#### `#emit(object)` a.k.a. `#e(object)`
|
387
|
+
|
388
|
+
- `object`: `Proc`, `Rubyoshka` instance or `String`
|
389
|
+
|
390
|
+
Adds the given object to the current template. If a `String` is given, it is
|
391
|
+
rendered verbatim, i.e. without escaping.
|
392
|
+
|
393
|
+
#### `html5(&block)`
|
394
|
+
|
395
|
+
- `block`: inner HTML block
|
396
|
+
|
397
|
+
Adds an HTML5 `doctype` tag, followed by an `html` tag with the given block.
|
398
|
+
|
399
|
+
#### `#text(data)`
|
400
|
+
|
401
|
+
- `data` - text to add
|
402
|
+
|
403
|
+
Adds text without wrapping it in a tag. The text will be escaped.
|
404
|
+
|
405
|
+
#### `#with(**context, &block)`
|
406
|
+
|
407
|
+
- `context`: local context hash
|
408
|
+
- `block`: HTML block
|
409
|
+
|
410
|
+
Sets a [local context](#local-context) for use inside the given block. The
|
411
|
+
previous local context will be restored upon exiting the given block.
|
data/lib/rubyoshka/version.rb
CHANGED
data/lib/rubyoshka.rb
CHANGED
@@ -41,16 +41,22 @@ class Rubyoshka
|
|
41
41
|
# @param block [Proc] block passed to method call
|
42
42
|
# @return [void]
|
43
43
|
def method_missing(sym, *args, &block)
|
44
|
+
value = @local && @local[sym]
|
45
|
+
return value if value
|
46
|
+
|
44
47
|
if sym =~ R_CONST_SYM
|
45
48
|
o = instance_eval(sym.to_s) rescue Rubyoshka.const_get(sym) \
|
46
49
|
rescue Object.const_get(sym)
|
47
50
|
case o
|
48
51
|
when ::Proc
|
49
|
-
self.class.define_method(sym) { |*a, &b| emit
|
50
|
-
emit
|
52
|
+
self.class.define_method(sym) { |*a, &b| emit(o.(*a, &b)) }
|
53
|
+
emit(o.(*args, &block))
|
51
54
|
when Rubyoshka
|
52
|
-
self.class.define_method(sym) {
|
53
|
-
|
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)
|
54
60
|
when ::String
|
55
61
|
@buffer << o
|
56
62
|
else
|
@@ -73,6 +79,7 @@ class Rubyoshka
|
|
73
79
|
instance_eval(&o)
|
74
80
|
when Rubyoshka
|
75
81
|
instance_eval(&o.block)
|
82
|
+
when nil
|
76
83
|
else
|
77
84
|
@buffer << o.to_s
|
78
85
|
end
|
@@ -160,30 +167,45 @@ class Rubyoshka
|
|
160
167
|
# Emits text into the rendering buffer
|
161
168
|
# @param data [String] text
|
162
169
|
def text(data)
|
163
|
-
@buffer << E.escape_html(
|
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
|
164
182
|
end
|
165
183
|
end
|
166
184
|
|
167
185
|
attr_reader :block
|
168
186
|
|
169
187
|
# Initializes a Rubyoshka with the given block
|
188
|
+
# @param ctx [Hash] local context
|
170
189
|
# @param block [Proc] nested HTML block
|
171
190
|
# @param [void]
|
172
|
-
def initialize(&block)
|
173
|
-
@block = block
|
191
|
+
def initialize(**ctx, &block)
|
192
|
+
@block = ctx.empty? ? block : proc { with(ctx, &block) }
|
174
193
|
end
|
175
194
|
|
176
195
|
# Renders the associated block and returns the string result
|
177
196
|
# @param context [Hash] context
|
178
197
|
# @return [String]
|
179
|
-
def render(context
|
198
|
+
def render(**context)
|
180
199
|
Rendering.new(context, &block).to_s
|
181
200
|
end
|
182
201
|
end
|
183
202
|
|
184
203
|
module ::Kernel
|
185
204
|
# Convenience method for creating a new Rubyoshka
|
186
|
-
|
187
|
-
|
205
|
+
# @param ctx [Hash] local context
|
206
|
+
# @param block [Proc] nested block
|
207
|
+
# @return [Rubyoshka] Rubyoshka template
|
208
|
+
def H(**ctx, &block)
|
209
|
+
Rubyoshka.new(ctx, &block)
|
188
210
|
end
|
189
211
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubyoshka
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-01-
|
11
|
+
date: 2019-01-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: modulation
|
@@ -115,8 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
115
115
|
- !ruby/object:Gem::Version
|
116
116
|
version: '0'
|
117
117
|
requirements: []
|
118
|
-
|
119
|
-
rubygems_version: 2.7.3
|
118
|
+
rubygems_version: 3.0.1
|
120
119
|
signing_key:
|
121
120
|
specification_version: 4
|
122
121
|
summary: 'Rubyoshka: composable HTML templating for Ruby'
|