rubyoshka 0.6.1 → 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: ae561e0250334107b9b82a72fc72ab672622a7f037044e8e18154e79b71a54b5
4
- data.tar.gz: e5cddea7df6231799b38339bc014373a6c9d6a96bb60d3c7b7107e48321b3c6d
3
+ metadata.gz: 82ae63e4b9ff27ff20f8635c7e619b4a1dd88c1a6b2e1e7c4a7dbdad64381b40
4
+ data.tar.gz: ff00cf661c6394e2cd00443cbc7204dff6bbff434d9602cb244a274b257f1059
5
5
  SHA512:
6
- metadata.gz: 675b83551aa791f6bb1539c80c40d267e3caa1aceed3748af04ca1d2a49bcac3ad237a8d8c0f0dd69d24714eadbb47bf4a929af77c3ef2c71155e806ff611c66
7
- data.tar.gz: 7dd19daa9df716417c894130ff8fdae0505f83a63de17709634b2c7c83384f56453c13adbee109e4961f44a64f1c46e151752d1fc5bf690ad6738402b023f9bf
6
+ metadata.gz: 8a2aff7151025e7a83466da15c21878868bc1160bb6bab37396a38ce6ecad08a49c807c8177bc153f22785ff6e6cdfe2eadb82950d4b06cc4d492d5654aa37b0
7
+ data.tar.gz: f11bfa4d801a259d0825f91267217009be156feba3fc567ea68de4941779fa40c07755a63a2f9812507c2858d7fb54d0d78f5601546003165fe27b912caf6f1e
data/CHANGELOG.md CHANGED
@@ -1,41 +1,39 @@
1
- 0.6.1 2021-03-03
2
- ----------------
1
+ ## 0.7 2021-09-29
3
2
 
4
- * Remove support for Ruby 2.6
3
+ - Add `#emit_yield` for rendering layouts
4
+ - Add experimental template compilation (WIP)
5
5
 
6
- 0.6 2021-03-03
7
- --------------
6
+ ## 0.6.1 2021-03-03
8
7
 
9
- * Fix Rubyoshka on Ruby 3.0
10
- * Refactor and add more tests
8
+ - Remove support for Ruby 2.6
11
9
 
12
- 0.5 2021-02-27
13
- --------------
10
+ ## 0.6 2021-03-03
14
11
 
15
- * Add support for rendering XML
16
- * Add Rubyoshka.component method
17
- * Remove Modulation dependency
12
+ - Fix Rubyoshka on Ruby 3.0
13
+ - Refactor and add more tests
18
14
 
19
- 0.4 2019-02-05
20
- --------------
15
+ ## 0.5 2021-02-27
21
16
 
22
- * Add support for emitting component modules
17
+ - Add support for rendering XML
18
+ - Add Rubyoshka.component method
19
+ - Remove Modulation dependency
23
20
 
24
- 0.3 2019-01-13
25
- --------------
21
+ ## 0.4 2019-02-05
26
22
 
27
- * Implement caching
28
- * Improve performance
29
- * Handle attributes with `false` value correctly
23
+ - Add support for emitting component modules
30
24
 
31
- 0.2 2019-01-07
32
- --------------
25
+ ## 0.3 2019-01-13
33
26
 
34
- * Better documentation
35
- * Fix #text
36
- * Add local context
27
+ - Implement caching
28
+ - Improve performance
29
+ - Handle attributes with `false` value correctly
37
30
 
38
- 0.1 2019-01-06
39
- --------------
31
+ ## 0.2 2019-01-07
40
32
 
41
- * First working version
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
@@ -284,6 +284,43 @@ H {
284
284
  }
