curly-templates 1.0.1 → 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -4
- data/README.md +179 -8
- data/Rakefile +1 -1
- data/curly-templates.gemspec +10 -3
- data/lib/curly.rb +4 -4
- data/lib/curly/attribute_parser.rb +69 -0
- data/lib/curly/compiler.rb +77 -47
- data/lib/curly/component_compiler.rb +119 -0
- data/lib/curly/component_parser.rb +13 -0
- data/lib/curly/incomplete_block_error.rb +3 -3
- data/lib/curly/incorrect_ending_error.rb +17 -3
- data/lib/curly/invalid_component.rb +13 -0
- data/lib/curly/presenter.rb +31 -11
- data/lib/curly/scanner.rb +24 -11
- data/spec/attribute_parser_spec.rb +46 -0
- data/spec/compiler/collections_spec.rb +153 -0
- data/spec/compiler_spec.rb +18 -34
- data/spec/component_compiler_spec.rb +160 -0
- data/spec/incorrect_ending_error_spec.rb +13 -0
- data/spec/presenter_spec.rb +27 -10
- data/spec/scanner_spec.rb +22 -13
- data/spec/spec_helper.rb +15 -0
- data/spec/template_handler_spec.rb +1 -1
- metadata +16 -5
- data/lib/curly/invalid_reference.rb +0 -13
data/lib/curly/scanner.rb
CHANGED
@@ -15,8 +15,9 @@ module Curly
|
|
15
15
|
ESCAPED_CURLY_START = /\{\{\{/
|
16
16
|
|
17
17
|
COMMENT_MARKER = /!/
|
18
|
-
|
18
|
+
CONDITIONAL_BLOCK_MARKER = /#/
|
19
19
|
INVERSE_BLOCK_MARKER = /\^/
|
20
|
+
COLLECTION_BLOCK_MARKER = /\*/
|
20
21
|
END_BLOCK_MARKER = /\//
|
21
22
|
|
22
23
|
|
@@ -27,7 +28,7 @@ module Curly
|
|
27
28
|
# Example
|
28
29
|
#
|
29
30
|
# Curly::Scanner.scan("hello {{name}}!")
|
30
|
-
# #=> [[:text, "hello "], [:
|
31
|
+
# #=> [[:text, "hello "], [:component, "name"], [:text, "!"]]
|
31
32
|
#
|
32
33
|
# Returns an Array of type/value pairs representing the tokens in the
|
33
34
|
# template.
|
@@ -70,14 +71,16 @@ module Curly
|
|
70
71
|
def scan_tag
|
71
72
|
if @scanner.scan(COMMENT_MARKER)
|
72
73
|
scan_comment
|
73
|
-
elsif @scanner.scan(
|
74
|
-
|
74
|
+
elsif @scanner.scan(CONDITIONAL_BLOCK_MARKER)
|
75
|
+
scan_conditional_block_start
|
75
76
|
elsif @scanner.scan(INVERSE_BLOCK_MARKER)
|
76
77
|
scan_inverse_block_start
|
78
|
+
elsif @scanner.scan(COLLECTION_BLOCK_MARKER)
|
79
|
+
scan_collection_block_start
|
77
80
|
elsif @scanner.scan(END_BLOCK_MARKER)
|
78
81
|
scan_block_end
|
79
82
|
else
|
80
|
-
|
83
|
+
scan_component
|
81
84
|
end
|
82
85
|
end
|
83
86
|
|
@@ -87,27 +90,37 @@ module Curly
|
|
87
90
|
end
|
88
91
|
end
|
89
92
|
|
90
|
-
def
|
93
|
+
def scan_conditional_block_start
|
91
94
|
if value = scan_until_end_of_curly
|
92
|
-
[:
|
95
|
+
[:conditional_block_start, value]
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def scan_collection_block_start
|
100
|
+
if value = scan_until_end_of_curly
|
101
|
+
[:collection_block_start, value]
|
93
102
|
end
|
94
103
|
end
|
95
104
|
|
96
105
|
def scan_inverse_block_start
|
97
106
|
if value = scan_until_end_of_curly
|
98
|
-
[:
|
107
|
+
[:inverse_conditional_block_start, value]
|
99
108
|
end
|
100
109
|
end
|
101
110
|
|
102
111
|
def scan_block_end
|
103
112
|
if value = scan_until_end_of_curly
|
104
|
-
|
113
|
+
if value.end_with?("?")
|
114
|
+
[:conditional_block_end, value]
|
115
|
+
else
|
116
|
+
[:collection_block_end, value]
|
117
|
+
end
|
105
118
|
end
|
106
119
|
end
|
107
120
|
|
108
|
-
def
|
121
|
+
def scan_component
|
109
122
|
if value = scan_until_end_of_curly
|
110
|
-
[:
|
123
|
+
[:component, value]
|
111
124
|
end
|
112
125
|
end
|
113
126
|
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Curly::AttributeParser do
|
4
|
+
it "parses attributes" do
|
5
|
+
parse("width=10px height=20px").should == {
|
6
|
+
"width" => "10px",
|
7
|
+
"height" => "20px"
|
8
|
+
}
|
9
|
+
end
|
10
|
+
|
11
|
+
it "parses single quoted values" do
|
12
|
+
parse("title='hello world'").should == { "title" => "hello world" }
|
13
|
+
end
|
14
|
+
|
15
|
+
it "parses double quoted values" do
|
16
|
+
parse('title="hello world"').should == { "title" => "hello world" }
|
17
|
+
end
|
18
|
+
|
19
|
+
it "parses mixed quotes" do
|
20
|
+
parse(%[x=y q="foo's bar" v='bim " bum' t="foo ' bar"]).should == {
|
21
|
+
"x" => "y",
|
22
|
+
"q" => "foo's bar",
|
23
|
+
"t" => "foo ' bar",
|
24
|
+
"v" => 'bim " bum'
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
it "deals with weird whitespace" do
|
29
|
+
parse(" size=big ").should == { "size" => "big" }
|
30
|
+
end
|
31
|
+
|
32
|
+
it "parses empty attribute lists" do
|
33
|
+
parse(nil).should == {}
|
34
|
+
parse("").should == {}
|
35
|
+
parse(" ").should == {}
|
36
|
+
end
|
37
|
+
|
38
|
+
it "fails when an invalid attribute list is passed" do
|
39
|
+
expect { parse("foo") }.to raise_exception(Curly::AttributeError)
|
40
|
+
expect { parse("foo=") }.to raise_exception(Curly::AttributeError)
|
41
|
+
end
|
42
|
+
|
43
|
+
def parse(str)
|
44
|
+
described_class.parse(str)
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Curly::Compiler do
|
4
|
+
include CompilationSupport
|
5
|
+
|
6
|
+
let(:presenter_class) do
|
7
|
+
Class.new(Curly::Presenter) do
|
8
|
+
presents :list
|
9
|
+
|
10
|
+
def title
|
11
|
+
@list.title
|
12
|
+
end
|
13
|
+
|
14
|
+
def items(status: nil)
|
15
|
+
if status
|
16
|
+
@list.items.select {|item| item.status == status }
|
17
|
+
else
|
18
|
+
@list.items
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def companies
|
23
|
+
"Nike, Adidas"
|
24
|
+
end
|
25
|
+
|
26
|
+
def numbers
|
27
|
+
"one, two, three"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:simple_presenter_class) do
|
33
|
+
Class.new(Curly::Presenter) do
|
34
|
+
presents :company
|
35
|
+
|
36
|
+
def name
|
37
|
+
@company
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
let(:inner_presenter_class) do
|
43
|
+
Class.new(Curly::Presenter) do
|
44
|
+
presents :item, :item_counter
|
45
|
+
presents :list, default: nil
|
46
|
+
|
47
|
+
attr_reader :item_counter
|
48
|
+
|
49
|
+
def name
|
50
|
+
@item.name
|
51
|
+
end
|
52
|
+
|
53
|
+
def list_title
|
54
|
+
@list.title
|
55
|
+
end
|
56
|
+
|
57
|
+
def parts
|
58
|
+
@item.parts
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
let(:inner_inner_presenter_class) do
|
64
|
+
Class.new(Curly::Presenter) do
|
65
|
+
presents :part
|
66
|
+
|
67
|
+
def identifier
|
68
|
+
@part.identifier
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
let(:list) { double("list", title: "Inventory") }
|
74
|
+
let(:context) { double("context") }
|
75
|
+
let(:presenter) { presenter_class.new(context, list: list) }
|
76
|
+
|
77
|
+
before do
|
78
|
+
stub_const("ItemPresenter", inner_presenter_class)
|
79
|
+
stub_const("PartPresenter", inner_inner_presenter_class)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "compiles collection blocks" do
|
83
|
+
item1 = double("item1", name: "foo")
|
84
|
+
item2 = double("item2", name: "bar")
|
85
|
+
|
86
|
+
list.stub(:items) { [item1, item2] }
|
87
|
+
|
88
|
+
template = "<ul>{{*items}}<li>{{name}}</li>{{/items}}</ul>"
|
89
|
+
expect(evaluate(template)).to eql "<ul><li>foo</li><li>bar</li></ul>"
|
90
|
+
end
|
91
|
+
|
92
|
+
it "allows attributes on collection blocks" do
|
93
|
+
item1 = double("item1", name: "foo", status: "active")
|
94
|
+
item2 = double("item2", name: "bar", status: "inactive")
|
95
|
+
|
96
|
+
list.stub(:items) { [item1, item2] }
|
97
|
+
|
98
|
+
template = "<ul>{{*items status=active}}<li>{{name}}</li>{{/items}}</ul>"
|
99
|
+
expect(evaluate(template)).to eql "<ul><li>foo</li></ul>"
|
100
|
+
end
|
101
|
+
|
102
|
+
it "fails if the component isn't available" do
|
103
|
+
template = "<ul>{{*doodads}}<li>{{name}}</li>{{/doodads}}</ul>"
|
104
|
+
expect { evaluate(template) }.to raise_exception(Curly::Error)
|
105
|
+
end
|
106
|
+
|
107
|
+
it "fails if the component doesn't support enumeration" do
|
108
|
+
template = "<ul>{{*numbers}}<li>{{name}}</li>{{/numbers}}</ul>"
|
109
|
+
expect { evaluate(template) }.to raise_exception(Curly::Error)
|
110
|
+
end
|
111
|
+
|
112
|
+
it "works even if the component method doesn't return an Array" do
|
113
|
+
stub_const("CompanyPresenter", simple_presenter_class)
|
114
|
+
template = "<ul>{{*companies}}<li>{{name}}</li>{{/companies}}</ul>"
|
115
|
+
expect(evaluate(template)).to eql "<ul><li>Nike, Adidas</li></ul>"
|
116
|
+
end
|
117
|
+
|
118
|
+
it "passes the index of the current item to the nested presenter" do
|
119
|
+
item1 = double("item1")
|
120
|
+
item2 = double("item2")
|
121
|
+
|
122
|
+
list.stub(:items) { [item1, item2] }
|
123
|
+
|
124
|
+
template = "<ul>{{*items}}<li>{{item_counter}}</li>{{/items}}</ul>"
|
125
|
+
expect(evaluate(template)).to eql "<ul><li>1</li><li>2</li></ul>"
|
126
|
+
end
|
127
|
+
|
128
|
+
it "restores the previous scope after exiting the collection block" do
|
129
|
+
part = double("part", identifier: "X")
|
130
|
+
item = double("item", name: "foo", parts: [part])
|
131
|
+
list.stub(:items) { [item] }
|
132
|
+
|
133
|
+
template = "{{*items}}{{*parts}}{{identifier}}{{/parts}}{{name}}{{/items}}{{title}}"
|
134
|
+
expect(evaluate(template)).to eql "XfooInventory"
|
135
|
+
end
|
136
|
+
|
137
|
+
it "passes the parent presenter's options to the nested presenter" do
|
138
|
+
list.stub(:items) { [double(name: "foo"), double(name: "bar")] }
|
139
|
+
|
140
|
+
template = "{{*items}}{{list_title}}: {{name}}. {{/items}}"
|
141
|
+
expect(evaluate(template, list: list)).to eql "Inventory: foo. Inventory: bar. "
|
142
|
+
end
|
143
|
+
|
144
|
+
it "compiles nested collection blocks" do
|
145
|
+
item1 = double("item1", name: "item1", parts: [double(identifier: "A"), double(identifier: "B")])
|
146
|
+
item2 = double("item2", name: "item2", parts: [double(identifier: "C"), double(identifier: "D")])
|
147
|
+
|
148
|
+
list.stub(:items) { [item1, item2] }
|
149
|
+
|
150
|
+
template = "{{title}}: {{*items}}{{name}} - {{*parts}}{{identifier}}{{/parts}}. {{/items}}"
|
151
|
+
expect(evaluate(template)).to eql "Inventory: item1 - AB. item2 - CD. "
|
152
|
+
end
|
153
|
+
end
|
data/spec/compiler_spec.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Curly::Compiler do
|
4
|
+
include CompilationSupport
|
5
|
+
|
4
6
|
let :presenter_class do
|
5
7
|
Class.new do
|
6
8
|
def foo
|
@@ -27,6 +29,10 @@ describe Curly::Compiler do
|
|
27
29
|
nil
|
28
30
|
end
|
29
31
|
|
32
|
+
def square?(width:, height:)
|
33
|
+
width.to_i == height.to_i
|
34
|
+
end
|
35
|
+
|
30
36
|
def false?
|
31
37
|
false
|
32
38
|
end
|
@@ -35,16 +41,11 @@ describe Curly::Compiler do
|
|
35
41
|
true
|
36
42
|
end
|
37
43
|
|
38
|
-
def
|
39
|
-
|
40
|
-
end
|
41
|
-
|
42
|
-
def self.method_available?(method)
|
43
|
-
[:foo, :parameterized, :high_yield, :yield_value, :dirty,
|
44
|
-
:false?, :true?, :hello?].include?(method)
|
44
|
+
def self.component_available?(method)
|
45
|
+
%w[foo high_yield yield_value dirty false? true? hello? square?].include?(method)
|
45
46
|
end
|
46
47
|
|
47
|
-
def self.
|
48
|
+
def self.available_components
|
48
49
|
public_instance_methods
|
49
50
|
end
|
50
51
|
|
@@ -63,14 +64,6 @@ describe Curly::Compiler do
|
|
63
64
|
evaluate("{{foo}}").should == "FOO"
|
64
65
|
end
|
65
66
|
|
66
|
-
it "passes on an optional reference parameter to the presenter method" do
|
67
|
-
evaluate("{{parameterized.foo.bar}}").should == "foo.bar"
|
68
|
-
end
|
69
|
-
|
70
|
-
it "passes an empty string to methods that take a parameter when none is provided" do
|
71
|
-
evaluate("{{parameterized}}").should == ""
|
72
|
-
end
|
73
|
-
|
74
67
|
it "raises ArgumentError if the presenter class is nil" do
|
75
68
|
expect do
|
76
69
|
Curly::Compiler.compile("foo", nil)
|
@@ -78,15 +71,15 @@ describe Curly::Compiler do
|
|
78
71
|
end
|
79
72
|
|
80
73
|
it "makes sure only public methods are called on the presenter object" do
|
81
|
-
expect { evaluate("{{bar}}") }.to raise_exception(Curly::
|
74
|
+
expect { evaluate("{{bar}}") }.to raise_exception(Curly::InvalidComponent)
|
82
75
|
end
|
83
76
|
|
84
|
-
it "includes the invalid
|
77
|
+
it "includes the invalid component when failing to compile" do
|
85
78
|
begin
|
86
79
|
evaluate("{{bar}}")
|
87
80
|
fail
|
88
|
-
rescue Curly::
|
89
|
-
e.
|
81
|
+
rescue Curly::InvalidComponent => e
|
82
|
+
e.component.should == "bar"
|
90
83
|
end
|
91
84
|
end
|
92
85
|
|
@@ -141,6 +134,10 @@ describe Curly::Compiler do
|
|
141
134
|
evaluate("{{#hello.world?}}foo{{/hello.world?}}{{#hello.foo?}}bar{{/hello.foo?}}").should == "foo"
|
142
135
|
end
|
143
136
|
|
137
|
+
it "passes attributes to blocks" do
|
138
|
+
evaluate("{{#square? width=2 height=2}}yeah!{{/square?}}").should == "yeah!"
|
139
|
+
end
|
140
|
+
|
144
141
|
it "gives an error on mismatching blocks" do
|
145
142
|
expect do
|
146
143
|
evaluate("test{{#false?}}bar{{/true?}}")
|
@@ -174,7 +171,7 @@ describe Curly::Compiler do
|
|
174
171
|
end
|
175
172
|
|
176
173
|
it "returns false if an unavailable method is referenced" do
|
177
|
-
presenter_class.stub(:
|
174
|
+
presenter_class.stub(:available_components) { [:foo] }
|
178
175
|
validate("Hello, {{inspect}}").should == false
|
179
176
|
end
|
180
177
|
|
@@ -190,17 +187,4 @@ describe Curly::Compiler do
|
|
190
187
|
Curly.valid?(template, presenter_class)
|
191
188
|
end
|
192
189
|
end
|
193
|
-
|
194
|
-
def evaluate(template, &block)
|
195
|
-
code = Curly::Compiler.compile(template, presenter_class)
|
196
|
-
context = double("context", presenter: presenter)
|
197
|
-
|
198
|
-
context.instance_eval(<<-RUBY)
|
199
|
-
def self.render
|
200
|
-
#{code}
|
201
|
-
end
|
202
|
-
RUBY
|
203
|
-
|
204
|
-
context.render(&block)
|
205
|
-
end
|
206
190
|
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Curly::ComponentCompiler do
|
4
|
+
describe ".compile_conditional" do
|
5
|
+
let(:presenter_class) do
|
6
|
+
Class.new do
|
7
|
+
def monday?
|
8
|
+
true
|
9
|
+
end
|
10
|
+
|
11
|
+
def tuesday?
|
12
|
+
false
|
13
|
+
end
|
14
|
+
|
15
|
+
def day?(name)
|
16
|
+
name == "monday"
|
17
|
+
end
|
18
|
+
|
19
|
+
def season?(name:)
|
20
|
+
name == "summer"
|
21
|
+
end
|
22
|
+
|
23
|
+
def hello
|
24
|
+
"hello"
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.component_available?(name)
|
28
|
+
true
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
it "compiles simple components" do
|
34
|
+
evaluate("monday?").should == true
|
35
|
+
evaluate("tuesday?").should == false
|
36
|
+
end
|
37
|
+
|
38
|
+
it "compiles components with an identifier" do
|
39
|
+
evaluate("day.monday?").should == true
|
40
|
+
evaluate("day.tuesday?").should == false
|
41
|
+
end
|
42
|
+
|
43
|
+
it "compiles components with attributes" do
|
44
|
+
evaluate("season? name=summer").should == true
|
45
|
+
evaluate("season? name=winter").should == false
|
46
|
+
end
|
47
|
+
|
48
|
+
it "fails if the component is missing a question mark" do
|
49
|
+
expect { evaluate("hello") }.to raise_exception(Curly::Error)
|
50
|
+
end
|
51
|
+
|
52
|
+
def evaluate(component, &block)
|
53
|
+
method, argument, attributes = Curly::ComponentParser.parse(component)
|
54
|
+
code = Curly::ComponentCompiler.compile_conditional(presenter_class, method, argument, attributes)
|
55
|
+
presenter = presenter_class.new
|
56
|
+
context = double("context", presenter: presenter)
|
57
|
+
|
58
|
+
context.instance_eval(<<-RUBY)
|
59
|
+
def self.render
|
60
|
+
#{code}
|
61
|
+
end
|
62
|
+
RUBY
|
63
|
+
|
64
|
+
context.render(&block)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe ".compile_component" do
|
69
|
+
let(:presenter_class) do
|
70
|
+
Class.new do
|
71
|
+
def title
|
72
|
+
"Welcome!"
|
73
|
+
end
|
74
|
+
|
75
|
+
def i18n(key, fallback: nil)
|
76
|
+
case key
|
77
|
+
when "home.welcome" then "Welcome to our lovely place!"
|
78
|
+
else fallback
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def summary(length = "long")
|
83
|
+
case length
|
84
|
+
when "long" then "This is a long summary"
|
85
|
+
when "short" then "This is a short summary"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def invalid(x, y)
|
90
|
+
end
|
91
|
+
|
92
|
+
def widget(size:, color: nil)
|
93
|
+
s = "Widget (#{size})"
|
94
|
+
s << " - #{color}" if color
|
95
|
+
s
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.component_available?(name)
|
99
|
+
true
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
it "compiles components with identifiers" do
|
105
|
+
evaluate("i18n.home.welcome").should == "Welcome to our lovely place!"
|
106
|
+
end
|
107
|
+
|
108
|
+
it "compiles components with optional identifiers" do
|
109
|
+
evaluate("summary").should == "This is a long summary"
|
110
|
+
evaluate("summary.short").should == "This is a short summary"
|
111
|
+
end
|
112
|
+
|
113
|
+
it "compiles components with attributes" do
|
114
|
+
evaluate("widget size=100px").should == "Widget (100px)"
|
115
|
+
end
|
116
|
+
|
117
|
+
it "compiles components with optional attributes" do
|
118
|
+
evaluate("widget color=blue size=50px").should == "Widget (50px) - blue"
|
119
|
+
end
|
120
|
+
|
121
|
+
it "allows both identifier and attributes" do
|
122
|
+
evaluate("i18n.hello fallback=yolo").should == "yolo"
|
123
|
+
end
|
124
|
+
|
125
|
+
it "fails when an invalid attribute is used" do
|
126
|
+
expect { evaluate("i18n.foo extreme=true") }.to raise_exception(Curly::Error)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "fails when a component is missing a required identifier" do
|
130
|
+
expect { evaluate("i18n") }.to raise_exception(Curly::Error)
|
131
|
+
end
|
132
|
+
|
133
|
+
it "fails when a component is missing a required attribute" do
|
134
|
+
expect { evaluate("widget") }.to raise_exception(Curly::Error)
|
135
|
+
end
|
136
|
+
|
137
|
+
it "fails when an identifier is specified for a component that doesn't support one" do
|
138
|
+
expect { evaluate("title.rugby") }.to raise_exception(Curly::Error)
|
139
|
+
end
|
140
|
+
|
141
|
+
it "fails when the method takes more than one argument" do
|
142
|
+
expect { evaluate("invalid") }.to raise_exception(Curly::Error)
|
143
|
+
end
|
144
|
+
|
145
|
+
def evaluate(component, &block)
|
146
|
+
method, argument, attributes = Curly::ComponentParser.parse(component)
|
147
|
+
code = Curly::ComponentCompiler.compile_component(presenter_class, method, argument, attributes)
|
148
|
+
presenter = presenter_class.new
|
149
|
+
context = double("context", presenter: presenter)
|
150
|
+
|
151
|
+
context.instance_eval(<<-RUBY)
|
152
|
+
def self.render
|
153
|
+
#{code}
|
154
|
+
end
|
155
|
+
RUBY
|
156
|
+
|
157
|
+
context.render(&block)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|