garterbelt 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. data/.document +5 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +16 -0
  4. data/Gemfile.lock +38 -0
  5. data/LICENSE.txt +20 -0
  6. data/README.rdoc +21 -0
  7. data/Rakefile +46 -0
  8. data/TODO +3 -0
  9. data/VERSION +1 -0
  10. data/garterbelt.gemspec +165 -0
  11. data/lib/garterbelt.rb +23 -0
  12. data/lib/page.rb +46 -0
  13. data/lib/renderers/cache.rb +35 -0
  14. data/lib/renderers/closed_tag.rb +60 -0
  15. data/lib/renderers/comment.rb +14 -0
  16. data/lib/renderers/content_rendering.rb +41 -0
  17. data/lib/renderers/content_tag.rb +36 -0
  18. data/lib/renderers/doctype.rb +24 -0
  19. data/lib/renderers/renderer.rb +33 -0
  20. data/lib/renderers/text.rb +28 -0
  21. data/lib/renderers/xml.rb +9 -0
  22. data/lib/stocking.rb +11 -0
  23. data/lib/support/string.rb +165 -0
  24. data/lib/view.rb +341 -0
  25. data/spec/benchmark/templates/erector.rb +37 -0
  26. data/spec/benchmark/templates/garterbelt.rb +37 -0
  27. data/spec/benchmark/vs_erector.rb +53 -0
  28. data/spec/garterbelt_spec.rb +49 -0
  29. data/spec/integration/expectations/general_view.html +17 -0
  30. data/spec/integration/expectations/variables/view_with_user_and_params.html +23 -0
  31. data/spec/integration/expectations/variables/view_with_user_email.html +23 -0
  32. data/spec/integration/expectations/view_partial_nest.html +24 -0
  33. data/spec/integration/expectations/view_with_tags.html +19 -0
  34. data/spec/integration/templates/view_partial_nest.rb +22 -0
  35. data/spec/integration/templates/view_with_cache.rb +30 -0
  36. data/spec/integration/templates/view_with_partial.rb +36 -0
  37. data/spec/integration/templates/view_with_partial_2.rb +36 -0
  38. data/spec/integration/templates/view_with_tags.rb +26 -0
  39. data/spec/integration/templates/view_with_vars.rb +32 -0
  40. data/spec/integration/view_spec.rb +57 -0
  41. data/spec/page_spec.rb +99 -0
  42. data/spec/renderers/cache_spec.rb +85 -0
  43. data/spec/renderers/closed_tag_spec.rb +172 -0
  44. data/spec/renderers/comment_spec.rb +68 -0
  45. data/spec/renderers/content_tag_spec.rb +150 -0
  46. data/spec/renderers/doctype_spec.rb +46 -0
  47. data/spec/renderers/text_spec.rb +68 -0
  48. data/spec/spec_helper.rb +17 -0
  49. data/spec/support/mock_view.rb +14 -0
  50. data/spec/support/puters.rb +10 -0
  51. data/spec/view/view_basics_spec.rb +106 -0
  52. data/spec/view/view_caching_spec.rb +132 -0
  53. data/spec/view/view_partial_spec.rb +63 -0
  54. data/spec/view/view_rails_type_helpers.rb +148 -0
  55. data/spec/view/view_render_spec.rb +408 -0
  56. data/spec/view/view_variables_spec.rb +159 -0
  57. metadata +367 -0
