rubyoshka 0.1 → 0.2

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 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'