rubyoshka 0.4 → 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 +4 -4
- data/CHANGELOG.md +32 -16
- data/README.md +58 -11
- data/lib/rubyoshka/compiler.rb +428 -0
- data/lib/rubyoshka/html.rb +38 -0
- data/lib/rubyoshka/renderer.rb +204 -0
- data/lib/rubyoshka/version.rb +1 -1
- data/lib/rubyoshka.rb +56 -195
- metadata +7 -18
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 82ae63e4b9ff27ff20f8635c7e619b4a1dd88c1a6b2e1e7c4a7dbdad64381b40
|
4
|
+
data.tar.gz: ff00cf661c6394e2cd00443cbc7204dff6bbff434d9602cb244a274b257f1059
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8a2aff7151025e7a83466da15c21878868bc1160bb6bab37396a38ce6ecad08a49c807c8177bc153f22785ff6e6cdfe2eadb82950d4b06cc4d492d5654aa37b0
|
7
|
+
data.tar.gz: f11bfa4d801a259d0825f91267217009be156feba3fc567ea68de4941779fa40c07755a63a2f9812507c2858d7fb54d0d78f5601546003165fe27b912caf6f1e
|
data/CHANGELOG.md
CHANGED
@@ -1,23 +1,39 @@
|
|
1
|
-
0.
|
2
|
-
--------------
|
1
|
+
## 0.7 2021-09-29
|
3
2
|
|
4
|
-
|
3
|
+
- Add `#emit_yield` for rendering layouts
|
4
|
+
- Add experimental template compilation (WIP)
|
5
5
|
|
6
|
-
0.
|
7
|
-
--------------
|
6
|
+
## 0.6.1 2021-03-03
|
8
7
|
|
9
|
-
|
10
|
-
* Improve performance
|
11
|
-
* Handle attributes with `false` value correctly
|
8
|
+
- Remove support for Ruby 2.6
|
12
9
|
|
13
|
-
0.
|
14
|
-
--------------
|
10
|
+
## 0.6 2021-03-03
|
15
11
|
|
16
|
-
|
17
|
-
|
18
|
-
* Add local context
|
12
|
+
- Fix Rubyoshka on Ruby 3.0
|
13
|
+
- Refactor and add more tests
|
19
14
|
|
20
|
-
0.
|
21
|
-
--------------
|
15
|
+
## 0.5 2021-02-27
|
22
16
|
|
23
|
-
|
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
|
-
|
231
|
-
|
232
|
-
|
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
|
-
#
|
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
|
-
|
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
|
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.
|
323
|
-
|
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
|
data/lib/rubyoshka/version.rb
CHANGED
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
|
-
|
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
|
-
|
11
|
-
|
12
|
-
|
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
|
-
|
144
|
-
|
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
|
-
|
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
|
-
@
|
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
|
-
|
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
|
83
|
+
# @param template [Proc] template block
|
228
84
|
# @return [Rubyoshka] Rubyoshka template
|
229
|
-
def H(**ctx, &
|
230
|
-
Rubyoshka.new(ctx, &
|
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
|
+
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:
|
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: '
|
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.
|
121
|
+
rubygems_version: 3.1.4
|
133
122
|
signing_key:
|
134
123
|
specification_version: 4
|
135
124
|
summary: 'Rubyoshka: composable HTML templating for Ruby'
|