mullen-wee 2.2.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 (73) hide show
  1. data/README.rdoc +127 -0
  2. data/Rakefile +25 -0
  3. data/aWee.gemspec +26 -0
  4. data/examples/ObjectSpaceBrowser.rb +191 -0
  5. data/examples/ajax.rb +73 -0
  6. data/examples/apotomo-webhunter/main.rb +75 -0
  7. data/examples/apotomo-webhunter/public/images/bear_trap_charged.png +0 -0
  8. data/examples/apotomo-webhunter/public/images/bear_trap_snapped.png +0 -0
  9. data/examples/apotomo-webhunter/public/images/cheese.png +0 -0
  10. data/examples/apotomo-webhunter/public/images/dark_forest.jpg +0 -0
  11. data/examples/apotomo-webhunter/public/images/mouse.png +0 -0
  12. data/examples/apotomo-webhunter/public/javascripts/jquery-1.3.2.min.js +19 -0
  13. data/examples/apotomo-webhunter/public/javascripts/wee-jquery.js +19 -0
  14. data/examples/apotomo-webhunter/public/stylesheets/forest.css +33 -0
  15. data/examples/arc_challenge.rb +42 -0
  16. data/examples/arc_challenge2.rb +46 -0
  17. data/examples/cheese_task.rb +27 -0
  18. data/examples/continuations.rb +28 -0
  19. data/examples/demo.rb +135 -0
  20. data/examples/demo/calculator.rb +63 -0
  21. data/examples/demo/calendar.rb +333 -0
  22. data/examples/demo/counter.rb +38 -0
  23. data/examples/demo/editable_counter.rb +36 -0
  24. data/examples/demo/example.rb +142 -0
  25. data/examples/demo/file_upload.rb +19 -0
  26. data/examples/demo/messagebox.rb +15 -0
  27. data/examples/demo/radio.rb +33 -0
  28. data/examples/demo/window.rb +71 -0
  29. data/examples/hw.rb +11 -0
  30. data/examples/i18n/app.rb +16 -0
  31. data/examples/i18n/locale/de/app.po +25 -0
  32. data/examples/i18n/locale/en/app.po +25 -0
  33. data/examples/pager.rb +102 -0
  34. data/lib/wee.rb +109 -0
  35. data/lib/wee/application.rb +89 -0
  36. data/lib/wee/callback.rb +109 -0
  37. data/lib/wee/component.rb +363 -0
  38. data/lib/wee/decoration.rb +251 -0
  39. data/lib/wee/dialog.rb +171 -0
  40. data/lib/wee/external_resource.rb +39 -0
  41. data/lib/wee/html_brushes.rb +795 -0
  42. data/lib/wee/html_canvas.rb +254 -0
  43. data/lib/wee/html_document.rb +52 -0
  44. data/lib/wee/html_writer.rb +71 -0
  45. data/lib/wee/id_generator.rb +81 -0
  46. data/lib/wee/jquery.rb +11 -0
  47. data/lib/wee/jquery/jquery-1.3.2.min.js +19 -0
  48. data/lib/wee/jquery/wee-jquery.js +19 -0
  49. data/lib/wee/locale.rb +56 -0
  50. data/lib/wee/lru_cache.rb +91 -0
  51. data/lib/wee/presenter.rb +44 -0
  52. data/lib/wee/renderer.rb +72 -0
  53. data/lib/wee/request.rb +56 -0
  54. data/lib/wee/response.rb +68 -0
  55. data/lib/wee/rightjs.rb +11 -0
  56. data/lib/wee/rightjs/rightjs-1.5.2.min.js +9 -0
  57. data/lib/wee/rightjs/wee-rightjs.js +18 -0
  58. data/lib/wee/root_component.rb +45 -0
  59. data/lib/wee/session.rb +366 -0
  60. data/lib/wee/state.rb +102 -0
  61. data/lib/wee/task.rb +16 -0
  62. data/test/bm_render.rb +34 -0
  63. data/test/component_spec.rb +40 -0
  64. data/test/stress/plotter.rb +84 -0
  65. data/test/stress/stress_client.rb +51 -0
  66. data/test/stress/stress_local.rb +86 -0
  67. data/test/stress/stress_server.rb +83 -0
  68. data/test/test_component.rb +106 -0
  69. data/test/test_html_canvas.rb +25 -0
  70. data/test/test_html_writer.rb +32 -0
  71. data/test/test_lru_cache.rb +51 -0
  72. data/test/test_request.rb +42 -0
  73. metadata +185 -0
