rubyoshka 0.6.1 → 0.7

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