curlybars 0.9.13
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 +7 -0
- data/lib/curlybars.rb +108 -0
- data/lib/curlybars/configuration.rb +41 -0
- data/lib/curlybars/dependency_tracker.rb +8 -0
- data/lib/curlybars/error/base.rb +18 -0
- data/lib/curlybars/error/compile.rb +11 -0
- data/lib/curlybars/error/lex.rb +22 -0
- data/lib/curlybars/error/parse.rb +41 -0
- data/lib/curlybars/error/presenter/not_found.rb +23 -0
- data/lib/curlybars/error/render.rb +11 -0
- data/lib/curlybars/error/validate.rb +18 -0
- data/lib/curlybars/lexer.rb +60 -0
- data/lib/curlybars/method_whitelist.rb +69 -0
- data/lib/curlybars/node/block_helper_else.rb +108 -0
- data/lib/curlybars/node/boolean.rb +24 -0
- data/lib/curlybars/node/each_else.rb +69 -0
- data/lib/curlybars/node/if_else.rb +33 -0
- data/lib/curlybars/node/item.rb +31 -0
- data/lib/curlybars/node/literal.rb +28 -0
- data/lib/curlybars/node/option.rb +25 -0
- data/lib/curlybars/node/output.rb +24 -0
- data/lib/curlybars/node/partial.rb +24 -0
- data/lib/curlybars/node/path.rb +137 -0
- data/lib/curlybars/node/root.rb +29 -0
- data/lib/curlybars/node/string.rb +24 -0
- data/lib/curlybars/node/template.rb +32 -0
- data/lib/curlybars/node/text.rb +24 -0
- data/lib/curlybars/node/unless_else.rb +33 -0
- data/lib/curlybars/node/variable.rb +34 -0
- data/lib/curlybars/node/with_else.rb +54 -0
- data/lib/curlybars/parser.rb +183 -0
- data/lib/curlybars/position.rb +7 -0
- data/lib/curlybars/presenter.rb +288 -0
- data/lib/curlybars/processor/tilde.rb +31 -0
- data/lib/curlybars/processor/token_factory.rb +9 -0
- data/lib/curlybars/railtie.rb +18 -0
- data/lib/curlybars/rendering_support.rb +222 -0
- data/lib/curlybars/safe_buffer.rb +11 -0
- data/lib/curlybars/template_handler.rb +93 -0
- data/lib/curlybars/version.rb +3 -0
- data/spec/acceptance/application_layout_spec.rb +60 -0
- data/spec/acceptance/collection_blocks_spec.rb +28 -0
- data/spec/acceptance/global_helper_spec.rb +25 -0
- data/spec/curlybars/configuration_spec.rb +57 -0
- data/spec/curlybars/error/base_spec.rb +41 -0
- data/spec/curlybars/error/compile_spec.rb +19 -0
- data/spec/curlybars/error/lex_spec.rb +25 -0
- data/spec/curlybars/error/parse_spec.rb +74 -0
- data/spec/curlybars/error/render_spec.rb +19 -0
- data/spec/curlybars/error/validate_spec.rb +19 -0
- data/spec/curlybars/lexer_spec.rb +466 -0
- data/spec/curlybars/method_whitelist_spec.rb +168 -0
- data/spec/curlybars/processor/tilde_spec.rb +60 -0
- data/spec/curlybars/rendering_support_spec.rb +426 -0
- data/spec/curlybars/safe_buffer_spec.rb +46 -0
- data/spec/curlybars/template_handler_spec.rb +222 -0
- data/spec/integration/cache_spec.rb +124 -0
- data/spec/integration/comment_spec.rb +60 -0
- data/spec/integration/exception_spec.rb +31 -0
- data/spec/integration/node/block_helper_else_spec.rb +422 -0
- data/spec/integration/node/each_else_spec.rb +204 -0
- data/spec/integration/node/each_spec.rb +291 -0
- data/spec/integration/node/escape_spec.rb +27 -0
- data/spec/integration/node/helper_spec.rb +176 -0
- data/spec/integration/node/if_else_spec.rb +129 -0
- data/spec/integration/node/if_spec.rb +143 -0
- data/spec/integration/node/output_spec.rb +68 -0
- data/spec/integration/node/partial_spec.rb +66 -0
- data/spec/integration/node/path_spec.rb +286 -0
- data/spec/integration/node/root_spec.rb +15 -0
- data/spec/integration/node/template_spec.rb +86 -0
- data/spec/integration/node/unless_else_spec.rb +129 -0
- data/spec/integration/node/unless_spec.rb +130 -0
- data/spec/integration/node/with_spec.rb +116 -0
- data/spec/integration/processor/tilde_spec.rb +38 -0
- data/spec/integration/processors_spec.rb +30 -0
- metadata +358 -0
@@ -0,0 +1,46 @@
|
|
1
|
+
describe Curlybars::SafeBuffer do
|
2
|
+
let(:configuration) { double(:configuration) }
|
3
|
+
|
4
|
+
before do
|
5
|
+
allow(Curlybars).to receive(:configuration) { configuration }
|
6
|
+
end
|
7
|
+
|
8
|
+
describe '#is_a?' do
|
9
|
+
it "is a String" do
|
10
|
+
expect(Curlybars::SafeBuffer.new).to be_a(String)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "is a ActiveSupport::SafeBuffer" do
|
14
|
+
expect(Curlybars::SafeBuffer.new).to be_a(ActiveSupport::SafeBuffer)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe "#concat" do
|
19
|
+
it "accepts when (buffer length + the existing output lenght) <= output_limit" do
|
20
|
+
allow(configuration).to receive(:output_limit) { 10 }
|
21
|
+
|
22
|
+
buffer = Curlybars::SafeBuffer.new('*' * 5)
|
23
|
+
|
24
|
+
expect do
|
25
|
+
buffer.concat('*' * 5)
|
26
|
+
end.not_to raise_error
|
27
|
+
end
|
28
|
+
|
29
|
+
it "raises when (buffer length + the existing output lenght) > output_limit" do
|
30
|
+
allow(configuration).to receive(:output_limit) { 10 }
|
31
|
+
buffer = Curlybars::SafeBuffer.new('*' * 10)
|
32
|
+
|
33
|
+
expect do
|
34
|
+
buffer.concat('*')
|
35
|
+
end.to raise_error(Curlybars::Error::Render)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "raises when buffer length > output_limit" do
|
39
|
+
allow(configuration).to receive(:output_limit) { 10 }
|
40
|
+
|
41
|
+
expect do
|
42
|
+
Curlybars::SafeBuffer.new.concat('*' * 11)
|
43
|
+
end.to raise_error(Curlybars::Error::Render)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,222 @@
|
|
1
|
+
describe Curlybars::TemplateHandler do
|
2
|
+
let :presenter_class do
|
3
|
+
Class.new do
|
4
|
+
def initialize(context, options = {})
|
5
|
+
@context = context
|
6
|
+
@cache_key = options.fetch(:cache_key, nil)
|
7
|
+
@cache_duration = options.fetch(:cache_duration, nil)
|
8
|
+
@cache_options = options.fetch(:cache_options, {})
|
9
|
+
end
|
10
|
+
|
11
|
+
def setup!
|
12
|
+
@context.content_for(:foo, "bar")
|
13
|
+
end
|
14
|
+
|
15
|
+
def foo
|
16
|
+
"FOO"
|
17
|
+
end
|
18
|
+
|
19
|
+
def bar
|
20
|
+
@context.bar
|
21
|
+
end
|
22
|
+
|
23
|
+
def cache_key
|
24
|
+
@cache_key
|
25
|
+
end
|
26
|
+
|
27
|
+
def cache_duration
|
28
|
+
@cache_duration
|
29
|
+
end
|
30
|
+
|
31
|
+
def cache_options
|
32
|
+
@cache_options
|
33
|
+
end
|
34
|
+
|
35
|
+
def allows_method?(method)
|
36
|
+
true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
let :context_class do
|
42
|
+
Class.new do
|
43
|
+
attr_reader :output_buffer
|
44
|
+
attr_reader :local_assigns, :assigns
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
@cache = Hash.new
|
48
|
+
@local_assigns = Hash.new
|
49
|
+
@assigns = Hash.new
|
50
|
+
@clock = 0
|
51
|
+
end
|
52
|
+
|
53
|
+
def reset!
|
54
|
+
@output_buffer = ActiveSupport::SafeBuffer.new
|
55
|
+
end
|
56
|
+
|
57
|
+
def advance_clock(duration)
|
58
|
+
@clock += duration
|
59
|
+
end
|
60
|
+
|
61
|
+
def content_for(key, value = nil)
|
62
|
+
@contents ||= {}
|
63
|
+
@contents[key] = value if value.present?
|
64
|
+
@contents[key]
|
65
|
+
end
|
66
|
+
|
67
|
+
def cache(key, options = {})
|
68
|
+
fragment, expired_at = @cache[key]
|
69
|
+
|
70
|
+
if fragment.nil? || @clock >= expired_at
|
71
|
+
old_buffer = @output_buffer
|
72
|
+
@output_buffer = ActiveSupport::SafeBuffer.new
|
73
|
+
|
74
|
+
yield
|
75
|
+
|
76
|
+
fragment = @output_buffer.to_s
|
77
|
+
duration = options[:expires_in] || Float::INFINITY
|
78
|
+
|
79
|
+
@cache[key] = [fragment, @clock + duration]
|
80
|
+
|
81
|
+
@output_buffer = old_buffer
|
82
|
+
end
|
83
|
+
|
84
|
+
safe_concat(fragment)
|
85
|
+
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
|
89
|
+
def safe_concat(str)
|
90
|
+
@output_buffer.safe_concat(str)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
let(:template) { double("template", virtual_path: "test", identifier: "test.hbs") }
|
96
|
+
let(:context) { context_class.new }
|
97
|
+
|
98
|
+
before do
|
99
|
+
stub_const("TestPresenter", presenter_class)
|
100
|
+
end
|
101
|
+
|
102
|
+
it "strips the `# encoding: *` directive away from the template" do
|
103
|
+
allow(template).to receive(:source) do
|
104
|
+
<<-TEMPLATE.strip_heredoc
|
105
|
+
# encoding: utf-8"
|
106
|
+
first line
|
107
|
+
TEMPLATE
|
108
|
+
end
|
109
|
+
expect(output).to eq(<<-TEMPLATE.strip_heredoc)
|
110
|
+
|
111
|
+
first line
|
112
|
+
TEMPLATE
|
113
|
+
end
|
114
|
+
|
115
|
+
it "passes in the presenter context to the presenter class" do
|
116
|
+
allow(context).to receive(:bar) { "BAR" }
|
117
|
+
allow(template).to receive(:source) { "{{bar}}" }
|
118
|
+
expect(output).to eq("BAR")
|
119
|
+
end
|
120
|
+
|
121
|
+
it "fails if there's no matching presenter class" do
|
122
|
+
allow(template).to receive(:virtual_path) { "missing" }
|
123
|
+
allow(template).to receive(:source) { " FOO " }
|
124
|
+
expect { output }.to raise_exception(Curlybars::Error::Presenter::NotFound)
|
125
|
+
end
|
126
|
+
|
127
|
+
it "allows calling public methods on the presenter" do
|
128
|
+
allow(template).to receive(:source) { "{{foo}}" }
|
129
|
+
expect(output).to eq("FOO")
|
130
|
+
end
|
131
|
+
|
132
|
+
it "marks its output as HTML safe" do
|
133
|
+
allow(template).to receive(:source) { "{{foo}}" }
|
134
|
+
expect(output).to be_html_safe
|
135
|
+
end
|
136
|
+
|
137
|
+
it "calls the #setup! method before rendering the view" do
|
138
|
+
allow(template).to receive(:source) { "{{foo}}" }
|
139
|
+
output
|
140
|
+
expect(context.content_for(:foo)).to eq("bar")
|
141
|
+
end
|
142
|
+
|
143
|
+
context "caching" do
|
144
|
+
before do
|
145
|
+
allow(template).to receive(:source) { "{{bar}}" }
|
146
|
+
allow(context).to receive(:bar) { "BAR" }
|
147
|
+
end
|
148
|
+
|
149
|
+
it "caches the result with the #cache_key from the presenter" do
|
150
|
+
context.assigns[:cache_key] = "x"
|
151
|
+
expect(output).to eq("BAR")
|
152
|
+
|
153
|
+
allow(context).to receive(:bar) { "BAZ" }
|
154
|
+
expect(output).to eq("BAR")
|
155
|
+
|
156
|
+
context.assigns[:cache_key] = "y"
|
157
|
+
expect(output).to eq("BAZ")
|
158
|
+
end
|
159
|
+
|
160
|
+
it "doesn't cache when the cache key is nil" do
|
161
|
+
context.assigns[:cache_key] = nil
|
162
|
+
expect(output).to eq("BAR")
|
163
|
+
|
164
|
+
allow(context).to receive(:bar) { "BAZ" }
|
165
|
+
expect(output).to eq("BAZ")
|
166
|
+
end
|
167
|
+
|
168
|
+
it "adds the presenter class' cache key to the instance's cache key" do
|
169
|
+
# Make sure caching is enabled
|
170
|
+
context.assigns[:cache_key] = "x"
|
171
|
+
|
172
|
+
allow(presenter_class).to receive(:cache_key) { "foo" }
|
173
|
+
|
174
|
+
expect(output).to eq("BAR")
|
175
|
+
|
176
|
+
allow(presenter_class).to receive(:cache_key) { "bar" }
|
177
|
+
|
178
|
+
allow(context).to receive(:bar) { "FOOBAR" }
|
179
|
+
expect(output).to eq("FOOBAR")
|
180
|
+
end
|
181
|
+
|
182
|
+
it "expires the cache keys after #cache_duration" do
|
183
|
+
context.assigns[:cache_key] = "x"
|
184
|
+
context.assigns[:cache_duration] = 42
|
185
|
+
|
186
|
+
expect(output).to eq("BAR")
|
187
|
+
|
188
|
+
allow(context).to receive(:bar) { "FOO" }
|
189
|
+
|
190
|
+
# Cached fragment has not yet expired.
|
191
|
+
context.advance_clock(41)
|
192
|
+
expect(output).to eq("BAR")
|
193
|
+
|
194
|
+
# Now it has! Huzzah!
|
195
|
+
context.advance_clock(1)
|
196
|
+
expect(output).to eq("FOO")
|
197
|
+
end
|
198
|
+
|
199
|
+
it "passes #cache_options to the cache backend" do
|
200
|
+
context.assigns[:cache_key] = "x"
|
201
|
+
context.assigns[:cache_options] = { expires_in: 42 }
|
202
|
+
|
203
|
+
expect(output).to eq("BAR")
|
204
|
+
|
205
|
+
allow(context).to receive(:bar) { "FOO" }
|
206
|
+
|
207
|
+
# Cached fragment has not yet expired.
|
208
|
+
context.advance_clock(41)
|
209
|
+
expect(output).to eq("BAR")
|
210
|
+
|
211
|
+
# Now it has! Huzzah!
|
212
|
+
context.advance_clock(1)
|
213
|
+
expect(output).to eq("FOO")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
def output
|
218
|
+
code = Curlybars::TemplateHandler.call(template)
|
219
|
+
context.reset!
|
220
|
+
context.instance_eval(code)
|
221
|
+
end
|
222
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
describe "caching" do
|
2
|
+
class DummyCache
|
3
|
+
attr_reader :reads, :hits
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@store = {}
|
7
|
+
@reads = 0
|
8
|
+
@hits = 0
|
9
|
+
end
|
10
|
+
|
11
|
+
def fetch(key)
|
12
|
+
@reads += 1
|
13
|
+
if @store.key?(key)
|
14
|
+
@hits += 1
|
15
|
+
@store[key]
|
16
|
+
else
|
17
|
+
value = yield
|
18
|
+
@store[key] = value
|
19
|
+
value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:global_helpers_providers) { [] }
|
25
|
+
let(:presenter) { IntegrationTest::Presenter.new(double("view_context")) }
|
26
|
+
let(:cache) { DummyCache.new }
|
27
|
+
|
28
|
+
before do
|
29
|
+
Curlybars.configure do |config|
|
30
|
+
config.cache = cache.method(:fetch)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
after do
|
35
|
+
Curlybars.reset
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "{{#each}}" do
|
39
|
+
it "invokes cache if presenter responds to #cache_key" do
|
40
|
+
template = Curlybars.compile(<<-HBS)
|
41
|
+
{{#each array_of_users}}{{/each}}
|
42
|
+
HBS
|
43
|
+
|
44
|
+
eval(template)
|
45
|
+
|
46
|
+
expect(cache.reads).to eq(1)
|
47
|
+
expect(cache.hits).to eq(0)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "reuses cached values" do
|
51
|
+
template = Curlybars.compile(<<-HBS)
|
52
|
+
{{#each array_of_users}}
|
53
|
+
a
|
54
|
+
{{/each}}
|
55
|
+
|
56
|
+
{{#each array_of_users}}
|
57
|
+
a
|
58
|
+
{{/each}}
|
59
|
+
HBS
|
60
|
+
|
61
|
+
eval(template)
|
62
|
+
|
63
|
+
expect(cache.reads).to eq(2)
|
64
|
+
expect(cache.hits).to eq(1)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "generates unique cache keys per template" do
|
68
|
+
template = Curlybars.compile(<<-HBS)
|
69
|
+
{{#each array_of_users}}
|
70
|
+
a
|
71
|
+
{{/each}}
|
72
|
+
|
73
|
+
{{#each array_of_users}}
|
74
|
+
b
|
75
|
+
{{/each}}
|
76
|
+
HBS
|
77
|
+
|
78
|
+
eval(template)
|
79
|
+
|
80
|
+
expect(cache.reads).to eq(2)
|
81
|
+
expect(cache.hits).to eq(0)
|
82
|
+
end
|
83
|
+
|
84
|
+
it "produces correct output from cached presenters" do
|
85
|
+
template = Curlybars.compile(<<-HBS)
|
86
|
+
{{#each array_of_users}}
|
87
|
+
- {{first_name}}
|
88
|
+
{{/each}}
|
89
|
+
HBS
|
90
|
+
|
91
|
+
expect(eval(template)).to resemble(<<-HTML)
|
92
|
+
- Libo
|
93
|
+
HTML
|
94
|
+
end
|
95
|
+
|
96
|
+
it "works for empty templates" do
|
97
|
+
template = Curlybars.compile(<<-HBS)
|
98
|
+
before
|
99
|
+
{{#each array_of_users}}{{/each}}
|
100
|
+
{{#each array_of_users}}{{/each}}
|
101
|
+
after
|
102
|
+
HBS
|
103
|
+
|
104
|
+
expect(eval(template)).to resemble(<<-HTML)
|
105
|
+
before
|
106
|
+
after
|
107
|
+
HTML
|
108
|
+
end
|
109
|
+
|
110
|
+
it "leaves variables and contexts in correct state after a cache hit" do
|
111
|
+
template = Curlybars.compile(<<-HBS)
|
112
|
+
{{#each array_of_users}}a{{/each}}
|
113
|
+
{{#each array_of_users}}a{{/each}}
|
114
|
+
{{context}}
|
115
|
+
HBS
|
116
|
+
|
117
|
+
expect(eval(template)).to resemble(<<-HTML)
|
118
|
+
a
|
119
|
+
a
|
120
|
+
root_context
|
121
|
+
HTML
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
describe "{{!-- --}} and {{! }}" do
|
2
|
+
let(:post) { double("post") }
|
3
|
+
let(:presenter) { IntegrationTest::Presenter.new(double("view_context"), post: post) }
|
4
|
+
let(:global_helpers_providers) { [] }
|
5
|
+
|
6
|
+
it "ignores one line comment" do
|
7
|
+
template = Curlybars.compile(<<-HBS)
|
8
|
+
before{{! This is a comment }}after
|
9
|
+
HBS
|
10
|
+
|
11
|
+
expect(eval(template)).to resemble(<<-HTML)
|
12
|
+
before after
|
13
|
+
HTML
|
14
|
+
end
|
15
|
+
|
16
|
+
it "ignores multi line comment" do
|
17
|
+
template = Curlybars.compile(<<-HBS)
|
18
|
+
before
|
19
|
+
{{! 2 lines
|
20
|
+
lines }}
|
21
|
+
after
|
22
|
+
HBS
|
23
|
+
|
24
|
+
expect(eval(template)).to resemble(<<-HTML)
|
25
|
+
before after
|
26
|
+
HTML
|
27
|
+
end
|
28
|
+
|
29
|
+
it "ignores multi lines with curly inside comment" do
|
30
|
+
template = Curlybars.compile(<<-HBS)
|
31
|
+
before
|
32
|
+
{{!
|
33
|
+
And another one
|
34
|
+
in
|
35
|
+
3 lines
|
36
|
+
}
|
37
|
+
}}
|
38
|
+
after
|
39
|
+
HBS
|
40
|
+
|
41
|
+
expect(eval(template)).to resemble(<<-HTML)
|
42
|
+
before after
|
43
|
+
HTML
|
44
|
+
end
|
45
|
+
|
46
|
+
it "ignores multi line comment with {{!-- --}}" do
|
47
|
+
template = Curlybars.compile(<<-HBS)
|
48
|
+
before
|
49
|
+
{{!--
|
50
|
+
And this is the {{ test }} other style
|
51
|
+
}}
|
52
|
+
--}}
|
53
|
+
after
|
54
|
+
HBS
|
55
|
+
|
56
|
+
expect(eval(template)).to resemble(<<-HTML)
|
57
|
+
before after
|
58
|
+
HTML
|
59
|
+
end
|
60
|
+
end
|