flavour_saver 0.3.3

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.travis.yml +11 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +81 -0
  6. data/Guardfile +12 -0
  7. data/LICENSE +22 -0
  8. data/README.md +339 -0
  9. data/Rakefile +2 -0
  10. data/flavour_saver.gemspec +28 -0
  11. data/lib/flavour_saver/helpers.rb +118 -0
  12. data/lib/flavour_saver/lexer.rb +127 -0
  13. data/lib/flavour_saver/nodes.rb +177 -0
  14. data/lib/flavour_saver/parser.rb +183 -0
  15. data/lib/flavour_saver/partial.rb +29 -0
  16. data/lib/flavour_saver/rails_partial.rb +10 -0
  17. data/lib/flavour_saver/runtime.rb +269 -0
  18. data/lib/flavour_saver/template.rb +19 -0
  19. data/lib/flavour_saver/version.rb +3 -0
  20. data/lib/flavour_saver.rb +78 -0
  21. data/spec/acceptance/backtrack_spec.rb +14 -0
  22. data/spec/acceptance/comment_spec.rb +12 -0
  23. data/spec/acceptance/custom_block_helper_spec.rb +35 -0
  24. data/spec/acceptance/custom_helper_spec.rb +15 -0
  25. data/spec/acceptance/ensure_no_rce_spec.rb +26 -0
  26. data/spec/acceptance/handlebars_qunit_spec.rb +911 -0
  27. data/spec/acceptance/if_else_spec.rb +17 -0
  28. data/spec/acceptance/multi_level_with_spec.rb +15 -0
  29. data/spec/acceptance/one_character_identifier_spec.rb +13 -0
  30. data/spec/acceptance/runtime_run_spec.rb +27 -0
  31. data/spec/acceptance/sections_spec.rb +25 -0
  32. data/spec/acceptance/segment_literals_spec.rb +26 -0
  33. data/spec/acceptance/simple_expression_spec.rb +13 -0
  34. data/spec/fixtures/backtrack.hbs +4 -0
  35. data/spec/fixtures/comment.hbs +1 -0
  36. data/spec/fixtures/custom_block_helper.hbs +3 -0
  37. data/spec/fixtures/custom_helper.hbs +1 -0
  38. data/spec/fixtures/if_else.hbs +5 -0
  39. data/spec/fixtures/multi_level_if.hbs +12 -0
  40. data/spec/fixtures/multi_level_with.hbs +11 -0
  41. data/spec/fixtures/one_character_identifier.hbs +1 -0
  42. data/spec/fixtures/sections.hbs +9 -0
  43. data/spec/fixtures/simple_expression.hbs +1 -0
  44. data/spec/lib/flavour_saver/lexer_spec.rb +187 -0
  45. data/spec/lib/flavour_saver/parser_spec.rb +277 -0
  46. data/spec/lib/flavour_saver/runtime_spec.rb +190 -0
  47. data/spec/lib/flavour_saver/template_spec.rb +5 -0
  48. metadata +243 -0
