camping 1.1 → 1.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.
@@ -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