rubyoshka 0.2 → 0.3

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: ff842a3a48141bcef19e1298cb26ff59cd9dbb0a58075efe50b3f73892a75c2f
4
- data.tar.gz: 4556f2fc468d48d9a98b4582ab25f0068d814d933e0e0c198d45fce9f692960b
3
+ metadata.gz: 3be682087384cd706b089c77e53334d5e60923106e3fa514bdb298c73aae64fc
4
+ data.tar.gz: 6efb14adab373708f4a171ed8b07b22b535a71fe05874d26e75f7147f3eefbc0
5
5
  SHA512:
6
- metadata.gz: 79e2fd4e9f970201191e576f1b4bd971bef99cd1a7fbcbaee2102d6be8b8e0ec59e6ad0d26f6770ba756bd2e7b92ab84128a6bded62219417af94e1f9dbce159
7
- data.tar.gz: 9d78ee6402ddc3e873d7b47073c655cb6501c46f91e8f4f6bb2041581367ee7cc5c661cc63e6f356e17a8d9cab59f9cf719e4497166f3fa890b4b320c62473c0
6
+ metadata.gz: '002368e44d062f4eeb02f8ef33a5eb09ead01b48895bc4fd1b899e88ab99da80ccc3c7a193c814d0b97ac0766245c39dbf1b2ccda96702c58090dc7106442ea5'
7
+ data.tar.gz: ea84ebb4e043e76ce51d6916d5b814116d6d5dc2ba2c781150e43c052d55c2ef611c8c2444316765a4f20e6798fb474452ab5623fbc221568949e0ca44317788
data/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ 0.3 2019-01-13
2
+ --------------
3
+
4
+ * Implement caching
5
+ * Improve performance
6
+ * Handle attributes with `false` value correctly
7
+
1
8
  0.2 2019-01-07
2
9
  --------------
3
10
 
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,19 +16,27 @@ 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
- - Access to a global context from anywhere in the component hierarchy
20
- - About 4 times faster than ERubis (see [benchmark](examples/perf.rb)).
21
19
 
22
20
  > **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!
21
+ > contain bugs. Also, its API may change unexpectedly. Your issue reports and
22
+ > code contributions are most welcome!
24
23
 
25
24
  With Rubyoshka you can structure your templates like a Russian doll, each
26
25
  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