@@ -0,0 +1,109 @@
1
+ module Wee
2
+ Version = "2.2.0"
3
+ end
4
+
5
+ require 'rack'
6
+
7
+ require 'wee/state'
8
+ require 'wee/callback'
9
+
10
+ require 'wee/presenter'
11
+ require 'wee/decoration'
12
+ require 'wee/component'
13
+ require 'wee/root_component'
14
+ require 'wee/task'
15
+ require 'wee/dialog'
16
+
17
+ require 'wee/application'
18
+ require 'wee/request'
19
+ require 'wee/response'
20
+ require 'wee/session'
21
+
22
+ require 'wee/html_document'
23
+ require 'wee/html_brushes'
24
+ require 'wee/html_canvas'
25
+
26
+ if RUBY_VERSION >= "1.9"
27
+ begin
28
+ require 'continuation'
29
+ rescue LoadError
30
+ end
31
+ end
32
+
33
+ class Wee::HelloWorld < Wee::RootComponent
34
+ def render(r)
35
+ r.text "Hello World from Wee!"
36
+ end
37
+ end
38
+
39
+ def Wee.run(component_class=nil, params=nil, &block)
40
+ raise ArgumentError if component_class and block
41
+
42
+ params ||= Hash.new
43
+ params[:mount_path] ||= '/'
44
+ params[:port] ||= 2000
45
+ params[:public_path] ||= nil
46
+ params[:additional_builder_procs] ||= []
47
+ params[:use_continuations] ||= true
48
+ params[:print_message] ||= false
49
+ params[:autoreload] ||= false
50
+
51
+ if component_class <= Wee::RootComponent
52
+ component_class.external_resources.each do |ext_res|
53
+ params[:additional_builder_procs] << proc {|builder| ext_res.install(builder)}
54
+ end
55
+ end
56
+
57
+ raise ArgumentError if params[:use_continuations] and block
58
+
59
+ unless block
60
+ block ||= if params[:use_continuations]
61
+ proc { Wee::Session.new(component_class.instanciate,
62
+ Wee::Session::ThreadSerializer.new) }
63
+ else
64
+ proc { Wee::Session.new(component_class.instanciate) }
65
+ end
66
+ end
67
+
68
+ app = Rack::Builder.app do
69
+ map params[:mount_path] do
70
+ a = Wee::Application.new(&block)
71
+
72
+ if params[:auth_md5]
73
+ a = Rack::Auth::Digest::MD5.new(a, &params[:auth_md5])
74
+ a.realm = params[:auth_realm] || 'Wee App'
75
+ a.opaque = params[:auth_md5_opaque] || Wee::IdGenerator::Secure.new.next
76
+ end
77
+
78
+ if params[:auth_basic]
79
+ a = Rack::Auth::Basic.new(a, params[:auth_realm] || 'Wee App', &params[:auth_basic])
80
+ end
81
+
82
+ if params[:autoreload]
83
+ if params[:autoreload].kind_of?(Integer)
84
+ timer = Integer(params[:autoreload])
85
+ else
86
+ timer = 0
87
+ end
88
+ use Rack::Reloader, timer
89
+ end
90
+
91
+ if params[:public_path]
92
+ run Rack::Cascade.new([Rack::File.new(params[:public_path]), a])
93
+ else
94
+ run a
95
+ end
96
+ end
97
+ params[:additional_builder_procs].each {|bproc| bproc.call(self)}
98
+ end
99
+
100
+ if params[:print_message]
101
+ url = "http://localhost:#{params[:port]}#{params[:mount_path]}"
102
+ io = params[:print_message].kind_of?(IO) ? params[:print_message] : STDERR
103
+ io.puts
104
+ io.puts "Open your browser at: #{url}"
105
+ io.puts
106
+ end
107
+
108
+ Rack::Handler::WEBrick.run(app, :Port => params[:port])
109
+ end
@@ -0,0 +1,89 @@
1
+ require 'thread'
2
+ require 'wee/id_generator'
3
+ require 'wee/lru_cache'
4
+
5
+ module Wee
6
+
7
+ #
8
+ # A Wee::Application manages all Session's of a single application. It
9
+ # dispatches the request to the correct handler by examining the request.
10
+ #
11
+ class Application
12
+
13
+ def self.for(component_class, session_class=Wee::Session, *component_args)
14
+ new { session_class.new(component_class.new(*component_args)) }
15
+ end
16
+
17
+ class SessionCache < Wee::LRUCache
18
+ def garbage_collect
19
+ delete_if {|id, session| session.dead? }
20
+ end
21
+ end
22
+
23
+ #
24
+ # Creates a new application. The block, when called, must
25
+ # return a new Session instance.
26
+ #
27
+ # Wee::Application.new { Wee::Session.new(root_component) }
28
+ #
29
+ def initialize(max_sessions=10_000, &block)
30
+ @session_factory = block || raise(ArgumentError)
31
+ @session_ids ||= Wee::IdGenerator::Secure.new
32
+ @sessions = SessionCache.new(max_sessions)
33
+ @mutex = Mutex.new
34
+ end
35
+
36
+ #
37
+ # Garbage collect dead sessions
38
+ #
39
+ def cleanup_sessions
40
+ @mutex.synchronize { @sessions.garbage_collect }
41
+ end
42
+
43
+ #
44
+ # Handles a web request
45
+ #
46
+ def call(env)
47
+ request = Wee::Request.new(env)
48
+
49
+ if request.session_id
50
+ session = @mutex.synchronize { @sessions.fetch(request.session_id) }
51
+ if session and session.alive?
52
+ session.call(env)
53
+ else
54
+ url = request.build_url(:session_id => nil, :page_id => nil)
55
+ Wee::RefreshResponse.new("Invalid or expired session", url).finish
56
+ end
57
+ else
58
+ session = new_session()
59
+ url = request.build_url(:session_id => session.id, :page_id => nil)
60
+ Wee::RedirectResponse.new(url).finish
61
+ end
62
+ end
63
+
64
+ protected
65
+
66
+ def new_session
67
+ session = @session_factory.call
68
+ session.application = self
69
+ insert_session(session)
70
+ return session
71
+ end
72
+
73
+ def insert_session(session, retries=3)
74
+ retries.times do
75
+ @mutex.synchronize {
76
+ id = @session_ids.next
77
+ if not @sessions.has_key?(id)
78
+ @sessions.store(id, session)
79
+ session.id = id
80
+ return
81
+ end
82
+ }
83
+ end
84
+ raise
85
+ end
86
+
87
+ end # class Application
88
+
89
+ end # module Wee
@@ -0,0 +1,109 @@
1
+ module Wee
2
+
3
+ class CallbackRegistry
4
+ def initialize(prefix="")
5
+ @prefix = prefix
6
+ @next_id = 0
7
+ @callbacks = {} # {callback_id1 => callback1, callback_id2 => callback2}
8
+ @triggered = nil
9
+ @obj_map = {} # obj => [callback_id1, callback_id2, ...]
10
+ end
11
+
12
+ def empty?
13
+ @callbacks.empty?
14
+ end
15
+
16
+ def register(object, callback)
17
+ id = @next_id
18
+ @next_id += 1
19
+ @callbacks[id] = callback
20
+ (@obj_map[object] ||= []) << id
21
+ return "#{@prefix}#{id}"
22
+ end
23
+
24
+ def unregister(object)
25
+ if arr = @obj_map.delete(object)
26
+ arr.each {|id| @callbacks.delete(id) }
27
+ end
28
+ end
29
+
30
+ #
31
+ # NOTE that if fields named "xxx" and "xxx.yyy" occur, the value of
32
+ # @fields['xxx'] is { nil => ..., 'yyy' => ... }. This is required
33
+ # to make image buttons work correctly.
34
+ #
35
+ def prepare_triggered(ids_and_values)
36
+ @triggered = {}
37
+ ids_and_values.each do |id, value|
38
+ if id =~ /^#{@prefix}(\d+)([.](.*))?$/
39
+ id, suffix = Integer($1), $3
40
+ next unless @callbacks[id]
41
+
42
+ if @triggered[id].kind_of?(Hash)
43
+ @triggered[id][suffix] = value
44
+ elsif suffix
45
+ @triggered[id] = {nil => @triggered[id], suffix => value}
46
+ else
47
+ @triggered[id] = value
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ def reset_triggered
54
+ @triggered = nil
55
+ end
56
+
57
+ def each_triggered(object)
58
+ if ary = @obj_map[object]
59
+ for id in ary
60
+ yield @callbacks[id], @triggered[id] if @triggered.has_key?(id)
61
+ end
62
+ end
63
+ end
64
+
65
+ def each_triggered_call_with_value(object)
66
+ if ary = @obj_map[object]
67
+ for id in ary
68
+ @callbacks[id].call(@triggered[id]) if @triggered.has_key?(id)
69
+ end
70
+ end
71
+ end
72
+
73
+ def first_triggered(object)
74
+ if ary = @obj_map[object]
75
+ for id in ary
76
+ return @callbacks[id] if @triggered.has_key?(id)
77
+ end
78
+ end
79
+ return nil
80
+ end
81
+
82
+ end # class CallbackRegistry
83
+
84
+ class Callbacks
85
+ attr_reader :input_callbacks
86
+ attr_reader :action_callbacks
87
+
88
+ def initialize
89
+ @input_callbacks = CallbackRegistry.new("")
90
+ @action_callbacks = CallbackRegistry.new("a")
91
+ end
92
+
93
+ def unregister(object)
94
+ @input_callbacks.unregister(object)
95
+ @action_callbacks.unregister(object)
96
+ end
97
+
98
+ def with_triggered(ids_and_values)
99
+ @input_callbacks.prepare_triggered(ids_and_values)
100
+ @action_callbacks.prepare_triggered(ids_and_values)
101
+ yield
102
+ ensure
103
+ @input_callbacks.reset_triggered
104
+ @action_callbacks.reset_triggered
105
+ end
106
+
107
+ end # class Callbacks
108
+
109
+ end # module Wee
@@ -0,0 +1,363 @@
1
+ require 'wee/presenter'
2
+ require 'wee/decoration'
3
+
4
+ module Wee
5
+
6
+ #
7
+ # The base class of all components. You should at least overwrite method
8
+ # #render in your own subclasses.
9
+ #
10
+ class Component < Presenter
11
+
12
+ #
13
+ # Constructs a new instance of the component.
14
+ #
15
+ # Overwrite this method when you want to use it both as a root component
16
+ # and as a non-root component. Here you can add neccessary decorations
17
+ # when used as root component, as for example a PageDecoration or a
18
+ # FormDecoration.
19
+ #
20
+ # By default this methods adds no decoration.
21
+ #
22
+ # See also class RootComponent.
23
+ #
24
+ def self.instanciate(*args, &block)
25
+ new(*args, &block)
26
+ end
27
+
28
+ #
29
+ # Return an array of classes onto which the current component depends.
30
+ # Right now this is only used to determine the required ExternalResources.
31
+ #
32
+ def self.depends
33
+ []
34
+ end
35
+
36
+ #
37
+ # Initializes a newly created component.
38
+ #
39
+ def initialize
40
+ end
41
+
42
+ #
43
+ # This method renders the content of the component.
44
+ #
45
+ # *OVERWRITE* this method in your own component classes to implement the
46
+ # view. By default this method does nothing!
47
+ #
48
+ # [+r+]
49
+ # An instance of class <tt>renderer_class()</tt>
50
+ #
51
+ def render(r)
52
+ end
53
+
54
+ #
55
+ # Take snapshots of objects that should correctly be backtracked.
56
+ #
57
+ # Backtracking means that you can go back in time of the components' state.
58
+ # Therefore it is neccessary to take snapshots of those objects that want to
59
+ # participate in backtracking. Taking snapshots of the whole component tree
60
+ # would be too expensive and unflexible. Note that methods
61
+ # <i>take_snapshot</i> and <i>restore_snapshot</i> are called for those
62
+ # objects to take the snapshot (they behave like <i>marshal_dump</i> and
63
+ # <i>marshal_load</i>). Overwrite them if you want to define special
64
+ # behaviour.
65
+ #
66
+ # By default only the decoration chain is backtracked. This is
67
+ # required to correctly backtrack called components. To disable
68
+ # backtracking of the decorations, change method
69
+ # Component#state_decoration to a no-operation:
70
+ #
71
+ # def state_decoration(s)
72
+ # # nothing here
73
+ # end
74
+ #
75
+ # [+s+]
76
+ # An object of class State
77
+ #
78
+ def state(s)
79
+ state_decoration(s)
80
+ for child in self.children
81
+ child.decoration.state(s)
82
+ end
83
+ end
84
+
85
+ NO_CHILDREN = [].freeze
86
+ #
87
+ # Return all child components.
88
+ #
89
+ # *OVERWRITE* this method and return all child components
90
+ # collected in an array.
91
+ #
92
+ def children
93
+ return NO_CHILDREN
94
+ end
95
+
96
+ #
97
+ # Process and invoke all input callbacks specified for this component
98
+ # and all of it's child components.
99
+ #
100
+ # Returns the action callback to be invoked.
101
+ #
102
+ def process_callbacks(callbacks)
103
+ callbacks.input_callbacks.each_triggered_call_with_value(self)
104
+
105
+ action_callback = nil
106
+
107
+ # process callbacks of all children
108
+ for child in self.children
109
+ if act = child.decoration.process_callbacks(callbacks)
110
+ raise "Duplicate action callback" if action_callback
111
+ action_callback = act
112
+ end
113
+ end
114
+
115
+ if act = callbacks.action_callbacks.first_triggered(self)
116
+ raise "Duplicate action callback" if action_callback
117
+ action_callback = act
118
+ end
119
+
120
+ return action_callback
121
+ end
122
+
123
+ def state_decoration(s)
124
+ s.add_ivar(self, :@decoration, @decoration)
125
+ end
126
+
127
+ protected :state_decoration
128
+
129
+ # -------------------------------------------------------------
130
+ # Decoration Methods
131
+ # -------------------------------------------------------------
132
+
133
+ def decoration=(d) @decoration = d end
134
+ def decoration() @decoration || self end
135
+
136
+ #
137
+ # Iterates over all decorations
138
+ # (note that the component itself is excluded)
139
+ #
140
+ def each_decoration # :yields: decoration
141
+ d = @decoration
142
+ while d and d != self
143
+ yield d
144
+ d = d.next
145
+ end
146
+ end
147
+
148
+ #
149
+ # Searches a decoration in the decoration chain
150
+ #
151
+ def find_decoration
152
+ each_decoration {|d| yield d and return d }
153
+ return nil
154
+ end
155
+
156
+ #
157
+ # Adds decoration +d+ to the decoration chain.
158
+ #
159
+ # A global decoration is added in front of the decoration chain, a local
160
+ # decoration is added in front of all other local decorations but after all
161
+ # global decorations.
162
+ #
163
+ # Returns: +self+
164
+ #
165
+ def add_decoration(d)
166
+ if d.global?
167
+ d.next = self.decoration
168
+ self.decoration = d
169
+ else
170
+ last_global = nil
171
+ each_decoration {|i|
172
+ if i.global?
173
+ last_global = i
174
+ else
175
+ break
176
+ end
177
+ }
178
+ if last_global.nil?
179
+ # no global decorations specified -> add in front
180
+ d.next = self.decoration
181
+ self.decoration = d
182
+ else
183
+ # add after last_global
184
+ d.next = last_global.next
185
+ last_global.next = d
186
+ end
187
+ end
188
+
189
+ return self
190
+ end
191
+
192
+ #
193
+ # Remove decoration +d+ from the decoration chain.
194
+ #
195
+ # Returns the removed decoration or +nil+ if it did not exist in the
196
+ # decoration chain.
197
+ #
198
+ def remove_decoration(d)
199
+ if d == self.decoration # 'd' is in front
200
+ self.decoration = d.next
201
+ else
202
+ last_decoration = self.decoration
203
+ next_decoration = nil
204
+ loop do
205
+ return nil if last_decoration == self or last_decoration.nil?
206
+ next_decoration = last_decoration.next
207
+ break if d == next_decoration
208
+ last_decoration = next_decoration
209
+ end
210
+ last_decoration.next = d.next
211
+ end
212
+ d.next = nil # decoration 'd' no longer is an owner of anything!
213
+ return d
214
+ end
215
+
216
+ #
217
+ # Remove all decorations that match the block condition.
218
+ #
219
+ # Example (removes all decorations of class +HaloDecoration+):
220
+ #
221
+ # remove_decoration_if {|d| d.class == HaloDecoration}
222
+ #
223
+ def remove_decoration_if # :yields: decoration
224
+ to_remove = []
225
+ each_decoration {|d| to_remove << d if yield d}
226
+ to_remove.each {|d| remove_decoration(d)}
227
+ end
228
+
229
+ # -------------------------------------------------------------
230
+ # Call/Answer Methods
231
+ # -------------------------------------------------------------
232
+
233
+ #
234
+ # Call another component (without using continuations). The calling
235
+ # component is neither rendered nor are it's callbacks processed
236
+ # until the called component answers using method #answer.
237
+ #
238
+ # [+component+]
239
+ # The component to be called.
240
+ #
241
+ # [+return_callback+]
242
+ # Is invoked when the called component answers.
243
+ #
244
+ # <b>How it works</b>
245
+ #
246
+ # The component to be called is wrapped with an AnswerDecoration and a
247
+ # Delegate decoration. The latter is used to redirect to the called
248
+ # component. Once the decorations are installed, we end the processing of
249
+ # callbacks prematurely.
250
+ #
251
+ # When at a later point in time the called component invokes #answer, this
252
+ # will raise a AnswerDecoration::Answer exception which is catched by the
253
+ # AnswerDecoration we installed before calling this component, and as such,
254
+ # whose process_callbacks method was called before we gained control.
255
+ #
256
+ # The AnswerDecoration then invokes the <tt>answer_callback</tt> to cleanup
257
+ # the decorations we added during #call and finally passes control to the
258
+ # <tt>return_callback</tt>.
259
+ #
260
+ def call(component, &return_callback)
261
+ delegate = Delegate.new(component)
262
+ answer = AnswerDecoration.new
263
+ answer.answer_callback = UnwindCall.new(self, component, delegate, answer, &return_callback)
264
+ add_decoration(delegate)
265
+ component.add_decoration(answer)
266
+ session.send_response(nil)
267
+ end
268
+
269
+ protected :call
270
+
271
+ #
272
+ # Reverts the changes made due to Component#call. Is called when
273
+ # Component#call 'answers'.
274
+ #
275
+ class UnwindCall
276
+ def initialize(calling, called, delegate, answer, &return_callback)
277
+ @calling, @called, @delegate, @answer = calling, called, delegate, answer
278
+ @return_callback = return_callback
279
+ end
280
+
281
+ def call(answ)
282
+ @calling.remove_decoration(@delegate)
283
+ @called.remove_decoration(@answer)
284
+ @return_callback.call(*answ.args) if @return_callback
285
+ end
286
+ end
287
+
288
+ #
289
+ # Similar to method #call, but using continuations.
290
+ #
291
+ def callcc(component)
292
+ delegate = Delegate.new(component)
293
+ answer = AnswerDecoration.new
294
+
295
+ add_decoration(delegate)
296
+ component.add_decoration(answer)
297
+
298
+ answ = Kernel.callcc {|cc|
299
+ answer.answer_callback = cc
300
+ session.send_response(nil)
301
+ }
302
+ remove_decoration(delegate)
303
+ component.remove_decoration(answer)
304
+
305
+ args = answ.args
306
+ case args.size
307
+ when 0
308
+ return
309
+ when 1
310
+ return args.first
311
+ else
312
+ return *args
313
+ end
314
+ end
315
+
316
+ protected :callcc
317
+
318
+ #
319
+ # Chooses one of #call or #callcc depending on whether a block is
320
+ # given or not.
321
+ #
322
+ def call!(comp, &block)
323
+ if block
324
+ call comp, &block
325
+ else
326
+ callcc comp
327
+ end
328
+ end
329
+
330
+ protected :call!
331
+
332
+ def call_inline(&render_block)
333
+ callcc BlockComponent.new(&render_block)
334
+ end
335
+
336
+ protected :call_inline
337
+
338
+ #
339
+ # Return from a called component.
340
+ #
341
+ # NOTE that #answer never returns.
342
+ #
343
+ # See #call for a detailed description of the call/answer mechanism.
344
+ #
345
+ def answer(*args)
346
+ raise AnswerDecoration::Answer.new(args)
347
+ end
348
+
349
+ protected :answer
350
+
351
+ end # class Component
352
+
353
+ class BlockComponent < Component
354
+ def initialize(&block)
355
+ @block = block
356
+ end
357
+
358
+ def render(r)
359
+ instance_exec(r, &@block)
360
+ end
361
+ end # class BlockComponent
362
+
363
+ end # module Wee