garterbelt 0.0.2

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 (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