285
285
  ```
286
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
+
287
324
  ## Fragment caching
288
325
 
289
326
  Any part of a Rubyoshka template can be cached - a fragment, a component, or a
@@ -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
@@ -11,10 +11,10 @@ class Rubyoshka
11
11
  # @param context [Hash] rendering context
12
12
  # @param block [Proc] template block
13
13
  # @return [void]
14
- def initialize(context, &block)
14
+ def initialize(context, template)
15
15
  @context = context
16
16
  @buffer = +''
17
- instance_eval(&block)
17
+ instance_eval(&template)
18
18
  end
19
19
 
20
20
  # Returns the result of the rendering
@@ -74,7 +74,6 @@ class Rubyoshka
74
74
  case o
75
75
  when ::Proc
76
76
  self.class.define_method(sym) { |*a, **c, &b| emit(o.(*a, **c, &b)) }
77
- STDOUT.puts({o: o, args: args, opts: opts, block: block}.inspect)
78
77
  emit(o.(*args, **opts, &block))
79
78
  when Rubyoshka
80
79
  self.class.define_method(sym) do |**ctx|
@@ -104,7 +103,7 @@ class Rubyoshka
104
103
  when ::Proc
105
104
  instance_eval(&o)
106
105
  when Rubyoshka
107
- instance_eval(&o.block)
106
+ instance_eval(&o.template)
108
107
  when Module
109
108
  # If module is given, the component is expected to be a const inside the module
110
109
  emit(o::Component)
@@ -114,6 +113,13 @@ class Rubyoshka
114
113
  end
115
114
  end
116
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
117
123
 
118
124
  S_LT = '<'
119
125
  S_GT = '>'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Rubyoshka
4
- VERSION = '0.6.1'
4
+ VERSION = '0.7'
5
5
  end
data/lib/rubyoshka.rb CHANGED
@@ -3,10 +3,21 @@
3
3
  require 'escape_utils'
4
4
 
5
5
  require_relative 'rubyoshka/renderer'
6
+ require_relative 'rubyoshka/compiler'
6
7
 
7
8
  # A Rubyoshka is a template representing a piece of HTML
8
9
  class Rubyoshka
9
- attr_reader :block
10
+ module Encoding
11
+ def __html_encode__(text)
12
+ EscapeUtils.escape_html(text.to_s)
13
+ end
14
+
15
+ def __uri_encode__(text)
16
+ EscapeUtils.escape_uri(text.to_s)
17
+ end
18
+ end
19
+
20
+ attr_reader :template
10
21
 
11
22
  # Initializes a Rubyoshka with the given block
12
23
  # @param ctx [Hash] local context
@@ -14,7 +25,7 @@ class Rubyoshka
14
25
  # @param [void]
15
26
  def initialize(mode: :html, **ctx, &block)
16
27
  @mode = mode
17
- @block = ctx.empty? ? block : proc { with(**ctx, &block) }
28
+ @template = ctx.empty? ? block : proc { with(**ctx, &block) }
18
29
  end
19
30
 
20
31
  H_EMPTY = {}.freeze
@@ -22,8 +33,12 @@ class Rubyoshka
22
33
  # Renders the associated block and returns the string result
23
34
  # @param context [Hash] context
24
35
  # @return [String]
25
- def render(context = H_EMPTY)
26
- renderer_class.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
27
42
  end
28
43
 
29
44
  def renderer_class
@@ -37,6 +52,14 @@ class Rubyoshka
37
52
  end
38
53
  end
39
54
 
55
+ def compile
56
+ Rubyoshka::Compiler.new.compile(self)
57
+ end
58
+
59
+ def to_proc
60
+ @template
61
+ end
62
+
40
63
  @@cache = {}
41
64
 
42
65
  def self.cache
@@ -53,12 +76,18 @@ class Rubyoshka
53
76
  end
54
77
  ::H = Rubyoshka
55
78
 
79
+ # Kernel extensions
56
80
  module ::Kernel
57
81
  # Convenience method for creating a new Rubyoshka
58
82
  # @param ctx [Hash] local context
59
- # @param block [Proc] nested block
83
+ # @param template [Proc] template block
60
84
  # @return [Rubyoshka] Rubyoshka template
61
- def H(**ctx, &block)
62
- Rubyoshka.new(**ctx, &block)
85
+ def H(**ctx, &template)
86
+ Rubyoshka.new(**ctx, &template)
63
87
  end
64
- end
88
+ end
89
+
90
+ # Object extensions
91
+ class Object
92
+ include Rubyoshka::Encoding
93
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyoshka
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
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: 2021-03-03 00:00:00.000000000 Z
11
+ date: 2021-09-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: escape_utils
@@ -90,6 +90,7 @@ files:
90
90
  - CHANGELOG.md
91
91
  - README.md
92
92
  - lib/rubyoshka.rb
93
+ - lib/rubyoshka/compiler.rb
93
94
  - lib/rubyoshka/html.rb
94
95
  - lib/rubyoshka/renderer.rb
95
96
  - lib/rubyoshka/version.rb