rubyoshka 0.4 → 0.7

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: 6c084b7ff1e4a364ea95df6fa3824c1a67a190aae85c8ad50812132ef7acd5cf
4
- data.tar.gz: 8b04fd36caf85ca4dc7883f2efad96ed53753d5b59548e19a9a3c5ec85f6fa28
3
+ metadata.gz: 82ae63e4b9ff27ff20f8635c7e619b4a1dd88c1a6b2e1e7c4a7dbdad64381b40
4
+ data.tar.gz: ff00cf661c6394e2cd00443cbc7204dff6bbff434d9602cb244a274b257f1059
5
5
  SHA512:
6
- metadata.gz: 27c7bafa5f1851982ee05ccf9f90e1513c4b426217531f6daa205ce872ad57be9c672ba35ce1e5f0a6b882aa93dbf3987dab2375282e0bda22c897df1cf2ff5f
7
- data.tar.gz: 8a35f8f8ef434e21a48208d079d62acb4df497d6a44234caee41c4a1df92de8d20c8bc3329ce624eed2691fde5205961681b4a8ebd2e80966b99ee65e58c8b4f
6
+ metadata.gz: 8a2aff7151025e7a83466da15c21878868bc1160bb6bab37396a38ce6ecad08a49c807c8177bc153f22785ff6e6cdfe2eadb82950d4b06cc4d492d5654aa37b0
7
+ data.tar.gz: f11bfa4d801a259d0825f91267217009be156feba3fc567ea68de4941779fa40c07755a63a2f9812507c2858d7fb54d0d78f5601546003165fe27b912caf6f1e
data/CHANGELOG.md CHANGED
@@ -1,23 +1,39 @@
1
- 0.4 2019-02-05
2
- --------------
1
+ ## 0.7 2021-09-29
3
2
 
4
- * Add support for emitting component modules
3
+ - Add `#emit_yield` for rendering layouts
4
+ - Add experimental template compilation (WIP)
5
5
 
6
- 0.3 2019-01-13
7
- --------------
6
+ ## 0.6.1 2021-03-03
8
7
 
9
- * Implement caching
10
- * Improve performance
11
- * Handle attributes with `false` value correctly
8
+ - Remove support for Ruby 2.6
12
9
 
13
- 0.2 2019-01-07
14
- --------------
10
+ ## 0.6 2021-03-03
15
11
 
16
- * Better documentation
17
- * Fix #text
18
- * Add local context
12
+ - Fix Rubyoshka on Ruby 3.0
13
+ - Refactor and add more tests
19
14
 
20
- 0.1 2019-01-06
21
- --------------
15
+ ## 0.5 2021-02-27
22
16
 
23
- * First working version
17
+ - Add support for rendering XML
18
+ - Add Rubyoshka.component method
19
+ - Remove Modulation dependency
20
+
21
+ ## 0.4 2019-02-05
22
+
23
+ - Add support for emitting component modules
24
+
25
+ ## 0.3 2019-01-13
26
+
27
+ - Implement caching
28
+ - Improve performance
29
+ - Handle attributes with `false` value correctly
30
+
31
+ ## 0.2 2019-01-07
32
+
33
+ - Better documentation
34
+ - Fix #text
35
+ - Add local context
36
+
37
+ ## 0.1 2019-01-06
38
+
39
+ - First working version
data/README.md CHANGED
@@ -226,16 +226,17 @@ greeting.render(name: 'world')
226
226
  ## Templates as components
227
227
 
228
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
230
- be reused inside other components. Components should be defined as constants,
231
- either in the global namespace, or on the `Rubyoshka` namespace. Each component
232
- can be defined as either a Rubyoshka instance (using `#H`) or as a `proc` that
233
- returns a Rubyoshka instance:
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`:
234
233
 
235
234
  ```ruby
235
+ # Simple component relying on global/local context
236
236
  Title = H { h1 title }
237
237
 
