rubyoshka 0.1 → 0.6

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: ba3dd09fc4ab56c9af5a69c47d9da50949180b0c636c788ed74660bbc04b4b5d
4
+ data.tar.gz: faf523787f5ff11bb3e0d5346ad684b8e3c22c8fdf522e30ff0f43ff4da40a74
5
5
  SHA512:
6
- metadata.gz: 4f693a62532ae3235383068ed789cc144fd38ec7723c5ff728ace5b0a5b0ae267e61fc3e8140ac8972420a8c35fa0731b543dbd84016bda049843038ae5bafa4
7
- data.tar.gz: 3ca48bcd081c077635cffa99d36823e2f19c1ac832a26426ae510e233d1054880a0e74b3047aeef389dcb88b4905a95339e90145cd131efcd2a8edba23599dea
6
+ metadata.gz: e10f0611cc2596ac27dd3f93df91db2d77e6ba12bc3159cdb3f29ab1b6abd3caedc66161e14a18b776cba72164a7dad725c1d60e8b4a09bcc39f96a34fb1790f
7
+ data.tar.gz: ad0b35a97f0d76fa7c82fd456751004f75af759b9d98efc824f3ae99d6fdb5d3a6905aecbb083dc5f44d272cd828c6728e27330ceab5355f1d25c24a7090f3fc
data/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ 0.6 2021-03-03
2
+ --------------
3
+
4
+ * Fix Rubyoshka on Ruby 3.0
5
+ * Refactor and add more tests
6
+
7
+ 0.5 2021-02-27
8
+ --------------
9
+
10
+ * Add support for rendering XML
11
+ * Add Rubyoshka.component method
12
+ * Remove Modulation dependency
13
+
14
+ 0.4 2019-02-05
15
+ --------------
16
+
17
+ * Add support for emitting component modules
18
+
19
+ 0.3 2019-01-13
20
+ --------------
21
+
22
+ * Implement caching
23
+ * Improve performance
24
+ * Handle attributes with `false` value correctly
25
+
26
+ 0.2 2019-01-07
27
+ --------------
28
+
29
+ * Better documentation
30
+ * Fix #text
31
+ * Add local context
32
+
1
33
  0.1 2019-01-06
2
34
  --------------
