curlybars 1.3.0 → 1.6.0

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/lib/curlybars.rb +0 -1
  3. data/lib/curlybars/configuration.rb +1 -9
  4. data/lib/curlybars/error/base.rb +2 -0
  5. data/lib/curlybars/lexer.rb +3 -0
  6. data/lib/curlybars/method_whitelist.rb +16 -11
  7. data/lib/curlybars/node/block_helper_else.rb +1 -0
  8. data/lib/curlybars/node/if_else.rb +1 -1
  9. data/lib/curlybars/node/path.rb +6 -0
  10. data/lib/curlybars/node/sub_expression.rb +62 -0
  11. data/lib/curlybars/node/unless_else.rb +1 -1
  12. data/lib/curlybars/parser.rb +11 -0
  13. data/lib/curlybars/processor/tilde.rb +3 -0
  14. data/lib/curlybars/rendering_support.rb +9 -1
  15. data/lib/curlybars/template_handler.rb +18 -6
  16. data/lib/curlybars/version.rb +1 -1
  17. data/lib/curlybars/visitor.rb +6 -0
  18. data/spec/acceptance/application_layout_spec.rb +2 -2
  19. data/spec/acceptance/collection_blocks_spec.rb +1 -1
  20. data/spec/acceptance/global_helper_spec.rb +1 -1
  21. data/spec/curlybars/lexer_spec.rb +25 -2
  22. data/spec/curlybars/method_whitelist_spec.rb +15 -0
  23. data/spec/curlybars/rendering_support_spec.rb +4 -9
  24. data/spec/curlybars/template_handler_spec.rb +33 -30
  25. data/spec/integration/cache_spec.rb +20 -18
  26. data/spec/integration/node/block_helper_else_spec.rb +0 -2
  27. data/spec/integration/node/each_else_spec.rb +0 -2
  28. data/spec/integration/node/each_spec.rb +0 -2
  29. data/spec/integration/node/helper_spec.rb +12 -2
  30. data/spec/integration/node/if_else_spec.rb +0 -2
  31. data/spec/integration/node/if_spec.rb +2 -4
  32. data/spec/integration/node/output_spec.rb +0 -2
  33. data/spec/integration/node/partial_spec.rb +0 -2
  34. data/spec/integration/node/path_spec.rb +0 -2
  35. data/spec/integration/node/root_spec.rb +0 -2
  36. data/spec/integration/node/sub_expression_spec.rb +426 -0
  37. data/spec/integration/node/template_spec.rb +0 -2
  38. data/spec/integration/node/unless_else_spec.rb +2 -4
  39. data/spec/integration/node/unless_spec.rb +0 -2
  40. data/spec/integration/node/with_spec.rb +0 -2
  41. data/spec/integration/processors_spec.rb +0 -1
  42. data/spec/integration/visitor_spec.rb +13 -5
  43. metadata +47 -15
@@ -9,7 +9,7 @@ describe "Collection blocks", type: :request do
9
9
  example "Render a global helper" do
10
10
  get '/welcome'
11
11
 
12
- expect(body).to eq(<<-HTML.strip_heredoc)
12
+ expect(body).to eq(<<~HTML)
13
13
  <html>
14
14
  <head>
15
15
  <title>Dummy app</title>
@@ -37,6 +37,7 @@ describe Curlybars::Lexer do
37
37
  it "is resilient to whitespaces" do
38
38
  expect(lex('{{! }}')).to produce []
39
39
  end
40
+
40
41
  it "is resilient to newlines" do
41
42
  expect(lex("{{!\n}}")).to produce []
42
43
  end
@@ -160,6 +161,28 @@ describe Curlybars::Lexer do
160
161
  end
161
162
  end
162
163
 
