rubyoshka 0.2 → 0.3

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: 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: []