camping 1.1 → 1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/ruby
2
+ require 'webrick/httpserver'
3
+ s = WEBrick::HTTPServer.new(:BindAddress => '0.0.0.0', :Port => 3000)
4
+ s.mount("/", WEBrick::HTTPServlet::CGIHandler, "tepee.rb")
5
+ trap( :INT ) { s.shutdown }
6
+ s.start
@@ -0,0 +1,137 @@
1
+ #!/usr/bin/ruby
2
+ $:.unshift File.dirname(__FILE__) + "/../../lib"
3
+ %w(rubygems redcloth camping acts_as_versioned).each { |lib| require lib }
4
+
5
+ Camping.goes :Tepee
6
+
7
+ module Tepee::Models
8
+ def self.schema(&block)
9
+ @@schema = block if block_given?
10
+ @@schema
11
+ end
12
+
13
+ class Page < Base
14
+ PAGE_LINK = /\[\[([^\]|]*)[|]?([^\]]*)\]\]/
15
+ validates_uniqueness_of :title
16
+ before_save { |r| r.title = r.title.underscore }
17
+ acts_as_versioned
18
+ end
19
+ end
20
+
21
+ Tepee::Models.schema do
22
+ create_table :pages, :force => true do |t|
23
+ t.column :title, :string, :limit => 255
24
+ t.column :body, :text
25
+ end
26
+ Tepee::Models::Page.create_versioned_table
27
+ end
28
+
29
+ module Tepee::Controllers
30
+ class Index < R '/'
31
+ def get
32
+ redirect Show, 'home_page'
33
+ end
34
+ end
35
+
36
+ class List < R '/list'
37
+ def get
38
+ @pages = Page.find :all, :order => 'title'
39
+ render :list
40
+ end
41
+ end
42
+
43
+ class Show < R '/s/(\w+)', '/s/(\w+)/(\d+)'
44
+ def get page_name, version = nil
45
+ redirect(Edit, page_name, 1) and return unless @page = Page.find_by_title(page_name)
46
+ @version = (version.nil? or version == @page.version.to_s) ? @page : @page.versions.find_by_version(version)
47
+ render :show
48
+ end
49
+ end
50
+
51
+ class Edit < R '/e/(\w+)/(\d+)', '/e/(\w+)'
52
+ def get page_name, version = nil
53
+ @page = Page.find_or_create_by_title(page_name)
54
+ @page = @page.versions.find_by_version(version) unless version.nil? or version == @page.version.to_s
55
+ render :edit
56
+ end
57
+
58
+ def post page_name
59
+ Page.find_or_create_by_title(page_name).update_attributes :body => input.post_body and redirect Show, page_name
60
+ end
61
+ end
62
+ end
63
+
64
+ module Tepee::Views
65
+ def layout
66
+ html do
67
+ head do
68
+ title 'test'
69
+ end
70
+ body do
71
+ p do
72
+ small do
73
+ span "welcome to " ; a 'tepee', :href => "http://code.whytheluckystiff.net/svn/camping/trunk/examples/tepee/"
74
+ span '. go ' ; a 'home', :href => R(Show, 'home_page')
75
+ span '. list all ' ; a 'pages', :href => R(List)
76
+ end
77
+ end
78
+ div.content do
79
+ self << yield
80
+ end
81
+ end
82
+ end
83
+ end
84
+
85
+ def show
86
+ h1 @page.title
87
+ div { _markup @version.body }
88
+ p do
89
+ a 'edit', :href => R(Edit, @version.title, @version.version)
90
+ a 'back', :href => R(Show, @version.title, @version.version-1) unless @version.version == 1
91
+ a 'next', :href => R(Show, @version.title, @version.version+1) unless @version.version == @page.version
92
+ a 'current', :href => R(Show, @version.title) unless @version.version == @page.version
93
+ end
94
+ end
95
+
96
+ def edit
97
+ form :method => 'post', :action => R(Edit, @page.title) do
98
+ p do
99
+ label 'Body' ; br
100
+ textarea @page.body, :name => 'post_body', :rows => 50, :cols => 100
101
+ end
102
+
103
+ p do
104
+ input :type => 'submit'
105
+ a 'cancel', :href => R(Show, @page.title, @page.version)
106
+ end
107
+ end
108
+ end
109
+
110
+ def list
111
+ h1 'all pages'
112
+ ul { @pages.each { |p| li { a p.title, :href => R(Show, p.title) } } }
113
+ end
114
+
115
+ def _markup body
116
+ return '' if body.blank?
117
+ body.gsub!(Tepee::Models::Page::PAGE_LINK) do
118
+ page = title = $1.underscore
119
+ title = $2 unless $2.empty?
120
+ if Tepee::Models::Page.find(:all, :select => 'title').collect { |p| p.title }.include?(page)
121
+ %Q{<a href="#{R Show, page}">#{title}</a>}
122
+ else
123
+ %Q{<span>#{title}<a href="#{R Edit, page, 1}">?</a></span>}
124
+ end
125
+ end
126
+ RedCloth.new(body, [ :hard_breaks ]).to_html
127
+ end
128
+ end
129
+
130
+ db_exists = File.exists?('tepee.db')
131
+ Tepee::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'tepee.db'
132
+ Tepee::Models::Base.logger = Logger.new('camping.log')
133
+ ActiveRecord::Schema.define(&Tepee::Models.schema) unless db_exists
134
+
135
+ if __FILE__ == $0
136
+ Tepee.run
137
+ end
@@ -0,0 +1,468 @@
1
+ %w[rubygems active_record markaby metaid ostruct tempfile].each { |lib| require lib }
2
+
3
+ # == Camping
4
+ #
5
+ # The camping module contains three modules for separating your application:
6
+ #
7
+ # * Camping::Models for storing classes derived from ActiveRecord::Base.
8
+ # * Camping::Controllers for storing controller classes, which map URLs to code.
9
+ # * Camping::Views for storing methods which generate HTML.
10
+ #
11
+ # Of use to you is also one module for storing helpful additional methods:
12
+ #
13
+ # * Camping::Helpers which can be used in controllers and views.
14
+ #
15
+ # == The postamble
16
+ #
17
+ # Most Camping applications contain the entire application in a single script.
18
+ # The script begins by requiring Camping, then fills each of the three modules
19
+ # described above with classes and methods. Finally, a postamble puts the wheels
20
+ # in motion.
21
+ #
22
+ # if __FILE__ == $0
23
+ # Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
24
+ # Camping::Models::Base.logger = Logger.new('camping.log')
25
+ # Camping.run
26
+ # end
27
+ #
28
+ # In the postamble, your job is to setup Camping::Models::Base (see: ActiveRecord::Base)
29
+ # and call Camping::run in a request loop. The above postamble is for a standard
30
+ # CGI setup, where the web server manages the request loop and calls the script once
31
+ # for every request.
32
+ #
33
+ # For other configurations, see
34
+ # http://code.whytheluckystiff.net/camping/wiki/PostAmbles
35
+ module Camping
36
+ C = self
37
+ S = File.read(__FILE__).gsub(/_{2}FILE_{2}/,__FILE__.dump)
38
+
39
+ # Helpers contains methods available in your controllers and views.
40
+ module Helpers
41
+ # From inside your controllers and views, you will often need to figure out
42
+ # the route used to get to a certain controller +c+. Pass the controller class
43
+ # and any arguments into the R method, a string containing the route will be
44
+ # returned to you.
45
+ #
46
+ # Assuming you have a specific route in an edit controller:
47
+ #
48
+ # class Edit < R '/edit/(\d+)'
49
+ #
50
+ # A specific route to the Edit controller can be built with:
51
+ #
52
+ # R(Edit, 1)
53
+ #
54
+ # Which outputs: <tt>/edit/1</tt>.
55
+ #
56
+ # You may also pass in a model object and the ID of the object will be used.
57
+ #
58
+ # If a controller has many routes, the route will be selected if it is the
59
+ # first in the routing list to have the right number of arguments.
60
+ #
61
+ # Keep in mind that this route doesn't include the root path. Occassionally
62
+ # you will need to use <tt>/</tt> (the slash method above).
63
+ def R(c,*args)
64
+ p = /\(.+?\)/
65
+ args.inject(c.urls.detect{|x|x.scan(p).size==args.size}.dup){|str,a|
66
+ str.sub(p,(a.method(a.class.primary_key)[] rescue a).to_s)
67
+ }
68
+ end
69
+ # Shows AR validation errors for the object passed.
70
+ # There is no output if there are no errors.
71
+ #
72
+ # An example might look like:
73
+ #
74
+ # errors_for @post
75
+ #
76
+ # Might (depending on actual data) render something like this in Markaby:
77
+ #
78
+ # ul.errors do
79
+ # li "Body can't be empty"
80
+ # li "Title must be unique"
81
+ # end
82
+ #
83
+ # Add a simple ul.errors {color:red; font-weight:bold;} CSS rule and you
84
+ # have built-in, usable error checking in only one line of code. :-)
85
+ #
86
+ # See AR validation documentation for details on validations.
87
+ def errors_for(o); ul.errors { o.errors.each_full { |er| li er } } unless o.errors.empty?; end
88
+ # Simply builds the complete URL from a relative or absolute path +p+. If your
89
+ # application is running from <tt>/blog</tt>:
90
+ #
91
+ # self / "/view/1" #=> "/blog/view/1"
92
+ # self / "styles.css" #=> "styles.css"
93
+ # self / R(Edit, 1) #=> "/blog/edit/1"
94
+ #
95
+ def /(p); p[/^\//]?@root+p:p end
96
+ end
97
+
98
+ # Controllers is a module for placing classes which handle URLs. This is done
99
+ # by defining a route to each class using the Controllers::R method.
100
+ #
101
+ # module Camping::Controllers
102
+ # class Edit < R '/edit/(\d+)'
103
+ # def get; end
104
+ # def post; end
105
+ # end
106
+ # end
107
+ #
108
+ # If no route is set, Camping will guess the route from the class name.
109
+ # The rule is very simple: the route becomes a slash followed by the lowercased
110
+ # class name. See Controllers::D for the complete rules of dispatch.
111
+ #
112
+ # == Special classes
113
+ #
114
+ # There are two special classes used for handling 404 and 500 errors. The
115
+ # NotFound class handles URLs not found. The ServerError class handles exceptions
116
+ # uncaught by your application.
117
+ module Controllers
118
+ # Controllers::Base is built into each controller by way of the generic routing
119
+ # class Controllers::R. In some ways, this class is trying to do too much, but
120
+ # it saves code for all the glue to stay in one place.
121
+ #
122
+ # Forgivable, considering that it's only really a handful of methods and accessors.
123
+ #
124
+ # == Treating controller methods like Response objects
125
+ #
126
+ # Camping originally came with a barebones Response object, but it's often much more readable
127
+ # to just use your controller as the response.
128
+ #
129
+ # Go ahead and alter the status, cookies, headers and body instance variables as you
130
+ # see fit in order to customize the response.
131
+ #
132
+ # module Camping::Controllers
133
+ # class SoftLink
134
+ # def get
135
+ # redirect "/"
136
+ # end
137
+ # end
138
+ # end
139
+ #
140
+ # Is equivalent to:
141
+ #
142
+ # module Camping::Controllers
143
+ # class SoftLink
144
+ # def get
145
+ # @status = 302
146
+ # @headers['Location'] = "/"
147
+ # end
148
+ # end
149
+ # end
150
+ #
151
+ module Base
152
+ include Helpers
153
+ attr_accessor :input, :cookies, :headers, :body, :status, :root
154
+ # Display a view, calling it by its method name +m+. If a <tt>layout</tt>
155
+ # method is found in Camping::Views, it will be used to wrap the HTML.
156
+ #
157
+ # module Camping::Controllers
158
+ # class Show
159
+ # def get
160
+ # @posts = Post.find :all
161
+ # render :index
162
+ # end
163
+ # end
164
+ # end
165
+ #
166
+ def render(m); end; undef_method :render
167
+
168
+ # Any stray method calls will be passed to Markaby. This means you can reply
169
+ # with HTML directly from your controller for quick debugging.
170
+ #
171
+ # module Camping::Controllers
172
+ # class Info
173
+ # def get; code ENV.inspect end
174
+ # end
175
+ # end
176
+ #
177
+ # If you have a <tt>layout</tt> method in Camping::Views, it will be used to
178
+ # wrap the HTML.
179
+ def method_missing(m, *args, &blk)
180
+ str = m==:render ? markaview(*args, &blk):eval("markaby.#{m}(*args, &blk)")
181
+ str = markaview(:layout) { str } rescue nil
182
+ r(200, str.to_s)
183
+ end
184
+
185
+ # Formulate a redirect response: a 302 status with <tt>Location</tt> header
186
+ # and a blank body. If +c+ is a string, the root path will be added. If
187
+ # +c+ is a controller class, Helpers::R will be used to route the redirect
188
+ # and the root path will be added.
189
+ #
190
+ # So, given a root of <tt>/articles</tt>:
191
+ #
192
+ # redirect "view/12" # redirects to "/articles/view/12"
193
+ # redirect View, 12 # redirects to "/articles/view/12"
194
+ #
195
+ def redirect(c, *args)
196
+ c = R(c,*args) if c.respond_to? :urls
197
+ r(302, '', 'Location' => self/c)
198
+ end
199
+
200
+ # A quick means of setting this controller's status, body and headers.
201
+ # Used internally by Camping, but... by all means...
202
+ #
203
+ # r(302, '', 'Location' => self / "/view/12")
204
+ #
205
+ # Is equivalent to:
206
+ #
207
+ # redirect "/view/12"
208
+ #
209
+ def r(s, b, h = {}); @status = s; @headers.merge!(h); @body = b; end
210
+
211
+ def service(r, e, m, a) #:nodoc:
212
+ @status, @headers, @root = 200, {}, e['SCRIPT_NAME']
213
+ cook = C.cookie_parse(e['HTTP_COOKIE'] || e['COOKIE'])
214
+ qs = C.qs_parse(e['QUERY_STRING'])
215
+ if "POST" == m
216
+ inp = r.read(e['CONTENT_LENGTH'].to_i)
217
+ if %r|\Amultipart/form-data.*boundary=\"?([^\";,]+)|n.match(e['CONTENT_TYPE'])
218
+ b = "--#$1"
219
+ inp.split(/(?:\r?\n|\A)#{ Regexp::quote( b ) }(?:--)?\r\n/m).each { |pt|
220
+ h,v=pt.split("\r\n\r\n",2);fh={}
221
+ [:name, :filename].each { |x|
222
+ fh[x] = $1 if h =~ /^Content-Disposition: form-data;.*(?:\s#{x}="([^"]+)")/m
223
+ }
224
+ fn = fh[:name]
225
+ if fh[:filename]
226
+ fh[:type]=$1 if h =~ /^Content-Type: (.+?)(\r\n|\Z)/m
227
+ fh[:tempfile]=Tempfile.new("#{C}").instance_eval {binmode;write v;rewind;self}
228
+ else
229
+ fh=v
230
+ end
231
+ qs[fn]=fh if fn
232
+ }
233
+ else
234
+ qs.merge!(C.qs_parse(inp))
235
+ end
236
+ end
237
+ @cookies, @input = [cook, qs].map{|_|OpenStruct.new(_)}
238
+
239
+ @body = method( m.downcase ).call(*a)
240
+ @headers['Set-Cookie'] = @cookies.marshal_dump.map { |k,v| "#{k}=#{C.escape(v)}; path=/" if v != cook[k] }.compact
241
+ self
242
+ end
243
+ def to_s #:nodoc:
244
+ "Status: #{@status}\n#{{'Content-Type'=>'text/html'}.merge(@headers).map{|k,v|v.to_a.map{|v2|"#{k}: #{v2}"}}.flatten.join("\n")}\n\n#{@body}"
245
+ end
246
+ private
247
+ def markaby
248
+ Mab.new( instance_variables.map { |iv|
249
+ [iv[1..-1], instance_variable_get(iv)] }, {} )
250
+ end
251
+ def markaview(m, *args, &blk)
252
+ b=markaby
253
+ b.method(m).call(*args, &blk)
254
+ b.to_s
255
+ end
256
+ end
257
+
258
+ # The R class is the parent class for all controllers and ensures they all get the Base mixin.
259
+ class R; include Base end
260
+
261
+ # The NotFound class is a special controller class for handling 404 errors, in case you'd
262
+ # like to alter the appearance of the 404. The path is passed in as +p+.
263
+ #
264
+ # module Camping::Controllers
265
+ # class NotFound
266
+ # def get(p)
267
+ # @status = 404
268
+ # div do
269
+ # h1 'Camping Problem!'
270
+ # h2 "#{p} not found"
271
+ # end
272
+ # end
273
+ # end
274
+ # end
275
+ #
276
+ class NotFound; def get(p); r(404, div{h1("#{C} Problem!")+h2("#{p} not found")}); end end
277
+
278
+ # The ServerError class is a special controller class for handling many (but not all) 500 errors.
279
+ # If there is a parse error in Camping or in your application's source code, it will not be caught
280
+ # by Camping. The controller class +k+ and request method +m+ (GET, POST, etc.) where the error
281
+ # took place are passed in, along with the Exception +e+ which can be mined for useful info.
282
+ #
283
+ # module Camping::Controllers
284
+ # class ServerError
285
+ # def get(k,m,e)
286
+ # @status = 500
287
+ # div do
288
+ # h1 'Camping Problem!'
289
+ # h2 "in #{k}.#{m}"
290
+ # h3 "#{e.class} #{e.message}:"
291
+ # ul do
292
+ # e.backtrace.each do |bt|
293
+ # li bt
294
+ # end
295
+ # end
296
+ # end
297
+ # end
298
+ # end
299
+ # end
300
+ #
301
+ class ServerError; include Base; def get(k,m,e); r(500, markaby.div{ h1 "#{C} Problem!"; h2 "#{k}.#{m}"; h3 "#{e.class} #{e.message}:"; ul { e.backtrace.each { |bt| li bt } } }) end end
302
+
303
+ class << self
304
+ # Add routes to a controller class by piling them into the R method.
305
+ #
306
+ # module Camping::Controllers
307
+ # class Edit < R '/edit/(\d+)', '/new'
308
+ # def get(id)
309
+ # if id # edit
310
+ # else # new
311
+ # end
312
+ # end
313
+ # end
314
+ # end
315
+ #
316
+ # You will need to use routes in either of these cases:
317
+ #
318
+ # * You want to assign multiple routes to a controller.
319
+ # * You want your controller to receive arguments.
320
+ #
321
+ # Most of the time the rules inferred by dispatch method Controllers::D will get you
322
+ # by just fine.
323
+ def R(*urls); Class.new(R) { meta_def(:inherited) { |c| c.meta_def(:urls) { urls } } }; end
324
+
325
+ # Dispatch routes to controller classes. Classes are searched in no particular order.
326
+ # For each class, routes are checked for a match based on their order in the routing list
327
+ # given to Controllers::R. If no routes were given, the dispatcher uses a slash followed
328
+ # by the name of the controller lowercased.
329
+ def D(path)
330
+ constants.inject(nil) do |d,c|
331
+ k = const_get(c)
332
+ k.meta_def(:urls){["/#{c.downcase}"]}if !(k<R)
333
+ d||([k, $~[1..-1]] if k.urls.find { |x| path =~ /^#{x}\/?$/ })
334
+ end||[NotFound, [path]]
335
+ end
336
+ end
337
+ end
338
+
339
+ class << self
340
+ # When you are running many applications, you may want to create independent
341
+ # modules for each Camping application. Namespaces for each. Camping::goes
342
+ # defines a toplevel constant with the whole MVC rack inside.
343
+ #
344
+ # require 'camping'
345
+ # Camping.goes :Blog
346
+ #
347
+ # module Blog::Controllers; ... end
348
+ # module Blog::Models; ... end
349
+ # module Blog::Views; ... end
350
+ #
351
+ def goes(m)
352
+ eval(S.gsub(/Camping/,m.to_s),TOPLEVEL_BINDING)
353
+ end
354
+
355
+ # URL escapes a string.
356
+ #
357
+ # Camping.escape("I'd go to the museum straightway!")
358
+ # #=> "I%27d+go+to+the+museum+straightway%21"
359
+ #
360
+
361
+ def escape(s); s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/n){'%'+$1.unpack('H2'*$1.size).join('%').upcase}.tr(' ', '+') end
362
+ # Unescapes a URL-encoded string.
363
+ #
364
+ # Camping.unescape("I%27d+go+to+the+museum+straightway%21")
365
+ # #=> "I'd go to the museum straightway!"
366
+ #
367
+ def unescape(s); s.tr('+', ' ').gsub(/((?:%[0-9a-fA-F]{2})+)/n){[$1.delete('%')].pack('H*')} end
368
+
369
+ # Parses a query string into an OpenStruct object.
370
+ #
371
+ # input = Camping.qs_parse("name=Philarp+Tremain&hair=sandy+blonde")
372
+ # input.name
373
+ # #=> "Philarp Tremaine"
374
+ #
375
+ def qs_parse(qs, d = '&;'); (qs||'').split(/[#{d}] */n).
376
+ inject({}){|hsh, p|k, v = p.split('=',2).map {|v| unescape(v)}; hsh[k] = v unless v.blank?; hsh} end
377
+
378
+ # Parses a string of cookies from the <tt>Cookie</tt> header.
379
+ def cookie_parse(s); c = qs_parse(s, ';,'); end
380
+
381
+ # Fields a request through Camping. For traditional CGI applications, the method can be
382
+ # executed without arguments.
383
+ #
384
+ # if __FILE__ == $0
385
+ # Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
386
+ # Camping::Models::Base.logger = Logger.new('camping.log')
387
+ # Camping.run
388
+ # end
389
+ #
390
+ # For FastCGI and Webrick-loaded applications, you will need to use a request loop, with <tt>run</tt>
391
+ # at the center, passing in the read +r+ and write +w+ streams. You will also need to mimick or
392
+ # replace <tt>ENV</tt> as part of your wrapper.
393
+ #
394
+ # if __FILE__ == $0
395
+ # require 'fcgi'
396
+ # Camping::Models::Base.establish_connection :adapter => 'sqlite3', :database => 'blog3.db'
397
+ # Camping::Models::Base.logger = Logger.new('camping.log')
398
+ # FCGI.each do |req|
399
+ # ENV.replace req.env
400
+ # Camping.run req.in, req.out
401
+ # req.finish
402
+ # end
403
+ # end
404
+ # end
405
+ #
406
+ def run(r=$stdin,w=$stdout)
407
+ w <<
408
+ begin
409
+ k, a = Controllers.D "/#{ENV['PATH_INFO']}".gsub(%r!/+!,'/')
410
+ m = ENV['REQUEST_METHOD']||"GET"
411
+ k.class_eval { include C; include Controllers::Base; include Models }
412
+ o = k.new
413
+ o.service(r, ENV, m, a)
414
+ rescue => e
415
+ Controllers::ServerError.new.service(r, ENV, "GET", [k,m,e])
416
+ end
417
+ end
418
+ end
419
+
420
+ # Models is an empty Ruby module for housing model classes derived
421
+ # from ActiveRecord::Base. As a shortcut, you may derive from Base
422
+ # which is an alias for ActiveRecord::Base.
423
+ #
424
+ # module Camping::Models
425
+ # class Post < Base; belongs_to :user end
426
+ # class User < Base; has_many :posts end
427
+ # end
428
+ #
429
+ # == Where Models are Used
430
+ #
431
+ # Models are used in your controller classes. However, if your model class
432
+ # name conflicts with a controller class name, you will need to refer to it
433
+ # using the Models module.
434
+ #
435
+ # module Camping::Controllers
436
+ # class Post < R '/post/(\d+)'
437
+ # def get(post_id)
438
+ # @post = Models::Post.find post_id
439
+ # render :index
440
+ # end
441
+ # end
442
+ # end
443
+ #
444
+ # Models cannot be referred to in Views at this time.
445
+ module Models; end
446
+
447
+ # Views is an empty module for storing methods which create HTML. The HTML is described
448
+ # using the Markaby language.
449
+ #
450
+ # == Using the layout method
451
+ #
452
+ # If your Views module has a <tt>layout</tt> method defined, it will be called with a block
453
+ # which will insert content from your view.
454
+ module Views; include Controllers; include Helpers end
455
+ Models::Base = ActiveRecord::Base
456
+
457
+ # The Mab class wraps Markaby, allowing it to run methods from Camping::Views
458
+ # and also to replace :href and :action attributes in tags by prefixing the root
459
+ # path.
460
+ class Mab < Markaby::Builder
461
+ include Views
462
+ def tag!(*g,&b)
463
+ h=g[-1]
464
+ [:href,:action].each{|a|(h[a]=self/h[a])rescue 0}
465
+ super
466
+ end
467
+ end
468
+ end