rubyoshka 0.1 → 0.6

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