164
+ describe "{{path (path context options)}}" do
165
+ it "is lexed with path, context and options" do
166
+ expect(lex('{{path (path context key=value)}}')).to produce [:START, :PATH, :LPAREN, :PATH, :PATH, :KEY, :PATH, :RPAREN, :END]
167
+ end
168
+
169
+ it "is lexed without options" do
170
+ expect(lex('{{path (path context)}}')).to produce [:START, :PATH, :LPAREN, :PATH, :PATH, :RPAREN, :END]
171
+ end
172
+
173
+ it "is lexed without context" do
174
+ expect(lex('{{path (path key=value)}}')).to produce [:START, :PATH, :LPAREN, :PATH, :KEY, :PATH, :RPAREN, :END]
175
+ end
176
+
177
+ it "is lexed without context and options" do
178
+ expect(lex('{{path (path)}}')).to produce [:START, :PATH, :LPAREN, :PATH, :RPAREN, :END]
179
+ end
180
+
181
+ it "is lexed with a nested subexpression" do
182
+ expect(lex('{{path (path (path context key=value) key=value)}}')).to produce [:START, :PATH, :LPAREN, :PATH, :LPAREN, :PATH, :PATH, :KEY, :PATH, :RPAREN, :KEY, :PATH, :RPAREN, :END]
183
+ end
184
+ end
185
+
163
186
  describe "{{#if path}}...{{/if}}" do
164
187
  it "is lexed" do
