radiant 0.5.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of radiant might be problematic. Click here for more details.

Files changed (205) hide show
  1. data/CHANGELOG +1 -0
  2. data/CONTRIBUTORS +12 -0
  3. data/LICENSE +9 -0
  4. data/README +91 -0
  5. data/Rakefile +10 -0
  6. data/app/behaviors/archive_behavior.rb +42 -0
  7. data/app/behaviors/archive_day_index_behavior.rb +30 -0
  8. data/app/behaviors/archive_month_index_behavior.rb +30 -0
  9. data/app/behaviors/archive_year_index_behavior.rb +30 -0
  10. data/app/behaviors/env_behavior.rb +18 -0
  11. data/app/behaviors/page_missing_behavior.rb +31 -0
  12. data/app/controllers/admin/export_controller.rb +8 -0
  13. data/app/controllers/admin/layout_controller.rb +23 -0
  14. data/app/controllers/admin/model_controller.rb +119 -0
  15. data/app/controllers/admin/page_controller.rb +119 -0
  16. data/app/controllers/admin/snippet_controller.rb +6 -0
  17. data/app/controllers/admin/user_controller.rb +45 -0
  18. data/app/controllers/admin/welcome_controller.rb +39 -0
  19. data/app/controllers/application.rb +32 -0
  20. data/app/controllers/site_controller.rb +54 -0
  21. data/app/filters/markdown_filter.rb +9 -0
  22. data/app/filters/textile_filter.rb +9 -0
  23. data/app/helpers/admin/export_helper.rb +2 -0
  24. data/app/helpers/admin/layout_helper.rb +2 -0
  25. data/app/helpers/admin/page_helper.rb +10 -0
  26. data/app/helpers/admin/snippet_helper.rb +2 -0
  27. data/app/helpers/admin/user_helper.rb +2 -0
  28. data/app/helpers/admin/welcome_helper.rb +2 -0
  29. data/app/helpers/application_helper.rb +141 -0
  30. data/app/helpers/site_helper.rb +2 -0
  31. data/app/models/archive_finder.rb +66 -0
  32. data/app/models/behavior.rb +194 -0
  33. data/app/models/layout.rb +13 -0
  34. data/app/models/page.rb +91 -0
  35. data/app/models/page_context.rb +526 -0
  36. data/app/models/page_part.rb +13 -0
  37. data/app/models/radiant/config.rb +53 -0
  38. data/app/models/radiant/exporter.rb +11 -0
  39. data/app/models/response_cache.rb +112 -0
  40. data/app/models/snippet.rb +16 -0
  41. data/app/models/status.rb +31 -0
  42. data/app/models/text_filter.rb +11 -0
  43. data/app/models/user.rb +70 -0
  44. data/app/models/user_action_observer.rb +13 -0
  45. data/app/views/admin/layout/index.rhtml +38 -0
  46. data/app/views/admin/layout/new.rhtml +37 -0
  47. data/app/views/admin/layout/remove.rhtml +17 -0
  48. data/app/views/admin/page/_node.rhtml +51 -0
  49. data/app/views/admin/page/_part.rhtml +17 -0
  50. data/app/views/admin/page/children.rhtml +4 -0
  51. data/app/views/admin/page/index.rhtml +172 -0
  52. data/app/views/admin/page/new.rhtml +164 -0
  53. data/app/views/admin/page/remove.rhtml +14 -0
  54. data/app/views/admin/snippet/index.rhtml +36 -0
  55. data/app/views/admin/snippet/new.rhtml +28 -0
  56. data/app/views/admin/snippet/remove.rhtml +16 -0
  57. data/app/views/admin/user/index.rhtml +43 -0
  58. data/app/views/admin/user/new.rhtml +48 -0
  59. data/app/views/admin/user/preferences.rhtml +29 -0
  60. data/app/views/admin/user/remove.rhtml +16 -0
  61. data/app/views/admin/welcome/login.rhtml +51 -0
  62. data/app/views/layouts/application.rhtml +75 -0
  63. data/app/views/site/not_found.rhtml +3 -0
  64. data/bin/radiant +305 -0
  65. data/config/boot.rb +80 -0
  66. data/config/database.mysql.yml +20 -0
  67. data/config/database.postgresql.yml +20 -0
  68. data/config/database.sqlite.yml +20 -0
  69. data/config/environment.rb +76 -0
  70. data/config/environments/development.rb +20 -0
  71. data/config/environments/production.rb +22 -0
  72. data/config/environments/test.rb +20 -0
  73. data/config/locomotive.yml +6 -0
  74. data/config/routes.rb +64 -0
  75. data/db/development_structure.sql +80 -0
  76. data/db/migrate/001_create_radiant_tables.rb +73 -0
  77. data/db/migrate/002_insert_initial_data.rb +45 -0
  78. data/db/migrate/003_rename_behavior_column.rb +9 -0
  79. data/db/migrate/004_rename_filter_column.rb +11 -0
  80. data/db/migrate/005_add_virtual_column_to_page.rb +9 -0
  81. data/db/migrate/006_integer_columns_to_boolean.rb +11 -0
  82. data/db/migrate/007_remove_virtual_column_from_page.rb +9 -0
  83. data/db/migrate/008_add_virtual_column_to_page_again.rb +9 -0
  84. data/db/migrate/009_add_content_type_field_to_layout.rb +9 -0
  85. data/db/schema.rb +74 -0
  86. data/db/templates/empty.yml +2 -0
  87. data/db/templates/simple-blog.yml +197 -0
  88. data/db/templates/styled-blog.yml +472 -0
  89. data/lib/advanced_delegation.rb +21 -0
  90. data/lib/archive_index_behavior_tags_and_methods.rb +48 -0
  91. data/lib/generators/behavior/USAGE +16 -0
  92. data/lib/generators/behavior/behavior_generator.rb +22 -0
  93. data/lib/generators/behavior/templates/model.rb.template +9 -0
  94. data/lib/generators/behavior/templates/unit_test.rb.template +16 -0
  95. data/lib/generators/filter/USAGE +16 -0
  96. data/lib/generators/filter/filter_generator.rb +22 -0
  97. data/lib/generators/filter/templates/model.rb.template +8 -0
  98. data/lib/generators/filter/templates/unit_test.rb.template +7 -0
  99. data/lib/inheritable_class_attributes.rb +65 -0
  100. data/lib/login_system.rb +80 -0
  101. data/lib/plugins/index_quoting_fix/init.rb +32 -0
  102. data/lib/plugins/string_io/init.rb +2 -0
  103. data/lib/radiant.rb +5 -0
  104. data/lib/registerable.rb +70 -0
  105. data/lib/tasks/release.rake +84 -0
  106. data/public/404.html +8 -0
  107. data/public/500.html +8 -0
  108. data/public/dispatch.cgi +10 -0
  109. data/public/dispatch.fcgi +24 -0
  110. data/public/dispatch.rb +10 -0
  111. data/public/favicon.ico +0 -0
  112. data/public/images/add-child.png +0 -0
  113. data/public/images/brown-bottom-line.gif +0 -0
  114. data/public/images/clear-page-cache.png +0 -0
  115. data/public/images/collapse.png +0 -0
  116. data/public/images/expand.png +0 -0
  117. data/public/images/minus.png +0 -0
  118. data/public/images/new-homepage.png +0 -0
  119. data/public/images/new-layout.png +0 -0
  120. data/public/images/new-snippet.png +0 -0
  121. data/public/images/new-user.png +0 -0
  122. data/public/images/page.png +0 -0
  123. data/public/images/plus.png +0 -0
  124. data/public/images/remove-disabled.png +0 -0
  125. data/public/images/remove.png +0 -0
  126. data/public/images/snippet.png +0 -0
  127. data/public/images/spinner.gif +0 -0
  128. data/public/images/view-site.gif +0 -0
  129. data/public/images/virtual-page.png +0 -0
  130. data/public/javascripts/application.js +2 -0
  131. data/public/javascripts/controls.js +815 -0
  132. data/public/javascripts/dragdrop.js +913 -0
  133. data/public/javascripts/effects.js +958 -0
  134. data/public/javascripts/pngfix.js +78 -0
  135. data/public/javascripts/prototype.js +2006 -0
  136. data/public/javascripts/ruledtable.js +28 -0
  137. data/public/javascripts/string.js +23 -0
  138. data/public/javascripts/tabcontrol.js +140 -0
  139. data/public/robots.txt +1 -0
  140. data/public/stylesheets/admin.css +464 -0
  141. data/script/about +3 -0
  142. data/script/breakpointer +3 -0
  143. data/script/console +3 -0
  144. data/script/destroy +3 -0
  145. data/script/generate +3 -0
  146. data/script/performance/benchmarker +3 -0
  147. data/script/performance/profiler +3 -0
  148. data/script/plugin +3 -0
  149. data/script/process/reaper +3 -0
  150. data/script/process/spawner +3 -0
  151. data/script/process/spinner +3 -0
  152. data/script/runner +3 -0
  153. data/script/server +3 -0
  154. data/script/setup_database +297 -0
  155. data/test/fixtures/layouts.yml +26 -0
  156. data/test/fixtures/page_parts.yml +99 -0
  157. data/test/fixtures/pages.yml +359 -0
  158. data/test/fixtures/pages.yml.rej +28 -0
  159. data/test/fixtures/snippets.yml +18 -0
  160. data/test/fixtures/users.yml +30 -0
  161. data/test/functional/admin/export_controller_test.rb +22 -0
  162. data/test/functional/admin/layout_controller_test.rb +40 -0
  163. data/test/functional/admin/model_controller_test.rb +152 -0
  164. data/test/functional/admin/page_controller_test.rb +179 -0
  165. data/test/functional/admin/snippet_controller_test.rb +11 -0
  166. data/test/functional/admin/user_controller_test.rb +71 -0
  167. data/test/functional/admin/welcome_controller_test.rb +48 -0
  168. data/test/functional/application_controller_test.rb +44 -0
  169. data/test/functional/login_system_test.rb +155 -0
  170. data/test/functional/site_controller_test.rb +172 -0
  171. data/test/helpers/archive_index_test_helper.rb +35 -0
  172. data/test/helpers/behavior_render_test_helper.rb +34 -0
  173. data/test/helpers/behavior_test_helper.rb +47 -0
  174. data/test/helpers/caching_test_helper.rb +41 -0
  175. data/test/helpers/layout_test_helper.rb +35 -0
  176. data/test/helpers/page_part_test_helper.rb +49 -0
  177. data/test/helpers/page_test_helper.rb +36 -0
  178. data/test/helpers/snippet_test_helper.rb +32 -0
  179. data/test/helpers/user_test_helper.rb +34 -0
  180. data/test/helpers/validation_test_helper.rb +42 -0
  181. data/test/test_helper.rb +54 -0
  182. data/test/unit/behavior_test.rb +196 -0
  183. data/test/unit/behaviors/archive_behavior_test.rb +33 -0
  184. data/test/unit/behaviors/archive_day_index_behavior_test.rb +21 -0
  185. data/test/unit/behaviors/archive_month_index_behavior_test.rb +21 -0
  186. data/test/unit/behaviors/archive_year_index_behavior_test.rb +21 -0
  187. data/test/unit/behaviors/page_missing_behavior_test.rb +27 -0
  188. data/test/unit/filters/markdown_filter_test.rb +14 -0
  189. data/test/unit/filters/textile_filter_test.rb +14 -0
  190. data/test/unit/inheritable_class_attributes_test.rb +47 -0
  191. data/test/unit/layout_test.rb +29 -0
  192. data/test/unit/page_context_test.rb +375 -0
  193. data/test/unit/page_context_test.rb.rej +26 -0
  194. data/test/unit/page_part_test.rb +44 -0
  195. data/test/unit/page_test.rb +224 -0
  196. data/test/unit/radiant/config_test.rb +46 -0
  197. data/test/unit/radiant/exporter_test.rb +26 -0
  198. data/test/unit/registerable_test.rb +68 -0
  199. data/test/unit/response_cache_test.rb +133 -0
  200. data/test/unit/snippet_test.rb +47 -0
  201. data/test/unit/status_test.rb +43 -0
  202. data/test/unit/text_filter_test.rb +14 -0
  203. data/test/unit/user_action_observer_test.rb +40 -0
  204. data/test/unit/user_test.rb +138 -0
  205. metadata +355 -0