@@ -0,0 +1,19 @@
1
+ require 'tilt/template'
2
+
3
+ module FlavourSaver
4
+ class Template < Tilt::Template
5
+
6
+ def self.engine_initialized?
7
+ true
8
+ end
9
+
10
+ def prepare
11
+ @ast = Parser.parse(Lexer.lex(data))
12
+ end
13
+
14
+ def evaluate(scope=Object.new,locals={},&block)
15
+ Runtime.run(@ast,scope,locals)
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,3 @@
1
+ module FlavourSaver
2
+ VERSION = "0.3.3"
3
+ end
@@ -0,0 +1,78 @@
1
+ require "flavour_saver/version"
2
+ require 'tilt'
3
+
4
+ module FlavourSaver
5
+
6
+ autoload :Lexer, 'flavour_saver/lexer'
7
+ autoload :Parser, 'flavour_saver/parser'
8
+ autoload :Runtime, 'flavour_saver/runtime'
9
+ autoload :Helpers, 'flavour_saver/helpers'
10
+ autoload :Partial, 'flavour_saver/partial'
11
+ autoload :RailsPartial, 'flavour_saver/rails_partial'
12
+ autoload :Template, 'flavour_saver/template'
13
+ autoload :NodeCollection, 'flavour_saver/node_collection'
14
+
15
+ if defined? Rails
16
+ class Engine < Rails::Engine
17
+ end
18
+
19
+ ActiveSupport.on_load(:action_view) do
20
+ handler = proc do |template|
21
+ # I'd rather be caching the Runtime object ready to fire, but apparently I don't get that luxury.
22
+ <<-SOURCE
23
+ FlavourSaver.evaluate((begin;#{template.source.inspect};end),self)
24
+ SOURCE
25
+ end
26
+ ActionView::Template.register_template_handler(:hbs, handler)
27
+ ActionView::Template.register_template_handler(:handlebars, handler)
28
+ end
29
+
30
+ @default_logger = proc { Rails.logger }
31
+ @partial_handler = RailsPartial
32
+ else
33
+ @default_logger = proc { Logger.new }
34
+ @partial_handler = Partial
35
+ end
36
+
37
+ module_function
38
+
39
+ def lex(template)
40
+ Lexer.lex(template)
41
+ end
42
+
43
+ def parse(tokens)
44
+ Parser.parse(tokens)
45
+ end
46
+
47
+ def evaluate(template,context)
48
+ Runtime.run(parse(lex(template)), context)
49
+ end
50
+
51
+ def register_helper(*args,&b)
52
+ Helpers.register_helper(*args,&b)
53
+ end
54
+
55
+ def reset_helpers
56
+ Helpers.reset_helpers
57
+ end
58
+
59
+ def register_partial(name,content=nil,&block)
60
+ Partial.register_partial(name,content,&block)
61
+ end
62
+
63
+ def reset_partials
64
+ Partial.reset_partials
65
+ end
66
+
67
+ def logger
68
+ @logger || @default_logger.call
69
+ end
70
+
71
+ def logger=(logger)
72
+ @logger=logger
73
+ end
74
+
75
+ Tilt.register(Template, 'handlebars', 'hbs')
76
+ end
77
+
78
+ FS = FlavourSaver
@@ -0,0 +1,14 @@
1
+ require 'tilt'
2
+ require 'flavour_saver'
3
+
4
+ describe 'Fixture: backtrack.hbs' do
5
+ subject { Tilt.new(template).render(context).gsub(/[\s\r\n]+/, ' ').strip }
6
+ let(:template) { File.expand_path('../../fixtures/backtrack.hbs', __FILE__) }
7
+ let(:context) { Struct.new(:person,:company).new }
8
+
9
+ it 'renders correctly' do
10
+ context.person = Struct.new(:name).new('Alan')
11
+ context.company = Struct.new(:name).new('Rad, Inc.')
12
+ subject.should == "Alan - Rad, Inc."
13
+ end
14
+ end
@@ -0,0 +1,12 @@
1
+ require 'tilt'
2
+ require 'flavour_saver'
3
+
4
+ describe 'Fixture: comment.hbs' do
5
+ subject { Tilt.new(template).render(context).gsub(/[\s\r\n]+/, ' ').strip }
6
+ let(:template) { File.expand_path('../../fixtures/comment.hbs', __FILE__) }
7
+ let(:context) { double(:context) }
8
+
9
+ it 'renders correctly' do
10
+ subject.should == "I am a very nice person!"
11
+ end
12
+ end
@@ -0,0 +1,35 @@
1
+ require 'tilt'
2
+ require 'flavour_saver'
3
+
4
+ describe 'Fixture: custom_block_helper.hbs' do
5
+ subject { Tilt.new(template).render(context).gsub(/[\s\r\n]+/, ' ').strip }
6
+ let(:template) { File.expand_path('../../fixtures/custom_block_helper.hbs', __FILE__) }
7
+ let(:context) { double(:context) }
8
+
9
+ before(:each) do
10
+ FlavourSaver::Helpers.reset_helpers
11
+ end
12
+
13
+ describe 'method helper' do
14
+ it 'renders correctly' do
15
+ def three_times
16
+ (1..3).map do |i|
17
+ yield.contents i
18
+ end.join ''
19
+ end
20
+ FlavourSaver.register_helper(method(:three_times))
21
+ subject.should == "1 time. 2 time. 3 time."
22
+ end
23
+ end
24
+
25
+ describe 'proc helper' do
26
+ it 'renders correctly' do
27
+ FlavourSaver.register_helper(:three_times) { |&b|
28
+ (1..3).map do |i|
29
+ b.call.contents i
30
+ end.join ''
31
+ }
32
+ subject.should == "1 time. 2 time. 3 time."
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,15 @@
1
+ require 'tilt'
2
+ require 'flavour_saver'
3
+
4
+ describe 'Fixture: custom_helper.hbs' do
5
+ subject { Tilt.new(template).render(context).gsub(/[\s\r\n]+/, ' ').strip }
6
+ let(:template) { File.expand_path('../../fixtures/custom_helper.hbs', __FILE__) }
7
+ let(:context) { double(:context) }
8
+
9
+ it 'renders correctly' do
10
+ FlavourSaver.register_helper(:say_what_again) do
11
+ 'What?'
12
+ end
13
+ subject.should == "What?"
14
+ end
15
+ end
@@ -0,0 +1,26 @@
1
+ require 'tilt'
2
+ require 'flavour_saver'
3
+
4
+ describe "Can't call methods that the context doesn't respond to" do
5
+ subject { Tilt.new(template).render(context).gsub(/[\s\r\n]+/, ' ').strip }
6
+ let(:template) { '{{system "ls"}}' }
7
+ let(:context) { double(:context) }
8
+
9
+ it 'renders correctly' do
10
+ expect(Kernel).not_to receive(:system)
11
+ expect { subject }.to raise_error
12
+ end
13
+ end
14
+
15
+ describe "Can't eval arbitrary Ruby code" do
16
+ subject { Tilt.new(template).render(context).gsub(/[\s\r\n]+/, ' ').strip }
17
+ let(:template) { '{{eval "puts 1 + 1"}}' }
18
+ let(:context) { double(:context) }
19
+
20
+ it 'renders correctly' do
21
+ expect(Kernel).not_to receive(:eval)
22
+ expect { subject }.to raise_error
23
+ end
24
+ end
25
+
26
+
@@ -0,0 +1,911 @@
1
+ # These are the original Handlebars.js qunit acceptance tests, ported
2
+ # to run against FlavourSaver. Yes, this is a more brittle way of
3
+ # doing it.
4
+
5
+ require 'active_support'
6
+ ActiveSupport::SafeBuffer
7
+
8
+ require 'flavour_saver'
9
+
10
+ describe FlavourSaver do
11
+ let(:context) { double(:context) }
12
+ subject { FS.evaluate(template, context) }
13
+ after do
14
+ FS.reset_helpers
15
+ FS.reset_partials
16
+ end
17
+
18
+ describe "basic context" do
19
+ before { FS.register_helper(:link_to) { "<a>#{context}</a>" } }
20
+
21
+ describe 'most basic' do
22
+ let(:template) { "{{foo}}" }
23
+
24
+ it 'returns "foo"' do
25
+ context.stub(:foo).and_return('foo')
26
+ subject.should == 'foo'
27
+ end
28
+ end
29
+
30
+ describe 'compiling with a basic context' do
31
+ let(:template) { "Goodbye\n{{cruel}}\n{{world}}!" }
32
+
33
+ it 'it works if all the required keys are provided' do
34
+ context.should_receive(:cruel).and_return('cruel')
35
+ context.should_receive(:world).and_return('world')
36
+ subject.should == "Goodbye\ncruel\nworld!"
37
+ end
38
+ end
39
+
40
+ describe 'comments' do
41
+ let(:template) {"{{! Goodbye}}Goodbye\n{{cruel}}\n{{world}}!"}
42
+
43
+ it 'comments are ignored' do
44
+ context.should_receive(:cruel).and_return('cruel')
45
+ context.should_receive(:world).and_return('world')
46
+ subject.should == "Goodbye\ncruel\nworld!"
47
+ end
48
+ end
49
+
50
+ describe 'boolean' do
51
+ let(:template) { "{{#goodbye}}GOODBYE {{/goodbye}}cruel {{world}}!" }
52
+
53
+ it 'booleans show the contents when true' do
54
+ context.stub(:goodbye).and_return(true)
55
+ context.stub(:world).and_return('world')
56
+ subject.should == "GOODBYE cruel world!"
57
+ end
58
+
59
+ it 'booleans do not show the contents when false' do
60
+ context.stub(:goodbye).and_return(false)
61
+ context.stub(:world).and_return('world')
62
+ subject.should == "cruel world!"
63
+ end
64
+ end
65
+
66
+ describe 'zeros' do
67
+ describe '{num1: 42, num2: 0}' do
68
+ let (:template) { "num1: {{num1}}, num2: {{num2}}" }
69
+
70
+ it 'should compile to "num1: 42, num2: 0"' do
71
+ context.stub(:num1).and_return(42)
72
+ context.stub(:num2).and_return(0)
73
+ subject.should == 'num1: 42, num2: 0'
74
+ end
75
+ end
76
+
77
+ describe 0 do
78
+ let (:template) { 'num: {{.}}' }
79
+
80
+ it 'should compile to "num: 0"' do
81
+ FlavourSaver.evaluate(template,0).should == 'num: 0'
82
+ end
83
+ end
84
+
85
+ describe '{num1: {num2: 0}}' do
86
+ let(:template) { 'num: {{num1/num2}}' }
87
+
88
+ it 'should compile to "num: 0"' do
89
+ context.stub_chain(:num1, :num2).and_return(0)
90
+ subject.should == 'num: 0'
91
+ end
92
+ end
93
+ end
94
+
95
+ describe 'newlines' do
96
+ describe '\n' do
97
+ let(:template) { "Alan's\nTest" }
98
+
99
+ it 'works' do
100
+ subject.should == "Alan's\nTest"
101
+ end
102
+ end
103
+
104
+ describe '\r' do
105
+ let(:template) { "Alan's\rTest" }
106
+
107
+ it 'works' do
108
+ subject.should == "Alan's\rTest"
109
+ end
110
+ end
111
+ end
112
+
113
+ describe 'esaping text' do
114
+ describe 'apostrophes' do
115
+ let(:template) {"Awesome's"}
116
+
117
+ it "text is escapes so that it doesn't get caught in single quites" do
118
+ subject.should == "Awesome's"
119
+ end
120
+ end
121
+
122
+ describe 'backslashes' do
123
+ let(:template) { "Awesome \\" }
124
+
125
+ it "text is escaped so that the closing quote can't be ignored" do
126
+ subject.should == "Awesome \\"
127
+ end
128
+ end
129
+
130
+ describe 'more backslashes' do
131
+ let(:template) { "Awesome\\\\ foo" }
132
+
133
+ it "text is escapes so that it doesn't mess up the backslashes" do
134
+ subject.should == "Awesome\\\\ foo"
135
+ end
136
+ end
137
+
138
+ describe 'helper output containing backslashes' do
139
+ let(:template) { "Awesome {{foo}}" }
140
+
141
+ it "text is escaped so that it doesn't mess up backslashes" do
142
+ context.stub(:foo).and_return('\\')
143
+ subject.should == "Awesome \\"
144
+ end
145
+ end
146
+
147
+ describe 'doubled quotes' do
148
+ let(:template) { ' " " ' }
149
+
150
+ it "double quotes never produce invalid javascript" do
151
+ subject.should == ' " " '
152
+ end
153
+ end
154
+ end
155
+
156
+ describe 'escaping expressions' do
157
+ describe 'expressions with 3 handlebars' do
158
+ let(:template) { "{{{awesome}}}" }
159
+
160
+ it "shouldn't be escaped" do
161
+ context.stub(:awesome).and_return("&\"\\<>")
162
+ subject.should == "&\"\\<>"
163
+ end
164
+ end
165
+
166
+ describe 'expressions with {{& handlebars' do
167
+ let(:template) { "{{&awesome}}" }
168
+
169
+ it "shouldn't be escaped" do
170
+ context.stub(:awesome).and_return("&\"\\<>")
171
+ subject.should == "&\"\\<>"
172
+ end
173
+ end
174
+
175
+ describe 'expressions' do
176
+ let(:template) { "{{awesome}}" }
177
+
178
+ it "should be escaped" do
179
+ context.stub(:awesome).and_return("&\"'`\\<>")
180
+ if RUBY_VERSION == '2.0.0'
181
+ subject.should == "&amp;&quot;&#39;&#x60;\\&lt;&gt;"
182
+ else
183
+ subject.should == "&amp;&quot;&#x27;&#x60;\\&lt;&gt;"
184
+ end
185
+ end
186
+ end
187
+
188
+ describe 'ampersands' do
189
+ let(:template) { "{{awesome}}" }
190
+
191
+ it "should be escaped" do
192
+ context.stub(:awesome).and_return("Escaped, <b> looks like: &lt;b&gt;")
193
+ subject.should == "Escaped, &lt;b&gt; looks like: &amp;lt;b&amp;gt;"
194
+ end
195
+ end
196
+ end
197
+
198
+ describe "functions returning safe strings" do
199
+ let(:template) { "{{awesome}}" }
200
+
201
+ it "shouldn't be escaped" do
202
+ context.stub(:awesome).and_return("&\"\\<>".html_safe)
203
+ subject.should == "&\"\\<>"
204
+ end
205
+ end
206
+
207
+ describe 'functions' do
208
+ let(:template) { "{{awesome}}" }
209
+
210
+ it "are called and render their output" do
211
+ context.stub(:awesome).and_return("Awesome")
212
+ subject.should == "Awesome"
213
+ end
214
+ end
215
+
216
+ describe 'paths with hyphens' do
217
+ describe '{{foo-bar}}' do
218
+ let(:template) { "{{foo-bar}}" }
219
+
220
+ it 'paths can contain hyphens (-)' do
221
+ context.should_receive(:[]).with('foo-bar').and_return('baz')
222
+ subject.should == 'baz'
223
+ end
224
+ end
225
+
226
+ describe '{{foo.foo-bar}}' do
227
+ let(:template) { "{{foo.foo-bar}}" }
228
+
229
+ it 'paths can contain hyphens (-)' do
230
+ context.stub_chain(:foo, :[]).with('foo-bar').and_return(proc { 'baz' })
231
+ subject.should == 'baz'
232
+ end
233
+ end
234
+
235
+ describe '{{foo/foo-bar}}' do
236
+ let(:template) { "{{foo/foo-bar}}" }
237
+
238
+ it 'paths can contain hyphens (-)' do
239
+ context.stub_chain(:foo, :[]).with('foo-bar').and_return('baz')
240
+ subject.should == 'baz'
241
+ end
242
+ end
243
+
244
+ describe 'nested paths' do
245
+ let(:template) {"Goodbye {{alan/expression}} world!"}
246
+
247
+ it 'access nested object' do
248
+ context.stub_chain(:alan, :expression).and_return('beautiful')
249
+ subject.should == 'Goodbye beautiful world!'
250
+ end
251
+ end
252
+
253
+ describe 'nested path with empty string value' do
254
+ let(:template) {"Goodbye {{alan/expression}} world!"}
255
+
256
+ it 'access nested object' do
257
+ context.stub_chain(:alan, :expression).and_return('')
258
+ subject.should == 'Goodbye world!'
259
+ end
260
+ end
261
+
262
+ describe 'literal paths' do
263
+ let(:template) { "Goodbye {{[@alan]/expression}} world!" }
264
+
265
+ it 'literal paths can be used' do
266
+ alan = double(:alan)
267
+ context.should_receive(:[]).with('@alan').and_return(alan)
268
+ alan.should_receive(:expression).and_return('beautiful')
269
+ subject.should == 'Goodbye beautiful world!'
270
+ end
271
+ end
272
+
273
+ describe 'complex but empty paths' do
274
+ let(:template) { '{{person/name}}' }
275
+
276
+ it 'returns empty string from nested paths' do
277
+ context.stub_chain(:person,:name).and_return('')
278
+ subject.should == ''
279
+ end
280
+
281
+ it 'returns empty string from nil objects' do
282
+ context.stub_chain(:person,:name)
283
+ subject.should == ''
284
+ end
285
+ end
286
+
287
+ describe '"this" keyword' do
288
+ describe 'in a block' do
289
+ let(:template) { "{{#goodbyes}}{{this}}{{/goodbyes}}" }
290
+
291
+ it 'evaluates to the current context' do
292
+ context.stub(:goodbyes).and_return(["goodbye", "Goodbye", "GOODBYE"])
293
+ subject.should == "goodbyeGoodbyeGOODBYE"
294
+ end
295
+ end
296
+
297
+ describe 'in a block in a path' do
298
+ let(:template) { "{{#hellos}}{{this/text}}{{/hellos}}" }
299
+
300
+ it 'evaluates in more complex paths' do
301
+ hellos = []
302
+ hellos << double(:hello)
303
+ hellos[0].should_receive(:text).and_return('hello')
304
+ hellos << double(:Hello)
305
+ hellos[1].should_receive(:text).and_return('Hello')
306
+ hellos << double(:HELLO)
307
+ hellos[2].should_receive(:text).and_return('HELLO')
308
+ context.stub(:hellos).and_return(hellos)
309
+ subject.should == "helloHelloHELLO"
310
+ end
311
+ end
312
+ end
313
+
314
+ describe 'this keyword in helpers' do
315
+ before { FS.register_helper(:foo) { |value| "bar #{value}" } }
316
+
317
+ describe 'this keyword in arguments' do
318
+ let(:template) { "{{#goodbyes}}{{foo this}}{{/goodbyes}}" }
319
+
320
+ it 'evaluates to current context' do
321
+ context.stub(:goodbyes).and_return(["goodbye", "Goodbye", "GOODBYE"])
322
+ subject.should == "bar goodbyebar Goodbyebar GOODBYE"
323
+ end
324
+ end
325
+
326
+ describe 'this keyword in object path arguments' do
327
+ let(:template) { "{{#hellos}}{{foo this/text}}{{/hellos}}" }
328
+
329
+ it 'evaluates to current context' do
330
+ hellos = []
331
+ hellos << double(:hello)
332
+ hellos[0].should_receive(:text).and_return('hello')
333
+ hellos << double(:Hello)
334
+ hellos[1].should_receive(:text).and_return('Hello')
335
+ hellos << double(:HELLO)
336
+ hellos[2].should_receive(:text).and_return('HELLO')
337
+ context.stub(:hellos).and_return(hellos)
338
+ subject.should == "bar hellobar Hellobar HELLO"
339
+ end
340
+ end
341
+ end
342
+ end
343
+ end
344
+
345
+ describe 'Inverted sections' do
346
+ let(:template) { "{{#goodbyes}}{{this}}{{/goodbyes}}{{^goodbyes}}Right On!{{/goodbyes}}" }
347
+
348
+ describe 'with unset value' do
349
+ it 'renders' do
350
+ context.stub(:goodbyes)
351
+ subject.should == 'Right On!'
352
+ end
353
+ end
354
+
355
+ describe 'with false value' do
356
+ it 'renders' do
357
+ context.stub(:goodbyes).and_return(false)
358
+ subject.should == 'Right On!'
359
+ end
360
+ end
361
+
362
+ describe 'with an empty set' do
363
+ it 'renders' do
364
+ context.stub(:goodbyes).and_return([])
365
+ subject.should == 'Right On!'
366
+ end
367
+ end
368
+ end
369
+
370
+ describe 'Blocks' do
371
+ let(:template) { "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!" }
372
+
373
+ it 'arrays iterate the contents with non-empty' do
374
+ goodbyes = []
375
+ goodbyes << double(:goodbye)
376
+ goodbyes[0].should_receive(:text).and_return('goodbye')
377
+ goodbyes << double(:Goodbye)
378
+ goodbyes[1].should_receive(:text).and_return('Goodbye')
379
+ goodbyes << double(:GOODBYE)
380
+ goodbyes[2].should_receive(:text).and_return('GOODBYE')
381
+ context.stub(:goodbyes).and_return(goodbyes)
382
+ context.stub(:world).and_return('world')
383
+ subject.should == "goodbye! Goodbye! GOODBYE! cruel world!"
384
+ end
385
+
386
+ it 'ignores the contents when the array is empty' do
387
+ context.stub(:goodbyes).and_return([])
388
+ context.stub(:world).and_return('world')
389
+ subject.should == "cruel world!"
390
+ end
391
+
392
+ describe 'array with @index' do
393
+ let(:template) {"{{#goodbyes}}{{@index}}. {{text}}! {{/goodbyes}}cruel {{world}}!"}
394
+
395
+ it 'the @index variable is used' do
396
+ goodbyes = []
397
+ goodbyes << double(:goodbye)
398
+ goodbyes[0].should_receive(:text).and_return('goodbye')
399
+ goodbyes << double(:Goodbye)
400
+ goodbyes[1].should_receive(:text).and_return('Goodbye')
401
+ goodbyes << double(:GOODBYE)
402
+ goodbyes[2].should_receive(:text).and_return('GOODBYE')
403
+ context.stub(:goodbyes).and_return(goodbyes)
404
+ context.stub(:world).and_return('world')
405
+ subject.should == "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!"
406
+ end
407
+ end
408
+
409
+ describe 'empty block' do
410
+ let(:template) { "{{#goodbyes}}{{/goodbyes}}cruel {{world}}!" }
411
+
412
+ it 'arrays iterate the contents with non-empty' do
413
+ goodbyes = []
414
+ goodbyes << double(:goodbye)
415
+ goodbyes[0].stub(:text).and_return('goodbye')
416
+ goodbyes << double(:Goodbye)
417
+ goodbyes[1].stub(:text).and_return('Goodbye')
418
+ goodbyes << double(:GOODBYE)
419
+ goodbyes[2].stub(:text).and_return('GOODBYE')
420
+ context.stub(:goodbyes).and_return(goodbyes)
421
+ context.stub(:world).and_return('world')
422
+ subject.should == "cruel world!"
423
+ end
424
+
425
+ it 'ignores the contents when the array is empty' do
426
+ context.stub(:goodbyes).and_return([])
427
+ context.stub(:world).and_return('world')
428
+ subject.should == "cruel world!"
429
+ end
430
+ end
431
+
432
+ describe 'nested iteration'
433
+
434
+ describe 'block with complex lookup' do
435
+ let(:template) {"{{#goodbyes}}{{text}} cruel {{../name}}! {{/goodbyes}}"}
436
+
437
+ it 'templates can access variables in contexts up the stack with relative path syntax' do
438
+ context.stub(:name).and_return('Alan')
439
+ goodbyes = []
440
+ goodbyes << double(:goodbye)
441
+ goodbyes[0].should_receive(:text).and_return('goodbye')
442
+ goodbyes << double(:Goodbye)
443
+ goodbyes[1].should_receive(:text).and_return('Goodbye')
444
+ goodbyes << double(:GOODBYE)
445
+ goodbyes[2].should_receive(:text).and_return('GOODBYE')
446
+ context.stub(:goodbyes).and_return(goodbyes)
447
+ subject.should == "goodbye cruel Alan! Goodbye cruel Alan! GOODBYE cruel Alan! "
448
+ end
449
+ end
450
+
451
+ describe 'helper with complex lookup' do
452
+ let(:template) {"{{#goodbyes}}{{{link ../prefix}}}{{/goodbyes}}"}
453
+ before do
454
+ FS.register_helper(:link) do |prefix|
455
+ "<a href='#{prefix}/#{url}'>#{text}</a>"
456
+ end
457
+ end
458
+
459
+ it 'renders correctly' do
460
+ context.stub(:prefix).and_return('/root')
461
+ goodbyes = []
462
+ goodbyes << double(:Goodbye)
463
+ goodbyes[0].should_receive(:text).and_return('Goodbye')
464
+ goodbyes[0].should_receive(:url).and_return('goodbye')
465
+ context.stub(:goodbyes).and_return(goodbyes)
466
+ subject.should == "<a href='/root/goodbye'>Goodbye</a>"
467
+ end
468
+ end
469
+
470
+ describe 'helper with complex lookup expression' do
471
+ let(:template) { "{{#goodbyes}}{{../name}}{{/goodbyes}}" }
472
+ before do
473
+ FS.register_helper(:goodbyes) do |&b|
474
+ ["Goodbye", "goodbye", "GOODBYE"].map do |bye|
475
+ "#{bye} #{b.call.contents}! "
476
+ end.join('')
477
+ end
478
+ end
479
+
480
+ it 'renders correctly' do
481
+ context.stub(:name).and_return('Alan')
482
+ subject.should == "Goodbye Alan! goodbye Alan! GOODBYE Alan! "
483
+ end
484
+ end
485
+
486
+ describe 'helper with complex lookup and nested template' do
487
+ let(:template) { "{{#goodbyes}}{{#link ../prefix}}{{text}}{{/link}}{{/goodbyes}}" }
488
+ before do
489
+ FS.register_helper(:link) do |prefix,&b|
490
+ "<a href='#{prefix}/#{url}'>#{b.call.contents}</a>"
491
+ end
492
+ end
493
+
494
+ it 'renders correctly' do
495
+ context.stub(:prefix).and_return('/root')
496
+ goodbye = double(:goodbye)
497
+ goodbye.stub(:text).and_return('Goodbye')
498
+ goodbye.stub(:url).and_return('goodbye')
499
+ context.stub(:goodbyes).and_return([goodbye])
500
+ subject.should == "<a href='/root/goodbye'>Goodbye</a>"
501
+ end
502
+ end
503
+
504
+ describe 'block with deep nested complex lookup' do
505
+ let(:template) { "{{#outer}}Goodbye {{#inner}}cruel {{../../omg}}{{/inner}}{{/outer}}" }
506
+
507
+ example do
508
+ goodbye = double(:goodbye)
509
+ goodbye.stub(:text).and_return('goodbye')
510
+ inner = double(:inner)
511
+ inner.stub(:inner).and_return([goodbye])
512
+ context.stub(:omg).and_return('OMG!')
513
+ context.stub(:outer).and_return([inner])
514
+ subject.should == "Goodbye cruel OMG!"
515
+ end
516
+ end
517
+
518
+ describe 'block helper' do
519
+ let(:template) { "{{#goodbyes}}{{text}}! {{/goodbyes}}cruel {{world}}!" }
520
+ before do
521
+ FS.register_helper(:goodbyes) do |&block|
522
+ block.call.contents Struct.new(:text).new('GOODBYE')
523
+ end
524
+ end
525
+
526
+ example do
527
+ context.stub(:world).and_return('world')
528
+ end
529
+ end
530
+
531
+ describe 'block helper staying in the same context' do
532
+ let(:template) { "{{#form}}<p>{{name}}</p>{{/form}}" }
533
+ before do
534
+ FS.register_helper(:form) do |&block|
535
+ "<form>#{block.call.contents}</form>"
536
+ end
537
+ end
538
+
539
+ example do
540
+ context.stub(:name).and_return('Yehuda')
541
+ subject.should == "<form><p>Yehuda</p></form>"
542
+ end
543
+ end
544
+
545
+ describe 'block helper should have context in this' do
546
+ let(:template) { "<ul>{{#people}}<li>{{#link}}{{name}}{{/link}}</li>{{/people}}</ul>" }
547
+ before do
548
+ FS.register_helper(:link) do |&block|
549
+ "<a href=\"/people/#{this.id}\">#{block.call.contents}</a>"
550
+ end
551
+ end
552
+ example do
553
+ person = Struct.new(:name, :id)
554
+ context.stub(:people).and_return([person.new('Alan', 1), person.new('Yehuda', 2)])
555
+ subject.should == "<ul><li><a href=\"/people/1\">Alan</a></li><li><a href=\"/people/2\">Yehuda</a></li></ul>"
556
+ end
557
+ end
558
+
559
+ describe 'block helper for undefined value' do
560
+ let(:template) { "{{#empty}}shoulnd't render{{/empty}}" }
561
+ example do
562
+ subject.should == ""
563
+ end
564
+ end
565
+
566
+ describe 'block helper passing a new context' do
567
+ let(:template) { "{{#form yehuda}}<p>{{name}}</p>{{/form}}" }
568
+ before do
569
+ FS.register_helper(:form) do |whom,&block|
570
+ "<form>#{block.call.contents whom}</form>"
571
+ end
572
+ end
573
+ example do
574
+ context.stub_chain(:yehuda,:name).and_return('Yehuda')
575
+ subject.should == "<form><p>Yehuda</p></form>"
576
+ end
577
+ end
578
+
579
+ describe 'block helper passing a complex path context' do
580
+ let(:template) { "{{#form yehuda/cat}}<p>{{name}}</p>{{/form}}" }
581
+ before do
582
+ FS.register_helper(:form) do |context,&block|
583
+ "<form>#{block.call.contents context}</form>"
584
+ end
585
+ end
586
+ example do
587
+ yehuda = double(:yehuda)
588
+ yehuda.stub(:name).and_return('Yehuda')
589
+ yehuda.stub_chain(:cat,:name).and_return('Harold')
590
+ context.stub(:yehuda).and_return(yehuda)
591
+ subject.should == "<form><p>Harold</p></form>"
592
+ end
593
+ end
594
+
595
+ describe 'nested block helpers' do
596
+ let(:template) { "{{#form yehuda}}<p>{{name}}</p>{{#link}}Hello{{/link}}{{/form}}" }
597
+ before do
598
+ FS.register_helper(:link) do |&block|
599
+ "<a href='#{name}'>#{block.call.contents}</a>"
600
+ end
601
+ FS.register_helper(:form) do |context,&block|
602
+ "<form>#{block.call.contents context}</form>"
603
+ end
604
+ end
605
+ example do
606
+ context.stub_chain(:yehuda,:name).and_return('Yehuda')
607
+ subject.should == "<form><p>Yehuda</p><a href='Yehuda'>Hello</a></form>"
608
+ end
609
+ end
610
+
611
+ describe 'block inverted sections' do
612
+ let(:template) { "{{#people}}{{name}}{{^}}{{none}}{{/people}}" }
613
+ example do
614
+ context.stub(:none).and_return("No people")
615
+ subject.should == "No people"
616
+ end
617
+ end
618
+
619
+ describe 'block inverted sections with empty arrays' do
620
+ let(:template) { "{{#people}}{{name}}{{^}}{{none}}{{/people}}" }
621
+ example do
622
+ context.stub(:none).and_return('No people')
623
+ context.stub(:people).and_return([])
624
+ subject.should == "No people"
625
+ end
626
+ end
627
+
628
+ describe 'block helpers with inverted sections' do
629
+ let (:template) { "{{#list people}}{{name}}{{^}}<em>Nobody's here</em>{{/list}}" }
630
+ before do
631
+ FS.register_helper(:list) do |context,&block|
632
+ if context.any?
633
+ "<ul>" +
634
+ context.map { |e| "<li>#{block.call.contents e}</li>" }.join('') +
635
+ "</ul>"
636
+ else
637
+ "<p>#{block.call.inverse}</p>"
638
+ end
639
+ end
640
+ end
641
+
642
+ example 'an inverse wrapper is passed in as a new context' do
643
+ person = Struct.new(:name)
644
+ context.stub(:people).and_return([person.new('Alan'),person.new('Yehuda')])
645
+ subject.should == "<ul><li>Alan</li><li>Yehuda</li></ul>"
646
+ end
647
+
648
+ example 'an inverse wrapper can optionally be called' do
649
+ context.stub(:people).and_return([])
650
+ subject.should == "<p><em>Nobody's here</em></p>"
651
+ end
652
+
653
+ describe 'the context of an inverse is the parent of the block' do
654
+ let(:template) { "{{#list people}}Hello{{^}}{{message}}{{/list}}" }
655
+ example do
656
+ context.stub(:people).and_return([])
657
+ context.stub(:message).and_return("Nobody's here")
658
+ if RUBY_VERSION == '2.0.0'
659
+ subject.should == "<p>Nobody&#39;s here</p>"
660
+ else
661
+ subject.should == "<p>Nobody&#x27;s here</p>"
662
+ end
663
+ end
664
+ end
665
+ end
666
+ end
667
+
668
+ describe 'partials' do
669
+ let(:template) { "Dudes: {{#dudes}}{{> dude}}{{/dudes}}" }
670
+ before do
671
+ FS.register_partial(:dude, "{{name}} ({{url}}) ")
672
+ end
673
+ example do
674
+ person = Struct.new(:name, :url)
675
+ context.stub(:dudes).and_return([person.new('Yehuda', 'http://yehuda'), person.new('Alan', 'http://alan')])
676
+ subject.should == "Dudes: Yehuda (http://yehuda) Alan (http://alan) "
677
+ end
678
+ end
679
+
680
+ describe 'partials with context' do
681
+ let(:template) {"Dudes: {{>dude dudes}}"}
682
+ before do
683
+ FS.register_partial(:dude, "{{#this}}{{name}} ({{url}}) {{/this}}")
684
+ end
685
+ example "Partials can be passed a context" do
686
+ person = Struct.new(:name, :url)
687
+ context.stub(:dudes).and_return([person.new('Yehuda', 'http://yehuda'), person.new('Alan', 'http://alan')])
688
+ subject.should == "Dudes: Yehuda (http://yehuda) Alan (http://alan) "
689
+ end
690
+ end
691
+
692
+ describe 'partial in a partial' do
693
+ let(:template) {"Dudes: {{#dudes}}{{>dude}}{{/dudes}}"}
694
+ before do
695
+ FS.register_partial(:dude, "{{name}} {{>url}} ")
696
+ FS.register_partial(:url, "<a href='{{url}}'>{{url}}</a>")
697
+ end
698
+ example "Partials can be passed a context" do
699
+ person = Struct.new(:name, :url)
700
+ context.stub(:dudes).and_return([person.new('Yehuda', 'http://yehuda'), person.new('Alan', 'http://alan')])
701
+ subject.should == "Dudes: Yehuda <a href='http://yehuda'>http://yehuda</a> Alan <a href='http://alan'>http://alan</a> "
702
+ end
703
+ end
704
+
705
+ describe 'rendering undefined partial throws an exception' do
706
+ let(:template) { "{{> whatever}}" }
707
+ example do
708
+ -> { subject }.should raise_error(FS::UnknownPartialException)
709
+ end
710
+ end
711
+
712
+ describe 'rendering a function partial' do
713
+ let(:template) { "Dudes: {{#dudes}}{{> dude}}{{/dudes}}" }
714
+ before do
715
+ FS.register_partial(:dude) do |context|
716
+ "#{context.name} (#{context.url}) "
717
+ end
718
+ end
719
+ example do
720
+ person = Struct.new(:name, :url)
721
+ context.stub(:dudes).and_return([person.new('Yehuda', 'http://yehuda'), person.new('Alan', 'http://alan')])
722
+ subject.should == "Dudes: Yehuda (http://yehuda) Alan (http://alan) "
723
+ end
724
+ end
725
+
726
+ describe 'a partial preceding a selector' do
727
+ let(:template) { "Dudes: {{>dude}} {{another_dude}}" }
728
+ before do
729
+ FS.register_partial(:dude, "{{name}}")
730
+ end
731
+ example do
732
+ context.stub(:name).and_return('Jeepers')
733
+ context.stub(:another_dude).and_return('Creepers')
734
+ subject.should == "Dudes: Jeepers Creepers"
735
+ end
736
+ end
737
+
738
+ describe 'partials with literal paths' do
739
+ let(:template) { "Dudes: {{> [dude]}}" }
740
+ before do
741
+ FS.register_partial(:dude, "{{name}}")
742
+ end
743
+ example do
744
+ context.stub(:name).and_return('Jeepers')
745
+ context.stub(:another_dude).and_return('Creepers')
746
+ subject.should == "Dudes: Jeepers"
747
+ end
748
+ end
749
+
750
+ describe 'string literal parameters' do
751
+
752
+ describe 'simple literals work' do
753
+ let(:template) { "Message: {{hello \"world\" 12 true false}}" }
754
+ before do
755
+ FS.register_helper(:hello) do |param,times,bool1,bool2|
756
+ times = "NaN" unless times.is_a? Fixnum
757
+ bool1 = "NaB" unless bool1 == true
758
+ bool2 = "NaB" unless bool2 == false
759
+ "Hello #{param} #{times} times: #{bool1} #{bool2}"
760
+ end
761
+ end
762
+ example do
763
+ subject.should == "Message: Hello world 12 times: true false"
764
+ end
765
+ end
766
+
767
+ describe 'using a quote in the middle of a parameter raises an error' do
768
+ let(:template) { "Message: {{hello wo\"rld\"}}" }
769
+ example do
770
+ -> { subject }.should raise_error
771
+ end
772
+ end
773
+
774
+ describe 'escaping a string is possible' do
775
+ let(:template) { 'Message: {{{hello "\"world\""}}}' }
776
+ before do
777
+ FS.register_helper(:hello) do |param|
778
+ "Hello #{param}"
779
+ end
780
+ end
781
+ example do
782
+ subject.should == 'Message: Hello \"world\"'
783
+ end
784
+ end
785
+
786
+ describe 'string work with ticks' do
787
+ let(:template) { 'Message: {{{hello "Alan\'s world"}}}' }
788
+ before do
789
+ FS.register_helper(:hello) do |param|
790
+ "Hello #{param}"
791
+ end
792
+ end
793
+ example do
794
+ subject.should == "Message: Hello Alan's world"
795
+ end
796
+ end
797
+
798
+ end
799
+
800
+ describe 'multi-params' do
801
+ describe 'simple multi-params work' do
802
+ let(:template) { "Message: {{goodbye cruel world}}" }
803
+ before { FS.register_helper(:goodbye) { |cruel,world| "Goodbye #{cruel} #{world}" } }
804
+ example do
805
+ context.stub(:cruel).and_return('cruel')
806
+ context.stub(:world).and_return('world')
807
+ subject.should == "Message: Goodbye cruel world"
808
+ end
809
+ end
810
+
811
+ describe 'block multi-params' do
812
+ let(:template) { "Message: {{#goodbye cruel world}}{{greeting}} {{adj}} {{noun}}{{/goodbye}}" }
813
+ before { FS.register_helper(:goodbye) { |adj,noun,&b| b.call.contents Struct.new(:greeting,:adj,:noun).new('Goodbye', adj, noun) } }
814
+ example do
815
+ context.stub(:cruel).and_return('cruel')
816
+ context.stub(:world).and_return('world')
817
+ subject.should == "Message: Goodbye cruel world"
818
+ end
819
+ end
820
+ end
821
+
822
+ describe 'built-in helpers' do
823
+ describe 'with' do
824
+ let(:template) { "{{#with person}}{{first}} {{last}}{{/with}}" }
825
+ example do
826
+ context.stub(:person).and_return(Struct.new(:first,:last).new('Alan','Johnson'))
827
+ subject.should == 'Alan Johnson'
828
+ end
829
+ end
830
+
831
+ describe 'if' do
832
+ let(:template) { "{{#if goodbye}}GOODBYE {{/if}}cruel {{world}}!" }
833
+
834
+ example 'if with boolean argument shows the contents when true' do
835
+ context.stub(:goodbye).and_return(true)
836
+ context.stub(:world).and_return('world')
837
+ subject.should == "GOODBYE cruel world!"
838
+ end
839
+
840
+ example 'if with string argument shows the contents with true' do
841
+ context.stub(:goodbye).and_return('dummy')
842
+ context.stub(:world).and_return('world')
843
+ subject.should == "GOODBYE cruel world!"
844
+ end
845
+
846
+ example 'if with boolean argument does not show the contents when false' do
847
+ context.stub(:goodbye).and_return(false)
848
+ context.stub(:world).and_return('world')
849
+ subject.should == "cruel world!"
850
+ end
851
+
852
+ example 'if with undefined does not show the contents' do
853
+ context.stub(:goodbye)
854
+ context.stub(:world).and_return('world')
855
+ subject.should == "cruel world!"
856
+ end
857
+
858
+ example 'if with non-empty array shows the contents' do
859
+ context.stub(:goodbye).and_return(['foo'])
860
+ context.stub(:world).and_return('world')
861
+ subject.should == "GOODBYE cruel world!"
862
+ end
863
+
864
+ example 'if with empty array does not show the contents' do
865
+ context.stub(:goodbye).and_return([])
866
+ context.stub(:world).and_return('world')
867
+ subject.should == "cruel world!"
868
+ end
869
+ end
870
+
871
+ describe '#each' do
872
+ let(:template) { "{{#each goodbyes}}{{text}}! {{/each}}cruel {{world}}!" }
873
+
874
+ example 'each with array iterates over the contents with non-empty' do
875
+ g = Struct.new(:text)
876
+ context.stub(:goodbyes).and_return([g.new('goodbye'), g.new('Goodbye'), g.new('GOODBYE')])
877
+ context.stub(:world).and_return('world')
878
+ subject.should == "goodbye! Goodbye! GOODBYE! cruel world!"
879
+ end
880
+
881
+ example 'each with array ignores the contents when empty' do
882
+ context.stub(:goodbyes).and_return([])
883
+ context.stub(:world).and_return('world')
884
+ subject.should == "cruel world!"
885
+ end
886
+ end
887
+
888
+ describe 'each with @index' do
889
+ let(:template) { "{{#each goodbyes}}{{@index}}. {{text}}! {{/each}}cruel {{world}}!" }
890
+
891
+ example 'the @index variable is used' do
892
+ g = Struct.new(:text)
893
+ context.stub(:goodbyes).and_return([g.new('goodbye'), g.new('Goodbye'), g.new('GOODBYE')])
894
+ context.stub(:world).and_return('world')
895
+ subject.should == "0. goodbye! 1. Goodbye! 2. GOODBYE! cruel world!"
896
+ end
897
+ end
898
+
899
+ describe 'log' do
900
+ let(:template) { "{{log blah}}" }
901
+ let(:log) { double(:log) }
902
+ before { FS.logger = log }
903
+ after { FS.logger = nil }
904
+ example do
905
+ context.stub(:blah).and_return('whee')
906
+ log.should_receive(:debug).with('FlavourSaver: whee')
907
+ subject.should == ''
908
+ end
909
+ end
910
+ end
911
+ end