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 <r:find>) 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 <r:children:each>) 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 <r:children:each>) 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 <r:children:each>) 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 <r:children:each>) 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'] || ' > '
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