@@ -0,0 +1,2 @@
1
+ module SiteHelper
2
+ end
@@ -0,0 +1,66 @@
1
+ class ArchiveFinder
2
+ def initialize(&block)
3
+ @block = block
4
+ end
5
+
6
+ def find(method, options = {})
7
+ @block.call(method, options)
8
+ end
9
+
10
+ class << self
11
+ def year_finder(finder, year)
12
+ new do |method, options|
13
+ add_condition(options, "#{extract('year', 'published_at')} = ?", year.to_i)
14
+ finder.find(method, options)
15
+ end
16
+ end
17
+
18
+ def month_finder(finder, year, month)
19
+ finder = year_finder(finder, year)
20
+ new do |method, options|
21
+ add_condition(options, "#{extract('month', 'published_at')} = ?", month.to_i)
22
+ finder.find(method, options)
23
+ end
24
+ end
25
+
26
+ def day_finder(finder, year, month, day)
27
+ finder = month_finder(finder, year, month)
28
+ new do |method, options|
29
+ add_condition(options, "#{extract('day', 'published_at')} = ?", day.to_i)
30
+ finder.find(method, options)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def concat_conditions(a, b)
37
+ sql = "(#{ [a.shift, b.shift].compact.join(") AND (") })"
38
+ params = a + b
39
+ [sql, *params]
40
+ end
41
+
42
+ def add_condition(options, *condition)
43
+ old = options[:conditions] || []
44
+ conditions = concat_conditions(old, condition)
45
+ options[:conditions] = conditions
46
+ options
47
+ end
48
+
49
+ def extract(part, field)
50
+ case ActiveRecord::Base.connection.adapter_name
51
+ when /sqlite/i
52
+ case part
53
+ when /year/i
54
+ "STRFTIME('%Y', #{field})"
55
+ when /month/i
56
+ "STRFTIME('%m', #{field})"
57
+ when /day/i
58
+ "STRFTIME('%d', #{field})"
59
+ end
60
+ else
61
+ "EXTRACT(#{part.upcase} FROM #{field})"
62
+ end
63
+ end
64
+
65
+ end
66
+ end
@@ -0,0 +1,194 @@
1
+ require 'registerable'
2
+
3
+ module Behavior
4
+ include Registerable
5
+
6
+ class Base
7
+
8
+ include InheritableClassAttributes
9
+
10
+ cattr_inheritable_reader :additional_tag_definition_blocks
11
+ @additional_tag_definition_blocks = []
12
+
13
+ cattr_inheritable_reader :additional_child_tag_definition_blocks
14
+ @additional_child_tag_definition_blocks = []
15
+
16
+ cattr_inheritable_accessor :description
17
+
18
+ class << self
19
+ def description(value = nil)
20
+ if value.nil?
21
+ @description
22
+ else
23
+ @description = value
24
+ end
25
+ end
26
+
27
+ def define_tags(&block)
28
+ @additional_tag_definition_blocks << block
29
+ end
30
+
31
+ def define_child_tags(&block)
32
+ @additional_child_tag_definition_blocks << block
33
+ end
34
+ end
35
+
36
+ attr_accessor :page, :request, :response
37
+
38
+ def initialize(page)
39
+ @page = page
40
+ end
41
+
42
+ def description
43
+ self.class.description
44
+ end
45
+
46
+ def process(request, response)
47
+ @request, @response = request, response
48
+ if @page.layout
49
+ content_type = @page.layout.content_type.to_s.strip
50
+ @response.headers['Content-Type'] = content_type unless content_type.empty?
51
+ end
52
+ page_headers.each { |k,v| @response.headers[k] = v }
53
+ @response.body = render_page
54
+ @request, @response = nil, nil
55
+ end
56
+
57
+ def page_url
58
+ if parent_behavior?
59
+ clean_url(parent_behavior.child_url(@page))
60
+ else
61
+ clean_url(@page.slug)
62
+ end
63
+ end
64
+
65
+ def child_url(child)
66
+ clean_url(page_url + '/' + child.slug)
67
+ end
68
+
69
+ def page_headers
70
+ { 'Status' => ActionController::Base::DEFAULT_RENDER_STATUS_CODE }
71
+ end
72
+
73
+ def page_virtual?
74
+ false
75
+ end
76
+
77
+ def page_config
78
+ string = render_page_part(:config)
79
+ unless string.empty?
80
+ YAML::load(string)
81
+ else
82
+ {}
83
+ end
84
+ end
85
+
86
+ def cache_page?
87
+ true
88
+ end
89
+
90
+ def find_page_by_url(url, live = true, clean = true)
91
+ url = clean_url(url) if clean
92
+ if page_url == url && (not live or @page.published?)
93
+ @page
94
+ else
95
+ @page.children.each do |child|
96
+ if (url =~ Regexp.compile( '^' + Regexp.quote(child.url))) and (not child.virtual?)
97
+ found = child.behavior.find_page_by_url(url, live, clean)
98
+ return found if found
99
+ end
100
+ end
101
+ @page.children.find(:first, :conditions => "behavior_id = 'Page Missing'")
102
+ end
103
+ end
104
+
105
+ def test_find_page_by_url__when_virtual
106
+ setup_for_page(:homepage)
107
+ found = @behavior.find_page_by_url('/parent/virtual/')
108
+ assert_equal nil, found
109
+ end
110
+
111
+ def render_page
112
+ lazy_initialize_parser_and_context
113
+ if layout = @page.layout
114
+ parse_object(layout)
115
+ else
116
+ render_page_part(:body)
117
+ end
118
+ end
119
+
120
+ def render_page_part(part_name)
121
+ lazy_initialize_parser_and_context
122
+ part = @page.part(part_name)
123
+ if part
124
+ parse_object(part)
125
+ else
126
+ ''
127
+ end
128
+ end
129
+
130
+ def render_snippet(snippet)
131
+ lazy_initialize_parser_and_context
132
+ parse_object(snippet)
133
+ end
134
+
135
+ def render_text(text)
136
+ lazy_initialize_parser_and_context
137
+ parse(text)
138
+ end
139
+
140
+ def add_tags_to_child_context(behavior)
141
+ self.class.additional_child_tag_definition_blocks.each { |block| behavior.instance_eval &block }
142
+ end
143
+
144
+ private
145
+
146
+ def lazy_initialize_parser_and_context
147
+ unless @context and @parser
148
+ @context = PageContext.new(@page)
149
+ self.class.additional_tag_definition_blocks.each { |block| instance_eval &block }
150
+ add_tags_from_parent_to_context
151
+ @parser = Radius::Parser.new(@context, :tag_prefix => 'r')
152
+ end
153
+ end
154
+
155
+ def clean_url(url)
156
+ "/#{ url.strip }/".gsub(%r{//+}, '/')
157
+ end
158
+
159
+ def parse_object(object)
160
+ text = object.content
161
+ text = parse(text)
162
+ text = object.filter.filter(text) if object.respond_to? :filter_id
163
+ text
164
+ end
165
+
166
+ def parse(text)
167
+ @parser.parse(text)
168
+ end
169
+
170
+ def parent
171
+ @page.parent
172
+ end
173
+
174
+ def parent?
175
+ not parent.nil?
176
+ end
177
+
178
+ def parent_behavior
179
+ parent.behavior if parent?
180
+ end
181
+
182
+ def parent_behavior?
183
+ not parent_behavior.nil?
184
+ end
185
+
186
+ def add_tags_from_parent_to_context
187
+ parent_behavior.add_tags_to_child_context(self) if parent_behavior?
188
+ end
189
+
190
+ def tag(*args, &block)
191
+ @context.define_tag(*args, &block)
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,13 @@
1
+ class Layout < ActiveRecord::Base
2
+
3
+ # Associations
4
+ has_many :pages
5
+ belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by'
6
+ belongs_to :updated_by, :class_name => 'User', :foreign_key => 'updated_by'
7
+
8
+ # Validations
9
+ validates_presence_of :name, :message => 'required'
10
+ validates_uniqueness_of :name, :message => 'name already in use'
11
+ validates_length_of :name, :maximum => 100, :message => '%d-character limit'
12
+
13
+ end
@@ -0,0 +1,91 @@
1
+ require_dependency 'advanced_delegation'
2
+
3
+ class Page < ActiveRecord::Base
4
+
5
+ class MissingRootPageError < StandardError
6
+ def initialize(message = 'Database missing root page'); super end
7
+ end
8
+
9
+ # Callbacks
10
+ before_save :update_published_at, :update_virtual
11
+
12
+ # Associations
13
+ acts_as_tree :order => 'virtual DESC, title ASC'
14
+ has_many :parts, :class_name => 'PagePart', :dependent => :destroy
15
+ belongs_to :layout
16
+ belongs_to :created_by, :class_name => 'User', :foreign_key => 'created_by'
17
+ belongs_to :updated_by, :class_name => 'User', :foreign_key => 'updated_by'
18
+
19
+ # Validations
20
+ validates_presence_of :title, :slug, :breadcrumb, :status_id, :message => 'required'
21
+
22
+ validates_length_of :title, :maximum => 255, :message => '%d-character limit'
23
+ validates_length_of :slug, :maximum => 100, :message => '%d-character limit'
24
+ validates_length_of :breadcrumb, :maximum => 160, :message => '%d-character limit'
25
+ validates_length_of :behavior_id, :maximum => 25, :allow_nil => true, :message => '%d-character limit'
26
+
27
+ validates_format_of :slug, :with => %r{^([-_.A-Za-z0-9]*|/)$}, :message => 'invalid format'
28
+ validates_uniqueness_of :slug, :scope => :parent_id, :message => 'slug already in use for child of parent'
29
+ validates_numericality_of :id, :status_id, :parent_id, :allow_nil => true, :only_integer => true, :message => 'must be a number'
30
+
31
+ delegate_to :behavior, :url => :page_url, :cache? => :cache_page?, :find_by_url => :find_page_by_url,
32
+ :render => :render_page, :virtual? => :page_virtual?
33
+
34
+ delegate_to :behavior, :process, :child_url
35
+
36
+ def after_initialize
37
+ class << self
38
+ define_method(:layout) do |*params|
39
+ super || (parent and parent.layout)
40
+ end
41
+ end
42
+ end
43
+
44
+ def part(name)
45
+ parts.find_by_name name.to_s
46
+ end
47
+
48
+ def status
49
+ Status.find(self.status_id)
50
+ end
51
+
52
+ def status=(value)
53
+ self.status_id = value.id
54
+ end
55
+
56
+ def published?
57
+ status == Status[:published]
58
+ end
59
+
60
+ def behavior
61
+ if @behavior.nil? or (@old_behavior_id != behavior_id)
62
+ @old_behavior_id = behavior_id
63
+ @behavior = Behavior[behavior_id].new(self)
64
+ else
65
+ @behavior
66
+ end
67
+ end
68
+
69
+ def self.find_by_url(url, live = true)
70
+ root = find_by_parent_id(nil)
71
+ raise MissingRootPageError unless root
72
+ root.find_by_url(url, live)
73
+ end
74
+
75
+ def virtual
76
+ !(read_attribute('virtual').to_s =~ /^(false|f|0|)$/)
77
+ end
78
+
79
+ private
80
+
81
+ def update_published_at
82
+ write_attribute('published_at', Time.now) if (status_id.to_i == Status[:published].id) and published_at.nil?
83
+ true
84
+ end
85
+
86
+ def update_virtual
87
+ write_attribute('virtual', virtual?)
88
+ true
89
+ end
90
+
91
+ end
@@ -0,0 +1,526 @@
1
+ class PageContext < Radius::Context
2
+ class TagError < StandardError; end
3
+
4
+ attr_reader :page
5
+
6
+ def initialize(page)
7
+ super()
8
+
9
+ @page = page
10
+ globals.page = @page
11
+
12
+ # <r:page>...</r:page>
13
+ #
14
+ # Causes the tags referring to a page's attributes to refer to the current page.
15
+ #
16
+ define_tag 'page' do |tag|
17
+ tag.locals.page = tag.globals.page
18
+ tag.expand
19
+ end
20
+
21
+ #
22
+ # <r:url />
23
+ # <r:title />
24
+ # etc...
25
+ #
26
+ ((@page.attributes.symbolize_keys.keys + [:url]) - [:created_by, :updated_by]).each do |method|
27
+ define_tag(method.to_s) do |tag|
28
+ tag.locals.page.send(method)
29
+ end
30
+ end
31
+
32
+ #
33
+ # <r:children>...</r:children>
34
+ #
35
+ # Gives access to a page's children.
36
+ #
37
+ define_tag 'children' do |tag|
38
+ tag.locals.children = tag.locals.page.children
39
+ tag.expand
40
+ end
41
+
42
+ #
43
+ # <r:children:count />
44
+ #
45
+ # Renders the total number of children.
46
+ #
47
+ define_tag 'children:count' do |tag|
48
+ tag.locals.children.count
49
+ end
50
+
51
+ #
52
+ # <r:children:first>...<r:children:first>
53
+ #
54
+ # Returns the first child. Inside this tag all page attribute tags are mapped to
55
+ # the first child.
56
+ #
57
+ define_tag 'children:first' do |tag|
58
+ children = tag.locals.children
59
+ if first = children.first
60
+ tag.locals.page = first
61
+ tag.expand
62
+ end
63
+ end
64
+
65
+ #
66
+ # <r:children:last>...</r:children:last>
67
+ #
68
+ # Returns the last child. Inside this tag all page attribute tags are mapped to
69
+ # the last child.
70
+ #
71
+ define_tag 'children:last' do |tag|
72
+ children = tag.locals.children
73
+ if last = children.last
74
+ tag.locals.page = last
75
+ tag.expand
76
+ end
77
+ end
78
+
79
+ #
80
+ # <r:children:each [offset="number"] [limit="number"] [by="attribute"] [order="asc|desc"]
81
+ # [status="draft|reviewed|published|hidden|all"]>
82
+ # ...
83
+ # </r:children:each>
84
+ #
85
+ # Cycles through each of the children. Inside this tag all page attribute tags
86
+ # are mapped to the current child page.
87
+ #
88
+ define_tag "children:each" do |tag|
89
+ attr = tag.attr.symbolize_keys
90
+
91
+ options = {}
92
+
93
+ [:limit, :offset].each do |symbol|
94
+ if number = attr[symbol]
95
+ if number =~ /^\d{1,4}$/
96
+ options[symbol] = number.to_i
97
+ else
98
+ raise TagError.new("`#{symbol}' attribute of `each' tag must be a positive number between 1 and 4 digits")
99
+ end
100
+ end
101
+ end
102
+
103
+ by = (attr[:by] || 'published_at').strip
104
+ order = (attr[:order] || 'asc').strip
105
+ order_string = ''
106
+ if @page.attributes.keys.include?(by)
107
+ order_string << by
108
+ else
109
+ raise TagError.new("`by' attribute of `each' tag must be set to a valid field name")
110
+ end
111
+ if order =~ /^(asc|desc)$/i
112
+ order_string << " #{$1.upcase}"
113
+ else
114
+ raise TagError.new(%{`order' attribute of `each' tag must be set to either "asc" or "desc"})
115
+ end
116
+ options[:order] = order_string
117
+
118
+ status = (attr[:status] || 'published').downcase
119
+ unless status == 'all'
120
+ stat = Status[status]
121
+ unless stat.nil?
122
+ options[:conditions] = ["(virtual = ?) and (status_id = ?)", false, stat.id]
123
+ else
124
+ raise TagError.new(%{`status' attribute of `each' tag must be set to a valid status})
125
+ end
126
+ else
127
+ options[:conditions] = ["virtual = ?", false]
128
+ end
129
+
130
+ result = []
131
+ children = tag.locals.children
132
+ tag.locals.previous_headers = {}
133
+ children.find(:all, options).each do |item|
134
+ tag.locals.child = item
135
+ tag.locals.page = item
136
+ result << tag.expand
137
+ end
138
+ result
139
+ end
140
+
141
+ #
142
+ # <r:child>...</r:child>
143
+ #
144
+ # Page attribute tags inside of this tag refer to the current child. Not needed in
145
+ # most cases.
146
+ #
147
+ define_tag 'children:each:child' do |tag|
148
+ tag.locals.page = tag.locals.child
149
+ tag.expand
150
+ end
151
+
152
+ #
153
+ # <header [name="header_name"] [restart="name1[;name2;...]"]>...</r:header>
154
+ #
155
+ # Renders the tag contents only if the contents do not match the previous header. This
156
+ # is extremely useful for rendering date headers for a list of child pages.
157
+ #
158
+ # If you would like to use several header blocks you may use the 'name' attribute to
159
+ # name the header. When a header is named it will not restart until another header of
160
+ # the same name is different.
161
+ #
162
+ # Using the 'restart' attribute you can cause other named headers to restart when the
163
+ # present header changes. Simply specify the names of the other headers in a semicolon
164
+ # separated list.
165
+ #
166
+ define_tag 'children:each:header' do |tag|
167
+ previous_headers = tag.locals.previous_headers
168
+ name = tag.attr['name'] || :unnamed
169
+ restart = (tag.attr['restart'] || '').split(';')
170
+ header = tag.expand
171
+ unless header == previous_headers[name]
172
+ previous_headers[name] = header
173
+ unless restart.empty?
174
+ restart.each do |n|
175
+ previous_headers[n] = nil
176
+ end
177
+ end
178
+ header
179
+ end
180
+ end
181
+
182
+ #
183
+ # <r:content [part="part_name"] [inherit="true|false"] [contextual="true|false"] />
184
+ #
185
+ # Renders the main content of a page. Use the 'part' attribute to select a specific
186
+ # page part. By default the 'part' attribute is set to body. Use the 'inherit'
187
+ # attribute to specify that if a page does not have a content part by that name that
188
+ # the tag should render the parent's content part. By default 'inherit' is set to
189
+ # 'false'. Use the 'contextual' attribute to force a part inherited from a parent
190
+ # part to be evaluated in the context of the child page. By default 'contextual'
191
+ # is set to false.
192
+ #
193
+ define_tag 'content' do |tag|
194
+ page = tag.locals.page
195
+ part_name = tag_part_name(tag)
196
+ boolean_attr = proc do |attribute_name, default|
197
+ attribute = (tag.attr[attribute_name] || default).to_s
198
+ raise TagError.new(%{`#{attribute_name}' attribute of `content' tag must be set to either "true" or "false"}) unless attribute =~ /true|false/i
199
+ (attribute.downcase == 'true') ? true : false
200
+ end
201
+ inherit = boolean_attr['inherit', false]
202
+ part_page = page
203
+ if inherit
204
+ while (part_page.part(part_name).nil? and (not part_page.parent.nil?)) do
205
+ part_page = part_page.parent
206
+ end
207
+ end
208
+ contextual = boolean_attr['contextual', true]
209
+ if inherit and contextual
210
+ part = part_page.part(part_name)
211
+ page.behavior.render_snippet(part)
212
+ else
213
+ part_page.behavior.render_page_part(part_name)
214
+ end
215
+ end
216
+
217
+ #
218
+ # <r:if_content [part="part_name"]>...</r:if_content>
219
+ #
220
+ # Renders the containing elements only if the part exists on a page. By default the
221
+ # 'part' attribute is set 'body'.
222
+ #
223
+ define_tag 'if_content' do |tag|
224
+ page = tag.locals.page
225
+ part_name = tag_part_name(tag)
226
+ unless page.part(part_name).nil?
227
+ tag.expand
228
+ end
229
+ end
230
+
231
+ #
232
+ # <r:unless_content [part="part_name"]>...</r:unless_content>
233
+ #
234
+ # The opposite of the 'if_content' tag.
235
+ #
236
+ define_tag 'unless_content' do |tag|
237
+ page = tag.locals.page
238
+ part_name = tag_part_name(tag)
239
+ if page.part(part_name).nil?
240
+ tag.expand
241
+ end
242
+ end
243
+
244
+ #
245
+ # <r:if_url matches="regexp" [ignore_case="true|false"]>...</if_url>
246
+ #
247
+ # Renders the containing elements only if the page's url matches the regular expression
248
+ # given in the 'matches' attribute. If the 'ìgnore_case' attribute is set to false, the
249
+ # match is case sensitive. By default, 'ignore_case' is set to true.
250
+ #
251
+ define_tag 'if_url' do |tag|
252
+ raise TagError.new("`if_url' tag must contain a `matches' attribute.") unless tag.attr.has_key?('matches')
253
+ regexp = build_regexp_for(tag, 'matches')
254
+ unless tag.locals.page.url.match(regexp).nil?
255
+ tag.expand
256
+ end
257
+ end
258
+
259
+ #
260
+ # <r:unless_url matches="regexp" [ignore_case="true|false"]>...</unless_url>
261
+ #
262
+ # The opposite of the 'if_url' tag.
263
+ #
264
+ define_tag 'unless_url' do |tag|
265
+ raise TagError.new("`unless_url' tag must contain a `matches' attribute.") unless tag.attr.has_key?('matches')
266
+ regexp = build_regexp_for(tag, 'matches')
267
+ if tag.locals.page.url.match(regexp).nil?
268
+ tag.expand
269
+ end
270
+ end
271
+
272
+ #
273
+ # <r:author />
274
+ #
275
+ # Renders the name of the Author of the current page.
276
+ #
277
+ define_tag 'author' do |tag|
278
+ page = tag.locals.page
279
+ if author = page.created_by
280
+ author.name
281
+ end
282
+ end
283
+
284
+ #
285
+ # <r:date [format="format_string"] />
286
+ #
287
+ # Renders the date that a page was published (or in the event that it has
288
+ # not ben modified yet, the date that it was created). The format attribute
289
+ # uses the same formating codes used by the Ruby +strftime+ function. By
290
+ # default it's set to '%A, %B %d, %Y'.
291
+ #
292
+ define_tag 'date' do |tag|
293
+ page = tag.locals.page
294
+ format = tag_time_format(tag)
295
+ if date = page.published_at || page.created_at
296
+ date.strftime(format)
297
+ end
298
+ end
299
+
300
+ #
301
+ # <r:link [anchor="name"] [other attributes...] />
302
+ # <r:link>...</r:link>
303
+ #
304
+ # Renders a link to the page. When used as a single tag it uses the page's title
305
+ # for the link name. When used as a double tag the part inbetween both tags will
306
+ # be used as the link text. The link tag passes all attributes over to the HTML
307
+ # 'a' tag. This is very useful for passing attributes like the 'class' attribute
308
+ # or 'id' attribute. If the 'anchor' attribute is passed to the tag it will
309
+ # append a pound sign ('#') followed by the value of the attribute to the 'href'
310
+ # attribute of the HTML 'a' tag--effectively making an HTML anchor.
311
+ #
312
+ define_tag 'link' do |tag|
313
+ options = tag.attr.dup
314
+ anchor = options['anchor'] ? "##{options.delete('anchor')}" : ''
315
+ attributes = options.inject('') { |s, (k, v)| s << %{#{k.downcase}="#{v}" } }.strip
316
+ attributes = " #{attributes}" unless attributes.empty?
317
+ text = tag.double? ? tag.expand : tag.render('title')
318
+ %{<a href="#{tag.render('url')}#{anchor}"#{attributes}>#{text}</a>}
319
+ end
320
+
321
+ #
322
+ # <r:breadcrumbs [separator="separator_string"] />
323
+ #
324
+ # Renders a trail of breadcrumbs to the current page. The separator attribute
325
+ # specifies the HTML fragment that is inserted between each of the breadcrumbs. By
326
+ # default it is set to ' &gt; '.
327
+ #
328
+ define_tag 'breadcrumbs' do |tag|
329
+ page = tag.locals.page
330
+ breadcrumbs = [page.breadcrumb]
331
+ page.ancestors.each do |ancestor|
332
+ breadcrumbs.unshift %{<a href="#{ancestor.url}">#{ancestor.breadcrumb}</a>}
333
+ end
334
+ separator = tag.attr['separator'] || ' &gt; '
335
+ breadcrumbs.join(separator)
336
+ end
337
+
338
+ #
339
+ # <r:snippet name="snippet_name" />
340
+ #
341
+ # Renders the snippet specified in the 'name' attribute within the context of a page.
342
+ #
343
+ define_tag 'snippet' do |tag|
344
+ if name = tag.attr['name']
345
+ if snippet = Snippet.find_by_name(name.strip)
346
+ page = tag.locals.page
347
+ page.behavior.render_snippet(snippet)
348
+ else
349
+ raise TagError.new('snippet not found')
350
+ end
351
+ else
352
+ raise TagError.new("`snippet' tag must contain `name' attribute")
353
+ end
354
+ end
355
+
356
+ #
357
+ # <r:find url="value_to_find">...</r:find>
358
+ #
359
+ # Inside this tag all page related tags refer to the tag found at the 'url' attribute.
360
+ #
361
+ define_tag 'find' do |tag|
362
+ if url = tag.attr['url']
363
+ if found = Page.find_by_url(tag.attr['url'])
364
+ tag.locals.page = found
365
+ tag.expand
366
+ end
367
+ else
368
+ raise TagError.new("`find' tag must contain `url' attribute")
369
+ end
370
+ end
371
+
372
+ #
373
+ # <r:random>
374
+ # <r:option>...</r:option>
375
+ # <r:option>...</r:option>
376
+ # ...
377
+ # <r:random>
378
+ #
379
+ # Randomly renders one of the options specified by the 'option' tags.
380
+ #
381
+ define_tag 'random' do |tag|
382
+ tag.locals.random = []
383
+ tag.expand
384
+ options = tag.locals.random
385
+ option = options[rand(options.size)]
386
+ option.call if option
387
+ end
388
+ define_tag 'random:option' do |tag|
389
+ items = tag.locals.random
390
+ items << tag.block
391
+ end
392
+
393
+ #
394
+ # <r:comment>...</r:comment>
395
+ #
396
+ # Nothing inside a set of comment tags is rendered.
397
+ #
398
+ define_tag 'comment' do |tag|
399
+ end
400
+
401
+ #
402
+ # <r:escape_html>...</r:escape_html>
403
+ #
404
+ # Escapes angle brackets, etc...
405
+ #
406
+ define_tag "escape_html" do |tag|
407
+ CGI.escapeHTML(tag.expand)
408
+ end
409
+
410
+ #
411
+ # <r:rfc1123_date />
412
+ #
413
+ # Outputs the date using the format mandated by RFC 1123. (Ideal for RSS feeds.)
414
+ #
415
+ define_tag "rfc1123_date" do |tag|
416
+ page = tag.locals.page
417
+ if date = page.published_at || page.created_at
418
+ CGI.rfc1123_date(date.to_time)
419
+ end
420
+ end
421
+
422
+ #
423
+ # <r:navigation urls="[Title: url; Title: url; ...]">
424
+ # <r:normal><a href="<r:url />"><r:title /></a></r:normal>
425
+ # <r:here><strong><r:title /></strong></r:here>
426
+ # <r:selected><strong><a href="<r:url />"><r:title /></a></strong></r:selected>
427
+ # <r:between> | </r:between>
428
+ # </r:navigation>
429
+ #
430
+ # Renders a list of links specified in the 'urls' attribute according to three
431
+ # states:
432
+ #
433
+ # * 'normal' specifies the normal state for the link
434
+ # * 'here' specifies the state of the link when the url matches the current
435
+ # page's url
436
+ # * 'selected' specifies the state of the link when the current page matches
437
+ # is a child of the specified url
438
+ #
439
+ # The 'between' tag specifies what sould be inserted inbetween each of the links.
440
+ #
441
+ define_tag 'navigation' do |tag|
442
+ hash = tag.locals.navigation = {}
443
+ tag.expand
444
+ raise TagError.new("`navigation' tag must include a `normal' tag") unless hash.has_key? :normal
445
+ result = []
446
+ pairs = tag.attr['urls'].to_s.split(';').collect do |pair|
447
+ parts = pair.split(':')
448
+ value = parts.pop
449
+ key = parts.join(':')
450
+ [key.strip, value.strip]
451
+ end
452
+ pairs.each do |title, url|
453
+ compare_url = remove_trailing_slash(url)
454
+ page_url = remove_trailing_slash(@page.url)
455
+ hash[:title] = title
456
+ hash[:url] = url
457
+ case page_url
458
+ when compare_url
459
+ result << (hash[:here] || hash[:selected] || hash[:normal]).call
460
+ when Regexp.compile( '^' + Regexp.quote(url))
461
+ result << (hash[:selected] || hash[:normal]).call
462
+ else
463
+ result << hash[:normal].call
464
+ end
465
+ end
466
+ between = hash.has_key?(:between) ? hash[:between].call : ' '
467
+ result.join(between)
468
+ end
469
+ [:normal, :here, :selected, :between].each do |symbol|
470
+ define_tag "navigation:#{symbol}" do |tag|
471
+ hash = tag.locals.navigation
472
+ hash[symbol] = tag.block
473
+ end
474
+ end
475
+ [:title, :url].each do |symbol|
476
+ define_tag "navigation:#{symbol}" do |tag|
477
+ hash = tag.locals.navigation
478
+ hash[symbol]
479
+ end
480
+ end
481
+
482
+ end
483
+
484
+ def render_tag(name, attributes = {}, &block)
485
+ super
486
+ rescue Exception => e
487
+ render_error_message(e.message)
488
+ end
489
+
490
+ def tag_missing(name, attributes = {}, &block)
491
+ super
492
+ rescue Radius::UndefinedTagError => e
493
+ raise TagError.new(e.message)
494
+ end
495
+
496
+ private
497
+
498
+ def render_error_message(message)
499
+ "<div><strong>#{message}</strong></div>"
500
+ end
501
+
502
+ def remove_trailing_slash(string)
503
+ (string =~ %r{^(.*?)/$}) ? $1 : string
504
+ end
505
+
506
+ def tag_part_name(tag)
507
+ tag.attr['part'] || 'body'
508
+ end
509
+
510
+ def tag_time_format(tag)
511
+ (tag.attr['format'] || '%A, %B %d, %Y')
512
+ end
513
+
514
+ def postgres?
515
+ ActiveRecord::Base.connection.adapter_name =~ /postgres/i
516
+ end
517
+ def build_regexp_for(tag,attribute_name)
518
+ ignore_case = tag.attr.has_key?('ignore_case') && tag.attr['ignore_case']=='false' ? nil : true
519
+ begin
520
+ regexp = Regexp.new(tag.attr['matches'],ignore_case)
521
+ rescue RegexpError => e
522
+ raise TagError.new("Malformed regular expression in `#{attribute_name}' argument of `#{tag.name}' tag: #{e.message}")
523
+ end
524
+ return regexp
525
+ end
526
+ end