165
188
  expect(lex('{{#if path}} text {{/if}}')).to produce(
@@ -180,7 +203,7 @@ describe Curlybars::Lexer do
180
203
  end
181
204
  end
182
205
 
183
- # rubocop:disable Layout/AlignArray
206
+ # rubocop:disable Layout/ArrayAlignment
184
207
  describe "{{#if path}}...{{else}}...{{/if}}" do
185
208
  it "is lexed" do
186
209
  expect(lex('{{#if path}} text {{else}} text {{/if}}')).to produce(
@@ -298,7 +321,7 @@ describe Curlybars::Lexer do
298
321
  )
299
322
  end
300
323
  end
301
- # rubocop:enable Layout/AlignArray
324
+ # rubocop:enable Layout/ArrayAlignment
302
325
 
303
326
  describe "{{#with path}}...{{/with}}" do
304
327
  it "is lexed" do
@@ -16,6 +16,8 @@ describe Curlybars::MethodWhitelist do
16
16
 
17
17
  let(:validation_context_class) do
18
18
  Class.new do
19
+ attr_accessor :invocation_count
20
+
19
21
  def foo?
20
22
  true
21
23
  end
@@ -130,10 +132,15 @@ describe Curlybars::MethodWhitelist do
130
132
 
131
133
  Class.new do
132
134
  extend Curlybars::MethodWhitelist
135
+ attr_accessor :invocation_count
136
+
133
137
  allow_methods :cook, link: LinkPresenter do |context, allow_method|
134
138
  if context.foo?
135
139
  allow_method.call(:bar)
136
140
  end
141
+
142
+ context.invocation_count ||= 0
143
+ context.invocation_count += 1
137
144
  end
138
145
 
139
146
  def foo?
@@ -173,6 +180,14 @@ describe Curlybars::MethodWhitelist do
173
180
  expect(post_presenter.new.allowed_methods).to eq([:cook, :link, :bar, :form, :foo_bar, :wave])
174
181
  end
175
182
 
183
+ it "only invokes the context block once" do
184
+ presenter = post_presenter.new
185
+
186
+ 10.times { presenter.allowed_methods }
187
+
188
+ expect(presenter.invocation_count).to eq(1)
189
+ end
190
+
176
191
  it "returns a dependency_tree with inheritance and composition with context" do
177
192
  expect(post_presenter.dependency_tree(validation_context_class.new)).
178
193
  to eq(
@@ -1,3 +1,4 @@
1
+ # rubocop:disable RSpec/MultipleMemoizedHelpers
1
2
  describe Curlybars::RenderingSupport do
2
3
  let(:file_name) { '/app/views/template.hbs' }
3
4
  let(:presenter) { double(:presenter, allows_method?: true, meth: :value) }
@@ -7,7 +8,6 @@ describe Curlybars::RenderingSupport do
7
8
  let(:position) do
8
9
  double(:position, file_name: 'template.hbs', line_number: 1, line_offset: 0)
9
10
  end
10
- let(:block) { -> {} }
11
11
 
12
12
  describe "#check_timeout!" do
13
13
  it "skips checking if timeout is nil" do
@@ -179,14 +179,6 @@ describe Curlybars::RenderingSupport do
179
179
  end
180
180
 
181
181
  describe "#cached_call" do
182
- before do
183
- class APresenter
184
- def meth
185
- :value
186
- end
187
- end
188
- end
189
-
190
182
  it "(cache miss) calls the method if not cached already" do
191
183
  meth = presenter.method(:meth)
192
184
  allow(meth).to receive(:call)
@@ -217,6 +209,8 @@ describe Curlybars::RenderingSupport do
217
209
  end
218
210
 
219
211
  describe "#call" do
212
+ let(:block) { -> {} }
213
+
220
214
  it "calls with no arguments a method with no parameters" do
221
215
  method = -> { :return }
222
216
  arguments = []
@@ -424,3 +418,4 @@ describe Curlybars::RenderingSupport do
424
418
  allow(presenter).to receive(:allows_method?).and_return(false)
425
419
  end
426
420
  end
421
+ # rubocop:enable RSpec/MultipleMemoizedHelpers
@@ -100,13 +100,11 @@ describe Curlybars::TemplateHandler do
100
100
  end
101
101
 
102
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)
103
+ output = render(<<~TEMPLATE)
104
+ # encoding: utf-8"
105
+ first line
106
+ TEMPLATE
107
+ expect(output).to eq(<<~TEMPLATE)
110
108
 
111
109
  first line
112
110
  TEMPLATE
@@ -114,55 +112,54 @@ describe Curlybars::TemplateHandler do
114
112
 
115
113
  it "passes in the presenter context to the presenter class" do
116
114
  allow(context).to receive(:bar).and_return("BAR")
117
- allow(template).to receive(:source).and_return("{{bar}}")
115
+ output = render("{{bar}}")
118
116
  expect(output).to eq("BAR")
119
117
  end
120
118
 
121
119
  it "fails if there's no matching presenter class" do
122
120
  allow(template).to receive(:virtual_path).and_return("missing")
123
- allow(template).to receive(:source).and_return(" FOO ")
124
- expect { output }.to raise_exception(Curlybars::Error::Presenter::NotFound)
121
+ expect { render(" FOO ") }.to raise_exception(Curlybars::Error::Presenter::NotFound)
125
122
  end
126
123
 
127
124
  it "allows calling public methods on the presenter" do
128
- allow(template).to receive(:source).and_return("{{foo}}")
125
+ output = render("{{foo}}")
129
126
  expect(output).to eq("FOO")
130
127
  end
131
128
 
132
129
  it "marks its output as HTML safe" do
133
- allow(template).to receive(:source).and_return("{{foo}}")
130
+ output = render("{{foo}}")
134
131
  expect(output).to be_html_safe
135
132
  end
136
133
 
137
134
  it "calls the #setup! method before rendering the view" do
138
- allow(template).to receive(:source).and_return("{{foo}}")
139
- output
135
+ render("{{foo}}")
140
136
  expect(context.content_for(:foo)).to eq("bar")
141
137
  end
142
138
 
143
139
  describe "caching" do
144
140
  before do
145
- allow(template).to receive(:source).and_return("{{bar}}")
146
141
  allow(context).to receive(:bar).and_return("BAR")
147
142
  end
148
143
 
144
+ let(:output) { -> { render("{{bar}}") } }
145
+
149
146
  it "caches the result with the #cache_key from the presenter" do
150
147
  context.assigns[:cache_key] = "x"
151
- expect(output).to eq("BAR")
148
+ expect(output.call).to eq("BAR")
152
149
 
153
150
  allow(context).to receive(:bar).and_return("BAZ")
154
- expect(output).to eq("BAR")
151
+ expect(output.call).to eq("BAR")
155
152
 
156
153
  context.assigns[:cache_key] = "y"
157
- expect(output).to eq("BAZ")
154
+ expect(output.call).to eq("BAZ")
158
155
  end
159
156
 
160
157
  it "doesn't cache when the cache key is nil" do
161
158
  context.assigns[:cache_key] = nil
162
- expect(output).to eq("BAR")
159
+ expect(output.call).to eq("BAR")
163
160
 
164
161
  allow(context).to receive(:bar).and_return("BAZ")
165
- expect(output).to eq("BAZ")
162
+ expect(output.call).to eq("BAZ")
166
163
  end
167
164
 
168
165
  it "adds the presenter class' cache key to the instance's cache key" do
@@ -171,51 +168,57 @@ describe Curlybars::TemplateHandler do
171
168
 
172
169
  allow(presenter_class).to receive(:cache_key).and_return("foo")
173
170
 
174
- expect(output).to eq("BAR")
171
+ expect(output.call).to eq("BAR")
175
172
 
176
173
  allow(presenter_class).to receive(:cache_key).and_return("bar")
177
174
 
178
175
  allow(context).to receive(:bar).and_return("FOOBAR")
179
- expect(output).to eq("FOOBAR")
176
+ expect(output.call).to eq("FOOBAR")
180
177
  end
181
178
 
182
179
  it "expires the cache keys after #cache_duration" do
183
180
  context.assigns[:cache_key] = "x"
184
181
  context.assigns[:cache_duration] = 42
185
182
 
186
- expect(output).to eq("BAR")
183
+ expect(output.call).to eq("BAR")
187
184
 
188
185
  allow(context).to receive(:bar).and_return("FOO")
189
186
 
190
187
  # Cached fragment has not yet expired.
191
188
  context.advance_clock(41)
192
- expect(output).to eq("BAR")
189
+ expect(output.call).to eq("BAR")
193
190
 
194
191
  # Now it has! Huzzah!
195
192
  context.advance_clock(1)
196
- expect(output).to eq("FOO")
193
+ expect(output.call).to eq("FOO")
197
194
  end
198
195
 
199
196
  it "passes #cache_options to the cache backend" do
200
197
  context.assigns[:cache_key] = "x"
201
198
  context.assigns[:cache_options] = { expires_in: 42 }
202
199
 
203
- expect(output).to eq("BAR")
200
+ expect(output.call).to eq("BAR")
204
201
 
205
202
  allow(context).to receive(:bar).and_return("FOO")
206
203
 
207
204
  # Cached fragment has not yet expired.
208
205
  context.advance_clock(41)
209
- expect(output).to eq("BAR")
206
+ expect(output.call).to eq("BAR")
210
207
 
211
208
  # Now it has! Huzzah!
212
209
  context.advance_clock(1)
213
- expect(output).to eq("FOO")
210
+ expect(output.call).to eq("FOO")
214
211
  end
215
212
  end
216
213
 
217
- def output
218
- code = Curlybars::TemplateHandler.call(template)
214
+ def render(source)
215
+ if ActionView::VERSION::MAJOR < 6
216
+ allow(template).to receive(:source).and_return(source)
217
+ code = Curlybars::TemplateHandler.call(template)
218
+ else
219
+ code = Curlybars::TemplateHandler.call(template, source)
220
+ end
221
+
219
222
  context.reset!
220
223
  context.instance_eval(code)
221
224
  end
@@ -1,29 +1,31 @@
1
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
2
+ let(:dummy_cache) do
3
+ Class.new do
4
+ attr_reader :reads, :hits
5
+
6
+ def initialize
7
+ @store = {}
8
+ @reads = 0
9
+ @hits = 0
10
+ end
10
11
 
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
12
+ def fetch(key)
13
+ @reads += 1
14
+ if @store.key?(key)
15
+ @hits += 1
16
+ @store[key]
17
+ else
18
+ value = yield
19
+ @store[key] = value
20
+ value
21
+ end
20
22
  end
21
23
  end
22
24
  end
23
25
 
24
26
  let(:global_helpers_providers) { [] }
25
27
  let(:presenter) { IntegrationTest::Presenter.new(double("view_context")) }
26
- let(:cache) { DummyCache.new }
28
+ let(:cache) { dummy_cache.new }
27
29
 
28
30
  before do
29
31
  Curlybars.configure do |config|
@@ -230,8 +230,6 @@ describe "{{#helper arg1 arg2 ... key=value ...}}...<{{else}}>...{{/helper}}" do
230
230
  end
231
231
 
232
232
  describe "#validate" do
233
- let(:presenter_class) { double(:presenter_class) }
234
-
235
233
  it "without errors when global helper" do
236
234
  allow(Curlybars.configuration).to receive(:global_helpers_provider_classes).and_return([IntegrationTest::GlobalHelperProvider])
237
235
 
@@ -151,8 +151,6 @@ describe "{{#each collection}}...{{else}}...{{/each}}" do
151
151
  end
152
152
 
153
153
  describe "#validate" do
154
- let(:presenter_class) { double(:presenter_class) }
155
-
156
154
  it "without errors" do
157
155
  dependency_tree = { a_presenter_collection: [{}] }
158
156
 
@@ -238,8 +238,6 @@ describe "{{#each collection}}...{{/each}}" do
238
238
  end
239
239
 
240
240
  describe "#validate" do
241
- let(:presenter_class) { double(:presenter_class) }
242
-
243
241
  it "without errors" do
244
242
  dependency_tree = { a_presenter_collection: [{}] }
245
243
 
@@ -15,6 +15,18 @@ describe "{{helper context key=value}}" do
15
15
  HTML
16
16
  end
17
17
 
18
+ it "calls a helper without arguments in an if statement" do
19
+ template = Curlybars.compile(<<-HBS)
20
+ {{#if print_args_and_options}}
21
+ {{print_args_and_options 'first' 'second'}}
22
+ {{/if}}
23
+ HBS
24
+
25
+ expect(eval(template)).to resemble(<<-HTML)
26
+ first, second, key=
27
+ HTML
28
+ end
29
+
18
30
  it "passes two arguments and options" do
19
31
  template = Curlybars.compile(<<-HBS)
20
32
  {{print_args_and_options 'first' 'second' key='value'}}
@@ -109,8 +121,6 @@ describe "{{helper context key=value}}" do
109
121
  end
110
122
 
111
123
  describe "#validate" do
112
- let(:presenter_class) { double(:presenter_class) }
113
-
114
124
  it "with errors" do
115
125
  dependency_tree = {}
116
126
 
@@ -82,8 +82,6 @@ describe "{{#if}}...{{else}}...{{/if}}" do
82
82
  end
83
83
 
84
84
  describe "#validate" do
85
- let(:presenter_class) { double(:presenter_class) }
86
-
87
85
  it "validates without errors the literal condition" do
88
86
  dependency_tree = {}
89
87
 
@@ -112,8 +112,6 @@ describe "{{#if}}...{{/if}}" do
112
112
  end
113
113
 
114
114
  describe "#validate" do
115
- let(:presenter_class) { double(:presenter_class) }
116
-
117
115
  it "validates with errors the condition" do
118
116
  dependency_tree = {}
119
117
 
@@ -140,7 +138,7 @@ describe "{{#if}}...{{/if}}" do
140
138
  expect(errors).not_to be_empty
141
139
  end
142
140
 
143
- it "validates with errors the helper as condition" do
141
+ it "validates without errors the helper as condition" do
144
142
  dependency_tree = { helper: :helper }
145
143
 
146
144
  source = <<-HBS
@@ -149,7 +147,7 @@ describe "{{#if}}...{{/if}}" do
149
147
 
150
148
  errors = Curlybars.validate(dependency_tree, source)
151
149
 
152
- expect(errors).not_to be_empty
150
+ expect(errors).to be_empty
153
151
  end
154
152
  end
155
153
  end
@@ -51,8 +51,6 @@ describe '{{value}}' do
51
51
  end
52
52
 
53
53
  describe "#validate" do
54
- let(:presenter_class) { double(:presenter_class) }
55
-
56
54
  it "validates the path with errors" do
57
55
  dependency_tree = {}
58
56