root/trunk/radiant/app/models/standard_tags.rb

Revision 826, 23.9 kB (checked in by seancribbs, 4 months ago)

Add <r:meta /> tag and appropriate fields to pages table.

Line 
1 module StandardTags
2  
3   include Radiant::Taggable
4   include LocalTime
5    
6   class TagError < StandardError; end
7  
8   desc %{
9     Causes the tags referring to a page's attributes to refer to the current page.
10     
11     *Usage:*
12     <pre><code><r:page>...</r:page></code></pre>
13   }
14   tag 'page' do |tag|
15     tag.locals.page = tag.globals.page
16     tag.expand
17   end
18  
19   [:breadcrumb, :slug, :title].each do |method|
20     desc %{
21       Renders the @#{method}@ attribute of the current page.
22     }
23     tag method.to_s do |tag|
24       tag.locals.page.send(method)
25     end
26   end
27  
28   desc %{
29     Renders the @url@ attribute of the current page.
30   }
31   tag 'url' do |tag|
32     relative_url_for(tag.locals.page.url, tag.globals.page.request)
33   end
34  
35   desc %{
36     Gives access to a page's children.
37     
38     *Usage:*
39     <pre><code><r:children>...</r:children></code></pre>
40   }
41   tag 'children' do |tag|
42     tag.locals.children = tag.locals.page.children
43     tag.expand
44   end
45  
46   desc %{
47     Renders the total number of children.
48   }
49   tag 'children:count' do |tag|
50     tag.locals.children.count
51   end
52  
53   desc %{
54     Returns the first child. Inside this tag all page attribute tags are mapped to
55     the first child. Takes the same ordering options as @<r:children:each>@.
56     
57     *Usage:*
58     <pre><code><r:children:first>...</r:children:first></code></pre>
59   }
60   tag 'children:first' do |tag|
61     options = children_find_options(tag)
62     children = tag.locals.children.find(:all, options)
63     if first = children.first
64       tag.locals.page = first
65       tag.expand
66     end
67   end
68  
69   desc %{
70     Returns the last child. Inside this tag all page attribute tags are mapped to
71     the last child. Takes the same ordering options as @<r:children:each>@.
72     
73     *Usage:*
74     <pre><code><r:children:last>...</r:children:last></code></pre>
75   }
76   tag 'children:last' do |tag|
77     options = children_find_options(tag)
78     children = tag.locals.children.find(:all, options)
79     if last = children.last
80       tag.locals.page = last
81       tag.expand
82     end
83   end
84  
85   desc %{
86     Cycles through each of the children. Inside this tag all page attribute tags
87     are mapped to the current child page.
88     
89     *Usage:*
90     <pre><code><r:children:each [offset="number"] [limit="number"] [by="attribute"] [order="asc|desc"]
91      [status="draft|reviewed|published|hidden|all"]>
92      ...
93     </r:children:each>
94     </code></pre>
95   }
96   tag 'children:each' do |tag|
97     options = children_find_options(tag)
98     result = []
99     children = tag.locals.children
100     tag.locals.previous_headers = {}
101     children.find(:all, options).each do |item|
102       tag.locals.child = item
103       tag.locals.page = item
104       result << tag.expand
105     end
106     result
107   end
108  
109   desc %{
110     Page attribute tags inside of this tag refer to the current child. This is occasionally
111     useful if you are inside of another tag (like &lt;r:find&gt;) and need to refer back to the
112     current child.
113     
114     *Usage:*
115     <pre><code><r:children:each>
116       <r:child>...</r:child>
117     </r:children:each>
118     </code></pre>
119   }
120   tag 'children:each:child' do |tag|
121     tag.locals.page = tag.locals.child
122     tag.expand
123   end
124  
125   desc %{
126     Renders the tag contents only if the contents do not match the previous header. This
127     is extremely useful for rendering date headers for a list of child pages.
128     
129     If you would like to use several header blocks you may use the @name@ attribute to
130     name the header. When a header is named it will not restart until another header of
131     the same name is different.
132     
133     Using the @restart@ attribute you can cause other named headers to restart when the
134     present header changes. Simply specify the names of the other headers in a semicolon
135     separated list.
136     
137     *Usage:*
138     <pre><code><r:children:each>
139       <r:header [name="header_name"] [restart="name1[;name2;...]"]>
140         ...
141       </r:header>
142     </r:children:each>
143     </code></pre>
144   }
145   tag 'children:each:header' do |tag|
146     previous_headers = tag.locals.previous_headers
147     name = tag.attr['name'] || :unnamed
148     restart = (tag.attr['restart'] || '').split(';')
149     header = tag.expand
150     unless header == previous_headers[name]
151       previous_headers[name] = header
152       unless restart.empty?
153         restart.each do |n|
154           previous_headers[n] = nil
155         end
156       end
157       header
158     end
159   end
160  
161   desc %{
162     Page attribute tags inside this tag refer to the parent of the current page.
163     
164     *Usage:*
165     <pre><code><r:parent>...</r:parent></code></pre>
166   }
167   tag "parent" do |tag|
168     parent = tag.locals.page.parent
169     tag.locals.page = parent
170     tag.expand if parent
171   end
172  
173   desc %{
174     Renders the contained elements only if the current contextual page has a parent, i.e.
175     is not the root page.
176     
177     *Usage:*
178     <pre><code><r:if_parent>...</r:if_parent></code></pre>
179   }
180   tag "if_parent" do |tag|
181     parent = tag.locals.page.parent
182     tag.expand if parent
183   end
184  
185   desc %{
186     Renders the contained elements only if the current contextual page has no parent, i.e.
187     is the root page.
188     
189     *Usage:*
190     <pre><code><r:unless_parent>...</r:unless_parent></code></pre>
191   }
192   tag "unless_parent" do |tag|
193     parent = tag.locals.page.parent
194     tag.expand unless parent
195   end
196  
197   desc %{
198     Renders the contained elements only if the current contextual page has one or
199     more child pages.  The @status@ attribute limits the status of found child pages
200     to the given status, the default is @"published"@. @status="all"@ includes all
201     non-virtual pages regardless of status.
202     
203     *Usage:*
204     <pre><code><r:if_children [status="published"]>...</r:if_children></code></pre>
205   }
206   tag "if_children" do |tag|
207     children = tag.locals.page.children.count(:conditions => children_find_options(tag)[:conditions])
208     tag.expand if children > 0
209   end
210  
211   desc %{
212     Renders the contained elements only if the current contextual page has no children.
213     The @status@ attribute limits the status of found child pages to the given status,
214     the default is @"published"@. @status="all"@ includes all non-virtual pages
215     regardless of status.
216     
217     *Usage:*
218     <pre><code><r:unless_children [status="published"]>...</r:unless_children></code></pre>
219   }
220   tag "unless_children" do |tag|
221     children = tag.locals.page.children.count(:conditions => children_find_options(tag)[:conditions])
222     tag.expand unless children > 0
223   end
224  
225   desc %{
226     Renders one of the passed values based on a global cycle counter.  Use the @reset@
227     attribute to reset the cycle to the beginning.  Use the @name@ attribute to track
228     multiple cycles; the default is @cycle@.
229     
230     *Usage:*
231     <pre><code><r:cycle values="first, second, third" [reset="true|false"] [name="cycle"] /></code></pre>
232   }
233   tag 'cycle' do |tag|
234     raise TagError, "`cycle' tag must contain a `values' attribute." unless tag.attr['values']
235     cycle = (tag.globals.cycle ||= {})
236     values = tag.attr['values'].split(",").collect(&:strip)
237     cycle_name = tag.attr['name'] || 'cycle'
238     current_index = (cycle[cycle_name] ||=  0)
239     current_index = 0 if tag.attr['reset'] == 'true'
240     cycle[cycle_name] = (current_index + 1) % values.size
241     values[current_index]
242   end
243  
244   desc %{
245     Renders the main content of a page. Use the @part@ attribute to select a specific
246     page part. By default the @part@ attribute is set to body. Use the @inherit@
247     attribute to specify that if a page does not have a content part by that name that
248     the tag should render the parent's content part. By default @inherit@ is set to
249     @false@. Use the @contextual@ attribute to force a part inherited from a parent
250     part to be evaluated in the context of the child page. By default 'contextual'
251     is set to true.
252     
253     *Usage:*
254     <pre><code><r:content [part="part_name"] [inherit="true|false"] [contextual="true|false"] /></code></pre>
255   }
256   tag 'content' do |tag|
257     page = tag.locals.page
258     part_name = tag_part_name(tag)
259     boolean_attr = proc do |attribute_name, default|
260       attribute = (tag.attr[attribute_name] || default).to_s
261       raise TagError.new(%{`#{attribute_name}' attribute of `content' tag must be set to either "true" or "false"}) unless attribute =~ /true|false/i
262       (attribute.downcase == 'true') ? true : false
263     end
264     inherit = boolean_attr['inherit', false]
265     part_page = page
266     if inherit
267       while (part_page.part(part_name).nil? and (not part_page.parent.nil?)) do
268         part_page = part_page.parent
269       end
270     end
271     contextual = boolean_attr['contextual', true]
272     part = part_page.part(part_name)
273     tag.locals.page = part_page unless contextual
274     tag.globals.page.render_snippet(part) unless part.nil?
275   end
276  
277   desc %{
278     Renders the containing elements only if the part exists on a page. By default the
279     @part@ attribute is set to @body@.
280     
281     *Usage:*
282     <pre><code><r:if_content [part="part_name"]>...</r:if_content></code></pre>
283   }
284   tag 'if_content' do |tag|
285     page = tag.locals.page
286     part_name = tag_part_name(tag)
287     unless page.part(part_name).nil?
288       tag.expand
289     end
290   end
291  
292   desc %{
293     The opposite of the @if_content@ tag.
294     
295     *Usage:*
296     <pre><code><r:unless_content [part="part_name"]>...</r:unless_content></code></pre>
297   }
298   tag 'unless_content' do |tag|
299     page = tag.locals.page
300     part_name = tag_part_name(tag)
301     if page.part(part_name).nil?
302       tag.expand
303     end
304   end
305  
306   desc %{ 
307     Renders the containing elements only if the page's url matches the regular expression
308     given in the @matches@ attribute. If the @ignore_case@ attribute is set to false, the
309     match is case sensitive. By default, @ignore_case@ is set to true.
310     
311     *Usage:*
312     <pre><code><r:if_url matches="regexp" [ignore_case="true|false"]>...</if_url></code></pre>
313   }
314   tag 'if_url' do |tag|
315     raise TagError.new("`if_url' tag must contain a `matches' attribute.") unless tag.attr.has_key?('matches')
316     regexp = build_regexp_for(tag, 'matches')
317     unless tag.locals.page.url.match(regexp).nil?
318        tag.expand
319     end
320   end
321  
322   desc %{
323     The opposite of the @if_url@ tag.
324     
325     *Usage:*
326     <pre><code><r:unless_url matches="regexp" [ignore_case="true|false"]>...</unless_url></code></pre>
327   } 
328   tag 'unless_url' do |tag|
329     raise TagError.new("`unless_url' tag must contain a `matches' attribute.") unless tag.attr.has_key?('matches')
330     regexp = build_regexp_for(tag, 'matches')
331     if tag.locals.page.url.match(regexp).nil?
332         tag.expand
333     end
334   end
335
336   desc %{
337     Renders the contained elements if the current contextual page is either the actual page or one of its parents.
338     
339     This is typically used inside another tag (like &lt;r:children:each&gt;) to add conditional mark-up if the child element is or descends from the current page.
340     
341     *Usage:*
342     <pre><code><r:if_ancestor_or_self>...</if_ancestor_or_self></code></pre>
343   } 
344   tag "if_ancestor_or_self" do |tag|
345     tag.expand if (tag.globals.page.ancestors + [tag.globals.page]).include?(tag.locals.page)
346   end
347  
348   desc %{
349     Renders the contained elements if the current contextual page is also the actual page.
350     
351     This is typically used inside another tag (like &lt;r:children:each&gt;) to add conditional mark-up if the child element is the current page.
352     
353     *Usage:*
354     <pre><code><r:if_self>...</if_self></code></pre>
355   }
356   tag "if_self" do |tag|
357     tag.expand if tag.locals.page == tag.globals.page
358   end
359  
360   desc %{
361     Renders the name of the author of the current page.
362   }
363   tag 'author' do |tag|
364     page = tag.locals.page
365     if author = page.created_by
366       author.name
367     end
368   end
369  
370   desc %{
371     Renders the date based on the current page (by default when it was published or created).
372     The format attribute uses the same formating codes used by the Ruby @strftime@ function. By
373     default it's set to @%A, %B %d, %Y@.  The @for@ attribute selects which date to render.  Valid
374     options are @published_at@, @created_at@, @updated_at@, and @now@. @now@ will render the
375     current date/time, regardless of the  page.
376     
377     *Usage:*
378     <pre><code><r:date [format="%A, %B %d, %Y"] [for="published_at"]/></code></pre>
379   }
380   tag 'date' do |tag|
381     page = tag.locals.page
382     format = (tag.attr['format'] || '%A, %B %d, %Y')
383     time_attr = tag.attr['for']
384     date = if time_attr
385       case
386       when time_attr == 'now'
387         Time.now
388       when ['published_at', 'created_at', 'updated_at'].include?(time_attr)
389         page[time_attr]
390       else
391         raise TagError, "Invalid value for 'for' attribute."
392       end
393     else
394       page.published_at || page.created_at
395     end
396     adjust_time(date).strftime(format)
397   end
398  
399   desc %{
400     Renders a link to the page. When used as a single tag it uses the page's title
401     for the link name. When used as a double tag the part in between both tags will
402     be used as the link text. The link tag passes all attributes over to the HTML
403     @a@ tag. This is very useful for passing attributes like the @class@ attribute
404     or @id@ attribute. If the @anchor@ attribute is passed to the tag it will
405     append a pound sign (<code>#</code>) followed by the value of the attribute to
406     the @href@ attribute of the HTML @a@ tag--effectively making an HTML anchor.
407     
408     *Usage:*
409     <pre><code><r:link [anchor="name"] [other attributes...] /></code></pre>
410     or
411     <pre><code><r:link [anchor="name"] [other attributes...]>link text here</r:link></code></pre>
412   }
413   tag 'link' do |tag|
414     options = tag.attr.dup
415     anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
416     attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
417     attributes = " #{attributes}" unless attributes.empty?
418     text = tag.double? ? tag.expand : tag.render('title')
419     %{<a href="#{tag.render('url')}#{anchor}"#{attributes}>#{text}</a>}
420   end
421  
422   desc %{
423     Renders a trail of breadcrumbs to the current page. The separator attribute
424     specifies the HTML fragment that is inserted between each of the breadcrumbs. By
425     default it is set to @>@. The boolean nolinks attribute can be specified to render
426     breadcrumbs in plain text, without any links (useful when generating title tag).
427     
428     *Usage:*
429     <pre><code><r:breadcrumbs [separator="separator_string"] [nolinks="true"] /></code></pre>
430   }
431   tag 'breadcrumbs' do |tag|
432     page = tag.locals.page
433     breadcrumbs = [page.breadcrumb]
434     nolinks = (tag.attr['nolinks'] == 'true')
435     page.ancestors.each do |ancestor|
436       tag.locals.page = ancestor
437       if nolinks
438         breadcrumbs.unshift tag.render('breadcrumb')
439       else
440         breadcrumbs.unshift %{<a href="#{tag.render('url')}">#{tag.render('breadcrumb')}</a>}
441       end
442     end
443     separator = tag.attr['separator'] || ' &gt; '
444     breadcrumbs.join(separator)
445   end
446  
447   desc %{
448     Renders the snippet specified in the @name@ attribute within the context of a page.
449     
450     *Usage:*
451     <pre><code><r:snippet name="snippet_name" /></code></pre>
452   }
453   tag 'snippet' do |tag|
454     if name = tag.attr['name']
455       if snippet = Snippet.find_by_name(name.strip)
456         tag.globals.page.render_snippet(snippet)
457       else
458         raise TagError.new('snippet not found')
459       end
460     else
461       raise TagError.new("`snippet' tag must contain `name' attribute")
462     end
463   end
464
465   desc %{
466     Inside this tag all page related tags refer to the page found at the @url@ attribute. 
467     @url@s may be relative or absolute paths.
468     
469     *Usage:*
470     <pre><code><r:find url="value_to_find">...</r:find></code></pre>
471   }
472   tag 'find' do |tag|
473     url = tag.attr['url']
474     raise TagError.new("`find' tag must contain `url' attribute") unless url
475    
476     found = Page.find_by_url(absolute_path_for(tag.locals.page.url, url))
477     if page_found?(found)
478       tag.locals.page = found
479       tag.expand
480     end
481   end
482  
483   desc %{
484     Randomly renders one of the options specified by the @option@ tags.
485     
486     *Usage:*
487     <pre><code><r:random>
488       <r:option>...</r:option>
489       <r:option>...</r:option>
490       ...
491     <r:random>
492     </code></pre>
493   }
494   tag 'random' do |tag|
495     tag.locals.random = []
496     tag.expand
497     options = tag.locals.random
498     option = options[rand(options.size)]
499     option.call if option
500   end
501   tag 'random:option' do |tag|
502     items = tag.locals.random
503     items << tag.block
504   end
505  
506   desc %{ 
507     Nothing inside a set of comment tags is rendered.
508     
509     *Usage:*
510     <pre><code><r:comment>...</r:comment></code></pre>
511   }
512   tag 'comment' do |tag|
513   end
514  
515   desc %{ 
516     Escapes angle brackets, etc. for rendering in an HTML document.
517     
518     *Usage:*
519     <pre><code><r:escape_html>...</r:escape_html></code></pre>
520   }
521   tag "escape_html" do |tag|
522     CGI.escapeHTML(tag.expand)
523   end
524  
525   desc %{
526     Outputs the published date using the format mandated by RFC 1123. (Ideal for RSS feeds.)
527     
528     *Usage:*
529     <pre><code><r:rfc1123_date /></code></pre>
530   }
531   tag "rfc1123_date" do |tag|
532     page = tag.locals.page
533     if date = page.published_at || page.created_at
534       CGI.rfc1123_date(date.to_time)
535     end
536   end
537  
538   desc %{
539     Renders a list of links specified in the @urls@ attribute according to three
540     states:
541     
542     * @normal@ specifies the normal state for the link
543     * @here@ specifies the state of the link when the url matches the current
544        page's URL
545     * @selected@ specifies the state of the link when the current page matches
546        is a child of the specified url
547     
548     The @between@ tag specifies what should be inserted in between each of the links.
549     
550     *Usage:*
551     <pre><code><r:navigation urls="[Title: url | Title: url | ...]">
552       <r:normal><a href="<r:url />"><r:title /></a></r:normal>
553       <r:here><strong><r:title /></strong></r:here>
554       <r:selected><strong><a href="<r:url />"><r:title /></a></strong></r:selected>
555       <r:between> | </r:between>
556     </r:navigation>
557     </code></pre>
558   }   
559   tag 'navigation' do |tag|
560     hash = tag.locals.navigation = {}
561     tag.expand
562     raise TagError.new("`navigation' tag must include a `normal' tag") unless hash.has_key? :normal
563     result = []
564     pairs = tag.attr['urls'].to_s.split('|').map do