238
- # Item is actually a Proc that returns a template
238
+ # Proc component that returns a template
239
+ # Notice how the lambda expression takes keyword arguments
239
240
  Item = ->(id:, text:, checked:) {
240
241
  H {
241
242
  li {
@@ -245,20 +246,29 @@ Item = ->(id:, text:, checked:) {
245
246
  }
246
247
  }
247
248
 
248
- 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)
249
258
  html = H {
250
259
  Title()
260
+ NavBar(links)
251
261
  ul {
252
262
  items.each { |id, attributes|
253
263
  Item id: id, text: attributes[:text], checked: attributes[:active]
254
264
  }
255
265
  }
256
- }.render
266
+ }.render(title: 'Hello from components')
257
267
  end
258
268
  ```
259
269
 
260
270
  Note that a component is invoked as a method, which means that if no arguments
261
- are passed, you should add an empty pair of parens, as shown in the example
271
+ are passed, you must add an empty pair of parens, as shown in the example
262
272
  above.
263
273
 
264
274
  In addition to using components defined as constants, you can also use
@@ -274,6 +284,43 @@ H {
274
284
  }
275
285
  ```
276
286
 
287
+ ## Layout templates
288
+
289
+ Rubyoshka templates can also be used to implement layout templates by using
290
+ `#emit_yield`:
291
+
292
+ ```ruby
293
+ layout = H {
294
+ html5 {
295
+ head { ... }
296
+ body {
297
+ header { ... }
298
+ body {
299
+ emit_yield
300
+ }
301
+ footer { ... }
302
+ }
303
+ }
304
+ }
305
+ ```
306
+
307
+ To use the layout, supply a block when calling `#render`:
308
+
309
+ ```ruby
310
+ layout.render {
311
+ h1 'foo'
312
+ p 'bar'
313
+ }
314
+
315
+ # you can also pass a template as the block
316
+ inner = H {
317
+ h1 'foo'
318
+ p 'bar'
319
+ }
320
+
321
+ layout.render(&inner)
322
+ ```
323
+
277
324
  ## Fragment caching
278
325
 
279
326
  Any part of a Rubyoshka template can be cached - a fragment, a component, or a
@@ -319,8 +366,8 @@ reused in multiple different templates in your app.
319
366
  ### Changing the cache store
320
367
 
321
368
  Rubyoshka ships with a naïve in-memory cache store built-in. You can use