3
35
 
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](#api-reference)
6
7
 
7
8
  ## What is Rubyoshka
8
9
 
@@ -11,19 +12,34 @@ 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
- - Access to a global context from anywhere in the component hierarchy
17
- - High performance
19
+ - Template caching from fragments to whole templates
20
+
21
+ > **Note** Rubyoshka is a new library and as such may be missing features and
22
+ > contain bugs. Also, its API may change unexpectedly. Your issue reports and
23
+ > code contributions 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
28
+ [Matryoshka](https://en.wikipedia.org/wiki/Matryoshka_doll), the Russian
29
+ nesting doll.
22
30
 
23
31
  ## Installing Rubyoshka
24
32
 
33
+ Using bundler:
34
+
35
+ ```ruby
36
+ gem 'rubyoshka'
37
+ ```
38
+
39
+ Or manually:
40
+
25
41
  ```bash
26
- $ gem install polyphony
42
+ $ gem install rubyoshka
27
43
  ```
28
44
 
29
45
  ## Getting started
@@ -49,7 +65,9 @@ html = H {
49
65
  }
50
66
  ```
51
67
 
52
- To render the template use `render`:
68
+ ## Rendering a template
69
+
70
+ To render a Rubyoshka template use the `#render` method:
53
71
 
54
72
  ```ruby
55
73
  H { span 'best span' }.render #=> "<span>best span</span>"
@@ -102,15 +120,123 @@ H { img src: '/my.gif' }.render #=> "<img src="/my.gif"/>
102
120
  H { p "foobar", class: 'important' }.render #=> "<p class=\"important\">foobar</p>"
103
121
  ```
104
122
 
123
+ ## Logic in templates
124
+
125
+ Since Rubyoshka templates are just a bunch of Ruby, you can easily write your
126
+ view logic right in the template:
127
+
128
+ ```ruby
129
+ def user_message(user)
130
+ H {
131
+ if user
132
+ span "Hello, #{user.name}!"
133
+ else
134
+ span "Hello, guest!"
135
+ end
136
+ }
137
+ end
138
+ ```
139
+
140
+ ## Local context
141
+
142
+ When writing logic and referring to application values in you templates, there
143
+ are some ground rules to obey. Since the template code is evaluated using
144
+ `#instance_eval` that means that you will not be able to directly use instance
145
+ variables or do unqualified method calls (calls to `self`).
146
+
147
+ In order to facilitate exposing values to your template logic, Rubyoshka
148
+ provides an API for setting a local context. The local context is simply a set
149
+ of values that are accessible for a given block of code, and to any nested
150
+ blocks within it. The local context is primarily set using the `#with` method:
151
+
152
+ ```ruby
153
+ H {
154
+ with(name: 'world') {
155
+ div {
156
+ span "Hello, #{name}"
157
+ }
158
+ }
159
+ }
160
+ ```
161
+
162
+ The local context can alternatively be set by passing hash values to `#H`:
163
+
164
+ ```ruby
165
+ H(name: 'world') {
166
+ div { span "Hello, #{name}" }
167
+ }
168
+ ```
169
+
170
+ A local context can also be set for a component (see the next section) simply by
171
+ passing arguments to the component call:
172
+
173
+ ```ruby
174
+ Greeting = H { span "Hello, #{name}" }
175
+
176
+ H {
177
+ div {
178
+ Greeting(name: 'world')
179
+ }
180
+ }
181
+ ```
182
+
183
+ ### Tip: accessing `self` and instance variables from a template
184
+
185
+ In order to be able to access the object in the context of which the template is
186
+ defined or any of its methods, you can pass it in the local context:
187
+
188
+ ```ruby
189
+ class User
190
+ ...
191
+ def greeting_template
192
+ H(user: self) {
193
+ ...
194
+ span "Hello, #{user.name}"
195
+ span "your email: #{user.email}"
196
+ }
197
+ end
198
+ end
199
+ ```
200
+
201
+ Instance variables can be passed to the template in a similar fashion:
202
+
203
+ ```ruby
204
+ H(name: @name) { span "Hello, #{name}" }
205
+ ```
206
+
207
+ ## Global context
208
+
209
+ In addition to the local context, Rubyoshka also provides a way to set a global
210
+ context, accessible from anywhere in the template, and also in sub-components
211
+ used in the template.
212
+
213
+ The global context is a simple hash that can be accessed from within the
214
+ template with the `#context` method:
215
+
216
+ ```ruby
217
+ greeting = H { span "Hello, #{context[:name]}" }
218
+ ```
219
+
220
+ The global context can be set upon rendering the template:
221
+
222
+ ```ruby
223
+ greeting.render(name: 'world')
224
+ ```
225
+
105
226
  ## Templates as components
106
227
 
107
- Rubyoshka makes it easy to compose multiple templates into a whole HTML
108
- document. Each template can be defined as a self-contained component that can
109
- be reused inside other components. Components should be defined as constants,
110
- either in the global namespace, or on the Rubyoshka namespace:
228
+ Rubyoshka makes it easy to compose multiple separate templates into a whole HTML
229
+ document. Each template can be defined as a self-contained component that can be
230
+ reused inside other components. Components can be defined as either a Rubyoshka
231
+ instance (using `#H`), a `proc` that returns a Rubyoshka instance, or using
232
+ `Rubyoshka.component`:
111
233
 
112
234
  ```ruby
113
- # Item is actually a Proc that returns a template
235
+ # Simple component relying on global/local context
236
+ Title = H { h1 title }
237
+
238
+ # Proc component that returns a template
239
+ # Notice how the lambda expression takes keyword arguments
114
240
  Item = ->(id:, text:, checked:) {
115
241
  H {
116
242
  li {
@@ -120,37 +246,114 @@ Item = ->(id:, text:, checked:) {
120
246
  }
121
247
  }
122
248
 
123
- def render_items(items)
249
+ # Components using Rubyoshka.component (or H.component) are a bit more compact.
250
+ # Any parameters are passed as arguments to the block.
251
+ NavBar = Rubyoshka.component do |links|
252
+ div {
253
+ links.each { |l| a l[:title], href: l[:url] }
254
+ }
255
+ end
256
+
257
+ def render_items(items, links)
124
258
  html = H {
259
+ Title()
260
+ NavBar(links)
125
261
  ul {
126
262
  items.each { |id, attributes|
127
263
  Item id: id, text: attributes[:text], checked: attributes[:active]
128
264
  }
129
265
  }
130
- }.render
266
+ }.render(title: 'Hello from components')
131
267
  end
132
268
  ```
133
269
 
134
- ## Wrapping arbitrary HTML
270
+ Note that a component is invoked as a method, which means that if no arguments
271
+ are passed, you must add an empty pair of parens, as shown in the example
272
+ above.
273
+
274
+ In addition to using components defined as constants, you can also use
275
+ non-constant components by invoking the `#emit` method:
276
+
277
+ ```ruby
278
+ greeting = H { span "Hello, world" }
279
+
280
+ H {
281
+ div {
282
+ emit greeting
283
+ }
284
+ }
285
+ ```
286
+
287
+ ## Fragment caching
288
+
289
+ Any part of a Rubyoshka template can be cached - a fragment, a component, or a
290
+ whole template. It is up to you, the user, to determine which parts of the
291
+ template to cache. By default, a call to `#cache` creates a cache entry based on
292
+ the source location of the cached block:
293
+
294
+ ```ruby
295
+ Head = H {
296
+ cache {
297
+ head {
298
+ title 'My app'
299
+ style "@import '/app.css';"
300
+ }
301
+ }
302
+ }
303
+ ```
304
+
305
+ However, if your template references local or global variables, you'll want to
306
+ take those into account when caching. This is done by passing any variables used
307
+ in the template to `#cache` in order to create separate cache entries for each
308
+ discrete value or combination of values:
309
+
310
+ ```ruby
311
+ Greeting = H {
312
+ cache(name) {
313
+ div {
314
+ span "Hello, #{name}"
315
+ }
316
+ }
317
+ }
318
+
319
+ names = %w{tommy dolly world}
320
+ App = H {
321
+ names.each { |n| Greeting(name: n) }
322
+ }
323
+ ```
324
+
325
+ In the above example a separate cache entry will be created for each name. The
326
+ use of caching in components is especially beneficial since components may be
327
+ reused in multiple different templates in your app.
328
+
329
+ ### Changing the cache store
330
+
331
+ Rubyoshka ships with a naïve in-memory cache store built-in. You can use
332
+ another cache store by overriding the `Rubyoshka.cache` method (see API
333
+ [reference](#rubyoshkacache)).
135
334
 
136
- Components can be used to wrap arbitrary HTML content by defining them as procs
137
- that accept blocks:
335
+ ## Wrapping arbitrary HTML with a component
336
+
337
+ Components can also be used to wrap arbitrary HTML with addional markup. This is
338
+ done by implementing the component as a `proc` that takes a block:
138
339
 
139
340
  ```ruby
140
341
  Header = ->(&inner_html) {
141
342
  header {
142
- h1 'title'
343
+ h1 'This is a title'
143
344
  emit inner_html
144
345
  }
145
346
  }
146
347
 
147
- H { Header { button 'OK'} }.render #=> "<header><h1>title</h1><button>OK</button></header>"
348
+ Greeting = H { span "Hello, #{name}" }
349
+
350
+ H { Header { Greeting(name: 'world') }.render #=> "<header><h1>This is a title</h1><span>Hello, world</span></header>"
148
351
  ```
149
352
 
150
353
  ## Some interesting use cases
151
354
 
152
355
  Rubyoshka opens up all kinds of new possibilities when it comes to putting
153
- together pieces of HTML. Feel free to explore the possibilities!
356
+ together pieces of HTML. Feel free to explore the API!
154
357
 
155
358
  ### Routing in the view
156
359
 
@@ -163,12 +366,7 @@ Router = ->(path) {
163
366
  when '/'
164
367
  PostIndex()
165
368
  when /^posts\/(.+)$/
166
- post = get_post($1)
167
- if post
168
- Post(post)
169
- else
170
- ErrorPage(404)
171
- end
369
+ Post(get_post($1))
172
370
  end
173
371
  }
174
372
 
@@ -184,4 +382,166 @@ Blog = H {
184
382
  }
185
383
  }
186
384
  }
385
+ ```
386
+
387
+ ### A general purpose router
388
+
389
+ A more flexible, reusable approach could be achieved by implementing a
390
+ higher-order routing component, in a similar fashion to
391
+ [React Router](https://reacttraining.com/react-router/web/guides/quick-start):
392
+
393
+ ```ruby
394
+ Route = ->(path, &block) {
395
+ match = path.is_a?(Regexp) ?
396
+ context[:path] =~ path : context[:path] == /^#{path}/
397
+ emit block if match
398
+ }
399
+
400
+ Blog = H {
401
+ html {
402
+ head {
403
+ title: 'My blog'
404
+ }
405
+ body {
406
+ Topbar()
407
+ Sidebar()
408
+ div id: 'content' {
409
+ Route '/' { PostIndex() }
410
+ Route /^posts\/(.+)$/ { Post(get_post($1)) }
411
+ }
412
+ }
413
+ }
414
+ }
415
+ ```
416
+
417
+ ### A higher-order list component
418
+
419
+ Here's another demonstration of a higher-order component, a list component that
420
+ takes an item component as an argument. The `List` component can be reused for
421
+ rendering any kind of unordered list, and with any kind of item component:
422
+
423
+ ```ruby
424
+ List = ->(items, item_component) {
425
+ H {
426
+ ul {
427
+ items.each { |item|
428
+ with(item: item) {
429
+ li { emit item_component }
430
+ }
431
+ }
432
+ }
433
+ }
434
+ }
435
+
436
+ TodoItem = H {
437
+ span item.text, class: item.completed ? 'completed' : 'pending'
438
+ }
439
+
440
+ def todo_list(items)
441
+ H {
442
+ div { List(items, TodoItem) }
443
+ }
444
+ end
445
+ ```
446
+
447
+ ## API Reference
448
+
449
+ #### `Rubyoshka#initialize(**context, &block)` a.k.a. `Kernel#H`
450
+
451
+ - `context`: local context hash
452
+ - `block`: template block
453
+
454
+ Initializes a new Rubyoshka instance. This method takes a block of template
455
+ code, and an optional [local context](#local-context) in the form of a hash.
456
+ The `Kernel#H` method serves as a shortcut for creating Rubyoshka instances.
457
+
458
+ #### `Rubyoshka#render(**context)`
459
+
460
+ - `context`: global context hash
461
+
462
+ Renders the template with an optional [global context](#global-context)
463
+ hash.
464
+
465
+ #### Methods accessible inside template blocks
466
+
467
+ #### `#<tag/component>(*args, **props, &block)`
468
+
469
+ - `args`: tag arguments. For an HTML tag Rubyoshka expects a single `String`
470
+ argument containing the inner text of the tag.
471
+ - `props`: hash of tag attributes
472
+ - `block`: inner HTML block
473
+
474
+ Adds a tag or component to the current template. If the method name starts with
475
+ an upper-case letter, it is considered a [component](#templates-as-components).
476
+
477
+ If a text argument is given for a tag, it will be escaped.
478
+
479
+ #### `#cache(*vary, &block)`
480
+
481
+ - `vary`: variables used in cached block. The given values will be used to
482
+ create a separate cache entry.
483
+ - `block`: inner HTML block
484
+
485
+ Caches the markup in the given block, storing it in the Rubyoshka cache store.
486
+ If a cache entry for the given block is found, it will be used instead of
487
+ invoking the block. If one or more variables given, those will be used to create
488
+ a separate cache entry.
489
+
490
+ #### `#context`
491
+
492
+ Accesses the [global context](#global-context).
493
+
494
+ #### `#emit(object)` a.k.a. `#e(object)`
495
+
496
+ - `object`: `Proc`, `Rubyoshka` instance or `String`
497
+
498
+ Adds the given object to the current template. If a `String` is given, it is
499
+ rendered verbatim, i.e. without escaping.
500
+
501
+ #### `html5(&block)`
502
+
503
+ - `block`: inner HTML block
504
+
505
+ Adds an HTML5 `doctype` tag, followed by an `html` tag with the given block.
506
+
507
+ #### `#text(data)`
508
+
509
+ - `data` - text to add
510
+
511
+ Adds text without wrapping it in a tag. The text will be escaped.
512
+
513
+ #### `#with(**context, &block)`
514
+
515
+ - `context`: local context hash
516
+ - `block`: HTML block
517
+
518
+ Sets a [local context](#local-context) for use inside the given block. The
519
+ previous local context will be restored upon exiting the given block.
520
+
521
+ #### `Rubyoshka.cache`
522
+
523
+ Returns the cache store. A cache store should implement two methods - `#[]` and
524
+ `#[]=`. Here's an example implementing a Redis-based cache store:
525
+
526
+ ```ruby
527
+ class RedisTemplateCache
528
+ def initialize(conn, prefix)
529
+ @conn = conn
530
+ @prefix = prefix
531
+ end
532
+
533
+ def [](key)
534
+ @conn.get("#{prefix}:#{key}")
535
+ end
536
+
537
+ def []=(key, value)
538
+ @conn.set("#{prefix}:#{key}", value)
539
+ end
540
+ end
541
+
542
+ TEMPLATE_CACHE = RedisTemplaceCache.new(redis_conn, "templates:cache")
543
+
544
+ def Rubyoshka.cache
545
+ TEMPLATE_CACHE
546
+ end
187
547
  ```
data/lib/rubyoshka.rb CHANGED
@@ -1,189 +1,64 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'modulation/gem'
4
3
  require 'escape_utils'
5
4
 
6
- export_default :Rubyoshka
5
+ require_relative 'rubyoshka/renderer'
7
6
 
8
7
  # A Rubyoshka is a template representing a piece of HTML
9
8
  class Rubyoshka
10
- # A Rendering is a rendering of a Rubyoshka
11
- class Rendering
12
- attr_reader :context
13
-
14
- # Initializes attributes and renders the given block
15
- # @param context [Hash] rendering context
16
- # @param block [Proc] template block
17
- # @return [void]
18
- def initialize(context, &block)
19
- @context = context
20
- @buffer = +''
21
- instance_eval(&block)
22
- end
23
-
24
- # Returns the result of the rendering
25
- # @return [String]
26
- def to_s
27
- @buffer
28
- end
29
-
30
- S_TAG_METHOD = <<~EOF
31
- def %1$s(*args, &block)
32
- tag(:%1$s, *args, &block)
33
- end
34
- EOF
35
-
36
- R_CONST_SYM = /^[A-Z]/
37
-
38
- # Catches undefined tag method call and handles them by defining the method
39
- # @param sym [Symbol] HTML tag or component identifier
40
- # @param args [Array] method call arguments
41
- # @param block [Proc] block passed to method call
42
- # @return [void]
43
- def method_missing(sym, *args, &block)
44
- if sym =~ R_CONST_SYM
45
- o = instance_eval(sym.to_s) rescue Rubyoshka.const_get(sym) \
46
- rescue Object.const_get(sym)
47
- case o
48
- when ::Proc
49
- self.class.define_method(sym) { |*a, &b| emit o.(*a, &b) }
50
- emit o.(*args, &block)
51
- when Rubyoshka
52
- self.class.define_method(sym) { emit o }
53
- emit(o)
54
- when ::String
55
- @buffer << o
56
- else
57
- e = StandardError.new "Cannot render #{o.inspect}"
58
- e.set_backtrace(caller)
59
- raise e
60
- end
61
- else
62
- self.class.class_eval(S_TAG_METHOD % sym)
63
- tag(sym, *args, &block)
64
- end
65
- end
66
-
67
- # Emits the given object into the rendering buffer
68
- # @param o [Proc, Rubyoshka, String] emitted object
69
- # @return [void]
70
- def emit(o)
71
- case o
72
- when ::Proc
73
- instance_eval(&o)
74
- when Rubyoshka
75
- instance_eval(&o.block)
76
- else
77
- @buffer << o.to_s
78
- end
79
- end
80
- alias_method :e, :emit
81
-
82
- S_LT = '<'
83
- S_GT = '>'
84
- S_LT_SLASH = '</'
85
- S_SPACE_LT_SLASH = ' </'
86
- S_SLASH_GT = '/>'
87
- S_SPACE = ' '
88
- S_EQUAL_QUOTE = '="'
89
- S_QUOTE = '"'
90
-
91
- E = EscapeUtils
92
-
93
- # Emits an HTML tag
94
- # @param sym [Symbol] HTML tag
95
- # @param text [String] text content of tag
96
- # @param props [Hash] tag attributes
97
- # @param block [Proc] nested HTML block
98
- # @return [void]
99
- def tag(sym, text = nil, **props, &block)
100
- sym = sym.to_s
101
-
102
- @buffer << S_LT << sym
103
- emit_props(props) unless props.empty?
104
-
105
- if block
106
- @buffer << S_GT
107
- instance_eval(&block)
108
- @buffer << S_LT_SLASH << sym << S_GT
109
- elsif Rubyoshka === text
110
- @buffer << S_GT
111
- emit(text)
112
- @buffer << S_LT_SLASH << sym << S_GT
113
- elsif text
114
- @buffer << S_GT << E.escape_html(text.to_s) <<
115
- S_LT_SLASH << sym << S_GT
116
- else
117
- @buffer << S_SLASH_GT
118
- end
119
- end
120
-
121
- # Emits tag attributes into the rendering buffer
122
- # @param props [Hash] tag attributes
123
- # @return [void]
124
- def emit_props(props)
125
- props.each { |k, v|
126
- case k
127
- when :text
128
- when :src, :href
129
- @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
130
- E.escape_uri(v) << S_QUOTE
131
- else
132
- if v == true
133
- @buffer << S_SPACE << k.to_s
134
- else
135
- @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE << v << S_QUOTE
136
- end
137
- end
138
- }
139
- end
140
-
141
- # Emits the p tag
142
- # @param text [String] text content of tag
143
- # @param props [Hash] tag attributes
144
- # @para block [Proc] nested HTML block
145
- # @return [void]
146
- def p(text = nil, **props, &block)
147
- tag(:p, text, **props, &block)
148
- end
149
-
150
- S_HTML5_DOCTYPE = '<!DOCTYPE html>'
151
-
152
- # Emits an HTML5 doctype tag and an html tag with the given block
153
- # @param block [Proc] nested HTML block
154
- # @return [void]
155
- def html5(&block)
156
- @buffer << S_HTML5_DOCTYPE
157
- self.html(&block)
158
- end
159
-
160
- # Emits text into the rendering buffer
161
- # @param data [String] text
162
- def text(data)
163
- @buffer << E.escape_html(text)
164
- end
165
- end
166
-
167
9
  attr_reader :block
168
10
 
169
11
  # Initializes a Rubyoshka with the given block
12
+ # @param ctx [Hash] local context
170
13
  # @param block [Proc] nested HTML block
171
14
  # @param [void]
172
- def initialize(&block)
173
- @block = block
15
+ def initialize(mode: :html, **ctx, &block)
16
+ @mode = mode
17
+ @block = ctx.empty? ? block : proc { with(**ctx, &block) }
174
18
  end
175
19
 
20
+ H_EMPTY = {}.freeze
21
+
176
22
  # Renders the associated block and returns the string result
177
23
  # @param context [Hash] context
178
24
  # @return [String]
179
- def render(context = {})
180
- Rendering.new(context, &block).to_s
25
+ def render(context = H_EMPTY)
26
+ renderer_class.new(context, &block).to_s
27
+ end
28
+
29
+ def renderer_class
30
+ case @mode
31
+ when :html
32
+ HTMLRenderer
33
+ when :xml
34
+ XMLRenderer
35
+ else
36
+ raise "Invalid mode #{@mode.inspect}"
37
+ end
38
+ end
39
+
40
+ @@cache = {}
41
+
42
+ def self.cache
43
+ @@cache
44
+ end
45
+
46
+ def self.component(&block)
47
+ proc { |*args| new { instance_exec(*args, &block) } }
48
+ end
49
+
50
+ def self.xml(**ctx, &block)
51
+ new(mode: :xml, **ctx, &block)
181
52
  end
182
53
  end
54
+ ::H = Rubyoshka
183
55
 
184
56
  module ::Kernel
185
57
  # Convenience method for creating a new Rubyoshka
186
- def H(&block)
187
- Rubyoshka.new(&block)
58
+ # @param ctx [Hash] local context
59
+ # @param block [Proc] nested block
60
+ # @return [Rubyoshka] Rubyoshka template
61
+ def H(**ctx, &block)
62
+ Rubyoshka.new(**ctx, &block)
188
63
  end
189
64
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './html'
4
+
5
+ class Rubyoshka
6
+ # Markup extensions
7
+ module HTML
8
+ # Emits the p tag (overrides Object#p)
9
+ # @param text [String] text content of tag
10
+ # @param props [Hash] tag attributes
11
+ # @para block [Proc] nested HTML block
12
+ # @return [void]
13
+ def p(text = nil, **props, &block)
14
+ method_missing(:p, text, **props, &block)
15
+ end
16
+
17
+ S_HTML5_DOCTYPE = '<!DOCTYPE html>'
18
+
19
+ # Emits an HTML5 doctype tag and an html tag with the given block
20
+ # @param block [Proc] nested HTML block
21
+ # @return [void]
22
+ def html5(&block)
23
+ @buffer << S_HTML5_DOCTYPE
24
+ self.html(&block)
25
+ end
26
+
27
+ def link_stylesheet(href, custom_attributes = nil)
28
+ attributes = {
29
+ rel: 'stylesheet',
30
+ href: href
31
+ }
32
+ if custom_attributes
33
+ attributes = custom_attributes.merge(attributes)
34
+ end
35
+ link(**attributes)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './html'
4
+
5
+ class Rubyoshka
6
+ # A Renderer is a rendering of a Rubyoshka
7
+ class Renderer
8
+ attr_reader :context
9
+
10
+ # Initializes attributes and renders the given block
11
+ # @param context [Hash] rendering context
12
+ # @param block [Proc] template block
13
+ # @return [void]
14
+ def initialize(context, &block)
15
+ @context = context
16
+ @buffer = +''
17
+ instance_eval(&block)
18
+ end
19
+
20
+ # Returns the result of the rendering
21
+ # @return [String]
22
+ def to_s
23
+ @buffer
24
+ end
25
+
26
+ def escape_text(text)
27
+ raise NotImplementedError
28
+ end
29
+
30
+ def escape_uri(uri)
31
+ EscapeUtils.escape_uri(v)
32
+ end
33
+
34
+ S_TAG_METHOD_LINE = __LINE__ + 1
35
+ S_TAG_METHOD = <<~EOF
36
+ S_TAG_%<TAG>s_PRE = '<%<tag>s'
37
+ S_TAG_%<TAG>s_CLOSE = '</%<tag>s>'
38
+
39
+ def %<tag>s(text = nil, **props, &block)
40
+ @buffer << S_TAG_%<TAG>s_PRE
41
+ emit_props(props) unless props.empty?
42
+
43
+ if block
44
+ @buffer << S_GT
45
+ instance_eval(&block)
46
+ @buffer << S_TAG_%<TAG>s_CLOSE
47
+ elsif Rubyoshka === text
48
+ @buffer << S_GT
49
+ emit(text)
50
+ @buffer << S_TAG_%<TAG>s_CLOSE
51
+ elsif text
52
+ @buffer << S_GT << escape_text(text.to_s) << S_TAG_%<TAG>s_CLOSE
53
+ else
54
+ @buffer << S_SLASH_GT
55
+ end
56
+ end
57
+ EOF
58
+
59
+ R_CONST_SYM = /^[A-Z]/
60
+
61
+ # Catches undefined tag method call and handles them by defining the method
62
+ # @param sym [Symbol] HTML tag or component identifier
63
+ # @param args [Array] method call arguments
64
+ # @param block [Proc] block passed to method call
65
+ # @return [void]
66
+ def method_missing(sym, *args, **opts, &block)
67
+ value = @local && @local[sym]
68
+ return value if value
69
+
70
+ if sym =~ R_CONST_SYM
71
+ # Component reference (capitalized method name)
72
+ o = instance_eval(sym.to_s) rescue Rubyoshka.const_get(sym) \
73
+ rescue Object.const_get(sym)
74
+ case o
75
+ when ::Proc
76
+ self.class.define_method(sym) { |*a, **c, &b| emit(o.(*a, **c, &b)) }
77
+ emit(o.(*args, **opts,&block))
78
+ when Rubyoshka
79
+ self.class.define_method(sym) do |**ctx|
80
+ ctx.empty? ? emit(o) : with(**ctx) { emit(o) }
81
+ end
82
+ Hash === opts.empty? ? emit(o) : with(**opts) { emit(o) }
83
+ when ::String
84
+ @buffer << o
85
+ else
86
+ e = StandardError.new "Cannot render #{o.inspect}"
87
+ e.set_backtrace(caller)
88
+ raise e
89
+ end
90
+ else
91
+ tag = sym.to_s
92
+ code = S_TAG_METHOD % { tag: tag, TAG: tag.upcase }
93
+ self.class.class_eval(code, __FILE__, S_TAG_METHOD_LINE)
94
+ send(sym, *args, **opts, &block)
95
+ end
96
+ end
97
+
98
+ # Emits the given object into the rendering buffer
99
+ # @param o [Proc, Rubyoshka, Module, String] emitted object
100
+ # @return [void]
101
+ def emit(o)
102
+ case o
103
+ when ::Proc
104
+ instance_eval(&o)
105
+ when Rubyoshka
106
+ instance_eval(&o.block)
107
+ when Module
108
+ # If module is given, the component is expected to be a const inside the module
109
+ emit(o::Component)
110
+ when nil
111
+ else
112
+ @buffer << o.to_s
113
+ end
114
+ end
115
+ alias_method :e, :emit
116
+
117
+ S_LT = '<'
118
+ S_GT = '>'
119
+ S_LT_SLASH = '</'
120
+ S_SPACE_LT_SLASH = ' </'
121
+ S_SLASH_GT = '/>'
122
+ S_SPACE = ' '
123
+ S_EQUAL_QUOTE = '="'
124
+ S_QUOTE = '"'
125
+
126
+ # Emits tag attributes into the rendering buffer
127
+ # @param props [Hash] tag attributes
128
+ # @return [void]
129
+ def emit_props(props)
130
+ props.each { |k, v|
131
+ case k
132
+ when :src, :href
133
+ @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
134
+ EscapeUtils.escape_uri(v) << S_QUOTE
135
+ else
136
+ case v
137
+ when true
138
+ @buffer << S_SPACE << k.to_s
139
+ when false, nil
140
+ # emit nothing
141
+ else
142
+ @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE << v << S_QUOTE
143
+ end
144
+ end
145
+ }
146
+ end
147
+
148
+ # Emits text into the rendering buffer
149
+ # @param data [String] text
150
+ def text(data)
151
+ @buffer << escape_text(data)
152
+ end
153
+
154
+ # Sets a local context for the given block
155
+ # @param ctx [Hash] context hash
156
+ # @param block [Proc] nested HTML block
157
+ # @return [void]
158
+ def with(**ctx, &block)
159
+ old_local, @local = @local, ctx
160
+ instance_eval(&block)
161
+ ensure
162
+ @local = old_local
163
+ end
164
+
165
+ # Caches the given block with the given arguments as cache key
166
+ # @param vary [*Object] cache key
167
+ # @param block [Proc] nested HTML block
168
+ # @return [void]
169
+ def cache(*vary, **opts, &block)
170
+ key = [block.source_location.hash, vary.hash, opts.hash]
171
+
172
+ if (cached = Rubyoshka.cache[key])
173
+ @buffer << cached
174
+ return
175
+ end
176
+
177
+ cache_pos = @buffer.length
178
+ instance_eval(&block)
179
+ diff = @buffer[cache_pos..-1]
180
+ Rubyoshka.cache[key] = diff
181
+ end
182
+ end
183
+
184
+ class HTMLRenderer < Renderer
185
+ include HTML
186
+
187
+ def escape_text(text)
188
+ EscapeUtils.escape_html(text.to_s)
189
+ end
190
+ end
191
+
192
+ class XMLRenderer < Renderer
193
+ def escape_text(text)
194
+ EscapeUtils.escape_xml(text.to_s)
195
+ end
196
+ end
197
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rubyoshka
4
- VERSION = '0.1'
4
+ VERSION = '0.6'
5
5
  end
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyoshka
3
3
  version: !ruby/object:Gem::Version
4
- version: '0.1'
4
+ version: '0.6'
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: 2021-03-03 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: modulation
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - '='
18
- - !ruby/object:Gem::Version
19
- version: '0.18'
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - '='
25
- - !ruby/object:Gem::Version
26
- version: '0.18'
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: escape_utils
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -80,6 +66,20 @@ dependencies:
80
66
  - - '='
81
67
  - !ruby/object:Gem::Version
82
68
  version: 2.7.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: tilt
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - '='
74
+ - !ruby/object:Gem::Version
75
+ version: 2.0.9
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - '='
81
+ - !ruby/object:Gem::Version
82
+ version: 2.0.9
83
83
  description:
84
84
  email: ciconia@gmail.com
85
85
  executables: []
@@ -90,6 +90,8 @@ files:
90
90
  - CHANGELOG.md
91
91
  - README.md
92
92
  - lib/rubyoshka.rb
93
+ - lib/rubyoshka/html.rb
94
+ - lib/rubyoshka/renderer.rb
93
95
  - lib/rubyoshka/version.rb
94
96
  homepage: http://github.com/digital-fabric/rubyoshka
95
97
  licenses:
@@ -115,8 +117,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
117
  - !ruby/object:Gem::Version
116
118
  version: '0'
117
119
  requirements: []
118
- rubyforge_project:
119
- rubygems_version: 2.7.3
120
+ rubygems_version: 3.1.4
120
121
  signing_key:
121
122
  specification_version: 4
122
123
  summary: 'Rubyoshka: composable HTML templating for Ruby'