26
+ fashion to React. The name *Rubyoshka* is a nod to
27
+ [Matryoshka](https://en.wikipedia.org/wiki/Matryoshka_doll), the Russian
28
28
  nesting doll.
29
29
 
30
30
  ## Installing Rubyoshka
31
31
 
32
+ Using bundler:
33
+
34
+ ```ruby
35
+ gem 'polyphony'
36
+ ```
37
+
38
+ Or manually:
39
+
32
40
  ```bash
33
41
  $ gem install polyphony
34
42
  ```
@@ -265,6 +273,54 @@ H {
265
273
  }
266
274
  ```
267
275
 
276
+ ## Fragment caching
277
+
278
+ Any part of a Rubyoshka template can be cached - a fragment, a component, or a
279
+ whole template. It is up to you, the user, to determine which parts of the
280
+ template to cache. By default, a call to `#cache` creates a cache entry based on
281
+ the source location of the cached block:
282
+
283
+ ```ruby
284
+ Head = H {
285
+ cache {
286
+ head {
287
+ title 'My app'
288
+ style "@import '/app.css';"
289
+ }
290
+ }
291
+ }
292
+ ```
293
+
294
+ However, if your template references local or global variables, you'll want to
295
+ take those into account when caching. This is done by passing any variables used
296
+ in the template to `#cache` in order to create separate cache entries for each
297
+ discrete value or combination of values:
298
+
299
+ ```ruby
300
+ Greeting = H {
301
+ cache(name) {
302
+ div {
303
+ span "Hello, #{name}"
304
+ }
305
+ }
306
+ }
307
+
308
+ names = %w{tommy dolly world}
309
+ App = H {
310
+ names.each { |n| Greeting(name: n) }
311
+ }
312
+ ```
313
+
314
+ In the above example a separate cache entry will be created for each name. The
315
+ use of caching in components is especially beneficial since components may be
316
+ reused in multiple different templates in your app.
317
+
318
+ ### Changing the cache store
319
+
320
+ Rubyoshka ships with a naïve in-memory cache store built-in. You can use
321
+ another cache store by overriding the `Rubyoshka.cache_get` and
322
+ `Rubyoshka.cache_set` methods (see API [reference](#api-reference)).
323
+
268
324
  ## Wrapping arbitrary HTML with a component
269
325
 
270
326
  Components can also be used to wrap arbitrary HTML with addional markup. This is
@@ -347,7 +403,37 @@ Blog = H {
347
403
  }
348
404
  ```
349
405
 
350
- ## Reference
406
+ ### A higher-order list component
407
+
408
+ Here's another demonstration of a higher-order component, a list component that
409
+ takes an item component as an argument. The `List` component can be reused for
410
+ rendering any kind of unordered list, and with any kind of item component:
411
+
412
+ ```ruby
413
+ List = ->(items, item_component) {
414
+ H {
415
+ ul {
416
+ items.each { |item|
417
+ with(item: item) {
418
+ li { emit item_component }
419
+ }
420
+ }
421
+ }
422
+ }
423
+ }
424
+
425
+ TodoItem = H {
426
+ span item.text, class: item.completed ? 'completed' : 'pending'
427
+ }
428
+
429
+ def todo_list(items)
430
+ H {
431
+ div { List(items, TodoItem) }
432
+ }
433
+ end
434
+ ```
435
+
436
+ ## API Reference
351
437
 
352
438
  #### `Rubyoshka#initialize(**context, &block)` a.k.a. `Kernel#H`
353
439
 
@@ -379,6 +465,17 @@ an upper-case letter, it is considered a [component](#templates-as-components).
379
465
 
380
466
  If a text argument is given for a tag, it will be escaped.
381
467
 
468
+ #### `#cache(*vary, &block)`
469
+
470
+ - `vary`: variables used in cached block. The given values will be used to
471
+ create a separate cache entry.
472
+ - `block`: inner HTML block
473
+
474
+ Caches the markup in the given block, storing it in the Rubyoshka cache store.
475
+ If a cache entry for the given block is found, it will be used instead of
476
+ invoking the block. If one or more variables given, those will be used to create
477
+ a separate cache entry.
478
+
382
479
  #### `#context`
383
480
 
384
481
  Accesses the [global context](#global-context).
@@ -409,3 +506,31 @@ Adds text without wrapping it in a tag. The text will be escaped.
409
506
 
410
507
  Sets a [local context](#local-context) for use inside the given block. The
411
508
  previous local context will be restored upon exiting the given block.
509
+
510
+ #### `Rubyoshka.cache`
511
+
512
+ Returns the cache store. A cache store should implement two methods - `#[]` and
513
+ `#[]=`. Here's an example implementing a Redis-based cache store:
514
+
515
+ ```ruby
516
+ class RedisTemplateCache
517
+ def initialize(conn, prefix)
518
+ @conn = conn
519
+ @prefix = prefix
520
+ end
521
+
522
+ def [](key)
523
+ @conn.get("#{prefix}:#{key}")
524
+ end
525
+
526
+ def []=(key, value)
527
+ @conn.set("#{prefix}:#{key}", value)
528
+ end
529
+ end
530
+
531
+ TEMPLATE_CACHE = RedisTemplaceCache.new(redis_conn, "templates:cache")
532
+
533
+ def Rubyoshka.cache
534
+ TEMPLATE_CACHE
535
+ end
536
+ ```
data/lib/rubyoshka.rb CHANGED
@@ -26,10 +26,30 @@ class Rubyoshka
26
26
  def to_s
27
27
  @buffer
28
28
  end
29
+
30
+ E = EscapeUtils
29
31
 
30
32
  S_TAG_METHOD = <<~EOF
31
- def %1$s(*args, &block)
32
- tag(:%1$s, *args, &block)
33
+ S_TAG_%<TAG>s_PRE = '<%<tag>s'
34
+ S_TAG_%<TAG>s_CLOSE = '</%<tag>s>'
35
+
36
+ def %<tag>s(text = nil, **props, &block)
37
+ @buffer << S_TAG_%<TAG>s_PRE
38
+ emit_props(props) unless props.empty?
39
+
40
+ if block
41
+ @buffer << S_GT
42
+ instance_eval(&block)
43
+ @buffer << S_TAG_%<TAG>s_CLOSE
44
+ elsif Rubyoshka === text
45
+ @buffer << S_GT
46
+ emit(text)
47
+ @buffer << S_TAG_%<TAG>s_CLOSE
48
+ elsif text
49
+ @buffer << S_GT << E.escape_html(text.to_s) << S_TAG_%<TAG>s_CLOSE
50
+ else
51
+ @buffer << S_SLASH_GT
52
+ end
33
53
  end
34
54
  EOF
35
55
 
@@ -65,8 +85,9 @@ class Rubyoshka
65
85
  raise e
66
86
  end
67
87
  else
68
- self.class.class_eval(S_TAG_METHOD % sym)
69
- tag(sym, *args, &block)
88
+ tag = sym.to_s
89
+ self.class.class_eval(S_TAG_METHOD % { tag: tag, TAG: tag.upcase })
90
+ send(sym, *args, &block)
70
91
  end
71
92
  end
72
93
 
@@ -95,49 +116,21 @@ class Rubyoshka
95
116
  S_EQUAL_QUOTE = '="'
96
117
  S_QUOTE = '"'
97
118
 
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
119
  # Emits tag attributes into the rendering buffer
129
120
  # @param props [Hash] tag attributes
130
121
  # @return [void]
131
122
  def emit_props(props)
132
123
  props.each { |k, v|
133
124
  case k
134
- when :text
135
125
  when :src, :href
136
126
  @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
137
127
  E.escape_uri(v) << S_QUOTE
138
128
  else
139
- if v == true
129
+ case v
130
+ when true
140
131
  @buffer << S_SPACE << k.to_s
132
+ when false, nil
133
+ # emit nothing
141
134
  else
142
135
  @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE << v << S_QUOTE
143
136
  end
@@ -151,7 +144,7 @@ class Rubyoshka
151
144
  # @para block [Proc] nested HTML block
152
145
  # @return [void]
153
146
  def p(text = nil, **props, &block)
154
- tag(:p, text, **props, &block)
147
+ method_missing(:p, text, **props, &block)
155
148
  end
156
149
 
157
150
  S_HTML5_DOCTYPE = '<!DOCTYPE html>'
@@ -180,6 +173,24 @@ class Rubyoshka
180
173
  ensure
181
174
  @local = old_local
182
175
  end
176
+
177
+ # Caches the given block with the given arguments as cache key
178
+ # @param vary [*Object] cache key
179
+ # @param block [Proc] nested HTML block
180
+ # @return [void]
181
+ def cache(*vary, &block)
182
+ key = [block.source_location, *vary]
183
+
184
+ if (cached = Rubyoshka.cache[key])
185
+ @buffer << cached
186
+ return
187
+ end
188
+
189
+ cache_pos = @buffer.length
190
+ instance_eval(&block)
191
+ diff = @buffer[cache_pos..-1]
192
+ Rubyoshka.cache[key] = diff
193
+ end
183
194
  end
184
195
 
185
196
  attr_reader :block
@@ -192,12 +203,20 @@ class Rubyoshka
192
203
  @block = ctx.empty? ? block : proc { with(ctx, &block) }
193
204
  end
194
205
 
206
+ H_EMPTY = {}.freeze
207
+
195
208
  # Renders the associated block and returns the string result
196
209
  # @param context [Hash] context
197
210
  # @return [String]
198
- def render(**context)
211
+ def render(context = H_EMPTY)
199
212
  Rendering.new(context, &block).to_s
200
213
  end
214
+
215
+ @@cache = {}
216
+
217
+ def self.cache
218
+ @@cache
219
+ end
201
220
  end
202
221
 
203
222
  module ::Kernel
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rubyoshka
4
- VERSION = '0.2'
4
+ VERSION = '0.3'
5
5
  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.2'
4
+ version: '0.3'
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-07 00:00:00.000000000 Z
11
+ date: 2019-01-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: modulation
@@ -80,6 +80,20 @@ dependencies:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
82
  version: 2.7.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: tilt
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - '='
88
+ - !ruby/object:Gem::Version
89
+ version: 2.0.9
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - '='
95
+ - !ruby/object:Gem::Version
96
+ version: 2.0.9
83
97
  description:
84
98
  email: ciconia@gmail.com
85
99
  executables: []