File: app/models/standard_tags.rb

Overview
Module Structure
Class Hierarchy
Code

Overview

Module Structure

  module: <Toplevel Module>
  module: StandardTags#1
includes
  ViewHelpers ( WillPaginate )
  Taggable ( Radiant )
  LocalTime   
has properties
method: snippet_cache / 1 #863
method: render_children_with_pagination / 2 #1215
method: children_find_options / 1 #1241
method: aggregate_children / 1 #1285
method: pagination_find_options / 1 #1295
method: will_paginate_options / 1 #1304
method: remove_trailing_slash / 1 #1313
method: tag_part_name / 1 #1317
method: build_regexp_for / 2 #1321
method: relative_url_for / 2 #1331
method: absolute_path_for / 2 #1335
method: page_found? / 1 #1343
method: boolean_attr_or_error / 3 #1347
method: attr_or_error / 2 #1352
method: required_attr / 2 #1362
method: dev? / 1 #1367
  class: TagError#9
inherits from
  StandardError ( Builtin-Module )
  class: RequiredAttributeError#10
inherits from
  StandardError ( Builtin-Module )

Code

   1  module StandardTags
   2 
   3    include Radiant::Taggable
   4    include LocalTime
   5 
   6    require "will_paginate/view_helpers"
   7    include WillPaginate::ViewHelpers
   8 
   9    class TagError < StandardError; end
  10    class RequiredAttributeError < StandardError; end
  11 
  12    desc %{
  13      Causes the tags referring to a page's attributes to refer to the current page.
  14 
  15      *Usage:*
  16      
  17      <pre><code><r:page>...</r:page></code></pre>
  18    }
  19    tag 'page' do |tag|
  20      tag.locals.page = tag.globals.page
  21      tag.expand
  22    end
  23 
  24    [:breadcrumb, :slug, :title].each do |method|
  25      desc %{
  26        Renders the @#{method}@ attribute of the current page.
  27      }
  28      tag method.to_s do |tag|
  29        tag.locals.page.send(method)
  30      end
  31    end
  32 
  33    desc %{
  34      Renders the @path@ attribute of the current page.
  35    }
  36    tag 'path' do |tag|
  37      relative_url_for(tag.locals.page.path, tag.globals.page.request)
  38    end
  39    deprecated_tag 'url', :substitute => 'path', :deadline => '1.2'
  40 
  41    desc %{
  42      Gives access to a page's children.
  43 
  44      *Usage:*
  45      
  46      <pre><code><r:children>...</r:children></code></pre>
  47    }
  48    tag 'children' do |tag|
  49      tag.locals.children = tag.locals.page.children
  50      tag.expand
  51    end
  52 
  53    desc %{
  54      Renders the total number of children.
  55    }
  56    tag 'children:count' do |tag|
  57      options = children_find_options(tag)
  58      options.delete(:order) # Order is irrelevant
  59      tag.locals.children.count(options)
  60    end
  61 
  62    desc %{
  63      Returns the first child. Inside this tag all page attribute tags are mapped to
  64      the first child. Takes the same ordering options as @<r:children:each>@.
  65 
  66      *Usage:*
  67      
  68      <pre><code><r:children:first>...</r:children:first></code></pre>
  69    }
  70    tag 'children:first' do |tag|
  71      options = children_find_options(tag)
  72      children = tag.locals.children.find(:all, options)
  73      if first = children.first
  74        tag.locals.page = first
  75        tag.expand
  76      end
  77    end
  78 
  79    desc %{
  80      Returns the last child. Inside this tag all page attribute tags are mapped to
  81      the last child. Takes the same ordering options as @<r:children:each>@.
  82 
  83      *Usage:*
  84      
  85      <pre><code><r:children:last>...</r:children:last></code></pre>
  86    }
  87    tag 'children:last' do |tag|
  88      options = children_find_options(tag)
  89      children = tag.locals.children.find(:all, options)
  90      if last = children.last
  91        tag.locals.page = last
  92        tag.expand
  93      end
  94    end
  95 
  96    desc %{
  97      Cycles through each of the children. Inside this tag all page attribute tags
  98      are mapped to the current child page.
  99      
 100      Supply @paginated="true"@ to paginate the displayed list. will_paginate view helper
 101      options can also be specified, including @per_page@, @previous_label@, @next_label@,
 102      @class@, @separator@, @inner_window@ and @outer_window@.
 103 
 104      *Usage:*
 105      
 106      <pre><code><r:children:each [offset="number"] [limit="number"]
 107       [by="published_at|updated_at|created_at|slug|title|keywords|description"]
 108       [order="asc|desc"] 
 109       [status="draft|reviewed|published|hidden|all"]
 110       [paginated="true"]
 111       [per_page="number"]
 112       >
 113       ...
 114      </r:children:each>
 115      </code></pre>
 116    }
 117    tag 'children:each' do |tag|
 118      render_children_with_pagination(tag)
 119    end
 120 
 121    desc %{
 122      The pagination tag is not usually called directly. Supply paginated="true" when you display a list and it will
 123      be automatically display only the current page of results, with pagination controls at the bottom.
 124 
 125      *Usage:*
 126      
 127      <pre><code><r:children:each paginated="true" per_page="50" container="false" previous_label="foo" next_label="bar">
 128        <r:child>...</r:child>
 129      </r:children:each>
 130      </code></pre>
 131    }
 132    tag 'pagination' do |tag|
 133      if tag.locals.paginated_list
 134        will_paginate(tag.locals.paginated_list, will_paginate_options(tag))
 135      end
 136    end
 137 
 138    desc %{
 139      Page attribute tags inside of this tag refer to the current child. This is occasionally
 140      useful if you are inside of another tag (like &lt;r:find&gt;) and need to refer back to the
 141      current child.
 142 
 143      *Usage:*
 144      
 145      <pre><code><r:children:each>
 146        <r:child>...</r:child>
 147      </r:children:each>
 148      </code></pre>
 149    }
 150    tag 'children:each:child' do |tag|
 151      tag.locals.page = tag.locals.child
 152      tag.expand
 153    end
 154    
 155    desc %{
 156      Renders the tag contents only if the current page is the first child in the context of
 157      a children:each tag
 158      
 159      *Usage:*
 160      
 161      <pre><code><r:children:each>
 162        <r:if_first >
 163          ...
 164        </r:if_first>
 165      </r:children:each>
 166      </code></pre>
 167      
 168    }
 169    tag 'children:each:if_first' do |tag|
 170      tag.expand if tag.locals.first_child
 171    end
 172 
 173    
 174    desc %{
 175      Renders the tag contents unless the current page is the first child in the context of
 176      a children:each tag
 177      
 178      *Usage:*
 179      
 180      <pre><code><r:children:each>
 181        <r:unless_first >
 182          ...
 183        </r:unless_first>
 184      </r:children:each>
 185      </code></pre>
 186      
 187    }
 188    tag 'children:each:unless_first' do |tag|
 189      tag.expand unless tag.locals.first_child
 190    end
 191    
 192    desc %{
 193      Renders the tag contents only if the current page is the last child in the context of
 194      a children:each tag
 195      
 196      *Usage:*
 197      
 198      <pre><code><r:children:each>
 199        <r:if_last >
 200          ...
 201        </r:if_last>
 202      </r:children:each>
 203      </code></pre>
 204      
 205    }
 206    tag 'children:each:if_last' do |tag|
 207      tag.expand if tag.locals.last_child
 208    end
 209 
 210    
 211    desc %{
 212      Renders the tag contents unless the current page is the last child in the context of
 213      a children:each tag
 214      
 215      *Usage:*
 216      
 217      <pre><code><r:children:each>
 218        <r:unless_last >
 219          ...
 220        </r:unless_last>
 221      </r:children:each>
 222      </code></pre>
 223      
 224    }
 225    tag 'children:each:unless_last' do |tag|
 226      tag.expand unless tag.locals.last_child
 227    end
 228    
 229    desc %{
 230      Renders the tag contents only if the contents do not match the previous header. This
 231      is extremely useful for rendering date headers for a list of child pages.
 232 
 233      If you would like to use several header blocks you may use the @name@ attribute to
 234      name the header. When a header is named it will not restart until another header of
 235      the same name is different.
 236 
 237      Using the @restart@ attribute you can cause other named headers to restart when the
 238      present header changes. Simply specify the names of the other headers in a semicolon
 239      separated list.
 240 
 241      *Usage:*
 242      
 243      <pre><code><r:children:each>
 244        <r:header [name="header_name"] [restart="name1[;name2;...]"]>
 245          ...
 246        </r:header>
 247      </r:children:each>
 248      </code></pre>
 249    }
 250    tag 'children:each:header' do |tag|
 251      previous_headers = tag.locals.previous_headers
 252      name = tag.attr['name'] || :unnamed
 253      restart = (tag.attr['restart'] || '').split(';')
 254      header = tag.expand
 255      unless header == previous_headers[name]
 256        previous_headers[name] = header
 257        unless restart.empty?
 258          restart.each do |n|
 259            previous_headers[n] = nil
 260          end
 261        end
 262        header
 263      end
 264    end
 265 
 266    desc %{
 267      Page attribute tags inside this tag refer to the parent of the current page.
 268 
 269      *Usage:*
 270      
 271      <pre><code><r:parent>...</r:parent></code></pre>
 272    }
 273    tag "parent" do |tag|
 274      parent = tag.locals.page.parent
 275      tag.locals.page = parent
 276      tag.expand if parent
 277    end
 278 
 279    desc %{
 280      Renders the contained elements only if the current contextual page has a parent, i.e.
 281      is not the root page.
 282 
 283      *Usage:*
 284      
 285      <pre><code><r:if_parent>...</r:if_parent></code></pre>
 286    }
 287    tag "if_parent" do |tag|
 288      parent = tag.locals.page.parent
 289      tag.expand if parent
 290    end
 291 
 292    desc %{
 293      Renders the contained elements only if the current contextual page has no parent, i.e.
 294      is the root page.
 295 
 296      *Usage:*
 297      
 298      <pre><code><r:unless_parent>...</r:unless_parent></code></pre>
 299    }
 300    tag "unless_parent" do |tag|
 301      parent = tag.locals.page.parent
 302      tag.expand unless parent
 303    end
 304 
 305    desc %{
 306      Renders the contained elements only if the current contextual page has one or
 307      more child pages.  The @status@ attribute limits the status of found child pages
 308      to the given status, the default is @"published"@. @status="all"@ includes all
 309      non-virtual pages regardless of status.
 310 
 311      *Usage:*
 312      
 313      <pre><code><r:if_children [status="published"]>...</r:if_children></code></pre>
 314    }
 315    tag "if_children" do |tag|
 316      children = tag.locals.page.children.count(:conditions => children_find_options(tag)[:conditions])
 317      tag.expand if children > 0
 318    end
 319 
 320    desc %{
 321      Renders the contained elements only if the current contextual page has no children.
 322      The @status@ attribute limits the status of found child pages to the given status,
 323      the default is @"published"@. @status="all"@ includes all non-virtual pages
 324      regardless of status.
 325 
 326      *Usage:*
 327      
 328      <pre><code><r:unless_children [status="published"]>...</r:unless_children></code></pre>
 329    }
 330    tag "unless_children" do |tag|
 331      children = tag.locals.page.children.count(:conditions => children_find_options(tag)[:conditions])
 332      tag.expand unless children > 0
 333    end
 334    
 335      desc %{
 336      Aggregates the children of multiple paths using the @paths@ attribute.
 337      Useful for combining many different sections/categories into a single
 338      feed or listing.
 339      
 340      *Usage*:
 341      
 342      <pre><code><r:aggregate paths="/section1; /section2; /section3"> ... </r:aggregate></code></pre>
 343    }
 344    tag "aggregate" do |tag|
 345      required_attr(tag, 'paths', 'urls')
 346      paths = (tag.attr['paths']||tag.attr["urls"]).split(";").map(&:strip).reject(&:blank?).map { |u| clean_path u }
 347      parent_ids = paths.map {|u| Page.find_by_path(u) }.map(&:id)
 348      tag.locals.parent_ids = parent_ids
 349      tag.expand
 350    end
 351    
 352    desc %{
 353      Sets the scope to the individual aggregated page allowing you to
 354      iterate through each of the listed paths.
 355      
 356      *Usage*:
 357      
 358      <pre><code><r:aggregate:each paths="/section1; /section2; /section3"> ... </r:aggregate:each></code></pre>
 359    }
 360    tag "aggregate:each" do |tag|
 361      aggregates = []
 362      tag.locals.aggregated_pages = tag.locals.parent_ids.map {|p| Page.find(p)}
 363      tag.locals.aggregated_pages.each do |aggregate_page|
 364        tag.locals.page = aggregate_page
 365        aggregates << tag.expand
 366      end
 367      aggregates.flatten.join('')
 368    end
 369    
 370    tag "aggregate:each:children" do |tag|
 371      tag.locals.children = tag.locals.page.children
 372      tag.expand
 373    end
 374    
 375    tag "aggregate:each:children:each" do |tag|
 376      options = children_find_options(tag)
 377      result = []
 378      children = tag.locals.children
 379      tag.locals.previous_headers = {}
 380      children.find(:all, options).each do |item|
 381        tag.locals.child = item
 382        tag.locals.page = item
 383        result << tag.expand
 384      end
 385      result.flatten.join('')
 386    end
 387    
 388    tag "aggregate:children" do |tag|
 389      tag.expand
 390    end
 391    
 392    desc %{
 393      Renders the total count of children of the aggregated pages.  Accepts the
 394      same options as @<r:children:each />@.
 395 
 396      *Usage*:
 397      
 398      <pre><code><r:aggregate paths="/section1; /section2; /section3">
 399        <r:children:count />
 400      </r:aggregate></code></pre>
 401    }  
 402    tag "aggregate:children:count" do |tag|
 403      options = aggregate_children(tag)
 404      if ActiveRecord::Base.connection.adapter_name.downcase == 'postgresql'
 405        options[:group] = Page.columns.map {|c| c.name}.join(', ')
 406        Page.find(:all, options).size
 407      else
 408        Page.count(options)
 409      end
 410    end
 411    desc %{
 412      Renders the contained block for each child of the aggregated pages.  Accepts the
 413      same options as the plain @<r:children:each />@.
 414 
 415      *Usage*:
 416      
 417      <pre><code><r:aggregate paths="/section1; /section2; /section3">
 418        <r:children:each>
 419          ...
 420        </r:children:each>
 421      </r:aggregate></code></pre>
 422    }
 423    tag "aggregate:children:each" do |tag|
 424      render_children_with_pagination(tag, :aggregate => true)
 425    end
 426    
 427    desc %{
 428      Renders the first child of the aggregated pages.  Accepts the
 429      same options as @<r:children:each />@.
 430 
 431      *Usage*:
 432      
 433      <pre><code><r:aggregate paths="/section1; /section2; /section3">
 434        <r:children:first>
 435          ...
 436        </r:children:first>
 437      </r:aggregate></code></pre>
 438    }
 439    tag "aggregate:children:first" do |tag|
 440      options = aggregate_children(tag)
 441      children = Page.find(:all, options)
 442      if first = children.first
 443        tag.locals.page = first
 444        tag.expand
 445      end
 446    end
 447    
 448    desc %{
 449      Renders the last child of the aggregated pages.  Accepts the
 450      same options as @<r:children:each />@.
 451 
 452      *Usage*:
 453      
 454      <pre><code><r:aggregate paths="/section1; /section2; /section3">
 455        <r:children:last>
 456          ...
 457        </r:children:last>
 458      </r:aggregate></code></pre>
 459    }
 460    tag "aggregate:children:last" do |tag|
 461      options = aggregate_children(tag)
 462      children = Page.find(:all, options)
 463      if last = children.last
 464        tag.locals.page = last
 465        tag.expand
 466      end
 467    end
 468 
 469    desc %{
 470      Renders a counter value or one of the values given based on a global cycle counter. 
 471      
 472      To get a numeric counter just use the tag, or specify a start value with @start@.
 473      Use the @reset@ attribute to reset the cycle to the beginning. Using @reset@ on a
 474      numbered cycle will begin at 0. Use the @name@  attribute to track multiple cycles; 
 475      the default is @cycle@.
 476 
 477      *Usage:*
 478      
 479      <pre><code><r:cycle [values="first, second, third"] [reset="true|false"] [name="cycle"] [start="second"] /></code></pre>
 480      <pre><code><r:cycle start="3" /></code></pre>
 481    }
 482    tag 'cycle' do |tag|
 483      cycle = (tag.globals.cycle ||= {})
 484      if tag.attr['values']
 485        values = tag.attr['values'].split(",").collect(&:strip)
 486      end
 487      start = tag.attr['start']
 488      cycle_name = tag.attr['name'] || 'cycle'
 489      if values
 490        if start
 491          current_index = (cycle[cycle_name] ||= values.index(start))
 492        else
 493          current_index = (cycle[cycle_name] ||=  0)
 494        end
 495        current_index = 0 if tag.attr['reset'] == 'true'
 496        cycle[cycle_name] = (current_index + 1) % values.size
 497        values[current_index]
 498      else
 499        cycle[cycle_name] ||= (start.presence || 0).to_i
 500        output = cycle[cycle_name]
 501        cycle[cycle_name] += 1
 502        if tag.attr['reset'] == 'true'
 503          cycle[cycle_name] = 0
 504          output = cycle[cycle_name]
 505        end
 506        output
 507      end
 508    end
 509 
 510    desc %{
 511      Renders the main content of a page. Use the @part@ attribute to select a specific
 512      page part. By default the @part@ attribute is set to body. Use the @inherit@
 513      attribute to specify that if a page does not have a content part by that name that
 514      the tag should render the parent's content part. By default @inherit@ is set to
 515      @false@. Use the @contextual@ attribute to force a part inherited from a parent
 516      part to be evaluated in the context of the child page. By default 'contextual'
 517      is set to true.
 518 
 519      *Usage:*
 520      
 521      <pre><code><r:content [part="part_name"] [inherit="true|false"] [contextual="true|false"] /></code></pre>
 522    }
 523    tag 'content' do |tag|
 524      page = tag.locals.page
 525      part_name = tag_part_name(tag)
 526      # Prevent simple and deep recursive rendering of the same page part
 527      rendering_parts = (tag.locals.rendering_parts ||= Hash.new {|h,k| h[k] = []})
 528      if rendering_parts[page.id].include?(part_name)
 529        raise TagError.new(%{Recursion error: already rendering the `#{part_name}' part.})
 530      else
 531        rendering_parts[page.id] << part_name
 532      end
 533      inherit = boolean_attr_or_error(tag,'inherit',false)
 534      part_page = page
 535      if inherit
 536        while (part_page.part(part_name).nil? and (not part_page.parent.nil?)) do
 537          part_page = part_page.parent
 538        end
 539      end
 540      contextual = boolean_attr_or_error(tag,'contextual', true)
 541      part = part_page.part(part_name)
 542      tag.locals.page = part_page unless contextual
 543      result = tag.globals.page.render_snippet(part) unless part.nil?
 544      rendering_parts[page.id].delete(part_name)
 545      result
 546    end
 547 
 548    desc %{
 549      Renders the containing elements if all of the listed parts exist on a page.
 550      By default the @part@ attribute is set to @body@, but you may list more than one
 551      part by separating them with a comma. Setting the optional @inherit@ to true will
 552      search ancestors independently for each part. By default @inherit@ is set to @false@.
 553 
 554      When listing more than one part, you may optionally set the @find@ attribute to @any@
 555      so that it will render the containing elements if any of the listed parts are found.
 556      By default the @find@ attribute is set to @all@.
 557 
 558      *Usage:*
 559      
 560      <pre><code><r:if_content [part="part_name, other_part"] [inherit="true"] [find="any"]>...</r:if_content></code></pre>
 561    }
 562    tag 'if_content' do |tag|
 563      part_name = tag_part_name(tag)
 564      parts_arr = part_name.split(',')
 565      inherit = boolean_attr_or_error(tag, 'inherit', 'false')
 566      find = attr_or_error(tag, :attribute_name => 'find', :default => 'all', :values => 'any, all')
 567      expandable = true
 568      one_found = false
 569      parts_arr.each do |name|
 570        part_page = tag.locals.page
 571        name.strip!
 572        if inherit
 573          while (part_page.part(name).nil? and (not part_page.parent.nil?)) do
 574            part_page = part_page.parent
 575          end
 576        end
 577        expandable = false if part_page.part(name).nil?
 578        one_found ||= true if !part_page.part(name).nil?
 579      end
 580      expandable = true if (find == 'any' and one_found)
 581      tag.expand if expandable
 582    end
 583 
 584    desc %{
 585      The opposite of the @if_content@ tag. It renders the contained elements if all of the
 586      specified parts do not exist. Setting the optional @inherit@ to true will search
 587      ancestors independently for each part. By default @inherit@ is set to @false@.
 588 
 589      When listing more than one part, you may optionally set the @find@ attribute to @any@
 590      so that it will not render the containing elements if any of the listed parts are found.
 591      By default the @find@ attribute is set to @all@.
 592 
 593      *Usage:*
 594      
 595      <pre><code><r:unless_content [part="part_name, other_part"] [inherit="false"] [find="any"]>...</r:unless_content></code></pre>
 596    }
 597    tag 'unless_content' do |tag|
 598      part_name = tag_part_name(tag)
 599      parts_arr = part_name.split(',')
 600      inherit = boolean_attr_or_error(tag, 'inherit', false)
 601      find = attr_or_error(tag, :attribute_name => 'find', :default => 'all', :values => 'any, all')
 602      expandable, all_found = true, true
 603      parts_arr.each do |name|
 604        part_page = tag.locals.page
 605        name.strip!
 606        if inherit
 607          while (part_page.part(name).nil? and (not part_page.parent.nil?)) do
 608            part_page = part_page.parent
 609          end
 610        end
 611        expandable = false if !part_page.part(name).nil?
 612        all_found = false if part_page.part(name).nil?
 613      end
 614      if all_found == false and find == 'all'
 615        expandable = true
 616      end
 617      tag.expand if expandable
 618    end
 619 
 620    desc %{
 621      Renders the containing elements only if the page's path matches the regular expression
 622      given in the @matches@ attribute. If the @ignore_case@ attribute is set to false, the
 623      match is case sensitive. By default, @ignore_case@ is set to true.
 624 
 625      *Usage:*
 626      
 627      <pre><code><r:if_path matches="regexp" [ignore_case="true|false"]>...</r:if_path></code></pre>
 628    }
 629    tag 'if_path' do |tag|
 630      required_attr(tag,'matches')
 631      regexp = build_regexp_for(tag, 'matches')
 632      unless tag.locals.page.path.match(regexp).nil?
 633         tag.expand
 634      end
 635    end
 636    deprecated_tag 'if_url', :substitute => 'if_path', :deadline => '1.2'
 637 
 638    desc %{
 639      The opposite of the @if_path@ tag.
 640 
 641      *Usage:*
 642      
 643      <pre><code><r:unless_path matches="regexp" [ignore_case="true|false"]>...</r:unless_path></code></pre>
 644    }
 645    tag 'unless_path' do |tag|
 646      required_attr(tag, 'matches')
 647      regexp = build_regexp_for(tag, 'matches')
 648      if tag.locals.page.path.match(regexp).nil?
 649          tag.expand
 650      end
 651    end
 652    deprecated_tag 'unless_url', :substitute => 'unless_path', :deadline => '1.2'
 653 
 654    desc %{
 655      Renders the contained elements if the current contextual page is either the actual page or one of its parents.
 656 
 657      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.
 658 
 659      *Usage:*
 660      
 661      <pre><code><r:if_ancestor_or_self>...</r:if_ancestor_or_self></code></pre>
 662    }
 663    tag "if_ancestor_or_self" do |tag|
 664      tag.expand if (tag.globals.page.ancestors + [tag.globals.page]).include?(tag.locals.page)
 665    end
 666 
 667    desc %{
 668      Renders the contained elements unless the current contextual page is either the actual page or one of its parents.
 669 
 670      This is typically used inside another tag (like &lt;r:children:each&gt;) to add conditional mark-up unless the child element is or descends from the current page.
 671 
 672      *Usage:*
 673      
 674      <pre><code><r:unless_ancestor_or_self>...</r:unless_ancestor_or_self></code></pre>
 675    }
 676    tag "unless_ancestor_or_self" do |tag|
 677      tag.expand unless (tag.globals.page.ancestors + [tag.globals.page]).include?(tag.locals.page)
 678    end
 679 
 680    desc %{
 681      Renders the contained elements if the current contextual page is also the actual page.
 682 
 683      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.
 684 
 685      *Usage:*
 686      
 687      <pre><code><r:if_self>...</r:if_self></code></pre>
 688    }
 689    tag "if_self" do |tag|
 690      tag.expand if tag.locals.page == tag.globals.page
 691    end
 692 
 693    desc %{
 694      Renders the contained elements unless the current contextual page is also the actual page.
 695 
 696      This is typically used inside another tag (like &lt;r:children:each&gt;) to add conditional mark-up unless the child element is the current page.
 697 
 698      *Usage:*
 699 
 700      <pre><code><r:unless_self>...</r:unless_self></code></pre>
 701    }
 702    tag "unless_self" do |tag|
 703      tag.expand unless tag.locals.page == tag.globals.page
 704    end
 705 
 706    desc %{
 707      Renders the name of the author of the current page.
 708    }
 709    tag 'author' do |tag|
 710      page = tag.locals.page
 711      if author = page.created_by
 712        author.name
 713      end
 714    end
 715 
 716    desc %{
 717      Renders the Gravatar of the author of the current page or the named user.
 718 
 719      *Usage:*
 720 
 721      <pre><code><r:gravatar /></code></pre>
 722 
 723      or
 724 
 725      <pre><code><r:gravatar [name="User Name"]
 726          [rating="G | PG | R | X"]
 727          [size="32px"] /></code></pre>
 728    }
 729    tag 'gravatar' do |tag|
 730      page = tag.locals.page
 731      name = (tag.attr['name'] || page.created_by.name)
 732      rating = (tag.attr['rating'] || 'G')
 733      size = (tag.attr['size'] || '32px')
 734      email = User.find_by_name(name).email
 735      default = "#{request.protocol}#{request.host_with_port}/images/admin/avatar_#{([size.to_i] * 2).join('x')}.png"
 736      unless email.blank?
 737        url = 'http://www.gravatar.com/avatar.php?'
 738        url << "gravatar_id=#{Digest::MD5.new.update(email)}"
 739        url << "&rating=#{rating}"
 740        url << "&size=#{size.to_i}"
 741        url << "&default=#{default}"
 742        url
 743      else
 744        default
 745      end
 746    end
 747 
 748    desc %{
 749      Renders the date based on the current page (by default when it was published or created).
 750      The format attribute uses the same formating codes used by the Ruby @strftime@ function. By
 751      default it's set to @%A, %B %d, %Y@.  The @for@ attribute selects which date to render.  Valid
 752      options are @published_at@, @created_at@, @updated_at@, and @now@. @now@ will render the
 753      current date/time, regardless of the  page.
 754 
 755      *Usage:*
 756 
 757      <pre><code><r:date [format="%A, %B %d, %Y"] [for="published_at"]/></code></pre>
 758    }
 759    tag 'date' do |tag|
 760      page = tag.locals.page
 761      format = (tag.attr['format'] || '%A, %B %d, %Y')
 762      time_attr = tag.attr['for']
 763      date = if time_attr
 764        case
 765        when time_attr == 'now'
 766          Time.zone.now
 767        when Page.date_column_names.include?(time_attr)
 768          page[time_attr]
 769        else
 770          raise TagError, "Invalid value for 'for' attribute."
 771        end
 772      else
 773        page.published_at || page.created_at
 774      end
 775      @i18n_date_format_keys ||= (I18n.config.backend.send(:translations)[I18n.locale][:date][:formats].keys rescue [])
 776      format = @i18n_date_format_keys.include?(format.to_sym) ? format.to_sym : format
 777      I18n.l date, :format => format
 778    end
 779 
 780    desc %{
 781      Renders a link to the page. When used as a single tag it uses the page's title
 782      for the link name. When used as a double tag the part in between both tags will
 783      be used as the link text. The link tag passes all attributes over to the HTML
 784      @a@ tag. This is very useful for passing attributes like the @class@ attribute
 785      or @id@ attribute. If the @anchor@ attribute is passed to the tag it will
 786      append a pound sign (<code>#</code>) followed by the value of the attribute to
 787      the @href@ attribute of the HTML @a@ tag--effectively making an HTML anchor.
 788 
 789      *Usage:*
 790 
 791      <pre><code><r:link [anchor="name"] [other attributes...] /></code></pre>
 792      
 793      or
 794      
 795      <pre><code><r:link [anchor="name"] [other attributes...]>link text here</r:link></code></pre>
 796    }
 797    tag 'link' do |tag|
 798      options = tag.attr.dup
 799      anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
 800      attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
 801      attributes = " #{attributes}" unless attributes.empty?
 802      text = tag.double? ? tag.expand : tag.render('title')
 803      %{<a href="#{tag.render('path')}#{anchor}"#{attributes}>#{text}</a>}
 804    end
 805 
 806    desc %{
 807      Renders a trail of breadcrumbs to the current page. The separator attribute
 808      specifies the HTML fragment that is inserted between each of the breadcrumbs. By
 809      default it is set to @>@. The boolean @nolinks@ attribute can be specified to render
 810      breadcrumbs in plain text, without any links (useful when generating title tag). 
 811      Set the boolean @noself@ attribute to omit the present page (useful in page headers).
 812 
 813      *Usage:*
 814 
 815      <pre><code><r:breadcrumbs [separator="separator_string"] [nolinks="true"] [noself="true"]/></code></pre>
 816    }
 817    tag 'breadcrumbs' do |tag|
 818      page = tag.locals.page
 819      nolinks = (tag.attr['nolinks'] == 'true')
 820      noself = (tag.attr['noself'] == 'true')
 821      breadcrumbs = []
 822      breadcrumbs.unshift page.breadcrumb unless noself
 823      page.ancestors.each do |ancestor|
 824        tag.locals.page = ancestor
 825        if nolinks
 826          breadcrumbs.unshift tag.render('breadcrumb')
 827        else
 828          breadcrumbs.unshift %{<a href="#{tag.render('path')}">#{tag.render('breadcrumb')}</a>}
 829        end
 830      end
 831      separator = tag.attr['separator'] || ' &gt; '
 832      breadcrumbs.join(separator)
 833    end
 834 
 835    desc %{
 836      Renders the snippet specified in the @name@ attribute within the context of a page.
 837 
 838      *Usage:*
 839 
 840      <pre><code><r:snippet name="snippet_name" /></code></pre>
 841 
 842      When used as a double tag, the part in between both tags may be used within the
 843      snippet itself, being substituted in place of @<r:yield/>@.
 844 
 845      *Usage:*
 846 
 847      <pre><code><r:snippet name="snippet_name">Lorem ipsum dolor...</r:snippet></code></pre>
 848    }
 849    tag 'snippet' do |tag|
 850      required_attr(tag, 'name')
 851      name = tag['name']
 852 
 853      snippet = snippet_cache(name.strip)
 854      
 855      if snippet
 856        tag.locals.yield = tag.expand if tag.double?
 857        tag.globals.page.render_snippet(snippet)
 858      else
 859        raise TagError.new("snippet '#{name}' not found")
 860      end
 861    end
 862 
 863    def snippet_cache(name)
 864      @snippet_cache ||= {}
 865 
 866      snippet = @snippet_cache[name]
 867      unless snippet
 868        snippet = Snippet.find_by_name(name)
 869        @snippet_cache[name] = snippet
 870      end
 871      snippet
 872    end
 873    private :snippet_cache
 874 
 875    desc %{
 876      Used within a snippet as a placeholder for substitution of child content, when
 877      the snippet is called as a double tag.
 878 
 879      *Usage (within a snippet):*
 880      
 881      <pre><code>
 882      <div id="outer">
 883        <p>before</p>
 884        <r:yield/>
 885        <p>after</p>
 886      </div>
 887      </code></pre>
 888 
 889      If the above snippet was named "yielding", you could call it from any Page,
 890      Layout or Snippet as follows:
 891 
 892      <pre><code><r:snippet name="yielding">Content within</r:snippet></code></pre>
 893 
 894      Which would output the following:
 895 
 896      <pre><code>
 897      <div id="outer">
 898        <p>before</p>
 899        Content within
 900        <p>after</p>
 901      </div>
 902      </code></pre>
 903 
 904      When called in the context of a Page or a Layout, @<r:yield/>@ outputs nothing.
 905    }
 906    tag 'yield' do |tag|
 907      tag.locals.yield
 908    end
 909 
 910    desc %{
 911      Inside this tag all page related tags refer to the page found at the @path@ attribute.
 912      @path@s may be relative or absolute paths.
 913 
 914      *Usage:*
 915 
 916      <pre><code><r:find path="value_to_find">...</r:find></code></pre>
 917    }
 918    tag 'find' do |tag|
 919      required_attr(tag,'path','url')
 920      path = tag.attr['path'] || tag.attr['url']
 921 
 922      found = Page.find_by_path(absolute_path_for(tag.locals.page.path, path))
 923      if page_found?(found)
 924        tag.locals.page = found
 925        tag.expand
 926      end
 927    end
 928 
 929    desc %{
 930      Randomly renders one of the options specified by the @option@ tags.
 931 
 932      *Usage:*
 933 
 934      <pre><code><r:random>
 935        <r:option>...</r:option>
 936        <r:option>...</r:option>
 937        ...
 938      <r:random>
 939      </code></pre>
 940    }
 941    tag 'random' do |tag|
 942      tag.locals.random = []
 943      tag.expand
 944      options = tag.locals.random
 945      option = options[rand(options.size)]
 946      option if option
 947    end
 948    tag 'random:option' do |tag|
 949      items = tag.locals.random
 950      items << tag.expand
 951    end
 952 
 953    desc %{
 954      Nothing inside a set of hide tags is rendered.
 955 
 956      *Usage:*
 957 
 958      <pre><code><r:hide>...</r:hide></code></pre>
 959    }
 960    tag 'hide' do |tag|
 961    end
 962 
 963    desc %{
 964      Escapes angle brackets, etc. for rendering in an HTML document.
 965 
 966      *Usage:*
 967 
 968      <pre><code><r:escape_html>...</r:escape_html></code></pre>
 969    }
 970    tag "escape_html" do |tag|
 971      CGI.escapeHTML(tag.expand)
 972    end
 973 
 974    desc %{
 975      Outputs the published date using the format mandated by RFC 1123. (Ideal for RSS feeds.)
 976 
 977      *Usage:*
 978 
 979      <pre><code><r:rfc1123_date /></code></pre>
 980    }
 981    tag "rfc1123_date" do |tag|
 982      page = tag.locals.page
 983      if date = page.published_at || page.created_at
 984        CGI.rfc1123_date(date.to_time)
 985      end
 986    end
 987 
 988    desc %{
 989      Renders a list of links specified in the @paths@ attribute according to three
 990      states:
 991 
 992      * @normal@ specifies the normal state for the link
 993      * @here@ specifies the state of the link when the path matches the current
 994         page's PATH
 995      * @selected@ specifies the state of the link when the current page matches
 996         is a child of the specified path
 997      # @if_last@ renders its contents within a @normal@, @here@ or
 998        @selected@ tag if the item is the last in the navigation elements
 999      # @if_first@ renders its contents within a @normal@, @here@ or
1000        @selected@ tag if the item is the first in the navigation elements
1001 
1002      The @between@ tag specifies what should be inserted in between each of the links.
1003 
1004      *Usage:*
1005 
1006      <pre><code><r:navigation paths="[Title: path | Title: path | ...]">
1007        <r:normal><a href="<r:path />"><r:title /></a></r:normal>
1008        <r:here><strong><r:title /></strong></r:here>
1009        <r:selected><strong><a href="<r:path />"><r:title /></a></strong></r:selected>
1010        <r:between> | </r:between>
1011      </r:navigation>
1012      </code></pre>
1013    }
1014    tag 'navigation' do |tag|
1015      hash = tag.locals.navigation = {}
1016      tag.expand
1017      raise TagError.new("`navigation' tag must include a `normal' tag") unless hash.has_key? :normal
1018      ActiveSupport::Deprecation.warn("The 'urls' attribute of the r:navigation tag has been deprecated in favour of 'paths'. Please update your site.") if tag.attr['urls']
1019      result = []
1020      pairs = (tag.attr['paths']||tag.attr['urls']).to_s.split('|').map do |pair|
1021        parts = pair.split(':')
1022        value = parts.pop
1023        key = parts.join(':')
1024        [key.strip, value.strip]
1025      end
1026      pairs.each_with_index do |(title, path), i|
1027        compare_path = remove_trailing_slash(path)
1028        page_path = remove_trailing_slash(self.path)
1029        hash[:title] = title
1030        hash[:path] = path
1031        tag.locals.first_child = i == 0
1032        tag.locals.last_child = i == pairs.length - 1
1033        case page_path
1034        when compare_path
1035          result << (hash[:here] || hash[:selected] || hash[:normal]).call
1036        when Regexp.compile( '^' + Regexp.quote(path))
1037          result << (hash[:selected] || hash[:normal]).call
1038        else
1039          result << hash[:normal].call
1040        end
1041      end
1042      between = hash.has_key?(:between) ? hash[:between].call : ' '
1043      result.reject { |i| i.blank? }.join(between)
1044    end
1045    [:normal, :here, :selected, :between].each do |symbol|
1046      tag "navigation:#{symbol}" do |tag|
1047        hash = tag.locals.navigation
1048        hash[symbol] = tag.block
1049      end
1050    end
1051    [:title, :path].each do |symbol|
1052      tag "navigation:#{symbol}" do |tag|
1053        hash = tag.locals.navigation
1054        hash[symbol]
1055      end
1056    end
1057    tag "navigation:url" do |tag|
1058      hash = tag.locals.navigation
1059      ActiveSupport::Deprecation.warn("The 'r:navigation:url' tag has been deprecated in favour of 'r:navigation:path'. Please update your site.")
1060      hash[:path]
1061    end
1062 
1063    desc %{
1064      Renders the containing elements if the element is the first
1065      in the navigation list
1066 
1067      *Usage:*
1068 
1069      <pre><code><r:normal><r:if_first>...</r:if_first></r:normal></code></pre>
1070    }
1071    tag 'navigation:if_first' do |tag|
1072      tag.expand if tag.locals.first_child
1073    end
1074 
1075    desc %{
1076      Renders the containing elements unless the element is the first
1077      in the navigation list
1078 
1079      *Usage:*
1080 
1081      <pre><code><r:normal><r:unless_first>...</r:unless_first></r:normal></code></pre>
1082    }
1083    tag 'navigation:unless_first' do |tag|
1084      tag.expand unless tag.locals.first_child
1085    end
1086 
1087    desc %{
1088      Renders the containing elements unless the element is the last
1089      in the navigation list
1090 
1091      *Usage:*
1092 
1093      <pre><code><r:normal><r:unless_last>...</r:unless_last></r:normal></code></pre>
1094    }
1095    tag 'navigation:unless_last' do |tag|
1096      tag.expand unless tag.locals.last_child
1097    end
1098 
1099    desc %{
1100      Renders the containing elements if the element is the last
1101      in the navigation list
1102 
1103      *Usage:*
1104 
1105      <pre><code><r:normal><r:if_last>...</r:if_last></r:normal></code></pre>
1106    }
1107    tag 'navigation:if_last' do |tag|
1108      tag.expand if tag.locals.last_child
1109    end
1110 
1111    desc %{
1112      Renders the containing elements only if Radiant in is development mode.
1113 
1114      *Usage:*
1115 
1116      <pre><code><r:if_dev>...</r:if_dev></code></pre>
1117    }
1118    tag 'if_dev' do |tag|
1119      tag.expand if dev?(tag.globals.page.request)
1120    end
1121 
1122    desc %{
1123      The opposite of the @if_dev@ tag.
1124 
1125      *Usage:*
1126 
1127      <pre><code><r:unless_dev>...</r:unless_dev></code></pre>
1128    }
1129    tag 'unless_dev' do |tag|
1130      tag.expand unless dev?(tag.globals.page.request)
1131    end
1132 
1133    desc %{
1134      Prints the page's status as a string.  Optional attribute 'downcase'
1135      will cause the status to be all lowercase.
1136 
1137      *Usage:*
1138 
1139      <pre><code><r:status [downcase='true'] /></code></pre>
1140    }
1141    tag 'status' do |tag|
1142      status = tag.globals.page.status.name
1143      return status.downcase if tag.attr['downcase']
1144      status
1145    end
1146 
1147    desc %(
1148      Renders the content of the field given in the @name@ attribute.
1149 
1150      *Usage:*
1151 
1152      <pre><code><r:field name="Keywords" /></code></pre>
1153    )
1154    tag 'field' do |tag|
1155      required_attr(tag,'name')
1156      tag.locals.page.field(tag.attr['name']).try(:content)
1157    end
1158 
1159    desc %(
1160      Renders the contained elements if the field given in the @name@ attribute
1161      exists. The tag also takes an optional @equals@ or @matches@ attribute;
1162      these will expand the tag if the field's content equals or matches the
1163      given string or regex.
1164 
1165      *Usage:*
1166 
1167      <pre><code><r:if_field name="author" [equals|matches="John"] [ignore_case="true|false"]>...</r:if_field></code></pre>
1168    )
1169    tag 'if_field' do |tag|
1170      required_attr(tag,'name')
1171      field = tag.locals.page.field(tag.attr['name'])
1172      return '' if field.nil?
1173      tag.expand if case
1174        when (tag.attr['equals'] and tag.attr['ignore_case'] == 'false') then field.content == tag.attr['equals']
1175        when tag.attr['equals'] then field.content.downcase == tag.attr['equals'].downcase
1176        when tag.attr['matches'] then field.content =~ build_regexp_for(tag, 'matches')
1177        else field
1178      end
1179    end
1180 
1181    desc %(
1182      The opposite of @if_field@. Renders the contained elements unless the field
1183      given in the @name@ attribute exists. The tag also takes an optional
1184      @equals@ or @matches@ attribute; these will expand the tag unless the
1185      field's content equals or matches the given string or regex.
1186 
1187      *Usage:*
1188 
1189      <pre><code><r:unless_field name="author" [equals|matches="John"] [ignore_case="true|false"]>...</r:unless_field></code></pre>
1190    )
1191    tag 'unless_field' do |tag|
1192      required_attr(tag,'name')
1193      field = tag.locals.page.field(tag.attr['name'])
1194      tag.expand unless case
1195        when (field and (tag.attr['equals'] and tag.attr['ignore_case'] == 'false')) then field.content == tag.attr['equals']
1196        when (field and tag.attr['equals']) then field.content.downcase == tag.attr['equals'].downcase
1197        when (field and tag.attr['matches']) then field.content =~ build_regexp_for(tag, 'matches')
1198        else field
1199      end
1200    end
1201    
1202    tag 'site' do |tag|
1203      tag.expand
1204    end
1205    %w(title domain dev_domain).each do |attr|
1206      desc %{
1207        Returns Radiant::Config['site.#{attr}'] as configured under the Settings tab.
1208      }
1209      tag "site:#{attr}" do |tag|
1210        Radiant::Config["site.#{attr}"]
1211      end
1212    end  
1213 
1214    private
1215      def render_children_with_pagination(tag, opts={})
1216        if opts[:aggregate]
1217          findable = Page
1218          options = aggregate_children(tag)
1219        else
1220          findable = tag.locals.children
1221          options = children_find_options(tag)
1222        end
1223        paging = pagination_find_options(tag)
1224        result = []
1225        tag.locals.previous_headers = {}
1226        displayed_children = paging ? findable.paginate(options.merge(paging)) : findable.all(options)
1227        displayed_children.each_with_index do |item, i|
1228          tag.locals.child = item
1229          tag.locals.page = item
1230          tag.locals.first_child = i == 0
1231          tag.locals.last_child = i == displayed_children.length - 1
1232          result << tag.expand
1233        end
1234        if paging && displayed_children.total_pages > 1
1235          tag.locals.paginated_list = displayed_children
1236          result << tag.render('pagination', tag.attr.dup)
1237        end
1238        result.flatten.join('')
1239      end
1240      
1241      def children_find_options(tag)
1242        attr = tag.attr.symbolize_keys
1243 
1244        options = {}
1245 
1246        [:limit, :offset].each do |symbol|
1247          if number = attr[symbol]
1248            if number =~ /^\d+$/
1249              options[symbol] = number.to_i
1250            else
1251              raise TagError.new("`#{symbol}' attribute must be a positive number")
1252            end
1253          end
1254        end
1255 
1256        by = (attr[:by] || 'published_at').strip
1257        order = (attr[:order] || 'asc').strip
1258        order_string = ''
1259        if self.attributes.keys.include?(by)
1260          order_string << by
1261        else
1262          raise TagError.new("`by' attribute of `each' tag must be set to a valid field name")
1263        end
1264        if order =~ /^(asc|desc)$/i
1265          order_string << " #{$1.upcase}"
1266        else
1267          raise TagError.new(%{`order' attribute of `each' tag must be set to either "asc" or "desc"})
1268        end
1269        options[:order] = order_string
1270 
1271        status = (attr[:status] || ( dev?(tag.globals.page.request) ? 'all' : 'published')).downcase
1272        unless status == 'all'
1273          stat = Status[status]
1274          unless stat.nil?
1275            options[:conditions] = ["(virtual = ?) and (status_id = ?)", false, stat.id]
1276          else
1277            raise TagError.new(%{`status' attribute of `each' tag must be set to a valid status})
1278          end
1279        else
1280          options[:conditions] = ["virtual = ?", false]
1281        end
1282        options
1283      end
1284        
1285      def aggregate_children(tag)
1286        options = children_find_options(tag)
1287        parent_ids = tag.locals.parent_ids
1288      
1289        conditions = options[:conditions]
1290        conditions.first << " AND parent_id IN (?)"
1291        conditions << parent_ids
1292        options
1293      end
1294      
1295      def pagination_find_options(tag)
1296        attr = tag.attr.symbolize_keys
1297        if attr[:paginated] == 'true'
1298          pagination_parameters.merge(attr.slice(:per_page))
1299        else
1300          false
1301        end
1302      end
1303      
1304      def will_paginate_options(tag)
1305        attr = tag.attr.symbolize_keys
1306        if attr[:paginated] == 'true'
1307          attr.slice(:class, :previous_label, :next_label, :inner_window, :outer_window, :separator, :per_page).merge({:renderer => Radiant::Pagination::LinkRenderer.new(tag.globals.page.path)})
1308        else
1309          {}
1310        end
1311      end
1312 
1313      def remove_trailing_slash(string)
1314        (string =~ %r{^(.*?)/$}) ? $1 : string
1315      end
1316 
1317      def tag_part_name(tag)
1318        tag.attr['part'] || 'body'
1319      end
1320 
1321      def build_regexp_for(tag, attribute_name)
1322        ignore_case = tag.attr.has_key?('ignore_case') && tag.attr['ignore_case']=='false' ? nil : true
1323        begin
1324          regexp = Regexp.new(tag.attr['matches'], ignore_case)
1325        rescue RegexpError => e
1326          raise TagError.new("Malformed regular expression in `#{attribute_name}' argument of `#{tag.name}' tag: #{e.message}")
1327        end
1328        regexp
1329      end
1330 
1331      def relative_url_for(url, request)
1332        File.join(ActionController::Base.relative_url_root || '', url)
1333      end
1334 
1335      def absolute_path_for(base_path, new_path)
1336        if new_path.first == '/'
1337          new_path
1338        else
1339          File.expand_path(File.join(base_path, new_path))
1340        end
1341      end
1342 
1343      def page_found?(page)
1344        page && !(FileNotFoundPage === page)
1345      end
1346 
1347      def boolean_attr_or_error(tag, attribute_name, default)
1348        attribute = attr_or_error(tag, :attribute_name => attribute_name, :default => default.to_s, :values => 'true, false')
1349        (attribute.to_s.downcase == 'true') ? true : false
1350      end
1351 
1352      def attr_or_error(tag, options = {})
1353        attribute_name = options[:attribute_name].to_s
1354        default = options[:default]
1355        values = options[:values].split(',').map!(&:strip)
1356 
1357        attribute = (tag.attr[attribute_name] || default).to_s
1358        raise TagError.new(%{`#{attribute_name}' attribute of `#{tag.name}' tag must be one of: #{values.join(', ')}}) unless values.include?(attribute)
1359        return attribute
1360      end
1361      
1362      def required_attr(tag, *attribute_names)
1363        attr_collection = attribute_names.map{|a| "`#{a}'"}.join(' or ')
1364        raise TagError.new("`#{tag.name}' tag must contain a #{attr_collection} attribute.") if (tag.attr.keys & attribute_names).blank?
1365      end
1366 
1367      def dev?(request)
1368        return false if request.nil?
1369        if dev_host = Radiant::Config['dev.host']
1370          dev_host == request.host
1371        else
1372          request.host =~ /^dev\./
1373        end
1374      end
1375      
1376  end