322
- another cache store by overriding the `Rubyoshka.cache_get` and
323
- `Rubyoshka.cache_set` methods (see API [reference](#api-reference)).
369
+ another cache store by overriding the `Rubyoshka.cache` method (see API
370
+ [reference](#rubyoshkacache)).
324
371
 
325
372
  ## Wrapping arbitrary HTML with a component
326
373
 
@@ -0,0 +1,428 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Rubyoshka
4
+ # The Compiler class compiles Rubyoshka templates
5
+ class Compiler
6
+ DEFAULT_CODE_BUFFER_CAPACITY = 8192
7
+ DEFAULT_EMIT_BUFFER_CAPACITY = 4096
8
+
9
+ def initialize
10
+ @level = 1
11
+ @code_buffer = String.new(capacity: DEFAULT_CODE_BUFFER_CAPACITY)
12
+ end
13
+
14
+ def emit_output
15
+ @output_mode = true
16
+ yield
17
+ @output_mode = false
18
+ end
19
+
20
+ def emit_code_line_break
21
+ return if @code_buffer.empty?
22
+
23
+ @code_buffer << "\n" if @code_buffer[-1] != "\n"
24
+ @line_break = nil
25
+ end
26
+
27
+ def emit_literal(lit)
28
+ if @output_mode
29
+ emit_code_line_break if @line_break
30
+ @emit_buffer ||= String.new(capacity: DEFAULT_EMIT_BUFFER_CAPACITY)
31
+ @emit_buffer << lit
32
+ else
33
+ emit_code(lit)
34
+ end
35
+ end
36
+
37
+ def emit_text(str, encoding: :html)
38
+ emit_code_line_break if @line_break
39
+ @emit_buffer ||= String.new(capacity: DEFAULT_EMIT_BUFFER_CAPACITY)
40
+ @emit_buffer << encode(str, encoding).inspect[1..-2]
41
+ end
42
+
43
+ def encode(str, encoding)
44
+ case encoding
45
+ when :html
46
+ __html_encode__(str)
47
+ when :uri
48
+ __uri_encode__(str)
49
+ else
50
+ raise "Invalid encoding #{encoding.inspect}"
51
+ end
52
+ end
53
+
54
+ def emit_expression
55
+ if @output_mode
56
+ emit_literal('#{__html_encode__(')
57
+ yield
58
+ emit_literal(')}')
59
+ else
60
+ yield
61
+ end
62
+ end
63
+
64
+ def flush_emit_buffer
65
+ return if !@emit_buffer
66
+
67
+ @code_buffer << "#{' ' * @level}__buffer__ << \"#{@emit_buffer}\"\n"
68
+ @emit_buffer = nil
69
+ true
70
+ end
71
+
72
+ def emit_code(code)
73
+ if flush_emit_buffer || @line_break
74
+ emit_code_line_break if @line_break
75
+ @code_buffer << "#{' ' * @level}#{code}"
76
+ else
77
+ if @code_buffer.empty? || (@code_buffer[-1] == "\n")
78
+ @code_buffer << "#{' ' * @level}#{code}"
79
+ else
80
+ @code_buffer << "#{code}"
81
+ end
82
+ end
83
+ end
84
+
85
+ def compile(template)
86
+ @block = template.to_proc
87
+ ast = RubyVM::AbstractSyntaxTree.of(@block)
88
+ # Compiler.pp_ast(ast)
89
+ parse(ast)
90
+ flush_emit_buffer
91
+ self
92
+ end
93
+
94
+ attr_reader :code_buffer
95
+
96
+ def to_code
97
+ "->(__buffer__, __context__) do\n#{@code_buffer}end"
98
+ end
99
+
100
+ def to_proc
101
+ @block.binding.eval(to_code)
102
+ end
103
+
104
+ def parse(node)
105
+ @line_break = @last_node && node.first_lineno != @last_node.first_lineno
106
+ @last_node = node
107
+ # puts "- parse(#{node.type}) (break: #{@line_break.inspect})"
108
+ send(:"parse_#{node.type.downcase}", node)
109
+ end
110
+
111
+ def parse_scope(node)
112
+ parse(node.children[2])
113
+ end
114
+
115
+ def parse_iter(node)
116
+ call, scope = node.children
117
+ if call.type == :FCALL
118
+ parse_fcall(call, scope)
119
+ else
120
+ parse(call)
121
+ emit_code(" do")
122
+ args = scope.children[0]
123
+ emit_code(" |#{args.join(', ')}|") if args
124
+ emit_code("\n")
125
+ @level += 1
126
+ parse(scope)
127
+ flush_emit_buffer
128
+ @level -= 1
129
+ emit_code("end\n")
130
+ end
131
+ end
132
+
133
+ def parse_ivar(node)
134
+ ivar = node.children.first.match(/^@(.+)*/)[1]
135
+ emit_literal("__context__[:#{ivar}]")
136
+ end
137
+
138
+ def parse_fcall(node, block = nil)
139
+ tag, args = node.children
140
+ args = args.children.compact if args
141
+ text = fcall_inner_text_from_args(args)
142
+ atts = fcall_attributes_from_args(args)
143
+ if block
144
+ emit_tag(tag, atts) { parse(block) }
145
+ elsif text
146
+ emit_tag(tag, atts) do
147
+ emit_output do
148
+ if text.is_a?(String)
149
+ emit_text(text)
150
+ else
151
+ emit_expression { parse(text) }
152
+ end
153
+ end
154
+ end
155
+ else
156
+ emit_tag(tag, atts)
157
+ end
158
+ end
159
+
160
+ def fcall_inner_text_from_args(args)
161
+ return nil if !args
162
+
163
+ first = args.first
164
+ case first.type
165
+ when :STR
166
+ first.children.first
167
+ when :LIT
168
+ first.children.first.to_s
169
+ when :HASH
170
+ nil
171
+ else
172
+ first
173
+ end
174
+ end
175
+
176
+ def fcall_attributes_from_args(args)
177
+ return nil if !args
178
+
179
+ last = args.last
180
+ (last.type == :HASH) ? last : nil
181
+ end
182
+
183
+ def emit_tag(tag, atts, &block)
184
+ emit_output do
185
+ if atts
186
+ emit_literal("<#{tag}")
187
+ emit_tag_attributes(atts)
188
+ emit_literal(block ? '>' : '/>')
189
+ else
190
+ emit_literal(block ? "<#{tag}>" : "<#{tag}/>")
191
+ end
192
+ end
193
+ if block
194
+ block.call
195
+ emit_output { emit_literal("</#{tag}>") }
196
+ end
197
+ end
198
+
199
+ def emit_tag_attributes(atts)
200
+ list = atts.children.first.children
201
+ while true
202
+ key = list.shift
203
+ break unless key
204
+
205
+ value = list.shift
206
+ value_type = value.type
207
+ case value_type
208
+ when :FALSE, :NIL
209
+ next
210
+ end
211
+
212
+ emit_literal(' ')
213
+ emit_tag_attribute_key(key)
214
+ next if value_type == :TRUE
215
+
216
+ emit_literal('=\"')
217
+ emit_tag_attribute_value(value, key)
218
+ emit_literal('\"')
219
+ end
220
+ end
221
+
222
+ def emit_tag_attribute_key(key)
223
+ case key.type
224
+ when :STR
225
+ emit_literal(key.children.first)
226
+ when :LIT
227
+ emit_literal(key.children.first.to_s)
228
+ when :NIL
229
+ emit_literal('nil')
230
+ else
231
+ emit_expression { parse(key) }
232
+ end
233
+ end
234
+
235
+ def emit_tag_attribute_value(value, key)
236
+ case value.type
237
+ when :STR
238
+ encoding = (key.type == :LIT) && (key.children.first == :href) ? :uri : :html
239
+ emit_text(value.children.first, encoding: encoding)
240
+ when :LIT
241
+ emit_text(value.children.first.to_s)
242
+ else
243
+ parse(value)
244
+ end
245
+ end
246
+
247
+ def parse_call(node)
248
+ receiver, method, args = node.children
249
+ if receiver.type == :VCALL && receiver.children == [:context]
250
+ emit_literal('__context__')
251
+ else
252
+ parse(receiver)
253
+ end
254
+ if method == :[]
255
+ emit_literal('[')
256
+ args = args.children.compact
257
+ while true
258
+ arg = args.shift
259
+ break unless arg
260
+
261
+ parse(arg)
262
+ emit_literal(', ') if !args.empty?
263
+ end
264
+ emit_literal(']')
265
+ else
266
+ emit_literal('.')
267
+ emit_literal(method.to_s)
268
+ if args
269
+ emit_literal('(')
270
+ args = args.children.compact
271
+ while true
272
+ arg = args.shift
273
+ break unless arg
274
+
275
+ parse(arg)
276
+ emit_literal(', ') if !args.empty?
277
+ end
278
+ emit_literal(')')
279
+ end
280
+ end
281
+ end
282
+
283
+ def parse_str(node)
284
+ str = node.children.first
285
+ emit_literal(str.inspect)
286
+ end
287
+
288
+ def parse_lit(node)
289
+ value = node.children.first
290
+ emit_literal(value.inspect)
291
+ end
292
+
293
+ def parse_true(node)
294
+ emit_expression { emit_literal('true') }
295
+ end
296
+
297
+ def parse_false(node)
298
+ emit_expression { emit_literal('true') }
299
+ end
300
+
301
+ def parse_list(node)
302
+ emit_literal('[')
303
+ items = node.children.compact
304
+ while true
305
+ item = items.shift
306
+ break unless item
307
+
308
+ parse(item)
309
+ emit_literal(', ') if !items.empty?
310
+ end
311
+ emit_literal(']')
312
+ end
313
+
314
+ def parse_vcall(node)
315
+ tag = node.children.first
316
+ emit_tag(tag, nil)
317
+ end
318
+
319
+ def parse_opcall(node)
320
+ left, op, right = node.children
321
+ parse(left)
322
+ emit_literal(" #{op} ")
323
+ right.children.compact.each { |c| parse(c) }
324
+ end
325
+
326
+ def parse_block(node)
327
+ node.children.each { |c| parse(c) }
328
+ end
329
+
330
+ def parse_if(node)
331
+ cond, then_branch, else_branch = node.children
332
+ if @output_mode
333
+ emit_if_output(cond, then_branch, else_branch)
334
+ else
335
+ emit_if_code(cond, then_branch, else_branch)
336
+ end
337
+ end
338
+
339
+ def parse_unless(node)
340
+ cond, then_branch, else_branch = node.children
341
+ if @output_mode
342
+ emit_unless_output(cond, then_branch, else_branch)
343
+ else
344
+ emit_unless_code(cond, then_branch, else_branch)
345
+ end
346
+ end
347
+
348
+ def emit_if_output(cond, then_branch, else_branch)
349
+ parse(cond)
350
+ emit_literal(" ? ")
351
+ parse(then_branch)
352
+ emit_literal(" : ")
353
+ if else_branch
354
+ parse(else_branch)
355
+ else
356
+ emit_literal(nil)
357
+ end
358
+ end
359
+
360
+ def emit_unless_output(cond, then_branch, else_branch)
361
+ parse(cond)
362
+ emit_literal(" ? ")
363
+ if else_branch
364
+ parse(else_branch)
365
+ else
366
+ emit_literal(nil)
367
+ end
368
+ emit_literal(" : ")
369
+ parse(then_branch)
370
+ end
371
+
372
+ def emit_if_code(cond, then_branch, else_branch)
373
+ emit_code('if ')
374
+ parse(cond)
375
+ emit_code("\n")
376
+ @level += 1
377
+ parse(then_branch)
378
+ flush_emit_buffer
379
+ @level -= 1
380
+ if else_branch
381
+ emit_code("else\n")
382
+ @level += 1
383
+ parse(else_branch)
384
+ flush_emit_buffer
385
+ @level -= 1
386
+ end
387
+ emit_code("end\n")
388
+ end
389
+
390
+ def emit_unless_code(cond, then_branch, else_branch)
391
+ emit_code('unless ')
392
+ parse(cond)
393
+ emit_code("\n")
394
+ @level += 1
395
+ parse(then_branch)
396
+ flush_emit_buffer
397
+ @level -= 1
398
+ if else_branch
399
+ emit_code("else\n")
400
+ @level += 1
401
+ parse(else_branch)
402
+ flush_emit_buffer
403
+ @level -= 1
404
+ end
405
+ emit_code("end\n")
406
+ end
407
+
408
+ def parse_dvar(node)
409
+
410
+ emit_literal(node.children.first.to_s)
411
+ end
412
+
413
+ def self.pp_ast(node, level = 0)
414
+ case node
415
+ when RubyVM::AbstractSyntaxTree::Node
416
+ puts "#{' ' * level}#{node.type.inspect}"
417
+ node.children.each { |c| pp_ast(c, level + 1) }
418
+ when Array
419
+ puts "#{' ' * level}["
420
+ node.each { |c| pp_ast(c, level + 1) }
421
+ puts "#{' ' * level}]"
422
+ else
423
+ puts "#{' ' * level}#{node.inspect}"
424
+ return
425
+ end
426
+ end
427
+ end
428
+ 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,204 @@
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, template)
15
+ @context = context
16
+ @buffer = +''
17
+ instance_eval(&template)
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.template)
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
+ def emit_yield
118
+ block = @context[:__block__]
119
+ raise LocalJumpError, "no block given (emit_yield)" unless block
120
+
121
+ instance_eval(&block)
122
+ end
123
+
124
+ S_LT = '<'
125
+ S_GT = '>'
126
+ S_LT_SLASH = '</'
127
+ S_SPACE_LT_SLASH = ' </'
128
+ S_SLASH_GT = '/>'
129
+ S_SPACE = ' '
130
+ S_EQUAL_QUOTE = '="'
131
+ S_QUOTE = '"'
132
+
133
+ # Emits tag attributes into the rendering buffer
134
+ # @param props [Hash] tag attributes
135
+ # @return [void]
136
+ def emit_props(props)
137
+ props.each { |k, v|
138
+ case k
139
+ when :src, :href
140
+ @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
141
+ EscapeUtils.escape_uri(v) << S_QUOTE
142
+ else
143
+ case v
144
+ when true
145
+ @buffer << S_SPACE << k.to_s
146
+ when false, nil
147
+ # emit nothing
148
+ else
149
+ @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE << v << S_QUOTE
150
+ end
151
+ end
152
+ }
153
+ end
154
+
155
+ # Emits text into the rendering buffer
156
+ # @param data [String] text
157
+ def text(data)
158
+ @buffer << escape_text(data)
159
+ end
160
+
161
+ # Sets a local context for the given block
162
+ # @param ctx [Hash] context hash
163
+ # @param block [Proc] nested HTML block
164
+ # @return [void]
165
+ def with(**ctx, &block)
166
+ old_local, @local = @local, ctx
167
+ instance_eval(&block)
168
+ ensure
169
+ @local = old_local
170
+ end
171
+
172
+ # Caches the given block with the given arguments as cache key
173
+ # @param vary [*Object] cache key
174
+ # @param block [Proc] nested HTML block
175
+ # @return [void]
176
+ def cache(*vary, **opts, &block)
177
+ key = [block.source_location.hash, vary.hash, opts.hash]
178
+
179
+ if (cached = Rubyoshka.cache[key])
180
+ @buffer << cached
181
+ return
182
+ end
183
+
184
+ cache_pos = @buffer.length
185
+ instance_eval(&block)
186
+ diff = @buffer[cache_pos..-1]
187
+ Rubyoshka.cache[key] = diff
188
+ end
189
+ end
190
+
191
+ class HTMLRenderer < Renderer
192
+ include HTML
193
+
194
+ def escape_text(text)
195
+ EscapeUtils.escape_html(text.to_s)
196
+ end
197
+ end
198
+
199
+ class XMLRenderer < Renderer
200
+ def escape_text(text)
201
+ EscapeUtils.escape_xml(text.to_s)
202
+ end
203
+ end
204
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rubyoshka
4
- VERSION = '0.4'
4
+ VERSION = '0.7'
5
5
  end
data/lib/rubyoshka.rb CHANGED
@@ -1,208 +1,31 @@
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'
6
+ require_relative 'rubyoshka/compiler'
7
7
 
8
8
  # A Rubyoshka is a template representing a piece of HTML
9
9
  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
- E = EscapeUtils
31
-
32
- S_TAG_METHOD = <<~EOF
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
53
- end
54
- EOF
55
-
56
- R_CONST_SYM = /^[A-Z]/
57
-
58
- # Catches undefined tag method call and handles them by defining the method
59
- # @param sym [Symbol] HTML tag or component identifier
60
- # @param args [Array] method call arguments
61
- # @param block [Proc] block passed to method call
62
- # @return [void]
63
- def method_missing(sym, *args, &block)
64
- value = @local && @local[sym]
65
- return value if value
66
-
67
- if sym =~ R_CONST_SYM
68
- o = instance_eval(sym.to_s) rescue Rubyoshka.const_get(sym) \
69
- rescue Object.const_get(sym)
70
- case o
71
- when ::Proc
72
- self.class.define_method(sym) { |*a, &b| emit(o.(*a, &b)) }
73
- emit(o.(*args, &block))
74
- when Rubyoshka
75
- self.class.define_method(sym) { |**ctx|
76
- ctx.empty? ? emit(o) : with(ctx) { emit(o) }
77
- }
78
- ctx = args.first
79
- Hash === ctx ? with(ctx) { emit(o) } : emit(o)
80
- when ::String
81
- @buffer << o
82
- else
83
- e = StandardError.new "Cannot render #{o.inspect}"
84
- e.set_backtrace(caller)
85
- raise e
86
- end
87
- else
88
- tag = sym.to_s
89
- self.class.class_eval(S_TAG_METHOD % { tag: tag, TAG: tag.upcase })
90
- send(sym, *args, &block)
91
- end
92
- end
93
-
94
- # Emits the given object into the rendering buffer
95
- # @param o [Proc, Rubyoshka, Module, String] emitted object
96
- # @return [void]
97
- def emit(o)
98
- case o
99
- when ::Proc
100
- instance_eval(&o)
101
- when Rubyoshka
102
- instance_eval(&o.block)
103
- when Module
104
- emit(o::Component)
105
- when nil
106
- else
107
- @buffer << o.to_s
108
- end
109
- end
110
- alias_method :e, :emit
111
-
112
- S_LT = '<'
113
- S_GT = '>'
114
- S_LT_SLASH = '</'
115
- S_SPACE_LT_SLASH = ' </'
116
- S_SLASH_GT = '/>'
117
- S_SPACE = ' '
118
- S_EQUAL_QUOTE = '="'
119
- S_QUOTE = '"'
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 :src, :href
128
- @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE <<
129
- E.escape_uri(v) << S_QUOTE
130
- else
131
- case v
132
- when true
133
- @buffer << S_SPACE << k.to_s
134
- when false, nil
135
- # emit nothing
136
- else
137
- @buffer << S_SPACE << k.to_s << S_EQUAL_QUOTE << v << S_QUOTE
138
- end
139
- end
140
- }
10
+ module Encoding
11
+ def __html_encode__(text)
12
+ EscapeUtils.escape_html(text.to_s)
141
13
  end
142
14
 
143
- # Emits the p tag
144
- # @param text [String] text content of tag
145
- # @param props [Hash] tag attributes
146
- # @para block [Proc] nested HTML block
147
- # @return [void]
148
- def p(text = nil, **props, &block)
149
- method_missing(:p, text, **props, &block)
15
+ def __uri_encode__(text)
16
+ EscapeUtils.escape_uri(text.to_s)
150
17
  end
18
+ end
151
19
 
152
- S_HTML5_DOCTYPE = '<!DOCTYPE html>'
153
-
154
- # Emits an HTML5 doctype tag and an html tag with the given block
155
- # @param block [Proc] nested HTML block
156
- # @return [void]
157
- def html5(&block)
158
- @buffer << S_HTML5_DOCTYPE
159
- self.html(&block)
160
- end
161
-
162
- # Emits text into the rendering buffer
163
- # @param data [String] text
164
- def text(data)
165
- @buffer << E.escape_html(data)
166
- end
167
-
168
- # Sets a local context for the given block
169
- # @param ctx [Hash] context hash
170
- # @param block [Proc] nested HTML block
171
- # @return [void]
172
- def with(**ctx, &block)
173
- old_local, @local = @local, ctx
174
- instance_eval(&block)
175
- ensure
176
- @local = old_local
177
- end
178
-
179
- # Caches the given block with the given arguments as cache key
180
- # @param vary [*Object] cache key
181
- # @param block [Proc] nested HTML block
182
- # @return [void]
183
- def cache(*vary, &block)
184
- key = [block.source_location, *vary]
185
-
186
- if (cached = Rubyoshka.cache[key])
187
- @buffer << cached
188
- return
189
- end
190
-
191
- cache_pos = @buffer.length
192
- instance_eval(&block)
193
- diff = @buffer[cache_pos..-1]
194
- Rubyoshka.cache[key] = diff
195
- end
196
- end
197
-
198
- attr_reader :block
20
+ attr_reader :template
199
21
 
200
22
  # Initializes a Rubyoshka with the given block
201
23
  # @param ctx [Hash] local context
202
24
  # @param block [Proc] nested HTML block
203
25
  # @param [void]
204
- def initialize(**ctx, &block)
205
- @block = ctx.empty? ? block : proc { with(ctx, &block) }
26
+ def initialize(mode: :html, **ctx, &block)
27
+ @mode = mode
28
+ @template = ctx.empty? ? block : proc { with(**ctx, &block) }
206
29
  end
207
30
 
208
31
  H_EMPTY = {}.freeze
@@ -210,8 +33,31 @@ class Rubyoshka
210
33
  # Renders the associated block and returns the string result
211
34
  # @param context [Hash] context
212
35
  # @return [String]
213
- def render(context = H_EMPTY)
214
- Rendering.new(context, &block).to_s
36
+ def render(context = H_EMPTY, &block)
37
+ if block
38
+ context = context.dup if context.frozen?
39
+ context[:__block__] = block
40
+ end
41
+ renderer_class.new(context, @template).to_s
42
+ end
43
+
44
+ def renderer_class
45
+ case @mode
46
+ when :html
47
+ HTMLRenderer
48
+ when :xml
49
+ XMLRenderer
50
+ else
51
+ raise "Invalid mode #{@mode.inspect}"
52
+ end
53
+ end
54
+
55
+ def compile
56
+ Rubyoshka::Compiler.new.compile(self)
57
+ end
58
+
59
+ def to_proc
60
+ @template
215
61
  end
216
62
 
217
63
  @@cache = {}
@@ -219,14 +65,29 @@ class Rubyoshka
219
65
  def self.cache
220
66
  @@cache
221
67
  end
68
+
69
+ def self.component(&block)
70
+ proc { |*args| new { instance_exec(*args, &block) } }
71
+ end
72
+
73
+ def self.xml(**ctx, &block)
74
+ new(mode: :xml, **ctx, &block)
75
+ end
222
76
  end
77
+ ::H = Rubyoshka
223
78
 
79
+ # Kernel extensions
224
80
  module ::Kernel
225
81
  # Convenience method for creating a new Rubyoshka
226
82
  # @param ctx [Hash] local context
227
- # @param block [Proc] nested block
83
+ # @param template [Proc] template block
228
84
  # @return [Rubyoshka] Rubyoshka template
229
- def H(**ctx, &block)
230
- Rubyoshka.new(ctx, &block)
85
+ def H(**ctx, &template)
86
+ Rubyoshka.new(**ctx, &template)
231
87
  end
232
- end
88
+ end
89
+
90
+ # Object extensions
91
+ class Object
92
+ include Rubyoshka::Encoding
93
+ 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.4'
4
+ version: '0.7'
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-02-05 00:00:00.000000000 Z
11
+ date: 2021-09-29 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
@@ -104,6 +90,9 @@ files:
104
90
  - CHANGELOG.md
105
91
  - README.md
106
92
  - lib/rubyoshka.rb
93
+ - lib/rubyoshka/compiler.rb
94
+ - lib/rubyoshka/html.rb
95
+ - lib/rubyoshka/renderer.rb
107
96
  - lib/rubyoshka/version.rb
108
97
  homepage: http://github.com/digital-fabric/rubyoshka
109
98
  licenses:
@@ -122,14 +111,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
122
111
  requirements:
123
112
  - - ">="
124
113
  - !ruby/object:Gem::Version
125
- version: '0'
114
+ version: '2.7'
126
115
  required_rubygems_version: !ruby/object:Gem::Requirement
127
116
  requirements:
128
117
  - - ">="
129
118
  - !ruby/object:Gem::Version
130
119
  version: '0'
131
120
  requirements: []
132
- rubygems_version: 3.0.1
121
+ rubygems_version: 3.1.4
133
122
  signing_key:
134
123
  specification_version: 4
135
124
  summary: 'Rubyoshka: composable HTML templating for Ruby'