@@ -0,0 +1,408 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Garterbelt::View do
4
+ class BasicView < Garterbelt::View
5
+ def content
6
+ end
7
+
8
+ def alt_content
9
+ end
10
+ end
11
+
12
+ before do
13
+ @view = BasicView.new
14
+ end
15
+
16
+ describe 'rendering' do
17
+ it 'clears the output on start' do
18
+ @view.output = 'Foo the magic output!'
19
+ @view.render
20
+ @view.output.should_not include 'Foo the magic output!'
21
+ end
22
+
23
+ describe 'methods' do
24
+ it 'renders the :content method by default' do
25
+ @view.should_receive(:content)
26
+ @view.render
27
+ end
28
+
29
+ it 'renders an alternate method when requested' do
30
+ @view.should_receive(:foo)
31
+ @view.render(:foo)
32
+ end
33
+ end
34
+
35
+ it 'returns the output' do
36
+ @view.render.should === @view.output
37
+ end
38
+
39
+ describe "render_buffer" do
40
+ before do
41
+ @hr = Garterbelt::ContentTag.new(:view => @view, :type => :hr)
42
+ @input = Garterbelt::ContentTag.new(:view => @view, :type => :input)
43
+ @img = Garterbelt::ContentTag.new(:view => @view, :type => :img)
44
+ end
45
+
46
+ it 'will clear the buffer' do
47
+ @view.buffer = [:foo]
48
+ @view.render_buffer
49
+ @view.buffer.should == []
50
+ end
51
+
52
+ it 'will call render on each tag in the buffer' do
53
+ @view.buffer << @hr << @input << @img
54
+ @hr.should_receive(:render)
55
+ @input.should_receive(:render)
56
+ @img.should_receive(:render)
57
+
58
+ @view.render_buffer
59
+ end
60
+
61
+ it 'should recycle the tags' do
62
+ @view.buffer << @hr << @input << @img
63
+ @hr.stub(:render)
64
+ @input.stub(:render)
65
+ @img.stub(:render)
66
+
67
+ @hr.should_receive(:recycle)
68
+ @input.should_receive(:recycle)
69
+ @img.should_receive(:recycle)
70
+
71
+ @view.render_buffer
72
+ end
73
+
74
+ it 'will add non renderable items to the output as strings' do
75
+ @view.buffer << "foo " << :bar
76
+ @view.render_buffer
77
+ @view.output.should include 'foo bar'
78
+ end
79
+ end
80
+
81
+ describe 'tag nesting' do
82
+ it 'should render correctly at one layer deep' do
83
+ @view.buffer << Garterbelt::ClosedTag.new(:type => :hr, :view => @view)
84
+ @view.render.should == "<hr>\n"
85
+ end
86
+
87
+ describe 'second level' do
88
+ before do
89
+ @view.buffer << Garterbelt::ContentTag.new(:type => :p, :view => @view) do
90
+ @view.buffer << Garterbelt::ClosedTag.new(:type => :hr, :view => @view)
91
+ end
92
+ @view.render
93
+ end
94
+
95
+ it 'should leave an empty buffer' do
96
+ @view.buffer.should be_empty
97
+ end
98
+
99
+ it 'should include the content' do
100
+ @view.output.should include "<hr>"
101
+ end
102
+
103
+ it 'should puts the nested tag inside the parent tag' do
104
+ @view.output.should match /<p>\W*<hr>\W*<\/p>/
105
+ end
106
+ end
107
+
108
+ describe 'multi level' do
109
+ before do
110
+ @view.buffer << Garterbelt::ContentTag.new(:type => :form, :view => @view) do
111
+ @view.buffer << Garterbelt::ContentTag.new(:type => :fieldset, :view => @view) do
112
+ @view.buffer << Garterbelt::ContentTag.new(:type => :label, :view => @view, :attributes => {:for => 'email'}) do
113
+ @view.buffer << Garterbelt::ClosedTag.new(:type => :input, :view => @view, :attributes => {:name => 'email', :type => 'text'})
114
+ end
115
+ @view.buffer << Garterbelt::ContentTag.new(:type => :input, :view => @view, :attributes => {:type => 'submit', :value => 'Login or whatever'})
116
+ end
117
+ end
118
+ @view.render
119
+ end
120
+
121
+ it 'should include the deepest level content' do
122
+ @view.output.should include "<input name=\"email\" type=\"text\">"
123
+ end
124
+
125
+ it 'should nest properly' do
126
+ @view.output.should match /<form>\W*<fieldset>\W*<label[^>]*>\W*<input/
127
+ end
128
+
129
+ it 'should indent properly' do
130
+ @view.output.should match /^<form>/
131
+ @view.output.should match /^ <fieldset>/
132
+ @view.output.should match /^ <label/
133
+ @view.output.should match /^ <input name="email"/
134
+ end
135
+
136
+ it 'should include content after the nesting' do
137
+ @view.output.should include "<input type=\"submit\" value=\"Login or whatever\""
138
+ @view.output.should match /^ <input type="submit"/
139
+ end
140
+ end
141
+ end
142
+
143
+ describe 'class method' do
144
+ before do
145
+ @rendered = @view.render
146
+ @view.stub(:recycle)
147
+ end
148
+
149
+ it 'makes a new view' do
150
+ BasicView.should_receive(:new).and_return(@view)
151
+ BasicView.render
152
+ end
153
+
154
+ it 'renders it' do
155
+ BasicView.stub(:new).and_return(@view)
156
+ @view.should_receive(:render)
157
+ BasicView.render
158
+ end
159
+
160
+ it 'passes the :method option to render' do
161
+ BasicView.stub(:new).and_return(@view)
162
+ @view.should_receive(:render).with(:alt_content)
163
+ BasicView.render :method => :alt_content
164
+ end
165
+
166
+ it 'recycles the view' do
167
+ BasicView.stub(:new).and_return(@view)
168
+ @view.should_receive(:recycle)
169
+ BasicView.render
170
+ end
171
+
172
+ it 'returns the output' do
173
+ BasicView.stub(:new).and_return(@view)
174
+ BasicView.render.should == @rendered
175
+ end
176
+ end
177
+ end
178
+
179
+ describe 'tag helpers' do
180
+ describe '#tag' do
181
+ it 'makes a new tag' do
182
+ Garterbelt::ContentTag.should_receive(:new).with(
183
+ :type => :p, :view => @view, :content => 'content', :attributes => {:class => 'classy'}
184
+ ).and_return('content')
185
+ @view.tag(:p, "content", {:class => 'classy'})
186
+ end
187
+
188
+ it 'returns the tag' do
189
+ @view.tag(:p, "content", {:class => 'classy'}).is_a?(Garterbelt::ContentTag).should be_true
190
+ end
191
+
192
+ it 'adds it to the buffer' do
193
+ tag = @view.tag(:p, "content", {:class => 'classy'})
194
+ @view.buffer.should include tag
195
+ end
196
+
197
+ it 'works with block content' do
198
+ tag = @view.tag(:p, "content", {:class => 'classy'}) do
199
+ @view.buffer << "foo"
200
+ end
201
+ tag.content.is_a?(Proc).should be_true
202
+ end
203
+ end
204
+
205
+ describe '#closed_tag' do
206
+ it 'makes a new closed tag' do
207
+ Garterbelt::ClosedTag.should_receive(:new).with(
208
+ :type => :hr, :view => @view, :attributes => {:class => 'linear'}
209
+ ).and_return('content')
210
+ @view.closed_tag(:hr, :class => 'linear')
211
+ end
212
+
213
+ it 'returns the tag' do
214
+ @view.closed_tag(:hr, :class => 'linear').is_a?(Garterbelt::ClosedTag).should be_true
215
+ end
216
+
217
+ it 'adds it to the buffer' do
218
+ tag = @view.closed_tag(:hr, :class => 'linear')
219
+ @view.buffer.should include tag
220
+ end
221
+ end
222
+
223
+ describe '#non_escape_tag' do
224
+ it 'calls #tag' do
225
+ @view.should_receive(:tag)
226
+ @view.non_escape_tag(:pre, "<div>content</div>", {:class => 'classy'})
227
+ end
228
+
229
+ it 'sets and resets the escape when escape is originally set to true' do
230
+ @view.should_receive(:escape=).with(false).ordered
231
+ @view.should_receive(:tag).ordered
232
+ @view.should_receive(:escape=).with(true).ordered
233
+ @view.non_escape_tag(:pre, "<div>content</div>", {:class => 'classy'})
234
+ end
235
+
236
+ it 'does not set the escape when set to false' do
237
+ @view.escape = false
238
+ @view.should_not_receive(:escape=)
239
+ @view.non_escape_tag(:pre, "<div>content</div>", {:class => 'classy'})
240
+ end
241
+ end
242
+
243
+ describe '#text' do
244
+ it 'makes a new Text' do
245
+ Garterbelt::Text.should_receive(:new).and_return('some content')
246
+ @view.text("content")
247
+ end
248
+
249
+ it 'passes the right options to Text' do
250
+ Garterbelt::Text.should_receive(:new).with({
251
+ :view => @view, :content => 'content'
252
+ }).and_return('text renderer')
253
+ @view.text("content")
254
+ end
255
+
256
+ it 'adds the Text object to the buffer' do
257
+ @view.text("content")
258
+ text = @view.buffer.last
259
+ text.is_a?(Garterbelt::Text).should be_true
260
+ text.content.should == 'content'
261
+ end
262
+ end
263
+
264
+ describe '#raw_text' do
265
+ it 'calls #text' do
266
+ @view.should_receive(:text).and_return('text')
267
+ @view.raw_text("<div>foo</div>")
268
+ end
269
+
270
+ it 'sets escape before and after for a view that is set to escape' do
271
+ @view.should_receive(:escape=).with(false).ordered
272
+ @view.should_receive(:text).and_return('text')
273
+ @view.should_receive(:escape=).with(true).ordered
274
+ @view.raw_text("<div>foo</div>")
275
+ end
276
+
277
+ it 'does not set escape if the view is not escaping' do
278
+ @view.escape = false
279
+ @view.should_not_receive(:escape=)
280
+ @view.raw_text("<div>foo</div>")
281
+ end
282
+ end
283
+
284
+ describe 'html tag helpers' do
285
+ describe 'content tags' do
286
+ Garterbelt::View::CONTENT_TAGS.each do |type|
287
+ it "should have a method ##{type}" do
288
+ @view.should respond_to(type)
289
+ end
290
+
291
+ it "##{type} should call #tag with argument content" do
292
+ @view.should_receive(:tag).with(type.to_sym, "my great content", {:class => 'classy'})
293
+ @view.send(type, "my great content", {:class => 'classy'})
294
+ end
295
+
296
+ it "##{type} should send along block content" do
297
+ tag = @view.send(type, {:class => 'classy'}) do
298
+ 'foo'
299
+ end
300
+ tag.content.is_a?(Proc).should be_true
301
+ end
302
+ end
303
+ end
304
+
305
+ describe 'closed tags' do
306
+ Garterbelt::View::CLOSED_TAGS.each do |type|
307
+ it "should have a method ##{type}" do
308
+ @view.should respond_to(type)
309
+ end
310
+
311
+ it "##{type} should call #closed_tag with argument" do
312
+ @view.should_receive(:closed_tag).with(type.to_sym, {:class => 'classy'})
313
+ @view.send(type, {:class => 'classy'})
314
+ end
315
+ end
316
+ end
317
+
318
+ describe 'non-escaping tags' do
319
+ Garterbelt::View::NON_ESCAPE_TAGS.each do |tag|
320
+ it "responds to :#{tag}" do
321
+ @view.should respond_to(tag)
322
+ end
323
+
324
+ it 'calls non_escape_tag' do
325
+ @view.should_receive :non_escape_tag
326
+ @view.send(tag)
327
+ end
328
+ end
329
+ end
330
+
331
+ describe 'comment' do
332
+ it 'makes a comment object' do
333
+ @view.comment('This is a comment.').is_a?(Garterbelt::Comment).should be_true
334
+ end
335
+
336
+ it 'puts it on the buffer' do
337
+ comment = @view.comment("new comment now")
338
+ @view.buffer.last.should == comment
339
+ end
340
+ end
341
+
342
+ describe 'doctype' do
343
+ it 'makes a Doctype object' do
344
+ @view.doctype(:html5).is_a?(Garterbelt::Doctype).should be_true
345
+ end
346
+
347
+ it 'puts it on the buffer' do
348
+ doctype = @view.doctype
349
+ @view.buffer.last.should == doctype
350
+ end
351
+ end
352
+
353
+ describe 'xml' do
354
+ it 'adds an xml to the buffer' do
355
+ xml = @view.xml
356
+ xml.is_a?(Garterbelt::Xml).should be_true
357
+ @view.buffer.last.should == xml
358
+ end
359
+
360
+ it 'makes a closed tag with default options' do
361
+ xml = @view.xml
362
+ xml.attributes[:version].should == 1.0
363
+ xml.attributes[:encoding].should == 'utf-8'
364
+ end
365
+
366
+ it 'uses custom attributes when desired' do
367
+ xml = @view.xml(:version => 0)
368
+ xml.attributes[:version].should == 0
369
+ xml.attributes[:encoding].should == 'utf-8'
370
+ end
371
+ end
372
+
373
+ describe 'head tags' do
374
+ Garterbelt::View::HEAD_TAGS.each do |type|
375
+ describe "_#{type}" do
376
+ it "it is a method" do
377
+ @view.should respond_to("_#{type}")
378
+ end
379
+
380
+ it "makes a closed tag" do
381
+ @view.should_receive(:closed_tag).with(type.to_sym)
382
+ @view.send("_#{type}")
383
+ end
384
+ end
385
+ end
386
+
387
+ describe 'page_title' do
388
+ it 'makes a content tag of type :title' do
389
+ @view.should_receive(:tag).with(:title, "My Great Page Title!")
390
+ @view.page_title "My Great Page Title!"
391
+ end
392
+ end
393
+
394
+ describe 'helpers' do
395
+ it 'stylesheet_link makes a link closed tag with the right options' do
396
+ @view.should_receive(:_link).with(:rel => "stylesheet", 'type' => "text/css", :href => "/foo/theme.css")
397
+ @view.stylesheet_link('/foo/theme')
398
+ end
399
+
400
+ it 'javascript_link makes a script tag with the right options' do
401
+ @view.should_receive(:script).with( :src => "/foo/script.js", 'type' => "text/javascript")
402
+ @view.javascript_link('/foo/script')
403
+ end
404
+ end
405
+ end
406
+ end
407
+ end
408
+ end
@@ -0,0 +1,159 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Garterbelt::View, 'Variables' do
4
+ class NeedyView < Garterbelt::View
5
+ requires :x, :y
6
+ end
7
+
8
+ class SelectiveView < Garterbelt::View
9
+ requires_only :x, :y
10
+ end
11
+
12
+ class ExtraNeedy < NeedyView
13
+ requires :z
14
+ end
15
+
16
+ class LessNeedy < Garterbelt::View
17
+ requires :x => 'x', :y => 'y'
18
+ end
19
+
20
+ describe 'class level requirements' do
21
+ it 'the class should store a list of required variables' do
22
+ NeedyView.required.should == [:x, :y]
23
+ SelectiveView.required.should == [:x, :y]
24
+ end
25
+
26
+ it 'should add to the required set of variable of the subclass' do
27
+ ExtraNeedy.required.should == [:x, :y, :z]
28
+ end
29
+
30
+ describe 'default values' do
31
+ it '#requires and #requires_only should allow the last argument to be a hash with default values' do
32
+ lambda{ LessNeedy.new }.should_not raise_error
33
+ end
34
+
35
+ it 'stores the default values on the class' do
36
+ LessNeedy.default_variables.should == {:y => 'y', :x => 'x'}
37
+ ExtraNeedy.default_variables.should == {}
38
+ end
39
+
40
+ it 'inherits default values from superclass' do
41
+ class SecondGenLessNeedy < LessNeedy
42
+ end
43
+
44
+ SecondGenLessNeedy.default_variables.should == {:y => 'y', :x => 'x'}
45
+ end
46
+
47
+ it 'adds to default values of the superclass' do
48
+ class SecondGenLessNeedy < LessNeedy
49
+ requires :z => 'z'
50
+ end
51
+
52
+ SecondGenLessNeedy.default_variables.should == {:x => 'x', :y => 'y', :z => 'z'}
53
+ end
54
+
55
+ it 'overwrites superclass defaults' do
56
+ class SecondGenLessNeedy < LessNeedy
57
+ requires :z => 'z'
58
+ end
59
+
60
+ class ThirdGen < SecondGenLessNeedy
61
+ requires :z => 'x'
62
+ end
63
+
64
+ ThirdGen.default_variables.should == {:x => 'x', :y => 'y', :z => 'x'}
65
+ end
66
+ end
67
+
68
+ it 'aliases #requires to #needs' do
69
+ class AltlyNeedy < NeedyView
70
+ needs :z
71
+ end
72
+ AltlyNeedy.required.should == [:x, :y, :z]
73
+ end
74
+
75
+ it 'aliases #requires_only to #needs_only' do
76
+ class AltlyView < Garterbelt::View
77
+ needs_only :x, :y
78
+ end
79
+ AltlyView.required.should == [:x, :y]
80
+ end
81
+
82
+ describe 'accessor' do
83
+ describe 'without default' do
84
+ it 'builds readers for the required variables' do
85
+ NeedyView.instance_methods.should include 'x'
86
+ NeedyView.instance_methods.should include 'y'
87
+ end
88
+
89
+ it 'builds writers for the required variables' do
90
+ NeedyView.instance_methods.should include 'x='
91
+ NeedyView.instance_methods.should include 'y='
92
+ end
93
+ end
94
+
95
+ describe 'with defaults' do
96
+ it 'makes readers' do
97
+ LessNeedy.instance_methods.should include 'x'
98
+ LessNeedy.instance_methods.should include 'y'
99
+ end
100
+
101
+ it 'makes writers' do
102
+ LessNeedy.instance_methods.should include 'x='
103
+ LessNeedy.instance_methods.should include 'y='
104
+ end
105
+ end
106
+
107
+ it 'raises on compile if the required variables map to existing methods' do
108
+ lambda {
109
+ class Failer < Garterbelt::View
110
+ requires :p
111
+ end
112
+ }.should raise_error(ArgumentError, ":p cannot be a required variable because it maps to an existing method")
113
+ end
114
+ end
115
+ end
116
+
117
+ describe 'initialization' do
118
+ describe 'without defaults' do
119
+ it 'sets the accessors with the values provided' do
120
+ view = NeedyView.new :x => 'foo', :y => 'bar'
121
+ view.x.should == 'foo'
122
+ view.y.should == 'bar'
123
+ end
124
+
125
+ it 'raises an error when values are not provided' do
126
+ lambda { NeedyView.new :x => 'x' }.should raise_error(ArgumentError, "[:y] required as an initialization option")
127
+ end
128
+ end
129
+
130
+ describe 'with defaults' do
131
+ it 'sets accessors with default values' do
132
+ view = LessNeedy.new
133
+ view.x.should == 'x'
134
+ view.y.should == 'y'
135
+ end
136
+
137
+ it 'sets accessors with custom value' do
138
+ view = LessNeedy.new(:x => 'foo', :y => 'bar')
139
+ view.x.should == 'foo'
140
+ view.y.should == 'bar'
141
+ end
142
+ end
143
+
144
+ it 'builds class level accessors when it receives additional parameters' do
145
+ class ExtraNeed < Garterbelt::View
146
+ requires :x, :y
147
+ end
148
+
149
+ view = ExtraNeed.new :x => 'foo', :y => 'bar', :z => 'zardoz'
150
+ view.should respond_to :z
151
+ end
152
+
153
+ it 'raises an error when selective and it receives additional parameters' do
154
+ lambda { SelectiveView.new(:x => 'x', :y => 'y', :z => 'zardoz') }.should raise_error(
155
+ ArgumentError, "Allowed initalization options are only [:x, :y]"
156
+ )
157
+ end
158
+ end
159
+ end