rubyoshka 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dcac0e7abbbb0bad4f11b3c3b500ed7c14f506d06c6aa8ecbcec4cfc3503ce16
4
- data.tar.gz: 8fd105613444edd272644aff2cb054b4c0a540789f5e63f41a4d9c033fc36577
3
+ metadata.gz: ff842a3a48141bcef19e1298cb26ff59cd9dbb0a58075efe50b3f73892a75c2f
4
+ data.tar.gz: 4556f2fc468d48d9a98b4582ab25f0068d814d933e0e0c198d45fce9f692960b
5
5
  SHA512:
6
- metadata.gz: 4f693a62532ae3235383068ed789cc144fd38ec7723c5ff728ace5b0a5b0ae267e61fc3e8140ac8972420a8c35fa0731b543dbd84016bda049843038ae5bafa4
7
- data.tar.gz: 3ca48bcd081c077635cffa99d36823e2f19c1ac832a26426ae510e233d1054880a0e74b3047aeef389dcb88b4905a95339e90145cd131efcd2a8edba23599dea
6
+ metadata.gz: 79e2fd4e9f970201191e576f1b4bd971bef99cd1a7fbcbaee2102d6be8b8e0ec59e6ad0d26f6770ba756bd2e7b92ab84128a6bded62219417af94e1f9dbce159
7
+ data.tar.gz: 9d78ee6402ddc3e873d7b47073c655cb6501c46f91e8f4f6bb2041581367ee7cc5c661cc63e6f356e17a8d9cab59f9cf719e4497166f3fa890b4b320c62473c0
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ 0.2 2019-01-07
2
+ --------------
3
+
4
+ * Better documentation
5
+ * Fix #text
6
+ * Add local context
7
+
1
8
  0.1 2019-01-06
2
9
  --------------
3
10
 
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
- - High performance
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 fashion to
21
- React. The name *Rubyoshka* is a nod to Matryoshka, the Russian nesting doll.
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
- To render the template use `render`:
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
- ## Wrapping arbitrary HTML
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
- Components can be used to wrap arbitrary HTML content by defining them as procs
137
- that accept blocks:
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 { Header { button 'OK'} }.render #=> "<header><h1>title</h1><button>OK</button></header>"
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 possibilities!
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
- post = get_post($1)
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.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rubyoshka
4
- VERSION = '0.1'
4
+ VERSION = '0.2'
5
5
  end
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 o.(*a, &b) }
50
- emit o.(*args, &block)
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) { emit o }
53
- emit(o)
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(text)
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
- def H(&block)
187
- Rubyoshka.new(&block)
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.1'
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-06 00:00:00.000000000 Z
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
- rubyforge_project:
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'